From 7dfc2e4085e8fde416890b4c8a8b67558a899d83 Mon Sep 17 00:00:00 2001 From: PegaSys Admin Date: Tue, 9 Oct 2018 15:17:20 +0000 Subject: [PATCH] Initial commit Signed-off-by: Adrian Sutton --- .dockerignore | 4 + .gitattributes | 5 + .gitignore | 27 + .gitmodules | 4 + CONTRIBUTING.md | 60 + Dockerfile | 13 + Jenkinsfile | 69 + LICENSE | 201 + README.md | 126 + acceptance-tests/build.gradle | 33 + .../CreateAccountAcceptanceTest.java | 32 + .../PantheonClusterAcceptanceTest.java | 29 + .../RpcApisTogglesAcceptanceTest.java | 67 + .../acceptance/dsl/AcceptanceTestBase.java | 18 + .../tests/acceptance/dsl/JsonRpc.java | 19 + .../tests/acceptance/dsl/WaitUtils.java | 12 + .../tests/acceptance/dsl/account/Account.java | 52 + .../acceptance/dsl/account/Accounts.java | 108 + .../tests/acceptance/dsl/node/Cluster.java | 102 + .../tests/acceptance/dsl/node/Eth.java | 25 + .../acceptance/dsl/node/PantheonNode.java | 350 + .../dsl/node/PantheonNodeConfig.java | 111 + .../dsl/node/PantheonNodeRunner.java | 41 + .../dsl/node/ProcessPantheonNodeRunner.java | 118 + .../dsl/node/ThreadPantheonNodeRunner.java | 102 + .../tests/acceptance/dsl/node/Web3.java | 24 + .../dsl/pubsub/JsonRpcSuccessEvent.java | 33 + .../acceptance/dsl/pubsub/Subscription.java | 77 + .../dsl/pubsub/SubscriptionEvent.java | 35 + .../acceptance/dsl/pubsub/WebSocket.java | 46 + .../dsl/pubsub/WebSocketConnection.java | 127 + .../acceptance/dsl/pubsub/WebSocketEvent.java | 22 + .../jsonrpc/Web3Sha3AcceptanceTest.java | 33 + .../mining/MiningAcceptanceTest.java | 44 + .../NewPendingTransactionAcceptanceTest.java | 269 + .../pantheon/tests/web3j/EventEmitter.sol | 29 + .../web3j/EventEmitterAcceptanceTest.java | 61 + .../tests/web3j/generated/EventEmitter.abi | 1 + .../tests/web3j/generated/EventEmitter.bin | 1 + .../tests/web3j/generated/EventEmitter.java | 184 + .../src/test/resources/log4j2.xml | 16 + .../truffle-pet-shop-tutorial/README.md | 78 + .../contracts/Adoption.sol | 37 + .../contracts/Migrations.sol | 23 + .../migrations/1_initial_migration.js | 5 + .../migrations/2_deploy_contracts.js | 5 + .../test/TestAdoption.sol | 52 + .../truffle-pet-shop-tutorial/test/test.js | 32 + .../truffle-pet-shop-tutorial/test/test2.js | 24 + .../truffle-pet-shop-tutorial/truffle.js | 21 + build.gradle | 401 + .../main/groovy/ProjectPropertiesFile.groovy | 124 + consensus/build.gradle | 1 + consensus/clique/build.gradle | 28 + .../BlockHeaderValidationRulesetFactory.java | 49 + .../consensus/clique/CliqueBlockHashing.java | 82 + .../consensus/clique/CliqueContext.java | 26 + .../clique/CliqueDifficultyCalculator.java | 30 + .../consensus/clique/CliqueExtraData.java | 97 + .../consensus/clique/CliqueHelpers.java | 73 + .../clique/CliqueProtocolSchedule.java | 67 + .../consensus/clique/CliqueProtocolSpecs.java | 70 + .../clique/CliqueVoteTallyUpdater.java | 64 + .../consensus/clique/VoteTallyCache.java | 91 + .../blockcreation/CliqueBlockCreator.java | 95 + .../blockcreation/CliqueBlockScheduler.java | 56 + .../blockcreation/CliqueProposerSelector.java | 44 + .../CliqueDifficultyValidationRule.java | 32 + .../CliqueExtraDataValidationRule.java | 92 + .../CoinbaseHeaderValidationRule.java | 25 + .../SignerRateLimitValidationRule.java | 22 + .../jsonrpc/CliqueJsonRpcMethodsFactory.java | 39 + .../jsonrpc/methods/CliqueGetSigners.java | 51 + .../methods/CliqueGetSignersAtHash.java | 48 + .../clique/jsonrpc/methods/Discard.java | 32 + .../clique/jsonrpc/methods/Propose.java | 37 + .../clique/CliqueBlockHashingTest.java | 116 + .../CliqueDifficultyCalculatorTest.java | 69 + .../consensus/clique/CliqueExtraDataTest.java | 79 + .../clique/CliqueProtocolScheduleTest.java | 38 + .../clique/CliqueProtocolSpecsTest.java | 44 + .../clique/CliqueVoteTallyUpdaterTest.java | 171 + .../consensus/clique/TestHelpers.java | 41 + .../consensus/clique/VoteTallyCacheTest.java | 135 + .../blockcreation/CliqueBlockCreatorTest.java | 117 + .../CliqueBlockSchedulerTest.java | 111 + .../CliqueProposerSelectorTest.java | 50 + .../CliqueDifficultyValidationRuleTest.java | 139 + .../CliqueExtraDataValidationRuleTest.java | 136 + .../SignerRateLimitValidationRuleTest.java | 258 + .../methods/CliqueGetSignersAtHashTest.java | 110 + .../jsonrpc/methods/CliqueGetSignersTest.java | 104 + .../clique/jsonrpc/methods/DiscardTest.java | 103 + .../clique/jsonrpc/methods/ProposeTest.java | 113 + consensus/common/build.gradle | 20 + .../consensus/common/EpochManager.java | 18 + .../consensus/common/ValidatorProvider.java | 11 + .../consensus/common/VoteProposer.java | 122 + .../pantheon/consensus/common/VoteTally.java | 116 + .../pantheon/consensus/common/VoteType.java | 27 + .../VoteValidationRule.java | 30 + .../consensus/common/VoteProposerTest.java | 203 + .../consensus/common/VoteTallyTest.java | 294 + .../VoteValidationRuleTest.java | 47 + consensus/ibft/build.gradle | 33 + .../ibft/ConsensusRoundIdentifier.java | 80 + .../consensus/ibft/IbftBlockHashing.java | 155 + ...ftBlockHeaderValidationRulesetFactory.java | 61 + .../consensus/ibft/IbftBlockImporter.java | 53 + .../pantheon/consensus/ibft/IbftContext.java | 24 + .../pantheon/consensus/ibft/IbftEvent.java | 8 + .../consensus/ibft/IbftEventQueue.java | 52 + .../pantheon/consensus/ibft/IbftEvents.java | 14 + .../consensus/ibft/IbftExtraData.java | 99 + .../pantheon/consensus/ibft/IbftHelpers.java | 14 + .../consensus/ibft/IbftProcessor.java | 84 + .../consensus/ibft/IbftProtocolSchedule.java | 36 + .../consensus/ibft/IbftProtocolSpecs.java | 48 + .../consensus/ibft/IbftStateMachine.java | 19 + .../pantheon/consensus/ibft/RoundTimer.java | 65 + .../consensus/ibft/VoteTallyUpdater.java | 73 + .../ibft/blockcreation/IbftBlockCreator.java | 109 + .../IbftExtraDataCalculator.java | 30 + .../ibft/blockcreation/ProposerSelector.java | 150 + .../IbftExtraDataValidationRule.java | 119 + .../consensus/ibft/ibftevent/RoundExpiry.java | 54 + .../jsonrpc/IbftJsonRpcMethodsFactory.java | 34 + .../methods/IbftProposeValidatorVote.java | 40 + .../ibft/protocol/IbftProtocolManager.java | 82 + .../ibft/protocol/IbftSubProtocol.java | 68 + .../ibft/protocol/Istanbul64Protocol.java | 63 + .../protocol/Istanbul64ProtocolManager.java | 46 + .../consensus/ibft/IbftBlockHashingTest.java | 118 + ...ockHeaderValidationRulesetFactoryTest.java | 124 + .../consensus/ibft/IbftBlockImporterTest.java | 100 + .../consensus/ibft/IbftEventQueueTest.java | 72 + .../consensus/ibft/IbftExtraDataTest.java | 135 + .../consensus/ibft/IbftProcessorTest.java | 153 + .../ibft/IbftProtocolContextFixture.java | 22 + .../consensus/ibft/RoundTimerTest.java | 123 + .../consensus/ibft/VoteTallyUpdaterTest.java | 185 + .../blockcreation/IbftBlockCreatorTest.java | 104 + .../blockcreation/ProposerSelectorTest.java | 261 + .../IbftExtraDataValidationRuleTest.java | 320 + .../methods/IbftProposeValidatorVoteTest.java | 108 + .../ibft/protocol/IbftSubProtocolTest.java | 32 + crypto/build.gradle | 24 + .../BouncyCastleMessageDigestFactory.java | 16 + .../net/consensys/pantheon/crypto/Hash.java | 90 + ...validSEC256K1PrivateKeyStoreException.java | 3 + .../pantheon/crypto/PRNGSecureRandom.java | 81 + .../crypto/PersonalisationString.java | 53 + .../pantheon/crypto/QuickEntropy.java | 14 + .../consensys/pantheon/crypto/SECP256K1.java | 676 + .../pantheon/crypto/SecureRandomProvider.java | 16 + .../crypto/altbn128/AbstractFieldPoint.java | 127 + .../pantheon/crypto/altbn128/AbstractFqp.java | 263 + .../crypto/altbn128/AltBn128Fq12Pairer.java | 92 + .../crypto/altbn128/AltBn128Fq12Point.java | 63 + .../crypto/altbn128/AltBn128Fq2Point.java | 64 + .../crypto/altbn128/AltBn128Point.java | 50 + .../crypto/altbn128/FieldElement.java | 35 + .../pantheon/crypto/altbn128/FieldPoint.java | 23 + .../pantheon/crypto/altbn128/Fq.java | 159 + .../pantheon/crypto/altbn128/Fq12.java | 71 + .../pantheon/crypto/altbn128/Fq2.java | 45 + .../consensys/pantheon/crypto/HashTest.java | 26 + .../pantheon/crypto/PRNGSecureRandomTest.java | 52 + .../pantheon/crypto/SECP256K1Test.java | 273 + .../altbn128/AltBn128Fq12PairerTest.java | 81 + .../altbn128/AltBn128Fq12PointTest.java | 50 + .../crypto/altbn128/AltBn128Fq2PointTest.java | 58 + .../crypto/altbn128/AltBn128PointTest.java | 409 + .../pantheon/crypto/altbn128/Fq12Test.java | 47 + .../pantheon/crypto/altbn128/Fq2Test.java | 57 + .../pantheon/crypto/altbn128/FqTest.java | 117 + crypto/src/test/resources/log4j2.xml | 16 + .../pantheon/crypto/validPrivateKey.txt | 1 + errorprone-checks/README.md | 9 + errorprone-checks/build.gradle | 37 + .../DoNotCreateSecureRandomDirectly.java | 47 + .../DoNotInvokeMessageDigestDirectly.java | 32 + .../DoNotReturnNullOptionals.java | 57 + .../MethodInputParametersMustBeFinal.java | 97 + .../DoNotCreateSecureRandomDirectlyTest.java | 26 + .../DoNotInvokeMessageDigestDirectlyTest.java | 26 + .../DoNotReturnNullOptionalsTest.java | 26 + .../MethodInputParametersMustBeFinalTest.java | 40 + ...eateSecureRandomDirectlyNegativeCases.java | 19 + ...eateSecureRandomDirectlyPositiveCases.java | 26 + ...okeMessageDigestDirectlyNegativeCases.java | 11 + ...okeMessageDigestDirectlyPositiveCases.java | 12 + ...DoNotReturnNullOptionalsNegativeCases.java | 22 + ...DoNotReturnNullOptionalsPositiveCases.java | 20 + ...tersMustBeFinalInterfaceNegativeCases.java | 26 + ...tersMustBeFinalInterfacePositiveCases.java | 10 + ...putParametersMustBeFinalNegativeCases.java | 16 + ...putParametersMustBeFinalPositiveCases.java | 27 + ethereum/build.gradle | 1 + ethereum/core/build.gradle | 151 + .../vm/EntriesFromIntegrationTest.java | 58 + .../vm/TraceTransactionIntegrationTest.java | 248 + .../pantheon/ethereum/ProtocolContext.java | 38 + .../blockcreation/AbstractBlockCreator.java | 269 + .../blockcreation/AsyncBlockCreator.java | 6 + .../blockcreation/BaseBlockScheduler.java | 44 + .../ethereum/blockcreation/BlockCreator.java | 8 + .../ethereum/blockcreation/BlockMiner.java | 102 + .../BlockTransactionSelector.java | 194 + .../CoinbaseNotSetException.java | 8 + .../blockcreation/DefaultBlockScheduler.java | 40 + .../blockcreation/EthHashBlockMiner.java | 47 + .../blockcreation/EthHashMinerExecutor.java | 102 + .../IncrementingNonceGenerator.java | 27 + .../blockcreation/MiningCoordinator.java | 146 + .../blockcreation/MiningParameters.java | 42 + .../blockcreation/RandomNonceGenerator.java | 31 + .../ethereum/chain/BlockAddedEvent.java | 66 + .../ethereum/chain/BlockAddedObserver.java | 6 + .../pantheon/ethereum/chain/Blockchain.java | 163 + .../pantheon/ethereum/chain/ChainHead.java | 25 + .../ethereum/chain/GenesisConfig.java | 321 + .../ethereum/chain/MutableBlockchain.java | 21 + .../ethereum/chain/TransactionLocation.java | 65 + .../ethereum/core/AbstractWorldUpdater.java | 378 + .../pantheon/ethereum/core/Account.java | 116 + .../core/AccountTransactionOrder.java | 47 + .../pantheon/ethereum/core/Address.java | 118 + .../pantheon/ethereum/core/Block.java | 86 + .../pantheon/ethereum/core/BlockBody.java | 86 + .../ethereum/core/BlockHashFunction.java | 19 + .../pantheon/ethereum/core/BlockHeader.java | 180 + .../ethereum/core/BlockHeaderBuilder.java | 252 + .../pantheon/ethereum/core/BlockImporter.java | 45 + .../pantheon/ethereum/core/BlockMetadata.java | 51 + .../consensys/pantheon/ethereum/core/Gas.java | 140 + .../pantheon/ethereum/core/Hash.java | 53 + .../consensys/pantheon/ethereum/core/Log.java | 95 + .../pantheon/ethereum/core/LogSeries.java | 84 + .../pantheon/ethereum/core/LogTopic.java | 54 + .../ethereum/core/LogsBloomFilter.java | 140 + .../ethereum/core/MutableAccount.java | 89 + .../ethereum/core/MutableWorldState.java | 14 + .../ethereum/core/MutableWorldView.java | 12 + .../core/PendingTransactionListener.java | 7 + .../ethereum/core/PendingTransactions.java | 243 + .../ethereum/core/ProcessableBlockHeader.java | 89 + .../ethereum/core/SealableBlockHeader.java | 109 + .../pantheon/ethereum/core/SyncStatus.java | 26 + .../pantheon/ethereum/core/Synchronizer.java | 15 + .../pantheon/ethereum/core/Transaction.java | 493 + .../ethereum/core/TransactionBuilder.java | 106 + .../ethereum/core/TransactionPool.java | 154 + .../ethereum/core/TransactionReceipt.java | 203 + .../pantheon/ethereum/core/Util.java | 25 + .../consensys/pantheon/ethereum/core/Wei.java | 64 + .../pantheon/ethereum/core/WorldState.java | 29 + .../pantheon/ethereum/core/WorldUpdater.java | 91 + .../pantheon/ethereum/core/WorldView.java | 15 + .../ethereum/db/BlockchainStorage.java | 60 + .../ethereum/db/DefaultMutableBlockchain.java | 376 + ...ueStoragePrefixedKeyBlockchainStorage.java | 194 + .../ethereum/db/WorldStateArchive.java | 33 + .../pantheon/ethereum/debug/TraceFrame.java | 97 + .../pantheon/ethereum/debug/TraceOptions.java | 29 + .../DevelopmentDifficultyCalculators.java | 20 + .../DevelopmentProtocolSchedule.java | 22 + .../development/DevelopmentProtocolSpecs.java | 24 + .../mainnet/AbstractMessageProcessor.java | 182 + .../mainnet/AbstractPrecompiledContract.java | 33 + .../AttachedBlockHeaderValidationRule.java | 17 + .../ethereum/mainnet/BlockBodyValidator.java | 39 + .../mainnet/BlockHeaderValidator.java | 128 + .../ethereum/mainnet/BlockProcessor.java | 69 + .../ethereum/mainnet/BodyValidation.java | 91 + .../mainnet/ConstantinopleGasCalculator.java | 17 + .../DetachedBlockHeaderValidationRule.java | 19 + .../mainnet/DifficultyCalculator.java | 21 + .../pantheon/ethereum/mainnet/EthHash.java | 329 + .../ethereum/mainnet/EthHashBlockCreator.java | 95 + .../ethereum/mainnet/EthHashCacheFactory.java | 49 + .../ethereum/mainnet/EthHashSolution.java | 27 + .../ethereum/mainnet/EthHashSolver.java | 147 + .../ethereum/mainnet/EthHashSolverInputs.java | 27 + .../pantheon/ethereum/mainnet/EthHasher.java | 167 + .../mainnet/FrontierGasCalculator.java | 443 + .../mainnet/HeaderValidationMode.java | 18 + .../mainnet/HomesteadGasCalculator.java | 13 + .../mainnet/MainnetBlockBodyValidator.java | 221 + .../mainnet/MainnetBlockHashFunction.java | 15 + .../mainnet/MainnetBlockHeaderValidator.java | 48 + .../mainnet/MainnetBlockImporter.java | 97 + .../mainnet/MainnetBlockProcessor.java | 170 + .../MainnetContractCreationProcessor.java | 138 + .../mainnet/MainnetDifficultyCalculators.java | 110 + .../mainnet/MainnetEvmRegistries.java | 262 + .../mainnet/MainnetMessageCallProcessor.java | 123 + .../MainnetPrecompiledContractRegistries.java | 43 + .../mainnet/MainnetProtocolSchedule.java | 105 + .../mainnet/MainnetProtocolSpecs.java | 320 + .../mainnet/MainnetTransactionProcessor.java | 310 + .../mainnet/MainnetTransactionValidator.java | 151 + .../mainnet/MiningBeneficiaryCalculator.java | 9 + .../mainnet/MutableProtocolSchedule.java | 40 + .../mainnet/PrecompileContractRegistry.java | 24 + .../ethereum/mainnet/PrecompiledContract.java | 37 + .../ethereum/mainnet/ProtocolSchedule.java | 6 + .../ethereum/mainnet/ProtocolSpec.java | 196 + .../ethereum/mainnet/ProtocolSpecBuilder.java | 254 + .../ScheduleBasedBlockHashFunction.java | 30 + .../mainnet/ScheduledProtocolSpec.java | 20 + .../mainnet/SpuriousDragonGasCalculator.java | 59 + .../TangerineWhistleGasCalculator.java | 110 + .../mainnet/TransactionProcessor.java | 125 + .../mainnet/TransactionReceiptType.java | 6 + .../mainnet/TransactionValidator.java | 48 + .../ethereum/mainnet/ValidationResult.java | 79 + .../AncestryValidationRule.java | 37 + .../CalculatedDifficultyValidationRule.java | 39 + .../ConstantFieldValidationRule.java | 38 + .../ExtraDataMaxLengthValidationRule.java | 39 + .../GasLimitRangeAndDeltaValidationRule.java | 56 + .../GasUsageValidationRule.java | 29 + .../ProofOfWorkValidationRule.java | 111 + .../TimestampValidationRule.java | 68 + .../AltBN128AddPrecompiledContract.java | 55 + .../AltBN128MulPrecompiledContract.java | 58 + .../AltBN128PairingPrecompiledContract.java | 95 + ...ularExponentiationPrecompiledContract.java | 155 + .../precompiles/ECRECPrecompiledContract.java | 74 + .../precompiles/IDPrecompiledContract.java | 23 + .../RIPEMD160PrecompiledContract.java | 25 + .../SHA256PrecompiledContract.java | 24 + .../ethereum/util/BlockchainUtil.java | 41 + .../pantheon/ethereum/util/ByteArrayUtil.java | 44 + .../ethereum/util/RawBlockIterator.java | 93 + .../ethereum/vm/AbstractCallOperation.java | 205 + .../ethereum/vm/AbstractOperation.java | 71 + .../consensys/pantheon/ethereum/vm/Code.java | 94 + .../ethereum/vm/DebugOperationTracer.java | 98 + .../consensys/pantheon/ethereum/vm/EVM.java | 133 + .../ethereum/vm/ExceptionalHaltReason.java | 12 + .../pantheon/ethereum/vm/GasCalculator.java | 360 + .../pantheon/ethereum/vm/Memory.java | 529 + .../pantheon/ethereum/vm/MessageFrame.java | 937 + .../pantheon/ethereum/vm/OperandStack.java | 80 + .../pantheon/ethereum/vm/Operation.java | 57 + .../ethereum/vm/OperationRegistry.java | 35 + .../pantheon/ethereum/vm/OperationTracer.java | 21 + .../ethereum/vm/PreAllocatedOperandStack.java | 98 + .../consensys/pantheon/ethereum/vm/Words.java | 48 + .../vm/ehalt/ExceptionalHaltException.java | 23 + .../vm/ehalt/ExceptionalHaltManager.java | 40 + .../vm/ehalt/ExceptionalHaltPredicate.java | 14 + ...sufficientGasExceptionalHaltPredicate.java | 34 + ...alidOperationExceptionalHaltPredicate.java | 21 + ...StackOverflowExceptionalHaltPredicate.java | 21 + ...tackUnderflowExceptionalHaltPredicate.java | 23 + .../operations/AbstractCreateOperation.java | 137 + .../vm/operations/AddModOperation.java | 30 + .../ethereum/vm/operations/AddOperation.java | 29 + .../vm/operations/AddressOperation.java | 26 + .../ethereum/vm/operations/AndOperation.java | 29 + .../vm/operations/BalanceOperation.java | 29 + .../vm/operations/BlockHashOperation.java | 53 + .../ethereum/vm/operations/ByteOperation.java | 49 + .../vm/operations/CallCodeOperation.java | 99 + .../vm/operations/CallDataCopyOperation.java | 34 + .../vm/operations/CallDataLoadOperation.java | 43 + .../vm/operations/CallDataSizeOperation.java | 26 + .../ethereum/vm/operations/CallOperation.java | 114 + .../vm/operations/CallValueOperation.java | 25 + .../vm/operations/CallerOperation.java | 26 + .../vm/operations/CodeCopyOperation.java | 34 + .../vm/operations/CodeSizeOperation.java | 26 + .../vm/operations/CoinbaseOperation.java | 26 + .../vm/operations/Create2Operation.java | 35 + .../vm/operations/CreateOperation.java | 26 + .../vm/operations/DelegateCallOperation.java | 99 + .../vm/operations/DifficultyOperation.java | 25 + .../ethereum/vm/operations/DivOperation.java | 29 + .../ethereum/vm/operations/DupOperation.java | 26 + .../ethereum/vm/operations/EqOperation.java | 30 + .../ethereum/vm/operations/ExpOperation.java | 33 + .../vm/operations/ExtCodeCopyOperation.java | 39 + .../vm/operations/ExtCodeSizeOperation.java | 30 + .../vm/operations/GasLimitOperation.java | 27 + .../ethereum/vm/operations/GasOperation.java | 27 + .../vm/operations/GasPriceOperation.java | 25 + .../ethereum/vm/operations/GtOperation.java | 30 + .../vm/operations/InvalidOperation.java | 25 + .../vm/operations/IsZeroOperation.java | 27 + .../vm/operations/JumpDestOperation.java | 23 + .../ethereum/vm/operations/JumpOperation.java | 44 + .../vm/operations/JumpiOperation.java | 55 + .../ethereum/vm/operations/LogOperation.java | 63 + .../ethereum/vm/operations/LtOperation.java | 30 + .../vm/operations/MLoadOperation.java | 31 + .../vm/operations/MSizeOperation.java | 24 + .../vm/operations/MStore8Operation.java | 30 + .../vm/operations/MStoreOperation.java | 30 + .../ethereum/vm/operations/ModOperation.java | 29 + .../vm/operations/MulModOperation.java | 30 + .../ethereum/vm/operations/MulOperation.java | 29 + .../ethereum/vm/operations/NotOperation.java | 28 + .../vm/operations/NumberOperation.java | 25 + .../ethereum/vm/operations/OrOperation.java | 29 + .../vm/operations/OriginOperation.java | 26 + .../ethereum/vm/operations/PCOperation.java | 24 + .../ethereum/vm/operations/PopOperation.java | 23 + .../ethereum/vm/operations/PushOperation.java | 36 + .../operations/ReturnDataCopyOperation.java | 59 + .../operations/ReturnDataSizeOperation.java | 26 + .../vm/operations/ReturnOperation.java | 31 + .../vm/operations/RevertOperation.java | 31 + .../ethereum/vm/operations/SDivOperation.java | 29 + .../ethereum/vm/operations/SGtOperation.java | 30 + .../vm/operations/SLoadOperation.java | 30 + .../ethereum/vm/operations/SLtOperation.java | 30 + .../ethereum/vm/operations/SModOperation.java | 29 + .../vm/operations/SStoreOperation.java | 64 + .../ethereum/vm/operations/SarOperation.java | 51 + .../vm/operations/SelfDestructOperation.java | 59 + .../ethereum/vm/operations/Sha3Operation.java | 33 + .../ethereum/vm/operations/ShlOperation.java | 35 + .../ethereum/vm/operations/ShrOperation.java | 35 + .../vm/operations/SignExtendOperation.java | 30 + .../vm/operations/StaticCallOperation.java | 99 + .../ethereum/vm/operations/StopOperation.java | 25 + .../ethereum/vm/operations/SubOperation.java | 29 + .../ethereum/vm/operations/SwapOperation.java | 29 + .../vm/operations/TimestampOperation.java | 25 + .../ethereum/vm/operations/XorOperation.java | 29 + .../DebuggableMutableWorldState.java | 160 + .../worldstate/DefaultMutableWorldState.java | 377 + .../KeyValueStorageWorldStateStorage.java | 71 + .../worldstate/WorldStateStorage.java | 31 + .../core/src/main/resources/daoAddresses.json | 118 + ethereum/core/src/main/resources/dev.json | 34 + ethereum/core/src/main/resources/infura.json | 35 + ethereum/core/src/main/resources/mainnet.json | 26707 ++++++++++++++++ ethereum/core/src/main/resources/rinkeby.json | 798 + ethereum/core/src/main/resources/ropsten.json | 875 + .../ethereum/core/AddressHelpers.java | 29 + .../ethereum/core/BlockHeaderTestFixture.java | 125 + .../ethereum/core/BlockSyncTestUtils.java | 41 + .../core/ExecutionContextTestFixture.java | 58 + .../ethereum/core/HeaderDecodingHelpers.java | 97 + .../ethereum/core/InMemoryWorldState.java | 13 + .../core/MiningParametersTestBuilder.java | 36 + .../ethereum/core/TransactionTestFixture.java | 80 + .../BlockTransactionSelectorTest.java | 550 + .../DefaultBlockSchedulerTest.java | 87 + .../blockcreation/EthHashBlockMinerTest.java | 120 + .../EthHashMinerExecutorTest.java | 52 + .../IncrementingNonceGeneratorTest.java | 26 + .../blockcreation/MiningCoordinatorTest.java | 57 + .../ethereum/chain/GenesisConfigTest.java | 91 + .../core/AccountTransactionOrderTest.java | 58 + .../ethereum/core/BlockHeaderMock.java | 47 + .../pantheon/ethereum/core/LogTest.java | 19 + .../ethereum/core/LogsBloomFilterTest.java | 33 + .../core/LogsBloomFilterTestCaseSpec.java | 47 + .../core/PendingTransactionsTest.java | 337 + .../core/TransactionIntegrationTest.java | 43 + .../ethereum/core/TransactionPoolTest.java | 415 + .../ethereum/core/TransactionReceiptTest.java | 20 + .../ethereum/core/TransactionTest.java | 100 + .../core/TransactionTestCaseSpec.java | 92 + .../db/DefaultMutableBlockchainTest.java | 715 + .../DevelopmentProtocolScheduleTest.java | 45 + .../mainnet/BlockHeaderValidatorTest.java | 257 + .../ethereum/mainnet/BodyValidationTest.java | 35 + .../mainnet/EthHashBlockCreatorTest.java | 56 + .../ethereum/mainnet/EthHashSolverTest.java | 116 + .../ethereum/mainnet/EthHashTest.java | 112 + .../ethereum/mainnet/EthHasherTest.java | 40 + .../MainnetBlockHeaderValidatorTest.java | 67 + .../mainnet/MainnetBlockProcessorTest.java | 42 + .../mainnet/MainnetProtocolScheduleTest.java | 85 + .../MainnetTransactionValidatorTest.java | 157 + .../mainnet/ProtocolScheduleTest.java | 48 + .../mainnet/ValidationResultTest.java | 62 + .../ethereum/mainnet/ValidationTestUtils.java | 59 + .../AncestryValidationRuleTest.java | 54 + .../ConstantFieldValidationRuleTest.java | 47 + .../ExtraDataMaxLengthValidationRuleTest.java | 36 + ...sLimitRangeAndDeltaValidationRuleTest.java | 62 + .../GasUsageValidationRuleTest.java | 50 + .../TimestampValidationRuleTest.java | 107 + .../ethereum/testutil/BlockDataGenerator.java | 366 + .../ethereum/util/BlockchainUtilTest.java | 119 + .../consensys/pantheon/ethereum/vm/.gitignore | 2 + .../ethereum/vm/AbstractRetryingTest.java | 67 + .../pantheon/ethereum/vm/AddressMock.java | 19 + .../pantheon/ethereum/vm/AddressTest.java | 42 + .../vm/BlockchainReferenceTestCaseSpec.java | 221 + .../vm/BlockchainReferenceTestTools.java | 100 + .../pantheon/ethereum/vm/CodeMock.java | 19 + .../ethereum/vm/DebugOperationTracerTest.java | 227 + .../ethereum/vm/EnvironmentInformation.java | 175 + .../vm/GeneralStateReferenceTestTools.java | 124 + .../vm/GeneralStateTestCaseEipSpec.java | 72 + .../ethereum/vm/GeneralStateTestCaseSpec.java | 114 + .../pantheon/ethereum/vm/LogMock.java | 35 + .../pantheon/ethereum/vm/MemoryTest.java | 117 + .../vm/PreAllocatedOperandStackTest.java | 93 + .../vm/ReferenceTestProtocolSchedules.java | 86 + .../vm/StateTestVersionedTransaction.java | 88 + .../pantheon/ethereum/vm/TestBlockchain.java | 98 + .../pantheon/ethereum/vm/VMReferenceTest.java | 164 + .../ethereum/vm/VMReferenceTestCaseSpec.java | 106 + .../pantheon/ethereum/vm/WorldStateMock.java | 89 + .../pantheon/ethereum/vm/blockchain/.keep | 0 .../pantheon/ethereum/vm/generalstate/.keep | 0 .../vm/operations/Create2OperationTest.java | 126 + .../vm/operations/SarOperationTest.java | 153 + .../vm/operations/ShlOperationTest.java | 103 + .../vm/operations/ShrOperationTest.java | 122 + .../DefaultMutableWorldStateTest.java | 371 + ethereum/core/src/test/resources/log4j2.xml | 16 + .../blockvalidation/block_1200000.blocks | Bin 0 -> 541 bytes .../blockvalidation/block_1200001.blocks | Bin 0 -> 541 bytes .../blockvalidation/block_300005.blocks | Bin 0 -> 655 bytes .../blockvalidation/block_300006.blocks | Bin 0 -> 655 bytes .../blockvalidation/block_4400000.blocks | Bin 0 -> 8419 bytes .../blockvalidation/block_4400001.blocks | Bin 0 -> 21790 bytes .../blockvalidation/block_4400002.blocks | Bin 0 -> 6918 bytes .../ethereum/chain/genesis-olympic.json | 48 + .../pantheon/ethereum/chain/genesis1.json | 24 + .../pantheon/ethereum/chain/genesis2.json | 17 + .../pantheon/ethereum/mainnet/block_1.blocks | Bin 0 -> 537 bytes .../ethereum/mainnet/block_1200000.blocks | Bin 0 -> 541 bytes .../ethereum/mainnet/block_1200001.blocks | Bin 0 -> 541 bytes .../ethereum/mainnet/block_300005.blocks | Bin 0 -> 655 bytes .../ethereum/mainnet/block_300006.blocks | Bin 0 -> 655 bytes .../ethereum/mainnet/block_4400000.blocks | Bin 0 -> 8419 bytes .../ethereum/mainnet/block_4400001.blocks | Bin 0 -> 21790 bytes .../ethereum/mainnet/block_4400002.blocks | Bin 0 -> 6918 bytes .../vm/BlockchainReferenceTest.java.template | 38 + .../GeneralStateReferenceTest.java.template | 38 + ...dTouchedTransactionSucceedsPostEIP158.json | 110 + ...edWhenEmptyAndTouchedTransactionFails.json | 110 + ...henNonEmptyAndTouchedTransactionFails.json | 110 + ...NonEmptyAndTouchedTransactionSucceeds.json | 110 + ethereum/eth/build.gradle | 30 + .../pantheon/ethereum/eth/EthProtocol.java | 74 + .../ethereum/eth/manager/AbstractEthTask.java | 114 + .../eth/manager/AbstractPeerRequestTask.java | 82 + .../eth/manager/AbstractPeerTask.java | 68 + .../eth/manager/AbstractRetryingPeerTask.java | 77 + .../ethereum/eth/manager/ChainState.java | 106 + .../ethereum/eth/manager/EthContext.java | 36 + .../ethereum/eth/manager/EthMessage.java | 22 + .../ethereum/eth/manager/EthMessages.java | 40 + .../ethereum/eth/manager/EthPeer.java | 292 + .../ethereum/eth/manager/EthPeers.java | 117 + .../eth/manager/EthProtocolManager.java | 267 + .../ethereum/eth/manager/EthScheduler.java | 215 + .../ethereum/eth/manager/EthServer.java | 217 + .../ethereum/eth/manager/EthTask.java | 10 + .../ethereum/eth/manager/PeerReputation.java | 67 + .../ethereum/eth/manager/RequestManager.java | 166 + .../manager/exceptions/EthTaskException.java | 22 + .../IncompleteResultsException.java | 8 + .../exceptions/NoAvailablePeersException.java | 8 + .../PeerBreachedProtocolException.java | 8 + .../exceptions/PeerDisconnectedException.java | 8 + .../eth/messages/BlockBodiesMessage.java | 60 + .../eth/messages/BlockHeadersMessage.java | 65 + .../ethereum/eth/messages/EthPV62.java | 24 + .../ethereum/eth/messages/EthPV63.java | 19 + .../eth/messages/GetBlockBodiesMessage.java | 65 + .../eth/messages/GetBlockHeadersMessage.java | 151 + .../eth/messages/GetNodeDataMessage.java | 65 + .../eth/messages/GetReceiptsMessage.java | 65 + .../eth/messages/NewBlockHashesMessage.java | 121 + .../eth/messages/NewBlockMessage.java | 109 + .../eth/messages/NodeDataMessage.java | 64 + .../eth/messages/ReceiptsMessage.java | 76 + .../ethereum/eth/messages/StatusMessage.java | 142 + .../eth/messages/TransactionsMessage.java | 63 + .../eth/sync/BlockPropagationManager.java | 263 + .../ethereum/eth/sync/ChainHeadTracker.java | 70 + .../eth/sync/DefaultSynchronizer.java | 64 + .../ethereum/eth/sync/Downloader.java | 396 + .../pantheon/ethereum/eth/sync/SyncMode.java | 17 + .../eth/sync/SynchronizerConfiguration.java | 308 + .../eth/sync/TrailingPeerLimiter.java | 71 + .../eth/sync/state/FastSyncState.java | 27 + .../eth/sync/state/PendingBlocks.java | 89 + .../ethereum/eth/sync/state/SyncState.java | 76 + .../ethereum/eth/sync/state/SyncTarget.java | 42 + .../tasks/AbstractGetHeadersFromPeerTask.java | 106 + .../eth/sync/tasks/CompleteBlocksTask.java | 126 + .../tasks/DetermineCommonAncestorTask.java | 139 + .../tasks/DownloadHeaderSequenceTask.java | 184 + .../eth/sync/tasks/GetBlockFromPeerTask.java | 83 + .../eth/sync/tasks/GetBodiesFromPeerTask.java | 161 + .../tasks/GetHeadersFromPeerByHashTask.java | 83 + .../tasks/GetHeadersFromPeerByNumberTask.java | 78 + .../eth/sync/tasks/ImportBlocksTask.java | 117 + .../eth/sync/tasks/PersistBlockTask.java | 163 + .../PipelinedImportChainSegmentTask.java | 303 + .../eth/sync/tasks/WaitForPeerTask.java | 50 + .../eth/sync/tasks/WaitForPeersTask.java | 60 + .../exceptions/InvalidBlockException.java | 10 + .../transactions/PeerTransactionTracker.java | 72 + .../transactions/TransactionPoolFactory.java | 40 + .../eth/transactions/TransactionSender.java | 35 + .../TransactionsMessageHandler.java | 28 + .../TransactionsMessageProcessor.java | 48 + .../TransactionsMessageSender.java | 38 + .../eth/manager/AbstractEthTaskTest.java | 92 + .../ethereum/eth/manager/ChainStateTest.java | 219 + .../manager/DeterministicEthScheduler.java | 44 + .../ethereum/eth/manager/EthPeerTest.java | 266 + .../ethereum/eth/manager/EthPeersTest.java | 64 + .../eth/manager/EthProtocolManagerTest.java | 752 + .../manager/EthProtocolManagerTestUtil.java | 75 + .../eth/manager/EthSchedulerTest.java | 195 + .../ethereum/eth/manager/MockEthTask.java | 23 + .../eth/manager/MockExecutorService.java | 120 + .../eth/manager/MockPeerConnection.java | 89 + .../eth/manager/MockScheduledExecutor.java | 86 + .../eth/manager/PeerReputationTest.java | 68 + .../eth/manager/RequestManagerTest.java | 212 + .../eth/manager/RespondingEthPeer.java | 310 + .../ethtaskutils/AbstractMessageTaskTest.java | 126 + .../ethtaskutils/BlockchainSetupUtil.java | 194 + .../ethtaskutils/PeerMessageTaskTest.java | 141 + .../ethtaskutils/RetryingMessageTaskTest.java | 143 + .../RetryingMessageTaskWithResultsTest.java | 14 + .../eth/messages/BlockBodiesMessageTest.java | 68 + .../eth/messages/BlockHeadersMessageTest.java | 62 + .../messages/GetBlockBodiesMessageTest.java | 60 + .../messages/GetBlockHeadersMessageTest.java | 67 + .../eth/messages/GetNodeDataMessageTest.java | 51 + .../eth/messages/GetReceiptsMessageTest.java | 52 + .../messages/NewBlockHashesMessageTest.java | 61 + .../eth/messages/NewBlockMessageTest.java | 68 + .../eth/messages/NodeDataMessageTest.java | 51 + .../eth/messages/ReceiptsMessageTest.java | 56 + .../eth/messages/StatusMessageTest.java | 64 + .../eth/messages/TransactionsMessageTest.java | 52 + .../eth/sync/BlockPropagationManagerTest.java | 511 + .../eth/sync/ChainHeadTrackerTest.java | 83 + .../ethereum/eth/sync/DownloaderTest.java | 627 + .../eth/sync/TrailingPeerLimiterTest.java | 145 + .../eth/sync/state/PendingBlocksTest.java | 113 + .../sync/tasks/CompleteBlocksTaskTest.java | 33 + ...neCommonAncestorTaskParameterizedTest.java | 169 + .../DetermineCommonAncestorTaskTest.java | 386 + .../tasks/DownloadHeaderSequenceTaskTest.java | 31 + .../sync/tasks/GetBlockFromPeerTaskTest.java | 109 + .../sync/tasks/GetBodiesFromPeerTaskTest.java | 45 + .../GetHeadersFromPeerByHashTaskTest.java | 114 + .../GetHeadersFromPeerByNumberTaskTest.java | 104 + .../eth/sync/tasks/ImportBlocksTaskTest.java | 174 + .../eth/sync/tasks/PersistBlockTaskTest.java | 307 + .../PipelinedImportChainSegmentTaskTest.java | 433 + .../eth/sync/tasks/WaitForPeerTaskTest.java | 70 + .../eth/sync/tasks/WaitForPeersTaskTest.java | 88 + .../PeerTransactionTrackerTest.java | 79 + .../ethereum/eth/transactions/TestNode.java | 182 + .../eth/transactions/TestNodeList.java | 267 + .../TransactionPoolPropagationTest.java | 121 + .../TransactionsMessageProcessorTest.java | 47 + .../TransactionsMessageSenderTest.java | 93 + ethereum/eth/src/test/resources/50.blocks | Bin 0 -> 30779 bytes .../src/test/resources/testBlockchain.blocks | Bin 0 -> 23287 bytes .../eth/src/test/resources/testGenesis.json | 20 + ethereum/jsonrpc/build.gradle | 40 + .../ethereum/jsonrpc/BlockchainImporter.java | 61 + .../ethereum/jsonrpc/JsonRpcResponseKey.java | 21 + .../jsonrpc/JsonRpcResponseUtils.java | 205 + .../jsonrpc/JsonRpcTestMethodsFactory.java | 86 + .../methods/EthCallIntegrationTest.java | 168 + .../EthEstimateGasIntegrationTest.java | 134 + .../EthGetBlockByHashIntegrationTest.java | 200 + .../EthGetBlockByNumberIntegrationTest.java | 395 + .../EthGetFilterChangesIntegrationTest.java | 266 + ...cleByBlockHashAndIndexIntegrationTest.java | 111 + ...eByBlockNumberAndIndexIntegrationTest.java | 108 + .../jsonrpc/jsonRpcTestBlockchain.blocks | Bin 0 -> 23287 bytes .../ethereum/jsonrpc/jsonRpcTestGenesis.json | 20 + .../jsonrpc/JsonRpcConfiguration.java | 119 + .../jsonrpc/JsonRpcErrorConverter.java | 28 + .../ethereum/jsonrpc/JsonRpcHttpService.java | 341 + .../jsonrpc/JsonRpcMethodsFactory.java | 214 + .../jsonrpc/JsonRpcServiceException.java | 8 + .../jsonrpc/internal/JsonRpcRequest.java | 95 + .../jsonrpc/internal/JsonRpcRequestId.java | 71 + .../exception/InvalidJsonRpcParameters.java | 12 + .../InvalidJsonRpcRequestException.java | 11 + .../internal/filter/FilterIdGenerator.java | 10 + .../internal/filter/FilterManager.java | 296 + .../jsonrpc/internal/filter/LogsQuery.java | 101 + .../methods/AbstractBlockParameterMethod.java | 66 + .../jsonrpc/internal/methods/AdminPeers.java | 41 + .../internal/methods/DebugStorageRangeAt.java | 77 + .../methods/DebugTraceTransaction.java | 60 + .../jsonrpc/internal/methods/EthAccounts.java | 19 + .../internal/methods/EthBlockNumber.java | 26 + .../jsonrpc/internal/methods/EthCall.java | 75 + .../jsonrpc/internal/methods/EthCoinbase.java | 34 + .../internal/methods/EthEstimateGas.java | 82 + .../jsonrpc/internal/methods/EthGasPrice.java | 33 + .../internal/methods/EthGetBalance.java | 34 + .../internal/methods/EthGetBlockByHash.java | 61 + .../internal/methods/EthGetBlockByNumber.java | 58 + .../EthGetBlockTransactionCountByHash.java | 37 + .../EthGetBlockTransactionCountByNumber.java | 30 + .../jsonrpc/internal/methods/EthGetCode.java | 31 + .../internal/methods/EthGetFilterChanges.java | 58 + .../internal/methods/EthGetFilterLogs.java | 41 + .../jsonrpc/internal/methods/EthGetLogs.java | 60 + .../internal/methods/EthGetStorageAt.java | 37 + .../EthGetTransactionByBlockHashAndIndex.java | 44 + ...thGetTransactionByBlockNumberAndIndex.java | 38 + .../methods/EthGetTransactionByHash.java | 58 + .../methods/EthGetTransactionCount.java | 59 + .../methods/EthGetTransactionReceipt.java | 49 + .../EthGetUncleByBlockHashAndIndex.java | 41 + .../EthGetUncleByBlockNumberAndIndex.java | 37 + .../methods/EthGetUncleCountByBlockHash.java | 33 + .../EthGetUncleCountByBlockNumber.java | 30 + .../jsonrpc/internal/methods/EthMining.java | 26 + .../internal/methods/EthNewBlockFilter.java | 25 + .../internal/methods/EthNewFilter.java | 38 + .../EthNewPendingTransactionFilter.java | 25 + .../internal/methods/EthProtocolVersion.java | 35 + .../methods/EthSendRawTransaction.java | 73 + .../jsonrpc/internal/methods/EthSyncing.java | 33 + .../internal/methods/EthUninstallFilter.java | 30 + .../internal/methods/JsonRpcMethod.java | 22 + .../internal/methods/NetListening.java | 25 + .../internal/methods/NetPeerCount.java | 25 + .../jsonrpc/internal/methods/NetVersion.java | 29 + .../internal/methods/Web3ClientVersion.java | 24 + .../jsonrpc/internal/methods/Web3Sha3.java | 40 + .../methods/miner/MinerSetCoinbase.java | 35 + .../methods/miner/MinerSetEtherbase.java | 25 + .../internal/methods/miner/MinerStart.java | 35 + .../internal/methods/miner/MinerStop.java | 30 + .../internal/parameters/BlockParameter.java | 63 + .../internal/parameters/CallParameter.java | 87 + .../internal/parameters/FilterParameter.java | 78 + .../internal/parameters/JsonRpcParameter.java | 71 + .../internal/parameters/TopicsParameter.java | 40 + .../internal/parameters/UInt256Parameter.java | 19 + .../parameters/UnsignedIntParameter.java | 20 + .../parameters/UnsignedLongParameter.java | 20 + .../internal/processor/BlockReplay.java | 94 + .../internal/processor/TransactionTrace.java | 37 + .../processor/TransactionTraceParams.java | 29 + .../internal/processor/TransactionTracer.java | 35 + .../TransientTransactionProcessingResult.java | 57 + .../TransientTransactionProcessor.java | 95 + .../internal/queries/BlockWithMetadata.java | 55 + .../internal/queries/BlockchainQueries.java | 612 + .../internal/queries/LogWithMetadata.java | 119 + .../TransactionReceiptWithMetadata.java | 74 + .../queries/TransactionWithMetadata.java | 39 + .../internal/response/JsonRpcError.java | 63 + .../response/JsonRpcErrorResponse.java | 51 + .../internal/response/JsonRpcNoResponse.java | 9 + .../internal/response/JsonRpcResponse.java | 13 + .../response/JsonRpcResponseType.java | 8 + .../response/JsonRpcSuccessResponse.java | 51 + .../jsonrpc/internal/results/BlockResult.java | 176 + .../internal/results/BlockResultFactory.java | 60 + .../results/DebugStorageRangeAtResult.java | 74 + .../results/DebugTraceTransactionResult.java | 57 + .../internal/results/JsonRpcResult.java | 7 + .../jsonrpc/internal/results/LogResult.java | 96 + .../jsonrpc/internal/results/LogsResult.java | 27 + .../internal/results/NetworkResult.java | 36 + .../jsonrpc/internal/results/PeerResult.java | 68 + .../jsonrpc/internal/results/Quantity.java | 77 + .../jsonrpc/internal/results/StructLog.java | 121 + .../internal/results/StructLogWithError.java | 28 + .../internal/results/SyncingResult.java | 54 + .../results/TransactionCompleteResult.java | 130 + .../results/TransactionHashResult.java | 17 + .../results/TransactionPendingResult.java | 107 + .../results/TransactionReceiptLogResult.java | 105 + .../results/TransactionReceiptResult.java | 143 + .../results/TransactionReceiptRootResult.java | 20 + .../TransactionReceiptStatusResult.java | 20 + .../internal/results/TransactionResult.java | 3 + .../internal/results/UncleBlockResult.java | 24 + .../websocket/WebSocketConfiguration.java | 95 + .../websocket/WebSocketRequestHandler.java | 67 + .../jsonrpc/websocket/WebSocketService.java | 140 + .../methods/AbstractSubscriptionMethod.java | 25 + .../websocket/methods/EthSubscribe.java | 39 + .../websocket/methods/EthUnsubscribe.java | 41 + .../methods/WebSocketMethodsFactory.java | 41 + .../methods/WebSocketRpcRequest.java | 33 + .../websocket/subscription/Subscription.java | 54 + .../subscription/SubscriptionBuilder.java | 52 + .../subscription/SubscriptionManager.java | 147 + .../SubscriptionNotFoundException.java | 15 + .../NewBlockHeadersSubscription.java | 18 + .../NewBlockHeadersSubscriptionService.java | 55 + .../subscription/logs/LogsSubscription.java | 23 + .../logs/LogsSubscriptionService.java | 102 + .../pending/PendingTransactionResult.java | 20 + ...PendingTransactionSubscriptionService.java | 38 + .../request/InvalidRequestException.java | 8 + .../InvalidSubscriptionRequestException.java | 12 + .../request/LogsSubscriptionParam.java | 28 + .../NewBlockHeadersSubscriptionParam.java | 19 + .../request/SubscribeRequest.java | 74 + .../request/SubscriptionRequestMapper.java | 109 + .../request/SubscriptionType.java | 37 + .../request/UnsubscribeRequest.java | 50 + .../response/SubscriptionResponse.java | 35 + .../response/SubscriptionResponseResult.java | 28 + .../syncing/NotSynchronisingResult.java | 13 + .../syncing/SyncingSubscription.java | 20 + .../syncing/SyncingSubscriptionService.java | 74 + .../AbstractEthJsonRpcHttpServiceTest.java | 194 + .../jsonrpc/AdminJsonRpcHttpServiceTest.java | 101 + .../jsonrpc/EthJsonRpcHttpBySpecTest.java | 260 + .../jsonrpc/JsonRpcConfigurationTest.java | 57 + .../jsonrpc/JsonRpcHttpServiceCorsTest.java | 161 + .../JsonRpcHttpServiceRpcApisTest.java | 174 + .../jsonrpc/JsonRpcHttpServiceTest.java | 2037 ++ .../ethereum/jsonrpc/JsonRpcTestHelper.java | 64 + .../ethereum/jsonrpc/MockPeerConnection.java | 66 + .../filter/EthJsonRpcHttpServiceTest.java | 133 + .../filter/FilterIdGeneratorTest.java | 18 + .../filter/FilterManagerLogFilterTest.java | 176 + .../internal/filter/FilterManagerTest.java | 203 + .../internal/filter/LogsQueryTest.java | 405 + .../internal/methods/AdminPeersTest.java | 88 + .../methods/DebugStorageRangeAtTest.java | 109 + .../methods/DebugTraceTransactionTest.java | 99 + .../jsonrpc/internal/methods/EthCallTest.java | 152 + .../internal/methods/EthCoinbaseTest.java | 71 + .../internal/methods/EthEstimateGasTest.java | 99 + .../internal/methods/EthGasPriceTest.java | 55 + .../methods/EthGetBlockByHashTest.java | 107 + .../methods/EthGetFilterChangesTest.java | 195 + .../methods/EthGetFilterLogsTest.java | 128 + .../methods/EthGetTransactionCountTest.java | 52 + .../methods/EthGetTransactionReceiptTest.java | 142 + .../EthGetUncleByBlockHashAndIndexTest.java | 160 + .../EthGetUncleByBlockNumberAndIndexTest.java | 136 + .../internal/methods/EthMiningTest.java | 64 + .../methods/EthNewBlockFilterTest.java | 46 + .../internal/methods/EthNewFilterTest.java | 159 + .../methods/EthProtocolVersionTest.java | 75 + .../methods/EthSendRawTransactionTest.java | 150 + .../internal/methods/EthSyncingTest.java | 72 + .../internal/methods/NetListeningTest.java | 52 + .../TransientTransactionProcessorTest.java | 230 + .../internal/methods/Web3Sha3Test.java | 118 + .../methods/miner/MinerSetCoinbaseTest.java | 77 + .../methods/miner/MinerSetEtherbaseTest.java | 48 + .../methods/miner/MinerStartTest.java | 63 + .../internal/methods/miner/MinerStopTest.java | 46 + .../parameters/FilterParameterTest.java | 52 + .../processor/TransactionTracerTest.java | 179 + ...nsientTransactionProcessingResultTest.java | 75 + .../queries/BlockchainQueriesTest.java | 572 + .../internal/results/NetworkResultTest.java | 20 + .../websocket/WebSocketConfigurationTest.java | 21 + .../WebSocketRequestHandlerTest.java | 166 + .../websocket/WebSocketServiceTest.java | 127 + .../methods/EthSubscribeIntegrationTest.java | 126 + .../websocket/methods/EthSubscribeTest.java | 86 + .../EthUnsubscribeIntegrationTest.java | 132 + .../websocket/methods/EthUnsubscribeTest.java | 97 + .../methods/WebSocketMethodsFactoryTest.java | 73 + .../subscription/SubscriptionBuilderTest.java | 130 + .../SubscriptionManagerSendMessageTest.java | 86 + .../subscription/SubscriptionManagerTest.java | 204 + ...ewBlockHeadersSubscriptionServiceTest.java | 179 + .../logs/LogsSubscriptionServiceTest.java | 242 + ...ingTransactionSubscriptionServiceTest.java | 97 + .../SubscriptionRequestMapperTest.java | 283 + .../SyncingSubscriptionServiceTest.java | 99 + .../ethereum/jsonrpc/eth_blockNumber.json | 14 + .../ethereum/jsonrpc/eth_call_block_8.json | 21 + .../eth_call_callParamsMissing_block_8.json | 19 + .../jsonrpc/eth_call_earliestBlock.json | 21 + .../eth_call_gasLimitTooLow_block_8.json | 24 + .../eth_call_gasPriceTooHigh_block_8.json | 24 + .../jsonrpc/eth_call_latestBlock.json | 21 + .../jsonrpc/eth_call_toMissing_block_8.json | 23 + .../eth_call_valueTooHigh_block_8.json | 24 + .../eth_estimateGas_contractDeploy.json | 19 + .../eth_estimateGas_insufficientGas.json | 18 + .../jsonrpc/eth_estimateGas_noParams.json | 16 + .../jsonrpc/eth_estimateGas_transfer.json | 20 + ...th_getBalance_illegalRangeGreaterThan.json | 17 + .../eth_getBalance_illegalRangeLessThan.json | 17 + .../jsonrpc/eth_getBalance_invalidParams.json | 17 + .../jsonrpc/eth_getBalance_latest.json | 17 + ...eth_getBlockTransactionCountByHash_00.json | 16 + ...eth_getBlockTransactionCountByHash_01.json | 16 + ...eth_getBlockTransactionCountByHash_02.json | 16 + ...eth_getBlockTransactionCountByHash_03.json | 16 + ...eth_getBlockTransactionCountByHash_04.json | 16 + ...eth_getBlockTransactionCountByHash_05.json | 16 + ...eth_getBlockTransactionCountByHash_06.json | 16 + ...eth_getBlockTransactionCountByHash_07.json | 16 + ...eth_getBlockTransactionCountByHash_08.json | 16 + ...eth_getBlockTransactionCountByHash_09.json | 16 + ...eth_getBlockTransactionCountByHash_10.json | 16 + ...eth_getBlockTransactionCountByHash_11.json | 16 + ...kTransactionCountByHash_invalidParams.json | 17 + ...tBlockTransactionCountByHash_noResult.json | 16 + ...h_getBlockTransactionCountByNumber_00.json | 16 + ...lockTransactionCountByNumber_earliest.json | 16 + ...CountByNumber_illegalRangeGreaterThan.json | 16 + ...ionCountByNumber_illegalRangeLessThan.json | 16 + ...ransactionCountByNumber_invalidParams.json | 17 + ...tBlockTransactionCountByNumber_latest.json | 16 + ...getBlockTransactionCountByNumber_null.json | 16 + .../eth_getCode_illegalRangeGreaterThan.json | 17 + .../eth_getCode_illegalRangeLessThan.json | 17 + .../jsonrpc/eth_getCode_invalidParams.json | 17 + .../jsonrpc/eth_getCode_noCodeLatest.json | 17 + .../jsonrpc/eth_getCode_noCodeNumber.json | 17 + .../ethereum/jsonrpc/eth_getCode_success.json | 17 + ...eth_getFilterChanges_FilterIdNegative.json | 17 + .../eth_getFilterChanges_FilterIdTooLong.json | 17 + ...th_getFilterChanges_NonexistentFilter.json | 17 + .../jsonrpc/eth_getLogs_blockhash.json | 28 + .../eth_getLogs_failTopicPosition.json | 19 + .../eth_getLogs_fromBlockExceedToBlock.json | 19 + .../jsonrpc/eth_getLogs_invalidInput.json | 29 + .../jsonrpc/eth_getLogs_matchTopic.json | 29 + .../jsonrpc/eth_getLogs_nullParam.json | 29 + .../eth_getLogs_toBlockOutOfRange.json | 19 + .../jsonrpc/eth_getNewFilter_addressOnly.json | 16 + .../jsonrpc/eth_getNewFilter_emptyFilter.json | 14 + .../eth_getNewFilter_invalidFilter.json | 19 + .../jsonrpc/eth_getNewFilter_topicOnly.json | 16 + ...h_getNewFilter_validFilterLatestBlock.json | 19 + ...tNewFilter_validFilterWithBlockNumber.json | 19 + ..._getStorageAt_illegalRangeGreaterThan.json | 18 + ...eth_getStorageAt_illegalRangeLessThan.json | 18 + .../eth_getStorageAt_invalidParams.json | 17 + .../jsonrpc/eth_getStorageAt_latest.json | 18 + ..._getTransactionByBlockHashAndIndex_00.json | 32 + ..._getTransactionByBlockHashAndIndex_01.json | 32 + ..._getTransactionByBlockHashAndIndex_02.json | 32 + ...actionByBlockHashAndIndex_intOverflow.json | 20 + ...onByBlockHashAndIndex_missingParam_00.json | 19 + ...onByBlockHashAndIndex_missingParam_01.json | 19 + ...tionByBlockHashAndIndex_missingParams.json | 17 + ...etTransactionByBlockHashAndIndex_null.json | 17 + ...ionByBlockHashAndIndex_wrongParamType.json | 20 + ...etTransactionByBlockNumberAndIndex_00.json | 32 + ...etTransactionByBlockNumberAndIndex_01.json | 32 + ...ionByBlockNumberAndIndex_earliestNull.json | 17 + ...onByBlockNumberAndIndex_invalidParams.json | 17 + ...ansactionByBlockNumberAndIndex_latest.json | 32 + ...TransactionByBlockNumberAndIndex_null.json | 17 + ...tionByBlockNumberAndIndex_pendingNull.json | 17 + ..._getTransactionByHash_addressReceiver.json | 31 + ...getTransactionByHash_contractCreation.json | 31 + ...TransactionByHash_invalidHashAndIndex.json | 20 + ...th_getTransactionByHash_invalidParams.json | 17 + .../eth_getTransactionByHash_null.json | 16 + ...eth_getTransactionByHash_typeMismatch.json | 19 + .../eth_getTransactionCount_blockNumber.json | 17 + .../eth_getTransactionCount_earliest.json | 17 + .../eth_getTransactionCount_illegalRange.json | 17 + .../eth_getTransactionCount_latest.json | 17 + ...h_getTransactionCount_missingArgument.json | 17 + ...getTransactionReceipt_contractAddress.json | 29 + .../eth_getTransactionReceipt_logs.json | 46 + ...ransactionReceipt_nullContractAddress.json | 29 + .../ethereum/jsonrpc/eth_newBlockFilter.json | 14 + .../eth_newPendingTransactionFilter.json | 14 + ...h_sendRawTransaction_contractCreation.json | 16 + ...endRawTransaction_invalidByteValueHex.json | 19 + ...dRawTransaction_invalidRawTransaction.json | 19 + .../eth_sendRawTransaction_messageCall.json | 16 + .../eth_sendRawTransaction_transferEther.json | 16 + ...endRawTransaction_unsignedTransaction.json | 19 + .../eth_uninstallFilter_FilterIdNegative.json | 14 + .../eth_uninstallFilter_FilterIdTooLong.json | 14 + ...eth_uninstallFilter_NonexistentFilter.json | 14 + .../jsonrpc/jsonRpcTestBlockchain.blocks | Bin 0 -> 23287 bytes .../ethereum/jsonrpc/jsonRpcTestGenesis.json | 20 + ethereum/mock-p2p/build.gradle | 19 + .../ethereum/p2p/testing/MockNetwork.java | 256 + .../ethereum/p2p/testing/MockNetworkTest.java | 102 + ethereum/p2p/build.gradle | 38 + .../ethereum/p2p/NetworkMemoryPool.java | 14 + .../pantheon/ethereum/p2p/NetworkRunner.java | 200 + .../ethereum/p2p/api/DisconnectCallback.java | 8 + .../pantheon/ethereum/p2p/api/Message.java | 19 + .../ethereum/p2p/api/MessageData.java | 34 + .../pantheon/ethereum/p2p/api/P2PNetwork.java | 76 + .../ethereum/p2p/api/PeerConnection.java | 86 + .../ethereum/p2p/api/ProtocolManager.java | 61 + .../p2p/config/DiscoveryConfiguration.java | 146 + .../p2p/config/NetworkingConfiguration.java | 82 + .../p2p/config/RlpxConfiguration.java | 88 + .../p2p/config/SubProtocolConfiguration.java | 28 + .../p2p/config/WireProtocolConfig.java | 65 + .../ethereum/p2p/discovery/DiscoveryPeer.java | 88 + .../p2p/discovery/PeerDiscoveryAgent.java | 403 + .../p2p/discovery/PeerDiscoveryEvent.java | 54 + .../PeerDiscoveryPacketDecodingException.java | 12 + .../PeerDiscoveryServiceException.java | 9 + .../p2p/discovery/PeerDiscoveryStatus.java | 28 + .../p2p/discovery/internal/Bucket.java | 133 + .../internal/FindNeighborsPacketData.java | 59 + .../internal/NeighborsPacketData.java | 63 + .../p2p/discovery/internal/Packet.java | 178 + .../p2p/discovery/internal/PacketData.java | 19 + .../p2p/discovery/internal/PacketType.java | 52 + .../internal/PeerDiscoveryController.java | 547 + .../discovery/internal/PeerRequirement.java | 12 + .../p2p/discovery/internal/PeerTable.java | 276 + .../discovery/internal/PingPacketData.java | 85 + .../discovery/internal/PongPacketData.java | 71 + .../internal/RetryDelayFunction.java | 15 + .../p2p/netty/AbstractHandshakeHandler.java | 139 + .../ethereum/p2p/netty/ApiHandler.java | 98 + .../ethereum/p2p/netty/Callbacks.java | 55 + .../p2p/netty/CapabilityMultiplexer.java | 180 + .../pantheon/ethereum/p2p/netty/DeFramer.java | 114 + .../p2p/netty/HandshakeHandlerInbound.java | 38 + .../p2p/netty/HandshakeHandlerOutbound.java | 61 + .../ethereum/p2p/netty/MessageFramer.java | 25 + .../ethereum/p2p/netty/NettyP2PNetwork.java | 390 + .../p2p/netty/NettyPeerConnection.java | 150 + .../ethereum/p2p/netty/OutboundMessage.java | 24 + .../p2p/netty/PeerConnectionRegistry.java | 41 + .../ethereum/p2p/netty/TimeoutHandler.java | 45 + .../ethereum/p2p/netty/WireKeepAlive.java | 49 + .../exceptions/IncompatiblePeerException.java | 8 + .../ethereum/p2p/peers/DefaultPeer.java | 190 + .../ethereum/p2p/peers/DefaultPeerId.java | 42 + .../pantheon/ethereum/p2p/peers/Endpoint.java | 152 + .../pantheon/ethereum/p2p/peers/Peer.java | 40 + .../ethereum/p2p/peers/PeerBlacklist.java | 80 + .../pantheon/ethereum/p2p/peers/PeerId.java | 22 + .../rlpx/framing/CompressionException.java | 13 + .../ethereum/p2p/rlpx/framing/Compressor.java | 25 + .../ethereum/p2p/rlpx/framing/Framer.java | 371 + .../p2p/rlpx/framing/FramingException.java | 13 + .../p2p/rlpx/framing/SnappyCompressor.java | 35 + .../rlpx/handshake/HandshakeException.java | 13 + .../p2p/rlpx/handshake/HandshakeSecrets.java | 188 + .../p2p/rlpx/handshake/Handshaker.java | 148 + .../ecies/ECIESEncryptionEngine.java | 416 + .../rlpx/handshake/ecies/ECIESHandshaker.java | 412 + .../handshake/ecies/EncryptedMessage.java | 152 + .../ecies/InitiatorHandshakeMessage.java | 18 + .../ecies/InitiatorHandshakeMessageV1.java | 163 + .../ecies/InitiatorHandshakeMessageV4.java | 101 + .../ecies/ResponderHandshakeMessage.java | 14 + .../ecies/ResponderHandshakeMessageV1.java | 93 + .../ecies/ResponderHandshakeMessageV4.java | 53 + .../ethereum/p2p/utils/ByteBufUtils.java | 39 + .../p2p/wire/AbstractMessageData.java | 36 + .../ethereum/p2p/wire/Capability.java | 85 + .../ethereum/p2p/wire/DefaultMessage.java | 31 + .../pantheon/ethereum/p2p/wire/PeerInfo.java | 129 + .../ethereum/p2p/wire/RawMessage.java | 18 + .../ethereum/p2p/wire/SubProtocol.java | 28 + .../p2p/wire/WireProtocolException.java | 13 + .../p2p/wire/messages/DisconnectMessage.java | 137 + .../p2p/wire/messages/EmptyMessage.java | 23 + .../p2p/wire/messages/HelloMessage.java | 58 + .../p2p/wire/messages/PingMessage.java | 22 + .../p2p/wire/messages/PongMessage.java | 17 + .../p2p/wire/messages/WireMessageCodes.java | 10 + .../ethereum/p2p/NettyP2PNetworkTest.java | 393 + .../p2p/NetworkingServiceLifecycleTest.java | 189 + .../ethereum/p2p/NetworkingTestHelper.java | 14 + .../discovery/AbstractPeerDiscoveryTest.java | 273 + .../p2p/discovery/PeerDiscoveryAgentTest.java | 329 + .../discovery/PeerDiscoveryBondingTest.java | 82 + .../PeerDiscoveryBootstrappingTest.java | 124 + .../discovery/PeerDiscoveryObserversTest.java | 174 + .../PeerDiscoveryPacketPcapSedesTest.java | 122 + .../PeerDiscoveryPacketSedesTest.java | 117 + .../discovery/PeerDiscoveryTestHelper.java | 27 + .../PeerDiscoveryTimestampsTest.java | 136 + .../p2p/discovery/internal/BucketTest.java | 122 + .../internal/MockPacketDataFactory.java | 59 + .../p2p/discovery/internal/PacketTest.java | 51 + ...overyControllerDistanceCalculatorTest.java | 68 + .../internal/PeerDiscoveryControllerTest.java | 932 + .../PeerDiscoveryTableRefreshTest.java | 86 + .../p2p/discovery/internal/PeerTableTest.java | 54 + .../p2p/netty/CapabilityMultiplexerTest.java | 102 + .../p2p/netty/NettyPeerConnectionTest.java | 47 + .../p2p/netty/PeerConnectionRegistryTest.java | 61 + .../ethereum/p2p/peers/PeerBlacklistTest.java | 127 + .../pantheon/ethereum/p2p/peers/PeerTest.java | 187 + .../ethereum/p2p/rlpx/framing/FramerTest.java | 176 + .../rlpx/framing/SnappyCompressorTest.java | 86 + .../handshake/ecies/ECIESHandshakeTest.java | 196 + .../handshake/ecies/EncryptedMessageTest.java | 26 + .../InitiatorHandshakeMessageV4Test.java | 49 + .../p2p/wire/WireMessagesSedesTest.java | 50 + ethereum/p2p/src/test/resources/log4j2.xml | 16 + .../handshake/ecies/test.initiatormessage | 1 + .../p2p/rlpx/handshake/ecies/test.keypair | 1 + ethereum/p2p/src/test/resources/peer1.json | 46 + ethereum/p2p/src/test/resources/peer2.json | 18 + ethereum/p2p/src/test/resources/udp.pcap | Bin 0 -> 396844 bytes ethereum/referencetests/build.gradle | 10 + ethereum/rlp/build.gradle | 26 + .../pantheon/ethereum/rlp/RLPBench.java | 65 + .../ethereum/rlp/AbstractRLPInput.java | 552 + .../ethereum/rlp/AbstractRLPOutput.java | 177 + .../ethereum/rlp/BytesValueRLPInput.java | 60 + .../ethereum/rlp/BytesValueRLPOutput.java | 23 + .../rlp/CorruptedRLPInputException.java | 8 + .../pantheon/ethereum/rlp/FileRLPInput.java | 95 + .../rlp/MalformedRLPInputException.java | 11 + .../consensys/pantheon/ethereum/rlp/RLP.java | 263 + .../ethereum/rlp/RLPDecodingHelpers.java | 73 + .../ethereum/rlp/RLPEncodingHelpers.java | 94 + .../pantheon/ethereum/rlp/RLPException.java | 11 + .../pantheon/ethereum/rlp/RLPInput.java | 346 + .../pantheon/ethereum/rlp/RLPOutput.java | 269 + .../pantheon/ethereum/rlp/RlpUtils.java | 132 + .../ethereum/rlp/VertxBufferRLPInput.java | 84 + .../ethereum/rlp/VertxBufferRLPOutput.java | 28 + .../pantheon/ethereum/rlp/package-info.java | 12 + .../ethereum/rlp/BytesValueRLPInputTest.java | 436 + .../ethereum/rlp/BytesValueRLPOutputTest.java | 295 + .../ethereum/rlp/InvalidRLPRefTest.java | 37 + .../rlp/InvalidRLPRefTestCaseSpec.java | 23 + .../pantheon/ethereum/rlp/RLPRefTest.java | 40 + .../ethereum/rlp/RLPRefTestCaseSpec.java | 83 + .../pantheon/ethereum/rlp/invalidRLPTest.json | 90 + ethereum/trie/build.gradle | 26 + .../pantheon/ethereum/trie/BranchNode.java | 209 + .../pantheon/ethereum/trie/CommitVisitor.java | 61 + .../ethereum/trie/CompactEncoding.java | 109 + .../ethereum/trie/DefaultNodeFactory.java | 57 + .../pantheon/ethereum/trie/ExtensionNode.java | 131 + .../pantheon/ethereum/trie/GetVisitor.java | 48 + .../ethereum/trie/KeyValueMerkleStorage.java | 53 + .../pantheon/ethereum/trie/LeafNode.java | 122 + .../ethereum/trie/MerklePatriciaTrie.java | 63 + .../pantheon/ethereum/trie/MerkleStorage.java | 36 + .../ethereum/trie/MerkleStorageException.java | 16 + .../pantheon/ethereum/trie/Node.java | 33 + .../pantheon/ethereum/trie/NodeFactory.java | 17 + .../pantheon/ethereum/trie/NodeLoader.java | 10 + .../pantheon/ethereum/trie/NodeUpdater.java | 8 + .../pantheon/ethereum/trie/NodeVisitor.java | 12 + .../pantheon/ethereum/trie/NullNode.java | 78 + .../ethereum/trie/PathNodeVisitor.java | 14 + .../pantheon/ethereum/trie/PutVisitor.java | 94 + .../pantheon/ethereum/trie/RemoveVisitor.java | 49 + .../trie/SimpleMerklePatriciaTrie.java | 73 + .../trie/StorageEntriesCollector.java | 44 + .../trie/StoredMerklePatriciaTrie.java | 109 + .../pantheon/ethereum/trie/StoredNode.java | 89 + .../ethereum/trie/StoredNodeFactory.java | 216 + .../pantheon/ethereum/trie/TrieIterator.java | 96 + .../ethereum/trie/CompactEncodingTest.java | 55 + .../trie/SimpleMerklePatriciaTrieTest.java | 276 + .../trie/StoredMerklePatriciaTrieTest.java | 410 + .../ethereum/trie/TrieIteratorTest.java | 132 + .../pantheon/ethereum/trie/TrieRefTest.java | 46 + .../ethereum/trie/TrieRefTestCaseSpec.java | 81 + gradle.properties | 1 + gradle/check-licenses.gradle | 162 + gradle/eclipse-java-google-style.xml | 336 + gradle/formatter.properties | 51 + gradle/versions.gradle | 49 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54413 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 + gradlew.bat | 84 + pantheon/build.gradle | 52 + pantheon/libs/picocli-3.6.0-SNAPSHOT.jar | Bin 0 -> 240631 bytes .../java/net/consensys/pantheon/Pantheon.java | 32 + .../net/consensys/pantheon/PantheonInfo.java | 12 + .../java/net/consensys/pantheon/Runner.java | 133 + .../net/consensys/pantheon/RunnerBuilder.java | 278 + .../cli/ConfigOptionSearchAndRunHandler.java | 56 + .../cli/ExportPublicKeySubCommand.java | 55 + .../cli/ImportBlockchainSubCommand.java | 105 + .../pantheon/cli/ImportSubCommand.java | 56 + .../pantheon/cli/PantheonCommand.java | 516 + .../cli/PantheonControllerBuilder.java | 54 + .../cli/TomlConfigFileDefaultProvider.java | 123 + .../pantheon/cli/VersionProvider.java | 12 + .../custom/CorsAllowedOriginsProperty.java | 71 + .../controller/CliquePantheonController.java | 163 + .../controller/IbftPantheonController.java | 226 + .../pantheon/controller/KeyPairUtil.java | 33 + .../controller/MainnetPantheonController.java | 207 + .../controller/PantheonController.java | 86 + .../management/RestfulRouteBuilder.java | 140 + .../pantheon/util/BlockImporter.java | 122 + .../pantheon/util/BlockchainImporter.java | 558 + pantheon/src/main/resources/log4j2.xml | 16 + .../net/consensys/pantheon/RunnerTest.java | 253 + .../pantheon/cli/CommandTestAbstract.java | 93 + .../ConfigOptionSearchAndRunHandlerTest.java | 95 + .../cli/ExportPublicKeySubCommandTest.java | 48 + .../pantheon/cli/ImportSubCommandTest.java | 41 + .../pantheon/cli/PantheonCommandTest.java | 783 + .../TomlConfigFileDefaultProviderTest.java | 139 + .../pantheon/util/BlockImporterTest.java | 71 + .../pantheon/util/BlockchainImporterTest.java | 73 + .../src/test/resources/complete_config.toml | 21 + pantheon/src/test/resources/ibft.blocks | Bin 0 -> 865389 bytes pantheon/src/test/resources/ibft_genesis.json | 40 + pantheon/src/test/resources/log4j2.xml | 16 + .../ethereum/jsonrpc/json-rpc-test.bin | Bin 0 -> 36860 bytes .../ethereum/jsonrpc/jsonRpcTestGenesis.json | 20 + .../src/test/resources/partial_config.toml | 4 + quickstart/README.md | 1 + quickstart/docker-compose.yml | 53 + quickstart/explorer/App.js.patch | 35 + quickstart/explorer/Dockerfile | 20 + quickstart/explorer/favicon.png | Bin 0 -> 14901 bytes quickstart/explorer/index.html.patch | 13 + quickstart/explorer/index.js.patch | 10 + quickstart/explorer/init.js.patch | 11 + quickstart/listQuickstartServices.sh | 50 + quickstart/pantheon/.gitignore | 2 + quickstart/pantheon/Dockerfile | 16 + quickstart/pantheon/bootnode_start.sh | 7 + quickstart/pantheon/node_start.sh | 24 + quickstart/removePantheonPrivateNetwork.sh | 15 + quickstart/runPantheonPrivateNetwork.sh | 18 + services/build.gradle | 1 + services/kvstore/build.gradle | 21 + .../kvstore/InMemoryKeyValueStorage.java | 113 + .../services/kvstore/KeyValueStorage.java | 166 + .../kvstore/RocksDbKeyValueStorage.java | 223 + .../kvstore/src/main/resources/log4j2.xml | 16 + .../kvstore/AbstractKeyValueStorageTest.java | 323 + .../kvstore/InMemoryKeyValueStorageTest.java | 9 + .../kvstore/RocksDbKeyValueStorageTest.java | 14 + settings.gradle | 23 + system-tests/build.gradle | 41 + .../pantheon/tests/ClusterTestBase.java | 152 + .../pantheon/tests/LogClusterInfoTest.java | 86 + .../pantheon/tests/PantheonSmokeTest.java | 101 + .../pantheon/tests/cluster/DockerUtils.java | 121 + .../tests/cluster/NodeAdminRpcUtils.java | 65 + .../pantheon/tests/cluster/TestCluster.java | 494 + .../tests/cluster/TestClusterNode.java | 177 + .../tests/cluster/TestDockerNode.java | 74 + .../tests/cluster/TestGethLocalNode.java | 71 + .../tests/cluster/docker/geth/Dockerfile-geth | 25 + .../docker/geth/Dockerfile-geth-ubuntu | 45 + .../tests/cluster/docker/geth/bash_aliases | 1 + .../tests/cluster/docker/geth/genesis.json | 25 + .../tests/cluster/docker/geth/gethUtils.sh | 42 + .../pantheon/tests/cluster/docker/geth/run.sh | 11 + .../tests/cluster/docker/geth/runBootNode.sh | 13 + .../net/consensys/pantheon/tests/ibft.json | 26707 ++++++++++++++++ testutil/build.gradle | 14 + .../pantheon/testutil/BlockTestUtil.java | 32 + .../pantheon/testutil/JsonTestParameters.java | 236 + testutil/src/main/resources/1000.blocks | Bin 0 -> 700472 bytes testutil/src/main/resources/log4j2.xml | 1 + util/build.gradle | 18 + .../pantheon/util/ExceptionUtils.java | 26 + .../pantheon/util/NetworkUtility.java | 33 + .../pantheon/util/Preconditions.java | 17 + .../consensys/pantheon/util/Subscribers.java | 77 + .../util/bytes/AbstractBytes32Backed.java | 15 + .../util/bytes/AbstractBytesValue.java | 139 + .../util/bytes/ArrayWrappingBytes32.java | 46 + .../util/bytes/ArrayWrappingBytesValue.java | 170 + .../util/bytes/BaseDelegatingBytesValue.java | 97 + .../pantheon/util/bytes/Bytes32.java | 173 + .../pantheon/util/bytes/Bytes32Backed.java | 7 + .../pantheon/util/bytes/Bytes32s.java | 109 + .../pantheon/util/bytes/BytesBacked.java | 7 + .../pantheon/util/bytes/BytesValue.java | 515 + .../pantheon/util/bytes/BytesValues.java | 326 + .../util/bytes/DelegatingBytes32.java | 17 + .../util/bytes/DelegatingBytesValue.java | 21 + .../bytes/MutableArrayWrappingBytes32.java | 24 + .../bytes/MutableArrayWrappingBytesValue.java | 55 + .../MutableBufferWrappingBytesValue.java | 70 + .../MutableByteBufWrappingBytesValue.java | 70 + .../pantheon/util/bytes/MutableBytes32.java | 133 + .../util/bytes/MutableBytesValue.java | 171 + .../pantheon/util/bytes/WrappingBytes32.java | 47 + .../consensys/pantheon/util/time/Clock.java | 6 + .../pantheon/util/time/SystemClock.java | 9 + .../util/uint/AbstractUInt256Value.java | 184 + .../pantheon/util/uint/BaseUInt256Value.java | 62 + .../consensys/pantheon/util/uint/Counter.java | 83 + .../pantheon/util/uint/DefaultInt256.java | 64 + .../pantheon/util/uint/DefaultUInt256.java | 31 + .../consensys/pantheon/util/uint/Int256.java | 40 + .../pantheon/util/uint/Int256Bytes.java | 81 + .../consensys/pantheon/util/uint/UInt256.java | 46 + .../pantheon/util/uint/UInt256Bytes.java | 420 + .../pantheon/util/uint/UInt256Value.java | 171 + .../pantheon/util/uint/UInt256s.java | 33 + .../pantheon/util/ExceptionUtilsTest.java | 29 + .../pantheon/util/NetworkUtilityTest.java | 20 + .../pantheon/util/SubscribersTest.java | 50 + .../pantheon/util/bytes/Bytes32Test.java | 35 + .../bytes/Bytes32sSingleLeftShiftTest.java | 67 + .../pantheon/util/bytes/BytesValueTest.java | 890 + .../pantheon/util/bytes/BytesValuesTest.java | 222 + .../pantheon/util/uint/UInt256BytesTest.java | 291 + versions.gradle | 4 + 1319 files changed, 168036 insertions(+) create mode 100755 .dockerignore create mode 100755 .gitattributes create mode 100755 .gitignore create mode 100755 .gitmodules create mode 100755 CONTRIBUTING.md create mode 100755 Dockerfile create mode 100755 Jenkinsfile create mode 100755 LICENSE create mode 100755 README.md create mode 100755 acceptance-tests/build.gradle create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/CreateAccountAcceptanceTest.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/PantheonClusterAcceptanceTest.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/RpcApisTogglesAcceptanceTest.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/JsonRpc.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/WaitUtils.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Account.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Accounts.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Cluster.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Eth.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNode.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeConfig.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeRunner.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Web3.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/JsonRpcSuccessEvent.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/Subscription.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/SubscriptionEvent.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocket.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketConnection.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketEvent.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/jsonrpc/Web3Sha3AcceptanceTest.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/mining/MiningAcceptanceTest.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/pubsub/NewPendingTransactionAcceptanceTest.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitter.sol create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitterAcceptanceTest.java create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.abi create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.bin create mode 100755 acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.java create mode 100755 acceptance-tests/src/test/resources/log4j2.xml create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/README.md create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/contracts/Adoption.sol create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/contracts/Migrations.sol create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/migrations/1_initial_migration.js create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/migrations/2_deploy_contracts.js create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/test/TestAdoption.sol create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/test/test.js create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/test/test2.js create mode 100755 acceptance-tests/truffle-pet-shop-tutorial/truffle.js create mode 100755 build.gradle create mode 100755 buildSrc/src/main/groovy/ProjectPropertiesFile.groovy create mode 100755 consensus/build.gradle create mode 100755 consensus/clique/build.gradle create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/BlockHeaderValidationRulesetFactory.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashing.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueContext.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculator.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueExtraData.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueHelpers.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSchedule.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecs.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/VoteTallyCache.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreator.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockScheduler.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelector.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRule.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRule.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRule.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSigners.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Discard.java create mode 100755 consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Propose.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashingTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculatorTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueExtraDataTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolScheduleTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecsTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/TestHelpers.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/VoteTallyCacheTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreatorTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelectorTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRuleTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRuleTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRuleTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/DiscardTest.java create mode 100755 consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/ProposeTest.java create mode 100755 consensus/common/build.gradle create mode 100755 consensus/common/src/main/java/net/consensys/pantheon/consensus/common/EpochManager.java create mode 100755 consensus/common/src/main/java/net/consensys/pantheon/consensus/common/ValidatorProvider.java create mode 100755 consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteProposer.java create mode 100755 consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteTally.java create mode 100755 consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteType.java create mode 100755 consensus/common/src/main/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRule.java create mode 100755 consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteProposerTest.java create mode 100755 consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteTallyTest.java create mode 100755 consensus/common/src/test/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRuleTest.java create mode 100755 consensus/ibft/build.gradle create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ConsensusRoundIdentifier.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashing.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporter.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftContext.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvent.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEventQueue.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvents.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftExtraData.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftHelpers.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProcessor.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSchedule.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSpecs.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftStateMachine.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/RoundTimer.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdater.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreator.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftExtraDataCalculator.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelector.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRule.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ibftevent/RoundExpiry.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/IbftJsonRpcMethodsFactory.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVote.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftProtocolManager.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocol.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64Protocol.java create mode 100755 consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64ProtocolManager.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashingTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporterTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftEventQueueTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftExtraDataTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProcessorTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProtocolContextFixture.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/RoundTimerTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdaterTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreatorTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelectorTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRuleTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVoteTest.java create mode 100755 consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocolTest.java create mode 100755 crypto/build.gradle create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/BouncyCastleMessageDigestFactory.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/Hash.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/InvalidSEC256K1PrivateKeyStoreException.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/PRNGSecureRandom.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/PersonalisationString.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/QuickEntropy.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/SECP256K1.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/SecureRandomProvider.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFieldPoint.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFqp.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Pairer.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Point.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2Point.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Point.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldElement.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldPoint.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq12.java create mode 100755 crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq2.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/HashTest.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/PRNGSecureRandomTest.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/SECP256K1Test.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PairerTest.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PointTest.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2PointTest.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128PointTest.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq12Test.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq2Test.java create mode 100755 crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/FqTest.java create mode 100755 crypto/src/test/resources/log4j2.xml create mode 100755 crypto/src/test/resources/net/consensys/pantheon/crypto/validPrivateKey.txt create mode 100755 errorprone-checks/README.md create mode 100755 errorprone-checks/build.gradle create mode 100755 errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectly.java create mode 100755 errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectly.java create mode 100755 errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotReturnNullOptionals.java create mode 100755 errorprone-checks/src/main/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinal.java create mode 100755 errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyTest.java create mode 100755 errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyTest.java create mode 100755 errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotReturnNullOptionalsTest.java create mode 100755 errorprone-checks/src/test/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalTest.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyNegativeCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyPositiveCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyNegativeCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyPositiveCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsNegativeCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsPositiveCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfaceNegativeCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfacePositiveCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalNegativeCases.java create mode 100755 errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalPositiveCases.java create mode 100755 ethereum/build.gradle create mode 100755 ethereum/core/build.gradle create mode 100755 ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/EntriesFromIntegrationTest.java create mode 100755 ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/TraceTransactionIntegrationTest.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/ProtocolContext.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AsyncBlockCreator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BaseBlockScheduler.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockCreator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockMiner.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/CoinbaseNotSetException.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockScheduler.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGenerator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningParameters.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/RandomNonceGenerator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedEvent.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedObserver.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/Blockchain.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/ChainHead.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/GenesisConfig.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/MutableBlockchain.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/TransactionLocation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AbstractWorldUpdater.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Account.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrder.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Address.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Block.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockBody.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHashFunction.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeader.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeaderBuilder.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockImporter.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockMetadata.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Gas.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Hash.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Log.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogSeries.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogTopic.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogsBloomFilter.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableAccount.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldState.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldView.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactionListener.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactions.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/ProcessableBlockHeader.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SealableBlockHeader.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SyncStatus.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Synchronizer.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Transaction.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionBuilder.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionPool.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionReceipt.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Util.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Wei.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldState.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldUpdater.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldView.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/BlockchainStorage.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchain.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/KeyValueStoragePrefixedKeyBlockchainStorage.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/WorldStateArchive.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceFrame.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceOptions.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentDifficultyCalculators.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSchedule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSpecs.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractMessageProcessor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractPrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AttachedBlockHeaderValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockBodyValidator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockProcessor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BodyValidation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ConstantinopleGasCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DetachedBlockHeaderValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DifficultyCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHash.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashCacheFactory.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolution.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolver.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverInputs.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHasher.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/FrontierGasCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HeaderValidationMode.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HomesteadGasCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockBodyValidator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHashFunction.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockImporter.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetContractCreationProcessor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetDifficultyCalculators.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetEvmRegistries.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetMessageCallProcessor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetPrecompiledContractRegistries.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSchedule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSpecs.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionProcessor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MiningBeneficiaryCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompileContractRegistry.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSchedule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpec.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpecBuilder.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduleBasedBlockHashFunction.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduledProtocolSpec.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/SpuriousDragonGasCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TangerineWhistleGasCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionProcessor.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionReceiptType.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionValidator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ValidationResult.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/CalculatedDifficultyValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ProofOfWorkValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRule.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128AddPrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128MulPrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128PairingPrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/BigIntegerModularExponentiationPrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/ECRECPrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/IDPrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/RIPEMD160PrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/SHA256PrecompiledContract.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/BlockchainUtil.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/ByteArrayUtil.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/RawBlockIterator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractCallOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Code.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracer.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/EVM.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ExceptionalHaltReason.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/GasCalculator.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Memory.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/MessageFrame.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperandStack.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Operation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationRegistry.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationTracer.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStack.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Words.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltException.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltManager.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltPredicate.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InsufficientGasExceptionalHaltPredicate.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InvalidOperationExceptionalHaltPredicate.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackOverflowExceptionalHaltPredicate.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackUnderflowExceptionalHaltPredicate.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AbstractCreateOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddModOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddressOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AndOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BalanceOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BlockHashOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ByteOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallCodeOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataCopyOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataLoadOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataSizeOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallValueOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallerOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeCopyOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeSizeOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CoinbaseOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Create2Operation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CreateOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DelegateCallOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DifficultyOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DivOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DupOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/EqOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExpOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeCopyOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeSizeOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasLimitOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasPriceOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GtOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/InvalidOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/IsZeroOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpDestOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpiOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LogOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LtOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MLoadOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MSizeOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStore8Operation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStoreOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ModOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulModOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NotOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NumberOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OrOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OriginOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PCOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PopOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PushOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataCopyOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataSizeOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/RevertOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SDivOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SGtOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLoadOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLtOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SModOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SStoreOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SarOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SelfDestructOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Sha3Operation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SignExtendOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StaticCallOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StopOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SubOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SwapOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/TimestampOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/XorOperation.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DebuggableMutableWorldState.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldState.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/KeyValueStorageWorldStateStorage.java create mode 100755 ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/WorldStateStorage.java create mode 100755 ethereum/core/src/main/resources/daoAddresses.json create mode 100755 ethereum/core/src/main/resources/dev.json create mode 100755 ethereum/core/src/main/resources/infura.json create mode 100755 ethereum/core/src/main/resources/mainnet.json create mode 100755 ethereum/core/src/main/resources/rinkeby.json create mode 100755 ethereum/core/src/main/resources/ropsten.json create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/AddressHelpers.java create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockHeaderTestFixture.java create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockSyncTestUtils.java create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/ExecutionContextTestFixture.java create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/HeaderDecodingHelpers.java create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/InMemoryWorldState.java create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/MiningParametersTestBuilder.java create mode 100755 ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/TransactionTestFixture.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelectorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockSchedulerTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMinerTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGeneratorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinatorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/chain/GenesisConfigTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrderTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/BlockHeaderMock.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTestCaseSpec.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/PendingTransactionsTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionIntegrationTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionPoolTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionReceiptTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTestCaseSpec.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchainTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolScheduleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidatorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BodyValidationTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreatorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHasherTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidatorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolScheduleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ProtocolScheduleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationResultTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationTestUtils.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRuleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRuleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRuleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRuleTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/testutil/BlockDataGenerator.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/util/BlockchainUtilTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/.gitignore create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AbstractRetryingTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressMock.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestCaseSpec.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestTools.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/CodeMock.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracerTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/EnvironmentInformation.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTestTools.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseEipSpec.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseSpec.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/LogMock.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/MemoryTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStackTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/ReferenceTestProtocolSchedules.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/StateTestVersionedTransaction.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/TestBlockchain.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTestCaseSpec.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/WorldStateMock.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/blockchain/.keep create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/generalstate/.keep create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/Create2OperationTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/SarOperationTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperationTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperationTest.java create mode 100755 ethereum/core/src/test/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldStateTest.java create mode 100755 ethereum/core/src/test/resources/log4j2.xml create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_1200000.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_1200001.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_300005.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_300006.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400000.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400001.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400002.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis-olympic.json create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis1.json create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis2.json create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_1.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_1200000.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_1200001.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_300005.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_300006.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400000.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400001.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400002.blocks create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTest.java.template create mode 100755 ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTest.java.template create mode 100755 ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldBeDeletedWhenEmptyAndTouchedTransactionSucceedsPostEIP158.json create mode 100755 ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenEmptyAndTouchedTransactionFails.json create mode 100755 ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionFails.json create mode 100755 ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionSucceeds.json create mode 100755 ethereum/eth/build.gradle create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/EthProtocol.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerRequestTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractRetryingPeerTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/ChainState.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthContext.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessages.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeer.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeers.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManager.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthScheduler.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthServer.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputation.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/RequestManager.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/EthTaskException.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/IncompleteResultsException.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/NoAvailablePeersException.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerBreachedProtocolException.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerDisconnectedException.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV62.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV63.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessage.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManager.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTracker.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/Downloader.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SyncMode.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SynchronizerConfiguration.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiter.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/FastSyncState.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocks.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncState.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncTarget.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/AbstractGetHeadersFromPeerTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTask.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTracker.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolFactory.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionSender.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageHandler.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessor.java create mode 100755 ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSender.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ChainStateTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/DeterministicEthScheduler.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeerTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeersTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTestUtil.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthSchedulerTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockEthTask.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockExecutorService.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockPeerConnection.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockScheduledExecutor.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputationTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RequestManagerTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RespondingEthPeer.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/AbstractMessageTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/PeerMessageTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskWithResultsTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessageTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManagerTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTrackerTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/DownloaderTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiterTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocksTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTaskTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTrackerTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNode.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNodeList.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolPropagationTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessorTest.java create mode 100755 ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSenderTest.java create mode 100755 ethereum/eth/src/test/resources/50.blocks create mode 100755 ethereum/eth/src/test/resources/testBlockchain.blocks create mode 100755 ethereum/eth/src/test/resources/testGenesis.json create mode 100755 ethereum/jsonrpc/build.gradle create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/BlockchainImporter.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseKey.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseUtils.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthCallIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthEstimateGasIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByHashIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByNumberIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetFilterChangesIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockHashAndIndexIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockNumberAndIndexIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/integration-test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks create mode 100755 ethereum/jsonrpc/src/integration-test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcConfiguration.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcServiceException.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequest.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequestId.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcParameters.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcRequestException.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGenerator.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManager.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQuery.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AbstractBlockParameterMethod.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeers.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAt.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransaction.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthAccounts.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthBlockNumber.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCall.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbase.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGas.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPrice.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBalance.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHash.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByNumber.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByHash.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByNumber.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetCode.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChanges.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogs.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetLogs.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetStorageAt.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockHashAndIndex.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockNumberAndIndex.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByHash.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCount.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceipt.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndex.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndex.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockHash.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockNumber.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMining.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewPendingTransactionFilter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersion.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransaction.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncing.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthUninstallFilter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/JsonRpcMethod.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListening.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetPeerCount.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetVersion.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3ClientVersion.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbase.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbase.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStart.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStop.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/BlockParameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/CallParameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/JsonRpcParameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/TopicsParameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UInt256Parameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedIntParameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedLongParameter.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/BlockReplay.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTrace.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTraceParams.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracer.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessor.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockWithMetadata.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueries.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/LogWithMetadata.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionReceiptWithMetadata.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionWithMetadata.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcErrorResponse.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcNoResponse.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponse.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponseType.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcSuccessResponse.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResultFactory.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugStorageRangeAtResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugTraceTransactionResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/JsonRpcResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogsResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/PeerResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/Quantity.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLog.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLogWithError.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/SyncingResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionCompleteResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionHashResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionPendingResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptLogResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptRootResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptStatusResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/UncleBlockResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandler.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/AbstractSubscriptionMethod.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribe.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribe.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactory.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketRpcRequest.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/Subscription.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilder.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManager.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionNotFoundException.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscription.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionService.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscription.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionService.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidRequestException.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/LogsSubscriptionParam.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/NewBlockHeadersSubscriptionParam.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscribeRequest.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionType.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/UnsubscribeRequest.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponse.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/NotSynchronisingResult.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscription.java create mode 100755 ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionService.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AdminJsonRpcHttpServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/EthJsonRpcHttpBySpecTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcConfigurationTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestHelper.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/MockPeerConnection.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/EthJsonRpcHttpServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGeneratorTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerLogFilterTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQueryTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeersTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAtTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransactionTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCallTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbaseTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGasTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPriceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHashTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChangesTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogsTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCountTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndexTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndexTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMiningTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilterTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilterTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersionTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncingTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListeningTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/TransientTransactionProcessorTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3Test.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbaseTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbaseTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStartTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStopTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameterTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracerTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResultTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueriesTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResultTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfigurationTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandlerTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeIntegrationTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactoryTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java create mode 100755 ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionServiceTest.java create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_blockNumber.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_block_8.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_callParamsMissing_block_8.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_earliestBlock.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasLimitTooLow_block_8.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasPriceTooHigh_block_8.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_latestBlock.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_toMissing_block_8.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_valueTooHigh_block_8.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_contractDeploy.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_insufficientGas.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_noParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_transfer.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeGreaterThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeLessThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_invalidParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_latest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_00.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_01.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_02.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_03.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_04.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_05.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_06.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_07.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_08.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_09.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_10.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_11.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_invalidParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_noResult.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_00.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_earliest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeGreaterThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeLessThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_invalidParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_latest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_null.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeGreaterThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeLessThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_invalidParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeLatest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeNumber.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_success.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdNegative.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdTooLong.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_NonexistentFilter.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_blockhash.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_failTopicPosition.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_fromBlockExceedToBlock.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_invalidInput.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_matchTopic.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_nullParam.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_toBlockOutOfRange.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_addressOnly.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_emptyFilter.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_invalidFilter.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_topicOnly.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterLatestBlock.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterWithBlockNumber.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeGreaterThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeLessThan.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_invalidParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_latest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_00.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_01.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_02.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_intOverflow.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_00.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_01.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_null.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_wrongParamType.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_00.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_01.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_earliestNull.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_invalidParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_latest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_null.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_pendingNull.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_addressReceiver.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_contractCreation.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidHashAndIndex.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidParams.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_null.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_typeMismatch.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_blockNumber.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_earliest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_illegalRange.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_latest.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_missingArgument.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_contractAddress.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_logs.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_nullContractAddress.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newBlockFilter.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newPendingTransactionFilter.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_contractCreation.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidByteValueHex.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidRawTransaction.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_messageCall.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_transferEther.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_unsignedTransaction.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdNegative.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdTooLong.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_NonexistentFilter.json create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks create mode 100755 ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json create mode 100755 ethereum/mock-p2p/build.gradle create mode 100755 ethereum/mock-p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/testing/MockNetwork.java create mode 100755 ethereum/mock-p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/testing/MockNetworkTest.java create mode 100755 ethereum/p2p/build.gradle create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkMemoryPool.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkRunner.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/DisconnectCallback.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/Message.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/MessageData.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/P2PNetwork.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/PeerConnection.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/ProtocolManager.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/DiscoveryConfiguration.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/NetworkingConfiguration.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/RlpxConfiguration.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/SubProtocolConfiguration.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/WireProtocolConfig.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/DiscoveryPeer.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgent.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryEvent.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketDecodingException.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryServiceException.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryStatus.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Bucket.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/FindNeighborsPacketData.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/NeighborsPacketData.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Packet.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketData.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketType.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerRequirement.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PingPacketData.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PongPacketData.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/RetryDelayFunction.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/AbstractHandshakeHandler.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/ApiHandler.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/Callbacks.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexer.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/DeFramer.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerInbound.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerOutbound.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/MessageFramer.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnection.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/OutboundMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/TimeoutHandler.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/WireKeepAlive.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/exceptions/IncompatiblePeerException.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeer.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeerId.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Endpoint.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Peer.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklist.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerId.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/CompressionException.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Compressor.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Framer.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramingException.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressor.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeException.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeSecrets.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/Handshaker.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESEncryptionEngine.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV1.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV4.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/utils/ByteBufUtils.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/AbstractMessageData.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/Capability.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/DefaultMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/PeerInfo.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/RawMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/SubProtocol.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/WireProtocolException.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/DisconnectMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/EmptyMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/HelloMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PingMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PongMessage.java create mode 100755 ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/WireMessageCodes.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingTestHelper.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/AbstractPeerDiscoveryTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgentTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBondingTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBootstrappingTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryObserversTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketPcapSedesTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketSedesTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTestHelper.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTimestampsTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/BucketTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/MockPacketDataFactory.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryTableRefreshTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTableTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexerTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnectionTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklistTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramerTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressorTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessageTest.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4Test.java create mode 100755 ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/wire/WireMessagesSedesTest.java create mode 100755 ethereum/p2p/src/test/resources/log4j2.xml create mode 100755 ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.initiatormessage create mode 100755 ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.keypair create mode 100755 ethereum/p2p/src/test/resources/peer1.json create mode 100755 ethereum/p2p/src/test/resources/peer2.json create mode 100755 ethereum/p2p/src/test/resources/udp.pcap create mode 100755 ethereum/referencetests/build.gradle create mode 100755 ethereum/rlp/build.gradle create mode 100755 ethereum/rlp/src/jmh/java/net/consensys/pantheon/ethereum/rlp/RLPBench.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPInput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPOutput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/CorruptedRLPInputException.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/FileRLPInput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/MalformedRLPInputException.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLP.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPDecodingHelpers.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPEncodingHelpers.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPException.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPInput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPOutput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RlpUtils.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPInput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPOutput.java create mode 100755 ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/package-info.java create mode 100755 ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java create mode 100755 ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java create mode 100755 ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTest.java create mode 100755 ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTestCaseSpec.java create mode 100755 ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTest.java create mode 100755 ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTestCaseSpec.java create mode 100755 ethereum/rlp/src/test/resources/net/consensys/pantheon/ethereum/rlp/invalidRLPTest.json create mode 100755 ethereum/trie/build.gradle create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/BranchNode.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CommitVisitor.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CompactEncoding.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/DefaultNodeFactory.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/ExtensionNode.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/GetVisitor.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/KeyValueMerkleStorage.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/LeafNode.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerklePatriciaTrie.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorage.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorageException.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/Node.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeFactory.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeLoader.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeUpdater.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeVisitor.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NullNode.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PathNodeVisitor.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PutVisitor.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/RemoveVisitor.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StorageEntriesCollector.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNode.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNodeFactory.java create mode 100755 ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/TrieIterator.java create mode 100755 ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/CompactEncodingTest.java create mode 100755 ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java create mode 100755 ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java create mode 100755 ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieIteratorTest.java create mode 100755 ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTest.java create mode 100755 ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTestCaseSpec.java create mode 100755 gradle.properties create mode 100755 gradle/check-licenses.gradle create mode 100755 gradle/eclipse-java-google-style.xml create mode 100755 gradle/formatter.properties create mode 100755 gradle/versions.gradle create mode 100755 gradle/wrapper/gradle-wrapper.jar create mode 100755 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100755 gradlew.bat create mode 100755 pantheon/build.gradle create mode 100755 pantheon/libs/picocli-3.6.0-SNAPSHOT.jar create mode 100755 pantheon/src/main/java/net/consensys/pantheon/Pantheon.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/PantheonInfo.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/Runner.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/RunnerBuilder.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandler.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/ExportPublicKeySubCommand.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/ImportBlockchainSubCommand.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/ImportSubCommand.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/PantheonCommand.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/PantheonControllerBuilder.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProvider.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/VersionProvider.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/cli/custom/CorsAllowedOriginsProperty.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/controller/CliquePantheonController.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/controller/IbftPantheonController.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/controller/KeyPairUtil.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/controller/MainnetPantheonController.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/controller/PantheonController.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/management/RestfulRouteBuilder.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/util/BlockImporter.java create mode 100755 pantheon/src/main/java/net/consensys/pantheon/util/BlockchainImporter.java create mode 100755 pantheon/src/main/resources/log4j2.xml create mode 100755 pantheon/src/test/java/net/consensys/pantheon/RunnerTest.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/cli/CommandTestAbstract.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/cli/ExportPublicKeySubCommandTest.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/cli/ImportSubCommandTest.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/cli/PantheonCommandTest.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProviderTest.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/util/BlockImporterTest.java create mode 100755 pantheon/src/test/java/net/consensys/pantheon/util/BlockchainImporterTest.java create mode 100755 pantheon/src/test/resources/complete_config.toml create mode 100755 pantheon/src/test/resources/ibft.blocks create mode 100755 pantheon/src/test/resources/ibft_genesis.json create mode 100755 pantheon/src/test/resources/log4j2.xml create mode 100755 pantheon/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/json-rpc-test.bin create mode 100755 pantheon/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json create mode 100755 pantheon/src/test/resources/partial_config.toml create mode 100755 quickstart/README.md create mode 100755 quickstart/docker-compose.yml create mode 100755 quickstart/explorer/App.js.patch create mode 100755 quickstart/explorer/Dockerfile create mode 100755 quickstart/explorer/favicon.png create mode 100755 quickstart/explorer/index.html.patch create mode 100755 quickstart/explorer/index.js.patch create mode 100755 quickstart/explorer/init.js.patch create mode 100755 quickstart/listQuickstartServices.sh create mode 100755 quickstart/pantheon/.gitignore create mode 100755 quickstart/pantheon/Dockerfile create mode 100755 quickstart/pantheon/bootnode_start.sh create mode 100755 quickstart/pantheon/node_start.sh create mode 100755 quickstart/removePantheonPrivateNetwork.sh create mode 100755 quickstart/runPantheonPrivateNetwork.sh create mode 100755 services/build.gradle create mode 100755 services/kvstore/build.gradle create mode 100755 services/kvstore/src/main/java/net/consensys/pantheon/services/kvstore/InMemoryKeyValueStorage.java create mode 100755 services/kvstore/src/main/java/net/consensys/pantheon/services/kvstore/KeyValueStorage.java create mode 100755 services/kvstore/src/main/java/net/consensys/pantheon/services/kvstore/RocksDbKeyValueStorage.java create mode 100755 services/kvstore/src/main/resources/log4j2.xml create mode 100755 services/kvstore/src/test/java/net/consensys/pantheon/services/kvstore/AbstractKeyValueStorageTest.java create mode 100755 services/kvstore/src/test/java/net/consensys/pantheon/services/kvstore/InMemoryKeyValueStorageTest.java create mode 100755 services/kvstore/src/test/java/net/consensys/pantheon/services/kvstore/RocksDbKeyValueStorageTest.java create mode 100755 settings.gradle create mode 100755 system-tests/build.gradle create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/ClusterTestBase.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/LogClusterInfoTest.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/PantheonSmokeTest.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/cluster/DockerUtils.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/cluster/NodeAdminRpcUtils.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/cluster/TestCluster.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/cluster/TestClusterNode.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/cluster/TestDockerNode.java create mode 100755 system-tests/src/test/java/net/consensys/pantheon/tests/cluster/TestGethLocalNode.java create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/Dockerfile-geth create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/Dockerfile-geth-ubuntu create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/bash_aliases create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/genesis.json create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/gethUtils.sh create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/run.sh create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/runBootNode.sh create mode 100755 system-tests/src/test/resources/net/consensys/pantheon/tests/ibft.json create mode 100755 testutil/build.gradle create mode 100755 testutil/src/main/java/net/consensys/pantheon/testutil/BlockTestUtil.java create mode 100755 testutil/src/main/java/net/consensys/pantheon/testutil/JsonTestParameters.java create mode 100755 testutil/src/main/resources/1000.blocks create mode 120000 testutil/src/main/resources/log4j2.xml create mode 100755 util/build.gradle create mode 100755 util/src/main/java/net/consensys/pantheon/util/ExceptionUtils.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/NetworkUtility.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/Preconditions.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/Subscribers.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/AbstractBytes32Backed.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/AbstractBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/ArrayWrappingBytes32.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/ArrayWrappingBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/BaseDelegatingBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/Bytes32.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/Bytes32Backed.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/Bytes32s.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/BytesBacked.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/BytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/BytesValues.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/DelegatingBytes32.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/DelegatingBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/MutableArrayWrappingBytes32.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/MutableArrayWrappingBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/MutableBufferWrappingBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/MutableByteBufWrappingBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/MutableBytes32.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/MutableBytesValue.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/bytes/WrappingBytes32.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/time/Clock.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/time/SystemClock.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/AbstractUInt256Value.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/BaseUInt256Value.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/Counter.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/DefaultInt256.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/DefaultUInt256.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/Int256.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/Int256Bytes.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/UInt256.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/UInt256Bytes.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/UInt256Value.java create mode 100755 util/src/main/java/net/consensys/pantheon/util/uint/UInt256s.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/ExceptionUtilsTest.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/NetworkUtilityTest.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/SubscribersTest.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/bytes/Bytes32Test.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/bytes/Bytes32sSingleLeftShiftTest.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/bytes/BytesValueTest.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/bytes/BytesValuesTest.java create mode 100755 util/src/test/java/net/consensys/pantheon/util/uint/UInt256BytesTest.java create mode 100755 versions.gradle diff --git a/.dockerignore b/.dockerignore new file mode 100755 index 00000000000..442cbc37b48 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.gradle +.idea +.vertx +build \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 00000000000..4a54e3bbb39 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text eol=lf +*.jar -text +*.bat -text +*.pcap binary +*.blocks binary diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000000..d001131c932 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.bak +*.swp +*.tmp +*~.nib +*.iml +*.launch +*.swp +*.log +.classpath +.DS_Store +.externalToolBuilders/ +.gradle/ +.idea/ +.loadpath +.metadata +.prefs +.project +.recommenders/ +.settings +.springBeans +.vertx +bin/ +local.properties +target/ +tmp/ +build/ +out/ diff --git a/.gitmodules b/.gitmodules new file mode 100755 index 00000000000..0efd88e0991 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "eth-ref-tests"] + path = ethereum/referencetests/src/test/resources + url = https://github.com/ethereum/tests.git + ignore = all diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 00000000000..deffa11ccf9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to Pantheon + +Welcome to the Pantheon repository! This document describes the procedure and guidelines for contributing to the Pantheon project. The subsequent sections encapsulate the criteria used to evaluate additions to, and modifications of, the existing codebase. + +## Contributor Workflow + +The codebase is maintained using the "*contributor workflow*" where everyone without exception contributes patch proposals using "*pull-requests*". This facilitates social contribution, easy testing and peer review. + +To contribute a patch, the workflow is as follows: + +* Fork repository +* Create topic branch +* Commit patch +* Create pull-request, adhering to the coding conventions herein set forth + +In general a commit serves a single purpose and diffs should be easily comprehensible. For this reason do not mix any formatting fixes or code moves with actual code changes. + +## Style Guide + +`La mode se démode, le style jamais.` + +Guided by the immortal words of Gabrielle Bonheur, we strive to adhere strictly to best stylistic practices for each line of code in this software. + +At this stage one should expect comments and reviews from fellow contributors. You can add more commits to your pull request by committing them locally and pushing to your fork until you have satisfied all feedback. That being said, before the pull request is merged, it should be squashed. + +#### Stylistic + +The fundamental resource Pantheon contributors should familiarize themselves with is Oracle's [Code Conventions for the Java TM Programming Language](http://www.oracle.com/technetwork/java/codeconvtoc-136057.html), to establish a general programme on Java coding. Furthermore, all pull-requests should be formatted according to the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html), as the the [Google Java Style reformatter](https://github.com/google/google-java-format) is a component piece of our continuous integration architecture, code that does not comply stylistically will fail to pass the requisite candidate tests. + +#### Architectural Best Practices + +Questions on architectural best practices will be guided by the principles set forth in [Effective Java](http://index-of.es/Java/Effective%20Java.pdf) by Joshua Bloch + +#### Clear Commit/PR Messages + +Commit messages should be verbose by default consisting of a short subject line (50 chars max), a blank line and detailed explanatory text as separate paragraph(s), unless the title alone is self-explanatory (such as "`Implement EXP EVM opcode`") in which case a single title line is sufficient. Commit messages should be helpful to people reading your code in the future, so explain the reasoning for your decisions. Further explanation on commit messages can be found [here](https://chris.beams.io/posts/git-commit/). + +#### Test coverage + +The test cases are sufficient enough to provide confidence in the code’s robustness, while avoiding redundant tests. + +#### Readability + +The code is easy to understand. + +#### Simplicity + +The code is not over-engineered, sufficient effort is made to minimize the cyclomatic complexity of the software. + +#### Functional + +Insofar as is possible the code intuitively and expeditiously executes the designated task. + +#### Clean + +The code is free from glaring typos (*e.g. misspelled comments*), thinkos, or formatting issues (*e.g. incorrect indentation*). + +#### Appropriately Commented + +Ambiguous or unclear code segments are commented. The comments are written in complete sentences. diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 00000000000..52e22518394 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +# Base Alpine Linux based image with OpenJDK JRE only +#FROM openjdk:8-jre-alpine +FROM openjdk:8-jdk + +# copy application (with libraries inside) +ADD build/install/pantheon /opt/pantheon/ +ADD integration-tests/src/test/resources/net/consensys/pantheon/tests/cluster/docker/geth/genesis.json /opt/pantheon/genesis.json + +# List Exposed Ports +EXPOSE 8084 8545 30303 30303/udp + +# specify default command +ENTRYPOINT ["/opt/pantheon/bin/pantheon"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100755 index 00000000000..7ba8f4b454f --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,69 @@ +#!/usr/bin/env groovy + +if (env.BRANCH_NAME == "master") { + properties([ + buildDiscarder( + logRotator( + daysToKeepStr: '90' + ) + ) + ]) +} else { + properties([ + buildDiscarder( + logRotator( + numToKeepStr: '10' + ) + ) + ]) +} + +node { + checkout scm + docker.image('docker:18.06.0-ce-dind').withRun('--privileged') { d -> + docker.image('openjdk:8u181-jdk').inside("--link ${d.id}:docker") { + try { + stage('Compile') { + sh './gradlew --no-daemon --parallel clean compileJava' + } + stage('compile tests') { + sh './gradlew --no-daemon --parallel compileTestJava' + } + stage('assemble') { + sh './gradlew --no-daemon --parallel assemble' + } + stage('Build') { + sh './gradlew --no-daemon --parallel build' + } + stage('Reference tests') { + sh './gradlew --no-daemon --parallel referenceTest' + } + stage('Integration Tests') { + sh './gradlew --no-daemon --parallel integrationTest' + } + stage('Acceptance Tests') { + sh './gradlew --no-daemon --parallel acceptanceTest --tests Web3Sha3AcceptanceTest --tests PantheonClusterAcceptanceTest --tests MiningAcceptanceTest' + } + stage('Check Licenses') { + sh './gradlew --no-daemon --parallel checkLicenses' + } + stage('Check javadoc') { + sh './gradlew --no-daemon --parallel javadoc' + } + // stage('Smoke test') { + // sh 'DOCKER_HOST=$DOCKER_PORT DOCKER_HOSTNAME=docker ./gradlew --no-daemon smokeTest' + // } + stage('Jacoco root report') { + sh './gradlew --no-daemon jacocoRootReport' + } + } finally { + archiveArtifacts '**/build/reports/**' + archiveArtifacts '**/build/test-results/**' + archiveArtifacts 'build/reports/**' + archiveArtifacts 'build/distributions/**' + + junit '**/build/test-results/**/*.xml' + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100755 index 00000000000..8dada3edaf5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/README.md b/README.md new file mode 100755 index 00000000000..69e7b9be6e0 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# Pantheon Ethereum Client · [![Build Status](https://circleci.com/gh/ConsenSys/pantheon.svg?style=shield&circle-token=fe99ba1f7e99c65632a1b1ae69a821ef52ee9bc4)](https://circleci.com/gh/ConsenSys/pantheon) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ConsenSys/pantheon/blob/master/LICENSE) + +## Pantheon Users + +The process for building and running Pantheon as a user is different to when developing. All user documentation is on our Wiki and some processes are different to those described in this Readme. + +### Build, Install, and Run Pantheon + +Building, installing, and running Pantheon is described in the Wiki: +* [Build and Install](https://github.com/ConsenSys/pantheon/wiki/Installation) +* [Quickstart](https://github.com/ConsenSys/pantheon/wiki/Quickstart) + +### Documentation + +User and reference documentation available on the Wiki includes: +* [Command Line Options](https://github.com/ConsenSys/pantheon/wiki/Pantheon-CLI-Syntax) +* [https://github.com/ConsenSys/pantheon/wiki/JSON-RPC-API](https://github.com/ConsenSys/pantheon/wiki/JSON-RPC-API) +* [Docker Quickstart Tutorial](https://github.com/ConsenSys/pantheon/wiki/Docker-Quickstart) + +## Pantheon Developers + +## Build Instructions + +To build, clone this repo and run with `./gradlew` like so: + +``` +git clone --recursive https://github.com/ConsenSys/pantheon +cd pantheon +./gradlew +``` + +After a successful build, distribution packages will be available in `build/distribution`. + +## Code Style + +We use Google's Java coding conventions for the project. To reformat code, run: + +``` +./gradlew spotlessApply +``` + +Code style will be checked automatically during a build. + +## Testing + +All the unit tests are run as part of the build, but can be explicitly triggered with: +``` +./gradlew test +``` +The integration tests can be triggered with: +``` +./gradlew integrationTest +``` + +The reference tests (described below) can be triggered with: +``` +./gradlew referenceTest +``` +The system tests can be triggered with: +``` +./gradlew smokeTest +``` + +## Running Pantheon + +You can build and run Pantheon with default options via: + +``` +./gradlew run +``` + +By default this stores all persistent data in `build/pantheon`. + +If you want to set custom CLI arguments for the Pantheon execution, you can use the property `pantheon.run.args` like e.g.: + +```sh +./gradlew run -Ppantheon.run.args="--discovery=false --home=/tmp/pantheontmp" +``` + +which will pass `--discovery=false` and `--home=/tmp/pantheontmp` to the invocation. + +### Ethereum reference tests + +On top of the project proper unit tests, specific unit tests are provided to +run the Ethereum reference tests available at https://github.com/ethereum/tests +and described at http://ethereum-tests.readthedocs.io/en/latest/. Those are run +as part of the unit test suite as described above, but for debugging, it is +often convenient to run only a subset of those tests, for which a few convenience +as provided. For instance, one can run only "Frontier" general state tests with +``` +./gradlew :ethereum:net.consensys.pantheon.ethereum.vm:referenceTest -Dtest.single=GeneralStateTest -Dtest.ethereum.state.eip=Frontier +``` +or only the tests that match a particular pattern with something like: +``` +gradle :ethereum:net.consensys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.include='^CALLCODE.*-Frontier' +``` +Please see the comment on the `test` target in the top level `build.gradle` +file for more details. + +### Logging + +This project employs the logging utility [Apache Log4j](https://logging.apache.org/log4j/2.x/), +accordingly levels of detail can be specified as follows: + +``` +ALL: All levels including custom levels. +DEBUG: Designates fine-grained informational events that are most useful to debug an application. +ERROR: Designates error events that might still allow the application to continue running. +FATAL: Designates very severe error events that will presumably lead the application to abort. +INFO: Designates informational messages that highlight the progress of the application at coarse-grained level. +OFF: The highest possible rank and is intended to turn off logging. +TRACE: Designates finer-grained informational events than the DEBUG. +WARN: Designates potentially harmful situations. +``` + +One mechanism of globally effecting the log output of a running client is though modification the file +`/pantheon/src/main/resources/log4j2.xml`, where it can be specified under the ``. +As such, corresponding instances of information logs throughout the codebase, e.g. `log.fatal("Fatal Message!");`, +will be rendered to the console while the client is in use. + +## Contribution + +Welcome to the Pantheon Ethereum project repo. If you would like to help contribute +code to the project, please fork, commit and send us a pull request. + +Please read the [Contribution guidelines](docs/CONTRIBUTORS.md) for this project. diff --git a/acceptance-tests/build.gradle b/acceptance-tests/build.gradle new file mode 100755 index 00000000000..97c7c048880 --- /dev/null +++ b/acceptance-tests/build.gradle @@ -0,0 +1,33 @@ +dependencies { + + testRuntime 'org.apache.logging.log4j:log4j-core' + testRuntime 'org.apache.logging.log4j:log4j-slf4j-impl' + + testImplementation project(':crypto') + testImplementation project(':ethereum:client') + testImplementation project(':ethereum:eth') + testImplementation project(':ethereum:core') + testImplementation project(':ethereum:jsonrpc') + testImplementation project(':pantheon') + testImplementation project(':util') + + testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts') + testImplementation 'junit:junit' + testImplementation 'org.awaitility:awaitility' + testImplementation 'com.squareup.okhttp3:okhttp' + testImplementation 'io.vertx:vertx-core' + testImplementation 'org.apache.logging.log4j:log4j-api' + testImplementation 'org.assertj:assertj-core' + testImplementation 'com.google.guava:guava' + testImplementation 'org.web3j:core' +} + +test.enabled = false + +task acceptanceTest(type: Test) { + System.setProperty('acctests.runPantheonAsProcess', 'true') + mustRunAfter rootProject.subprojects*.test + description = 'Runs Pantheon acceptance tests.' + group = 'verification' +} +acceptanceTest.dependsOn(rootProject.installDist) diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/CreateAccountAcceptanceTest.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/CreateAccountAcceptanceTest.java new file mode 100755 index 00000000000..8e5270bf76e --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/CreateAccountAcceptanceTest.java @@ -0,0 +1,32 @@ +package net.consensys.pantheon.tests.acceptance; + +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode; +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode; +import static org.web3j.utils.Convert.Unit.ETHER; + +import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import net.consensys.pantheon.tests.acceptance.dsl.account.Account; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import org.junit.Before; +import org.junit.Test; + +public class CreateAccountAcceptanceTest extends AcceptanceTestBase { + + private PantheonNode minerNode; + private PantheonNode fullNode; + + @Before + public void setUp() throws Exception { + minerNode = cluster.create(pantheonMinerNode("node1")); + fullNode = cluster.create(pantheonNode("node2")); + cluster.start(minerNode, fullNode); + } + + @Test + public void shouldCreateAnAccount() { + final Account account = accounts.createAccount("account1", "20", ETHER, fullNode); + accounts.waitForAccountBalance(account, "20", ETHER, minerNode); + accounts.waitForAccountBalance(account, "20", ETHER, fullNode); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/PantheonClusterAcceptanceTest.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/PantheonClusterAcceptanceTest.java new file mode 100755 index 00000000000..f9648d303f4 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/PantheonClusterAcceptanceTest.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.tests.acceptance; + +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode; +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode; + +import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import org.junit.Before; +import org.junit.Test; + +public class PantheonClusterAcceptanceTest extends AcceptanceTestBase { + + private PantheonNode minerNode; + private PantheonNode fullNode; + + @Before + public void setUp() throws Exception { + minerNode = cluster.create(pantheonMinerNode("node1")); + fullNode = cluster.create(pantheonNode("node2")); + cluster.start(minerNode, fullNode); + } + + @Test + public void shouldConnectToOtherPeer() { + jsonRpc.waitForPeersConnected(minerNode, 1); + jsonRpc.waitForPeersConnected(fullNode, 1); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/RpcApisTogglesAcceptanceTest.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/RpcApisTogglesAcceptanceTest.java new file mode 100755 index 00000000000..1df51e3ed64 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/RpcApisTogglesAcceptanceTest.java @@ -0,0 +1,67 @@ +package net.consensys.pantheon.tests.acceptance; + +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode; +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonRpcDisabledNode; +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.patheonNodeWithRpcApis; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import org.junit.Before; +import org.junit.Test; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.exceptions.ClientConnectionException; + +public class RpcApisTogglesAcceptanceTest extends AcceptanceTestBase { + + private PantheonNode rpcEnabledNode; + private PantheonNode rpcDisabledNode; + private PantheonNode ethApiDisabledNode; + + @Before + public void before() throws Exception { + rpcEnabledNode = cluster.create(pantheonNode("rpc-enabled")); + rpcDisabledNode = cluster.create(pantheonRpcDisabledNode("rpc-disabled")); + ethApiDisabledNode = cluster.create(patheonNodeWithRpcApis("eth-api-disabled", RpcApis.NET)); + cluster.start(rpcEnabledNode, rpcDisabledNode, ethApiDisabledNode); + } + + @Test + public void shouldSucceedConnectingToNodeWithJsonRpcEnabled() { + rpcEnabledNode.verifyJsonRpcEnabled(); + } + + @Test + public void shouldFailConnectingToNodeWithJsonRpcDisabled() { + rpcDisabledNode.verifyJsonRpcDisabled(); + } + + @Test + public void shouldSucceedConnectingToNodeWithWsRpcEnabled() { + rpcEnabledNode.verifyWsRpcEnabled(); + } + + @Test + public void shouldFailConnectingToNodeWithWsRpcDisabled() { + rpcDisabledNode.verifyWsRpcDisabled(); + } + + @Test + public void shouldSucceedCallingMethodFromEnabledApiGroup() throws Exception { + final Web3j web3j = ethApiDisabledNode.web3j(); + + assertThat(web3j.netVersion().send().getError()).isNull(); + } + + @Test + public void shouldFailCallingMethodFromDisabledApiGroup() { + final Web3j web3j = ethApiDisabledNode.web3j(); + + assertThat(catchThrowable(() -> web3j.ethAccounts().send())) + .isInstanceOf(ClientConnectionException.class) + .hasMessageContaining("Invalid response received: 400"); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java new file mode 100755 index 00000000000..da306ee0a04 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/AcceptanceTestBase.java @@ -0,0 +1,18 @@ +package net.consensys.pantheon.tests.acceptance.dsl; + +import net.consensys.pantheon.tests.acceptance.dsl.account.Accounts; +import net.consensys.pantheon.tests.acceptance.dsl.node.Cluster; + +import org.junit.After; + +public class AcceptanceTestBase { + + protected Cluster cluster = new Cluster(); + protected Accounts accounts = new Accounts(); + protected JsonRpc jsonRpc = new JsonRpc(cluster); + + @After + public void tearDownAcceptanceTestBase() throws Exception { + cluster.close(); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/JsonRpc.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/JsonRpc.java new file mode 100755 index 00000000000..30ee9584aef --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/JsonRpc.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.tests.acceptance.dsl; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.tests.acceptance.dsl.node.Cluster; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +public class JsonRpc { + + private final Cluster nodes; + + public JsonRpc(final Cluster nodes) { + this.nodes = nodes; + } + + public void waitForPeersConnected(final PantheonNode node, final int expectedNumberOfPeers) { + WaitUtils.waitFor(() -> assertThat(node.getPeerCount()).isEqualTo(expectedNumberOfPeers)); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/WaitUtils.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/WaitUtils.java new file mode 100755 index 00000000000..c1b7a6138d4 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/WaitUtils.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.tests.acceptance.dsl; + +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.awaitility.core.ThrowingRunnable; + +public class WaitUtils { + public static void waitFor(final ThrowingRunnable condition) { + Awaitility.await().ignoreExceptions().atMost(30, TimeUnit.SECONDS).untilAsserted(condition); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Account.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Account.java new file mode 100755 index 00000000000..0d4e9582dc0 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Account.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.tests.acceptance.dsl.account; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.PrivateKey; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.math.BigInteger; + +import org.web3j.crypto.Credentials; + +public class Account { + + private final String name; + private final KeyPair keyPair; + private long nonce = 0; + + private Account(final String name, final KeyPair keyPair) { + this.name = name; + this.keyPair = keyPair; + } + + public static Account create(final String name) { + return new Account(name, KeyPair.generate()); + } + + public static Account fromPrivateKey(final String name, final String privateKey) { + return new Account(name, KeyPair.create(PrivateKey.create(Bytes32.fromHexString(privateKey)))); + } + + public Credentials web3jCredentials() { + return Credentials.create( + keyPair.getPrivateKey().toString(), keyPair.getPublicKey().toString()); + } + + public String getAddress() { + return Address.extract(Hash.hash(keyPair.getPublicKey().getEncodedBytes())).toString(); + } + + public BigInteger getNextNonce() { + return BigInteger.valueOf(nonce++); + } + + public void setNextNonce(final long nonce) { + this.nonce = nonce; + } + + public String getName() { + return name; + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Accounts.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Accounts.java new file mode 100755 index 00000000000..19a220cbdc8 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/account/Accounts.java @@ -0,0 +1,108 @@ +package net.consensys.pantheon.tests.acceptance.dsl.account; + +import static net.consensys.pantheon.tests.acceptance.dsl.WaitUtils.waitFor; +import static org.apache.logging.log4j.LogManager.getLogger; +import static org.assertj.core.api.Assertions.assertThat; +import static org.web3j.utils.Convert.Unit.ETHER; +import static org.web3j.utils.Convert.toWei; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.web3j.utils.Convert.Unit; + +public class Accounts { + + private static final Logger LOG = getLogger(); + + private final Account richBenefactorOne; + private final Account richBenefactorTwo; + + public Accounts() { + richBenefactorOne = + Account.fromPrivateKey( + "Rich Benefactor One", + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"); + richBenefactorTwo = + Account.fromPrivateKey( + "Rich Benefactor Two", + "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3"); + } + + public Account createAccount( + final String accountName, + final String initialBalance, + final Unit initialBalanceUnit, + final PantheonNode createOnNode) { + final Account account = Account.create(accountName); + createOnNode.transferFunds(richBenefactorOne, account, initialBalance, initialBalanceUnit); + + return account; + } + + public Account createAccount(final String accountName) { + return Account.create(accountName); + } + + public void waitForAccountBalance( + final Account account, + final String expectedBalance, + final Unit balanceUnit, + final PantheonNode node) { + LOG.info( + "Waiting for {} to have a balance of {} {} on node {}", + account.getName(), + expectedBalance, + balanceUnit, + node.getName()); + + waitFor( + () -> + assertThat(node.getAccountBalance(account)) + .isEqualTo(toWei(expectedBalance, balanceUnit).toBigIntegerExact())); + } + + public void waitForAccountBalance( + final Account account, final int etherAmount, final PantheonNode node) { + waitForAccountBalance(account, String.valueOf(etherAmount), ETHER, node); + } + + public Hash transfer(final Account recipient, final int amount, final PantheonNode node) { + return transfer(richBenefactorOne, recipient, amount, node); + } + + public Hash transfer( + final Account sender, final Account recipient, final int amount, final PantheonNode node) { + return node.transferFunds(sender, recipient, String.valueOf(amount), Unit.ETHER); + } + + /** + * Transfer funds in separate transactions (1 eth increments). This is a strategy to increase the + * total of transactions. + * + * @param fromAccount account sending the ether value + * @param toAccount account receiving the ether value + * @param etherAmount amount of ether to transfer + * @return a list with the hashes of each transaction + */ + public List incrementalTransfer( + final Account fromAccount, + final Account toAccount, + final int etherAmount, + final PantheonNode node) { + final List txHashes = new ArrayList<>(); + for (int i = 1; i <= etherAmount; i++) { + final Hash hash = node.transferFunds(fromAccount, toAccount, String.valueOf(1), Unit.ETHER); + txHashes.add(hash); + } + return txHashes; + } + + public Account getSecondaryBenefactor() { + return richBenefactorTwo; + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Cluster.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Cluster.java new file mode 100755 index 00000000000..33d8173a7ed --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Cluster.java @@ -0,0 +1,102 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import static net.consensys.pantheon.tests.acceptance.dsl.WaitUtils.waitFor; +import static org.assertj.core.api.Assertions.assertThat; +import static org.web3j.utils.Convert.toWei; + +import net.consensys.pantheon.tests.acceptance.dsl.WaitUtils; +import net.consensys.pantheon.tests.acceptance.dsl.account.Account; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.web3j.utils.Convert.Unit; + +public class Cluster implements AutoCloseable { + + private static final Logger LOG = LogManager.getLogger(Cluster.class); + + private final Map nodes = new HashMap<>(); + private final PantheonNodeRunner pantheonNodeRunner = PantheonNodeRunner.instance(); + + public void start(final PantheonNode... nodes) { + this.nodes.clear(); + + final List bootNodes = new ArrayList<>(); + + for (final PantheonNode node : nodes) { + this.nodes.put(node.getName(), node); + bootNodes.add(node.enodeUrl()); + } + + for (final PantheonNode node : nodes) { + node.bootnodes(bootNodes); + node.start(pantheonNodeRunner); + } + + for (final PantheonNode node : nodes) { + awaitPeerDiscovery(node, nodes.length); + } + } + + public void stop() { + for (final PantheonNode node : nodes.values()) { + node.stop(); + } + pantheonNodeRunner.shutdown(); + } + + @Override + public void close() { + for (final PantheonNode node : nodes.values()) { + node.close(); + } + pantheonNodeRunner.shutdown(); + } + + public PantheonNode create(final PantheonNodeConfig config) throws IOException { + config.initSocket(); + final PantheonNode node = + new PantheonNode( + config.getName(), + config.getSocketPort(), + config.getMiningParameters(), + config.getJsonRpcConfiguration(), + config.getWebSocketConfiguration()); + config.closeSocket(); + return node; + } + + private void awaitPeerDiscovery(final PantheonNode node, final int nodeCount) { + if (node.jsonRpcEnabled()) { + WaitUtils.waitFor(() -> assertThat(node.getPeerCount()).isEqualTo(nodeCount - 1)); + } + } + + public void awaitPropagation(final Account account, final int expectedBalance) { + awaitPropagation(account, String.valueOf(expectedBalance), Unit.ETHER); + } + + public void awaitPropagation( + final Account account, final String expectedBalance, final Unit balanceUnit) { + + for (final PantheonNode node : nodes.values()) { + LOG.info( + "Waiting for {} to have a balance of {} {} on node {}", + account.getName(), + expectedBalance, + balanceUnit, + node.getName()); + + waitFor( + () -> + assertThat(node.getAccountBalance(account)) + .isEqualTo(toWei(expectedBalance, balanceUnit).toBigIntegerExact())); + } + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Eth.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Eth.java new file mode 100755 index 00000000000..ba5f6e60cb9 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Eth.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.math.BigInteger; + +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.EthBlockNumber; + +public class Eth { + + private final Web3j web3j; + + public Eth(final Web3j web3) { + this.web3j = web3; + } + + public BigInteger blockNumber() throws IOException { + final EthBlockNumber result = web3j.ethBlockNumber().send(); + assertThat(result).isNotNull(); + assertThat(result.hasError()).isFalse(); + return result.getBlockNumber(); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNode.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNode.java new file mode 100755 index 00000000000..83088e4705b --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNode.java @@ -0,0 +1,350 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import static org.apache.logging.log4j.LogManager.getLogger; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.web3j.protocol.core.DefaultBlockParameterName.LATEST; +import static org.web3j.utils.Numeric.toHexString; + +import net.consensys.pantheon.controller.KeyPairUtil; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import net.consensys.pantheon.tests.acceptance.dsl.account.Account; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.net.ConnectException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Collectors; + +import com.google.common.base.MoreObjects; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import org.apache.logging.log4j.Logger; +import org.java_websocket.exceptions.WebsocketNotConnectedException; +import org.web3j.crypto.RawTransaction; +import org.web3j.crypto.TransactionEncoder; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.Web3jService; +import org.web3j.protocol.core.methods.response.EthGetBalance; +import org.web3j.protocol.http.HttpService; +import org.web3j.protocol.websocket.WebSocketService; +import org.web3j.utils.Async; +import org.web3j.utils.Convert; +import org.web3j.utils.Convert.Unit; + +public class PantheonNode implements AutoCloseable { + + private static final String LOCALHOST = "127.0.0.1"; + private static final Logger LOG = getLogger(); + private static final BigInteger MINIMUM_GAS_PRICE = BigInteger.valueOf(1000); + private static final BigInteger TRANSFER_GAS_COST = BigInteger.valueOf(21000); + + private final String name; + private final Path homeDirectory; + private final KeyPair keyPair; + private final int p2pPort; + private final MiningParameters miningParameters; + private final JsonRpcConfiguration jsonRpcConfiguration; + private final WebSocketConfiguration webSocketConfiguration; + private final boolean jsonRpcEnabled; + private final boolean wsRpcEnabled; + private final Properties portsProperties = new Properties(); + + private List bootnodes = new ArrayList<>(); + private Eth eth; + private Web3 web3; + private Web3j web3j; + + public PantheonNode( + final String name, + final int p2pPort, + final MiningParameters miningParameters, + final JsonRpcConfiguration jsonRpcConfiguration, + final WebSocketConfiguration webSocketConfiguration) + throws IOException { + this.name = name; + this.homeDirectory = Files.createTempDirectory("acctest"); + this.keyPair = KeyPairUtil.loadKeyPair(homeDirectory); + this.p2pPort = p2pPort; + this.miningParameters = miningParameters; + this.jsonRpcConfiguration = jsonRpcConfiguration; + this.webSocketConfiguration = webSocketConfiguration; + this.jsonRpcEnabled = jsonRpcConfiguration.isEnabled(); + this.wsRpcEnabled = webSocketConfiguration.isEnabled(); + LOG.info("Created PantheonNode {}", this.toString()); + } + + public String getName() { + return name; + } + + String enodeUrl() { + return "enode://" + keyPair.getPublicKey().toString() + "@" + LOCALHOST + ":" + p2pPort; + } + + private Optional jsonRpcBaseUrl() { + if (jsonRpcEnabled) { + return Optional.of( + "http://" + + jsonRpcConfiguration.getHost() + + ":" + + portsProperties.getProperty("json-rpc")); + } else { + return Optional.empty(); + } + } + + private Optional wsRpcBaseUrl() { + if (wsRpcEnabled) { + return Optional.of( + "ws://" + webSocketConfiguration.getHost() + ":" + portsProperties.getProperty("ws-rpc")); + } else { + return Optional.empty(); + } + } + + public Optional jsonRpcWebSocketPort() { + if (wsRpcEnabled) { + return Optional.of(Integer.valueOf(portsProperties.getProperty("ws-rpc"))); + } else { + return Optional.empty(); + } + } + + public String getHost() { + return LOCALHOST; + } + + @Deprecated + public Web3j web3j() { + if (!jsonRpcBaseUrl().isPresent()) { + throw new IllegalStateException( + "Can't create a web3j instance for a node with RPC disabled."); + } + + if (web3j == null) { + return web3j(new HttpService(jsonRpcBaseUrl().get())); + } + + return web3j; + } + + @Deprecated + public Web3j web3j(final Web3jService web3jService) { + if (web3j == null) { + web3j = Web3j.build(web3jService, 2000, Async.defaultExecutorService()); + } + + return web3j; + } + + public Hash transferFunds( + final Account from, final Account to, final String amount, final Unit unit) { + final RawTransaction transaction = + RawTransaction.createEtherTransaction( + from.getNextNonce(), + MINIMUM_GAS_PRICE, + TRANSFER_GAS_COST, + to.getAddress(), + Convert.toWei(amount, unit).toBigIntegerExact()); + final String signedTransactionData = + toHexString(TransactionEncoder.signMessage(transaction, from.web3jCredentials())); + try { + return Hash.fromHexString( + web3j().ethSendRawTransaction(signedTransactionData).send().getTransactionHash()); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public BigInteger getAccountBalance(final Account account) { + try { + final EthGetBalance balanceResponse = + web3j().ethGetBalance(account.getAddress(), LATEST).send(); + return balanceResponse.getBalance(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public int getPeerCount() { + try { + return web3j().netPeerCount().send().getQuantity().intValueExact(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void start(final PantheonNodeRunner runner) { + runner.startNode(this); + loadPortsFile(); + } + + private void loadPortsFile() { + try (FileInputStream fis = + new FileInputStream(new File(homeDirectory.toFile(), "pantheon.ports"))) { + portsProperties.load(fis); + } catch (final IOException e) { + throw new RuntimeException("Error reading Pantheon ports file", e); + } + } + + public void verifyJsonRpcEnabled() { + if (!jsonRpcBaseUrl().isPresent()) { + throw new RuntimeException("JSON-RPC is not enabled in node configuration"); + } + + try { + assertThat(web3j().netVersion().send().getError()).isNull(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void verifyJsonRpcDisabled() { + if (jsonRpcBaseUrl().isPresent()) { + throw new RuntimeException("JSON-RPC is enabled in node configuration"); + } + + final HttpService web3jService = new HttpService("http://" + LOCALHOST + ":8545"); + assertThat(catchThrowable(() -> web3j(web3jService).netVersion().send())) + .isInstanceOf(ConnectException.class) + .hasMessage("Failed to connect to /127.0.0.1:8545"); + } + + public void verifyWsRpcEnabled() { + if (!wsRpcBaseUrl().isPresent()) { + throw new RuntimeException("WS-RPC is not enabled in node configuration"); + } + + try { + final WebSocketService webSocketService = new WebSocketService(wsRpcBaseUrl().get(), true); + assertThat(web3j(webSocketService).netVersion().send().getError()).isNull(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public void verifyWsRpcDisabled() { + if (wsRpcBaseUrl().isPresent()) { + throw new RuntimeException("WS-RPC is enabled in node configuration"); + } + + final WebSocketService webSocketService = + new WebSocketService("ws://" + LOCALHOST + ":8546", true); + assertThat(catchThrowable(() -> web3j(webSocketService).netVersion().send())) + .isInstanceOf(WebsocketNotConnectedException.class); + } + + Path homeDirectory() { + return homeDirectory; + } + + boolean jsonRpcEnabled() { + return jsonRpcEnabled; + } + + JsonRpcConfiguration jsonRpcConfiguration() { + return jsonRpcConfiguration; + } + + Optional jsonRpcListenAddress() { + if (jsonRpcEnabled) { + return Optional.of(jsonRpcConfiguration.getHost() + ":" + jsonRpcConfiguration.getPort()); + } else { + return Optional.empty(); + } + } + + boolean wsRpcEnabled() { + return wsRpcEnabled; + } + + WebSocketConfiguration webSocketConfiguration() { + return webSocketConfiguration; + } + + Optional wsRpcListenAddress() { + return Optional.of(webSocketConfiguration.getHost() + ":" + webSocketConfiguration.getPort()); + } + + int p2pPort() { + return p2pPort; + } + + String p2pListenAddress() { + return LOCALHOST + ":" + p2pPort; + } + + List bootnodes() { + return bootnodes + .stream() + .filter(node -> !node.equals(this.enodeUrl())) + .collect(Collectors.toList()); + } + + void bootnodes(final List bootnodes) { + this.bootnodes = bootnodes; + } + + public MiningParameters getMiningParameters() { + return miningParameters; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("p2pPort", p2pPort) + .add("homeDirectory", homeDirectory) + .add("keyPair", keyPair) + .toString(); + } + + public void stop() { + if (web3j != null) { + web3j.shutdown(); + web3j = null; + } + + eth = null; + web3 = null; + } + + @Override + public void close() { + stop(); + try { + MoreFiles.deleteRecursively(homeDirectory, RecursiveDeleteOption.ALLOW_INSECURE); + } catch (final IOException e) { + LOG.info("Failed to clean up temporary file: {}", homeDirectory, e); + } + } + + public Web3 web3() { + if (web3 == null) { + web3 = new Web3(web3j()); + } + + return web3; + } + + public Eth eth() { + if (eth == null) { + eth = new Eth(web3j()); + } + + return eth; + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeConfig.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeConfig.java new file mode 100755 index 00000000000..65af3ab4b45 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeConfig.java @@ -0,0 +1,111 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.core.MiningParametersTestBuilder; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Arrays; + +public class PantheonNodeConfig { + + private final String name; + private final MiningParameters miningParameters; + private final JsonRpcConfiguration jsonRpcConfiguration; + private final WebSocketConfiguration webSocketConfiguration; + private ServerSocket serverSocket; + + private PantheonNodeConfig( + final String name, + final MiningParameters miningParameters, + final JsonRpcConfiguration jsonRpcConfiguration, + final WebSocketConfiguration webSocketConfiguration) { + this.name = name; + this.miningParameters = miningParameters; + this.jsonRpcConfiguration = jsonRpcConfiguration; + this.webSocketConfiguration = webSocketConfiguration; + } + + private PantheonNodeConfig(final String name, final MiningParameters miningParameters) { + this.name = name; + this.miningParameters = miningParameters; + this.jsonRpcConfiguration = createJsonRpcConfig(); + this.webSocketConfiguration = createWebSocketConfig(); + } + + private static MiningParameters createMiningParameters(final boolean miner) { + return new MiningParametersTestBuilder().enabled(miner).build(); + } + + public static PantheonNodeConfig pantheonMinerNode(final String name) { + return new PantheonNodeConfig(name, createMiningParameters(true)); + } + + public static PantheonNodeConfig pantheonNode(final String name) { + return new PantheonNodeConfig(name, createMiningParameters(false)); + } + + public static PantheonNodeConfig pantheonRpcDisabledNode(final String name) { + return new PantheonNodeConfig( + name, + createMiningParameters(false), + JsonRpcConfiguration.createDefault(), + WebSocketConfiguration.createDefault()); + } + + public static PantheonNodeConfig patheonNodeWithRpcApis( + final String name, final RpcApis... enabledRpcApis) { + final JsonRpcConfiguration jsonRpcConfig = createJsonRpcConfig(); + jsonRpcConfig.setRpcApis(Arrays.asList(enabledRpcApis)); + final WebSocketConfiguration webSocketConfig = createWebSocketConfig(); + webSocketConfig.setRpcApis(Arrays.asList(enabledRpcApis)); + + return new PantheonNodeConfig( + name, createMiningParameters(false), jsonRpcConfig, webSocketConfig); + } + + private static JsonRpcConfiguration createJsonRpcConfig() { + final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); + config.setEnabled(true); + config.setPort(0); + return config; + } + + private static WebSocketConfiguration createWebSocketConfig() { + final WebSocketConfiguration config = WebSocketConfiguration.createDefault(); + config.setEnabled(true); + config.setPort(0); + return config; + } + + public void initSocket() throws IOException { + serverSocket = new ServerSocket(0); + } + + public void closeSocket() throws IOException { + serverSocket.close(); + } + + public int getSocketPort() { + return serverSocket.getLocalPort(); + } + + public String getName() { + return name; + } + + public MiningParameters getMiningParameters() { + return miningParameters; + } + + public JsonRpcConfiguration getJsonRpcConfiguration() { + return jsonRpcConfiguration; + } + + public WebSocketConfiguration getWebSocketConfiguration() { + return webSocketConfiguration; + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeRunner.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeRunner.java new file mode 100755 index 00000000000..4a2be3a82ee --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/PantheonNodeRunner.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.awaitility.Awaitility; + +public interface PantheonNodeRunner { + + static PantheonNodeRunner instance() { + if (Boolean.getBoolean("acctests.runPantheonAsProcess")) { + return new ProcessPantheonNodeRunner(); + } else { + return new ThreadPantheonNodeRunner(); + } + } + + void startNode(PantheonNode node); + + void stopNode(PantheonNode node); + + void shutdown(); + + default void waitForPortsFile(final Path dataDir) { + final File file = new File(dataDir.toFile(), "pantheon.ports"); + Awaitility.waitAtMost(30, TimeUnit.SECONDS) + .until( + () -> { + if (file.exists()) { + try (Stream s = Files.lines(file.toPath())) { + return s.count() > 0; + } + } else { + return false; + } + }); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java new file mode 100755 index 00000000000..4150c515b00 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ProcessPantheonNodeRunner.java @@ -0,0 +1,118 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.Awaitility; + +public class ProcessPantheonNodeRunner implements PantheonNodeRunner { + + private final Logger LOG = LogManager.getLogger(); + + private final Map pantheonProcesses = new HashMap<>(); + + ProcessPantheonNodeRunner() { + Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown)); + } + + @Override + public void startNode(final PantheonNode node) { + final Path dataDir = node.homeDirectory(); + + final List params = new ArrayList<>(); + params.add("build/install/pantheon/bin/pantheon"); + params.add("--datadir"); + params.add(dataDir.toAbsolutePath().toString()); + + params.add("--dev-mode"); + + params.add("--p2p-listen"); + params.add(node.p2pListenAddress()); + + if (node.getMiningParameters().isMiningEnabled()) { + params.add("--miner-enabled"); + params.add("--miner-coinbase"); + params.add(node.getMiningParameters().getCoinbase().get().toString()); + } + + params.add("--bootnodes"); + params.add(String.join(",", node.bootnodes())); + + if (node.jsonRpcEnabled()) { + params.add("--rpc-enabled"); + params.add("--rpc-listen"); + params.add(node.jsonRpcListenAddress().get()); + params.add("--rpc-api"); + params.add(apiList(node.jsonRpcConfiguration().getRpcApis())); + } + + if (node.wsRpcEnabled()) { + params.add("--ws-enabled"); + params.add("--ws-listen"); + params.add(node.wsRpcListenAddress().get()); + params.add("--ws-api"); + params.add(apiList(node.webSocketConfiguration().getRpcApis())); + } + + final ProcessBuilder processBuilder = + new ProcessBuilder(params) + .directory(new File(System.getProperty("user.dir")).getParentFile()) + .inheritIO(); + + try { + final Process process = processBuilder.start(); + pantheonProcesses.put(node.getName(), process); + } catch (final IOException e) { + LOG.error("Error starting PantheonNode process", e); + } + + waitForPortsFile(dataDir); + } + + private String apiList(final Collection rpcApis) { + return String.join(",", rpcApis.stream().map(RpcApis::getValue).collect(Collectors.toList())); + } + + @Override + public void stopNode(final PantheonNode node) { + node.stop(); + if (pantheonProcesses.containsKey(node.getName())) { + final Process process = pantheonProcesses.get(node.getName()); + killPantheonProcess(node.getName(), process); + } + } + + @Override + public synchronized void shutdown() { + final HashMap localMap = new HashMap<>(pantheonProcesses); + localMap.forEach(this::killPantheonProcess); + } + + private void killPantheonProcess(final String name, final Process process) { + LOG.info("Killing " + name + " process"); + + Awaitility.waitAtMost(30, TimeUnit.SECONDS) + .until( + () -> { + if (process.isAlive()) { + process.destroy(); + return false; + } else { + pantheonProcesses.remove(name); + return true; + } + }); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java new file mode 100755 index 00000000000..4104d3316bf --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java @@ -0,0 +1,102 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import net.consensys.pantheon.Runner; +import net.consensys.pantheon.RunnerBuilder; +import net.consensys.pantheon.cli.PantheonControllerBuilder; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration.Builder; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class ThreadPantheonNodeRunner implements PantheonNodeRunner { + + private static final int NETWORK_ID = 10; + + private final Logger LOG = LogManager.getLogger(); + private final Map pantheonRunners = new HashMap<>(); + private ExecutorService nodeExecutor = Executors.newCachedThreadPool(); + + @SuppressWarnings("rawtypes") + @Override + public void startNode(final PantheonNode node) { + if (nodeExecutor == null || nodeExecutor.isShutdown()) { + nodeExecutor = Executors.newCachedThreadPool(); + } + + final PantheonControllerBuilder builder = new PantheonControllerBuilder(); + PantheonController pantheonController; + try { + pantheonController = + builder.build( + new Builder().build(), + null, + node.homeDirectory(), + false, + node.getMiningParameters(), + true, + NETWORK_ID); + } catch (final IOException e) { + throw new RuntimeException("Error building PantheonController", e); + } + + final Runner runner = + new RunnerBuilder() + .build( + Vertx.vertx(), + pantheonController, + true, + node.bootnodes(), + node.getHost(), + node.p2pPort(), + 25, + node.jsonRpcConfiguration(), + node.webSocketConfiguration(), + node.homeDirectory()); + + nodeExecutor.submit(runner::execute); + + waitForPortsFile(node.homeDirectory().toAbsolutePath()); + + pantheonRunners.put(node.getName(), runner); + } + + @Override + public void stopNode(final PantheonNode node) { + node.stop(); + killRunner(node.getName()); + } + + @Override + public void shutdown() { + pantheonRunners.keySet().forEach(this::killRunner); + try { + nodeExecutor.shutdownNow(); + if (!nodeExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("Failed to shut down node executor"); + } + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void killRunner(final String name) { + LOG.info("Killing " + name + " runner"); + + if (pantheonRunners.containsKey(name)) { + try { + pantheonRunners.get(name).close(); + } catch (final Exception e) { + throw new RuntimeException("Error shutting down node " + name, e); + } + } + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Web3.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Web3.java new file mode 100755 index 00000000000..9b3e6d67f92 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/node/Web3.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.tests.acceptance.dsl.node; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.methods.response.Web3Sha3; + +public class Web3 { + + private final Web3j web3j; + + public Web3(final Web3j web3) { + this.web3j = web3; + } + + public String web3Sha3(final String input) throws IOException { + final Web3Sha3 result = web3j.web3Sha3(input).send(); + assertThat(result).isNotNull(); + assertThat(result.hasError()).isFalse(); + return result.getResult(); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/JsonRpcSuccessEvent.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/JsonRpcSuccessEvent.java new file mode 100755 index 00000000000..3c79bc66655 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/JsonRpcSuccessEvent.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.tests.acceptance.dsl.pubsub; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class JsonRpcSuccessEvent { + + private final String version; + private final long id; + private final String result; + + @JsonCreator + public JsonRpcSuccessEvent( + @JsonProperty("jsonrpc") final String version, + @JsonProperty("id") final long id, + @JsonProperty("result") final String result) { + this.id = id; + this.result = result; + this.version = version; + } + + public long getId() { + return id; + } + + public String getResult() { + return result; + } + + public String getVersion() { + return version; + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/Subscription.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/Subscription.java new file mode 100755 index 00000000000..92ce3942bcd --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/Subscription.java @@ -0,0 +1,77 @@ +package net.consensys.pantheon.tests.acceptance.dsl.pubsub; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Hash; + +import java.util.List; +import java.util.Map; + +public class Subscription { + + private static final String SIXTY_FOUR_HEX_PATTERN = "0x[0-9a-f]{64}"; + private static final String HEX_PATTERN = "0x[0-9a-f]+"; + + private final WebSocketConnection connection; + private final String value; + + public Subscription(final WebSocketConnection connection, final String value) { + assertThat(value).matches(HEX_PATTERN); + assertThat(connection).isNotNull(); + this.value = value; + this.connection = connection; + } + + @Override + public String toString() { + return value; + } + + public void verifyEventReceived(final Hash expectedTransaction) { + verifyEventReceived(expectedTransaction, 1); + } + + public void verifyEventReceived(final Hash expectedTransaction, final int expectedOccurrences) { + final List events = connection.getSubscriptionEvents(); + assertThat(events).isNotNull(); + int occurrences = 0; + + for (final SubscriptionEvent event : events) { + if (matches(expectedTransaction, event)) { + occurrences++; + } + } + + assertThat(occurrences) + .as("Expecting: %s occurrences, but found: %s", expectedOccurrences, occurrences) + .isEqualTo(expectedOccurrences); + } + + private boolean matches(final Hash expectedTransaction, final SubscriptionEvent event) { + return isEthSubscription(event) + && isExpectedSubscription(event) + && isExpectedTransaction(expectedTransaction, event); + } + + private boolean isEthSubscription(final SubscriptionEvent event) { + return "2.0".equals(event.getVersion()) + && "eth_subscription".equals(event.getMethod()) + && event.getParams() != null; + } + + private boolean isExpectedSubscription(final SubscriptionEvent event) { + final Map params = event.getParams(); + return params.size() == 2 + && params.containsKey("subscription") + && value.equals(params.get("subscription")); + } + + private boolean isExpectedTransaction( + final Hash expectedTransaction, final SubscriptionEvent event) { + final Map params = event.getParams(); + final String result = params.get("result"); + return params.containsKey("result") + && expectedTransaction.toString().equals(result) + && result.matches(SIXTY_FOUR_HEX_PATTERN); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/SubscriptionEvent.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/SubscriptionEvent.java new file mode 100755 index 00000000000..180d5d0587e --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/SubscriptionEvent.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.tests.acceptance.dsl.pubsub; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SubscriptionEvent { + + private final String version; + private final String method; + private final Map params; + + @JsonCreator + public SubscriptionEvent( + @JsonProperty("jsonrpc") final String version, + @JsonProperty("method") final String method, + @JsonProperty("params") final Map params) { + this.version = version; + this.method = method; + this.params = params; + } + + public String getVersion() { + return version; + } + + public String getMethod() { + return method; + } + + public Map getParams() { + return params; + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocket.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocket.java new file mode 100755 index 00000000000..c104baf76fd --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocket.java @@ -0,0 +1,46 @@ +package net.consensys.pantheon.tests.acceptance.dsl.pubsub; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import java.util.List; + +import io.vertx.core.Vertx; + +public class WebSocket { + + private static final String HEX_PATTERN = "0x[0-9a-f]+"; + + private final WebSocketConnection connection; + + public WebSocket(final Vertx vertx, final PantheonNode node) { + this.connection = new WebSocketConnection(vertx, node); + } + + public Subscription subscribe() { + final JsonRpcSuccessEvent subscribe = connection.subscribe("newPendingTransactions"); + + assertThat(subscribe).isNotNull(); + assertThat(subscribe.getVersion()).isEqualTo("2.0"); + assertThat(subscribe.getId()).isGreaterThan(0); + assertThat(subscribe.getResult()).matches(HEX_PATTERN); + + return new Subscription(connection, subscribe.getResult()); + } + + public void unsubscribe(final Subscription subscription) { + final JsonRpcSuccessEvent unsubscribe = connection.unsubscribe(subscription); + + assertThat(unsubscribe).isNotNull(); + assertThat(unsubscribe.getVersion()).isEqualTo("2.0"); + assertThat(unsubscribe.getId()).isGreaterThan(0); + assertThat(unsubscribe.getResult()).isEqualTo("true"); + } + + public void verifyTotalEventsReceived(final int expectedTotalEventCount) { + final List events = connection.getSubscriptionEvents(); + assertThat(events).isNotNull(); + assertThat(events.size()).isEqualTo(expectedTotalEventCount); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketConnection.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketConnection.java new file mode 100755 index 00000000000..a3ce9fddc3a --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketConnection.java @@ -0,0 +1,127 @@ +package net.consensys.pantheon.tests.acceptance.dsl.pubsub; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.tests.acceptance.dsl.WaitUtils; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.RequestOptions; +import io.vertx.core.http.WebSocket; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; + +public class WebSocketConnection { + + private final RequestOptions options; + private final ConcurrentLinkedDeque subscriptionEvents; + + private volatile String error; + private volatile boolean receivedResponse; + private volatile JsonRpcSuccessEvent latestEvent; + private volatile WebSocket connection; + + public WebSocketConnection(final Vertx vertx, final PantheonNode node) { + if (!node.jsonRpcWebSocketPort().isPresent()) { + throw new IllegalStateException( + "Can't start websocket connection for node with RPC disabled"); + } + subscriptionEvents = new ConcurrentLinkedDeque<>(); + options = new RequestOptions(); + options.setPort(node.jsonRpcWebSocketPort().get()); + options.setHost(node.getHost()); + + connect(vertx); + } + + public JsonRpcSuccessEvent subscribe(final String params) { + resetLatestResult(); + return send( + String.format("{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"%s\"]}", params)); + } + + public JsonRpcSuccessEvent unsubscribe(final Subscription subscription) { + resetLatestResult(); + return send( + String.format( + "{\"id\": 2, \"method\": \"eth_unsubscribe\", \"params\": [\"%s\"]}", subscription)); + } + + private JsonRpcSuccessEvent send(final String json) { + + connection.writeBinaryMessage(Buffer.buffer(json)); + + WaitUtils.waitFor(() -> assertThat(receivedResponse).isEqualTo(true)); + + assertThat(latestEvent) + .as( + "Expecting a JSON-RPC success response to message: %s, instead received: %s", + json, error) + .isNotNull(); + + return latestEvent; + } + + private void connect(final Vertx vertx) { + vertx + .createHttpClient(new HttpClientOptions()) + .websocket( + options, + websocket -> { + webSocketConnection(websocket); + + websocket.handler( + data -> { + try { + final WebSocketEvent eventType = Json.decodeValue(data, WebSocketEvent.class); + + if (eventType.isSubscription()) { + success(Json.decodeValue(data, SubscriptionEvent.class)); + } else { + success(Json.decodeValue(data, JsonRpcSuccessEvent.class)); + } + + } catch (final DecodeException e) { + error(data.toString()); + } + }); + }); + + WaitUtils.waitFor(() -> assertThat(connection).isNotNull()); + } + + private void webSocketConnection(final WebSocket connection) { + this.connection = connection; + } + + private void resetLatestResult() { + this.receivedResponse = false; + this.error = null; + this.latestEvent = null; + } + + private void error(final String response) { + this.receivedResponse = true; + this.error = response; + } + + private void success(final JsonRpcSuccessEvent result) { + this.receivedResponse = true; + this.latestEvent = result; + } + + private void success(final SubscriptionEvent result) { + this.receivedResponse = true; + this.subscriptionEvents.add(result); + } + + public List getSubscriptionEvents() { + return new ArrayList<>(subscriptionEvents); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketEvent.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketEvent.java new file mode 100755 index 00000000000..b69f0306a4f --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/dsl/pubsub/WebSocketEvent.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.tests.acceptance.dsl.pubsub; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class WebSocketEvent { + + private static final String SUBSCRIPTION_METHOD = "eth_subscription"; + + private final boolean subscription; + + @JsonCreator + public WebSocketEvent(@JsonProperty("method") final String method) { + this.subscription = SUBSCRIPTION_METHOD.equalsIgnoreCase(method); + } + + public boolean isSubscription() { + return subscription; + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/jsonrpc/Web3Sha3AcceptanceTest.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/jsonrpc/Web3Sha3AcceptanceTest.java new file mode 100755 index 00000000000..55777823352 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/jsonrpc/Web3Sha3AcceptanceTest.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.tests.acceptance.jsonrpc; + +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +public class Web3Sha3AcceptanceTest extends AcceptanceTestBase { + + private PantheonNode node; + + @Before + public void setUp() throws Exception { + node = cluster.create(pantheonNode("node1")); + cluster.start(node); + } + + @Test + public void shouldReturnCorrectSha3() throws IOException { + final String input = "0x68656c6c6f20776f726c64"; + final String sha3 = "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"; + + final String response = node.web3().web3Sha3(input); + + assertThat(response).isEqualTo(sha3); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/mining/MiningAcceptanceTest.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/mining/MiningAcceptanceTest.java new file mode 100755 index 00000000000..48fbdf0cb73 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/mining/MiningAcceptanceTest.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.tests.acceptance.mining; + +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode; +import static org.web3j.utils.Convert.Unit.ETHER; + +import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import net.consensys.pantheon.tests.acceptance.dsl.account.Account; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import org.junit.Before; +import org.junit.Test; + +public class MiningAcceptanceTest extends AcceptanceTestBase { + + private PantheonNode minerNode; + + @Before + public void setUp() throws Exception { + minerNode = cluster.create(pantheonMinerNode("miner1")); + cluster.start(minerNode); + } + + @Test + public void shouldMineTransactions() { + final Account fromAccount = accounts.createAccount("account1", "50", ETHER, minerNode); + final Account toAccount = accounts.createAccount("account2", "0", ETHER, minerNode); + accounts.waitForAccountBalance(fromAccount, 50, minerNode); + + accounts.incrementalTransfer(fromAccount, toAccount, 1, minerNode); + accounts.waitForAccountBalance(toAccount, 1, minerNode); + + accounts.incrementalTransfer(fromAccount, toAccount, 2, minerNode); + accounts.waitForAccountBalance(toAccount, 3, minerNode); + + accounts.incrementalTransfer(fromAccount, toAccount, 3, minerNode); + accounts.waitForAccountBalance(toAccount, 6, minerNode); + + accounts.incrementalTransfer(fromAccount, toAccount, 4, minerNode); + accounts.waitForAccountBalance(toAccount, 10, minerNode); + + accounts.incrementalTransfer(fromAccount, toAccount, 5, minerNode); + accounts.waitForAccountBalance(toAccount, 15, minerNode); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/pubsub/NewPendingTransactionAcceptanceTest.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/pubsub/NewPendingTransactionAcceptanceTest.java new file mode 100755 index 00000000000..5d50780901f --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/acceptance/pubsub/NewPendingTransactionAcceptanceTest.java @@ -0,0 +1,269 @@ +package net.consensys.pantheon.tests.acceptance.pubsub; + +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode; +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonNode; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import net.consensys.pantheon.tests.acceptance.dsl.account.Account; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; +import net.consensys.pantheon.tests.acceptance.dsl.pubsub.Subscription; +import net.consensys.pantheon.tests.acceptance.dsl.pubsub.WebSocket; + +import java.math.BigInteger; + +import io.vertx.core.Vertx; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class NewPendingTransactionAcceptanceTest extends AcceptanceTestBase { + + private Vertx vertx; + private Account accountOne; + private WebSocket minerWebSocket; + private WebSocket archiveWebSocket; + private PantheonNode minerNode; + private PantheonNode archiveNode; + + @Before + public void setUp() throws Exception { + vertx = Vertx.vertx(); + minerNode = cluster.create(pantheonMinerNode("miner-node1")); + archiveNode = cluster.create(pantheonNode("full-node1")); + cluster.start(minerNode, archiveNode); + accountOne = accounts.createAccount("account-one"); + minerWebSocket = new WebSocket(vertx, minerNode); + archiveWebSocket = new WebSocket(vertx, archiveNode); + } + + @After + public void tearDown() { + vertx.close(); + } + + @Test + public void transactionRemovedByChainReorganisationMustPublishEvent() throws Exception { + + // Create the light fork + final Subscription lightForkSubscription = minerWebSocket.subscribe(); + + final Hash lightForkEvent = accounts.transfer(accountOne, 5, minerNode); + cluster.awaitPropagation(accountOne, 5); + + minerWebSocket.verifyTotalEventsReceived(1); + lightForkSubscription.verifyEventReceived(lightForkEvent); + + final BigInteger lighterForkBlockNumber = minerNode.eth().blockNumber(); + + cluster.stop(); + + // Create the heavy fork + final PantheonNode minerNodeTwo = cluster.create(pantheonMinerNode("miner-node2")); + cluster.start(minerNodeTwo); + + final WebSocket heavyForkWebSocket = new WebSocket(vertx, minerNodeTwo); + final Subscription heavyForkSubscription = heavyForkWebSocket.subscribe(); + + final Account accountTwo = accounts.createAccount("account-two"); + + // Keep both forks transactions valid by using a different benefactor + final Account heavyForkBenefactor = accounts.getSecondaryBenefactor(); + + final Hash heavyForkEventOne = + accounts.transfer(heavyForkBenefactor, accountTwo, 1, minerNodeTwo); + cluster.awaitPropagation(accountTwo, 1); + final Hash heavyForkEventTwo = + accounts.transfer(heavyForkBenefactor, accountTwo, 2, minerNodeTwo); + cluster.awaitPropagation(accountTwo, 1 + 2); + final Hash heavyForkEventThree = + accounts.transfer(heavyForkBenefactor, accountTwo, 3, minerNodeTwo); + cluster.awaitPropagation(accountTwo, 1 + 2 + 3); + + heavyForkWebSocket.verifyTotalEventsReceived(3); + heavyForkSubscription.verifyEventReceived(heavyForkEventOne); + heavyForkSubscription.verifyEventReceived(heavyForkEventTwo); + heavyForkSubscription.verifyEventReceived(heavyForkEventThree); + + final BigInteger heavierForkBlockNumber = minerNodeTwo.eth().blockNumber(); + + cluster.stop(); + + // Restart the two nodes on the light fork with the additional node from the heavy fork + cluster.start(minerNode, archiveNode, minerNodeTwo); + + final WebSocket minerMergedForksWebSocket = new WebSocket(vertx, minerNode); + final WebSocket minerTwoMergedForksWebSocket = new WebSocket(vertx, minerNodeTwo); + final WebSocket archiveMergedForksWebSocket = new WebSocket(vertx, archiveNode); + final Subscription minerMergedForksSubscription = minerMergedForksWebSocket.subscribe(); + final Subscription minerTwoMergedForksSubscription = minerTwoMergedForksWebSocket.subscribe(); + final Subscription archiveMergedForksSubscription = archiveMergedForksWebSocket.subscribe(); + + // Check that all node have loaded their respective forks, i.e. not begin new chains + assertThat(minerNode.eth().blockNumber()).isGreaterThanOrEqualTo(lighterForkBlockNumber); + assertThat(archiveNode.eth().blockNumber()).isGreaterThanOrEqualTo(lighterForkBlockNumber); + assertThat(minerNodeTwo.eth().blockNumber()).isGreaterThanOrEqualTo(heavierForkBlockNumber); + + // This publish give time needed for heavy fork to be chosen + final Hash mergedForksEventOne = + accounts.transfer(accounts.getSecondaryBenefactor(), accountTwo, 3, minerNodeTwo); + cluster.awaitPropagation(accountTwo, 9); + + minerMergedForksWebSocket.verifyTotalEventsReceived(1); + minerMergedForksSubscription.verifyEventReceived(lightForkEvent); + archiveMergedForksWebSocket.verifyTotalEventsReceived(1); + archiveMergedForksSubscription.verifyEventReceived(lightForkEvent); + minerTwoMergedForksWebSocket.verifyTotalEventsReceived(2); + minerTwoMergedForksSubscription.verifyEventReceived(lightForkEvent); + minerTwoMergedForksSubscription.verifyEventReceived(mergedForksEventOne); + + // Check that account two (funded in heavier chain) can be mined on miner one (from lighter + // chain) + final Hash mergedForksEventTwo = accounts.transfer(accountTwo, 3, minerNode); + cluster.awaitPropagation(accountTwo, 9 + 3); + + // Check that account one (funded in lighter chain) can be mined on miner two (from heavier + // chain) + final Hash mergedForksEventThree = accounts.transfer(accountOne, 2, minerNodeTwo); + cluster.awaitPropagation(accountOne, 5 + 2); + + minerMergedForksWebSocket.verifyTotalEventsReceived(1 + 1 + 1); + minerMergedForksSubscription.verifyEventReceived(mergedForksEventTwo); + minerMergedForksSubscription.verifyEventReceived(mergedForksEventThree); + archiveMergedForksWebSocket.verifyTotalEventsReceived(1 + 1 + 1); + archiveMergedForksSubscription.verifyEventReceived(mergedForksEventTwo); + archiveMergedForksSubscription.verifyEventReceived(mergedForksEventThree); + minerTwoMergedForksWebSocket.verifyTotalEventsReceived(2 + 1 + 1); + minerTwoMergedForksSubscription.verifyEventReceived(mergedForksEventTwo); + minerTwoMergedForksSubscription.verifyEventReceived(mergedForksEventThree); + } + + @Test + public void subscriptionToMinerNodeMustReceivePublishEvent() { + final Subscription minerSubscription = minerWebSocket.subscribe(); + + final Hash event = accounts.transfer(accountOne, 4, minerNode); + cluster.awaitPropagation(accountOne, 4); + + minerWebSocket.verifyTotalEventsReceived(1); + minerSubscription.verifyEventReceived(event); + + minerWebSocket.unsubscribe(minerSubscription); + } + + @Test + public void subscriptionToArchiveNodeMustReceivePublishEvent() { + final Subscription archiveSubscription = archiveWebSocket.subscribe(); + + final Hash event = accounts.transfer(accountOne, 23, minerNode); + cluster.awaitPropagation(accountOne, 23); + + archiveWebSocket.verifyTotalEventsReceived(1); + archiveSubscription.verifyEventReceived(event); + + archiveWebSocket.unsubscribe(archiveSubscription); + } + + @Test + public void everySubscriptionMustReceivePublishEvent() { + final Subscription minerSubscriptionOne = minerWebSocket.subscribe(); + final Subscription minerSubscriptionTwo = minerWebSocket.subscribe(); + final Subscription archiveSubscriptionOne = archiveWebSocket.subscribe(); + final Subscription archiveSubscriptionTwo = archiveWebSocket.subscribe(); + final Subscription archiveSubscriptionThree = archiveWebSocket.subscribe(); + + final Hash event = accounts.transfer(accountOne, 30, minerNode); + cluster.awaitPropagation(accountOne, 30); + + minerWebSocket.verifyTotalEventsReceived(2); + minerSubscriptionOne.verifyEventReceived(event); + minerSubscriptionTwo.verifyEventReceived(event); + + archiveWebSocket.verifyTotalEventsReceived(3); + archiveSubscriptionOne.verifyEventReceived(event); + archiveSubscriptionTwo.verifyEventReceived(event); + archiveSubscriptionThree.verifyEventReceived(event); + + minerWebSocket.unsubscribe(minerSubscriptionOne); + minerWebSocket.unsubscribe(minerSubscriptionTwo); + archiveWebSocket.unsubscribe(archiveSubscriptionOne); + archiveWebSocket.unsubscribe(archiveSubscriptionTwo); + archiveWebSocket.unsubscribe(archiveSubscriptionThree); + } + + @Test + public void subscriptionToMinerNodeMustReceiveEveryPublishEvent() { + final Subscription minerSubscription = minerWebSocket.subscribe(); + + final Hash eventOne = accounts.transfer(accountOne, 1, minerNode); + cluster.awaitPropagation(accountOne, 1); + + minerWebSocket.verifyTotalEventsReceived(1); + minerSubscription.verifyEventReceived(eventOne); + + final Hash eventTwo = accounts.transfer(accountOne, 4, minerNode); + final Hash eventThree = accounts.transfer(accountOne, 5, minerNode); + cluster.awaitPropagation(accountOne, 1 + 4 + 5); + + minerWebSocket.verifyTotalEventsReceived(3); + minerSubscription.verifyEventReceived(eventTwo); + minerSubscription.verifyEventReceived(eventThree); + + minerWebSocket.unsubscribe(minerSubscription); + } + + @Test + public void subscriptionToArchiveNodeMustReceiveEveryPublishEvent() { + final Subscription archiveSubscription = archiveWebSocket.subscribe(); + + final Hash eventOne = accounts.transfer(accountOne, 2, minerNode); + final Hash eventTwo = accounts.transfer(accountOne, 5, minerNode); + cluster.awaitPropagation(accountOne, 2 + 5); + + archiveWebSocket.verifyTotalEventsReceived(2); + archiveSubscription.verifyEventReceived(eventOne); + archiveSubscription.verifyEventReceived(eventTwo); + + final Hash eventThree = accounts.transfer(accountOne, 8, minerNode); + cluster.awaitPropagation(accountOne, 2 + 5 + 8); + + archiveWebSocket.verifyTotalEventsReceived(3); + archiveSubscription.verifyEventReceived(eventThree); + + archiveWebSocket.unsubscribe(archiveSubscription); + } + + @Test + public void everySubscriptionMustReceiveEveryPublishEvent() { + final Subscription minerSubscriptionOne = minerWebSocket.subscribe(); + final Subscription minerSubscriptionTwo = minerWebSocket.subscribe(); + final Subscription archiveSubscriptionOne = archiveWebSocket.subscribe(); + final Subscription archiveSubscriptionTwo = archiveWebSocket.subscribe(); + final Subscription archiveSubscriptionThree = archiveWebSocket.subscribe(); + + final Hash eventOne = accounts.transfer(accountOne, 10, minerNode); + final Hash eventTwo = accounts.transfer(accountOne, 5, minerNode); + cluster.awaitPropagation(accountOne, 10 + 5); + + minerWebSocket.verifyTotalEventsReceived(4); + minerSubscriptionOne.verifyEventReceived(eventOne); + minerSubscriptionOne.verifyEventReceived(eventTwo); + minerSubscriptionTwo.verifyEventReceived(eventOne); + minerSubscriptionTwo.verifyEventReceived(eventTwo); + + archiveWebSocket.verifyTotalEventsReceived(6); + archiveSubscriptionOne.verifyEventReceived(eventOne); + archiveSubscriptionOne.verifyEventReceived(eventTwo); + archiveSubscriptionTwo.verifyEventReceived(eventOne); + archiveSubscriptionTwo.verifyEventReceived(eventTwo); + archiveSubscriptionThree.verifyEventReceived(eventOne); + archiveSubscriptionThree.verifyEventReceived(eventTwo); + + minerWebSocket.unsubscribe(minerSubscriptionOne); + minerWebSocket.unsubscribe(minerSubscriptionTwo); + archiveWebSocket.unsubscribe(archiveSubscriptionOne); + archiveWebSocket.unsubscribe(archiveSubscriptionTwo); + archiveWebSocket.unsubscribe(archiveSubscriptionThree); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitter.sol b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitter.sol new file mode 100755 index 00000000000..b1f3c456fd0 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitter.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.4.0; +// compile with: +// solc EventEmitter.sol --bin --abi --optimize --overwrite -o . +// then create web3j wrappers with: +// web3j solidity generate ./generated/EventEmitter.bin ./generated/EventEmitter.abi -o ../../../../../ -p net.consensys.pantheon.tests.web3j.generated +contract EventEmitter { + address owner; + event stored(address _to, uint _amount); + address _sender; + uint _value; + + constructor() public { + owner = msg.sender; + } + + function store(uint _amount) public { + emit stored(msg.sender, _amount); + _value = _amount; + _sender = msg.sender; + } + + function value() constant public returns (uint) { + return _value; + } + + function sender() constant public returns (address) { + return _sender; + } +} \ No newline at end of file diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitterAcceptanceTest.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitterAcceptanceTest.java new file mode 100755 index 00000000000..6dc5775c60d --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/EventEmitterAcceptanceTest.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.tests.web3j; + +import static net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNodeConfig.pantheonMinerNode; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import net.consensys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import net.consensys.pantheon.tests.acceptance.dsl.node.PantheonNode; +import net.consensys.pantheon.tests.web3j.generated.EventEmitter; +import net.consensys.pantheon.tests.web3j.generated.EventEmitter.StoredEventResponse; + +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Before; +import org.junit.Test; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import rx.Observable; + +/* + * This class is based around the EventEmitter solidity contract + * + */ +public class EventEmitterAcceptanceTest extends AcceptanceTestBase { + + public static final BigInteger DEFAULT_GAS_PRICE = BigInteger.valueOf(1000); + public static final BigInteger DEFAULT_GAS_LIMIT = BigInteger.valueOf(3000000); + Credentials MAIN_CREDENTIALS = + Credentials.create("0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"); + + private PantheonNode node; + + @Before + public void setUp() throws Exception { + node = cluster.create(pantheonMinerNode("node1")); + cluster.start(node); + } + + @Test + public void shouldDeployContractAndAllowLookupOfValuesAndEmittingEvents() throws Exception { + System.out.println("Sending Create Contract Transaction"); + final EventEmitter eventEmitter = + EventEmitter.deploy(node.web3j(), MAIN_CREDENTIALS, DEFAULT_GAS_PRICE, DEFAULT_GAS_LIMIT) + .send(); + final Observable storedEventResponseObservable = + eventEmitter.storedEventObservable(new EthFilter()); + final AtomicBoolean subscriptionReceived = new AtomicBoolean(false); + storedEventResponseObservable.subscribe( + storedEventResponse -> { + subscriptionReceived.set(true); + assertEquals(BigInteger.valueOf(12), storedEventResponse._amount); + }); + + final TransactionReceipt send = eventEmitter.store(BigInteger.valueOf(12)).send(); + + assertEquals(BigInteger.valueOf(12), eventEmitter.value().send()); + assertTrue(subscriptionReceived.get()); + } +} diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.abi b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.abi new file mode 100755 index 00000000000..4f6111952ae --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.abi @@ -0,0 +1 @@ +[{"constant":true,"inputs":[],"name":"value","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_amount","type":"uint256"}],"name":"store","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"sender","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_to","type":"address"},{"indexed":false,"name":"_amount","type":"uint256"}],"name":"stored","type":"event"}] \ No newline at end of file diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.bin b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.bin new file mode 100755 index 00000000000..1013ace5f4c --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b5060008054600160a060020a03191633179055610187806100326000396000f3006080604052600436106100565763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416633fa4f245811461005b5780636057361d1461008257806367e404ce1461009c575b600080fd5b34801561006757600080fd5b506100706100da565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061009a6004356100e0565b005b3480156100a857600080fd5b506100b161013f565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b60025490565b604080513381526020810183905281517fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f5929181900390910190a16002556001805473ffffffffffffffffffffffffffffffffffffffff191633179055565b60015473ffffffffffffffffffffffffffffffffffffffff16905600a165627a7a72305820f958aea7922a9538be4c34980ad3171806aad2d3fedb62682cef2ca4e1f1f3120029 \ No newline at end of file diff --git a/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.java b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.java new file mode 100755 index 00000000000..e1223a61395 --- /dev/null +++ b/acceptance-tests/src/test/java/net/consensys/pantheon/tests/web3j/generated/EventEmitter.java @@ -0,0 +1,184 @@ +package net.consensys.pantheon.tests.web3j.generated; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.web3j.abi.EventEncoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Event; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.RemoteCall; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.Log; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.tx.Contract; +import org.web3j.tx.TransactionManager; +import rx.Observable; +import rx.functions.Func1; + +/** + * Auto generated code. + * + *

Do not modify! + * + *

Please use the web3j command line tools, + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the codegen module to update. + * + *

Generated with web3j version 3.5.0. + */ +@SuppressWarnings("rawtypes") +public class EventEmitter extends Contract { + private static final String BINARY = + "608060405234801561001057600080fd5b5060008054600160a060020a03191633179055610187806100326000396000f3006080604052600436106100565763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416633fa4f245811461005b5780636057361d1461008257806367e404ce1461009c575b600080fd5b34801561006757600080fd5b506100706100da565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061009a6004356100e0565b005b3480156100a857600080fd5b506100b161013f565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b60025490565b604080513381526020810183905281517fc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f5929181900390910190a16002556001805473ffffffffffffffffffffffffffffffffffffffff191633179055565b60015473ffffffffffffffffffffffffffffffffffffffff16905600a165627a7a72305820f958aea7922a9538be4c34980ad3171806aad2d3fedb62682cef2ca4e1f1f3120029"; + + public static final String FUNC_VALUE = "value"; + + public static final String FUNC_STORE = "store"; + + public static final String FUNC_SENDER = "sender"; + + public static final Event STORED_EVENT = + new Event( + "stored", + Arrays.>asList( + new TypeReference

() {}, new TypeReference() {})); + + protected EventEmitter( + final String contractAddress, + final Web3j web3j, + final Credentials credentials, + final BigInteger gasPrice, + final BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected EventEmitter( + final String contractAddress, + final Web3j web3j, + final TransactionManager transactionManager, + final BigInteger gasPrice, + final BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public RemoteCall value() { + final Function function = + new Function( + FUNC_VALUE, + Arrays.asList(), + Arrays.>asList(new TypeReference() {})); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + + public RemoteCall store(final BigInteger _amount) { + final Function function = + new Function( + FUNC_STORE, + Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(_amount)), + Collections.>emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteCall sender() { + final Function function = + new Function( + FUNC_SENDER, + Arrays.asList(), + Arrays.>asList(new TypeReference
() {})); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public static RemoteCall deploy( + final Web3j web3j, + final Credentials credentials, + final BigInteger gasPrice, + final BigInteger gasLimit) { + return deployRemoteCall(EventEmitter.class, web3j, credentials, gasPrice, gasLimit, BINARY, ""); + } + + public static RemoteCall deploy( + final Web3j web3j, + final TransactionManager transactionManager, + final BigInteger gasPrice, + final BigInteger gasLimit) { + return deployRemoteCall( + EventEmitter.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, ""); + } + + public List getStoredEvents(final TransactionReceipt transactionReceipt) { + final List valueList = + extractEventParametersWithLog(STORED_EVENT, transactionReceipt); + final ArrayList responses = + new ArrayList(valueList.size()); + for (final Contract.EventValuesWithLog eventValues : valueList) { + final StoredEventResponse typedResponse = new StoredEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse._to = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse._amount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public Observable storedEventObservable(final EthFilter filter) { + return web3j + .ethLogObservable(filter) + .map( + new Func1() { + @Override + public StoredEventResponse call(final Log log) { + final Contract.EventValuesWithLog eventValues = + extractEventParametersWithLog(STORED_EVENT, log); + final StoredEventResponse typedResponse = new StoredEventResponse(); + typedResponse.log = log; + typedResponse._to = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse._amount = + (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + }); + } + + public Observable storedEventObservable( + final DefaultBlockParameter startBlock, final DefaultBlockParameter endBlock) { + final EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(STORED_EVENT)); + return storedEventObservable(filter); + } + + public static EventEmitter load( + final String contractAddress, + final Web3j web3j, + final Credentials credentials, + final BigInteger gasPrice, + final BigInteger gasLimit) { + return new EventEmitter(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + public static EventEmitter load( + final String contractAddress, + final Web3j web3j, + final TransactionManager transactionManager, + final BigInteger gasPrice, + final BigInteger gasLimit) { + return new EventEmitter(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static class StoredEventResponse { + public Log log; + + public String _to; + + public BigInteger _amount; + } +} diff --git a/acceptance-tests/src/test/resources/log4j2.xml b/acceptance-tests/src/test/resources/log4j2.xml new file mode 100755 index 00000000000..d357f53d894 --- /dev/null +++ b/acceptance-tests/src/test/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + INFO + + + + + + + + + + + + diff --git a/acceptance-tests/truffle-pet-shop-tutorial/README.md b/acceptance-tests/truffle-pet-shop-tutorial/README.md new file mode 100755 index 00000000000..f17aba4d938 --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/README.md @@ -0,0 +1,78 @@ +# Truffle Pet Shop example with Pantheon +Here you will find the bare bones of the [Pet Shop example](http://truffleframework.com/tutorials/pet-shop) and instructions to run it with Pantheon and a wallet to manage keys. +## Pre-requisites +* [Truffle](https://truffleframework.com/) installed +``` +npm install -g truffle +``` +* [Wallet](https://www.npmjs.com/package/truffle-privatekey-provider) installed +``` +npm install truffle-privatekey-provider +``` +## To run the pet shop example: +``` +cd acceptance-tests/truffle-pet-shop-tutorial +``` +* here you will find truffle.js which has network configurations for + * development (Ganache) and + * devwallet (points to localhost:8545) + * Note you don't need Ganache running unless you want to run the tests against it (see below) + * However this truffle.js uses address and private key generated by Ganache with default mnemonic "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat" + +* To run the Truffle example with Pantheon, you need Pantheon running + * [check out and build Pantheon](../../README.md) + * run Pantheon (either in IDE or via command line), with mining and rpc enabled. Eg: + +``` +cd $pantheon-working-dir +./gradlew run -Ppantheon.run.args="--miner-enabled --miner-coinbase 0x627306090abaB3A6e1400e9345bC60c78a8BEf57 --sync-mode FULL --no-discovery --dev-mode --rpc-enabled --rpc-api eth,net --rpc-cors-origins 'all'" +``` +* Run Truffle migrate +``` +truffle migrate --network devwallet +``` +* Output should look something like: +``` +Using network 'devwallet'. + +Running migration: 1_initial_migration.js + Deploying Migrations... + ... 0x2c16dd43c0adfe0c697279e388f531581c2b722e7f0e968e3e65e4345bdeb502 + Migrations: 0xfb88de099e13c3ed21f80a7a1e49f8caecf10df6 +Saving successful migration to network... + ... 0x1135ea1dd6947f262d65dde8712d17b4b0ec0a36cc917772ce8acd7fe01ca8e2 +Saving artifacts... +Running migration: 2_deploy_contracts.js + Deploying Adoption... + ... 0xa3d220639719b8e007a7aa8cb18e8caf3587337b77bac833959f4853b1695369 + Adoption: 0xf204a4ef082f5c04bb89f7d5e6568b796096735a +Saving successful migration to network... + ... 0xd7245d7b1c0a7eb5a5198754f7edd7abdae3b806605b54ecc4716f9b4b05de61 +Saving artifacts... + +``` +If migrate works, try running the tests + +``` +cd acceptance-tests/truffle-pet-shop-tutorial +truffle test --network devwallet +``` +* Output should look something like: +``` +Using network 'devwallet'. + +Compiling ./contracts/Adoption.sol... +Compiling ./test/TestAdoption.sol... +Compiling truffle/Assert.sol... +Compiling truffle/DeployedAddresses.sol... + + + TestAdoption + ✓ testUserCanAdoptPet (4050ms) + ✓ testGetAdopterAddressByPetId (5050ms) + ✓ testGetAdopterAddressByPetIdInArray (4039ms) + ✓ testUserCanUnadoptPet (5049ms) + + + 4 passing (52s) +``` \ No newline at end of file diff --git a/acceptance-tests/truffle-pet-shop-tutorial/contracts/Adoption.sol b/acceptance-tests/truffle-pet-shop-tutorial/contracts/Adoption.sol new file mode 100755 index 00000000000..03c9bb3bb4a --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/contracts/Adoption.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.4.17; + +contract Adoption { + address[16] public adopters; +// Adopting a pet +function adopt(uint petId) public returns (uint) { + require(petId >= 0 && petId <= 15); + + adopters[petId] = msg.sender; + + return petId; +} +// who has adopted this pet? +function getOwner(uint petId) public view returns (address) { + require(petId >= 0 && petId <= 15); + return adopters[petId]; +} + +// is this pet adopted? +function isAdopted(uint petId) public view returns (bool) { + require(petId >= 0 && petId <= 15); + return adopters[petId] != 0; +} +// Adopting a pet +function unadopt(uint petId) public returns (uint) { + require(petId >= 0 && petId <= 15); + + adopters[petId] = 0; + + return petId; +} + +// Retrieving the adopters +function getAdopters() public view returns (address[16]) { + return adopters; +} +} diff --git a/acceptance-tests/truffle-pet-shop-tutorial/contracts/Migrations.sol b/acceptance-tests/truffle-pet-shop-tutorial/contracts/Migrations.sol new file mode 100755 index 00000000000..4ea0833d253 --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.4.24; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + modifier restricted() { + if (msg.sender == owner) _; + } + + constructor() public { + owner = msg.sender; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) public restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/acceptance-tests/truffle-pet-shop-tutorial/migrations/1_initial_migration.js b/acceptance-tests/truffle-pet-shop-tutorial/migrations/1_initial_migration.js new file mode 100755 index 00000000000..4d5f3f9b02e --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +var Migrations = artifacts.require("./Migrations.sol"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/acceptance-tests/truffle-pet-shop-tutorial/migrations/2_deploy_contracts.js b/acceptance-tests/truffle-pet-shop-tutorial/migrations/2_deploy_contracts.js new file mode 100755 index 00000000000..cc3332cd682 --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/migrations/2_deploy_contracts.js @@ -0,0 +1,5 @@ +var Adoption = artifacts.require("Adoption"); + +module.exports = function(deployer) { + deployer.deploy(Adoption); +}; diff --git a/acceptance-tests/truffle-pet-shop-tutorial/test/TestAdoption.sol b/acceptance-tests/truffle-pet-shop-tutorial/test/TestAdoption.sol new file mode 100755 index 00000000000..5022fa31ed6 --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/test/TestAdoption.sol @@ -0,0 +1,52 @@ +pragma solidity ^0.4.17; + +import "truffle/Assert.sol"; +import "truffle/DeployedAddresses.sol"; +import "../contracts/Adoption.sol"; + +contract TestAdoption { + Adoption adoption = Adoption(DeployedAddresses.Adoption()); +// Testing the adopt() function +function testUserCanAdoptPet() public { + uint returnedId = adoption.adopt(8); + + uint expected = 8; + + Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded."); +} + +// Testing retrieval of a single pet's owner +function testGetAdopterAddressByPetId() public { + // Expected owner is this contract + address expected = this; + + address adopter = adoption.adopters(8); + + Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded."); +} + +// Testing retrieval of all pet owners +function testGetAdopterAddressByPetIdInArray() public { + // Expected owner is this contract + address expected = this; + + // Store adopters in memory rather than contract's storage + address[16] memory adopters = adoption.getAdopters(); + + Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded."); +} + + +// Testing the unadopt() function +function testUserCanUnadoptPet() public { + uint returnedId = adoption.unadopt(8); + + uint expected = 8; + + Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded."); + + // Store adopters in memory rather than contract's storage + address[16] memory adopters = adoption.getAdopters(); + Assert.equal(adopters[8], 0, "owner should be reset after unadopt"); +} +} diff --git a/acceptance-tests/truffle-pet-shop-tutorial/test/test.js b/acceptance-tests/truffle-pet-shop-tutorial/test/test.js new file mode 100755 index 00000000000..5da5f2cf09d --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/test/test.js @@ -0,0 +1,32 @@ +/* global artifacts contract describe assert it */ + +const TestAdoption = artifacts.require('Adoption.sol'); +var proxy; +contract('Adoption 1', () => { + describe('Function: adopt pet 1', () => { + it('Should successfully adopt pet within range', async () => { + proxy = await TestAdoption.new(); + await proxy.adopt(1); + + assert(true, 'expected adoption of pet within range to succeed'); + }); + + it('Should catch an error and then return', async () => { + try { + await proxy.adopt(22); + } catch (err) { + assert(true, err.toString().includes('revert'), 'expected revert in message'); + + return; + } + + assert(false, 'did not catch expected error from petID out of range'); + }); + + it('Should successfully adopt pet within range 2', async () => { + await proxy.adopt(2); + + assert(true, 'expected adoption of pet within range to succeed'); + }); + }); +}); diff --git a/acceptance-tests/truffle-pet-shop-tutorial/test/test2.js b/acceptance-tests/truffle-pet-shop-tutorial/test/test2.js new file mode 100755 index 00000000000..8f195c5899b --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/test/test2.js @@ -0,0 +1,24 @@ +/* global artifacts contract describe assert it */ + +const TestAdoption = artifacts.require('Adoption.sol'); +var proxy; +contract('Adoption 2', () => { + describe('Function: adopt pet 3', () => { + it('Should successfully adopt pet within range', async () => { + proxy = await TestAdoption.new(); + + await proxy.adopt(3); + assert(true, 'expected adoption of pet within range to succeed'); + + const isAdopted = await proxy.isAdopted(3); + assert.equal(isAdopted, true, 'expected pet 3 to be adopted (adopted in this test method)'); + }); + + it('Pet 2 should NOT be adopted (was adopted in a different test file)', async () => { + // check status of pet2 + const isAdopted = await proxy.isAdopted(2); + + assert.equal(isAdopted, false, 'expected pet 2 to NOT be adopted (was adopted in a different test file)'); + }); + }); +}); diff --git a/acceptance-tests/truffle-pet-shop-tutorial/truffle.js b/acceptance-tests/truffle-pet-shop-tutorial/truffle.js new file mode 100755 index 00000000000..07e98589d5d --- /dev/null +++ b/acceptance-tests/truffle-pet-shop-tutorial/truffle.js @@ -0,0 +1,21 @@ +const PrivateKeyProvider = require('truffle-privatekey-provider'); + +// address 627306090abaB3A6e1400e9345bC60c78a8BEf57 +const privateKey = 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'; +const localDev = 'http://127.0.0.1:8545'; + +const privateKeyProvider = new PrivateKeyProvider(privateKey, localDev); + +module.exports = { + networks: { + development: { + host: '127.0.0.1', + port: 7545, + network_id: '*', + }, + devwallet: { + provider: privateKeyProvider, + network_id: '2018', + }, + }, +}; diff --git a/build.gradle b/build.gradle new file mode 100755 index 00000000000..c741a6fd842 --- /dev/null +++ b/build.gradle @@ -0,0 +1,401 @@ +import net.ltgt.gradle.errorprone.CheckSeverity + +plugins { + id 'com.diffplug.gradle.spotless' version '3.13.0' + id 'com.palantir.git-version' version '0.10.0' + id 'io.spring.dependency-management' version '1.0.4.RELEASE' + id 'com.github.hierynomus.license' version '0.14.0' + id 'net.ltgt.errorprone' version '0.6' + id 'me.champeau.gradle.jmh' version '0.4.5' apply false + id 'com.jfrog.bintray' version '1.7.3' +} + +apply from: './versions.gradle' + +defaultTasks 'build', 'checkLicenses', 'javadoc' + +def buildAliases = ['dev': [ + 'spotlessApply', + 'build', + 'checkLicenses', + 'javadoc' + ]] + +def expandedTaskList = [] +gradle.startParameter.taskNames.each { + expandedTaskList << (buildAliases[it] ? buildAliases[it] : it) +} +gradle.startParameter.taskNames = expandedTaskList.flatten() + +// Gets a integer command argument, passed with -Pname=x, or the defaut if not provided. +def _intCmdArg(name, defaultValue) { + return project.hasProperty(name) ? project.property(name) as int : defaultValue +} + +def _intCmdArg(name) { + return _intCmdArg(name, null) +} + +def _strListCmdArg(name, defaultValue) { + if (!project.hasProperty(name)) + return defaultValue + + return ((String)project.property(name)).tokenize(',') +} + +def _strListCmdArg(name) { + return _strListCmdArg(name, null) +} + +def baseVersion = '0.8.0' +project.version = baseVersion + '-SNAPSHOT' + +allprojects { + apply plugin: 'java-library' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'jacoco' + apply plugin: 'net.ltgt.errorprone' + apply plugin: 'com.jfrog.bintray' + apply from: "${rootDir}/gradle/versions.gradle" + apply from: "${rootDir}/gradle/check-licenses.gradle" + + version = rootProject.version + + jacoco { toolVersion = '0.8.2' } + + sourceCompatibility = '1.8' + targetCompatibility = '1.8' + + repositories { + jcenter() + mavenCentral() + maven { url "https://consensys.bintray.com/pegasys-repo" } + } + + dependencies { + errorprone("com.google.errorprone:error_prone_core:$errorproneCore") + if (JavaVersion.current().isJava8()) { + errorproneJavac("com.google.errorprone:javac:$errorproneJavac") + } + } + + apply plugin: 'com.diffplug.gradle.spotless' + spotless { + java { + // This path needs to be relative to each project + target fileTree('.') { + include '**/*.java' + exclude '**/generalstate/GeneralStateReferenceTest*.java' + exclude '**/generalstate/GeneralStateRegressionReferenceTest*.java' + exclude '**/blockchain/BlockchainReferenceTest*.java' + exclude '**/.gradle/**' + } + removeUnusedImports() + googleJavaFormat() + importOrder 'net.consensys', 'java', '' + trimTrailingWhitespace() + endWithNewline() + } + groovyGradle { + target '*.gradle' + greclipse().configFile(rootProject.file('gradle/formatter.properties')) + endWithNewline() + } + } + + tasks.withType(JavaCompile) { + options.compilerArgs += [ + '-Xlint:unchecked', + '-Xlint:cast', + '-Xlint:rawtypes', + '-Xlint:overloads', + '-Xlint:divzero', + '-Xlint:finally', + '-Xlint:static', + '-Werror', + ] + + options.errorprone { + excludedPaths '.*/(generated|.*ReferenceTest_.*)' + check('FutureReturnValueIgnored', CheckSeverity.OFF) + check('InsecureCryptoUsage', CheckSeverity.WARN) + check('FieldCanBeFinal', CheckSeverity.WARN) + check('WildcardImport', CheckSeverity.WARN) + } + } + + /* + * Pass some system properties provided on the gradle command line to test executions for + * convenience. + * + * The properties passed are: + * - 'test.ethereum.include': allows to run a single Ethereum reference tests. For instance, + * running a single general state test can be done with: + * ./gradlew :ethereum:net.consensys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.include=callcodecallcallcode_101-Frontier + * The meaning being that will be run only the tests for which the value passed as "include" + * (which can be a java pattern) matches parts of the test name. Knowing that tests names for + * reference tests are of the form: + * (-([])?)? + * where is the test name as defined in the json file (usually the name of the json file + * as well), is the Ethereum milestone tested (not all test use it) and + * is only use in some general state tests where for the same json file and same milestone, + * multiple variant of that test are run. The variant is a simple number. + * - 'test.ethereum.state.eip': for general state tests, allows to only run tests for the + * milestone specified by this value. So for instance, + * ./gradlew :ethereum:net.consensys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.state.eip=Frontier + * only run general state tests for Frontier. Note that this behavior could be achieved as well + * with the 'include' option above since it is a pattern, but this is a slightly more convenient + * option. + * - 'root.log.level' and 'evm.log.level': allow to control the log level used during the tests. + */ + test { + jvmArgs = [ + '-Xmx4g', + '-XX:-UseGCOverheadLimit' + ] + Set toImport = [ + 'test.ethereum.include', + 'test.ethereum.state.eip', + 'root.log.level', + 'evm.log.level' + ] + for (String name : toImport) { + if (System.getProperty(name) != null) { + systemProperty name, System.getProperty(name) + } + } + } + + // Normalise Xdoclint behaviour across JDKs (OpenJDK 8 is more lenient than Oracle JDK by default). + javadoc { + options.addStringOption('Xdoclint:all', '-quiet') + options.encoding = 'UTF-8' + } + +} + +task deploy() {} + + + + + +subprojects { + + tasks.withType(Test) { + // If GRADLE_MAX_TEST_FORKS is not set, use half the available processors + maxParallelForks = (System.getenv('GRADLE_MAX_TEST_FORKS') ?: (Runtime.runtime.availableProcessors().intdiv(2) ?: 1)).toInteger() + } + + tasks.withType(JavaCompile) { + options.fork = true + options.incremental = true + } + apply plugin: 'maven-publish' + + sourceSets { + // test-support can be consumed as a library by other projects in their tests + testSupport { + java { + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/test-support/java') + } + resources.srcDir file('src/test-support/resources') + } + integrationTest { + java { + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/integration-test/java') + } + resources.srcDir file('src/integration-test/resources') + } + } + + configurations { + testSupportImplementation.extendsFrom implementation + integrationTestImplementation.extendsFrom implementation + testSupportArtifacts + } + + task testSupportJar (type: Jar) { + baseName = "${project.name}-support-test" + from sourceSets.testSupport.output + } + + dependencies { + testImplementation sourceSets.testSupport.output + integrationTestImplementation sourceSets.testSupport.output + } + + task integrationTest(type: Test, dependsOn:["compileTestJava"]){ + group = "verification" + description = "Runs the Pantheon Integration Test" + + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + outputs.upToDateWhen { false } + } + + if (file('src/jmh').directory) { + apply plugin: 'me.champeau.gradle.jmh' + + jmhCompileGeneratedClasses { + options.compilerArgs += [ + '-XepDisableAllChecks' + ] + } + + jmh { + // Allows to control JMH execution directly from the command line. I typical execution may look + // like: + // gradle jmh -Pf=2 -Pwi=3 -Pi=5 -Pinclude=MyBench + // which will run 2 forks with 3 warmup iterations and 5 normal ones for each, and will only + // run the benchmark matching 'MyBench' (a regexp). + warmupForks = _intCmdArg('wf') + warmupIterations = _intCmdArg('wi') + fork = _intCmdArg('f') + iterations = _intCmdArg('i') + benchmarkMode = _strListCmdArg('bm') + include = _strListCmdArg('include', ['']) + humanOutputFile = project.file("${project.buildDir}/reports/jmh/results.txt") + resultFormat = 'JSON' + } + + dependencies { jmh 'org.apache.logging.log4j:log4j-api' } + } + + afterEvaluate { + if (project.jar.enabled) { + publishing { + publications { + MavenDeployment(MavenPublication) { + from components.java + groupId 'net.consensys.pantheon' + artifactId project.jar.baseName + version project.version + } + } + } + + bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_KEY') + publications = ['MavenDeployment'] + pkg { + repo = rootProject.name.toLowerCase() + name = rootProject.name.capitalize() + userOrg = 'consensys' + licenses = ['Apache-2.0'] + version { + name = project.version + desc = rootProject.name.capitalize() + ' distribution' + released = new Date() + vcsTag = project.version + } + } + } + + deploy.dependsOn bintrayUpload + } + } +} + +jar { enabled = false } + +apply plugin: 'application' +mainClassName = "net.consensys.pantheon.Pantheon" +applicationDefaultJvmArgs = [ + "-Dvertx.disableFileCPResolving=true", + "-Dpantheon.home=PANTHEON_HOME", + // We shutdown log4j ourselves, as otherwise his shutdown hook runs before our own and whatever + // happens during shutdown is not logged. + "-Dlog4j.shutdownHookEnabled=false", + "-Djava.security.egd=file:/dev/urandom" +] + +run { + args project.hasProperty("pantheon.run.args") ? project.property("pantheon.run.args").toString().split("\\s+") : [] + doFirst { + applicationDefaultJvmArgs = applicationDefaultJvmArgs.collect{it.replace('PANTHEON_HOME', "$buildDir/pantheon")} + } +} + +startScripts { + doLast { + unixScript.text = unixScript.text.replace('PANTHEON_HOME', '\$APP_HOME') + windowsScript.text = windowsScript.text.replace('PANTHEON_HOME', '%~dp0..') + } +} + +dependencies { + compile project(':ethereum:evmtool') + compile project(':pantheon') + errorprone 'com.google.errorprone:error_prone_core:2.3.1' +} + +task createEVMToolApp(type: CreateStartScripts) { + outputDir = startScripts.outputDir + mainClassName = 'net.consensys.pantheon.EVMTool' + applicationName = 'evm' + classpath = startScripts.classpath +} + +applicationDistribution.into("bin") { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(createEVMToolApp) + fileMode = 0755 +} + +applicationDistribution.into("") { from("LICENSE") } + +distTar { + doFirst { + delete fileTree(dir: 'build/distributions', include: '*.tar.gz') + } + compression = Compression.GZIP + extension = 'tar.gz' +} + +distZip { + doFirst { + delete fileTree(dir: 'build/distributions', include: '*.zip') + } +} + +task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { + additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs) + sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs) + classDirectories = files(subprojects.sourceSets.main.output) + executionData = files(subprojects.jacocoTestReport.executionData) //how to exclude some package/classes com.test.** + reports { + xml.enabled true + csv.enabled true + html.destination file("build/reports/jacocoHtml") + } + onlyIf = { true } + doFirst { + executionData = files(executionData.findAll { it.exists() }) + } +} + +configurations { annotationProcessor } + +// Prevent errorprone-checks being dependent upon errorprone-checks! +// However, ensure all subprojects comply with the custom rules. +configure(subprojects.findAll {it.name != 'errorprone-checks'}) { + dependencies { annotationProcessor project(":errorprone-checks") } + + tasks.withType(JavaCompile) { + options.compilerArgs += [ + '-processorpath', + configurations.annotationProcessor.asPath + ] + } +} + + +if (!file("ethereum/referencetests/src/test/resources/README.md").exists()) { + throw new GradleException("ethereum/referencetests/src/test/resources/README.md missing: please clone submodules (git submodule update --init --recursive)") +} diff --git a/buildSrc/src/main/groovy/ProjectPropertiesFile.groovy b/buildSrc/src/main/groovy/ProjectPropertiesFile.groovy new file mode 100755 index 00000000000..ff1dd98f7e0 --- /dev/null +++ b/buildSrc/src/main/groovy/ProjectPropertiesFile.groovy @@ -0,0 +1,124 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +class ProjectPropertiesFile extends DefaultTask { + + private String destPackage = '' + private String filename = defaultFilename() + private List properties = new ArrayList<>() + + @OutputFile + File getOutputFile() { + String outputFile = "${project.projectDir}/src/main/java/${packagePath()}/${filename}.java" + return project.file(outputFile) + } + + @TaskAction + void generateFile() { + getOutputFile().text = generateFileContent() + } + + void setDestPackage(String destPackage) { + this.destPackage = destPackage + } + + @Input + String getDestPackage() { + return destPackage + } + + void setFilename(String filename) { + this.filename = filename + } + + @Input + String getFilename() { + return filename + } + + void addString(String name, String value) { + properties.add(new Property(name, value, PropertyType.STRING)) + } + + @Nested + List getProperties() { + return properties + } + + private String packagePath() { + return destPackage.replace(".", "/") + } + + private String defaultFilename() { + return "${project.name.capitalize()}Info" + } + + private String generateFileContent() { + String[] varDeclarations = properties.stream().map({p -> p.variableDeclaration()}).toArray() + String[] methodDeclarations = properties.stream().map({p -> p.methodDeclaration()}).toArray() + return """package ${destPackage}; + +// This file is generated via a gradle task and should not be edited directly. +public class ${filename} { +${String.join("\n ", varDeclarations)} + + private ${filename}() {} +${String.join("\n", methodDeclarations)} +} +""" + } + + private enum PropertyType { + STRING("String") + + private final String strVal + PropertyType(String strVal) { + this.strVal = strVal + } + + String toString() { + return strVal + } + } + + private static class Property { + private final String name + private final String value + private final PropertyType type + + Property(name, value, type) { + this.name = name + this.value = value + this.type = type + } + + @Input + String getName() { + return name + } + + @Input + String getValue() { + return value + } + + @Input + String getType() { + return type.toString() + } + + String variableDeclaration() { + return " private static final ${type} ${name} = \"${value}\";" + } + + String methodDeclaration() { + return """ + public static ${type} ${name}() { + return ${name}; + }""" + } + } +} diff --git a/consensus/build.gradle b/consensus/build.gradle new file mode 100755 index 00000000000..4e71f006a0b --- /dev/null +++ b/consensus/build.gradle @@ -0,0 +1 @@ +jar { enabled = false } diff --git a/consensus/clique/build.gradle b/consensus/clique/build.gradle new file mode 100755 index 00000000000..9d6f380bb40 --- /dev/null +++ b/consensus/clique/build.gradle @@ -0,0 +1,28 @@ +plugins { id 'java' } + +version '1.0.0-SNAPSHOT' + +sourceCompatibility = 1.8 + + +repositories { mavenCentral() } + +dependencies { + implementation project(':crypto') + implementation project(':ethereum:core') + implementation project(':ethereum:eth') + implementation project(':ethereum:jsonrpc') + implementation project(':ethereum:rlp') + implementation project(':ethereum:p2p') + implementation project(':services:kvstore') + implementation project(':consensus:common') + + implementation 'com.google.guava:guava' + implementation 'io.vertx:vertx-core' + + implementation project(':util') + testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts') + testImplementation group: 'junit', name: 'junit', version: '4.12' + testImplementation "org.assertj:assertj-core:3.10.0" + testImplementation 'org.mockito:mockito-core' +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/BlockHeaderValidationRulesetFactory.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/BlockHeaderValidationRulesetFactory.java new file mode 100755 index 00000000000..aec411aaf96 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/BlockHeaderValidationRulesetFactory.java @@ -0,0 +1,49 @@ +package net.consensys.pantheon.consensus.clique; + +import net.consensys.pantheon.consensus.clique.headervalidationrules.CliqueDifficultyValidationRule; +import net.consensys.pantheon.consensus.clique.headervalidationrules.CliqueExtraDataValidationRule; +import net.consensys.pantheon.consensus.clique.headervalidationrules.CoinbaseHeaderValidationRule; +import net.consensys.pantheon.consensus.clique.headervalidationrules.SignerRateLimitValidationRule; +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.headervalidationrules.VoteValidationRule; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.AncestryValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.ConstantFieldValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasLimitRangeAndDeltaValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasUsageValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.TimestampValidationRule; + +public class BlockHeaderValidationRulesetFactory { + + /** + * Creates a set of rules which when executed will determine if a given block header is valid with + * respect to its parent (or chain). + * + *

Specifically the set of rules provided by this function are to be used for a Clique chain. + * + * @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks. + * @param epochManager an object which determines if a given block is an epoch block. + * @return the header validator. + */ + public static BlockHeaderValidator cliqueBlockHeaderValidator( + final long secondsBetweenBlocks, final EpochManager epochManager) { + + return new BlockHeaderValidator.Builder() + .addRule(new AncestryValidationRule()) + .addRule(new GasUsageValidationRule()) + .addRule(new GasLimitRangeAndDeltaValidationRule(5000, 0x7fffffffffffffffL)) + .addRule(new TimestampValidationRule(10, secondsBetweenBlocks)) + .addRule(new ConstantFieldValidationRule<>("MixHash", BlockHeader::getMixHash, Hash.ZERO)) + .addRule( + new ConstantFieldValidationRule<>( + "OmmersHash", BlockHeader::getOmmersHash, Hash.EMPTY_LIST_HASH)) + .addRule(new CliqueExtraDataValidationRule(epochManager)) + .addRule(new VoteValidationRule()) + .addRule(new CliqueDifficultyValidationRule()) + .addRule(new SignerRateLimitValidationRule()) + .addRule(new CoinbaseHeaderValidationRule(epochManager)) + .build(); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashing.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashing.java new file mode 100755 index 00000000000..37690589b0a --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashing.java @@ -0,0 +1,82 @@ +package net.consensys.pantheon.consensus.clique; + +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.function.Supplier; + +public class CliqueBlockHashing { + + /** + * Constructs a hash of the block header, suitable for use when creating the proposer seal. The + * extra data is modified to have a null proposer seal and empty list of committed seals. + * + * @param header The header for which a proposer seal is to be calculated + * @param cliqueExtraData The extra data block which is to be inserted to the header once seal is + * calculated + * @return the hash of the header suitable for signing as the proposer seal + */ + public static Hash calculateDataHashForProposerSeal( + final BlockHeader header, final CliqueExtraData cliqueExtraData) { + final BytesValue headerRlp = serializeHeaderWithoutProposerSeal(header, cliqueExtraData); + return Hash.hash(headerRlp); // Proposer hash is the hash of the RLP + } + + /** + * Recovers the proposer's {@link Address} from the proposer seal. + * + * @param header the block header that was signed by the proposer seal + * @param cliqueExtraData the parsed CliqueExtraData from the header + * @return the proposer address + */ + public static Address recoverProposerAddress( + final BlockHeader header, final CliqueExtraData cliqueExtraData) { + if (!cliqueExtraData.getProposerSeal().isPresent()) { + throw new IllegalArgumentException( + "Supplied cliqueExtraData does not include a proposer " + "seal"); + } + final Hash proposerHash = calculateDataHashForProposerSeal(header, cliqueExtraData); + return Util.signatureToAddress(cliqueExtraData.getProposerSeal().get(), proposerHash); + } + + private static BytesValue serializeHeaderWithoutProposerSeal( + final BlockHeader header, final CliqueExtraData cliqueExtraData) { + return serializeHeader(header, () -> encodeExtraDataWithoutProposerSeal(cliqueExtraData)); + } + + private static BytesValue encodeExtraDataWithoutProposerSeal( + final CliqueExtraData cliqueExtraData) { + final BytesValue extraDataBytes = cliqueExtraData.encode(); + // Always trim off final 65 bytes (which maybe zeros) + return extraDataBytes.slice(0, extraDataBytes.size() - Signature.BYTES_REQUIRED); + } + + private static BytesValue serializeHeader( + final BlockHeader header, final Supplier extraDataSerializer) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + + out.writeBytesValue(header.getParentHash()); + out.writeBytesValue(header.getOmmersHash()); + out.writeBytesValue(header.getCoinbase()); + out.writeBytesValue(header.getStateRoot()); + out.writeBytesValue(header.getTransactionsRoot()); + out.writeBytesValue(header.getReceiptsRoot()); + out.writeBytesValue(header.getLogsBloom().getBytes()); + out.writeUInt256Scalar(header.getDifficulty()); + out.writeLongScalar(header.getNumber()); + out.writeLongScalar(header.getGasLimit()); + out.writeLongScalar(header.getGasUsed()); + out.writeLongScalar(header.getTimestamp()); + out.writeBytesValue(extraDataSerializer.get()); + out.writeBytesValue(header.getMixHash()); + out.writeLong(header.getNonce()); + out.endList(); + return out.encoded(); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueContext.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueContext.java new file mode 100755 index 00000000000..056f73a319c --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueContext.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.consensus.clique; + +import net.consensys.pantheon.consensus.common.VoteProposer; + +/** + * Holds the data which lives "in parallel" with the importation of blocks etc. when using the + * Clique consensus mechanism. + */ +public class CliqueContext { + + private final VoteTallyCache voteTallyCache; + private final VoteProposer voteProposer; + + public CliqueContext(final VoteTallyCache voteTallyCache, final VoteProposer voteProposer) { + this.voteTallyCache = voteTallyCache; + this.voteProposer = voteProposer; + } + + public VoteTallyCache getVoteTallyCache() { + return voteTallyCache; + } + + public VoteProposer getVoteProposer() { + return voteProposer; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculator.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculator.java new file mode 100755 index 00000000000..813fd410406 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculator.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.consensus.clique; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DifficultyCalculator; + +import java.math.BigInteger; + +public class CliqueDifficultyCalculator implements DifficultyCalculator { + + private final Address localAddress; + + private final BigInteger IN_TURN_DIFFICULTY = BigInteger.valueOf(2); + private final BigInteger OUT_OF_TURN_DIFFICULTY = BigInteger.ONE; + + public CliqueDifficultyCalculator(final Address localAddress) { + this.localAddress = localAddress; + } + + @Override + public BigInteger nextDifficulty( + final long time, final BlockHeader parent, final ProtocolContext context) { + + final Address nextProposer = + CliqueHelpers.getProposerForBlockAfter( + parent, context.getConsensusState().getVoteTallyCache()); + return nextProposer.equals(localAddress) ? IN_TURN_DIFFICULTY : OUT_OF_TURN_DIFFICULTY; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueExtraData.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueExtraData.java new file mode 100755 index 00000000000..96f59cd2000 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueExtraData.java @@ -0,0 +1,97 @@ +package net.consensys.pantheon.consensus.clique; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.Lists; + +/** + * Represents the data structure stored in the extraData field of the BlockHeader used when + * operating under an Clique consensus mechanism. + */ +public class CliqueExtraData { + + public static final int EXTRA_VANITY_LENGTH = 32; + + private final BytesValue vanityData; + private final List

validators; + private final Optional proposerSeal; + + public CliqueExtraData( + final BytesValue vanityData, final Signature proposerSeal, final List
validators) { + + checkNotNull(vanityData); + checkNotNull(validators); + checkArgument(vanityData.size() == EXTRA_VANITY_LENGTH); + + this.vanityData = vanityData; + this.proposerSeal = Optional.ofNullable(proposerSeal); + this.validators = validators; + } + + public static CliqueExtraData decode(final BytesValue input) { + if (input.size() < EXTRA_VANITY_LENGTH + Signature.BYTES_REQUIRED) { + throw new IllegalArgumentException( + "Invalid BytesValue supplied - too short to produce a valid Clique Extra Data object."); + } + + final int validatorByteCount = input.size() - EXTRA_VANITY_LENGTH - Signature.BYTES_REQUIRED; + if ((validatorByteCount % Address.SIZE) != 0) { + throw new IllegalArgumentException( + "BytesValue is of invalid size - i.e. contains unused bytes."); + } + + final BytesValue vanityData = input.slice(0, EXTRA_VANITY_LENGTH); + final List
validators = + extractValidators(input.slice(EXTRA_VANITY_LENGTH, validatorByteCount)); + + final int proposerSealStartIndex = input.size() - Signature.BYTES_REQUIRED; + final Signature proposerSeal = parseProposerSeal(input.slice(proposerSealStartIndex)); + + return new CliqueExtraData(vanityData, proposerSeal, validators); + } + + private static Signature parseProposerSeal(final BytesValue proposerSealRaw) { + return proposerSealRaw.isZero() ? null : Signature.decode(proposerSealRaw); + } + + private static List
extractValidators(final BytesValue validatorsRaw) { + final List
result = Lists.newArrayList(); + final int countValidators = validatorsRaw.size() / Address.SIZE; + for (int i = 0; i < countValidators; i++) { + final int startIndex = i * Address.SIZE; + result.add(Address.wrap(validatorsRaw.slice(startIndex, Address.SIZE))); + } + return result; + } + + public BytesValue encode() { + final BytesValue validatorData = BytesValues.concatenate(validators.toArray(new Address[0])); + return BytesValues.concatenate( + vanityData, + validatorData, + proposerSeal + .map(Signature::encodedBytes) + .orElse(BytesValue.wrap(new byte[Signature.BYTES_REQUIRED]))); + } + + public BytesValue getVanityData() { + return vanityData; + } + + public Optional getProposerSeal() { + return proposerSeal; + } + + public List
getValidators() { + return validators; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueHelpers.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueHelpers.java new file mode 100755 index 00000000000..6e60a0750d8 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueHelpers.java @@ -0,0 +1,73 @@ +package net.consensys.pantheon.consensus.clique; + +import net.consensys.pantheon.consensus.clique.blockcreation.CliqueProposerSelector; +import net.consensys.pantheon.consensus.common.ValidatorProvider; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +public class CliqueHelpers { + + public static Address getProposerOfBlock(final BlockHeader header) { + final CliqueExtraData extraData = CliqueExtraData.decode(header.getExtraData()); + return CliqueBlockHashing.recoverProposerAddress(header, extraData); + } + + public static List
getValidatorsOfBlock(final BlockHeader header) { + final BytesValue extraData = header.getExtraData(); + final CliqueExtraData cliqueExtraData = CliqueExtraData.decode(extraData); + return cliqueExtraData.getValidators(); + } + + public static Address getProposerForBlockAfter( + final BlockHeader parent, final VoteTallyCache voteTallyCache) { + final CliqueProposerSelector proposerSelector = new CliqueProposerSelector(voteTallyCache); + return proposerSelector.selectProposerForNextBlock(parent); + } + + public static boolean addressIsAllowedToProduceNextBlock( + final Address candidate, + final ProtocolContext protocolContext, + final BlockHeader parent) { + final VoteTally validatorProvider = + protocolContext.getConsensusState().getVoteTallyCache().getVoteTallyAtBlock(parent); + + final int minimumUnsignedPastBlocks = minimumBlocksSincePreviousSigning(validatorProvider); + + final Blockchain blockchain = protocolContext.getBlockchain(); + int unsignedBlockCount = 0; + BlockHeader localParent = parent; + + while (unsignedBlockCount < minimumUnsignedPastBlocks) { + + if (localParent.getNumber() == 0) { + return true; + } + + final Address parentSigner = CliqueHelpers.getProposerOfBlock(localParent); + if (parentSigner.equals(candidate)) { + return false; + } + unsignedBlockCount++; + + localParent = + blockchain + .getBlockHeader(localParent.getParentHash()) + .orElseThrow(() -> new IllegalStateException("The block was on a orphaned chain.")); + } + + return true; + } + + private static int minimumBlocksSincePreviousSigning(final ValidatorProvider validatorProvider) { + final int validatorCount = validatorProvider.getCurrentValidators().size(); + // The number of contiguous blocks in which a signer may only sign 1 (as taken from clique spec) + final int signerLimit = (validatorCount / 2) + 1; + return signerLimit - 1; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSchedule.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSchedule.java new file mode 100755 index 00000000000..bddec5210c5 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSchedule.java @@ -0,0 +1,67 @@ +package net.consensys.pantheon.consensus.clique; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +import java.util.Optional; + +import io.vertx.core.json.JsonObject; + +/** Defines the protocol behaviours for a blockchain using Clique. */ +public class CliqueProtocolSchedule extends MutableProtocolSchedule { + + private static final long DEFAULT_EPOCH_LENGTH = 30_000; + private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1; + private static final int DEFAULT_CHAIN_ID = 4; + + public static ProtocolSchedule create( + final JsonObject config, final KeyPair nodeKeys) { + + // Get Config Data + final Optional cliqueConfig = Optional.ofNullable(config.getJsonObject("clique")); + final long epochLength = + cliqueConfig.map(cc -> cc.getLong("epochLength")).orElse(DEFAULT_EPOCH_LENGTH); + final long blockPeriod = + cliqueConfig + .map(cc -> cc.getInteger("blockPeriodSeconds")) + .orElse(DEFAULT_BLOCK_PERIOD_SECONDS); + final int chainId = config.getInteger("chainId", DEFAULT_CHAIN_ID); + + final MutableProtocolSchedule protocolSchedule = new CliqueProtocolSchedule(); + + // TODO(tmm) replace address with passed in node data (coming later) + final CliqueProtocolSpecs specs = + new CliqueProtocolSpecs( + blockPeriod, + epochLength, + chainId, + Util.publicKeyToAddress(nodeKeys.getPublicKey()), + protocolSchedule); + + protocolSchedule.putMilestone(0, specs.frontier()); + + final Long homesteadBlockNumber = config.getLong("homesteadBlock"); + if (homesteadBlockNumber != null) { + protocolSchedule.putMilestone(homesteadBlockNumber, specs.homestead()); + } + + final Long tangerineWhistleBlockNumber = config.getLong("eip150Block"); + if (tangerineWhistleBlockNumber != null) { + protocolSchedule.putMilestone(tangerineWhistleBlockNumber, specs.tangerineWhistle()); + } + + final Long spuriousDragonBlockNumber = config.getLong("eip158Block"); + if (spuriousDragonBlockNumber != null) { + protocolSchedule.putMilestone(spuriousDragonBlockNumber, specs.spuriousDragon()); + } + + final Long byzantiumBlockNumber = config.getLong("byzantiumBlock"); + if (byzantiumBlockNumber != null) { + protocolSchedule.putMilestone(byzantiumBlockNumber, specs.byzantium()); + } + + return protocolSchedule; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecs.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecs.java new file mode 100755 index 00000000000..b11374675cd --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecs.java @@ -0,0 +1,70 @@ +package net.consensys.pantheon.consensus.clique; + +import static net.consensys.pantheon.consensus.clique.BlockHeaderValidationRulesetFactory.cliqueBlockHeaderValidator; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockBodyValidator; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockImporter; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpecBuilder; + +/** Factory for producing Clique protocol specs for given configurations and known fork points */ +public class CliqueProtocolSpecs { + + private final long secondsBetweenBlocks; + private final long epochLength; + private final int chainId; + private final Address localNodeAddress; + private final ProtocolSchedule protocolSchedule; + + public CliqueProtocolSpecs( + final long secondsBetweenBlocks, + final long epochLength, + final int chainId, + final Address localNodeAddress, + final ProtocolSchedule protocolSchedule) { + this.secondsBetweenBlocks = secondsBetweenBlocks; + this.epochLength = epochLength; + this.chainId = chainId; + this.localNodeAddress = localNodeAddress; + this.protocolSchedule = protocolSchedule; + } + + public ProtocolSpec frontier() { + return applyCliqueSpecificModifications(MainnetProtocolSpecs.frontierDefinition()); + } + + public ProtocolSpec homestead() { + return applyCliqueSpecificModifications(MainnetProtocolSpecs.homesteadDefinition()); + } + + public ProtocolSpec tangerineWhistle() { + return applyCliqueSpecificModifications(MainnetProtocolSpecs.tangerineWhistleDefinition()); + } + + public ProtocolSpec spuriousDragon() { + return applyCliqueSpecificModifications(MainnetProtocolSpecs.spuriousDragonDefinition(chainId)); + } + + public ProtocolSpec byzantium() { + return applyCliqueSpecificModifications(MainnetProtocolSpecs.byzantiumDefinition(chainId)); + } + + private ProtocolSpec applyCliqueSpecificModifications( + final ProtocolSpecBuilder specBuilder) { + final EpochManager epochManager = new EpochManager(epochLength); + return specBuilder + .changeConsensusContextType( + difficultyCalculator -> cliqueBlockHeaderValidator(secondsBetweenBlocks, epochManager), + MainnetBlockBodyValidator::new, + MainnetBlockImporter::new, + new CliqueDifficultyCalculator(localNodeAddress)) + .blockReward(Wei.ZERO) + .miningBeneficiaryCalculator(CliqueHelpers::getProposerOfBlock) + .build(protocolSchedule); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java new file mode 100755 index 00000000000..7e8af54d714 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.consensus.clique; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.common.VoteType; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import org.apache.logging.log4j.Logger; + +public class CliqueVoteTallyUpdater { + + private static final Logger LOGGER = getLogger(); + public static final Address NO_VOTE_SUBJECT = Address.wrap(BytesValue.wrap(new byte[20])); + + private final EpochManager epochManager; + + public CliqueVoteTallyUpdater(final EpochManager epochManager) { + this.epochManager = epochManager; + } + + public VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain) { + final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber(); + final long epochBlockNumber = epochManager.getLastEpochBlock(chainHeadBlockNumber); + LOGGER.info("Loading validator voting state starting from block {}", epochBlockNumber); + final BlockHeader epochBlock = blockchain.getBlockHeader(epochBlockNumber).get(); + final List
initialValidators = + CliqueExtraData.decode(epochBlock.getExtraData()).getValidators(); + final VoteTally voteTally = new VoteTally(initialValidators); + for (long blockNumber = epochBlockNumber + 1; + blockNumber <= chainHeadBlockNumber; + blockNumber++) { + updateForBlock(blockchain.getBlockHeader(blockNumber).get(), voteTally); + } + return voteTally; + } + + /** + * Update the vote tally to reflect changes caused by appending a new block to the chain. + * + * @param header the header of the block being added + * @param voteTally the vote tally to update + */ + public void updateForBlock(final BlockHeader header, final VoteTally voteTally) { + final Address candidate = header.getCoinbase(); + if (epochManager.isEpochBlock(header.getNumber())) { + // epoch blocks are not allowed to include a vote + voteTally.discardOutstandingVotes(); + return; + } + + if (!candidate.equals(NO_VOTE_SUBJECT)) { + final CliqueExtraData extraData = CliqueExtraData.decode(header.getExtraData()); + final Address proposer = CliqueBlockHashing.recoverProposerAddress(header, extraData); + voteTally.addVote(proposer, candidate, VoteType.fromNonce(header.getNonce()).get()); + } + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/VoteTallyCache.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/VoteTallyCache.java new file mode 100755 index 00000000000..cd858f4a931 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/VoteTallyCache.java @@ -0,0 +1,91 @@ +package net.consensys.pantheon.consensus.clique; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.NoSuchElementException; +import java.util.concurrent.ExecutionException; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +public class VoteTallyCache { + + private final Blockchain blockchain; + private final EpochManager epochManager; + private final CliqueVoteTallyUpdater voteTallyUpdater; + + private final Cache voteTallyCache = + CacheBuilder.newBuilder().maximumSize(100).build(); + + public VoteTallyCache( + final Blockchain blockchain, + final CliqueVoteTallyUpdater voteTallyUpdater, + final EpochManager epochManager) { + checkNotNull(blockchain); + checkNotNull(voteTallyUpdater); + checkNotNull(epochManager); + this.blockchain = blockchain; + this.voteTallyUpdater = voteTallyUpdater; + this.epochManager = epochManager; + } + + public VoteTally getVoteTallyAtBlock(final BlockHeader header) { + try { + return voteTallyCache.get(header.getHash(), () -> populateCacheUptoAndIncluding(header)); + } catch (final ExecutionException ex) { + throw new RuntimeException("Unable to determine a VoteTally object for the requested block."); + } + } + + private VoteTally populateCacheUptoAndIncluding(final BlockHeader start) { + BlockHeader header = start; + final Deque intermediateBlocks = new ArrayDeque<>(); + VoteTally voteTally = null; + + while (true) { // Will run into an epoch block (and thus a VoteTally) to break loop. + voteTally = findMostRecentAvailableVoteTally(header, intermediateBlocks); + if (voteTally != null) { + break; + } + + header = + blockchain + .getBlockHeader(header.getParentHash()) + .orElseThrow( + () -> + new NoSuchElementException( + "Supplied block was on a orphaned chain, unable to generate VoteTally.")); + } + return constructMissingCacheEntries(intermediateBlocks, voteTally); + } + + private VoteTally findMostRecentAvailableVoteTally( + final BlockHeader header, final Deque intermediateBlockHeaders) { + intermediateBlockHeaders.push(header); + VoteTally voteTally = voteTallyCache.getIfPresent(header.getParentHash()); + if ((voteTally == null) && (epochManager.isEpochBlock(header.getNumber()))) { + final CliqueExtraData extraData = CliqueExtraData.decode(header.getExtraData()); + voteTally = new VoteTally(extraData.getValidators()); + } + + return voteTally; + } + + private VoteTally constructMissingCacheEntries( + final Deque headers, final VoteTally tally) { + while (!headers.isEmpty()) { + final BlockHeader h = headers.pop(); + voteTallyUpdater.updateForBlock(h, tally); + voteTallyCache.put(h.getHash(), tally.copy()); + } + return tally; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreator.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreator.java new file mode 100755 index 00000000000..1647a31fd13 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreator.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.consensus.clique.blockcreation; + +import net.consensys.pantheon.consensus.clique.CliqueBlockHashing; +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueExtraData; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.AbstractBlockCreator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.SealableBlockHeader; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; + +import java.util.function.Function; + +public class CliqueBlockCreator extends AbstractBlockCreator { + + private final KeyPair nodeKeys; + private final ProtocolSchedule protocolSchedule; + + public CliqueBlockCreator( + final Address coinbase, + final ExtraDataCalculator extraDataCalculator, + final PendingTransactions pendingTransactions, + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final Function gasLimitCalculator, + final KeyPair nodeKeys, + final Wei minTransactionGasPrice, + final BlockHeader parentHeader) { + super( + coinbase, + extraDataCalculator, + pendingTransactions, + protocolContext, + protocolSchedule, + gasLimitCalculator, + minTransactionGasPrice, + Util.publicKeyToAddress(nodeKeys.getPublicKey()), + parentHeader); + this.nodeKeys = nodeKeys; + this.protocolSchedule = protocolSchedule; + } + + /** + * Responsible for signing (hash of) the block (including MixHash and Nonce), and then injecting + * the seal into the extraData. This is called after a suitable set of transactions have been + * identified, and all resulting hashes have been inserted into the passed-in SealableBlockHeader. + * + * @param sealableBlockHeader A block header containing StateRoots, TransactionHashes etc. + * @return The blockhead which is to be added to the block being proposed. + */ + @Override + protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) { + + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + + final BlockHeaderBuilder builder = + BlockHeaderBuilder.create() + .populateFrom(sealableBlockHeader) + .mixHash(Hash.ZERO) + .nonce(0) + .blockHashFunction(blockHashFunction); + + final CliqueExtraData sealedExtraData = constructSignedExtraData(builder.buildBlockHeader()); + + // Replace the extraData in the BlockHeaderBuilder, and return header. + return builder.extraData(sealedExtraData.encode()).buildBlockHeader(); + } + + /** + * Produces a CliqueExtraData object with a populated proposerSeal. The signature in the block is + * generated from the Hash of the header (minus proposer and committer seals) and the nodeKeys. + * + * @param headerToSign An almost fully populated header (proposer and committer seals are empty) + * @return Extra data containing the same vanity data and validators as extraData, however + * proposerSeal will also be populated. + */ + private CliqueExtraData constructSignedExtraData(final BlockHeader headerToSign) { + final CliqueExtraData extraData = CliqueExtraData.decode(headerToSign.getExtraData()); + final Hash hashToSign = + CliqueBlockHashing.calculateDataHashForProposerSeal(headerToSign, extraData); + return new CliqueExtraData( + extraData.getVanityData(), SECP256K1.sign(hashToSign, nodeKeys), extraData.getValidators()); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockScheduler.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockScheduler.java new file mode 100755 index 00000000000..fab3cdb60e2 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockScheduler.java @@ -0,0 +1,56 @@ +package net.consensys.pantheon.consensus.clique.blockcreation; + +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.ValidatorProvider; +import net.consensys.pantheon.ethereum.blockcreation.BaseBlockScheduler; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.time.Clock; + +import java.util.Random; + +import com.google.common.annotations.VisibleForTesting; + +public class CliqueBlockScheduler extends BaseBlockScheduler { + + private final int OUT_OF_TURN_DELAY_MULTIPLIER_MILLIS = 500; + + private final VoteTallyCache voteTallyCache; + private final Address localNodeAddress; + private final long secondsBetweenBlocks; + + public CliqueBlockScheduler( + final Clock clock, + final VoteTallyCache voteTallyCache, + final Address localNodeAddress, + final long secondsBetweenBlocks) { + super(clock); + this.voteTallyCache = voteTallyCache; + this.localNodeAddress = localNodeAddress; + this.secondsBetweenBlocks = secondsBetweenBlocks; + } + + @Override + @VisibleForTesting + public BlockCreationTimeResult getNextTimestamp(final BlockHeader parentHeader) { + final long nextHeaderTimestamp = parentHeader.getTimestamp() + secondsBetweenBlocks; + long milliSecondsUntilNextBlock = (nextHeaderTimestamp * 1000) - clock.millisecondsSinceEpoch(); + + final CliqueProposerSelector proposerSelector = new CliqueProposerSelector(voteTallyCache); + final Address nextSelector = proposerSelector.selectProposerForNextBlock(parentHeader); + if (!nextSelector.equals(localNodeAddress)) { + milliSecondsUntilNextBlock += + calculatorOutOfTurnDelay(voteTallyCache.getVoteTallyAtBlock(parentHeader)); + } + + return new BlockCreationTimeResult( + nextHeaderTimestamp, Math.max(0, milliSecondsUntilNextBlock)); + } + + private int calculatorOutOfTurnDelay(final ValidatorProvider validators) { + int countSigners = validators.getCurrentValidators().size(); + int maxDelay = ((countSigners / 2) + 1) * OUT_OF_TURN_DELAY_MULTIPLIER_MILLIS; + Random r = new Random(); + return r.nextInt((maxDelay) + 1); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelector.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelector.java new file mode 100755 index 00000000000..f1498c64d4b --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelector.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.consensus.clique.blockcreation; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; + +import java.util.ArrayList; +import java.util.List; + +/** + * Responsible for determining which member of the validator pool should create the next block. + * + *

It does this be determining the available validators at the previous block, then selecting the + * appropriate validator based on the chain height. + */ +public class CliqueProposerSelector { + + private final VoteTallyCache voteTallyCache; + + public CliqueProposerSelector(final VoteTallyCache voteTallyCache) { + checkNotNull(voteTallyCache); + this.voteTallyCache = voteTallyCache; + } + + /** + * Determines which validator should create the block after that supplied. + * + * @param parentHeader The header of the previously received block. + * @return The address of the node which is to propose a block for the provided Round. + */ + public Address selectProposerForNextBlock(final BlockHeader parentHeader) { + + final VoteTally parentVoteTally = voteTallyCache.getVoteTallyAtBlock(parentHeader); + final List

validatorSet = new ArrayList<>(parentVoteTally.getCurrentValidators()); + + final long nextBlockNumber = parentHeader.getNumber() + 1L; + final int indexIntoValidators = (int) (nextBlockNumber % validatorSet.size()); + + return validatorSet.get(indexIntoValidators); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRule.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRule.java new file mode 100755 index 00000000000..61e4f19ec50 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRule.java @@ -0,0 +1,32 @@ +package net.consensys.pantheon.consensus.clique.headervalidationrules; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueDifficultyCalculator; +import net.consensys.pantheon.consensus.clique.CliqueHelpers; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule; + +import java.math.BigInteger; + +public class CliqueDifficultyValidationRule + implements AttachedBlockHeaderValidationRule { + + @Override + public boolean validate( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext protocolContext) { + final Address actualBlockCreator = CliqueHelpers.getProposerOfBlock(header); + + final CliqueDifficultyCalculator diffCalculator = + new CliqueDifficultyCalculator(actualBlockCreator); + final BigInteger expectedDifficulty = diffCalculator.nextDifficulty(0, parent, protocolContext); + + final BigInteger actualDifficulty = + new BigInteger(1, header.getDifficulty().getBytes().extractArray()); + + return expectedDifficulty.equals(actualDifficulty); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRule.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRule.java new file mode 100755 index 00000000000..f71e6c8ddd1 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRule.java @@ -0,0 +1,92 @@ +package net.consensys.pantheon.consensus.clique.headervalidationrules; + +import net.consensys.pantheon.consensus.clique.CliqueBlockHashing; +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueExtraData; +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule; +import net.consensys.pantheon.ethereum.rlp.RLPException; + +import java.util.Collection; + +import com.google.common.collect.Iterables; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class CliqueExtraDataValidationRule + implements AttachedBlockHeaderValidationRule { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final EpochManager epochManager; + + public CliqueExtraDataValidationRule(final EpochManager epochManager) { + this.epochManager = epochManager; + } + + /** + * Responsible for determining the validity of the extra data field. Ensures: + * + *
    + *
  • Bytes in the extra data field can be decoded as per Clique specification + *
  • Proposer (derived from the proposerSeal) is a member of the validators + *
  • Validators are only validated on epoch blocks. + *
+ * + * @param header the block header containing the extraData to be validated. + * @return True if the extraData successfully produces an CliqueExtraData object, false otherwise + */ + @Override + public boolean validate( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext protocolContext) { + try { + final VoteTally validatorProvider = + protocolContext.getConsensusState().getVoteTallyCache().getVoteTallyAtBlock(parent); + + final Collection
storedValidators = validatorProvider.getCurrentValidators(); + return extraDataIsValid(storedValidators, header); + + } catch (final RLPException ex) { + LOGGER.trace("ExtraData field was unable to be deserialised into an Clique Struct.", ex); + return false; + } catch (final IllegalArgumentException ex) { + LOGGER.trace("Failed to verify extra data", ex); + return false; + } + } + + private boolean extraDataIsValid( + final Collection
expectedValidators, final BlockHeader header) { + + final CliqueExtraData cliqueExtraData = CliqueExtraData.decode(header.getExtraData()); + final Address proposer = CliqueBlockHashing.recoverProposerAddress(header, cliqueExtraData); + + if (!expectedValidators.contains(proposer)) { + LOGGER.trace("Proposer sealing block is not a member of the validators."); + return false; + } + + if (epochManager.isEpochBlock(header.getNumber())) { + if (!Iterables.elementsEqual(cliqueExtraData.getValidators(), expectedValidators)) { + LOGGER.trace( + "Incorrect validators. Expected {} but got {}.", + expectedValidators, + cliqueExtraData.getValidators()); + return false; + } + } else { + if (!cliqueExtraData.getValidators().isEmpty()) { + LOGGER.trace("Validator list on non-epoch blocks must be empty."); + return false; + } + } + + return true; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java new file mode 100755 index 00000000000..d4b276cf73f --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.consensus.clique.headervalidationrules; + +import net.consensys.pantheon.consensus.clique.CliqueVoteTallyUpdater; +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; + +public class CoinbaseHeaderValidationRule implements DetachedBlockHeaderValidationRule { + + private final EpochManager epochManager; + + public CoinbaseHeaderValidationRule(final EpochManager epochManager) { + this.epochManager = epochManager; + } + + @Override + // The coinbase field is used for voting nodes in/out of the validator group. However, no votes + // are allowed to be cast on epoch blocks + public boolean validate(final BlockHeader header, final BlockHeader parent) { + if (epochManager.isEpochBlock(header.getNumber())) { + return header.getCoinbase().equals(CliqueVoteTallyUpdater.NO_VOTE_SUBJECT); + } + return true; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRule.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRule.java new file mode 100755 index 00000000000..084ea211a0a --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRule.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.consensus.clique.headervalidationrules; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueHelpers; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule; + +public class SignerRateLimitValidationRule + implements AttachedBlockHeaderValidationRule { + + @Override + public boolean validate( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext protocolContext) { + final Address blockSigner = CliqueHelpers.getProposerOfBlock(header); + + return CliqueHelpers.addressIsAllowedToProduceNextBlock(blockSigner, protocolContext, parent); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java new file mode 100755 index 00000000000..05a5e2f6f22 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.jsonrpc.methods.CliqueGetSigners; +import net.consensys.pantheon.consensus.clique.jsonrpc.methods.Discard; +import net.consensys.pantheon.consensus.clique.jsonrpc.methods.Propose; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; + +import java.util.HashMap; +import java.util.Map; + +public class CliqueJsonRpcMethodsFactory { + + public Map methods(final ProtocolContext context) { + final MutableBlockchain blockchain = context.getBlockchain(); + final WorldStateArchive worldStateArchive = context.getWorldStateArchive(); + final BlockchainQueries blockchainQueries = + new BlockchainQueries(blockchain, worldStateArchive); + final JsonRpcParameter jsonRpcParameter = new JsonRpcParameter(); + final CliqueGetSigners cliqueGetSigners = + new CliqueGetSigners(blockchainQueries, jsonRpcParameter); + + final Propose proposeRpc = + new Propose(context.getConsensusState().getVoteProposer(), jsonRpcParameter); + final Discard discardRpc = + new Discard(context.getConsensusState().getVoteProposer(), jsonRpcParameter); + + final Map rpcMethods = new HashMap<>(); + rpcMethods.put(cliqueGetSigners.getName(), cliqueGetSigners); + rpcMethods.put(proposeRpc.getName(), proposeRpc); + rpcMethods.put(discardRpc.getName(), discardRpc); + return rpcMethods; + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSigners.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSigners.java new file mode 100755 index 00000000000..aa1bf14b187 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSigners.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import static net.consensys.pantheon.consensus.clique.CliqueHelpers.getValidatorsOfBlock; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Optional; + +public class CliqueGetSigners implements JsonRpcMethod { + public static final String CLIQUE_GET_SIGNERS = "clique_getSigners"; + private final BlockchainQueries blockchainQueries; + private final JsonRpcParameter parameters; + + public CliqueGetSigners( + final BlockchainQueries blockchainQueries, final JsonRpcParameter parameter) { + this.blockchainQueries = blockchainQueries; + this.parameters = parameter; + } + + @Override + public String getName() { + return CLIQUE_GET_SIGNERS; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Optional blockHeader = blockHeader(request); + return blockHeader + .map( + bh -> new JsonRpcSuccessResponse(request.getId(), getValidatorsOfBlock(bh))) + .orElse(new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR)); + } + + private Optional blockHeader(final JsonRpcRequest request) { + final Optional blockParameter = + parameters.optional(request.getParams(), 0, BlockParameter.class); + final long latest = blockchainQueries.headBlockNumber(); + final long blockNumber = blockParameter.map(b -> b.getNumber().orElse(latest)).orElse(latest); + return blockchainQueries.blockByNumber(blockNumber).map(BlockWithMetadata::getHeader); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java new file mode 100755 index 00000000000..41895b4baae --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHash.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import static net.consensys.pantheon.consensus.clique.CliqueHelpers.getValidatorsOfBlock; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Optional; + +public class CliqueGetSignersAtHash implements JsonRpcMethod { + public static final String CLIQUE_GET_SIGNERS_AT_HASH = "clique_getSignersAtHash"; + private final BlockchainQueries blockchainQueries; + private final JsonRpcParameter parameters; + + public CliqueGetSignersAtHash( + final BlockchainQueries blockchainQueries, final JsonRpcParameter parameter) { + this.blockchainQueries = blockchainQueries; + this.parameters = parameter; + } + + @Override + public String getName() { + return CLIQUE_GET_SIGNERS_AT_HASH; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Optional blockHeader = blockHeader(request); + return blockHeader + .map( + bh -> new JsonRpcSuccessResponse(request.getId(), getValidatorsOfBlock(bh))) + .orElse(new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR)); + } + + private Optional blockHeader(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + return blockchainQueries.blockByHash(hash).map(BlockWithMetadata::getHeader); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Discard.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Discard.java new file mode 100755 index 00000000000..36d3cb12fb8 --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Discard.java @@ -0,0 +1,32 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class Discard implements JsonRpcMethod { + private static final String CLIQUE_DISCARD = "clique_discard"; + private final VoteProposer proposer; + private final JsonRpcParameter parameters; + + public Discard(final VoteProposer proposer, final JsonRpcParameter parameters) { + this.proposer = proposer; + this.parameters = parameters; + } + + @Override + public String getName() { + return CLIQUE_DISCARD; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Address address = parameters.required(request.getParams(), 0, Address.class); + proposer.discard(address); + return new JsonRpcSuccessResponse(request.getId(), true); + } +} diff --git a/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Propose.java b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Propose.java new file mode 100755 index 00000000000..2c6cd76270a --- /dev/null +++ b/consensus/clique/src/main/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/Propose.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class Propose implements JsonRpcMethod { + private final VoteProposer proposer; + private final JsonRpcParameter parameters; + + public Propose(final VoteProposer proposer, final JsonRpcParameter parameters) { + this.proposer = proposer; + this.parameters = parameters; + } + + @Override + public String getName() { + return "clique_propose"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Address address = parameters.required(request.getParams(), 0, Address.class); + final Boolean auth = parameters.required(request.getParams(), 1, Boolean.class); + if (auth) { + proposer.auth(address); + } else { + proposer.drop(address); + } + // Return true regardless, the vote is always recorded + return new JsonRpcSuccessResponse(request.getId(), true); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashingTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashingTest.java new file mode 100755 index 00000000000..3e9d01d5d38 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueBlockHashingTest.java @@ -0,0 +1,116 @@ +package net.consensys.pantheon.consensus.clique; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +public class CliqueBlockHashingTest { + + public static class LoadedBlockHeader { + private final BlockHeader header; + private final Hash parsedBlockHash; + + public LoadedBlockHeader(final BlockHeader header, final Hash parsedBlockHash) { + this.header = header; + this.parsedBlockHash = parsedBlockHash; + } + + public BlockHeader getHeader() { + return header; + } + + public Hash getParsedBlockHash() { + return parsedBlockHash; + } + } + + private LoadedBlockHeader expectedHeader = null; + + // clique.getSignersAtHash("0x8b27a29300811af926039b90288d3d384dcb55931049c17c4f762e45c116776e") + private static final List
VALIDATORS_IN_HEADER = + Arrays.asList( + Address.fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"), + Address.fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"), + Address.fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0")); + private static final Hash KNOWN_BLOCK_HASH = + Hash.fromHexString("0x8b27a29300811af926039b90288d3d384dcb55931049c17c4f762e45c116776e"); + + @Before + public void setup() { + expectedHeader = createKnownHeaderFromCapturedData(); + } + + @Test + public void recoverProposerAddressFromSeal() { + final CliqueExtraData cliqueExtraData = + CliqueExtraData.decode(expectedHeader.getHeader().getExtraData()); + final Address proposerAddress = + CliqueBlockHashing.recoverProposerAddress(expectedHeader.getHeader(), cliqueExtraData); + + assertThat(VALIDATORS_IN_HEADER.contains(proposerAddress)).isTrue(); + } + + @Test + public void readValidatorListFromExtraData() { + final CliqueExtraData cliqueExtraData = + CliqueExtraData.decode(expectedHeader.getHeader().getExtraData()); + assertThat(cliqueExtraData.getValidators()).isEqualTo(VALIDATORS_IN_HEADER); + } + + @Test + public void calculateBlockHash() { + assertThat(expectedHeader.getHeader().getHash()).isEqualTo(KNOWN_BLOCK_HASH); + } + + private LoadedBlockHeader createKnownHeaderFromCapturedData() { + // The following text was a dump from the geth console of the 30_000 block on Rinkeby. + // eth.getBlock(30000) + final BlockHeaderBuilder builder = new BlockHeaderBuilder(); + builder.difficulty(UInt256.of(2)); + builder.extraData( + BytesValue.fromHexString( + "0xd783010600846765746887676f312e372e33856c696e7578000000000000000042eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f0c5bc40d0535af16266714ccb26fc49448c10bdf2969411514707d7442956b3397b09a980f4bea9347f70eea52183326247a0239b6d01fa0b07afc44e8a05463301")); + builder.gasLimit(4712388); + builder.gasUsed(0); + // Do not do Hash. + builder.logsBloom( + LogsBloomFilter.fromHexString( + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")); + builder.coinbase(Address.fromHexString("0x0000000000000000000000000000000000000000")); + builder.mixHash( + Hash.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000")); + builder.nonce(0); + builder.number(30000); + builder.parentHash( + Hash.fromHexString("0xff570bb9893cb9bac64e346419fb9ad51e203c1cf6da5cfcc0c0dff3351b454b")); + builder.receiptsRoot( + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); + builder.ommersHash( + Hash.fromHexString("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")); + builder.stateRoot( + Hash.fromHexString("0x829b58faacea7b0aa625617ba90c93e08150bed160364a7a96505e8205043d34")); + builder.timestamp(1492460444); + builder.transactionsRoot( + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); + + final Hash parsedHash = + Hash.fromHexString("0x8b27a29300811af926039b90288d3d384dcb55931049c17c4f762e45c116776e"); + + builder.blockHashFunction(MainnetBlockHashFunction::createHash); + + return new LoadedBlockHeader(builder.buildBlockHeader(), parsedHash); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculatorTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculatorTest.java new file mode 100755 index 00000000000..103e30043d8 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueDifficultyCalculatorTest.java @@ -0,0 +1,69 @@ +package net.consensys.pantheon.consensus.clique; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Util; + +import java.math.BigInteger; +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class CliqueDifficultyCalculatorTest { + + private final KeyPair proposerKeyPair = KeyPair.generate(); + private Address localAddr; + + private final List
validatorList = Lists.newArrayList(); + private ProtocolContext cliqueProtocolContext; + private BlockHeaderTestFixture blockHeaderBuilder; + + @Before + public void setup() { + localAddr = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + validatorList.add(localAddr); + validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, 1)); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(null, null, cliqueContext); + blockHeaderBuilder = new BlockHeaderTestFixture(); + } + + @Test + public void inTurnValidatorProducesDifficultyOfTwo() { + final CliqueDifficultyCalculator calculator = new CliqueDifficultyCalculator(localAddr); + + final BlockHeader parentHeader = blockHeaderBuilder.number(1).buildHeader(); + + assertThat(calculator.nextDifficulty(0, parentHeader, cliqueProtocolContext)) + .isEqualTo(BigInteger.valueOf(2)); + } + + @Test + public void outTurnValidatorProducesDifficultyOfOne() { + final CliqueDifficultyCalculator calculator = new CliqueDifficultyCalculator(localAddr); + + final BlockHeader parentHeader = blockHeaderBuilder.number(2).buildHeader(); + + assertThat(calculator.nextDifficulty(0, parentHeader, cliqueProtocolContext)) + .isEqualTo(BigInteger.valueOf(1)); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueExtraDataTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueExtraDataTest.java new file mode 100755 index 00000000000..de2004e7e84 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueExtraDataTest.java @@ -0,0 +1,79 @@ +package net.consensys.pantheon.consensus.clique; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +public class CliqueExtraDataTest { + + @Test + public void encodeAndDecodingDoNotAlterData() { + final Signature proposerSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); + final List
validators = + Arrays.asList( + AddressHelpers.ofValue(1), AddressHelpers.ofValue(2), AddressHelpers.ofValue(3)); + final BytesValue vanityData = BytesValue.fromHexString("11223344", 32); + + final CliqueExtraData extraData = new CliqueExtraData(vanityData, proposerSeal, validators); + + final BytesValue serialisedData = extraData.encode(); + + final CliqueExtraData decodedExtraData = CliqueExtraData.decode(serialisedData); + + assertThat(decodedExtraData.getValidators()).isEqualTo(validators); + assertThat(decodedExtraData.getProposerSeal().get()).isEqualTo(proposerSeal); + assertThat(decodedExtraData.getVanityData()).isEqualTo(vanityData); + } + + @Test + public void parseRinkebyGenesisBlockExtraData() { + // Rinkeby gensis block extra data text found @ rinkeby.io + final byte[] genesisBlockExtraData = + Hex.decode( + "52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + + final BytesValue bufferToInject = BytesValue.wrap(genesisBlockExtraData); + + final CliqueExtraData extraData = CliqueExtraData.decode(bufferToInject); + assertThat(extraData.getProposerSeal()).isEmpty(); + assertThat(extraData.getValidators().size()).isEqualTo(3); + } + + @Test + public void insufficientDataResultsInAnIllegalArgumentException() { + final BytesValue illegalData = + BytesValue.wrap( + new byte[Signature.BYTES_REQUIRED + CliqueExtraData.EXTRA_VANITY_LENGTH - 1]); + + assertThatThrownBy(() -> CliqueExtraData.decode(illegalData)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid BytesValue supplied - too short to produce a valid Clique Extra Data object."); + } + + @Test + public void sufficientlyLargeButIllegallySizedInputThrowsException() { + final BytesValue illegalData = + BytesValue.wrap( + new byte + [Signature.BYTES_REQUIRED + + CliqueExtraData.EXTRA_VANITY_LENGTH + + Address.SIZE + - 1]); + + assertThatThrownBy(() -> CliqueExtraData.decode(illegalData)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("BytesValue is of invalid size - i.e. contains unused bytes."); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolScheduleTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolScheduleTest.java new file mode 100755 index 00000000000..d9ce36aea1c --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolScheduleTest.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.consensus.clique; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; + +import io.vertx.core.json.JsonObject; +import org.junit.Test; + +public class CliqueProtocolScheduleTest { + + @Test + public void protocolSpecsAreCreatedAtBlockDefinedInJson() { + final String jsonInput = + "{\"chainId\": 4,\n" + + "\"homesteadBlock\": 1,\n" + + "\"eip150Block\": 2,\n" + + "\"eip155Block\": 3,\n" + + "\"eip158Block\": 3,\n" + + "\"byzantiumBlock\": 1035301}"; + + final JsonObject jsonObject = new JsonObject(jsonInput); + + final ProtocolSchedule protocolSchedule = + CliqueProtocolSchedule.create(jsonObject, KeyPair.generate()); + + final ProtocolSpec homesteadSpec = protocolSchedule.getByBlockNumber(1); + final ProtocolSpec tangerineWhistleSpec = protocolSchedule.getByBlockNumber(2); + final ProtocolSpec spuriousDragonSpec = protocolSchedule.getByBlockNumber(3); + final ProtocolSpec byzantiumSpec = protocolSchedule.getByBlockNumber(1035301); + + assertThat(homesteadSpec.equals(tangerineWhistleSpec)).isFalse(); + assertThat(tangerineWhistleSpec.equals(spuriousDragonSpec)).isFalse(); + assertThat(spuriousDragonSpec.equals(byzantiumSpec)).isFalse(); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecsTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecsTest.java new file mode 100755 index 00000000000..cda085c8247 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueProtocolSpecsTest.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.consensus.clique; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs; +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; + +import org.junit.Test; + +public class CliqueProtocolSpecsTest { + + CliqueProtocolSpecs protocolSpecs = + new CliqueProtocolSpecs( + 15, 30_000, 5, AddressHelpers.ofValue(5), new MutableProtocolSchedule<>()); + + @Test + public void homsteadParametersAlignWithMainnetWithAdjustments() { + final ProtocolSpec homestead = protocolSpecs.homestead(); + + assertThat(homestead.getName()).isEqualTo("Homestead"); + assertThat(homestead.getBlockReward()).isEqualTo(Wei.ZERO); + assertThat(homestead.getDifficultyCalculator()).isInstanceOf(CliqueDifficultyCalculator.class); + } + + @Test + public void allSpecsInheritFromMainnetCounterparts() { + final ProtocolSchedule mainnetProtocolSchedule = new MutableProtocolSchedule<>(); + + assertThat(protocolSpecs.frontier().getName()) + .isEqualTo(MainnetProtocolSpecs.frontier(mainnetProtocolSchedule).getName()); + assertThat(protocolSpecs.homestead().getName()) + .isEqualTo(MainnetProtocolSpecs.homestead(mainnetProtocolSchedule).getName()); + assertThat(protocolSpecs.tangerineWhistle().getName()) + .isEqualTo(MainnetProtocolSpecs.tangerineWhistle(mainnetProtocolSchedule).getName()); + assertThat(protocolSpecs.spuriousDragon().getName()) + .isEqualTo(MainnetProtocolSpecs.spuriousDragon(1, mainnetProtocolSchedule).getName()); + assertThat(protocolSpecs.byzantium().getName()) + .isEqualTo(MainnetProtocolSpecs.byzantium(1, mainnetProtocolSchedule).getName()); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java new file mode 100755 index 00000000000..427b86bf029 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java @@ -0,0 +1,171 @@ +package net.consensys.pantheon.consensus.clique; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.common.VoteType; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import org.junit.Test; + +public class CliqueVoteTallyUpdaterTest { + + private static final long EPOCH_LENGTH = 30_000; + public static final Signature INVALID_SEAL = + Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); + private final VoteTally voteTally = mock(VoteTally.class); + private final MutableBlockchain blockchain = mock(MutableBlockchain.class); + private final KeyPair proposerKeyPair = KeyPair.generate(); + private final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + private final Address subject = Address.fromHexString("007f4a23ca00cd043d25c2888c1aa5688f81a344"); + private final Address validator1 = + Address.fromHexString("00dae27b350bae20c5652124af5d8b5cba001ec1"); + + private final CliqueVoteTallyUpdater updater = + new CliqueVoteTallyUpdater(new EpochManager(EPOCH_LENGTH)); + + @Test + public void voteTallyUpdatedWithVoteFromBlock() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(1); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(subject); + addProposer(headerBuilder); + final BlockHeader header = headerBuilder.buildHeader(); + + updater.updateForBlock(header, voteTally); + + verify(voteTally).addVote(proposerAddress, subject, VoteType.ADD); + } + + @Test + public void voteTallyNotUpdatedWhenBlockHasNoVoteSubject() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(1); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder); + final BlockHeader header = headerBuilder.buildHeader(); + + updater.updateForBlock(header, voteTally); + + verifyZeroInteractions(voteTally); + } + + @Test + public void outstandingVotesDiscardedWhenEpochReached() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(EPOCH_LENGTH); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder); + final BlockHeader header = headerBuilder.buildHeader(); + + updater.updateForBlock(header, voteTally); + + verify(voteTally).discardOutstandingVotes(); + verifyNoMoreInteractions(voteTally); + } + + @Test + public void buildVoteTallyByExtractingValidatorsFromGenesisBlock() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(0); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder, asList(subject, validator1)); + final BlockHeader header = headerBuilder.buildHeader(); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); + when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header)); + + final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); + assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); + } + + @Test + public void buildVoteTallyByExtractingValidatorsFromEpochBlock() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(EPOCH_LENGTH); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder, asList(subject, validator1)); + final BlockHeader header = headerBuilder.buildHeader(); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); + when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header)); + + final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); + assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); + } + + @Test + public void addVotesFromBlocksAfterMostRecentEpoch() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(EPOCH_LENGTH); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder, singletonList(validator1)); + final BlockHeader epochHeader = headerBuilder.buildHeader(); + + headerBuilder.number(EPOCH_LENGTH + 1); + headerBuilder.coinbase(subject); + final BlockHeader voteBlockHeader = headerBuilder.buildHeader(); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH + 1); + when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(epochHeader)); + when(blockchain.getBlockHeader(EPOCH_LENGTH + 1)).thenReturn(Optional.of(voteBlockHeader)); + + final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); + assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); + } + + private void addProposer(final BlockHeaderTestFixture builder) { + addProposer(builder, singletonList(proposerAddress)); + } + + private void addProposer(final BlockHeaderTestFixture builder, final List
validators) { + + final CliqueExtraData initialIbftExtraData = + new CliqueExtraData( + BytesValue.wrap(new byte[CliqueExtraData.EXTRA_VANITY_LENGTH]), + INVALID_SEAL, + validators); + + builder.extraData(initialIbftExtraData.encode()); + final BlockHeader header = builder.buildHeader(); + final Hash proposerSealHash = + CliqueBlockHashing.calculateDataHashForProposerSeal(header, initialIbftExtraData); + + final Signature proposerSignature = SECP256K1.sign(proposerSealHash, proposerKeyPair); + + final CliqueExtraData sealedData = + new CliqueExtraData( + BytesValue.wrap(new byte[CliqueExtraData.EXTRA_VANITY_LENGTH]), + proposerSignature, + validators); + + builder.extraData(sealedData.encode()); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/TestHelpers.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/TestHelpers.java new file mode 100755 index 00000000000..6a1e29dc1b2 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/TestHelpers.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.consensus.clique; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +public class TestHelpers { + + public static BlockHeader createCliqueSignedBlockHeader( + final BlockHeaderTestFixture blockHeaderBuilder, + final KeyPair signer, + final List
validators) { + + final CliqueExtraData unsignedExtraData = + new CliqueExtraData(BytesValue.wrap(new byte[32]), null, validators); + blockHeaderBuilder.extraData(unsignedExtraData.encode()); + + final Hash signingHash = + CliqueBlockHashing.calculateDataHashForProposerSeal( + blockHeaderBuilder.buildHeader(), unsignedExtraData); + + final Signature proposerSignature = SECP256K1.sign(signingHash, signer); + + final CliqueExtraData signedExtraData = + new CliqueExtraData( + unsignedExtraData.getVanityData(), + proposerSignature, + unsignedExtraData.getValidators()); + + blockHeaderBuilder.extraData(signedExtraData.encode()); + + return blockHeaderBuilder.buildHeader(); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/VoteTallyCacheTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/VoteTallyCacheTest.java new file mode 100755 index 00000000000..c2761449f2e --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/VoteTallyCacheTest.java @@ -0,0 +1,135 @@ +package net.consensys.pantheon.consensus.clique; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.Arrays; + +import com.google.common.util.concurrent.UncheckedExecutionException; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class VoteTallyCacheTest { + + BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + private Block createEmptyBlock(final long blockNumber, final Hash parentHash) { + headerBuilder.number(blockNumber).parentHash(parentHash).coinbase(AddressHelpers.ofValue(0)); + return new Block( + headerBuilder.buildHeader(), new BlockBody(Lists.emptyList(), Lists.emptyList())); + } + + DefaultMutableBlockchain blockChain; + private Block genesisBlock; + private Block block_1; + private Block block_2; + + @Before + public void constructThreeBlockChain() { + headerBuilder.extraData( + new CliqueExtraData( + BytesValue.wrap(new byte[32]), + Signature.create(BigInteger.TEN, BigInteger.TEN, (byte) 1), + Lists.emptyList()) + .encode()); + + genesisBlock = createEmptyBlock(0, Hash.ZERO); + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + + blockChain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + + block_1 = createEmptyBlock(1, genesisBlock.getHeader().getHash()); + block_2 = createEmptyBlock(1, block_1.getHeader().getHash()); + + blockChain.appendBlock(block_1, Lists.emptyList()); + blockChain.appendBlock(block_2, Lists.emptyList()); + } + + @Test + public void parentBlockVoteTallysAreCachedWhenChildVoteTallyRequested() { + final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class); + final VoteTallyCache cache = + new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000)); + + // The votetallyUpdater should be invoked for the requested block, and all parents including + // the epoch (genesis) block. + final ArgumentCaptor varArgs = ArgumentCaptor.forClass(BlockHeader.class); + cache.getVoteTallyAtBlock(block_2.getHeader()); + verify(tallyUpdater, times(3)).updateForBlock(varArgs.capture(), any()); + assertThat(varArgs.getAllValues()) + .isEqualTo( + Arrays.asList(genesisBlock.getHeader(), block_1.getHeader(), block_2.getHeader())); + + reset(tallyUpdater); + + // Requesting the vote tally to the parent block should not invoke the voteTallyUpdater as the + // vote tally was cached from previous operation. + cache.getVoteTallyAtBlock(block_1.getHeader()); + verifyZeroInteractions(tallyUpdater); + + cache.getVoteTallyAtBlock(block_2.getHeader()); + verifyZeroInteractions(tallyUpdater); + } + + @Test + public void exceptionThrownIfNoParentBlockExists() { + final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class); + final VoteTallyCache cache = + new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000)); + + final Block orphanBlock = createEmptyBlock(4, Hash.ZERO); + + assertThatExceptionOfType(UncheckedExecutionException.class) + .isThrownBy(() -> cache.getVoteTallyAtBlock(orphanBlock.getHeader())) + .withMessageContaining( + "Supplied block was on a orphaned chain, unable to generate " + "VoteTally."); + } + + @Test + public void walkBackStopsWhenACachedVoteTallyIsFound() { + final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class); + final VoteTallyCache cache = + new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000)); + + // Load the Cache up to block_2 + cache.getVoteTallyAtBlock(block_2.getHeader()); + + reset(tallyUpdater); + + // Append new blocks to the chain, and ensure the walkback only goes as far as block_2. + final Block block_3 = createEmptyBlock(4, block_2.getHeader().getHash()); + // Load the Cache up to block_2 + cache.getVoteTallyAtBlock(block_3.getHeader()); + + // The votetallyUpdater should be invoked for the requested block, and all parents including + // the epoch (genesis) block. + final ArgumentCaptor varArgs = ArgumentCaptor.forClass(BlockHeader.class); + verify(tallyUpdater, times(1)).updateForBlock(varArgs.capture(), any()); + assertThat(varArgs.getAllValues()).isEqualTo(Arrays.asList(block_3.getHeader())); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreatorTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreatorTest.java new file mode 100755 index 00000000000..4c275fc0e37 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockCreatorTest.java @@ -0,0 +1,117 @@ +package net.consensys.pantheon.consensus.clique.blockcreation; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueExtraData; +import net.consensys.pantheon.consensus.clique.CliqueHelpers; +import net.consensys.pantheon.consensus.clique.CliqueProtocolSchedule; +import net.consensys.pantheon.consensus.clique.CliqueProtocolSpecs; +import net.consensys.pantheon.consensus.clique.TestHelpers; +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class CliqueBlockCreatorTest { + + private final KeyPair proposerKeyPair = KeyPair.generate(); + private final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + private final KeyPair otherKeyPair = KeyPair.generate(); + private final List
validatorList = Lists.newArrayList(); + + private final Block genesis = GenesisConfig.mainnet().getBlock(); + private final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + private final MutableBlockchain blockchain = + new DefaultMutableBlockchain(genesis, keyValueStorage, MainnetBlockHashFunction::createHash); + private final WorldStateArchive stateArchive = + new WorldStateArchive(new KeyValueStorageWorldStateStorage(keyValueStorage)); + + private ProtocolContext protocolContext; + private final MutableProtocolSchedule protocolSchedule = + new CliqueProtocolSchedule(); + + @Before + public void setup() { + final CliqueProtocolSpecs specs = + new CliqueProtocolSpecs( + 15, + 30_000, + 1, + Util.publicKeyToAddress(proposerKeyPair.getPublicKey()), + protocolSchedule); + + protocolSchedule.putMilestone(0, specs.frontier()); + + final Address otherAddress = Util.publicKeyToAddress(otherKeyPair.getPublicKey()); + validatorList.add(otherAddress); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, new VoteProposer()); + protocolContext = new ProtocolContext<>(blockchain, stateArchive, cliqueContext); + + // Add a block above the genesis + final BlockHeaderTestFixture headerTestFixture = new BlockHeaderTestFixture(); + headerTestFixture.number(1).parentHash(genesis.getHeader().getHash()); + final Block emptyBlock = + new Block( + TestHelpers.createCliqueSignedBlockHeader( + headerTestFixture, otherKeyPair, validatorList), + new BlockBody(Lists.newArrayList(), Lists.newArrayList())); + blockchain.appendBlock(emptyBlock, Lists.newArrayList()); + } + + @Test + public void proposerAddressCanBeExtractFromAConstructedBlock() { + + final CliqueExtraData extraData = + new CliqueExtraData(BytesValue.wrap(new byte[32]), null, validatorList); + + final Address coinbase = AddressHelpers.ofValue(1); + final CliqueBlockCreator blockCreator = + new CliqueBlockCreator( + coinbase, + parent -> extraData.encode(), + new PendingTransactions(5), + protocolContext, + protocolSchedule, + gasLimit -> gasLimit, + proposerKeyPair, + Wei.ZERO, + blockchain.getChainHeadHeader()); + + final Block createdBlock = blockCreator.createBlock(5L); + + assertThat(CliqueHelpers.getProposerOfBlock(createdBlock.getHeader())) + .isEqualTo(proposerAddress); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java new file mode 100755 index 00000000000..fd01248552f --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueBlockSchedulerTest.java @@ -0,0 +1,111 @@ +package net.consensys.pantheon.consensus.clique.blockcreation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.blockcreation.BaseBlockScheduler.BlockCreationTimeResult; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.util.time.Clock; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class CliqueBlockSchedulerTest { + + private final KeyPair proposerKeyPair = KeyPair.generate(); + private Address localAddr; + + private final List
validatorList = Lists.newArrayList(); + private VoteTallyCache voteTallyCache; + private BlockHeaderTestFixture blockHeaderBuilder; + + @Before + public void setup() { + localAddr = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + validatorList.add(localAddr); + validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, 1)); + + voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + + blockHeaderBuilder = new BlockHeaderTestFixture(); + } + + @Test + public void inturnValidatorWaitsExactlyBlockInterval() { + Clock clock = mock(Clock.class); + final long currentSecondsSinceEpoch = 10L; + final long secondsBetweenBlocks = 5L; + when(clock.millisecondsSinceEpoch()).thenReturn(currentSecondsSinceEpoch * 1000); + CliqueBlockScheduler scheduler = + new CliqueBlockScheduler(clock, voteTallyCache, localAddr, secondsBetweenBlocks); + + // There are 2 validators, therefore block 2 will put localAddr as the in-turn voter, therefore + // parent block should be number 1. + BlockHeader parentHeader = + blockHeaderBuilder.number(1).timestamp(currentSecondsSinceEpoch).buildHeader(); + + BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader); + + assertThat(result.getTimestampForHeader()) + .isEqualTo(currentSecondsSinceEpoch + secondsBetweenBlocks); + assertThat(result.getMillisecondsUntilValid()).isEqualTo(secondsBetweenBlocks * 1000); + } + + @Test + public void outOfturnValidatorWaitsLongerThanBlockInterval() { + Clock clock = mock(Clock.class); + final long currentSecondsSinceEpoch = 10L; + final long secondsBetweenBlocks = 5L; + when(clock.millisecondsSinceEpoch()).thenReturn(currentSecondsSinceEpoch * 1000); + CliqueBlockScheduler scheduler = + new CliqueBlockScheduler(clock, voteTallyCache, localAddr, secondsBetweenBlocks); + + // There are 2 validators, therefore block 3 will put localAddr as the out-turn voter, therefore + // parent block should be number 2. + BlockHeader parentHeader = + blockHeaderBuilder.number(2).timestamp(currentSecondsSinceEpoch).buildHeader(); + + BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader); + + assertThat(result.getTimestampForHeader()) + .isEqualTo(currentSecondsSinceEpoch + secondsBetweenBlocks); + assertThat(result.getMillisecondsUntilValid()).isGreaterThan(secondsBetweenBlocks * 1000); + } + + @Test + public void inTurnValidatorCreatesBlockNowIFParentTimestampSufficientlyBehindNow() { + Clock clock = mock(Clock.class); + final long currentSecondsSinceEpoch = 10L; + final long secondsBetweenBlocks = 5L; + when(clock.millisecondsSinceEpoch()).thenReturn(currentSecondsSinceEpoch * 1000); + CliqueBlockScheduler scheduler = + new CliqueBlockScheduler(clock, voteTallyCache, localAddr, secondsBetweenBlocks); + + // There are 2 validators, therefore block 2 will put localAddr as the in-turn voter, therefore + // parent block should be number 1. + BlockHeader parentHeader = + blockHeaderBuilder + .number(1) + .timestamp(currentSecondsSinceEpoch - secondsBetweenBlocks) + .buildHeader(); + + BlockCreationTimeResult result = scheduler.getNextTimestamp(parentHeader); + + assertThat(result.getTimestampForHeader()).isEqualTo(currentSecondsSinceEpoch); + assertThat(result.getMillisecondsUntilValid()).isEqualTo(0); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelectorTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelectorTest.java new file mode 100755 index 00000000000..d1980579bbb --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/blockcreation/CliqueProposerSelectorTest.java @@ -0,0 +1,50 @@ +package net.consensys.pantheon.consensus.clique.blockcreation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +public class CliqueProposerSelectorTest { + + private final List
validatorList = + Arrays.asList( + AddressHelpers.ofValue(1), + AddressHelpers.ofValue(2), + AddressHelpers.ofValue(3), + AddressHelpers.ofValue(4)); + private final VoteTally voteTally = new VoteTally(validatorList); + private VoteTallyCache voteTallyCache; + + @Before + public void setup() { + voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(voteTally); + } + + @Test + public void proposerForABlockIsBasedOnModBlockNumber() { + final BlockHeaderTestFixture headerBuilderFixture = new BlockHeaderTestFixture(); + + for (int prevBlockNumber = 0; prevBlockNumber < 10; prevBlockNumber++) { + headerBuilderFixture.number(prevBlockNumber); + final CliqueProposerSelector selector = new CliqueProposerSelector(voteTallyCache); + final Address nextProposer = + selector.selectProposerForNextBlock(headerBuilderFixture.buildHeader()); + assertThat(nextProposer) + .isEqualTo(validatorList.get((prevBlockNumber + 1) % validatorList.size())); + } + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRuleTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRuleTest.java new file mode 100755 index 00000000000..3b1d2656b51 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueDifficultyValidationRuleTest.java @@ -0,0 +1,139 @@ +package net.consensys.pantheon.consensus.clique.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.clique.CliqueBlockHashing; +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueExtraData; +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class CliqueDifficultyValidationRuleTest { + + private final KeyPair proposerKeyPair = KeyPair.generate(); + private final List
validatorList = Lists.newArrayList(); + private ProtocolContext cliqueProtocolContext; + private BlockHeaderTestFixture blockHeaderBuilder; + + @Before + public void setup() { + final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + validatorList.add(localAddress); + validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddress, 1)); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(null, null, cliqueContext); + blockHeaderBuilder = new BlockHeaderTestFixture(); + } + + private BlockHeader createCliqueSignedBlock(final BlockHeaderTestFixture blockHeaderBuilder) { + + final CliqueExtraData unsignedExtraData = + new CliqueExtraData(BytesValue.wrap(new byte[32]), null, validatorList); + blockHeaderBuilder.extraData(unsignedExtraData.encode()); + + final Hash signingHash = + CliqueBlockHashing.calculateDataHashForProposerSeal( + blockHeaderBuilder.buildHeader(), unsignedExtraData); + + final Signature proposerSignature = SECP256K1.sign(signingHash, proposerKeyPair); + + final CliqueExtraData signedExtraData = + new CliqueExtraData( + unsignedExtraData.getVanityData(), + proposerSignature, + unsignedExtraData.getValidators()); + + blockHeaderBuilder.extraData(signedExtraData.encode()); + + return blockHeaderBuilder.buildHeader(); + } + + @Test + public void isTrueIfInTurnValidatorSuppliesDifficultyOfTwo() { + final long IN_TURN_BLOCK_NUMBER = validatorList.size(); // i.e. proposer is 'in turn' + final UInt256 REPORTED_DIFFICULTY = UInt256.of(2); + + blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER - 1L); + final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder); + + blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY); + final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder); + + final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule(); + assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext)).isTrue(); + } + + @Test + public void isTrueIfOutTurnValidatorSuppliesDifficultyOfOne() { + final long OUT_OF_TURN_BLOCK_NUMBER = validatorList.size() - 1L; + final UInt256 REPORTED_DIFFICULTY = UInt256.of(1); + + blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER - 1L); + final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder); + + blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY); + final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder); + + final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule(); + assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext)).isTrue(); + } + + @Test + public void isFalseIfOutTurnValidatorSuppliesDifficultyOfTwo() { + final long OUT_OF_TURN_BLOCK_NUMBER = validatorList.size() - 1L; + final UInt256 REPORTED_DIFFICULTY = UInt256.of(2); + + blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER - 1L); + final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder); + + blockHeaderBuilder.number(OUT_OF_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY); + final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder); + + final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule(); + assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext)) + .isFalse(); + } + + @Test + public void isFalseIfInTurnValidatorSuppliesDifficultyOfOne() { + final long IN_TURN_BLOCK_NUMBER = validatorList.size(); + final UInt256 REPORTED_DIFFICULTY = UInt256.of(1); + + blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER - 1L); + final BlockHeader parentHeader = createCliqueSignedBlock(blockHeaderBuilder); + + blockHeaderBuilder.number(IN_TURN_BLOCK_NUMBER).difficulty(REPORTED_DIFFICULTY); + final BlockHeader newBlock = createCliqueSignedBlock(blockHeaderBuilder); + + final CliqueDifficultyValidationRule diffValidationRule = new CliqueDifficultyValidationRule(); + assertThat(diffValidationRule.validate(newBlock, parentHeader, cliqueProtocolContext)) + .isFalse(); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRuleTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRuleTest.java new file mode 100755 index 00000000000..c341f5a8bcc --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/CliqueExtraDataValidationRuleTest.java @@ -0,0 +1,136 @@ +package net.consensys.pantheon.consensus.clique.headervalidationrules; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueExtraData; +import net.consensys.pantheon.consensus.clique.TestHelpers; +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class CliqueExtraDataValidationRuleTest { + + private final KeyPair proposerKeyPair = KeyPair.generate(); + private Address localAddr; + + private final List
validatorList = Lists.newArrayList(); + private ProtocolContext cliqueProtocolContext; + + @Before + public void setup() { + localAddr = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + validatorList.add(localAddr); + validatorList.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, 1)); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, null); + cliqueProtocolContext = new ProtocolContext<>(null, null, cliqueContext); + } + + @Test + public void missingSignerFailsValidation() { + final CliqueExtraData extraData = + new CliqueExtraData(BytesValue.wrap(new byte[32]), null, Lists.newArrayList()); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final BlockHeader parent = headerBuilder.number(1).buildHeader(); + final BlockHeader child = headerBuilder.number(2).extraData(extraData.encode()).buildHeader(); + + final CliqueExtraDataValidationRule rule = + new CliqueExtraDataValidationRule(new EpochManager(10)); + + assertThat(rule.validate(child, parent, cliqueProtocolContext)).isFalse(); + } + + @Test + public void signerNotInExpectedValidatorsFailsValidation() { + final KeyPair otherSigner = KeyPair.generate(); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final BlockHeader parent = headerBuilder.number(1).buildHeader(); + headerBuilder.number(2); + final BlockHeader badlySignedChild = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, otherSigner, Lists.newArrayList()); + + final CliqueExtraDataValidationRule rule = + new CliqueExtraDataValidationRule(new EpochManager(10)); + assertThat(rule.validate(badlySignedChild, parent, cliqueProtocolContext)).isFalse(); + } + + @Test + public void signerIsInValidatorsAndValidatorsNotPresentWhenNotEpochIsSuccessful() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final BlockHeader parent = headerBuilder.number(1).buildHeader(); + headerBuilder.number(2); + final BlockHeader correctlySignedChild = + TestHelpers.createCliqueSignedBlockHeader( + headerBuilder, proposerKeyPair, Lists.newArrayList()); + + final CliqueExtraDataValidationRule rule = + new CliqueExtraDataValidationRule(new EpochManager(10)); + assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isTrue(); + } + + @Test + public void epochBlockContainsSameValidatorsAsContextIsSuccessful() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final BlockHeader parent = headerBuilder.number(9).buildHeader(); + headerBuilder.number(10); + final BlockHeader correctlySignedChild = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + + final CliqueExtraDataValidationRule rule = + new CliqueExtraDataValidationRule(new EpochManager(10)); + assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isTrue(); + } + + @Test + public void epochBlockWithMisMatchingListOfValidatorsFailsValidation() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final BlockHeader parent = headerBuilder.number(9).buildHeader(); + headerBuilder.number(10); + final BlockHeader correctlySignedChild = + TestHelpers.createCliqueSignedBlockHeader( + headerBuilder, + proposerKeyPair, + Lists.newArrayList(AddressHelpers.ofValue(1), AddressHelpers.ofValue(2), localAddr)); + + final CliqueExtraDataValidationRule rule = + new CliqueExtraDataValidationRule(new EpochManager(10)); + assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isFalse(); + } + + @Test + public void nonEpochBlockContainingValidatorsFailsValidation() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final BlockHeader parent = headerBuilder.number(8).buildHeader(); + headerBuilder.number(9); + final BlockHeader correctlySignedChild = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + + final CliqueExtraDataValidationRule rule = + new CliqueExtraDataValidationRule(new EpochManager(10)); + assertThat(rule.validate(correctlySignedChild, parent, cliqueProtocolContext)).isFalse(); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRuleTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRuleTest.java new file mode 100755 index 00000000000..0da9c01d6e3 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/headervalidationrules/SignerRateLimitValidationRuleTest.java @@ -0,0 +1,258 @@ +package net.consensys.pantheon.consensus.clique.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.TestHelpers; +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class SignerRateLimitValidationRuleTest { + + private final KeyPair proposerKeyPair = KeyPair.generate(); + private final KeyPair otherNodeKeyPair = KeyPair.generate(); + private final List
validatorList = Lists.newArrayList(); + private final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + private ProtocolContext cliqueProtocolContext; + + DefaultMutableBlockchain blockChain; + private Block genesisBlock; + + private Block createEmptyBlock(final KeyPair blockSigner) { + final BlockHeader header = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, blockSigner, validatorList); + return new Block(header, new BlockBody(Lists.newArrayList(), Lists.newArrayList())); + } + + @Test + public void networkWithOneValidatorIsAllowedToCreateConsecutiveBlocks() { + final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + validatorList.add(localAddress); + + genesisBlock = createEmptyBlock(proposerKeyPair); + + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockChain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext); + + final BlockHeader nextBlockHeader = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + + final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule(); + + assertThat( + validationRule.validate( + nextBlockHeader, genesisBlock.getHeader(), cliqueProtocolContext)) + .isTrue(); + } + + @Test + public void networkWithTwoValidatorsIsAllowedToProduceBlockIfNotPreviousBlockProposer() { + final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey()); + validatorList.add(localAddress); + validatorList.add(otherAddress); + + genesisBlock = createEmptyBlock(otherNodeKeyPair); + + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockChain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext); + + final BlockHeader nextBlockHeader = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + + final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule(); + + assertThat( + validationRule.validate( + nextBlockHeader, genesisBlock.getHeader(), cliqueProtocolContext)) + .isTrue(); + } + + @Test + public void networkWithTwoValidatorsIsNotAllowedToProduceBlockIfIsPreviousBlockProposer() { + final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey()); + validatorList.add(localAddress); + validatorList.add(otherAddress); + + genesisBlock = createEmptyBlock(proposerKeyPair); + + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockChain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext); + + headerBuilder.parentHash(genesisBlock.getHash()).number(1); + final Block block_1 = createEmptyBlock(proposerKeyPair); + blockChain.appendBlock(block_1, Lists.newArrayList()); + + headerBuilder.parentHash(block_1.getHeader().getHash()).number(2); + final BlockHeader block_2 = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + + final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule(); + + assertThat(validationRule.validate(block_2, block_1.getHeader(), cliqueProtocolContext)) + .isFalse(); + } + + @Test + public void withThreeValidatorsMustHaveOneBlockBetweenSignings() { + final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey()); + validatorList.add(localAddress); + validatorList.add(otherAddress); + validatorList.add(AddressHelpers.ofValue(1)); + + genesisBlock = createEmptyBlock(proposerKeyPair); + + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockChain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext); + + headerBuilder.parentHash(genesisBlock.getHash()).number(1); + final Block block_1 = createEmptyBlock(proposerKeyPair); + blockChain.appendBlock(block_1, Lists.newArrayList()); + + headerBuilder.parentHash(block_1.getHash()).number(2); + final Block block_2 = createEmptyBlock(otherNodeKeyPair); + blockChain.appendBlock(block_1, Lists.newArrayList()); + + final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule(); + BlockHeader nextBlockHeader; + + // Should not be able to proposer ontop of Block_1 (which has the same sealer) + headerBuilder.parentHash(block_1.getHash()).number(2); + nextBlockHeader = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + assertThat(validationRule.validate(nextBlockHeader, block_1.getHeader(), cliqueProtocolContext)) + .isFalse(); + + headerBuilder.parentHash(block_1.getHash()).number(3); + nextBlockHeader = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + assertThat(validationRule.validate(nextBlockHeader, block_2.getHeader(), cliqueProtocolContext)) + .isTrue(); + } + + @Test + public void signerIsValidIfInsufficientBlocksExistInHistory() { + final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey()); + validatorList.add(localAddress); + validatorList.add(otherAddress); + validatorList.add(AddressHelpers.ofValue(1)); + + genesisBlock = createEmptyBlock(otherNodeKeyPair); + + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockChain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext); + + final BlockHeader nextBlockHeader = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + + final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule(); + + assertThat( + validationRule.validate( + nextBlockHeader, genesisBlock.getHeader(), cliqueProtocolContext)) + .isTrue(); + } + + @Test + public void exceptionIsThrownIfOnAnOrphanedChain() { + final Address localAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + final Address otherAddress = Util.publicKeyToAddress(otherNodeKeyPair.getPublicKey()); + validatorList.add(localAddress); + validatorList.add(otherAddress); + + genesisBlock = createEmptyBlock(otherNodeKeyPair); + + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockChain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + + final VoteTallyCache voteTallyCache = mock(VoteTallyCache.class); + when(voteTallyCache.getVoteTallyAtBlock(any())).thenReturn(new VoteTally(validatorList)); + final VoteProposer voteProposer = new VoteProposer(); + final CliqueContext cliqueContext = new CliqueContext(voteTallyCache, voteProposer); + cliqueProtocolContext = new ProtocolContext<>(blockChain, null, cliqueContext); + + headerBuilder.parentHash(Hash.ZERO).number(4); + final BlockHeader nextBlock = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeyPair, validatorList); + + headerBuilder.parentHash(Hash.ZERO).number(3); + final BlockHeader parentHeader = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, otherNodeKeyPair, validatorList); + + final SignerRateLimitValidationRule validationRule = new SignerRateLimitValidationRule(); + + assertThatThrownBy( + () -> validationRule.validate(nextBlock, parentHeader, cliqueProtocolContext)) + .isInstanceOf(RuntimeException.class) + .hasMessage("The block was on a orphaned chain."); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java new file mode 100755 index 00000000000..652ef9f7114 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersAtHashTest.java @@ -0,0 +1,110 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import static java.util.Arrays.asList; +import static net.consensys.pantheon.ethereum.core.Address.fromHexString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; + +import org.assertj.core.api.AssertionsForClassTypes; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CliqueGetSignersAtHashTest { + + private CliqueGetSignersAtHash method; + private BlockHeader blockHeader; + private List
validators; + + @Mock private BlockchainQueries blockchainQueries; + + @Mock private BlockWithMetadata blockWithMetadata; + public static final String BLOCK_HASH = + "0xe36a3edf0d8664002a72ef7c5f8e271485e7ce5c66455a07cb679d855818415f"; + + @Before + public void setup() { + method = new CliqueGetSignersAtHash(blockchainQueries, new JsonRpcParameter()); + + final byte[] genesisBlockExtraData = + Hex.decode( + "52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + final BytesValue bufferToInject = BytesValue.wrap(genesisBlockExtraData); + final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture(); + blockHeader = blockHeaderTestFixture.extraData(bufferToInject).buildHeader(); + + validators = + asList( + fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"), + fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"), + fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0")); + } + + @Test + public void returnsMethodName() { + assertThat(method.getName()).isEqualTo("clique_getSignersAtHash"); + } + + @Test + @SuppressWarnings("unchecked") + public void failsWhenNoParam() { + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "clique_getSignersAtHash", new Object[] {}); + + final Throwable thrown = AssertionsForClassTypes.catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + } + + @Test + @SuppressWarnings("unchecked") + public void returnsValidatorsForBlockHash() { + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "clique_getSignersAtHash", new Object[] {BLOCK_HASH}); + + when(blockchainQueries.blockByHash(Hash.fromHexString(BLOCK_HASH))) + .thenReturn(Optional.of(blockWithMetadata)); + when(blockWithMetadata.getHeader()).thenReturn(blockHeader); + + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final List
result = (List
) response.getResult(); + assertEquals(validators, result); + } + + @Test + public void failsOnInvalidBlockHash() { + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {BLOCK_HASH}); + + when(blockchainQueries.blockByHash(Hash.fromHexString(BLOCK_HASH))) + .thenReturn(Optional.empty()); + + final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request); + assertThat(response.getError().name()).isEqualTo(JsonRpcError.INTERNAL_ERROR.name()); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java new file mode 100755 index 00000000000..b8d336ffd18 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/CliqueGetSignersTest.java @@ -0,0 +1,104 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import static java.util.Arrays.asList; +import static net.consensys.pantheon.ethereum.core.Address.fromHexString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CliqueGetSignersTest { + private CliqueGetSigners method; + private BlockHeader blockHeader; + private List
validators; + + @Mock private BlockchainQueries blockchainQueries; + + @Mock private BlockWithMetadata blockWithMetadata; + + @Before + public void setup() { + method = new CliqueGetSigners(blockchainQueries, new JsonRpcParameter()); + + final byte[] genesisBlockExtraData = + Hex.decode( + "52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + final BytesValue bufferToInject = BytesValue.wrap(genesisBlockExtraData); + final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture(); + blockHeader = blockHeaderTestFixture.extraData(bufferToInject).buildHeader(); + + validators = + asList( + fromHexString("0x42eb768f2244c8811c63729a21a3569731535f06"), + fromHexString("0x7ffc57839b00206d1ad20c69a1981b489f772031"), + fromHexString("0xb279182d99e65703f0076e4812653aab85fca0f0")); + } + + @Test + public void returnsMethodName() { + assertThat(method.getName()).isEqualTo("clique_getSigners"); + } + + @Test + @SuppressWarnings("unchecked") + public void returnsValidatorsWhenNoParam() { + final JsonRpcRequest request = new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {}); + + when(blockchainQueries.headBlockNumber()).thenReturn(3065995L); + when(blockchainQueries.blockByNumber(3065995L)).thenReturn(Optional.of(blockWithMetadata)); + when(blockWithMetadata.getHeader()).thenReturn(blockHeader); + + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final List
result = (List
) response.getResult(); + assertEquals(validators, result); + } + + @Test + @SuppressWarnings("unchecked") + public void returnsValidatorsForBlockNumber() { + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {"0x2EC88B"}); + + when(blockchainQueries.blockByNumber(3065995L)).thenReturn(Optional.of(blockWithMetadata)); + when(blockWithMetadata.getHeader()).thenReturn(blockHeader); + + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final List
result = (List
) response.getResult(); + assertEquals(validators, result); + } + + @Test + public void failsOnInvalidBlockNumber() { + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "clique_getSigners", new Object[] {"0x1234"}); + + when(blockchainQueries.blockByNumber(4660)).thenReturn(Optional.empty()); + + final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request); + assertThat(response.getError().name()).isEqualTo(JsonRpcError.INTERNAL_ERROR.name()); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/DiscardTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/DiscardTest.java new file mode 100755 index 00000000000..e9f492785bc --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/DiscardTest.java @@ -0,0 +1,103 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteProposer.Vote; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponseType; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Optional; + +import org.junit.Test; + +public class DiscardTest { + private final String JSON_RPC_VERSION = "2.0"; + private final String METHOD = "clique_discard"; + + @Test + public void discardEmpty() { + final VoteProposer proposer = new VoteProposer(); + final Discard discard = new Discard(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + final JsonRpcResponse response = discard.response(requestWithParams(a0)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.empty()); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void discardAuth() { + final VoteProposer proposer = new VoteProposer(); + final Discard discard = new Discard(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + proposer.auth(a0); + + final JsonRpcResponse response = discard.response(requestWithParams(a0)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.empty()); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void discardDrop() { + final VoteProposer proposer = new VoteProposer(); + final Discard discard = new Discard(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + proposer.drop(a0); + + final JsonRpcResponse response = discard.response(requestWithParams(a0)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.empty()); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void discardIsolation() { + final VoteProposer proposer = new VoteProposer(); + final Discard discard = new Discard(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + final Address a1 = Address.fromHexString("1"); + + proposer.auth(a0); + proposer.auth(a1); + + final JsonRpcResponse response = discard.response(requestWithParams(a0)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.empty()); + assertThat(proposer.get(a1)).isEqualTo(Optional.of(Vote.AUTH)); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void discardWithoutAddress() { + final VoteProposer proposer = new VoteProposer(); + final Discard discard = new Discard(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + assertThatThrownBy(() -> discard.response(requestWithParams())) + .hasMessage("Missing required json rpc parameter at index 0") + .isInstanceOf(InvalidJsonRpcParameters.class); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, METHOD, params); + } +} diff --git a/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/ProposeTest.java b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/ProposeTest.java new file mode 100755 index 00000000000..a17c51888f1 --- /dev/null +++ b/consensus/clique/src/test/java/net/consensys/pantheon/consensus/clique/jsonrpc/methods/ProposeTest.java @@ -0,0 +1,113 @@ +package net.consensys.pantheon.consensus.clique.jsonrpc.methods; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteProposer.Vote; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponseType; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Optional; + +import org.junit.Test; + +public class ProposeTest { + private final String JSON_RPC_VERSION = "2.0"; + private final String METHOD = "clique_propose"; + + @Test + public void testAuth() { + final VoteProposer proposer = new VoteProposer(); + final Propose propose = new Propose(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + final JsonRpcResponse response = propose.response(requestWithParams(a0, true)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.AUTH)); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void testDrop() { + final VoteProposer proposer = new VoteProposer(); + final Propose propose = new Propose(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + final JsonRpcResponse response = propose.response(requestWithParams(a0, false)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.DROP)); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void testRepeatAuth() { + final VoteProposer proposer = new VoteProposer(); + final Propose propose = new Propose(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + proposer.auth(a0); + final JsonRpcResponse response = propose.response(requestWithParams(a0, true)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.AUTH)); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void testRepeatDrop() { + final VoteProposer proposer = new VoteProposer(); + final Propose propose = new Propose(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + proposer.drop(a0); + final JsonRpcResponse response = propose.response(requestWithParams(a0, false)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.DROP)); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void testChangeToAuth() { + final VoteProposer proposer = new VoteProposer(); + final Propose propose = new Propose(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + proposer.drop(a0); + final JsonRpcResponse response = propose.response(requestWithParams(a0, true)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.AUTH)); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + @Test + public void testChangeToDrop() { + final VoteProposer proposer = new VoteProposer(); + final Propose propose = new Propose(proposer, new JsonRpcParameter()); + final Address a0 = Address.fromHexString("0"); + + proposer.auth(a0); + final JsonRpcResponse response = propose.response(requestWithParams(a0, false)); + + assertThat(proposer.get(a0)).isEqualTo(Optional.of(Vote.DROP)); + assertThat(response.getType()).isEqualTo(JsonRpcResponseType.SUCCESS); + final JsonRpcSuccessResponse successResponse = (JsonRpcSuccessResponse) response; + assertThat(successResponse.getResult()).isEqualTo(true); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, METHOD, params); + } +} diff --git a/consensus/common/build.gradle b/consensus/common/build.gradle new file mode 100755 index 00000000000..dfc5e5e4dd1 --- /dev/null +++ b/consensus/common/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-consensus-common' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + implementation project(':ethereum:core') + implementation project(':util') + implementation 'com.google.guava:guava' + + testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts') + testImplementation 'junit:junit' + testImplementation "org.assertj:assertj-core" + testImplementation 'org.mockito:mockito-core' +} diff --git a/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/EpochManager.java b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/EpochManager.java new file mode 100755 index 00000000000..3e5612a4957 --- /dev/null +++ b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/EpochManager.java @@ -0,0 +1,18 @@ +package net.consensys.pantheon.consensus.common; + +public class EpochManager { + + private final long epochLengthInBlocks; + + public EpochManager(final long epochLengthInBlocks) { + this.epochLengthInBlocks = epochLengthInBlocks; + } + + public boolean isEpochBlock(final long blockNumber) { + return (blockNumber % epochLengthInBlocks) == 0; + } + + public long getLastEpochBlock(final long blockNumber) { + return blockNumber - (blockNumber % epochLengthInBlocks); + } +} diff --git a/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/ValidatorProvider.java b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/ValidatorProvider.java new file mode 100755 index 00000000000..5229b848410 --- /dev/null +++ b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/ValidatorProvider.java @@ -0,0 +1,11 @@ +package net.consensys.pantheon.consensus.common; + +import net.consensys.pantheon.ethereum.core.Address; + +import java.util.Collection; + +public interface ValidatorProvider { + + // Returns the current list of validators + Collection
getCurrentValidators(); +} diff --git a/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteProposer.java b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteProposer.java new file mode 100755 index 00000000000..e189ac41a82 --- /dev/null +++ b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteProposer.java @@ -0,0 +1,122 @@ +package net.consensys.pantheon.consensus.common; + +import net.consensys.pantheon.ethereum.core.Address; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** Container for pending votes and selecting a vote for new blocks */ +public class VoteProposer { + public enum Vote { + AUTH, + DROP + } + + private final Map proposals = new ConcurrentHashMap<>(); + private final AtomicInteger votePosition = new AtomicInteger(0); + + /** + * Identifies an address that should be voted into the validator pool + * + * @param address The address to be voted in + */ + public void auth(final Address address) { + proposals.put(address, Vote.AUTH); + } + + /** + * Identifies an address that should be voted out of the validator pool + * + * @param address The address to be voted out + */ + public void drop(final Address address) { + proposals.put(address, Vote.DROP); + } + + /** + * Discards a pending vote for an address if one exists + * + * @param address The address that should no longer be voted for + */ + public void discard(final Address address) { + proposals.remove(address); + } + + /** Discards all pending votes */ + public void clear() { + proposals.clear(); + } + + public Optional get(final Address address) { + return Optional.ofNullable(proposals.get(address)); + } + + private boolean voteNotYetCast( + final Address localAddress, + final Address voteAddress, + final Vote vote, + final Collection
validators, + final VoteTally tally) { + + // Pre evaluate if we have a vote outstanding to auth or drop the target address + final boolean votedAuth = tally.getOutstandingAddVotesFor(voteAddress).contains(localAddress); + final boolean votedDrop = + tally.getOutstandingRemoveVotesFor(voteAddress).contains(localAddress); + + // if they're a validator, we want to see them dropped, and we haven't voted to drop them yet + if (validators.contains(voteAddress) && !votedDrop && vote == Vote.DROP) { + return true; + // or if we've previously voted to auth them and we want to drop them + } else if (votedAuth && vote == Vote.DROP) { + return true; + // if they're not currently a validator and we want to see them authed and we haven't voted to + // auth them yet + } else if (!validators.contains(voteAddress) && !votedAuth && vote == Vote.AUTH) { + return true; + // or if we've previously voted to drop them and we want to see them authed + } else if (votedDrop && vote == Vote.AUTH) { + return true; + } + + return false; + } + + /** + * Gets a valid vote from our list of pending votes + * + * @param localAddress The address of this validator node + * @param tally the vote tally at the height of the chain we need a vote for + * @return Either an address with the vote (auth or drop) or no vote if we have no valid pending + * votes + */ + public Optional> getVote( + final Address localAddress, final VoteTally tally) { + final Collection
validators = tally.getCurrentValidators(); + final List> validVotes = new ArrayList<>(); + + proposals + .entrySet() + .forEach( + proposal -> { + if (voteNotYetCast( + localAddress, proposal.getKey(), proposal.getValue(), validators, tally)) { + validVotes.add(proposal); + } + }); + + if (validVotes.isEmpty()) { + return Optional.empty(); + } + + // Get the next position in the voting queue we should propose + final int currentVotePosition = votePosition.updateAndGet(i -> ++i % validVotes.size()); + + // Get a vote from the valid votes and return it + return Optional.of(validVotes.get(currentVotePosition)); + } +} diff --git a/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteTally.java b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteTally.java new file mode 100755 index 00000000000..7653258f640 --- /dev/null +++ b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteTally.java @@ -0,0 +1,116 @@ +package net.consensys.pantheon.consensus.common; + +import net.consensys.pantheon.ethereum.core.Address; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import com.google.common.collect.Maps; + +/** Tracks the current list of validators and votes to add or drop validators. */ +public class VoteTally implements ValidatorProvider { + + private final SortedSet
currentValidators; + + private final Map> addVotesBySubject; + private final Map> removeVotesBySubject; + + public VoteTally(final List
initialValidators) { + this(new TreeSet<>(initialValidators), new HashMap<>(), new HashMap<>()); + } + + private VoteTally( + final Collection
initialValidators, + final Map> addVotesBySubject, + final Map> removeVotesBySubject) { + this.currentValidators = new TreeSet<>(initialValidators); + this.addVotesBySubject = addVotesBySubject; + this.removeVotesBySubject = removeVotesBySubject; + } + + /** + * Add a vote to the current tally. The current validator list will be updated if this vote takes + * the tally past the required votes to approve the change. + * + * @param proposer the address of the validator casting the vote via block proposal + * @param subject the validator the vote is about + * @param voteType the type of vote, either add or drop + */ + public void addVote(final Address proposer, final Address subject, final VoteType voteType) { + final Set
addVotesForSubject = + addVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>()); + final Set
removeVotesForSubject = + removeVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>()); + + if (voteType == VoteType.ADD) { + addVotesForSubject.add(proposer); + removeVotesForSubject.remove(proposer); + } else { + removeVotesForSubject.add(proposer); + addVotesForSubject.remove(proposer); + } + + final int validatorLimit = validatorLimit(); + if (addVotesForSubject.size() >= validatorLimit) { + currentValidators.add(subject); + discardOutstandingVotesFor(subject); + } + if (removeVotesForSubject.size() >= validatorLimit) { + currentValidators.remove(subject); + discardOutstandingVotesFor(subject); + addVotesBySubject.values().forEach(votes -> votes.remove(subject)); + removeVotesBySubject.values().forEach(votes -> votes.remove(subject)); + } + } + + private void discardOutstandingVotesFor(final Address subject) { + addVotesBySubject.remove(subject); + removeVotesBySubject.remove(subject); + } + + public Set
getOutstandingAddVotesFor(final Address subject) { + return Optional.ofNullable(addVotesBySubject.get(subject)).orElse(Collections.emptySet()); + } + + public Set
getOutstandingRemoveVotesFor(final Address subject) { + return Optional.ofNullable(removeVotesBySubject.get(subject)).orElse(Collections.emptySet()); + } + + private int validatorLimit() { + return (currentValidators.size() / 2) + 1; + } + + /** + * Reset the outstanding vote tallies as required at each epoch. The current validator list is + * unaffected. + */ + public void discardOutstandingVotes() { + addVotesBySubject.clear(); + } + + @Override + public Collection
getCurrentValidators() { + return currentValidators; + } + + public VoteTally copy() { + final Map> addVotesBySubject = Maps.newHashMap(); + final Map> removeVotesBySubject = Maps.newHashMap(); + + this.addVotesBySubject.forEach( + (key, value) -> addVotesBySubject.put(key, new TreeSet<>(value))); + this.removeVotesBySubject.forEach( + (key, value) -> removeVotesBySubject.put(key, new TreeSet<>(value))); + + return new VoteTally( + new TreeSet<>(this.currentValidators), addVotesBySubject, removeVotesBySubject); + } +} diff --git a/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteType.java b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteType.java new file mode 100755 index 00000000000..1a4be7212bb --- /dev/null +++ b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/VoteType.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.consensus.common; + +import java.util.Optional; + +public enum VoteType { + ADD(0x0L), + DROP(0xFFFFFFFFFFFFFFFFL); + + private final long nonceValue; + + VoteType(final long nonceValue) { + this.nonceValue = nonceValue; + } + + public long getNonceValue() { + return nonceValue; + } + + public static Optional fromNonce(final long nonce) { + for (final VoteType voteType : values()) { + if (Long.compareUnsigned(voteType.nonceValue, nonce) == 0) { + return Optional.of(voteType); + } + } + return Optional.empty(); + } +} diff --git a/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRule.java b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRule.java new file mode 100755 index 00000000000..f91c519f320 --- /dev/null +++ b/consensus/common/src/main/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRule.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.consensus.common.headervalidationrules; + +import net.consensys.pantheon.consensus.common.VoteType; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class VoteValidationRule implements DetachedBlockHeaderValidationRule { + + private static final Logger LOGGER = LogManager.getLogger(VoteValidationRule.class); + + /** + * Responsible for ensuring the nonce is either auth or drop. + * + * @param header the block header to validate + * @param parent the block header corresponding to the parent of the header being validated. + * @return true if the nonce in the header is a valid validator vote value. + */ + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + final long nonce = header.getNonce(); + if (!VoteType.fromNonce(nonce).isPresent()) { + LOGGER.trace("Nonce value ({}) is neither auth or drop.", nonce); + return false; + } + return true; + } +} diff --git a/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteProposerTest.java b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteProposerTest.java new file mode 100755 index 00000000000..d55a88a8688 --- /dev/null +++ b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteProposerTest.java @@ -0,0 +1,203 @@ +package net.consensys.pantheon.consensus.common; + +import static net.consensys.pantheon.consensus.common.VoteType.ADD; +import static net.consensys.pantheon.consensus.common.VoteType.DROP; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.consensus.common.VoteProposer.Vote; +import net.consensys.pantheon.ethereum.core.Address; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; + +import org.junit.Test; + +public class VoteProposerTest { + private final Address localAddress = Address.fromHexString("0"); + + @Test + public void emptyProposerReturnsNoVotes() { + final VoteProposer proposer = new VoteProposer(); + + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.empty()); + assertThat( + proposer.getVote( + localAddress, + new VoteTally( + Arrays.asList( + Address.fromHexString("0"), + Address.fromHexString("1"), + Address.fromHexString("2"))))) + .isEqualTo(Optional.empty()); + } + + @Test + public void demoteVotes() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + proposer.drop(a1); + + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.empty()); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.singletonList(a1)))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.DROP))); + assertThat(proposer.getVote(localAddress, new VoteTally(Arrays.asList(a1, a2, a3)))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.DROP))); + assertThat(proposer.getVote(localAddress, new VoteTally(Arrays.asList(a2, a3)))) + .isEqualTo(Optional.empty()); + } + + @Test + public void promoteVotes() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + proposer.auth(a1); + + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.singletonList(a1)))) + .isEqualTo(Optional.empty()); + assertThat(proposer.getVote(localAddress, new VoteTally(Arrays.asList(a1, a2, a3)))) + .isEqualTo(Optional.empty()); + assertThat(proposer.getVote(localAddress, new VoteTally(Arrays.asList(a2, a3)))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + } + + @Test + public void discardVotes() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + proposer.auth(a1); + proposer.auth(a2); + proposer.discard(a2); + + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.singletonList(a1)))) + .isEqualTo(Optional.empty()); + assertThat(proposer.getVote(localAddress, new VoteTally(Arrays.asList(a1, a2, a3)))) + .isEqualTo(Optional.empty()); + assertThat(proposer.getVote(localAddress, new VoteTally(Arrays.asList(a2, a3)))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + } + + @Test + public void getVoteCyclesAllOptions() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + proposer.auth(a1); + proposer.auth(a2); + proposer.auth(a3); + + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a2, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a3, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a2, Vote.AUTH))); + } + + @Test + public void getVoteSkipsInvalidVotes() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + final Address a4 = Address.fromHexString("4"); + + proposer.auth(a1); + proposer.auth(a2); + proposer.auth(a3); + proposer.drop(a4); + + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a2, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a3, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + assertThat(proposer.getVote(localAddress, new VoteTally(Collections.emptyList()))) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a2, Vote.AUTH))); + } + + @Test + public void revokesAuthVotesInTally() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + proposer.drop(a1); + + final VoteTally tally = new VoteTally(Arrays.asList(a2, a3)); + tally.addVote(localAddress, a1, ADD); + + assertThat(proposer.getVote(localAddress, tally)) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.DROP))); + } + + @Test + public void revokesDropVotesInTally() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + proposer.auth(a1); + + final VoteTally tally = new VoteTally(Arrays.asList(a2, a3)); + tally.addVote(localAddress, a1, DROP); + + assertThat(proposer.getVote(localAddress, tally)) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + } + + @Test + public void revokesAuthVotesInTallyWhenValidatorIsInValidatorList() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + final VoteTally tally = new VoteTally(Arrays.asList(a1, a2, a3)); + tally.addVote(localAddress, a1, ADD); + + proposer.drop(a1); + + assertThat(proposer.getVote(localAddress, tally)) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.DROP))); + } + + @Test + public void revokesDropVotesInTallyWhenValidatorIsInValidatorList() { + final VoteProposer proposer = new VoteProposer(); + final Address a1 = Address.fromHexString("1"); + final Address a2 = Address.fromHexString("2"); + final Address a3 = Address.fromHexString("3"); + + final VoteTally tally = new VoteTally(Arrays.asList(a1, a2, a3)); + tally.addVote(localAddress, a1, DROP); + + proposer.auth(a1); + + assertThat(proposer.getVote(localAddress, tally)) + .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, Vote.AUTH))); + } +} diff --git a/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteTallyTest.java b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteTallyTest.java new file mode 100755 index 00000000000..133561d611c --- /dev/null +++ b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteTallyTest.java @@ -0,0 +1,294 @@ +package net.consensys.pantheon.consensus.common; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Address; + +import org.junit.Test; + +public class VoteTallyTest { + + private static final Address validator1 = + Address.fromHexString("000d836201318ec6899a67540690382780743280"); + private static final Address validator2 = + Address.fromHexString("001762430ea9c3a26e5749afdb70da5f78ddbb8c"); + private static final Address validator3 = + Address.fromHexString("001d14804b399c6ef80e64576f657660804fec0b"); + private static final Address validator4 = + Address.fromHexString("0032403587947b9f15622a68d104d54d33dbd1cd"); + private static final Address validator5 = + Address.fromHexString("00497e92cdc0e0b963d752b2296acb87da828b24"); + + @Test + public void validatorsAreNotAddedBeforeRequiredVoteCountReached() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator5, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void validatorAddedToListWhenMoreThanHalfOfProposersVoteToAdd() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator3, validator5, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4, validator5); + } + + @Test + public void validatorsAreAddedInCorrectOrder() { + final VoteTally voteTally = + new VoteTally(asList(validator1, validator2, validator3, validator5)); + voteTally.addVote(validator1, validator4, VoteType.ADD); + voteTally.addVote(validator2, validator4, VoteType.ADD); + voteTally.addVote(validator3, validator4, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4, validator5); + } + + @Test + public void duplicateVotesFromSameProposerAreIgnored() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator5, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void proposerChangingAddVoteToDropBeforeLimitReachedDiscardsAddVote() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator1, validator5, VoteType.DROP); + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator3, validator5, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void proposerChangingAddVoteToDropAfterLimitReachedPreservesAddVote() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(validator1, validator5, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4, validator5); + } + + @Test + public void clearVotesAboutAValidatorWhenItIsAdded() { + final VoteTally voteTally = fourValidators(); + // Vote to add validator5 + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator3, validator5, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4, validator5); + + // Then vote it back out + voteTally.addVote(validator2, validator5, VoteType.DROP); + voteTally.addVote(validator3, validator5, VoteType.DROP); + voteTally.addVote(validator4, validator5, VoteType.DROP); + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + + // And then start voting to add it back in, but validator1's vote should have been discarded + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator3, validator5, VoteType.ADD); + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void requiresASingleVoteWhenThereIsOnlyOneValidator() { + final VoteTally voteTally = new VoteTally(singletonList(validator1)); + voteTally.addVote(validator1, validator2, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()).containsExactly(validator1, validator2); + } + + @Test + public void requiresTwoVotesWhenThereAreTwoValidators() { + final VoteTally voteTally = new VoteTally(asList(validator1, validator2)); + voteTally.addVote(validator1, validator3, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()).containsExactly(validator1, validator2); + + voteTally.addVote(validator2, validator3, VoteType.ADD); + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3); + } + + @Test + public void resetVotes() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.discardOutstandingVotes(); + voteTally.addVote(validator3, validator5, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void validatorsAreNotRemovedBeforeRequiredVoteCountReached() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator4, VoteType.DROP); + voteTally.addVote(validator2, validator4, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void validatorRemovedFromListWhenMoreThanHalfOfProposersVoteToDrop() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator4, VoteType.DROP); + voteTally.addVote(validator2, validator4, VoteType.DROP); + voteTally.addVote(validator3, validator4, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3); + } + + @Test + public void validatorsAreInCorrectOrderAfterRemoval() { + final VoteTally voteTally = new VoteTally(asList(validator1, validator2, validator4)); + voteTally.addVote(validator1, validator3, VoteType.DROP); + voteTally.addVote(validator2, validator3, VoteType.DROP); + voteTally.addVote(validator4, validator3, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator4); + } + + @Test + public void duplicateDropVotesFromSameProposerAreIgnored() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator4, VoteType.DROP); + voteTally.addVote(validator2, validator4, VoteType.DROP); + voteTally.addVote(validator2, validator4, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void proposerChangingDropVoteToAddBeforeLimitReachedDiscardsDropVote() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator4, VoteType.DROP); + voteTally.addVote(validator1, validator4, VoteType.ADD); + voteTally.addVote(validator2, validator4, VoteType.DROP); + voteTally.addVote(validator3, validator4, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + } + + @Test + public void proposerChangingDropVoteToAddAfterLimitReachedPreservesDropVote() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator4, VoteType.DROP); + voteTally.addVote(validator2, validator4, VoteType.DROP); + voteTally.addVote(validator3, validator4, VoteType.DROP); + voteTally.addVote(validator1, validator4, VoteType.ADD); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3); + } + + @Test + public void removedValidatorsVotesAreDiscarded() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator4, validator5, VoteType.ADD); + voteTally.addVote(validator4, validator3, VoteType.DROP); + + voteTally.addVote(validator1, validator4, VoteType.DROP); + voteTally.addVote(validator2, validator4, VoteType.DROP); + voteTally.addVote(validator3, validator4, VoteType.DROP); + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3); + + // Now adding only requires 2 votes (>50% of the 3 remaining validators) + // but validator4's vote no longer counts + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator1, validator3, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3); + } + + @Test + public void clearVotesAboutAValidatorWhenItIsDropped() { + final VoteTally voteTally = + new VoteTally(asList(validator1, validator2, validator3, validator4, validator5)); + // Vote to remove validator5 + voteTally.addVote(validator1, validator5, VoteType.DROP); + voteTally.addVote(validator2, validator5, VoteType.DROP); + voteTally.addVote(validator3, validator5, VoteType.DROP); + + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + + // Then vote it back in + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(validator4, validator5, VoteType.ADD); + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4, validator5); + + // And then start voting to drop it again, but validator1's vote should have been discarded + voteTally.addVote(validator2, validator5, VoteType.DROP); + voteTally.addVote(validator3, validator5, VoteType.DROP); + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4, validator5); + } + + @Test + public void trackMultipleOngoingVotesIndependently() { + final VoteTally voteTally = fourValidators(); + voteTally.addVote(validator1, validator5, VoteType.ADD); + voteTally.addVote(validator1, validator3, VoteType.DROP); + + voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(validator2, validator1, VoteType.DROP); + + // Neither vote has enough votes to complete. + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4); + + voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(validator3, validator1, VoteType.DROP); + + // Validator 5 now has 3 votes and is added + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator1, validator2, validator3, validator4, validator5); + + voteTally.addVote(validator4, validator5, VoteType.ADD); + voteTally.addVote(validator4, validator1, VoteType.DROP); + + // Validator 1 now gets dropped. + assertThat(voteTally.getCurrentValidators()) + .containsExactly(validator2, validator3, validator4, validator5); + } + + private VoteTally fourValidators() { + return new VoteTally(asList(validator1, validator2, validator3, validator4)); + } +} diff --git a/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRuleTest.java b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRuleTest.java new file mode 100755 index 00000000000..69d18ec502c --- /dev/null +++ b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/headervalidationrules/VoteValidationRuleTest.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.consensus.common.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.consensus.common.VoteType; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class VoteValidationRuleTest { + + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {VoteType.DROP.getNonceValue(), true}, + {VoteType.ADD.getNonceValue(), true}, + {0x01L, false}, + {0xFFFFFFFFFFFFFFFEL, false} + }); + } + + @Parameter public long actualVote; + + @Parameter(1) + public boolean expectedResult; + + @Test + public void test() { + final VoteValidationRule uut = new VoteValidationRule(); + final BlockHeaderTestFixture blockBuilder = new BlockHeaderTestFixture(); + blockBuilder.nonce(actualVote); + + final BlockHeader header = blockBuilder.buildHeader(); + + assertThat(uut.validate(header, null)).isEqualTo(expectedResult); + } +} diff --git a/consensus/ibft/build.gradle b/consensus/ibft/build.gradle new file mode 100755 index 00000000000..a81153d7525 --- /dev/null +++ b/consensus/ibft/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-ibft' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + implementation project(':consensus:common') + implementation project(':crypto') + implementation project(':ethereum:core') + implementation project(':ethereum:eth') + implementation project(':ethereum:jsonrpc') + implementation project(':ethereum:rlp') + implementation project(':ethereum:p2p') + implementation project(':services:kvstore') + + implementation 'com.google.guava:guava' + implementation 'io.vertx:vertx-core' + + testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts') + + testImplementation group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.0-beta.5' + testImplementation group: 'org.powermock', name: 'powermock-module-junit4', version: '2.0.0-beta.5' + testImplementation group: 'junit', name: 'junit', version: '4.12' + testImplementation "org.awaitility:awaitility:3.1.2" + + testImplementation "org.assertj:assertj-core:3.10.0" + testImplementation 'org.mockito:mockito-core' +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ConsensusRoundIdentifier.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ConsensusRoundIdentifier.java new file mode 100755 index 00000000000..e8dab38313a --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ConsensusRoundIdentifier.java @@ -0,0 +1,80 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import com.google.common.base.MoreObjects; + +/** + * Represents the chain index (i.e. height) and number of attempted consensuses conducted at this + * height. + */ +public class ConsensusRoundIdentifier implements Comparable { + + private final long sequence; + private final int round; + + /** + * Constructor for a round identifier + * + * @param sequence Sequence number for this round, synonymous with block height + * @param round round number for the current attempt at achieving consensus + */ + public ConsensusRoundIdentifier(final long sequence, final int round) { + this.sequence = sequence; + this.round = round; + } + + /** + * Constructor that derives the sequence and round information from an RLP encoded message + * + * @param in The RLP body of the message to check + * @return A derived sequence and round number + */ + public static ConsensusRoundIdentifier readFrom(final RLPInput in) { + return new ConsensusRoundIdentifier(in.readLong(), in.readInt()); + } + + /** + * Adds this rounds information to a given RLP buffer + * + * @param out The RLP buffer to add to + */ + public void writeTo(final RLPOutput out) { + out.writeLong(sequence); + out.writeInt(round); + } + + public int getRoundNumber() { + return this.round; + } + + public long getSequenceNumber() { + return this.sequence; + } + + /** + * Comparator for round identifiers to achieve ordering + * + * @param v The round to compare this one to + * @return a negative integer, zero, or a positive integer as this object is less than, equal to, + * or greater than the specified object. + */ + @Override + public int compareTo(final ConsensusRoundIdentifier v) { + final int sequenceComparison = Long.compareUnsigned(sequence, v.sequence); + if (sequenceComparison != 0) { + return sequenceComparison; + } else { + return Integer.compare(round, v.round); + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("Sequence", sequence) + .add("Round", round) + .toString(); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashing.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashing.java new file mode 100755 index 00000000000..362073b84bd --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashing.java @@ -0,0 +1,155 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class IbftBlockHashing { + + private static final BytesValue COMMIT_MSG_CODE = BytesValue.wrap(new byte[] {2}); + + /** + * Constructs a hash of the block header, suitable for use when creating the proposer seal. The + * extra data is modified to have a null proposer seal and empty list of committed seals. + * + * @param header The header for which a proposer seal is to be calculated + * @param ibftExtraData The extra data block which is to be inserted to the header once seal is + * calculated + * @return the hash of the header suitable for signing as the proposer seal + */ + public static Hash calculateDataHashForProposerSeal( + final BlockHeader header, final IbftExtraData ibftExtraData) { + final BytesValue headerRlp = + serializeHeader(header, () -> encodeExtraDataWithoutCommittedSeals(ibftExtraData, null)); + + // Proposer hash is the hash of the hash + return Hash.hash(Hash.hash(headerRlp)); + } + + /** + * Constructs a hash of the block header suitable for signing as a committed seal. The extra data + * in the hash uses an empty list for the committed seals. + * + * @param header The header for which a proposer seal is to be calculated (without extra data) + * @param ibftExtraData The extra data block which is to be inserted to the header once seal is + * calculated + * @return the hash of the header including the validator and proposer seal in the extra data + */ + public static Hash calculateDataHashForCommittedSeal( + final BlockHeader header, final IbftExtraData ibftExtraData) { + // The data signed by a committer is an array of [Hash, COMMIT_MSG_CODE] + final Hash dataHash = Hash.hash(serializeHeaderWithoutCommittedSeals(header, ibftExtraData)); + final BytesValue seal = BytesValue.wrap(dataHash, COMMIT_MSG_CODE); + return Hash.hash(seal); + } + + /** + * Constructs a hash of the block header, but omits the committerSeals (as this changes on each of + * the potentially circulated blocks at the current chain height). + * + * @param header The header for which a block hash is to be calculated + * @return the hash of the header including the validator and proposer seal in the extra data + */ + public static Hash calculateHashOfIbftBlockOnChain(final BlockHeader header) { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + return Hash.hash(serializeHeaderWithoutCommittedSeals(header, ibftExtraData)); + } + + private static BytesValue serializeHeaderWithoutCommittedSeals( + final BlockHeader header, final IbftExtraData ibftExtraData) { + return serializeHeader( + header, + () -> encodeExtraDataWithoutCommittedSeals(ibftExtraData, ibftExtraData.getProposerSeal())); + } + + /** + * Recovers the proposer's {@link Address} from the proposer seal. + * + * @param header the block header that was signed by the proposer seal + * @param ibftExtraData the parsed IBftExtraData from the header + * @return the proposer address + */ + public static Address recoverProposerAddress( + final BlockHeader header, final IbftExtraData ibftExtraData) { + final Hash proposerHash = calculateDataHashForProposerSeal(header, ibftExtraData); + return Util.signatureToAddress(ibftExtraData.getProposerSeal(), proposerHash); + } + + /** + * Recovers the {@link Address} for each validator that contributed a committed seal to the block. + * + * @param header the block header that was signed by the committed seals + * @param ibftExtraData the parsed IBftExtraData from the header + * @return the addresses of validators that provided a committed seal + */ + public static List
recoverCommitterAddresses( + final BlockHeader header, final IbftExtraData ibftExtraData) { + final Hash committerHash = + IbftBlockHashing.calculateDataHashForCommittedSeal(header, ibftExtraData); + + return ibftExtraData + .getSeals() + .stream() + .map(p -> Util.signatureToAddress(p, committerHash)) + .collect(Collectors.toList()); + } + + private static BytesValue encodeExtraDataWithoutCommittedSeals( + final IbftExtraData ibftExtraData, final Signature proposerSeal) { + final BytesValueRLPOutput extraDataEncoding = new BytesValueRLPOutput(); + extraDataEncoding.startList(); + extraDataEncoding.writeList( + ibftExtraData.getValidators(), (validator, rlp) -> rlp.writeBytesValue(validator)); + + if (proposerSeal != null) { + extraDataEncoding.writeBytesValue(proposerSeal.encodedBytes()); + } else { + extraDataEncoding.writeNull(); + } + + // Represents an empty committer list (i.e this is not included in the hashing of the block) + extraDataEncoding.startList(); + extraDataEncoding.endList(); + + extraDataEncoding.endList(); + + return BytesValue.wrap(ibftExtraData.getVanityData(), extraDataEncoding.encoded()); + } + + private static BytesValue serializeHeader( + final BlockHeader header, final Supplier extraDataSerializer) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + + out.writeBytesValue(header.getParentHash()); + out.writeBytesValue(header.getOmmersHash()); + out.writeBytesValue(header.getCoinbase()); + out.writeBytesValue(header.getStateRoot()); + out.writeBytesValue(header.getTransactionsRoot()); + out.writeBytesValue(header.getReceiptsRoot()); + out.writeBytesValue(header.getLogsBloom().getBytes()); + out.writeUInt256Scalar(header.getDifficulty()); + out.writeLongScalar(header.getNumber()); + out.writeLongScalar(header.getGasLimit()); + out.writeLongScalar(header.getGasUsed()); + out.writeLongScalar(header.getTimestamp()); + // Cannot decode an IbftExtraData on block 0 due to missing/illegal signatures + if (header.getNumber() == 0) { + out.writeBytesValue(header.getExtraData()); + } else { + out.writeBytesValue(extraDataSerializer.get()); + } + out.writeBytesValue(header.getMixHash()); + out.writeLong(header.getNonce()); + out.endList(); + return out.encoded(); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java new file mode 100755 index 00000000000..c6da1e3717f --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.consensus.common.headervalidationrules.VoteValidationRule; +import net.consensys.pantheon.consensus.ibft.headervalidationrules.IbftExtraDataValidationRule; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.AncestryValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.ConstantFieldValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasLimitRangeAndDeltaValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasUsageValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.TimestampValidationRule; +import net.consensys.pantheon.util.uint.UInt256; + +public class IbftBlockHeaderValidationRulesetFactory { + + /** + * Produces a BlockHeaderValidator configured for assessing ibft block headers which are to form + * part of the BlockChain (i.e. not proposed blocks, which do not contain commit seals) + * + * @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks. + * @return BlockHeaderValidator configured for assessing ibft block headers + */ + public static BlockHeaderValidator ibftBlockHeaderValidator( + final long secondsBetweenBlocks) { + return createValidator(secondsBetweenBlocks, true); + } + + /** + * Produces a BlockHeaderValidator configured for assessing IBFT proposed blocks (i.e. blocks + * which need to be vetted by the validators, and do not contain commit seals). + * + * @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks. + * @return BlockHeaderValidator configured for assessing ibft block headers + */ + public static BlockHeaderValidator ibftProposedBlockValidator( + final long secondsBetweenBlocks) { + return createValidator(secondsBetweenBlocks, false); + } + + private static BlockHeaderValidator createValidator( + final long secondsBetweenBlocks, final boolean validateCommitSeals) { + return new BlockHeaderValidator.Builder() + .addRule(new AncestryValidationRule()) + .addRule(new GasUsageValidationRule()) + .addRule(new GasLimitRangeAndDeltaValidationRule(5000, 0x7fffffffffffffffL)) + .addRule(new TimestampValidationRule(1, secondsBetweenBlocks)) + .addRule( + new ConstantFieldValidationRule<>( + "MixHash", BlockHeader::getMixHash, IbftHelpers.EXPECTED_MIX_HASH)) + .addRule( + new ConstantFieldValidationRule<>( + "OmmersHash", BlockHeader::getOmmersHash, Hash.EMPTY_LIST_HASH)) + .addRule( + new ConstantFieldValidationRule<>( + "Difficulty", BlockHeader::getDifficulty, UInt256.ONE)) + .addRule(new VoteValidationRule()) + .addRule(new IbftExtraDataValidationRule(validateCommitSeals)) + .build(); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporter.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporter.java new file mode 100755 index 00000000000..ce0be3e1ad7 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporter.java @@ -0,0 +1,53 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; + +import java.util.List; + +/** + * The IBFT BlockImporter implementation. Adds votes to VoteTally as blocks are added to the chain. + */ +public class IbftBlockImporter implements BlockImporter { + + private final BlockImporter delegate; + private final VoteTallyUpdater voteTallyUpdater; + + public IbftBlockImporter( + final BlockImporter delegate, final VoteTallyUpdater voteTallyUpdater) { + this.delegate = delegate; + this.voteTallyUpdater = voteTallyUpdater; + } + + @Override + public boolean importBlock( + final ProtocolContext context, + final Block block, + final HeaderValidationMode headerValidationMode) { + final boolean result = delegate.importBlock(context, block, headerValidationMode); + updateVoteTally(result, block.getHeader(), context); + return result; + } + + @Override + public boolean fastImportBlock( + final ProtocolContext context, + final Block block, + final List receipts, + final HeaderValidationMode headerValidationMode) { + final boolean result = delegate.fastImportBlock(context, block, receipts, headerValidationMode); + updateVoteTally(result, block.getHeader(), context); + return result; + } + + private void updateVoteTally( + final boolean result, final BlockHeader header, final ProtocolContext context) { + if (result) { + voteTallyUpdater.updateForBlock(header, context.getConsensusState().getVoteTally()); + } + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftContext.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftContext.java new file mode 100755 index 00000000000..5f5ca64f29b --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftContext.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; + +/** Holds the IBFT specific mutable state. */ +public class IbftContext { + + private final VoteTally voteTally; + private final VoteProposer voteProposer; + + public IbftContext(final VoteTally voteTally, final VoteProposer voteProposer) { + this.voteTally = voteTally; + this.voteProposer = voteProposer; + } + + public VoteTally getVoteTally() { + return voteTally; + } + + public VoteProposer getVoteProposer() { + return voteProposer; + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvent.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvent.java new file mode 100755 index 00000000000..d4f21b1ca6d --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvent.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.consensus.ibft.IbftEvents.Type; + +/** Category of events that will effect and are interpretable by the Ibft processing mechanism */ +public interface IbftEvent { + Type getType(); +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEventQueue.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEventQueue.java new file mode 100755 index 00000000000..900ae794803 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEventQueue.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.consensus.ibft; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Threadsafe queue that lets parts of the system inform the Ibft infrastructure about events */ +public class IbftEventQueue { + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + private static final int MAX_QUEUE_SIZE = 1000; + private static final Logger LOGGER = LogManager.getLogger(IbftEventQueue.class); + + /** + * Put an Ibft event onto the queue + * + * @param event Provided ibft event + */ + public void add(final IbftEvent event) { + if (queue.size() > MAX_QUEUE_SIZE) { + LOGGER.warn("Queue size exceeded trying to add new ibft event {}", event.toString()); + } else { + queue.add(event); + } + } + + public int size() { + return queue.size(); + } + + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** + * Blocking request for the next item available on the queue that will timeout after a specified + * period + * + * @param timeout number of time units after which this operation should timeout + * @param unit the time units in which to count + * @return The next IbftEvent to become available on the queue or null if the expiry passes + * @throws InterruptedException If the underlying queue implementation is interrupted + */ + @Nullable + public IbftEvent poll(final long timeout, final TimeUnit unit) throws InterruptedException { + return queue.poll(timeout, unit); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvents.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvents.java new file mode 100755 index 00000000000..b3eb214b5eb --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftEvents.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.ethereum.p2p.api.Message; + +/** Static helper functions for producing and working with IbftEvent objects */ +public class IbftEvents { + public static IbftEvent fromMessage(final Message message) { + throw new IllegalStateException("No IbftEvents are implemented yet"); + } + + public enum Type { + ROUND_EXPIRY + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftExtraData.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftExtraData.java new file mode 100755 index 00000000000..ba988b0909d --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftExtraData.java @@ -0,0 +1,99 @@ +package net.consensys.pantheon.consensus.ibft; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +/** + * Represents the data structure stored in the extraData field of the BlockHeader used when + * operating under an IBFT consensus mechanism. + */ +public class IbftExtraData { + + public static final int EXTRA_VANITY_LENGTH = 32; + + private final BytesValue vanityData; + private final List seals; + private final Signature proposerSeal; + private final List
validators; + + public IbftExtraData( + final BytesValue vanityData, + final List seals, + final Signature proposerSeal, + final List
validators) { + + checkNotNull(vanityData); + checkNotNull(seals); + checkNotNull(validators); + + this.vanityData = vanityData; + this.seals = seals; + this.proposerSeal = proposerSeal; + this.validators = validators; + } + + public static IbftExtraData decode(final BytesValue input) { + checkArgument( + input.size() > EXTRA_VANITY_LENGTH, + "Invalid BytesValue supplied - too short to produce a valid IBFT Extra Data object."); + + final BytesValue vanityData = input.slice(0, EXTRA_VANITY_LENGTH); + + final BytesValue rlpData = input.slice(EXTRA_VANITY_LENGTH); + final RLPInput rlpInput = new BytesValueRLPInput(rlpData, false); + + rlpInput.enterList(); // This accounts for the "root node" which contains IBFT data items. + final List
validators = rlpInput.readList(Address::readFrom); + final Signature proposerSeal = parseProposerSeal(rlpInput); + final List seals = rlpInput.readList(rlp -> Signature.decode(rlp.readBytesValue())); + rlpInput.leaveList(); + + return new IbftExtraData(vanityData, seals, proposerSeal, validators); + } + + private static Signature parseProposerSeal(final RLPInput rlpInput) { + final BytesValue data = rlpInput.readBytesValue(); + return data.isZero() ? null : Signature.decode(data); + } + + public BytesValue encode() { + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytesValue(validator)); + if (proposerSeal != null) { + encoder.writeBytesValue(proposerSeal.encodedBytes()); + } else { + encoder.writeNull(); + } + encoder.writeList(seals, (committer, rlp) -> rlp.writeBytesValue(committer.encodedBytes())); + encoder.endList(); + + return BytesValue.wrap(vanityData, encoder.encoded()); + } + + // Accessors + public BytesValue getVanityData() { + return vanityData; + } + + public List getSeals() { + return seals; + } + + public Signature getProposerSeal() { + return proposerSeal; + } + + public List
getValidators() { + return validators; + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftHelpers.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftHelpers.java new file mode 100755 index 00000000000..d509e9ceb0f --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftHelpers.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.ethereum.core.Hash; + +public class IbftHelpers { + + public static final Hash EXPECTED_MIX_HASH = + Hash.fromHexString("0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365"); + + public static int calculateRequiredValidatorQuorum(final int validatorCount) { + final int F = (validatorCount - 1) / 3; + return (2 * F) + 1; + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProcessor.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProcessor.java new file mode 100755 index 00000000000..1619872f6c2 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProcessor.java @@ -0,0 +1,84 @@ +package net.consensys.pantheon.consensus.ibft; + +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Execution context for draining queued ibft events and applying them to a maintained state */ +public class IbftProcessor implements Runnable { + private static final Logger LOGGER = LogManager.getLogger(IbftEventQueue.class); + + private final IbftEventQueue incomingQueue; + private final ScheduledExecutorService roundTimerExecutor; + private final RoundTimer roundTimer; + private final IbftStateMachine stateMachine; + private volatile boolean shutdown = false; + + /** + * Construct a new IbftProcessor + * + * @param incomingQueue The event queue from which to drain new events + * @param baseRoundExpiryMillis The expiry time in milliseconds of round 0 + * @param stateMachine an IbftStateMachine ready to process events and maintain state + */ + public IbftProcessor( + final IbftEventQueue incomingQueue, + final int baseRoundExpiryMillis, + final IbftStateMachine stateMachine) { + // Spawning the round timer with a single thread as we should never have more than 1 timer in + // flight at a time + this( + incomingQueue, + baseRoundExpiryMillis, + stateMachine, + Executors.newSingleThreadScheduledExecutor()); + } + + @VisibleForTesting + IbftProcessor( + final IbftEventQueue incomingQueue, + final int baseRoundExpiryMillis, + final IbftStateMachine stateMachine, + final ScheduledExecutorService roundTimerExecutor) { + this.incomingQueue = incomingQueue; + this.roundTimerExecutor = roundTimerExecutor; + + this.roundTimer = new RoundTimer(incomingQueue, baseRoundExpiryMillis, roundTimerExecutor); + this.stateMachine = stateMachine; + } + + /** Indicate to the processor that it should gracefully stop at its next opportunity */ + public void stop() { + shutdown = true; + } + + @Override + public void run() { + while (!shutdown) { + Optional newEvent = Optional.empty(); + try { + newEvent = Optional.ofNullable(incomingQueue.poll(2, TimeUnit.SECONDS)); + } catch (final InterruptedException interrupt) { + // If the queue was interrupted propagate it and spin to check our shutdown status + Thread.currentThread().interrupt(); + } + + newEvent.ifPresent( + ibftEvent -> { + try { + stateMachine.processEvent(ibftEvent, roundTimer); + } catch (final Exception e) { + LOGGER.error( + "State machine threw exception while processing event {" + ibftEvent + "}", e); + } + }); + } + // Clean up the executor service the round timer has been utilising + roundTimerExecutor.shutdownNow(); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSchedule.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSchedule.java new file mode 100755 index 00000000000..04c0380bc5a --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSchedule.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +import java.util.Optional; + +import io.vertx.core.json.JsonObject; + +/** Defines the protocol behaviours for a blockchain using IBFT. */ +public class IbftProtocolSchedule { + + private static final long DEFAULT_EPOCH_LENGTH = 30_000; + private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1; + + public static ProtocolSchedule create(final JsonObject config) { + final long spuriousDragonBlock = config.getLong("spuriousDragonBlock", 0L); + final Optional ibftConfig = Optional.ofNullable(config.getJsonObject("ibft")); + final int chainId = config.getInteger("chainId", 1); + final long epochLength = getEpochLength(ibftConfig); + final long blockPeriod = + ibftConfig + .map(iC -> iC.getInteger("blockPeriodSeconds")) + .orElse(DEFAULT_BLOCK_PERIOD_SECONDS); + + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone( + spuriousDragonBlock, + IbftProtocolSpecs.spuriousDragon(blockPeriod, epochLength, chainId, protocolSchedule)); + return protocolSchedule; + } + + public static long getEpochLength(final Optional ibftConfig) { + return ibftConfig.map(conf -> conf.getLong("epochLength")).orElse(DEFAULT_EPOCH_LENGTH); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSpecs.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSpecs.java new file mode 100755 index 00000000000..bb0e8ed8bcb --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftProtocolSpecs.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.consensus.ibft; + +import static net.consensys.pantheon.consensus.ibft.IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockBodyValidator; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockImporter; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; + +import java.math.BigInteger; + +/** Factory for producing Ibft protocol specs for given configurations and known fork points */ +public class IbftProtocolSpecs { + + /** + * Produce the ProtocolSpec for an IBFT chain that uses spurious dragon milestone configuration + * + * @param secondsBetweenBlocks the block period in seconds + * @param epochLength the number of blocks in each epoch + * @param chainId the id of the Chain. + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return a configured ProtocolSpec for dealing with IBFT blocks + */ + public static ProtocolSpec spuriousDragon( + final long secondsBetweenBlocks, + final long epochLength, + final int chainId, + final ProtocolSchedule protocolSchedule) { + final EpochManager epochManager = new EpochManager(epochLength); + return MainnetProtocolSpecs.spuriousDragonDefinition(chainId) + .changeConsensusContextType( + difficultyCalculator -> ibftBlockHeaderValidator(secondsBetweenBlocks), + MainnetBlockBodyValidator::new, + (blockHeaderValidator, blockBodyValidator, blockProcessor) -> + new IbftBlockImporter( + new MainnetBlockImporter<>( + blockHeaderValidator, blockBodyValidator, blockProcessor), + new VoteTallyUpdater(epochManager)), + (time, parent, protocolContext) -> BigInteger.ONE) + .blockReward(Wei.ZERO) + .blockHashFunction(IbftBlockHashing::calculateHashOfIbftBlockOnChain) + .name("IBFT") + .build(protocolSchedule); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftStateMachine.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftStateMachine.java new file mode 100755 index 00000000000..7e236e7ab5a --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/IbftStateMachine.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.consensus.ibft; + +/** Stateful evaluator for ibft events */ +public class IbftStateMachine { + + /** + * Attempt to consume the event and update the maintained state + * + * @param event the external action that has occurred + * @param roundTimer timer that will fire expiry events that are expected to be received back into + * this machine + * @return whether this event was consumed or requires reprocessing later once the state machine + * catches up + */ + public boolean processEvent(final IbftEvent event, final RoundTimer roundTimer) { + // TODO: don't just discard the event, do some logic + return true; + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/RoundTimer.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/RoundTimer.java new file mode 100755 index 00000000000..50c0028694e --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/RoundTimer.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.consensus.ibft; + +import net.consensys.pantheon.consensus.ibft.ibftevent.RoundExpiry; + +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** Class for starting and keeping organised round timers */ +public class RoundTimer { + private final ScheduledExecutorService timerExecutor; + private Optional> currentTimerTask; + private final IbftEventQueue queue; + private final long baseExpiryMillis; + + /** + * Construct a RoundTimer with primed executor service ready to start timers + * + * @param queue The queue in which to put round expiry events + * @param baseExpiryMillis The initial round length for round 0 + * @param timerExecutor executor service that timers can be scheduled with + */ + public RoundTimer( + final IbftEventQueue queue, + final long baseExpiryMillis, + final ScheduledExecutorService timerExecutor) { + this.queue = queue; + this.timerExecutor = timerExecutor; + this.currentTimerTask = Optional.empty(); + this.baseExpiryMillis = baseExpiryMillis; + } + + /** Cancels the current running round timer if there is one */ + public synchronized void cancelTimer() { + currentTimerTask.ifPresent(t -> t.cancel(false)); + currentTimerTask = Optional.empty(); + } + + /** + * Whether there is a timer currently running or not + * + * @return boolean of whether a timer is ticking or not + */ + public synchronized boolean isRunning() { + return currentTimerTask.map(t -> !t.isDone()).orElse(false); + } + + /** + * Starts a timer for the supplied round cancelling any previously active round timer + * + * @param round The round identifier which this timer is tracking + */ + public synchronized void startTimer(final ConsensusRoundIdentifier round) { + cancelTimer(); + + final long expiryTime = baseExpiryMillis * (long) Math.pow(2, round.getRoundNumber()); + + final Runnable newTimerRunnable = () -> queue.add(new RoundExpiry(round)); + + final ScheduledFuture newTimerTask = + timerExecutor.schedule(newTimerRunnable, expiryTime, TimeUnit.MILLISECONDS); + currentTimerTask = Optional.of(newTimerTask); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdater.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdater.java new file mode 100755 index 00000000000..036f49f823b --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdater.java @@ -0,0 +1,73 @@ +package net.consensys.pantheon.consensus.ibft; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.common.VoteType; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import org.apache.logging.log4j.Logger; + +/** + * Provides the logic to extract vote tally state from the blockchain and update it as blocks are + * added. + */ +public class VoteTallyUpdater { + + private static final Logger LOGGER = getLogger(VoteTallyUpdater.class); + private static final Address NO_VOTE_SUBJECT = Address.wrap(BytesValue.wrap(new byte[20])); + + private final EpochManager epochManager; + + public VoteTallyUpdater(final EpochManager epochManager) { + this.epochManager = epochManager; + } + + /** + * Create a new VoteTally based on the current blockchain state. + * + * @param blockchain the blockchain to load the current state from + * @return a VoteTally reflecting the state of the blockchain head + */ + public VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain) { + final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber(); + final long epochBlockNumber = epochManager.getLastEpochBlock(chainHeadBlockNumber); + LOGGER.info("Loading validator voting state starting from block {}", epochBlockNumber); + final BlockHeader epochBlock = blockchain.getBlockHeader(epochBlockNumber).get(); + final List
initialValidators = + IbftExtraData.decode(epochBlock.getExtraData()).getValidators(); + final VoteTally voteTally = new VoteTally(initialValidators); + for (long blockNumber = epochBlockNumber + 1; + blockNumber <= chainHeadBlockNumber; + blockNumber++) { + updateForBlock(blockchain.getBlockHeader(blockNumber).get(), voteTally); + } + return voteTally; + } + + /** + * Update the vote tally to reflect changes caused by appending a new block to the chain. + * + * @param header the header of the block being added + * @param voteTally the vote tally to update + */ + public void updateForBlock(final BlockHeader header, final VoteTally voteTally) { + final Address candidate = header.getCoinbase(); + if (epochManager.isEpochBlock(header.getNumber())) { + voteTally.discardOutstandingVotes(); + return; + } + + if (!candidate.equals(NO_VOTE_SUBJECT)) { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + final Address proposer = IbftBlockHashing.recoverProposerAddress(header, ibftExtraData); + voteTally.addVote(proposer, candidate, VoteType.fromNonce(header.getNonce()).get()); + } + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreator.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreator.java new file mode 100755 index 00000000000..35a13235f12 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreator.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.consensus.ibft.blockcreation; + +import net.consensys.pantheon.consensus.ibft.IbftBlockHashing; +import net.consensys.pantheon.consensus.ibft.IbftContext; +import net.consensys.pantheon.consensus.ibft.IbftExtraData; +import net.consensys.pantheon.consensus.ibft.IbftHelpers; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.AbstractBlockCreator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.SealableBlockHeader; +import net.consensys.pantheon.ethereum.core.Util; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; + +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Responsible for producing a Block which conforms to IBFT validation rules (other than missing + * commit seals). Transactions and associated Hashes (stateroot, receipts etc.) are loaded into the + * Block in the base class as part of the transaction selection process. + */ +public class IbftBlockCreator extends AbstractBlockCreator { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final KeyPair nodeKeys; + private final ProtocolSchedule protocolSchedule; + + public IbftBlockCreator( + final Address coinbase, + final ExtraDataCalculator extraDataCalculator, + final PendingTransactions pendingTransactions, + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final Function gasLimitCalculator, + final KeyPair nodeKeys, + final Wei minTransactionGasPrice, + final BlockHeader parentHeader) { + super( + coinbase, + extraDataCalculator, + pendingTransactions, + protocolContext, + protocolSchedule, + gasLimitCalculator, + minTransactionGasPrice, + Util.publicKeyToAddress(nodeKeys.getPublicKey()), + parentHeader); + this.nodeKeys = nodeKeys; + this.protocolSchedule = protocolSchedule; + } + + /** + * Responsible for signing (hash of) the block (including MixHash and Nonce), and then injecting + * the seal into the extraData. This is called after a suitable set of transactions have been + * identified, and all resulting hashes have been inserted into the passed-in SealableBlockHeader. + * + * @param sealableBlockHeader A block header containing StateRoots, TransactionHashes etc. + * @return The blockhead which is to be added to the block being proposed. + */ + @Override + protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) { + + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + + final BlockHeaderBuilder builder = + BlockHeaderBuilder.create() + .populateFrom(sealableBlockHeader) + .mixHash(IbftHelpers.EXPECTED_MIX_HASH) + .nonce(0) + .blockHashFunction(blockHashFunction); + + final IbftExtraData sealedExtraData = constructSignedExtraData(builder.buildBlockHeader()); + + // Replace the extraData in the BlockHeaderBuilder, and return header. + return builder.extraData(sealedExtraData.encode()).buildBlockHeader(); + } + + /** + * Produces an IbftExtraData object with a populated proposerSeal. The signature in the block is + * generated from the Hash of the header (minus proposer and committer seals) and the nodeKeys. + * + * @param headerToSign An almost fully populated header (proposer and committer seals are empty) + * @return Extra data containing the same vanity data and validators as extraData, however + * proposerSeal will also be populated. + */ + private IbftExtraData constructSignedExtraData(final BlockHeader headerToSign) { + final IbftExtraData extraData = IbftExtraData.decode(headerToSign.getExtraData()); + final Hash hashToSign = + IbftBlockHashing.calculateDataHashForProposerSeal(headerToSign, extraData); + return new IbftExtraData( + extraData.getVanityData(), + extraData.getSeals(), + SECP256K1.sign(hashToSign, nodeKeys), + extraData.getValidators()); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftExtraDataCalculator.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftExtraDataCalculator.java new file mode 100755 index 00000000000..329bba696a0 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftExtraDataCalculator.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.consensus.ibft.blockcreation; + +import net.consensys.pantheon.consensus.common.ValidatorProvider; +import net.consensys.pantheon.consensus.ibft.IbftExtraData; +import net.consensys.pantheon.ethereum.blockcreation.AbstractBlockCreator.ExtraDataCalculator; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.google.common.collect.Lists; + +public class IbftExtraDataCalculator implements ExtraDataCalculator { + + private final ValidatorProvider validatorProvider; + + public IbftExtraDataCalculator(final ValidatorProvider validatorProvider) { + this.validatorProvider = validatorProvider; + } + + @Override + public BytesValue get(final BlockHeader parent) { + final BytesValue vanityData = BytesValue.wrap(new byte[32]); + final IbftExtraData baseExtraData = + new IbftExtraData( + vanityData, + Lists.newArrayList(), + null, + Lists.newArrayList(validatorProvider.getCurrentValidators())); + return baseExtraData.encode(); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelector.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelector.java new file mode 100755 index 00000000000..e3b757f91c9 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelector.java @@ -0,0 +1,150 @@ +package net.consensys.pantheon.consensus.ibft.blockcreation; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.consensus.common.ValidatorProvider; +import net.consensys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import net.consensys.pantheon.consensus.ibft.IbftBlockHashing; +import net.consensys.pantheon.consensus.ibft.IbftExtraData; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; + +import java.util.ArrayList; +import java.util.List; +import java.util.NavigableSet; +import java.util.Optional; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Responsible for determining which member of the validator pool should propose the next block + * (i.e. send the Preprepare message). + * + *

It does this by extracting the previous block's proposer from the ProposerSeal (stored in the + * Blocks ExtraData) then iterating through the validator list (stored in {@link + * ValidatorProvider}), such that each new round for the given height is serviced by a different + * validator. + */ +public class ProposerSelector { + + private static final Logger LOGGER = LogManager.getLogger(ProposerSelector.class); + + private final Blockchain blockchain; + + /** Provides the current list of validators */ + private final ValidatorProvider validators; + + /** + * If set, will cause the proposer to change on successful addition of a block. Otherwise, the + * previously successful proposer will propose the next block as well. + */ + private final Boolean changeEachBlock; + + public ProposerSelector( + final Blockchain blockchain, + final ValidatorProvider validators, + final boolean changeEachBlock) { + this.blockchain = blockchain; + this.validators = validators; + this.changeEachBlock = changeEachBlock; + } + + /** + * Determines which validator should be acting as the proposer for a given sequence/round. + * + * @param roundIdentifier Identifies the chain height and proposal attempt number. + * @return The address of the node which is to propose a block for the provided Round. + */ + public Address selectProposerForRound(final ConsensusRoundIdentifier roundIdentifier) { + + checkArgument(roundIdentifier.getRoundNumber() >= 0); + checkArgument(roundIdentifier.getSequenceNumber() > 0); + + final long prevBlockNumber = roundIdentifier.getSequenceNumber() - 1; + final Address prevBlockProposer = getProposerOfBlock(prevBlockNumber); + + if (!validators.getCurrentValidators().contains(prevBlockProposer)) { + return handleMissingProposer(prevBlockProposer, roundIdentifier); + } else { + return handleWithExistingProposer(prevBlockProposer, roundIdentifier); + } + } + + /** + * If the proposer of the previous block is missing, the validator with an Address above the + * previous will become the next validator for the first round of the next block. + * + *

And validators will change from there. + */ + private Address handleMissingProposer( + final Address prevBlockProposer, final ConsensusRoundIdentifier roundIdentifier) { + final NavigableSet

validatorSet = new TreeSet<>(validators.getCurrentValidators()); + final SortedSet
latterValidators = validatorSet.tailSet(prevBlockProposer, false); + Address nextProposer; + if (latterValidators.isEmpty()) { + // i.e. prevBlockProposer was at the end of the validator list, so the right validator for + // the start of this round is the first. + nextProposer = validatorSet.first(); + } else { + // Else, use the first validator after the dropped entry. + nextProposer = latterValidators.first(); + } + return calculateRoundSpecificValidator(nextProposer, roundIdentifier.getRoundNumber()); + } + + /** + * If the previous Proposer is still a validator - determine what offset should be applied for the + * given round - factoring in a proposer change on the new block. + * + * @param prevBlockProposer + * @param roundIdentifier + * @return + */ + private Address handleWithExistingProposer( + final Address prevBlockProposer, final ConsensusRoundIdentifier roundIdentifier) { + int indexOffsetFromPrevBlock = roundIdentifier.getRoundNumber(); + if (changeEachBlock) { + indexOffsetFromPrevBlock += 1; + } + return calculateRoundSpecificValidator(prevBlockProposer, indexOffsetFromPrevBlock); + } + + /** + * Given Round 0 of the given height should start from given proposer (baseProposer) - determine + * which validator should be used given the indexOffset. + * + * @param baseProposer + * @param indexOffset + * @return + */ + private Address calculateRoundSpecificValidator( + final Address baseProposer, final int indexOffset) { + final List
currentValidatorList = new ArrayList<>(validators.getCurrentValidators()); + final int prevProposerIndex = currentValidatorList.indexOf(baseProposer); + final int roundValidatorIndex = (prevProposerIndex + indexOffset) % currentValidatorList.size(); + return currentValidatorList.get(roundValidatorIndex); + } + + /** + * Determines the proposer of an existing block, based on the proposer signature in the extra + * data. + * + * @param blockNumber The index of the block in the chain being queried. + * @return The unique identifier fo the node which proposed the block number in question. + */ + private Address getProposerOfBlock(final long blockNumber) { + final Optional maybeBlockHeader = blockchain.getBlockHeader(blockNumber); + if (maybeBlockHeader.isPresent()) { + final BlockHeader blockHeader = maybeBlockHeader.get(); + final IbftExtraData extraData = IbftExtraData.decode(blockHeader.getExtraData()); + return IbftBlockHashing.recoverProposerAddress(blockHeader, extraData); + } else { + LOGGER.trace("Unable to determine proposer for requested block"); + throw new RuntimeException("Unable to determine past proposer"); + } + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRule.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRule.java new file mode 100755 index 00000000000..f41fac2f162 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRule.java @@ -0,0 +1,119 @@ +package net.consensys.pantheon.consensus.ibft.headervalidationrules; + +import static net.consensys.pantheon.consensus.ibft.IbftHelpers.calculateRequiredValidatorQuorum; + +import net.consensys.pantheon.consensus.common.ValidatorProvider; +import net.consensys.pantheon.consensus.ibft.IbftBlockHashing; +import net.consensys.pantheon.consensus.ibft.IbftContext; +import net.consensys.pantheon.consensus.ibft.IbftExtraData; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule; +import net.consensys.pantheon.ethereum.rlp.RLPException; + +import java.util.Collection; +import java.util.List; + +import com.google.common.collect.Iterables; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Ensures the byte content of the extraData field can be deserialised into an appropriate + * structure, and that the structure created contains data matching expectations from preceding + * blocks. + */ +public class IbftExtraDataValidationRule implements AttachedBlockHeaderValidationRule { + + private static final Logger LOGGER = LogManager.getLogger(IbftExtraDataValidationRule.class); + + private final boolean validateCommitSeals; + + public IbftExtraDataValidationRule(final boolean validateCommitSeals) { + this.validateCommitSeals = validateCommitSeals; + } + + @Override + public boolean validate( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext context) { + return validateExtraData(header, context); + } + + /** + * Responsible for determining the validity of the extra data field. Ensures: + * + *
    + *
  • Bytes in the extra data field can be decoded as per IBFT specification + *
  • Proposer (derived from the proposerSeal) is a member of the validators + *
  • Committers (derived from committerSeals) are all members of the validators + *
+ * + * @param header the block header containing the extraData to be validated. + * @return True if the extraData successfully produces an IstanbulExtraData object, false + * otherwise + */ + private boolean validateExtraData( + final BlockHeader header, final ProtocolContext context) { + try { + final ValidatorProvider validatorProvider = context.getConsensusState().getVoteTally(); + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + + final Address proposer = IbftBlockHashing.recoverProposerAddress(header, ibftExtraData); + + final Collection
storedValidators = validatorProvider.getCurrentValidators(); + + if (!storedValidators.contains(proposer)) { + LOGGER.trace("Proposer sealing block is not a member of the validators."); + return false; + } + + if (validateCommitSeals) { + final List
committers = + IbftBlockHashing.recoverCommitterAddresses(header, ibftExtraData); + if (!validateCommitters(committers, storedValidators)) { + return false; + } + } + + if (!Iterables.elementsEqual(ibftExtraData.getValidators(), storedValidators)) { + LOGGER.trace( + "Incorrect validators. Expected {} but got {}.", + storedValidators, + ibftExtraData.getValidators()); + return false; + } + + } catch (final RLPException ex) { + LOGGER.trace("ExtraData field was unable to be deserialised into an IBFT Struct.", ex); + return false; + } catch (final IllegalArgumentException ex) { + LOGGER.trace("Failed to verify extra data", ex); + return false; + } + + return true; + } + + private boolean validateCommitters( + final Collection
committers, final Collection
storedValidators) { + + final int minimumSealsRequired = calculateRequiredValidatorQuorum(storedValidators.size()); + if (committers.size() < minimumSealsRequired) { + LOGGER.trace( + "Insufficient committers to seal block. (Required {}, received {})", + minimumSealsRequired, + committers.size()); + return false; + } + + if (!storedValidators.containsAll(committers)) { + LOGGER.trace("Not all committers are in the locally maintained validator list."); + return false; + } + + return true; + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ibftevent/RoundExpiry.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ibftevent/RoundExpiry.java new file mode 100755 index 00000000000..1ab66e696b4 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/ibftevent/RoundExpiry.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.consensus.ibft.ibftevent; + +import net.consensys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import net.consensys.pantheon.consensus.ibft.IbftEvent; +import net.consensys.pantheon.consensus.ibft.IbftEvents.Type; + +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +/** Event indicating a round timer has expired */ +public final class RoundExpiry implements IbftEvent { + private final ConsensusRoundIdentifier round; + + /** + * Constructor for a RoundExpiry event + * + * @param round The round that the expired timer belonged to + */ + public RoundExpiry(final ConsensusRoundIdentifier round) { + this.round = round; + } + + @Override + public Type getType() { + return Type.ROUND_EXPIRY; + } + + public ConsensusRoundIdentifier getView() { + return round; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("Round", round).toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final RoundExpiry that = (RoundExpiry) o; + return Objects.equals(round, that.round); + } + + @Override + public int hashCode() { + return Objects.hash(round); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/IbftJsonRpcMethodsFactory.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/IbftJsonRpcMethodsFactory.java new file mode 100755 index 00000000000..65802895023 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/IbftJsonRpcMethodsFactory.java @@ -0,0 +1,34 @@ +package net.consensys.pantheon.consensus.ibft.jsonrpc; + +import net.consensys.pantheon.consensus.ibft.IbftContext; +import net.consensys.pantheon.consensus.ibft.jsonrpc.methods.IbftProposeValidatorVote; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; + +import java.util.HashMap; +import java.util.Map; + +public class IbftJsonRpcMethodsFactory { + + private final JsonRpcParameter jsonRpcParameter = new JsonRpcParameter(); + + public Map methods(final ProtocolContext context) { + + final Map rpcMethods = new HashMap<>(); + // @formatter:off + addMethods( + rpcMethods, + new IbftProposeValidatorVote( + context.getConsensusState().getVoteProposer(), jsonRpcParameter)); + + return rpcMethods; + } + + private void addMethods( + final Map methods, final JsonRpcMethod... rpcMethods) { + for (JsonRpcMethod rpcMethod : rpcMethods) { + methods.put(rpcMethod.getName(), rpcMethod); + } + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVote.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVote.java new file mode 100755 index 00000000000..922af424e25 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVote.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.consensus.ibft.jsonrpc.methods; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class IbftProposeValidatorVote implements JsonRpcMethod { + private final VoteProposer voteProposer; + private final JsonRpcParameter parameters; + + public IbftProposeValidatorVote( + final VoteProposer voteProposer, final JsonRpcParameter parameters) { + this.voteProposer = voteProposer; + this.parameters = parameters; + } + + @Override + public String getName() { + return "ibft_proposeValidatorVote"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + + Address validatorAddress = parameters.required(req.getParams(), 0, Address.class); + Boolean add = parameters.required(req.getParams(), 1, Boolean.class); + + if (add) { + voteProposer.auth(validatorAddress); + } else { + voteProposer.drop(validatorAddress); + } + + return new JsonRpcSuccessResponse(req.getId(), true); + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftProtocolManager.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftProtocolManager.java new file mode 100755 index 00000000000..c2ff9703f12 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftProtocolManager.java @@ -0,0 +1,82 @@ +package net.consensys.pantheon.consensus.ibft.protocol; + +import net.consensys.pantheon.consensus.ibft.IbftEvent; +import net.consensys.pantheon.consensus.ibft.IbftEventQueue; +import net.consensys.pantheon.consensus.ibft.IbftEvents; +import net.consensys.pantheon.ethereum.p2p.api.Message; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.util.Arrays; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class IbftProtocolManager implements ProtocolManager { + private final IbftEventQueue ibftEventQueue; + + private final Logger LOGGER = LogManager.getLogger(IbftProtocolManager.class); + + /** + * Constructor for the ibft protocol manager + * + * @param ibftEventQueue Entry point into the ibft event processor + */ + public IbftProtocolManager(final IbftEventQueue ibftEventQueue) { + this.ibftEventQueue = ibftEventQueue; + } + + @Override + public String getSupportedProtocol() { + return IbftSubProtocol.get().getName(); + } + + @Override + public List getSupportedCapabilities() { + return Arrays.asList(IbftSubProtocol.IBFV1); + } + + @Override + public void stop() {} + + @Override + public void awaitStop() throws InterruptedException {} + + /** + * This function is called by the P2P framework when an "IBF" message has been received. This + * function is responsible for: + * + *
    + *
  • Determining if the message was from a current validator (discard if not) + *
  • Determining if the message received was for the 'current round', discarding if old and + * buffering for the future if ahead of current state. + *
  • If the received message is otherwise valid, it is sent to the state machine which is + * responsible for determining how to handle the message given its internal state. + *
+ * + * @param cap The capability under which the message was transmitted. + * @param message The message to be decoded. + */ + @Override + public void processMessage(final Capability cap, final Message message) { + final IbftEvent messageEvent = IbftEvents.fromMessage(message); + ibftEventQueue.add(messageEvent); + } + + @Override + public void handleNewConnection(final PeerConnection peerConnection) {} + + @Override + public void handleDisconnect( + final PeerConnection peerConnection, + final DisconnectReason disconnectReason, + final boolean initiatedByPeer) {} + + @Override + public boolean hasSufficientPeers() { + return true; + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocol.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocol.java new file mode 100755 index 00000000000..5f02459aa8b --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocol.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.consensus.ibft.protocol; + +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public class IbftSubProtocol implements SubProtocol { + + public static String NAME = "IBF"; + public static final Capability IBFV1 = Capability.create(NAME, 1); + + private static final IbftSubProtocol INSTANCE = new IbftSubProtocol(); + + public static IbftSubProtocol get() { + return INSTANCE; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public int messageSpace(final int protocolVersion) { + return NotificationType.getMax() + 1; + } + + @Override + public boolean isValidMessageCode(final int protocolVersion, final int code) { + return NotificationType.fromValue(code).isPresent(); + } + + public enum NotificationType { + PREPREPARE(0), + PREPARE(1), + COMMIT(2), + ROUND_CHANGE(3); + + private final int value; + + NotificationType(final int value) { + this.value = value; + } + + public final int getValue() { + return value; + } + + public static final int getMax() { + return Collections.max( + Arrays.asList(NotificationType.values()), + Comparator.comparing(NotificationType::getValue)) + .getValue(); + } + + public static final Optional fromValue(final int i) { + final List notifications = Arrays.asList(NotificationType.values()); + + return Stream.of(NotificationType.values()).filter(n -> n.getValue() == i).findFirst(); + } + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64Protocol.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64Protocol.java new file mode 100755 index 00000000000..fb2def8c504 --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64Protocol.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.consensus.ibft.protocol; + +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.EthPV63; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.Arrays; +import java.util.List; + +/** + * Represents the istanbul/64 protocol as used by Quorum (effectively an extension of eth/63, which + * adds a single message type (0x11) to encapsulate all communications required for IBFT block + * mining. + */ +public class Istanbul64Protocol implements SubProtocol { + + private static final String NAME = "istanbul"; + private static final int VERSION = 64; + + static final Capability ISTANBUL64 = Capability.create(NAME, 64); + static final int INSTANBUL_MSG = 0x11; + + private static final Istanbul64Protocol INSTANCE = new Istanbul64Protocol(); + + private static final List istanbul64Messages = + Arrays.asList( + EthPV62.STATUS, + EthPV62.NEW_BLOCK_HASHES, + EthPV62.TRANSACTIONS, + EthPV62.GET_BLOCK_HEADERS, + EthPV62.BLOCK_HEADERS, + EthPV62.GET_BLOCK_BODIES, + EthPV62.BLOCK_BODIES, + EthPV62.NEW_BLOCK, + EthPV63.GET_NODE_DATA, + EthPV63.NODE_DATA, + EthPV63.GET_RECEIPTS, + EthPV63.RECEIPTS, + INSTANBUL_MSG); + + @Override + public String getName() { + return NAME; + } + + @Override + public int messageSpace(final int protocolVersion) { + return INSTANBUL_MSG + 1; + } + + @Override + public boolean isValidMessageCode(final int protocolVersion, final int code) { + if (protocolVersion == VERSION) { + return istanbul64Messages.contains(code); + } + return false; + } + + public static Istanbul64Protocol get() { + return INSTANCE; + } +} diff --git a/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64ProtocolManager.java b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64ProtocolManager.java new file mode 100755 index 00000000000..72ebad2347e --- /dev/null +++ b/consensus/ibft/src/main/java/net/consensys/pantheon/consensus/ibft/protocol/Istanbul64ProtocolManager.java @@ -0,0 +1,46 @@ +package net.consensys.pantheon.consensus.ibft.protocol; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.p2p.api.Message; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.List; + +import com.google.common.collect.Lists; + +/** This allows for interoperability with Quorum, but shouldn't be used otherwise. */ +public class Istanbul64ProtocolManager extends EthProtocolManager { + + public Istanbul64ProtocolManager( + final Blockchain blockchain, + final int networkId, + final boolean fastSyncEnabled, + final int workers) { + super(blockchain, networkId, fastSyncEnabled, workers); + } + + @Override + public void processMessage(final Capability cap, final Message message) { + if (cap.equals(Istanbul64Protocol.ISTANBUL64)) { + if (message.getData().getCode() != Istanbul64Protocol.INSTANBUL_MSG) { + super.processMessage(EthProtocol.ETH63, message); + } else { + // TODO(tmm): Determine if the message should be routed to ibftController at a later date. + } + } + } + + @Override + public List getSupportedCapabilities() { + final List result = Lists.newArrayList(Istanbul64Protocol.ISTANBUL64); + result.addAll(super.getSupportedCapabilities()); + return result; + } + + @Override + public String getSupportedProtocol() { + return Istanbul64Protocol.get().getName(); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashingTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashingTest.java new file mode 100755 index 00000000000..4fc8321b474 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHashingTest.java @@ -0,0 +1,118 @@ +package net.consensys.pantheon.consensus.ibft; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +public class IbftBlockHashingTest { + + private static final Address PROPOSER_IN_HEADER = + Address.fromHexString("0x24defc2d149861d3d245749b81fe0e6b28e04f31"); + private static final List
VALIDATORS_IN_HEADER = + Arrays.asList( + PROPOSER_IN_HEADER, + Address.fromHexString("0x2a813d7db3de19b07f92268b6d4125ed295cbe00"), + Address.fromHexString("0x3814f17bd4b7ce47ab8146684b3443c0a4b2fc2c"), + Address.fromHexString("0xc332d0db1704d18f89a590e7586811e36d37ce04")); + private static final List
COMMITTERS_IN_HEADER = + Arrays.asList( + Address.fromHexString("0x3814f17bd4b7ce47ab8146684b3443c0a4b2fc2c"), + PROPOSER_IN_HEADER, + Address.fromHexString("0x2a813d7db3de19b07f92268b6d4125ed295cbe00")); + private static final Hash KNOWN_BLOCK_HASH = + Hash.fromHexString("0x0d60351c129af309fc8597c81358652d3d0f0e3141b5432888c4aae405ee0184"); + + private final BlockHeader header = createKnownHeaderFromCapturedData(); + + @Test + public void recoverProposerAddressFromSeal() { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + final Address proposerAddress = IbftBlockHashing.recoverProposerAddress(header, ibftExtraData); + + assertThat(proposerAddress).isEqualTo(PROPOSER_IN_HEADER); + } + + @Test + public void readValidatorListFromExtraData() { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + assertThat(ibftExtraData.getValidators()).isEqualTo(VALIDATORS_IN_HEADER); + } + + @Test + public void recoverCommitterAddresses() { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + final List
committers = + IbftBlockHashing.recoverCommitterAddresses(header, ibftExtraData); + + assertThat(committers).isEqualTo(COMMITTERS_IN_HEADER); + } + + @Test + public void calculateBlockHash() { + assertThat(header.getHash()).isEqualTo(KNOWN_BLOCK_HASH); + } + + /* + Header information was extracted from a chain export (RLP) from a Quorum IBFT network. + The hash was determined by looking at the parentHash of the subsequent block (i.e. not calculated + by internal calculations, but rather by Quorum). + */ + private BlockHeader createKnownHeaderFromCapturedData() { + final BlockHeaderBuilder builder = new BlockHeaderBuilder(); + + final String extraDataHexString = + "0xd783010800846765746887676f312e392e32856c696e757800000" + + "00000000000f90164f8549424defc2d149861d3d245749b81fe0e6b28e04f31942a813d7db3de19b07f92268b6d4" + + "125ed295cbe00943814f17bd4b7ce47ab8146684b3443c0a4b2fc2c94c332d0db1704d18f89a590e7586811e36d3" + + "7ce04b8417480a32e81936a40da3b8b730c28963a80011fdddb70470573675a11c7871873165e213b80b1ed5bf5a" + + "59a31874baf1d6e83d55141f719ada73815c8712c4c6501f8c9b8417ba97752c9a3d14ae8c5f6f864c2808b816a0" + + "d3ebef9a3b03c3cf9e31311baeb2e32609ccc99f13488e9e8ea192debf1c26f8c70c2332dfbb8456292fd9366110" + + "0b841bbf2d1710a41bee7895dadbbbc92713ba9e74129bb665984f349950d7b5275303db99b12ea3483430079dd5" + + "d90bcc3962f72217863725f6cd72ab5c10c8c540001b8415df74d9bf9687a3da10a4660cfd6fd6739df59db5535f" + + "a3a7c58382ec587f4fe581089a9e3cd4b8c3b77eeabdd756f1f34ffb990cfd47e81bb205bd10be619d001"; + + builder.parentHash( + Hash.fromHexString("0xa7762d3307dbf2ae6a1ae1b09cf61c7603722b2379731b6b90409cdb8c8288a0")); + builder.ommersHash( + Hash.fromHexString("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")); + builder.coinbase(Address.fromHexString("0x0000000000000000000000000000000000000000")); + builder.stateRoot( + Hash.fromHexString("0xca07595b82f908822971b7e848398e3395e59ee52565c7ef3603df1a1fa7bc80")); + builder.transactionsRoot( + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); + builder.receiptsRoot( + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); + builder.logsBloom( + LogsBloomFilter.fromHexString( + "0x000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "0000")); + builder.difficulty(UInt256.ONE); + builder.number(1); + builder.gasLimit(4704588); + builder.gasUsed(0); + builder.timestamp(1530674616); + builder.extraData(BytesValue.fromHexString(extraDataHexString)); + builder.mixHash( + Hash.fromHexString("0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365")); + builder.nonce(0); + builder.blockHashFunction(IbftBlockHashing::calculateHashOfIbftBlockOnChain); + + return builder.buildBlockHeader(); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java new file mode 100755 index 00000000000..d2fe26642d5 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java @@ -0,0 +1,124 @@ +package net.consensys.pantheon.consensus.ibft; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static net.consensys.pantheon.consensus.ibft.IbftProtocolContextFixture.protocolContext; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.consensus.common.VoteType; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import java.util.List; + +import org.junit.Test; + +public class IbftBlockHeaderValidationRulesetFactoryTest { + + @Test + public void ibftValidateHeaderPasses() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = buildBlockHeader(1, proposerKeyPair, validators, null); + final BlockHeader blockHeader = buildBlockHeader(2, proposerKeyPair, validators, parentHeader); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isTrue(); + } + + @Test + public void ibftValidateHeaderFails() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = buildBlockHeader(1, proposerKeyPair, validators, null); + final BlockHeader blockHeader = buildBlockHeader(2, proposerKeyPair, validators, null); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + private BlockHeader buildBlockHeader( + final long number, + final KeyPair proposerKeyPair, + final List
validators, + final BlockHeader parent) { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + + if (parent != null) { + builder.parentHash(parent.getHash()); + } + builder.number(number); + builder.gasLimit(5000); + builder.timestamp(6000 * number); + builder.mixHash( + Hash.fromHexString("0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365")); + builder.ommersHash(Hash.EMPTY_LIST_HASH); + builder.nonce(VoteType.DROP.getNonceValue()); + builder.difficulty(UInt256.ONE); + + // Construct an extraData block + final IbftExtraData initialIbftExtraData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + emptyList(), + Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0), + validators); + + builder.extraData(initialIbftExtraData.encode()); + final BlockHeader parentHeader = builder.buildHeader(); + final Hash proposerSealHash = + IbftBlockHashing.calculateDataHashForProposerSeal(parentHeader, initialIbftExtraData); + + final Signature proposerSignature = SECP256K1.sign(proposerSealHash, proposerKeyPair); + + final IbftExtraData proposedData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + singletonList(proposerSignature), + proposerSignature, + validators); + + final Hash headerHashForCommitters = + IbftBlockHashing.calculateDataHashForCommittedSeal(parentHeader, proposedData); + final Signature proposerAsCommitterSignature = + SECP256K1.sign(headerHashForCommitters, proposerKeyPair); + + final IbftExtraData sealedData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + singletonList(proposerAsCommitterSignature), + proposerSignature, + validators); + + builder.extraData(sealedData.encode()); + return builder.buildHeader(); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporterTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporterTest.java new file mode 100755 index 00000000000..c596d3aeb0d --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftBlockImporterTest.java @@ -0,0 +1,100 @@ +package net.consensys.pantheon.consensus.ibft; + +import static java.util.Collections.emptyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; + +import java.util.Collections; + +import org.junit.Test; + +public class IbftBlockImporterTest { + + private final VoteTallyUpdater voteTallyUpdater = mock(VoteTallyUpdater.class); + private final VoteTally voteTally = mock(VoteTally.class); + private final VoteProposer voteProposer = mock(VoteProposer.class); + + @SuppressWarnings("unchecked") + private final BlockImporter delegate = mock(BlockImporter.class); + + private final MutableBlockchain blockchain = mock(MutableBlockchain.class); + private final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); + private final ProtocolContext context = + new ProtocolContext<>( + blockchain, worldStateArchive, new IbftContext(voteTally, voteProposer)); + + private final IbftBlockImporter importer = new IbftBlockImporter(delegate, voteTallyUpdater); + + @Test + public void voteTallyNotUpdatedWhenBlockImportFails() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final Block block = + new Block( + headerBuilder.buildHeader(), + new BlockBody(Collections.emptyList(), Collections.emptyList())); + + when(delegate.importBlock(context, block, HeaderValidationMode.FULL)).thenReturn(false); + + importer.importBlock(context, block, HeaderValidationMode.FULL); + + verifyZeroInteractions(voteTallyUpdater); + } + + @Test + public void voteTallyNotUpdatedWhenFastBlockImportFails() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + final Block block = + new Block( + headerBuilder.buildHeader(), + new BlockBody(Collections.emptyList(), Collections.emptyList())); + + when(delegate.fastImportBlock(context, block, emptyList(), HeaderValidationMode.LIGHT)) + .thenReturn(false); + + importer.fastImportBlock(context, block, Collections.emptyList(), HeaderValidationMode.LIGHT); + + verifyZeroInteractions(voteTallyUpdater); + } + + @Test + public void voteTallyUpdatedWhenBlockImportSucceeds() { + final Block block = + new Block( + new BlockHeaderTestFixture().buildHeader(), + new BlockBody(Collections.emptyList(), Collections.emptyList())); + + when(delegate.importBlock(context, block, HeaderValidationMode.FULL)).thenReturn(true); + + importer.importBlock(context, block, HeaderValidationMode.FULL); + + verify(voteTallyUpdater).updateForBlock(block.getHeader(), voteTally); + } + + @Test + public void voteTallyUpdatedWhenFastBlockImportSucceeds() { + final Block block = + new Block( + new BlockHeaderTestFixture().buildHeader(), + new BlockBody(Collections.emptyList(), Collections.emptyList())); + + when(delegate.fastImportBlock(context, block, emptyList(), HeaderValidationMode.LIGHT)) + .thenReturn(true); + + importer.fastImportBlock(context, block, Collections.emptyList(), HeaderValidationMode.LIGHT); + + verify(voteTallyUpdater).updateForBlock(block.getHeader(), voteTally); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftEventQueueTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftEventQueueTest.java new file mode 100755 index 00000000000..f4a65d40c0c --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftEventQueueTest.java @@ -0,0 +1,72 @@ +package net.consensys.pantheon.consensus.ibft; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import net.consensys.pantheon.consensus.ibft.IbftEvents.Type; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +public class IbftEventQueueTest { + + private static class DummyIbftEvent implements IbftEvent { + @Override + public Type getType() { + return null; + } + } + + @Test + public void addPoll() throws InterruptedException { + final IbftEventQueue queue = new IbftEventQueue(); + + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isNull(); + final DummyIbftEvent dummyEvent = new DummyIbftEvent(); + queue.add(dummyEvent); + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isEqualTo(dummyEvent); + } + + @Test + public void queueOrdering() throws InterruptedException { + final IbftEventQueue queue = new IbftEventQueue(); + + final DummyIbftEvent dummyEvent1 = new DummyIbftEvent(); + final DummyIbftEvent dummyEvent2 = new DummyIbftEvent(); + final DummyIbftEvent dummyEvent3 = new DummyIbftEvent(); + assertThatCode( + () -> { + queue.add(dummyEvent1); + queue.add(dummyEvent2); + queue.add(dummyEvent3); + }) + .doesNotThrowAnyException(); + + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isEqualTo(dummyEvent1); + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isEqualTo(dummyEvent2); + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isEqualTo(dummyEvent3); + } + + @Test + public void addSizeLimit() throws InterruptedException { + final IbftEventQueue queue = new IbftEventQueue(); + + for (int i = 0; i <= 1000; i++) { + final DummyIbftEvent dummyEvent = new DummyIbftEvent(); + queue.add(dummyEvent); + } + + final DummyIbftEvent dummyEventDiscard = new DummyIbftEvent(); + queue.add(dummyEventDiscard); + + final List drain = new ArrayList<>(); + for (int i = 0; i <= 1000; i++) { + drain.add(queue.poll(0, TimeUnit.MICROSECONDS)); + } + assertThat(drain).doesNotContainNull(); + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isNull(); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftExtraDataTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftExtraDataTest.java new file mode 100755 index 00000000000..b3e0e2c13ad --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftExtraDataTest.java @@ -0,0 +1,135 @@ +package net.consensys.pantheon.consensus.ibft; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import com.google.common.collect.Lists; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +public class IbftExtraDataTest { + + @Test + public void emptyListsConstituteValidContent() { + final Signature proposerSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); + final List
validators = Lists.newArrayList(); + final List committerSeals = Lists.newArrayList(); + + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytesValue(validator)); + encoder.writeBytesValue(proposerSeal.encodedBytes()); + encoder.writeList( + committerSeals, (committer, rlp) -> rlp.writeBytesValue(committer.encodedBytes())); + encoder.endList(); + + // Create a byte buffer with no data. + final byte[] vanity_bytes = new byte[32]; + final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); + final BytesValue bufferToInject = BytesValue.wrap(vanity_data, encoder.encoded()); + + final IbftExtraData extraData = IbftExtraData.decode(bufferToInject); + + assertThat(extraData.getVanityData()).isEqualTo(vanity_data); + assertThat(extraData.getProposerSeal()).isEqualTo(proposerSeal); + assertThat(extraData.getSeals()).isEqualTo(committerSeals); + assertThat(extraData.getValidators()).isEqualTo(validators); + } + + @Test + public void fullyPopulatedDataProducesCorrectlyFormedExtraDataObject() { + final List
validators = Arrays.asList(Address.ECREC, Address.SHA256); + final Signature proposerSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); + final List committerSeals = + Arrays.asList( + Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0), + Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); // This is required to create a "root node" for all RLP'd data + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytesValue(validator)); + encoder.writeBytesValue(proposerSeal.encodedBytes()); + encoder.writeList( + committerSeals, (committer, rlp) -> rlp.writeBytesValue(committer.encodedBytes())); + encoder.endList(); + + // Create randomised vanity data. + final byte[] vanity_bytes = new byte[32]; + new Random().nextBytes(vanity_bytes); + final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); + final BytesValue bufferToInject = BytesValue.wrap(vanity_data, encoder.encoded()); + + final IbftExtraData extraData = IbftExtraData.decode(bufferToInject); + + assertThat(extraData.getVanityData()).isEqualTo(vanity_data); + assertThat(extraData.getProposerSeal()).isEqualTo(proposerSeal); + assertThat(extraData.getSeals()).isEqualTo(committerSeals); + assertThat(extraData.getValidators()).isEqualTo(validators); + } + + @Test(expected = RLPException.class) + public void incorrectlyStructuredRlpThrowsException() { + final Signature proposerSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); + final List
validators = Lists.newArrayList(); + final List committerSeals = Lists.newArrayList(); + + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytesValue(validator)); + encoder.writeBytesValue(proposerSeal.encodedBytes()); + encoder.writeList( + committerSeals, (committer, rlp) -> rlp.writeBytesValue(committer.encodedBytes())); + encoder.writeLong(1); + encoder.endList(); + + final BytesValue bufferToInject = + BytesValue.wrap(BytesValue.wrap(new byte[32]), encoder.encoded()); + + IbftExtraData.decode(bufferToInject); + } + + @Test(expected = RLPException.class) + public void incorrectlySizedVanityDataThrowsException() { + final List
validators = Arrays.asList(Address.ECREC, Address.SHA256); + final Signature proposerSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); + final List committerSeals = + Arrays.asList( + Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0), + Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytesValue(validator)); + encoder.writeBytesValue(proposerSeal.encodedBytes()); + encoder.writeList( + committerSeals, (committer, rlp) -> rlp.writeBytesValue(committer.encodedBytes())); + encoder.endList(); + + final BytesValue bufferToInject = + BytesValue.wrap(BytesValue.wrap(new byte[31]), encoder.encoded()); + + IbftExtraData.decode(bufferToInject); + } + + @Test + public void parseGenesisBlockWithZeroProposerSeal() { + final byte[] genesisBlockExtraData = + Hex.decode( + "0000000000000000000000000000000000000000000000000000000000000000f89af85494c332d0db1704d18f89a590e7586811e36d37ce049424defc2d149861d3d245749b81fe0e6b28e04f31943814f17bd4b7ce47ab8146684b3443c0a4b2fc2c942a813d7db3de19b07f92268b6d4125ed295cbe00b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0"); + + final BytesValue bufferToInject = BytesValue.wrap(genesisBlockExtraData); + + final IbftExtraData extraData = IbftExtraData.decode(bufferToInject); + assertThat(extraData.getProposerSeal()).isNull(); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProcessorTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProcessorTest.java new file mode 100755 index 00000000000..40ca7a7e797 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProcessorTest.java @@ -0,0 +1,153 @@ +package net.consensys.pantheon.consensus.ibft; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import net.consensys.pantheon.consensus.ibft.ibftevent.RoundExpiry; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class IbftProcessorTest { + private ScheduledExecutorService mockExecutorService; + private IbftStateMachine mockStateMachine; + + @Before + public void initialise() { + mockExecutorService = mock(ScheduledExecutorService.class); + mockStateMachine = mock(IbftStateMachine.class); + } + + @Test + public void handlesStopGracefully() throws InterruptedException { + final IbftEventQueue mockQueue = mock(IbftEventQueue.class); + Mockito.when(mockQueue.poll(anyLong(), any())).thenReturn(null); + final IbftProcessor processor = + new IbftProcessor(mockQueue, 1, mockStateMachine, mockExecutorService); + + // Start the IbftProcessor + final ExecutorService processorExecutor = Executors.newSingleThreadExecutor(); + final Future processorFuture = processorExecutor.submit(processor); + + // Make sure we've hit the queue at least once + verify(mockQueue, timeout(3000).atLeastOnce()).poll(anyLong(), any()); + + // Instruct the processor to stop + processor.stop(); + + // Executor shutdown should wait for the processor to gracefully exit + processorExecutor.shutdown(); + // If it hasn't within 200 ms then something will be wrong + final boolean executorCompleted = + processorExecutor.awaitTermination(2000, TimeUnit.MILLISECONDS); + assertThat(executorCompleted).isTrue(); + + // The processor task has exited + assertThat(processorFuture.isDone()).isTrue(); + + // Make sure the round timers executor got cleaned up + verify(mockExecutorService).shutdownNow(); + } + + @Test + public void cleanupExecutorsAfterShutdownNow() throws InterruptedException { + final IbftProcessor processor = + new IbftProcessor(new IbftEventQueue(), 1, mockStateMachine, mockExecutorService); + + // Start the IbftProcessor + final ExecutorService processorExecutor = Executors.newSingleThreadExecutor(); + final Future processorFuture = processorExecutor.submit(processor); + + // Instruct the processor to stop + processor.stop(); + + // Executor shutdown should interrupt the processor + processorExecutor.shutdownNow(); + // If it hasn't within 200 ms then something will be wrong + final boolean executorCompleted = + processorExecutor.awaitTermination(2000, TimeUnit.MILLISECONDS); + assertThat(executorCompleted).isTrue(); + + // The processor task has exited + assertThat(processorFuture.isDone()).isTrue(); + + // Make sure the round timers executor got cleaned up + verify(mockExecutorService).shutdownNow(); + } + + @Test + public void handlesQueueInterruptGracefully() throws InterruptedException { + // Setup a queue that will always interrupt + final IbftEventQueue mockQueue = mock(IbftEventQueue.class); + Mockito.when(mockQueue.poll(anyLong(), any())).thenThrow(new InterruptedException()); + + final IbftProcessor processor = + new IbftProcessor(mockQueue, 1, mockStateMachine, mockExecutorService); + + // Start the IbftProcessor + final ExecutorService processorExecutor = Executors.newSingleThreadExecutor(); + final Future processorFuture = processorExecutor.submit(processor); + + // Make sure we've hit the queue at least once + verify(mockQueue, timeout(3000).atLeastOnce()).poll(anyLong(), any()); + + // Executor shutdown should wait for the processor to gracefully exit + processorExecutor.shutdown(); + + // The processor task hasn't exited off the back of the interrupts + assertThat(processorFuture.isDone()).isFalse(); + + processor.stop(); + + // If it hasn't within 200 ms then something will be wrong and we're not waking up + final boolean executorCompleted = + processorExecutor.awaitTermination(200, TimeUnit.MILLISECONDS); + assertThat(executorCompleted).isTrue(); + + // The processor task has woken up and exited + assertThat(processorFuture.isDone()).isTrue(); + + // Make sure the round timers executor got cleaned up + verify(mockExecutorService).shutdownNow(); + } + + @Test + public void drainEventsIntoStateMachine() throws InterruptedException { + final IbftEventQueue queue = new IbftEventQueue(); + final IbftProcessor processor = + new IbftProcessor(queue, 1, mockStateMachine, mockExecutorService); + + // Start the IbftProcessor + final ExecutorService processorExecutor = Executors.newSingleThreadExecutor(); + processorExecutor.submit(processor); + + final RoundExpiry roundExpiryEvent = new RoundExpiry(new ConsensusRoundIdentifier(1, 1)); + + queue.add(roundExpiryEvent); + queue.add(roundExpiryEvent); + + await().atMost(3000, TimeUnit.MILLISECONDS).until(queue::isEmpty); + + processor.stop(); + processorExecutor.shutdown(); + + verify(mockStateMachine, times(2)).processEvent(eq(roundExpiryEvent), any()); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProtocolContextFixture.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProtocolContextFixture.java new file mode 100755 index 00000000000..229d1e84a68 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/IbftProtocolContextFixture.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.consensus.ibft; + +import static java.util.Arrays.asList; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; + +import java.util.List; + +public class IbftProtocolContextFixture { + + public static ProtocolContext protocolContext(final Address... validators) { + return protocolContext(asList(validators)); + } + + public static ProtocolContext protocolContext(final List
validators) { + return new ProtocolContext<>( + null, null, new IbftContext(new VoteTally(validators), new VoteProposer())); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/RoundTimerTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/RoundTimerTest.java new file mode 100755 index 00000000000..8e8659cfae7 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/RoundTimerTest.java @@ -0,0 +1,123 @@ +package net.consensys.pantheon.consensus.ibft; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +import net.consensys.pantheon.consensus.ibft.ibftevent.RoundExpiry; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class RoundTimerTest { + private ScheduledExecutorService mockExecutorService; + private IbftEventQueue queue; + private RoundTimer timer; + + @Before + public void initialise() { + mockExecutorService = mock(ScheduledExecutorService.class); + queue = new IbftEventQueue(); + timer = new RoundTimer(queue, 1, mockExecutorService); + } + + @Test + public void cancelTimerCancelsWhenNoTimer() { + // Starts with nothing running + assertThat(timer.isRunning()).isFalse(); + // cancel shouldn't die if there's nothing running + timer.cancelTimer(); + // there is still nothing running + assertThat(timer.isRunning()).isFalse(); + } + + @Test + public void startTimerSchedulesTimerCorrectlyForRound0() { + checkTimerForRound(0, 1); + } + + @Test + public void startTimerSchedulesTimerCorrectlyForRound1() { + checkTimerForRound(1, 2); + } + + @Test + public void startTimerSchedulesTimerCorrectlyForRound2() { + checkTimerForRound(2, 4); + } + + @Test + public void startTimerSchedulesTimerCorrectlyForRound3() { + checkTimerForRound(3, 8); + } + + private void checkTimerForRound(final int roundNumber, final long timeout) { + // Start a new timer for round + final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(1, roundNumber); + final ScheduledFuture mockedFuture = mock(ScheduledFuture.class); + Mockito.>when( + mockExecutorService.schedule( + any(Runnable.class), eq(timeout), eq(TimeUnit.MILLISECONDS))) + .thenReturn(mockedFuture); + timer.startTimer(round); + verify(mockExecutorService) + .schedule(any(Runnable.class), eq(timeout), eq(TimeUnit.MILLISECONDS)); + } + + @Test + public void checkRunnableSubmittedDistributesCorrectEvent() throws InterruptedException { + final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(1, 5); + final ScheduledFuture mockedFuture = mock(ScheduledFuture.class); + final ArgumentCaptor expiryRunnable = ArgumentCaptor.forClass(Runnable.class); + Mockito.>when( + mockExecutorService.schedule(any(Runnable.class), anyLong(), eq(TimeUnit.MILLISECONDS))) + .thenReturn(mockedFuture); + timer.startTimer(round); + verify(mockExecutorService) + .schedule(expiryRunnable.capture(), anyLong(), eq(TimeUnit.MILLISECONDS)); + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isNull(); + expiryRunnable.getValue().run(); + assertThat(queue.poll(0, TimeUnit.MICROSECONDS)).isEqualTo(new RoundExpiry(round)); + } + + @Test + public void startTimerCancelsExistingTimer() { + final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(1, 0); + final ScheduledFuture mockedFuture = mock(ScheduledFuture.class); + Mockito.>when( + mockExecutorService.schedule(any(Runnable.class), anyLong(), eq(TimeUnit.MILLISECONDS))) + .thenReturn(mockedFuture); + timer.startTimer(round); + verify(mockedFuture, times(0)).cancel(false); + timer.startTimer(round); + verify(mockedFuture, times(1)).cancel(false); + } + + @Test + public void runningFollowsTheStateOfTheTimer() { + final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(1, 0); + final ScheduledFuture mockedFuture = mock(ScheduledFuture.class); + Mockito.>when( + mockExecutorService.schedule(any(Runnable.class), anyLong(), eq(TimeUnit.MILLISECONDS))) + .thenReturn(mockedFuture); + timer.startTimer(round); + when(mockedFuture.isDone()).thenReturn(false); + assertThat(timer.isRunning()).isTrue(); + when(mockedFuture.isDone()).thenReturn(true); + assertThat(timer.isRunning()).isFalse(); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdaterTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdaterTest.java new file mode 100755 index 00000000000..37fa2b71f09 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/VoteTallyUpdaterTest.java @@ -0,0 +1,185 @@ +package net.consensys.pantheon.consensus.ibft; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.common.VoteType; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import org.junit.Test; + +public class VoteTallyUpdaterTest { + + private static final long EPOCH_LENGTH = 30_000; + public static final Signature INVALID_SEAL = + Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); + private final VoteTally voteTally = mock(VoteTally.class); + private final MutableBlockchain blockchain = mock(MutableBlockchain.class); + private final KeyPair proposerKeyPair = KeyPair.generate(); + private final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + private final Address subject = Address.fromHexString("007f4a23ca00cd043d25c2888c1aa5688f81a344"); + private final Address validator1 = + Address.fromHexString("00dae27b350bae20c5652124af5d8b5cba001ec1"); + + private final VoteTallyUpdater updater = new VoteTallyUpdater(new EpochManager(EPOCH_LENGTH)); + + @Test + public void voteTallyUpdatedWithVoteFromBlock() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(1); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(subject); + addProposer(headerBuilder); + final BlockHeader header = headerBuilder.buildHeader(); + + updater.updateForBlock(header, voteTally); + + verify(voteTally).addVote(proposerAddress, subject, VoteType.ADD); + } + + @Test + public void voteTallyNotUpdatedWhenBlockHasNoVoteSubject() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(1); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder); + final BlockHeader header = headerBuilder.buildHeader(); + + updater.updateForBlock(header, voteTally); + + verifyZeroInteractions(voteTally); + } + + @Test + public void outstandingVotesDiscardedWhenEpochReached() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(EPOCH_LENGTH); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder); + final BlockHeader header = headerBuilder.buildHeader(); + + updater.updateForBlock(header, voteTally); + + verify(voteTally).discardOutstandingVotes(); + verifyNoMoreInteractions(voteTally); + } + + @Test + public void buildVoteTallyByExtractingValidatorsFromGenesisBlock() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(0); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder, asList(subject, validator1)); + final BlockHeader header = headerBuilder.buildHeader(); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); + when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header)); + + final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); + assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); + } + + @Test + public void buildVoteTallyByExtractingValidatorsFromEpochBlock() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(EPOCH_LENGTH); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder, asList(subject, validator1)); + final BlockHeader header = headerBuilder.buildHeader(); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); + when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header)); + + final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); + assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); + } + + @Test + public void addVotesFromBlocksAfterMostRecentEpoch() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.number(EPOCH_LENGTH); + headerBuilder.nonce(VoteType.ADD.getNonceValue()); + headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); + addProposer(headerBuilder, singletonList(validator1)); + final BlockHeader epochHeader = headerBuilder.buildHeader(); + + headerBuilder.number(EPOCH_LENGTH + 1); + headerBuilder.coinbase(subject); + final BlockHeader voteBlockHeader = headerBuilder.buildHeader(); + + when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH + 1); + when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(epochHeader)); + when(blockchain.getBlockHeader(EPOCH_LENGTH + 1)).thenReturn(Optional.of(voteBlockHeader)); + + final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); + assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); + } + + private void addProposer(final BlockHeaderTestFixture builder) { + addProposer(builder, singletonList(proposerAddress)); + } + + private void addProposer(final BlockHeaderTestFixture builder, final List
validators) { + + final IbftExtraData initialIbftExtraData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + emptyList(), + INVALID_SEAL, + validators); + + builder.extraData(initialIbftExtraData.encode()); + final BlockHeader header = builder.buildHeader(); + final Hash proposerSealHash = + IbftBlockHashing.calculateDataHashForProposerSeal(header, initialIbftExtraData); + + final Signature proposerSignature = SECP256K1.sign(proposerSealHash, proposerKeyPair); + + final IbftExtraData proposedData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + singletonList(proposerSignature), + proposerSignature, + validators); + + final Hash headerHashForCommitters = + IbftBlockHashing.calculateDataHashForCommittedSeal(header, proposedData); + final Signature proposerAsCommitterSignature = + SECP256K1.sign(headerHashForCommitters, proposerKeyPair); + + final IbftExtraData sealedData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + singletonList(proposerAsCommitterSignature), + proposerSignature, + validators); + + builder.extraData(sealedData.encode()); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreatorTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreatorTest.java new file mode 100755 index 00000000000..85859aaf10f --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/IbftBlockCreatorTest.java @@ -0,0 +1,104 @@ +package net.consensys.pantheon.consensus.ibft.blockcreation; + +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.ibft.IbftBlockHeaderValidationRulesetFactory; +import net.consensys.pantheon.consensus.ibft.IbftContext; +import net.consensys.pantheon.consensus.ibft.IbftExtraData; +import net.consensys.pantheon.consensus.ibft.IbftProtocolSchedule; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.Lists; +import io.vertx.core.json.JsonObject; +import org.junit.Test; + +public class IbftBlockCreatorTest { + + @Test + public void headerProducedPassesValidationRules() { + // Construct a parent block. + final BlockHeaderTestFixture blockHeaderBuilder = new BlockHeaderTestFixture(); + blockHeaderBuilder.gasLimit(5000); // required to pass validation rule checks. + final BlockHeader parentHeader = blockHeaderBuilder.buildHeader(); + final Optional optionalHeader = Optional.of(parentHeader); + + // Construct a block chain and world state + final MutableBlockchain blockchain = mock(MutableBlockchain.class); + when(blockchain.getChainHeadHash()).thenReturn(parentHeader.getHash()); + when(blockchain.getBlockHeader(any())).thenReturn(optionalHeader); + + final KeyPair nodeKeys = KeyPair.generate(); + // Add the local node as a validator (can't propose a block if node is not a validator). + final Address localAddr = Address.extract(Hash.hash(nodeKeys.getPublicKey().getEncodedBytes())); + final List
initialValidatorList = + Arrays.asList( + Address.fromHexString(String.format("%020d", 1)), + Address.fromHexString(String.format("%020d", 2)), + Address.fromHexString(String.format("%020d", 3)), + Address.fromHexString(String.format("%020d", 4)), + localAddr); + + final VoteTally voteTally = new VoteTally(initialValidatorList); + + final ProtocolSchedule protocolSchedule = + IbftProtocolSchedule.create(new JsonObject("{\"spuriousDragonBlock\":0}")); + final ProtocolContext protContext = + new ProtocolContext<>( + blockchain, + createInMemoryWorldStateArchive(), + new IbftContext(voteTally, new VoteProposer())); + + final IbftBlockCreator blockCreator = + new IbftBlockCreator( + Address.fromHexString(String.format("%020d", 0)), + parent -> + new IbftExtraData( + BytesValue.wrap(new byte[32]), + Lists.newArrayList(), + null, + initialValidatorList) + .encode(), + new PendingTransactions(1), + protContext, + protocolSchedule, + parentGasLimit -> parentGasLimit, + nodeKeys, + Wei.ZERO, + parentHeader); + + final Block block = blockCreator.createBlock(Instant.now().getEpochSecond()); + + final BlockHeaderValidator rules = + IbftBlockHeaderValidationRulesetFactory.ibftProposedBlockValidator(0); + + final boolean validationResult = + rules.validateHeader( + block.getHeader(), parentHeader, protContext, HeaderValidationMode.FULL); + + assertThat(validationResult).isTrue(); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelectorTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelectorTest.java new file mode 100755 index 00000000000..35cfbfb7017 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/blockcreation/ProposerSelectorTest.java @@ -0,0 +1,261 @@ +package net.consensys.pantheon.consensus.ibft.blockcreation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import net.consensys.pantheon.consensus.ibft.IbftBlockHashing; +import net.consensys.pantheon.consensus.ibft.IbftExtraData; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.LinkedList; +import java.util.Optional; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class ProposerSelectorTest { + + private Blockchain createMockedBlockChainWithHeadOf( + final long blockNumber, final KeyPair nodeKeys) { + + final IbftExtraData unsignedExtraData = + new IbftExtraData( + BytesValue.wrap(new byte[32]), + Lists.newArrayList(), + // seals are not required for this test. + null, // No proposer seal till after block exists + Lists.newArrayList()); // Actual content of extradata is irrelevant. + + final BlockHeaderTestFixture headerBuilderFixture = new BlockHeaderTestFixture(); + headerBuilderFixture.number(blockNumber).extraData(unsignedExtraData.encode()); + + final Hash signingHash = + IbftBlockHashing.calculateDataHashForProposerSeal( + headerBuilderFixture.buildHeader(), unsignedExtraData); + + final Signature proposerSignature = SECP256K1.sign(signingHash, nodeKeys); + + // Duplicate the original extraData, but include the proposerSeal + final IbftExtraData signedExtraData = + new IbftExtraData( + unsignedExtraData.getVanityData(), + unsignedExtraData.getSeals(), + proposerSignature, + unsignedExtraData.getValidators()); + + final BlockHeader prevBlockHeader = + headerBuilderFixture.extraData(signedExtraData.encode()).buildHeader(); + + // Construct a block chain and world state + final MutableBlockchain blockchain = mock(MutableBlockchain.class); + when(blockchain.getBlockHeader(anyLong())).thenReturn(Optional.of(prevBlockHeader)); + + return blockchain; + } + + /** + * This creates a list of validators, with the a number of validators above and below the local + * address. The returned list is sorted. + * + * @param localAddr The address of the node which signed the parent block + * @param countLower The number of validators which have a higher address than localAddr + * @param countHigher The number of validators which have a lower address than localAddr + * @return A sorted list of validators which matches parameters (including the localAddr). + */ + private LinkedList
createValidatorList( + final Address localAddr, final int countLower, final int countHigher) { + final LinkedList
result = Lists.newLinkedList(); + + // Note: Order of this list is irrelevant, is sorted by value later. + result.add(localAddr); + + for (int i = 0; i < countLower; i++) { + result.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, i - countLower)); + } + + for (int i = 0; i < countHigher; i++) { + result.add(AddressHelpers.calculateAddressWithRespectTo(localAddr, i + 1)); + } + + result.sort(null); + return result; + } + + @Test + public void roundRobinChangesProposerOnRoundZeroOfNextBlock() { + final long PREV_BLOCK_NUMBER = 2; + final KeyPair prevProposerKeys = KeyPair.generate(); + final Address localAddr = + Address.extract(Hash.hash(prevProposerKeys.getPublicKey().getEncodedBytes())); + + final Blockchain blockchain = + createMockedBlockChainWithHeadOf(PREV_BLOCK_NUMBER, prevProposerKeys); + + final LinkedList
validatorList = createValidatorList(localAddr, 0, 4); + final VoteTally voteTally = new VoteTally(validatorList); + + final ProposerSelector uut = new ProposerSelector(blockchain, voteTally, true); + + final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 0); + + final Address nextProposer = uut.selectProposerForRound(roundId); + + assertThat(nextProposer).isEqualTo(validatorList.get(1)); + } + + @Test + public void lastValidatorInListValidatedPreviousBlockSoFirstIsNextProposer() { + final long PREV_BLOCK_NUMBER = 2; + final KeyPair prevProposerKeys = KeyPair.generate(); + + final Blockchain blockchain = + createMockedBlockChainWithHeadOf(PREV_BLOCK_NUMBER, prevProposerKeys); + + final Address localAddr = + Address.extract(Hash.hash(prevProposerKeys.getPublicKey().getEncodedBytes())); + + final LinkedList
validatorList = createValidatorList(localAddr, 4, 0); + final VoteTally voteTally = new VoteTally(validatorList); + + final ProposerSelector uut = new ProposerSelector(blockchain, voteTally, true); + + final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 0); + + final Address nextProposer = uut.selectProposerForRound(roundId); + + assertThat(nextProposer).isEqualTo(validatorList.get(0)); + } + + @Test + public void stickyProposerDoesNotChangeOnRoundZeroOfNextBlock() { + final long PREV_BLOCK_NUMBER = 2; + final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 0); + + final KeyPair prevProposerKeys = KeyPair.generate(); + final Blockchain blockchain = + createMockedBlockChainWithHeadOf(PREV_BLOCK_NUMBER, prevProposerKeys); + + final Address localAddr = + Address.extract(Hash.hash(prevProposerKeys.getPublicKey().getEncodedBytes())); + final LinkedList
validatorList = createValidatorList(localAddr, 4, 0); + final VoteTally voteTally = new VoteTally(validatorList); + + final ProposerSelector uut = new ProposerSelector(blockchain, voteTally, false); + final Address nextProposer = uut.selectProposerForRound(roundId); + + assertThat(nextProposer).isEqualTo(localAddr); + } + + @Test + public void stickyProposerChangesOnSubsequentRoundsAtSameBlockHeight() { + final long PREV_BLOCK_NUMBER = 2; + ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 0); + + final KeyPair prevProposerKeys = KeyPair.generate(); + final Blockchain blockchain = + createMockedBlockChainWithHeadOf(PREV_BLOCK_NUMBER, prevProposerKeys); + + final Address localAddr = + Address.extract(Hash.hash(prevProposerKeys.getPublicKey().getEncodedBytes())); + final LinkedList
validatorList = createValidatorList(localAddr, 4, 0); + final VoteTally voteTally = new VoteTally(validatorList); + + final ProposerSelector uut = new ProposerSelector(blockchain, voteTally, false); + assertThat(uut.selectProposerForRound(roundId)).isEqualTo(localAddr); + + roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 1); + assertThat(uut.selectProposerForRound(roundId)).isEqualTo(validatorList.get(0)); + + roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 2); + assertThat(uut.selectProposerForRound(roundId)).isEqualTo(validatorList.get(1)); + } + + @Test + public void whenProposerSelfRemovesSelectsNextProposerInLineEvenWhenSticky() { + final long PREV_BLOCK_NUMBER = 2; + final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 0); + + final KeyPair prevProposerKeys = KeyPair.generate(); + final Blockchain blockchain = + createMockedBlockChainWithHeadOf(PREV_BLOCK_NUMBER, prevProposerKeys); + + final Address localAddr = + Address.extract(Hash.hash(prevProposerKeys.getPublicKey().getEncodedBytes())); + + // LocalAddr will be in index 2 - the next proposer will also be in 2 (as prev proposer is + // removed) + final LinkedList
validatorList = createValidatorList(localAddr, 2, 2); + validatorList.remove(localAddr); + + // Note the signer of the Previous block was not included. + final VoteTally voteTally = new VoteTally(validatorList); + + final ProposerSelector uut = new ProposerSelector(blockchain, voteTally, false); + + assertThat(uut.selectProposerForRound(roundId)).isEqualTo(validatorList.get(2)); + } + + @Test + public void whenProposerSelfRemovesSelectsNextProposerInLineEvenWhenRoundRobin() { + final long PREV_BLOCK_NUMBER = 2; + final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 0); + + final KeyPair prevProposerKeys = KeyPair.generate(); + final Blockchain blockchain = + createMockedBlockChainWithHeadOf(PREV_BLOCK_NUMBER, prevProposerKeys); + + final Address localAddr = + Address.extract(Hash.hash(prevProposerKeys.getPublicKey().getEncodedBytes())); + + // LocalAddr will be in index 2 - the next proposer will also be in 2 (as prev proposer is + // removed) + final LinkedList
validatorList = createValidatorList(localAddr, 2, 2); + validatorList.remove(localAddr); + + // Note the signer of the Previous block was not included. + final VoteTally voteTally = new VoteTally(validatorList); + + final ProposerSelector uut = new ProposerSelector(blockchain, voteTally, true); + + assertThat(uut.selectProposerForRound(roundId)).isEqualTo(validatorList.get(2)); + } + + @Test + public void proposerSelfRemovesAndHasHighestAddressNewProposerIsFirstInList() { + final long PREV_BLOCK_NUMBER = 2; + final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(PREV_BLOCK_NUMBER + 1, 0); + + final KeyPair prevProposerKeys = KeyPair.generate(); + final Blockchain blockchain = + createMockedBlockChainWithHeadOf(PREV_BLOCK_NUMBER, prevProposerKeys); + + final Address localAddr = + Address.extract(Hash.hash(prevProposerKeys.getPublicKey().getEncodedBytes())); + + // LocalAddr will be in index 2 - the next proposer will also be in 2 (as prev proposer is + // removed) + final LinkedList
validatorList = createValidatorList(localAddr, 4, 0); + validatorList.remove(localAddr); + + // Note the signer of the Previous block was not included. + final VoteTally voteTally = new VoteTally(validatorList); + + final ProposerSelector uut = new ProposerSelector(blockchain, voteTally, false); + + assertThat(uut.selectProposerForRound(roundId)).isEqualTo(validatorList.get(0)); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRuleTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRuleTest.java new file mode 100755 index 00000000000..8d642f6ca81 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRuleTest.java @@ -0,0 +1,320 @@ +package net.consensys.pantheon.consensus.ibft.headervalidationrules; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.ibft.IbftBlockHashing; +import net.consensys.pantheon.consensus.ibft.IbftContext; +import net.consensys.pantheon.consensus.ibft.IbftExtraData; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class IbftExtraDataValidationRuleTest { + + private BlockHeader createProposedBlockHeader( + final KeyPair proposerKeyPair, final List
validators) { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + + // Construct an extraData block and add to a header + final IbftExtraData initialIbftExtraData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + emptyList(), + null, + validators); + builder.extraData(initialIbftExtraData.encode()); + final BlockHeader header = builder.buildHeader(); + + // Hash the header (ignoring committer and proposer seals), and create signature + final Hash proposerSealHash = + IbftBlockHashing.calculateDataHashForProposerSeal(header, initialIbftExtraData); + final Signature proposerSignature = SECP256K1.sign(proposerSealHash, proposerKeyPair); + + // Construct a new extraData block, containing the constructed proposer signature + final IbftExtraData proposedData = + new IbftExtraData( + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + emptyList(), + proposerSignature, + validators); + + // insert the signed ExtraData into the block + builder.extraData(proposedData.encode()); + return builder.buildHeader(); + } + + private IbftExtraData createExtraDataWithCommitSeals( + final BlockHeader header, final Collection committerKeyPairs) { + final IbftExtraData extraDataInHeader = IbftExtraData.decode(header.getExtraData()); + + final Hash headerHashForCommitters = + IbftBlockHashing.calculateDataHashForCommittedSeal(header, extraDataInHeader); + + final List commitSeals = + committerKeyPairs + .stream() + .map(keys -> SECP256K1.sign(headerHashForCommitters, keys)) + .collect(Collectors.toList()); + + return new IbftExtraData( + extraDataInHeader.getVanityData(), + commitSeals, + extraDataInHeader.getProposerSeal(), + extraDataInHeader.getValidators()); + } + + @Test + public void correctlyConstructedHeaderPassesValidation() { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = singletonList(proposerAddress); + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators); + + // Insert an extraData block with committer seals. + final IbftExtraData commitedExtraData = + createExtraDataWithCommitSeals(header, singletonList(proposerKeyPair)); + builder.extraData(commitedExtraData.encode()); + header = builder.buildHeader(); + + assertThat(extraDataValidationRule.validate(header, null, context)).isTrue(); + } + + @Test + public void insufficientCommitSealsFailsValidation() { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = singletonList(proposerAddress); + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + final BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators); + + // Note that no committer seals are in the header's IBFT extra data. + final IbftExtraData headerExtraData = IbftExtraData.decode(header.getExtraData()); + assertThat(headerExtraData.getSeals().size()).isEqualTo(0); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void outOfOrderValidatorListFailsValidation() { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = + Lists.newArrayList( + AddressHelpers.calculateAddressWithRespectTo(proposerAddress, 1), proposerAddress); + + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators); + + // Insert an extraData block with committer seals. + final IbftExtraData commitedExtraData = + createExtraDataWithCommitSeals(header, singletonList(proposerKeyPair)); + builder.extraData(commitedExtraData.encode()); + header = builder.buildHeader(); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void proposerNotInValidatorListFailsValidation() { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = + Lists.newArrayList( + AddressHelpers.calculateAddressWithRespectTo(proposerAddress, 1), proposerAddress); + + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators); + + // Insert an extraData block with committer seals. + final IbftExtraData commitedExtraData = + createExtraDataWithCommitSeals(header, singletonList(proposerKeyPair)); + builder.extraData(commitedExtraData.encode()); + header = builder.buildHeader(); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void mismatchingReportedValidatorsVsLocallyStoredListFailsValidation() { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = Lists.newArrayList(proposerAddress); + + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + // Add another validator to the list reported in the IbftExtraData (note, as the + validators.add(AddressHelpers.calculateAddressWithRespectTo(proposerAddress, 1)); + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators); + + // Insert an extraData block with committer seals. + final IbftExtraData commitedExtraData = + createExtraDataWithCommitSeals(header, singletonList(proposerKeyPair)); + builder.extraData(commitedExtraData.encode()); + header = builder.buildHeader(); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void committerNotInValidatorListFailsValidation() { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = singletonList(proposerAddress); + final VoteTally voteTally = new VoteTally(validators); + + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators); + + // Insert an extraData block with committer seals. + final KeyPair nonValidatorKeyPair = KeyPair.generate(); + final IbftExtraData commitedExtraData = + createExtraDataWithCommitSeals(header, singletonList(nonValidatorKeyPair)); + builder.extraData(commitedExtraData.encode()); + header = builder.buildHeader(); + + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void ratioOfCommittersToValidatorsAffectValidation() { + assertThat(subExecution(4, 4)).isEqualTo(true); + assertThat(subExecution(4, 3)).isEqualTo(true); + assertThat(subExecution(4, 2)).isEqualTo(false); + + assertThat(subExecution(5, 3)).isEqualTo(true); + assertThat(subExecution(5, 2)).isEqualTo(false); + + assertThat(subExecution(6, 4)).isEqualTo(true); + assertThat(subExecution(6, 3)).isEqualTo(true); + assertThat(subExecution(6, 2)).isEqualTo(false); + + assertThat(subExecution(7, 5)).isEqualTo(true); + assertThat(subExecution(7, 4)).isEqualTo(false); + + assertThat(subExecution(9, 5)).isEqualTo(true); + assertThat(subExecution(9, 4)).isEqualTo(false); + + assertThat(subExecution(10, 7)).isEqualTo(true); + assertThat(subExecution(10, 6)).isEqualTo(false); + + assertThat(subExecution(12, 7)).isEqualTo(true); + assertThat(subExecution(12, 6)).isEqualTo(false); + + // The concern in the above is that when using 6 validators, only 1/2 the validators are + // required to seal a block. All other combinations appear to be safe they always have >50% + // validators sealing the block. + + } + + private boolean subExecution(final int validatorCount, final int committerCount) { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + final KeyPair proposerKeyPair = KeyPair.generate(); + + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = Lists.newArrayList(); + final List committerKeys = Lists.newArrayList(); + validators.add(proposerAddress); + committerKeys.add(proposerKeyPair); + for (int i = 0; i < validatorCount - 1; i++) { // need -1 to account for proposer + final KeyPair committerKeyPair = KeyPair.generate(); + committerKeys.add(committerKeyPair); + validators.add(Address.extract(Hash.hash(committerKeyPair.getPublicKey().getEncodedBytes()))); + } + + Collections.sort(validators); + final VoteTally voteTally = new VoteTally(validators); + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators); + final IbftExtraData commitedExtraData = + createExtraDataWithCommitSeals(header, committerKeys.subList(0, committerCount)); + + builder.extraData(commitedExtraData.encode()); + header = builder.buildHeader(); + + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + return extraDataValidationRule.validate(header, null, context); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVoteTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVoteTest.java new file mode 100755 index 00000000000..a0ed7c6f806 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/jsonrpc/methods/IbftProposeValidatorVoteTest.java @@ -0,0 +1,108 @@ +package net.consensys.pantheon.consensus.ibft.jsonrpc.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class IbftProposeValidatorVoteTest { + private final VoteProposer voteProposer = mock(VoteProposer.class); + private final JsonRpcParameter jsonRpcParameter = new JsonRpcParameter(); + private final String IBFT_METHOD = "ibft_proposeValidatorVote"; + private final String JSON_RPC_VERSION = "2.0"; + private IbftProposeValidatorVote method; + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setup() { + method = new IbftProposeValidatorVote(voteProposer, jsonRpcParameter); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(IBFT_METHOD); + } + + @Test + public void exceptionWhenNoParamsSupplied() { + final JsonRpcRequest request = requestWithParams(); + + expectedException.expect(InvalidJsonRpcParameters.class); + expectedException.expectMessage("Missing required json rpc parameter at index 0"); + + method.response(request); + } + + @Test + public void exceptionWhenNoAuthSupplied() { + final JsonRpcRequest request = requestWithParams(Address.fromHexString("1")); + + expectedException.expect(InvalidJsonRpcParameters.class); + expectedException.expectMessage("Missing required json rpc parameter at index 1"); + + method.response(request); + } + + @Test + public void exceptionWhenNoAddressSupplied() { + final JsonRpcRequest request = requestWithParams("true"); + + expectedException.expect(InvalidJsonRpcParameters.class); + expectedException.expectMessage("Invalid json rpc parameter at index 0"); + + method.response(request); + } + + @Test + public void exceptionWhenInvalidBoolParameterSupplied() { + final JsonRpcRequest request = requestWithParams(Address.fromHexString("1"), "c"); + + expectedException.expect(InvalidJsonRpcParameters.class); + expectedException.expectMessage("Invalid json rpc parameter at index 1"); + + method.response(request); + } + + @Test + public void addValidator() { + final Address parameterAddress = Address.fromHexString("1"); + final JsonRpcRequest request = requestWithParams(parameterAddress, "true"); + JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), true); + + JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + + verify(voteProposer).auth(parameterAddress); + } + + @Test + public void removeValidator() { + final Address parameterAddress = Address.fromHexString("1"); + final JsonRpcRequest request = requestWithParams(parameterAddress, "false"); + JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), true); + + JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + + verify(voteProposer).drop(parameterAddress); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, IBFT_METHOD, params); + } +} diff --git a/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocolTest.java b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocolTest.java new file mode 100755 index 00000000000..b7a0be28bb9 --- /dev/null +++ b/consensus/ibft/src/test/java/net/consensys/pantheon/consensus/ibft/protocol/IbftSubProtocolTest.java @@ -0,0 +1,32 @@ +package net.consensys.pantheon.consensus.ibft.protocol; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class IbftSubProtocolTest { + + @Test + public void messageSpaceReportsCorrectly() { + final IbftSubProtocol subProt = new IbftSubProtocol(); + + assertThat(subProt.messageSpace(1)).isEqualTo(4); + } + + @Test + public void allIbftMessageTypesAreRecognisedAsValidByTheSubProtocol() { + final IbftSubProtocol subProt = new IbftSubProtocol(); + + assertThat(subProt.isValidMessageCode(1, 0)).isTrue(); + assertThat(subProt.isValidMessageCode(1, 1)).isTrue(); + assertThat(subProt.isValidMessageCode(1, 2)).isTrue(); + assertThat(subProt.isValidMessageCode(1, 3)).isTrue(); + } + + @Test + public void invalidMessageTypesAreNotAcceptedByTheSubprotocol() { + final IbftSubProtocol subProt = new IbftSubProtocol(); + + assertThat(subProt.isValidMessageCode(1, 4)).isFalse(); + } +} diff --git a/crypto/build.gradle b/crypto/build.gradle new file mode 100755 index 00000000000..de57176939e --- /dev/null +++ b/crypto/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-crypto' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + api project(':util') + + api 'org.bouncycastle:bcprov-jdk15on' + + implementation 'com.google.guava:guava' + implementation 'org.apache.logging.log4j:log4j-api' + + runtime 'org.apache.logging.log4j:log4j-core' + + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' + testImplementation 'junit:junit' +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/BouncyCastleMessageDigestFactory.java b/crypto/src/main/java/net/consensys/pantheon/crypto/BouncyCastleMessageDigestFactory.java new file mode 100755 index 00000000000..8ccda4a8db5 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/BouncyCastleMessageDigestFactory.java @@ -0,0 +1,16 @@ +package net.consensys.pantheon.crypto; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +public class BouncyCastleMessageDigestFactory { + + private static final BouncyCastleProvider securityProvider = new BouncyCastleProvider(); + + @SuppressWarnings("DoNotInvokeMessageDigestDirectly") + public static MessageDigest create(final String algorithm) throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm, securityProvider); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/Hash.java b/crypto/src/main/java/net/consensys/pantheon/crypto/Hash.java new file mode 100755 index 00000000000..126462f79f3 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/Hash.java @@ -0,0 +1,90 @@ +package net.consensys.pantheon.crypto; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** Various utilities for providing hashes (digests) of arbitrary data. */ +public abstract class Hash { + private Hash() {} + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + public static final String KECCAK256_ALG = "KECCAK-256"; + + private static final String SHA256_ALG = "SHA-256"; + private static final String RIPEMD160 = "RIPEMD160"; + + /** + * Helper method to generate a digest using the provided algorithm. + * + * @param input The input bytes to produce the digest for. + * @param alg The name of the digest algorithm to use. + * @return A digest. + */ + private static byte[] digestUsingAlgorithm(final byte[] input, final String alg) { + MessageDigest digest; + try { + digest = BouncyCastleMessageDigestFactory.create(alg); + digest.update(input); + return digest.digest(); + } catch (final NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Helper method to generate a digest using the provided algorithm. + * + * @param input The input bytes to produce the digest for. + * @param alg The name of the digest algorithm to use. + * @return A digest. + */ + private static byte[] digestUsingAlgorithm(final BytesValue input, final String alg) { + MessageDigest digest; + try { + digest = BouncyCastleMessageDigestFactory.create(alg); + input.update(digest); + return digest.digest(); + } catch (final NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Digest using SHA2-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes32 sha256(final BytesValue input) { + return Bytes32.wrap(digestUsingAlgorithm(input, SHA256_ALG)); + } + + /** + * Digest using keccak-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes32 keccak256(final BytesValue input) { + return Bytes32.wrap(digestUsingAlgorithm(input, KECCAK256_ALG)); + } + + /** + * Digest using RIPEMD-160. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static BytesValue ripemd160(final BytesValue input) { + return BytesValue.wrap(digestUsingAlgorithm(input, RIPEMD160)); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/InvalidSEC256K1PrivateKeyStoreException.java b/crypto/src/main/java/net/consensys/pantheon/crypto/InvalidSEC256K1PrivateKeyStoreException.java new file mode 100755 index 00000000000..2ab0bd70ef5 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/InvalidSEC256K1PrivateKeyStoreException.java @@ -0,0 +1,3 @@ +package net.consensys.pantheon.crypto; + +public class InvalidSEC256K1PrivateKeyStoreException extends RuntimeException {} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/PRNGSecureRandom.java b/crypto/src/main/java/net/consensys/pantheon/crypto/PRNGSecureRandom.java new file mode 100755 index 00000000000..b57cd1dedf1 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/PRNGSecureRandom.java @@ -0,0 +1,81 @@ +package net.consensys.pantheon.crypto; + +import static net.consensys.pantheon.crypto.PersonalisationString.getPersonalizationString; + +import java.security.SecureRandom; + +import com.google.common.annotations.VisibleForTesting; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.prng.SP800SecureRandomBuilder; + +public class PRNGSecureRandom extends SecureRandom { + private static final int SECURITY_STRENGTH = 256; + private final SecureRandom sp800SecureRandom; + private final QuickEntropy quickEntropy; + + public PRNGSecureRandom() { + this(new QuickEntropy(), new SP800SecureRandomBuilder()); + } + + @VisibleForTesting + protected PRNGSecureRandom( + final QuickEntropy quickEntropy, final SP800SecureRandomBuilder sp800SecureRandomBuilder) { + final Digest digest = new SHA256Digest(); + final byte[] personalizationString = getPersonalizationString(); + this.quickEntropy = quickEntropy; + // prediction resistance is not required as we are applying a light reseed on each nextBytes + // with quick entropy. + this.sp800SecureRandom = + sp800SecureRandomBuilder + .setSecurityStrength(SECURITY_STRENGTH) + .setPersonalizationString(personalizationString) + .buildHash(digest, null, false); + } + + @Override + public String getAlgorithm() { + return sp800SecureRandom.getAlgorithm(); + } + + @Override + /* + JDK SecureRandom.setSeed method is synchronized on some JDKs, it varies between versions. + But sync at method level isn't needed as we are delegating to SP800SecureRandom and it uses a sync block. + */ + @SuppressWarnings("UnsynchronizedOverridesSynchronized") + public void setSeed(final byte[] seed) { + sp800SecureRandom.setSeed(seed); + } + + @Override + /* + JDK SecureRandom.setSeed method is synchronized on some JDKs, it varies between versions. + But sync at method level isn't needed as we are delegating to SP800SecureRandom and it uses a sync block. + */ + @SuppressWarnings("UnsynchronizedOverridesSynchronized") + public void setSeed(final long seed) { + // As setSeed is called by the super constructor this can be called before the sp800SecureRandom + // field is initialised + if (sp800SecureRandom != null) { + sp800SecureRandom.setSeed(seed); + } + } + + @Override + /* + JDK SecureRandom.nextBytes method is synchronized on some JDKs, it varies between versions. + But sync at method level isn't needed as we are delegating to SP800SecureRandom and it uses a sync block. + */ + @SuppressWarnings("UnsynchronizedOverridesSynchronized") + public void nextBytes(final byte[] bytes) { + sp800SecureRandom.setSeed(quickEntropy.getQuickEntropy()); + sp800SecureRandom.nextBytes(bytes); + } + + @Override + public byte[] generateSeed(final int numBytes) { + sp800SecureRandom.setSeed(quickEntropy.getQuickEntropy()); + return sp800SecureRandom.generateSeed(numBytes); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/PersonalisationString.java b/crypto/src/main/java/net/consensys/pantheon/crypto/PersonalisationString.java new file mode 100755 index 00000000000..a7906c67ce6 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/PersonalisationString.java @@ -0,0 +1,53 @@ +package net.consensys.pantheon.crypto; + +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Enumeration; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class PersonalisationString { + private static final Logger LOGGER = LogManager.getLogger(PersonalisationString.class); + private static final byte[] NETWORK_MACS = networkHardwareAddresses(); + + public static byte[] getPersonalizationString() { + final Runtime runtime = Runtime.getRuntime(); + final byte[] threadId = Longs.toByteArray(Thread.currentThread().getId()); + final byte[] availProcessors = Ints.toByteArray(runtime.availableProcessors()); + final byte[] freeMem = Longs.toByteArray(runtime.freeMemory()); + final byte[] runtimeMem = Longs.toByteArray(runtime.maxMemory()); + return Bytes.concat(threadId, availProcessors, freeMem, runtimeMem, NETWORK_MACS); + } + + private static byte[] networkHardwareAddresses() { + final byte[] networkAddresses = new byte[256]; + final ByteBuffer buffer = ByteBuffer.wrap(networkAddresses); + try { + final Enumeration networkInterfaces = + NetworkInterface.getNetworkInterfaces(); + if (networkInterfaces != null) { + while (networkInterfaces.hasMoreElements()) { + final NetworkInterface networkInterface = networkInterfaces.nextElement(); + final byte[] hardwareAddress = networkInterface.getHardwareAddress(); + if (hardwareAddress != null) { + buffer.put(hardwareAddress); + } + } + } + } catch (SocketException | BufferOverflowException e) { + LOGGER.warn( + "Failed to obtain network hardware address for use in random number personalisation string, " + + "continuing without this piece of random information", + e); + } + + return Arrays.copyOf(networkAddresses, buffer.position()); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/QuickEntropy.java b/crypto/src/main/java/net/consensys/pantheon/crypto/QuickEntropy.java new file mode 100755 index 00000000000..7a131bddf38 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/QuickEntropy.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.crypto; + +import com.google.common.primitives.Bytes; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +public class QuickEntropy { + + public byte[] getQuickEntropy() { + final byte[] nanoTimeBytes = Longs.toByteArray(System.nanoTime()); + final byte[] objectHashBytes = Ints.toByteArray(new Object().hashCode()); + return Bytes.concat(nanoTimeBytes, objectHashBytes); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/SECP256K1.java b/crypto/src/main/java/net/consensys/pantheon/crypto/SECP256K1.java new file mode 100755 index 00000000000..ca9d57bb289 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/SECP256K1.java @@ -0,0 +1,676 @@ +package net.consensys.pantheon.crypto; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static net.consensys.pantheon.util.bytes.BytesValues.asUnsignedBigInteger; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.bytes.MutableBytesValue; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.UnaryOperator; + +import com.google.common.base.Objects; +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; + +/* + * Adapted from the BitcoinJ ECKey (Apache 2 License) implementation: + * https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/ECKey.java + * + * + * Adapted from the web3j (Apache 2 License) implementations: + * https://github.com/web3j/web3j/crypto/src/main/java/org/web3j/crypto/*.java + */ +public class SECP256K1 { + + private static final String ALGORITHM = "ECDSA"; + private static final String CURVE_NAME = "secp256k1"; + private static final String PROVIDER = "BC"; + + public static final ECDomainParameters CURVE; + public static final BigInteger HALF_CURVE_ORDER; + + private static final KeyPairGenerator KEY_PAIR_GENERATOR; + private static final BigInteger CURVE_ORDER; + + static { + Security.addProvider(new BouncyCastleProvider()); + + final X9ECParameters params = SECNamedCurves.getByName(CURVE_NAME); + CURVE = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + CURVE_ORDER = CURVE.getN(); + HALF_CURVE_ORDER = CURVE_ORDER.shiftRight(1); + try { + KEY_PAIR_GENERATOR = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER); + } catch (final Exception e) { + throw new RuntimeException(e); + } + final ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(CURVE_NAME); + try { + KEY_PAIR_GENERATOR.initialize(ecGenParameterSpec, SecureRandomProvider.createSecureRandom()); + } catch (final InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + /** Decompress a compressed public key (x co-ord and low-bit of y-coord). */ + private static ECPoint decompressKey(final BigInteger xBN, final boolean yBit) { + final X9IntegerConverter x9 = new X9IntegerConverter(); + final byte[] compEnc = x9.integerToBytes(xBN, 1 + x9.getByteLength(CURVE.getCurve())); + compEnc[0] = (byte) (yBit ? 0x03 : 0x02); + // TODO: Find a better way to handle an invalid point compression here. + // Currently ECCurve#decodePoint throws an IllegalArgumentException. + return CURVE.getCurve().decodePoint(compEnc); + } + + /** + * Given the components of a signature and a selector value, recover and return the public key + * that generated the signature according to the algorithm in SEC1v2 section 4.1.6. + * + *

If this method returns null it means recovery was not possible and recId should be iterated. + * + *

Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, + * and if the output is null OR a key that is not the one you expect, you try again with the next + * recId. + * + * @param recId Which possible key to recover. + * @param r The R component of the signature. + * @param s The S component of the signature. + * @param dataHash Hash of the data that was signed. + * @return An ECKey containing only the public part, or null if recovery wasn't possible. + */ + private static BigInteger recoverFromSignature( + final int recId, final BigInteger r, final BigInteger s, final Bytes32 dataHash) { + assert (recId >= 0); + assert (r.signum() >= 0); + assert (s.signum() >= 0); + assert (dataHash != null); + + // 1.0 For j from 0 to h (h == recId here and the loop is outside this function) + // 1.1 Let x = r + jn + final BigInteger n = CURVE.getN(); // Curve order. + final BigInteger i = BigInteger.valueOf((long) recId / 2); + final BigInteger x = r.add(i.multiply(n)); + // 1.2. Convert the integer x to an octet string X of length mlen using the conversion + // routine specified in Section 2.3.7, where mlen = ⌈(log2 p)/8⌉ or mlen = ⌈m/8⌉. + // 1.3. Convert the octet string (16 set binary digits)||X to an elliptic curve point R + // using the conversion routine specified in Section 2.3.4. If this conversion + // routine outputs "invalid", then do another iteration of Step 1. + // + // More concisely, what these points mean is to use X as a compressed public key. + final BigInteger prime = SecP256K1Curve.q; + if (x.compareTo(prime) >= 0) { + // Cannot have point co-ordinates larger than this as everything takes place modulo Q. + return null; + } + // Compressed keys require you to know an extra bit of data about the y-coord as there are + // two possibilities. So it's encoded in the recId. + final ECPoint R = decompressKey(x, (recId & 1) == 1); + // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers + // responsibility). + if (!R.multiply(n).isInfinity()) { + return null; + } + // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. + final BigInteger e = asUnsignedBigInteger(dataHash); + // 1.6. For k from 1 to 2 do the following. (loop is outside this function via + // iterating recId) + // 1.6.1. Compute a candidate public key as: + // Q = mi(r) * (sR - eG) + // + // Where mi(x) is the modular multiplicative inverse. We transform this into the following: + // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) + // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). + // In the above equation ** is point multiplication and + is point addition (the EC group + // operator). + // + // We can find the additive inverse by subtracting e from zero then taking the mod. For + // example the additive inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and + // -3 mod 11 = 8. + final BigInteger eInv = BigInteger.ZERO.subtract(e).mod(n); + final BigInteger rInv = r.modInverse(n); + final BigInteger srInv = rInv.multiply(s).mod(n); + final BigInteger eInvrInv = rInv.multiply(eInv).mod(n); + final ECPoint q = ECAlgorithms.sumOfTwoMultiplies(CURVE.getG(), eInvrInv, R, srInv); + + final byte[] qBytes = q.getEncoded(false); + // We remove the prefix + return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length)); + } + + public static Signature sign(final Bytes32 dataHash, final KeyPair keyPair) { + final ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + + final ECPrivateKeyParameters privKey = + new ECPrivateKeyParameters( + asUnsignedBigInteger(keyPair.getPrivateKey().getEncodedBytes()), CURVE); + signer.init(true, privKey); + + final BigInteger[] components = signer.generateSignature(dataHash.getArrayUnsafe()); + final BigInteger r = components[0]; + BigInteger s = components[1]; + + // Automatically adjust the S component to be less than or equal to half the curve + // order, if necessary. This is required because for every signature (r,s) the signature + // (r, -s (mod N)) is a valid signature of the same message. However, we dislike the + // ability to modify the bits of a Bitcoin transaction after it's been signed, as that + // violates various assumed invariants. Thus in future only one of those forms will be + // considered legal and the other will be banned. + if (s.compareTo(HALF_CURVE_ORDER) > 0) { + // The order of the curve is the number of valid points that exist on that curve. + // If S is in the upper half of the number of valid points, then bring it back to + // the lower half. Otherwise, imagine that + // N = 10 + // s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions. + // 10 - 8 == 2, giving us always the latter solution, which is canonical. + s = CURVE.getN().subtract(s); + } + + // Now we have to work backwards to figure out the recId needed to recover the signature. + int recId = -1; + final BigInteger publicKeyBI = asUnsignedBigInteger(keyPair.getPublicKey().getEncodedBytes()); + for (int i = 0; i < 4; i++) { + final BigInteger k = recoverFromSignature(i, r, s, dataHash); + if (k != null && k.equals(publicKeyBI)) { + recId = i; + break; + } + } + if (recId == -1) { + throw new RuntimeException( + "Could not construct a recoverable key. This should never happen."); + } + + return new Signature(r, s, (byte) recId); + } + + /** + * Verifies the given ECDSA signature against the message bytes using the public key bytes. + * + *

When using native ECDSA verification, data must be 32 bytes, and no element may be larger + * than 520 bytes. + * + * @param data Hash of the data to verify. + * @param signature ASN.1 encoded signature. + * @param pub The public key bytes to use. + * @return True if the verification is successful. + */ + public static boolean verify( + final BytesValue data, final Signature signature, final PublicKey pub) { + final ECDSASigner signer = new ECDSASigner(); + final BytesValue toDecode = BytesValue.wrap(BytesValue.of((byte) 4), pub.getEncodedBytes()); + final ECPublicKeyParameters params = + new ECPublicKeyParameters(CURVE.getCurve().decodePoint(toDecode.extractArray()), CURVE); + signer.init(false, params); + try { + return signer.verifySignature(data.extractArray(), signature.r, signature.s); + } catch (final NullPointerException e) { + // Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those + // signatures + // are inherently invalid/attack sigs so we just fail them here rather than crash the thread. + return false; + } + } + + /** + * Verifies the given ECDSA signature using the public key bytes against the message bytes, + * previously passed through a preprocessor function, which is normally a hashing function. + * + * @param data The data to verify. + * @param signature ASN.1 encoded signature. + * @param pub The public key bytes to use. + * @param preprocessor The function to apply to the data before verifying the signature, normally + * a hashing function. + * @return True if the verification is successful. + */ + public static boolean verify( + final BytesValue data, + final Signature signature, + final PublicKey pub, + final UnaryOperator preprocessor) { + checkArgument(preprocessor != null, "preprocessor must not be null"); + return verify(preprocessor.apply(data), signature, pub); + } + + /** + * Calculates an ECDH key agreement between the private and the public key. + * + * @param privKey The private key. + * @param theirPubKey The public key. + * @return The agreed secret. + */ + public static Bytes32 calculateKeyAgreement( + final PrivateKey privKey, final PublicKey theirPubKey) { + checkArgument(privKey != null, "missing private key"); + checkArgument(theirPubKey != null, "missing remote public key"); + + final ECPrivateKeyParameters privKeyP = new ECPrivateKeyParameters(privKey.getD(), CURVE); + final ECPublicKeyParameters pubKeyP = new ECPublicKeyParameters(theirPubKey.asEcPoint(), CURVE); + + final ECDHBasicAgreement agreement = new ECDHBasicAgreement(); + agreement.init(privKeyP); + final BigInteger agreed = agreement.calculateAgreement(pubKeyP); + + return UInt256.of(agreed).getBytes(); + } + + public static class PrivateKey implements java.security.PrivateKey { + private final Bytes32 encoded; + + private PrivateKey(final Bytes32 encoded) { + checkNotNull(encoded); + this.encoded = encoded; + } + + public static PrivateKey create(final BigInteger key) { + checkNotNull(key); + return create(toBytes32(key.toByteArray())); + } + + public static PrivateKey create(final Bytes32 key) { + return new PrivateKey(key); + } + + private static Bytes32 toBytes32(final byte[] backing) { + if (backing.length == Bytes32.SIZE) { + return Bytes32.wrap(backing); + } else if (backing.length > Bytes32.SIZE) { + return Bytes32.wrap(backing, backing.length - Bytes32.SIZE); + } else { + return Bytes32.leftPad(BytesValue.wrap(backing)); + } + } + + public static PrivateKey load(final File file) + throws IOException, InvalidSEC256K1PrivateKeyStoreException { + try { + final List info = Files.readAllLines(file.toPath()); + if (info.size() != 1) { + throw new InvalidSEC256K1PrivateKeyStoreException(); + } + return SECP256K1.PrivateKey.create(Bytes32.fromHexString((info.get(0)))); + } catch (final IllegalArgumentException ex) { + throw new InvalidSEC256K1PrivateKeyStoreException(); + } + } + + public ECPoint asEcPoint() { + return CURVE.getCurve().decodePoint(encoded.extractArray()); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof PrivateKey)) { + return false; + } + + final PrivateKey that = (PrivateKey) other; + return this.encoded.equals(that.encoded); + } + + @Override + public byte[] getEncoded() { + return encoded.getArrayUnsafe(); + } + + public Bytes32 getEncodedBytes() { + return encoded; + } + + public BigInteger getD() { + return asUnsignedBigInteger(encoded); + } + + @Override + public String getAlgorithm() { + return ALGORITHM; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public int hashCode() { + return encoded.hashCode(); + } + + public void store(final File file) throws IOException { + final File privateKeyDir = file.getParentFile(); + privateKeyDir.mkdirs(); + final Path tempPath = Files.createTempFile(privateKeyDir.toPath(), ".tmp", ""); + Files.write(tempPath, encoded.toString().getBytes(StandardCharsets.UTF_8)); + Files.move(tempPath, file.toPath(), REPLACE_EXISTING, ATOMIC_MOVE); + } + + @Override + public String toString() { + return encoded.toString(); + } + } + + public static class PublicKey implements java.security.PublicKey { + + private static final int BYTE_LENGTH = 64; + + private final BytesValue encoded; + + public static PublicKey create(final PrivateKey privateKey) { + BigInteger privKey = asUnsignedBigInteger(privateKey.getEncodedBytes()); + + /* + * TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group + * order, but that could change in future versions. + */ + if (privKey.bitLength() > CURVE.getN().bitLength()) { + privKey = privKey.mod(CURVE.getN()); + } + + final ECPoint point = new FixedPointCombMultiplier().multiply(CURVE.getG(), privKey); + return PublicKey.create(BytesValue.wrap(Arrays.copyOfRange(point.getEncoded(false), 1, 65))); + } + + private static BytesValue toBytes64(final byte[] backing) { + if (backing.length == BYTE_LENGTH) { + return BytesValue.wrap(backing); + } else if (backing.length > BYTE_LENGTH) { + return BytesValue.wrap(backing, backing.length - BYTE_LENGTH, BYTE_LENGTH); + } else { + final MutableBytesValue res = MutableBytesValue.create(BYTE_LENGTH); + BytesValue.wrap(backing).copyTo(res, BYTE_LENGTH - backing.length); + return res; + } + } + + public static PublicKey create(final BigInteger key) { + checkNotNull(key); + return create(toBytes64(key.toByteArray())); + } + + public static PublicKey create(final BytesValue encoded) { + return new PublicKey(encoded); + } + + public static Optional recoverFromSignature( + final Bytes32 dataHash, final Signature signature) { + final BigInteger publicKeyBI = + SECP256K1.recoverFromSignature( + signature.getRecId(), signature.getR(), signature.getS(), dataHash); + return publicKeyBI == null ? Optional.empty() : Optional.of(create(publicKeyBI)); + } + + private PublicKey(final BytesValue encoded) { + checkNotNull(encoded); + checkArgument( + encoded.size() == BYTE_LENGTH, + "Encoding must be %s bytes long, got %s", + BYTE_LENGTH, + encoded.size()); + this.encoded = encoded; + } + + /** + * Returns this public key as an {@link ECPoint} of Bouncy Castle, to facilitate cryptographic + * operations. + * + * @return This public key represented as an Elliptic Curve point. + */ + public ECPoint asEcPoint() { + // 0x04 is the prefix for uncompressed keys. + final BytesValue val = BytesValues.concatenate(BytesValue.of(0x04), encoded); + return CURVE.getCurve().decodePoint(val.extractArray()); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof PublicKey)) { + return false; + } + + final PublicKey that = (PublicKey) other; + return this.encoded.equals(that.encoded); + } + + @Override + public byte[] getEncoded() { + return encoded.getArrayUnsafe(); + } + + public BytesValue getEncodedBytes() { + return encoded; + } + + @Override + public String getAlgorithm() { + return ALGORITHM; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public int hashCode() { + return encoded.hashCode(); + } + + @Override + public String toString() { + return encoded.toString(); + } + } + + public static class KeyPair { + + private final PrivateKey privateKey; + private final PublicKey publicKey; + + public KeyPair(final PrivateKey privateKey, final PublicKey publicKey) { + checkNotNull(privateKey); + checkNotNull(publicKey); + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + public static KeyPair create(final PrivateKey privateKey) { + return new KeyPair(privateKey, PublicKey.create(privateKey)); + } + + public static KeyPair generate() { + final java.security.KeyPair rawKeyPair = KEY_PAIR_GENERATOR.generateKeyPair(); + final BCECPrivateKey privateKey = (BCECPrivateKey) rawKeyPair.getPrivate(); + final BCECPublicKey publicKey = (BCECPublicKey) rawKeyPair.getPublic(); + + final BigInteger privateKeyValue = privateKey.getD(); + + // Ethereum does not use encoded public keys like bitcoin - see + // https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm for details + // Additionally, as the first bit is a constant prefix (0x04) we ignore this value + final byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); + final BigInteger publicKeyValue = + new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length)); + + return new KeyPair(PrivateKey.create(privateKeyValue), PublicKey.create(publicKeyValue)); + } + + public static KeyPair load(final File file) + throws IOException, InvalidSEC256K1PrivateKeyStoreException { + return create(PrivateKey.load(file)); + } + + @Override + public int hashCode() { + return Objects.hashCode(privateKey, publicKey); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof KeyPair)) { + return false; + } + + final KeyPair that = (KeyPair) other; + return this.privateKey.equals(that.privateKey) && this.publicKey.equals(that.publicKey); + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public void store(final File file) throws IOException { + privateKey.store(file); + } + } + + public static class Signature { + public static final int BYTES_REQUIRED = 65; + /** + * The recovery id to reconstruct the public key used to create the signature. + * + *

The recId is an index from 0 to 3 which indicates which of the 4 possible keys is the + * correct one. Because the key recovery operation yields multiple potential keys, the correct + * key must either be stored alongside the signature, or you must be willing to try each recId + * in turn until you find one that outputs the key you are expecting. + */ + private final byte recId; + + private final BigInteger r; + private final BigInteger s; + + private Signature(final BigInteger r, final BigInteger s, final byte recId) { + this.r = r; + this.s = s; + this.recId = recId; + } + + /** + * Creates a new signature object given its parameters. + * + * @param r the 'r' part of the signature. + * @param s the 's' part of the signature. + * @param recId the recovery id part of the signature. + * @return the created {@link Signature} object. + * @throws NullPointerException if {@code r} or {@code s} are {@code null}. + * @throws IllegalArgumentException if any argument is invalid (for instance, {@code v} is + * neither 27 or 28). + */ + public static Signature create(final BigInteger r, final BigInteger s, final byte recId) { + checkNotNull(r); + checkNotNull(s); + checkInBounds("r", r); + checkInBounds("s", s); + if (recId != 0 && recId != 1) { + throw new IllegalArgumentException( + "Invalid 'recId' value, should be 0 or 1 but got " + recId); + } + return new Signature(r, s, recId); + } + + private static void checkInBounds(final String name, final BigInteger i) { + if (i.compareTo(BigInteger.ONE) < 0) { + throw new IllegalArgumentException( + String.format("Invalid '%s' value, should be >= 1 but got %s", name, i)); + } + + if (i.compareTo(CURVE_ORDER) >= 0) { + throw new IllegalArgumentException( + String.format("Invalid '%s' value, should be < %s but got %s", CURVE_ORDER, name, i)); + } + } + + public static Signature decode(final BytesValue bytes) { + checkArgument( + bytes.size() == BYTES_REQUIRED, "encoded SECP256K1 signature must be 65 bytes long"); + + final BigInteger r = asUnsignedBigInteger(bytes.slice(0, 32)); + final BigInteger s = asUnsignedBigInteger(bytes.slice(32, 32)); + final byte recId = bytes.get(64); + return SECP256K1.Signature.create(r, s, recId); + } + + public BytesValue encodedBytes() { + final MutableBytesValue bytes = MutableBytesValue.create(BYTES_REQUIRED); + UInt256Bytes.of(r).copyTo(bytes, 0); + UInt256Bytes.of(s).copyTo(bytes, 32); + bytes.set(64, recId); + return bytes; + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof Signature)) { + return false; + } + + final Signature that = (Signature) other; + return this.r.equals(that.r) && this.s.equals(that.s) && this.recId == that.recId; + } + + @Override + public int hashCode() { + return Objects.hashCode(r, s, recId); + } + + public byte getRecId() { + return recId; + } + + public BigInteger getR() { + return r; + } + + public BigInteger getS() { + return s; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("SECP256K1.Signature").append("{"); + sb.append("r=").append(r).append(", "); + sb.append("s=").append(s).append(", "); + sb.append("recId=").append(recId); + return sb.append("}").toString(); + } + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/SecureRandomProvider.java b/crypto/src/main/java/net/consensys/pantheon/crypto/SecureRandomProvider.java new file mode 100755 index 00000000000..21037d79944 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/SecureRandomProvider.java @@ -0,0 +1,16 @@ +package net.consensys.pantheon.crypto; + +import java.security.SecureRandom; + +public class SecureRandomProvider { + private static final SecureRandom publicSecureRandom = new PRNGSecureRandom(); + + // Returns a shared instance of secure random intended to be used where the value is used publicly + public static SecureRandom publicSecureRandom() { + return publicSecureRandom; + } + + public static SecureRandom createSecureRandom() { + return new PRNGSecureRandom(); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFieldPoint.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFieldPoint.java new file mode 100755 index 00000000000..f6e72651513 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFieldPoint.java @@ -0,0 +1,127 @@ +package net.consensys.pantheon.crypto.altbn128; + +import java.math.BigInteger; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +@SuppressWarnings("rawtypes") +public abstract class AbstractFieldPoint implements FieldPoint { + + private static final BigInteger TWO = BigInteger.valueOf(2); + + @SuppressWarnings("rawtypes") + protected final FieldElement x; + + @SuppressWarnings("rawtypes") + protected final FieldElement y; + + @SuppressWarnings("rawtypes") + AbstractFieldPoint(final FieldElement x, final FieldElement y) { + this.x = x; + this.y = y; + } + + protected abstract U infinity(); + + @SuppressWarnings("rawtypes") + protected abstract U newInstance(final FieldElement x, final FieldElement y); + + @Override + public boolean isInfinity() { + return x.isZero() && y.isZero(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public U add(final U other) { + if (isInfinity() || other.isInfinity()) { + return isInfinity() ? other : (U) this; + } else if (equals(other)) { + return doub(); + } else if (x.equals(other.x)) { + return infinity(); + } else { + final FieldElement x1 = x; + final FieldElement y1 = y; + final FieldElement x2 = other.x; + final FieldElement y2 = other.y; + + final FieldElement m = y2.subtract(y1).divide(x2.subtract(x1)); + final FieldElement mSquared = m.power(2); + final FieldElement newX = mSquared.subtract(x1).subtract(x2); + final FieldElement newY = m.negate().multiply(newX).add(m.multiply(x1)).subtract(y1); + + return newInstance(newX, newY); + } + } + + @SuppressWarnings("unchecked") + @Override + public U multiply(final U other) { + return null; + } + + @SuppressWarnings("unchecked") + @Override + public U multiply(final BigInteger n) { + if (n.compareTo(BigInteger.ZERO) == 0) { + return infinity(); + } else if (n.compareTo(BigInteger.ONE) == 0) { + return newInstance(x, y); + } else if (n.mod(TWO).compareTo(BigInteger.ZERO) == 0) { + return (U) doub().multiply(n.divide(TWO)); + } else { + return (U) doub().multiply(n.divide(TWO)).add(this); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public U doub() { + final FieldElement xSquared = x.power(2); + final FieldElement m = xSquared.multiply(3).divide(y.multiply(2)); + final FieldElement mSquared = m.power(2); + final FieldElement newX = mSquared.subtract(x.multiply(2)); + final FieldElement newY = m.negate().multiply(newX).add(m.multiply(x)).subtract(y); + return newInstance(newX, newY); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public U negate() { + if (isInfinity()) { + return (U) this; + } + + return newInstance(x, y.negate()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(getClass()).add("x", x).add("y", y).toString(); + } + + @Override + public int hashCode() { + return Objects.hashCode(x, y); + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof AbstractFieldPoint)) { + return false; + } + + final AbstractFieldPoint other = (AbstractFieldPoint) obj; + return Objects.equal(x, other.x) && Objects.equal(y, other.y); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFqp.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFqp.java new file mode 100755 index 00000000000..a5066c5cef2 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AbstractFqp.java @@ -0,0 +1,263 @@ +package net.consensys.pantheon.crypto.altbn128; + +import java.math.BigInteger; +import java.util.Arrays; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +@SuppressWarnings("rawtypes") +public abstract class AbstractFqp implements FieldElement { + private static final BigInteger BIGINT_2 = BigInteger.valueOf(2); + + protected final int degree; + protected final Fq[] modulusCoefficients; + protected final Fq[] coefficients; + + protected AbstractFqp(final int degree, final Fq[] modulusCoefficients, final Fq[] coefficients) { + if (degree != coefficients.length) { + throw new IllegalArgumentException( + String.format("point is %d degree but got %d coefficients", degree, coefficients.length)); + } + if (degree != modulusCoefficients.length) { + throw new IllegalArgumentException( + String.format( + "point is %d degree but got %d modulus coefficients", degree, coefficients.length)); + } + this.degree = degree; + this.modulusCoefficients = modulusCoefficients; + this.coefficients = coefficients; + } + + protected abstract T newInstance(final Fq[] coefficients); + + public Fq[] getCoefficients() { + return coefficients; + } + + @Override + public boolean isValid() { + for (final Fq fq : coefficients) { + if (!fq.isValid()) { + return false; + } + } + return true; + } + + @Override + public boolean isZero() { + for (final Fq fq : coefficients) { + if (!fq.isZero()) { + return false; + } + } + return true; + } + + @Override + public T add(final T other) { + final Fq[] result = new Fq[coefficients.length]; + + for (int i = 0; i < coefficients.length; ++i) { + result[i] = coefficients[i].add(other.coefficients[i]); + } + + return newInstance(result); + } + + @Override + public T subtract(final T other) { + final Fq[] result = new Fq[coefficients.length]; + + for (int i = 0; i < coefficients.length; ++i) { + result[i] = coefficients[i].subtract(other.coefficients[i]); + } + + return newInstance(result); + } + + @Override + public T multiply(final int n) { + final Fq[] result = new Fq[degree]; + for (int i = 0; i < degree; ++i) { + result[i] = coefficients[i].multiply(n); + } + return newInstance(result); + } + + @Override + public T multiply(final T other) { + final Fq[] b = new Fq[degree * 2 - 1]; + Arrays.fill(b, Fq.zero()); + for (int i = 0; i < degree; ++i) { + for (int j = 0; j < degree; ++j) { + b[i + j] = b[i + j].add(coefficients[i].multiply(other.coefficients[j])); + } + } + + for (int i = b.length; i > degree; --i) { + final Fq top = b[i - 1]; + final int exp = i - degree - 1; + for (int j = 0; j < degree; ++j) { + b[exp + j] = b[exp + j].subtract(top.multiply(modulusCoefficients[j])); + } + } + + return newInstance(Arrays.copyOfRange(b, 0, degree)); + } + + @Override + public T divide(final T other) { + final T inverse = newInstance(other.inverse()); + return multiply(inverse); + } + + @Override + public T negate() { + final Fq[] negated = Arrays.stream(coefficients).map(Fq::negate).toArray(Fq[]::new); + return newInstance(negated); + } + + private T one() { + final Fq[] result = new Fq[degree]; + Arrays.fill(result, Fq.zero()); + result[0] = Fq.one(); + return newInstance(result); + } + + @SuppressWarnings("unchecked") + @Override + public T power(final int n) { + if (n == 0) { + return one(); + } else if (n == 1) { + return newInstance(coefficients); + } else if (n % 2 == 0) { + return (T) multiply((T) this).power(n / 2); + } else { + return (T) multiply((T) this).power(n / 2).multiply(this); + } + } + + @SuppressWarnings("unchecked") + @Override + public T power(final BigInteger n) { + if (n.compareTo(BigInteger.ZERO) == 0) { + return one(); + } + if (n.compareTo(BigInteger.ONE) == 0) { + return (T) this; + } else if (n.mod(BIGINT_2).compareTo(BigInteger.ZERO) == 0) { + return (T) multiply((T) this).power(n.divide(BIGINT_2)); + } else { + return (T) multiply((T) this).power(n.divide(BIGINT_2)).multiply(this); + } + } + + protected Fq[] inverse() { + Fq[] lm = lm(); + Fq[] hm = hm(); + Fq[] low = low(); + Fq[] high = high(); + while (deg(low) > 0) { + final Fq[] r = polyRoundedDiv(high, low); + final Fq[] nm = Arrays.copyOf(hm, hm.length); + final Fq[] neww = Arrays.copyOf(high, high.length); + for (int i = 0; i < degree + 1; ++i) { + for (int j = 0; j < degree + 1 - i; ++j) { + nm[i + j] = nm[i + j].subtract(lm[i].multiply(r[j])); + neww[i + j] = neww[i + j].subtract(low[i].multiply(r[j])); + } + } + + high = low; + hm = lm; + low = neww; + lm = nm; + } + + for (int i = 0; i < lm.length; ++i) { + lm[i] = lm[i].divide(low[0]); + } + + return Arrays.copyOfRange(lm, 0, degree); + } + + private static Fq[] polyRoundedDiv(final Fq[] a, final Fq[] b) { + final int degA = deg(a); + final int degB = deg(b); + final Fq[] temp = Arrays.copyOf(a, a.length); + final Fq[] o = new Fq[a.length]; + Arrays.fill(o, Fq.zero()); + + for (int i = degA - degB; i >= 0; --i) { + o[i] = o[i].add(temp[degB + i].divide(b[degB])); + for (int j = 0; j <= degB; ++j) { + temp[i + j] = temp[i + j].subtract(o[j]); + } + } + return o; + } + + private static int deg(final Fq[] p) { + int d = p.length - 1; + while (p[d].equals(Fq.zero()) && d >= 0) { + --d; + } + return d; + } + + private Fq[] lm() { + final Fq[] lm = new Fq[degree + 1]; + Arrays.fill(lm, Fq.zero()); + lm[0] = Fq.one(); + return lm; + } + + private Fq[] hm() { + final Fq[] hm = new Fq[degree + 1]; + Arrays.fill(hm, Fq.zero()); + return hm; + } + + private Fq[] low() { + final Fq[] low = Arrays.copyOfRange(coefficients, 0, coefficients.length + 1); + low[low.length - 1] = Fq.zero(); + return low; + } + + private Fq[] high() { + final Fq[] high = Arrays.copyOfRange(modulusCoefficients, 0, modulusCoefficients.length + 1); + high[high.length - 1] = Fq.one(); + return high; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof AbstractFqp)) { + return false; + } + + final AbstractFqp other = (AbstractFqp) obj; + return Arrays.equals(coefficients, other.coefficients); + } + + @Override + public int hashCode() { + return Objects.hashCode( + degree, Arrays.hashCode(modulusCoefficients), Arrays.hashCode(coefficients)); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(getClass()).add("coefficients", coefficients).toString(); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Pairer.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Pairer.java new file mode 100755 index 00000000000..0c5a4a22c0b --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Pairer.java @@ -0,0 +1,92 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static net.consensys.pantheon.crypto.altbn128.AltBn128Fq12Point.twist; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128Fq12Pairer { + + private static final int LOG_ATE_LOOP_COUNT = 63; + + private static final BigInteger ATE_LOOP_COUNT = new BigInteger("29793968203157093288"); + + private static final BigInteger CURVE_ORDER = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495617"); + + public static Fq12 pair(final AltBn128Point p, final AltBn128Fq2Point q) { + return millerLoop(cast(p), twist(q)); + } + + private static AltBn128Fq12Point cast(final AltBn128Point p) { + final Fq[] newX = new Fq[Fq12.DEGREE]; + Arrays.fill(newX, Fq.zero()); + newX[0] = p.getX(); + final Fq[] newY = new Fq[Fq12.DEGREE]; + Arrays.fill(newY, Fq.zero()); + newY[0] = p.getY(); + + return new AltBn128Fq12Point(new Fq12(newX), new Fq12(newY)); + } + + private static Fq12 millerLoop(final AltBn128Fq12Point p, final AltBn128Fq12Point q) { + if (p.isInfinity() || q.isInfinity()) { + return Fq12.one(); + } + + AltBn128Fq12Point r = q; + Fq12 f = Fq12.one(); + for (int i = LOG_ATE_LOOP_COUNT; i >= 0; --i) { + f = f.multiply(f).multiply(lineFunc(r, r, p)); + r = r.doub(); + if (ATE_LOOP_COUNT.testBit(i)) { + f = f.multiply(lineFunc(r, q, p)); + r = r.add(q); + } + } + + final AltBn128Fq12Point q1 = + new AltBn128Fq12Point( + q.getX().power(FieldElement.FIELD_MODULUS), q.getY().power(FieldElement.FIELD_MODULUS)); + final AltBn128Fq12Point nQ2 = + new AltBn128Fq12Point( + q1.getX().power(FieldElement.FIELD_MODULUS), + q1.getY().negate().power(FieldElement.FIELD_MODULUS)); + f = f.multiply(lineFunc(r, q1, p)); + r = r.add(q1); + f = f.multiply(lineFunc(r, nQ2, p)); + + return f; + } + + public static Fq12 finalize(final Fq12 f) { + return f.power(FieldElement.FIELD_MODULUS.pow(12).subtract(BigInteger.ONE).divide(CURVE_ORDER)); + } + + private static Fq12 lineFunc( + final AltBn128Fq12Point p1, final AltBn128Fq12Point p2, final AltBn128Fq12Point t) { + final Fq12 x1 = p1.getX(); + final Fq12 y1 = p1.getY(); + final Fq12 x2 = p2.getX(); + final Fq12 y2 = p2.getY(); + final Fq12 xT = t.getX(); + final Fq12 yT = t.getY(); + + if (!x1.equals(x2)) { + final Fq12 m = y2.subtract(y1).divide(x2.subtract(x1)); + final Fq12 result = m.multiply(xT.subtract(x1)).subtract(yT.subtract(y1)); + return result; + } else if (y1.equals(y2)) { + final Fq12 m = x1.power(2).multiply(3).divide(y1.multiply(2)); + final Fq12 result = m.multiply(xT.subtract(x1)).subtract(yT.subtract(y1)); + return result; + } else { + return xT.subtract(x1); + } + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Point.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Point.java new file mode 100755 index 00000000000..6a5c5def14b --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12Point.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.crypto.altbn128; + +import java.util.Arrays; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128Fq12Point extends AbstractFieldPoint { + + public static AltBn128Fq12Point g12() { + return twist(AltBn128Fq2Point.g2()); + } + + public static AltBn128Fq12Point twist(final AltBn128Fq2Point p) { + final Fq2 x = p.getX(); + final Fq2 y = p.getY(); + + final Fq[] xCoeffs = x.getCoefficients(); + final Fq[] yCoeffs = y.getCoefficients(); + + final Fq[] nX = new Fq[Fq12.DEGREE]; + Arrays.fill(nX, Fq.zero()); + nX[0] = xCoeffs[0].subtract(xCoeffs[1].multiply(9)); + nX[6] = xCoeffs[1]; + final Fq[] nY = new Fq[Fq12.DEGREE]; + Arrays.fill(nY, Fq.zero()); + nY[0] = yCoeffs[0].subtract(yCoeffs[1].multiply(9)); + nY[6] = yCoeffs[1]; + + final Fq12 newX = new Fq12(nX); + final Fq12 newY = new Fq12(nY); + final Fq12 w = w(); + return new AltBn128Fq12Point(newX.multiply(w.power(2)), newY.multiply(w.power(3))); + } + + private static Fq12 w() { + return Fq12.create(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + public AltBn128Fq12Point(final Fq12 x, final Fq12 y) { + super(x, y); + } + + public Fq12 getX() { + return (Fq12) x; + } + + public Fq12 getY() { + return (Fq12) y; + } + + @Override + protected AltBn128Fq12Point infinity() { + return new AltBn128Fq12Point(Fq12.zero(), Fq12.zero()); + } + + @SuppressWarnings("rawtypes") + @Override + protected AltBn128Fq12Point newInstance(final FieldElement x, final FieldElement y) { + return new AltBn128Fq12Point((Fq12) x, (Fq12) y); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2Point.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2Point.java new file mode 100755 index 00000000000..ecceb08d003 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2Point.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.crypto.altbn128; + +import java.math.BigInteger; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128Fq2Point extends AbstractFieldPoint { + + public static AltBn128Fq2Point g2() { + final Fq2 x = + Fq2.create( + new BigInteger( + "10857046999023057135944570762232829481370756359578518086990519993285655852781"), + new BigInteger( + "11559732032986387107991004021392285783925812861821192530917403151452391805634")); + final Fq2 y = + Fq2.create( + new BigInteger( + "8495653923123431417604973247489272438418190587263600148770280649306958101930"), + new BigInteger( + "4082367875863433681332203403145435568316851327593401208105741076214120093531")); + return new AltBn128Fq2Point(x, y); + } + + public AltBn128Fq2Point(final Fq2 x, final Fq2 y) { + super(x, y); + } + + public Fq2 getX() { + return (Fq2) x; + } + + public Fq2 getY() { + return (Fq2) y; + } + + @Override + protected AltBn128Fq2Point infinity() { + return new AltBn128Fq2Point(Fq2.zero(), Fq2.zero()); + } + + public boolean isOnCurve() { + if (!x.isValid() || !y.isValid()) { + return false; + } + + if (isInfinity()) { + return true; + } + + final Fq2 x = getX(); + final Fq2 y = getY(); + + return y.power(2).subtract(x.power(3)).equals(Fq2.b2()); + } + + @SuppressWarnings("rawtypes") + @Override + protected AltBn128Fq2Point newInstance(final FieldElement x, final FieldElement y) { + return new AltBn128Fq2Point((Fq2) x, (Fq2) y); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Point.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Point.java new file mode 100755 index 00000000000..63922091063 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/AltBn128Point.java @@ -0,0 +1,50 @@ +package net.consensys.pantheon.crypto.altbn128; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128Point extends AbstractFieldPoint { + + static final Fq B = Fq.create(3); + + public static final AltBn128Point g1() { + return new AltBn128Point(Fq.create(1), Fq.create(2)); + } + + static final AltBn128Point INFINITY = new AltBn128Point(Fq.zero(), Fq.zero()); + + public AltBn128Point(final Fq x, final Fq y) { + super(x, y); + } + + public Fq getX() { + return (Fq) x; + } + + public Fq getY() { + return (Fq) y; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public boolean isOnCurve() { + if (!x.isValid() || !y.isValid()) { + return false; + } + if (isInfinity()) { + return true; + } + return y.power(2).subtract(x.power(3)).equals(B); + } + + @Override + protected AltBn128Point infinity() { + return new AltBn128Point(Fq.zero(), Fq.zero()); + } + + @SuppressWarnings("rawtypes") + @Override + protected AltBn128Point newInstance(final FieldElement x, final FieldElement y) { + return new AltBn128Point((Fq) x, (Fq) y); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldElement.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldElement.java new file mode 100755 index 00000000000..b9e45c7bc94 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldElement.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.crypto.altbn128; + +import java.math.BigInteger; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +@SuppressWarnings("rawtypes") +public interface FieldElement { + + BigInteger FIELD_MODULUS = + new BigInteger( + "21888242871839275222246405745257275088696311157297823662689037894645226208583"); + + boolean isValid(); + + boolean isZero(); + + T add(T other); + + T subtract(T other); + + T multiply(int val); + + T multiply(T other); + + T negate(); + + T divide(T other); + + T power(int n); + + T power(BigInteger n); +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldPoint.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldPoint.java new file mode 100755 index 00000000000..b28676a5811 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/FieldPoint.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.crypto.altbn128; + +import java.math.BigInteger; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +@SuppressWarnings("rawtypes") +public interface FieldPoint { + + boolean isInfinity(); + + T add(T other); + + T multiply(T other); + + T multiply(BigInteger n); + + T doub(); + + T negate(); +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq.java new file mode 100755 index 00000000000..fa86cf3f147 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq.java @@ -0,0 +1,159 @@ +package net.consensys.pantheon.crypto.altbn128; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.math.BigInteger; +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class Fq implements FieldElement { + + private static final BigInteger TWO = BigInteger.valueOf(2); + + public static Fq zero() { + return create(0); + } + + public static Fq one() { + return create(1); + } + + private final BigInteger n; + + public static Fq create(final BigInteger n) { + return new Fq(n); + } + + static Fq create(final long n) { + return create(BigInteger.valueOf(n)); + } + + private Fq(final BigInteger n) { + this.n = n; + } + + public BytesValue toBytesValue() { + return BytesValues.trimLeadingZeros(BytesValue.wrap(n.toByteArray())); + } + + @Override + public boolean isZero() { + return n.compareTo(BigInteger.ZERO) == 0; + } + + @Override + public boolean isValid() { + return n.compareTo(FIELD_MODULUS) < 0; + } + + @Override + public Fq add(final Fq other) { + final BigInteger result = n.add(other.n).mod(FIELD_MODULUS); + return new Fq(result); + } + + @Override + public Fq subtract(final Fq other) { + final BigInteger result = n.subtract(other.n).mod(FIELD_MODULUS); + return new Fq(result); + } + + @Override + public Fq multiply(final int val) { + return multiply(new Fq(BigInteger.valueOf(val))); + } + + @Override + public Fq multiply(final Fq other) { + final BigInteger result = n.multiply(other.n).mod(FIELD_MODULUS); + return new Fq(result); + } + + @Override + public Fq divide(final Fq other) { + final BigInteger inverse = inverse(other.n, FIELD_MODULUS); + final BigInteger result = n.multiply(inverse).mod(FIELD_MODULUS); + return new Fq(result); + } + + private BigInteger inverse(final BigInteger a, final BigInteger n) { + if (a.compareTo(BigInteger.ZERO) == 0) { + return BigInteger.ZERO; + } + BigInteger lm = BigInteger.ONE; + BigInteger hm = BigInteger.ZERO; + BigInteger low = a.mod(n); + BigInteger high = n; + while (low.compareTo(BigInteger.ONE) > 0) { + final BigInteger r = high.divide(low); + final BigInteger nm = hm.subtract(lm.multiply(r)); + final BigInteger neww = high.subtract(low.multiply(r)); + high = low; + hm = lm; + low = neww; + lm = nm; + } + return lm.mod(n); + } + + @Override + public Fq negate() { + return new Fq(n.negate()); + } + + @Override + public Fq power(final int n) { + if (n == 0) { + return one(); + } else if (n == 1) { + return this; + } else if (n % 2 == 0) { + return multiply(this).power(n / 2); + } else { + return multiply(this).power(n / 2).multiply(this); + } + } + + @Override + public Fq power(final BigInteger n) { + if (n.compareTo(BigInteger.ZERO) == 0) { + return one(); + } + if (n.compareTo(BigInteger.ONE) == 0) { + return this; + } else if (n.mod(TWO).compareTo(BigInteger.ZERO) == 0) { + return multiply(this).power(n.divide(TWO)); + } else { + return multiply(this).power(n.divide(TWO)).multiply(this); + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(Fq.class).add("n", n).toString(); + } + + @Override + public int hashCode() { + return Objects.hashCode(n); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Fq)) { + return false; + } + + final Fq other = (Fq) obj; + return n.compareTo(other.n) == 0; + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq12.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq12.java new file mode 100755 index 00000000000..5b2c043e3f7 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq12.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.crypto.altbn128; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class Fq12 extends AbstractFqp { + + public static final int DEGREE = 12; + + static final Fq12 zero() { + return create(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + public static final Fq12 one() { + return create(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + private static final Fq[] MODULUS_COEFFICIENTS = + new Fq[] { + Fq.create(82), + Fq.create(0), + Fq.create(0), + Fq.create(0), + Fq.create(0), + Fq.create(0), + Fq.create(-18), + Fq.create(0), + Fq.create(0), + Fq.create(0), + Fq.create(0), + Fq.create(0) + }; + + public static Fq12 create( + final long c0, + final long c1, + final long c2, + final long c3, + final long c4, + final long c5, + final long c6, + final long c7, + final long c8, + final long c9, + final long c10, + final long c11) { + return new Fq12( + Fq.create(c0), + Fq.create(c1), + Fq.create(c2), + Fq.create(c3), + Fq.create(c4), + Fq.create(c5), + Fq.create(c6), + Fq.create(c7), + Fq.create(c8), + Fq.create(c9), + Fq.create(c10), + Fq.create(c11)); + } + + protected Fq12(final Fq... coefficients) { + super(DEGREE, MODULUS_COEFFICIENTS, coefficients); + } + + @Override + protected Fq12 newInstance(final Fq[] coefficients) { + return new Fq12(coefficients); + } +} diff --git a/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq2.java b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq2.java new file mode 100755 index 00000000000..0083305a236 --- /dev/null +++ b/crypto/src/main/java/net/consensys/pantheon/crypto/altbn128/Fq2.java @@ -0,0 +1,45 @@ +package net.consensys.pantheon.crypto.altbn128; + +import java.math.BigInteger; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class Fq2 extends AbstractFqp { + + private static final int DEGREE = 2; + + static final Fq2 zero() { + return new Fq2(new Fq[] {Fq.zero(), Fq.zero()}); + } + + static final Fq2 one() { + return new Fq2(new Fq[] {Fq.one(), Fq.zero()}); + } + + private static final Fq[] MODULUS_COEFFICIENTS = new Fq[] {Fq.create(1), Fq.create(0)}; + + public static final Fq2 create(final long c0, final long c1) { + return create(BigInteger.valueOf(c0), BigInteger.valueOf((c1))); + } + + public static final Fq2 create(final BigInteger c0, final BigInteger c1) { + return new Fq2(Fq.create(c0), Fq.create(c1)); + } + + private Fq2(final Fq... coefficients) { + super(DEGREE, MODULUS_COEFFICIENTS, coefficients); + } + + public static Fq2 b2() { + final Fq2 numerator = create(3, 0); + final Fq2 denominator = create(9, 1); + return numerator.divide(denominator); + } + + @Override + protected Fq2 newInstance(final Fq[] coefficients) { + return new Fq2(coefficients); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/HashTest.java b/crypto/src/test/java/net/consensys/pantheon/crypto/HashTest.java new file mode 100755 index 00000000000..0b559e92483 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/HashTest.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class HashTest { + + private static final String cowKeccak256 = + "c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"; + private static final String horseKeccak256 = + "c87f65ff3f271bf5dc8643484f66b200109caffe4bf98c4cb393dc35740b28c0"; + + /** Validate keccak256 hash. */ + @Test + public void keccak256Hash() { + final BytesValue resultHorse = Hash.keccak256(BytesValue.wrap("horse".getBytes(UTF_8))); + assertEquals(BytesValue.fromHexString(horseKeccak256), resultHorse); + + final BytesValue resultCow = Hash.keccak256(BytesValue.wrap("cow".getBytes(UTF_8))); + assertEquals(BytesValue.fromHexString(cowKeccak256), resultCow); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/PRNGSecureRandomTest.java b/crypto/src/test/java/net/consensys/pantheon/crypto/PRNGSecureRandomTest.java new file mode 100755 index 00000000000..1d1dbb357c0 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/PRNGSecureRandomTest.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.crypto; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.prng.SP800SecureRandom; +import org.bouncycastle.crypto.prng.SP800SecureRandomBuilder; +import org.junit.Test; + +public class PRNGSecureRandomTest { + + @Test + public void createsSecureRandomInitialisedToUsePRNG() { + final QuickEntropy quickEntropy = mock(QuickEntropy.class); + final SP800SecureRandomBuilder sp800Builder = mock(SP800SecureRandomBuilder.class); + + when(sp800Builder.setSecurityStrength(anyInt())).thenReturn(sp800Builder); + when(sp800Builder.setPersonalizationString(any())).thenReturn(sp800Builder); + + new PRNGSecureRandom(quickEntropy, sp800Builder); + verify(sp800Builder).buildHash(any(SHA256Digest.class), eq(null), eq(false)); + verify(sp800Builder).setSecurityStrength(256); + verify(sp800Builder).setPersonalizationString(any()); + } + + @Test + public void reseedsUsingQuickEntropyOnEachNextByteCall() { + final QuickEntropy quickEntropy = mock(QuickEntropy.class); + final SP800SecureRandomBuilder sp800Builder = mock(SP800SecureRandomBuilder.class); + final SP800SecureRandom sp800SecureRandom = mock(SP800SecureRandom.class); + + final byte[] entropy = {1, 2, 3, 4}; + when(quickEntropy.getQuickEntropy()).thenReturn(entropy); + when(sp800Builder.setSecurityStrength(anyInt())).thenReturn(sp800Builder); + when(sp800Builder.setPersonalizationString(any())).thenReturn(sp800Builder); + when(sp800Builder.buildHash(any(), any(), anyBoolean())).thenReturn(sp800SecureRandom); + + final PRNGSecureRandom prngSecureRandom = new PRNGSecureRandom(quickEntropy, sp800Builder); + final byte[] bytes = new byte[] {}; + prngSecureRandom.nextBytes(bytes); + verify(quickEntropy, times(1)).getQuickEntropy(); + verify(sp800SecureRandom).setSeed(entropy); + verify(sp800SecureRandom).nextBytes(bytes); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/SECP256K1Test.java b/crypto/src/test/java/net/consensys/pantheon/crypto/SECP256K1Test.java new file mode 100755 index 00000000000..2c223bade17 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/SECP256K1Test.java @@ -0,0 +1,273 @@ +package net.consensys.pantheon.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static net.consensys.pantheon.crypto.Hash.keccak256; +import static net.consensys.pantheon.util.bytes.BytesValue.fromHexString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.File; +import java.math.BigInteger; +import java.net.URL; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.google.common.io.Resources; +import org.junit.BeforeClass; +import org.junit.Test; + +public class SECP256K1Test { + + protected static String suiteStartTime = null; + protected static String suiteName = null; + + @BeforeClass + public static void setTestSuiteStartTime() { + final SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd-HHmmss"); + suiteStartTime = fmt.format(new Date()); + suiteName(SECP256K1Test.class); + } + + public static void suiteName(final Class clazz) { + suiteName = clazz.getSimpleName() + "-" + suiteStartTime; + } + + public static String suiteName() { + return suiteName; + } + + @Test(expected = NullPointerException.class) + public void createPrivateKey_NullEncoding() { + SECP256K1.PrivateKey.create((Bytes32) null); + } + + @Test + public void privateKeyEquals() { + final SECP256K1.PrivateKey privateKey1 = SECP256K1.PrivateKey.create(BigInteger.TEN); + final SECP256K1.PrivateKey privateKey2 = SECP256K1.PrivateKey.create(BigInteger.TEN); + + assertEquals(privateKey1, privateKey2); + } + + @Test + public void privateHashCode() { + final SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.create(BigInteger.TEN); + + assertNotEquals(0, privateKey.hashCode()); + } + + @Test(expected = NullPointerException.class) + public void createPublicKey_NullEncoding() { + SECP256K1.PublicKey.create((BytesValue) null); + } + + @Test(expected = IllegalArgumentException.class) + public void createPublicKey_EncodingTooShort() { + SECP256K1.PublicKey.create(BytesValue.wrap(new byte[63])); + } + + @Test(expected = IllegalArgumentException.class) + public void createPublicKey_EncodingTooLong() { + SECP256K1.PublicKey.create(BytesValue.wrap(new byte[65])); + } + + @Test + public void publicKeyEquals() { + final SECP256K1.PublicKey publicKey1 = + SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + final SECP256K1.PublicKey publicKey2 = + SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + assertEquals(publicKey1, publicKey2); + } + + @Test + public void publicHashCode() { + final SECP256K1.PublicKey publicKey = + SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + assertNotEquals(0, publicKey.hashCode()); + } + + @Test(expected = NullPointerException.class) + public void createKeyPair_PublicKeyNull() { + new SECP256K1.KeyPair(null, SECP256K1.PublicKey.create(BytesValue.wrap(new byte[64]))); + } + + @Test(expected = NullPointerException.class) + public void createKeyPair_PrivateKeyNull() { + new SECP256K1.KeyPair(SECP256K1.PrivateKey.create(Bytes32.wrap(new byte[32])), null); + } + + @Test + public void keyPairGeneration() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + assertNotNull(keyPair); + assertNotNull(keyPair.getPrivateKey()); + assertNotNull(keyPair.getPublicKey()); + } + + @Test + public void keyPairEquals() { + final SECP256K1.PrivateKey privateKey1 = SECP256K1.PrivateKey.create(BigInteger.TEN); + final SECP256K1.PrivateKey privateKey2 = SECP256K1.PrivateKey.create(BigInteger.TEN); + final SECP256K1.PublicKey publicKey1 = + SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + final SECP256K1.PublicKey publicKey2 = + SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + final SECP256K1.KeyPair keyPair1 = new SECP256K1.KeyPair(privateKey1, publicKey1); + final SECP256K1.KeyPair keyPair2 = new SECP256K1.KeyPair(privateKey2, publicKey2); + + assertEquals(keyPair1, keyPair2); + } + + @Test + public void keyPairHashCode() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + assertNotEquals(0, keyPair.hashCode()); + } + + @Test + public void keyPairGeneration_PublicKeyRecovery() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + assertEquals(keyPair.getPublicKey(), SECP256K1.PublicKey.create(keyPair.getPrivateKey())); + } + + @Test + public void publicKeyRecovery() { + final SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.create(BigInteger.TEN); + final SECP256K1.PublicKey expectedPublicKey = + SECP256K1.PublicKey.create( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + final SECP256K1.PublicKey publicKey = SECP256K1.PublicKey.create(privateKey); + assertEquals(expectedPublicKey, publicKey); + } + + @Test + public void createSignature() { + final SECP256K1.Signature signature = + SECP256K1.Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0); + assertEquals(BigInteger.ONE, signature.getR()); + assertEquals(BigInteger.TEN, signature.getS()); + assertEquals((byte) 0, signature.getRecId()); + } + + @Test(expected = NullPointerException.class) + public void createSignature_NoR() { + SECP256K1.Signature.create(null, BigInteger.ZERO, (byte) 27); + } + + @Test(expected = NullPointerException.class) + public void createSignature_NoS() { + SECP256K1.Signature.create(BigInteger.ZERO, null, (byte) 27); + } + + @Test + public void recoverPublicKeyFromSignature() { + final SECP256K1.PrivateKey privateKey = + SECP256K1.PrivateKey.create( + new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(privateKey); + + final BytesValue data = + BytesValue.wrap("This is an example of a signed message.".getBytes(UTF_8)); + final Bytes32 dataHash = keccak256(data); + final SECP256K1.Signature signature = SECP256K1.sign(dataHash, keyPair); + + final SECP256K1.PublicKey recoveredPublicKey = + SECP256K1.PublicKey.recoverFromSignature(dataHash, signature).get(); + assertEquals(keyPair.getPublicKey().toString(), recoveredPublicKey.toString()); + } + + @Test + public void signatureGeneration() { + final SECP256K1.PrivateKey privateKey = + SECP256K1.PrivateKey.create( + new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(privateKey); + + final BytesValue data = + BytesValue.wrap("This is an example of a signed message.".getBytes(UTF_8)); + final Bytes32 dataHash = keccak256(data); + final SECP256K1.Signature expectedSignature = + SECP256K1.Signature.create( + new BigInteger("d2ce488f4da29e68f22cb05cac1b19b75df170a12b4ad1bdd4531b8e9115c6fb", 16), + new BigInteger("75c1fe50a95e8ccffcbb5482a1e42fbbdd6324131dfe75c3b3b7f9a7c721eccb", 16), + (byte) 1); + + final SECP256K1.Signature actualSignature = SECP256K1.sign(dataHash, keyPair); + assertEquals(expectedSignature, actualSignature); + } + + @Test + public void signatureVerification() { + final SECP256K1.PrivateKey privateKey = + SECP256K1.PrivateKey.create( + new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(privateKey); + + final BytesValue data = + BytesValue.wrap("This is an example of a signed message.".getBytes(UTF_8)); + final Bytes32 dataHash = keccak256(data); + + final SECP256K1.Signature signature = SECP256K1.sign(dataHash, keyPair); + assertTrue(SECP256K1.verify(data, signature, keyPair.getPublicKey(), Hash::keccak256)); + } + + @Test + public void fileContainsValidPrivateKey() throws Exception { + final URL url = Resources.getResource("net/consensys/pantheon/crypto/validPrivateKey.txt"); + final File file = new File(url.getFile()); + final SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.load(file); + assertEquals( + BytesValue.fromHexString( + "000000000000000000000000000000000000000000000000000000000000000A"), + privateKey.getEncodedBytes()); + } + + @Test + public void readWritePrivateKeyString() throws Exception { + final SECP256K1.PrivateKey privateKey = SECP256K1.PrivateKey.create(BigInteger.TEN); + final SECP256K1.KeyPair keyPair1 = SECP256K1.KeyPair.create(privateKey); + final File tempFile = Files.createTempFile(suiteName(), ".keypair").toFile(); + tempFile.deleteOnExit(); + keyPair1.store(tempFile); + final SECP256K1.KeyPair keyPair2 = SECP256K1.KeyPair.load(tempFile); + assertEquals(keyPair1, keyPair2); + } + + @Test(expected = InvalidSEC256K1PrivateKeyStoreException.class) + public void invalidFileThrowsInvalidKeyPairException() throws Exception { + final File tempFile = Files.createTempFile(suiteName(), ".keypair").toFile(); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), "not valid".getBytes(UTF_8)); + SECP256K1.PrivateKey.load(tempFile); + } + + @Test(expected = InvalidSEC256K1PrivateKeyStoreException.class) + public void invalidMultiLineFileThrowsInvalidIdException() throws Exception { + final File tempFile = Files.createTempFile(suiteName(), ".keypair").toFile(); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), "not\n\nvalid".getBytes(UTF_8)); + SECP256K1.PrivateKey.load(tempFile); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PairerTest.java b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PairerTest.java new file mode 100755 index 00000000000..f3461c11d28 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PairerTest.java @@ -0,0 +1,81 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.junit.Test; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128Fq12PairerTest { + + @Test + public void shouldEqualOneWhenNegatedPairsAreMultiplied() { + final Fq12 p1Paired = AltBn128Fq12Pairer.pair(AltBn128Point.g1(), AltBn128Fq2Point.g2()); + final Fq12 p1Finalzied = AltBn128Fq12Pairer.finalize(p1Paired); + + final Fq12 pn1Paired = + AltBn128Fq12Pairer.pair(AltBn128Point.g1().negate(), AltBn128Fq2Point.g2()); + final Fq12 pn1Finalzied = AltBn128Fq12Pairer.finalize(pn1Paired); + + assertThat(p1Finalzied.multiply(pn1Finalzied)).isEqualTo(Fq12.one()); + } + + @Test + public void shouldEqualOneWhenNegatedPairsAreMultipliedBothWays() { + final Fq12 p1Paired = AltBn128Fq12Pairer.pair(AltBn128Point.g1(), AltBn128Fq2Point.g2()); + final Fq12 p1Finalized = AltBn128Fq12Pairer.finalize(p1Paired); + final Fq12 pn1Paired = + AltBn128Fq12Pairer.pair(AltBn128Point.g1().negate(), AltBn128Fq2Point.g2()); + final Fq12 pn1Finalized = AltBn128Fq12Pairer.finalize(pn1Paired); + final Fq12 np1Paired = + AltBn128Fq12Pairer.pair(AltBn128Point.g1(), AltBn128Fq2Point.g2().negate()); + final Fq12 np1Finalized = AltBn128Fq12Pairer.finalize(np1Paired); + + assertThat(p1Finalized.multiply(np1Finalized)).isEqualTo(Fq12.one()); + assertThat(pn1Finalized).isEqualTo(np1Finalized); + } + + @Test + public void shouldEqualOneWhenRaisedToCurveOrder() { + final Fq12 p1Paired = AltBn128Fq12Pairer.pair(AltBn128Point.g1(), AltBn128Fq2Point.g2()); + final Fq12 p1Finalized = AltBn128Fq12Pairer.finalize(p1Paired); + + final BigInteger curveOrder = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495617"); + assertThat(p1Finalized.power(curveOrder)).isEqualTo(Fq12.one()); + } + + @Test + public void shouldBeBilinear() { + final Fq12 p1Paired = AltBn128Fq12Pairer.pair(AltBn128Point.g1(), AltBn128Fq2Point.g2()); + final Fq12 p1Finalized = AltBn128Fq12Pairer.finalize(p1Paired); + final Fq12 p2Paired = + AltBn128Fq12Pairer.pair( + AltBn128Point.g1().multiply(BigInteger.valueOf(2)), AltBn128Fq2Point.g2()); + final Fq12 p2Finalized = AltBn128Fq12Pairer.finalize(p2Paired); + + assertThat(p1Finalized.multiply(p1Finalized)).isEqualTo(p2Finalized); + } + + @Test + public void shouldBeNongenerate() { + final Fq12 p1Paired = AltBn128Fq12Pairer.pair(AltBn128Point.g1(), AltBn128Fq2Point.g2()); + final Fq12 p1Finalized = AltBn128Fq12Pairer.finalize(p1Paired); + final Fq12 p2Paired = + AltBn128Fq12Pairer.pair( + AltBn128Point.g1().multiply(BigInteger.valueOf(2)), AltBn128Fq2Point.g2()); + final Fq12 p2Finalized = AltBn128Fq12Pairer.finalize(p2Paired); + final Fq12 np1Paired = + AltBn128Fq12Pairer.pair(AltBn128Point.g1(), AltBn128Fq2Point.g2().negate()); + final Fq12 np1Finalized = AltBn128Fq12Pairer.finalize(np1Paired); + + assertThat(p1Finalized).isNotEqualTo(p2Finalized); + assertThat(p1Finalized).isNotEqualTo(np1Finalized); + assertThat(p2Finalized).isNotEqualTo(np1Finalized); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PointTest.java b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PointTest.java new file mode 100755 index 00000000000..8bbeebca792 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq12PointTest.java @@ -0,0 +1,50 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.junit.Test; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128Fq12PointTest { + + @Test + public void shouldProduceTheSameResultUsingAddsAndDoublings() { + assertThat( + AltBn128Fq12Point.g12() + .doub() + .add(AltBn128Fq12Point.g12()) + .add(AltBn128Fq12Point.g12())) + .isEqualTo(AltBn128Fq12Point.g12().doub().doub()); + } + + @Test + public void shouldNotEqualEachOtherWhenDiferentPoints() { + assertThat(AltBn128Fq12Point.g12().doub()).isNotEqualTo(AltBn128Fq12Point.g12()); + } + + @Test + public void shouldEqualEachOtherWhenImpartialFractionsAreTheSame() { + assertThat( + AltBn128Fq12Point.g12() + .multiply(BigInteger.valueOf(9)) + .add(AltBn128Fq12Point.g12().multiply(BigInteger.valueOf(5)))) + .isEqualTo( + AltBn128Fq12Point.g12() + .multiply(BigInteger.valueOf(12)) + .add(AltBn128Fq12Point.g12().multiply(BigInteger.valueOf(2)))); + } + + @Test + public void shouldBeInfinityWhenMultipliedByCurveOrder() { + final BigInteger curveOrder = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495617"); + + assertThat(AltBn128Fq12Point.g12().multiply(curveOrder).isInfinity()).isTrue(); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2PointTest.java b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2PointTest.java new file mode 100755 index 00000000000..ec96dd952e2 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128Fq2PointTest.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.junit.Test; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128Fq2PointTest { + + @Test + public void shouldProduceTheSameResultUsingAddsAndDoublings() { + assertThat(AltBn128Fq2Point.g2().doub().add(AltBn128Fq2Point.g2()).add(AltBn128Fq2Point.g2())) + .isEqualTo(AltBn128Fq2Point.g2().doub().doub()); + } + + @Test + public void shouldNotEqualEachOtherWhenDiferentPoints() { + assertThat(AltBn128Fq2Point.g2().doub()).isNotEqualTo(AltBn128Fq2Point.g2()); + } + + @Test + public void shouldEqualEachOtherWhenImpartialFractionsAreTheSame() { + assertThat( + AltBn128Fq2Point.g2() + .multiply(BigInteger.valueOf(9)) + .add(AltBn128Fq2Point.g2().multiply(BigInteger.valueOf(5)))) + .isEqualTo( + AltBn128Fq2Point.g2() + .multiply(BigInteger.valueOf(12)) + .add(AltBn128Fq2Point.g2().multiply(BigInteger.valueOf(2)))); + } + + @Test + public void shouldBeInfinityWhenMultipliedByCurveOrder() { + final BigInteger curveOrder = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495617"); + + assertThat(AltBn128Fq2Point.g2().multiply(curveOrder).isInfinity()).isTrue(); + } + + @Test + public void shouldNotBeInfinityWhenNotMultipliedByCurveOrder() { + // assert not is_inf(multiply(g2(), 2 * field_modulus - curve_order)) + final BigInteger two = BigInteger.valueOf(2); + final BigInteger curveOrder = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495617"); + final BigInteger factor = two.multiply(FieldElement.FIELD_MODULUS).subtract(curveOrder); + + assertThat(AltBn128Fq2Point.g2().multiply(factor).isInfinity()).isFalse(); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128PointTest.java b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128PointTest.java new file mode 100755 index 00000000000..54a0e72f400 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/AltBn128PointTest.java @@ -0,0 +1,409 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.junit.Test; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class AltBn128PointTest { + + @Test + public void shouldReturnEquivalentValueByAdditionAndDouble() { + assertThat(AltBn128Point.g1().doub().add(AltBn128Point.g1()).add(AltBn128Point.g1())) + .isEqualTo(AltBn128Point.g1().doub().doub()); + } + + @Test + public void shouldReturnInequivalentValueOnIdentityVsDouble() { + assertThat(AltBn128Point.g1().doub()).isNotEqualTo(AltBn128Point.g1()); + } + + @Test + public void shouldReturnEqualityOfValueByEquivalentAdditionMultiplication() { + assertThat( + AltBn128Point.g1() + .multiply(BigInteger.valueOf(9)) + .add(AltBn128Point.g1().multiply(BigInteger.valueOf(5)))) + .isEqualTo( + AltBn128Point.g1() + .multiply(BigInteger.valueOf(12)) + .add(AltBn128Point.g1().multiply(BigInteger.valueOf(2)))); + } + + @Test + public void shouldReturnInfinityOnMultiplicationByCurveOrder() { + final BigInteger curveOrder = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495617"); + assertThat(AltBn128Point.g1().multiply(curveOrder).isInfinity()).isTrue(); + } + + @Test + public void shouldReturnTrueWhenValuesAreInfinityBigIntZero() { + final AltBn128Point p = new AltBn128Point(Fq.create(0), Fq.create(0)); + assertThat(p.isInfinity()).isTrue(); + } + + @Test + public void shouldReturnInfinityWhenAddingTwoInfinities() { + final AltBn128Point p0 = AltBn128Point.INFINITY; + final AltBn128Point p1 = AltBn128Point.INFINITY; + assertThat(p0.add(p1).equals(AltBn128Point.INFINITY)).isTrue(); + } + + @Test + public void shouldReturnTrueWhenEqual() { + final AltBn128Point p0 = new AltBn128Point(Fq.create(3), Fq.create(4)); + final AltBn128Point p1 = new AltBn128Point(Fq.create(3), Fq.create(4)); + assertThat(p0.equals(p1)).isTrue(); + } + + @Test + public void shouldReturnFalseWhenNotEqual() { + final AltBn128Point p0 = new AltBn128Point(Fq.create(4), Fq.create(4)); + final AltBn128Point p1 = new AltBn128Point(Fq.create(3), Fq.create(4)); + assertThat(p0.equals(p1)).isFalse(); + } + + @Test + public void shouldReturnIdentityWhenPointAddInfinity() { + final AltBn128Point p0 = new AltBn128Point(Fq.create(1), Fq.create(2)); + final AltBn128Point p1 = new AltBn128Point(Fq.create(0), Fq.create(0)); + assertThat(p0.add(p1).equals(new AltBn128Point(Fq.create(1), Fq.create(2)))).isTrue(); + } + + @Test + public void shouldReturnPointOnInfinityAddPoint() { + final AltBn128Point p0 = + new AltBn128Point(Fq.create(BigInteger.valueOf(0)), Fq.create(BigInteger.valueOf(0))); + final AltBn128Point p1 = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + assertThat(p0.add(p1).equals(p1)).isTrue(); + } + + @Test + public void shouldReturnTrueSumOnDoubling() { + final AltBn128Point p0 = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + final AltBn128Point p1 = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + final Fq sumX = + Fq.create( + new BigInteger( + "1368015179489954701390400359078579693043519447331113978918064868415326638035")); + final Fq sumY = + Fq.create( + new BigInteger( + "9918110051302171585080402603319702774565515993150576347155970296011118125764")); + assertThat(p0.add(p1).equals(new AltBn128Point(sumX, sumY))).isTrue(); + } + + @Test + public void shouldReturnInfinityOnIdenticalInputPointValuesOfX() { + final Fq p0x = + Fq.create( + new BigInteger( + "10744596414106452074759370245733544594153395043370666422502510773307029471145")); + final Fq p0y = + Fq.create( + new BigInteger( + "848677436511517736191562425154572367705380862894644942948681172815252343932")); + final AltBn128Point p0 = new AltBn128Point(p0x, p0y); + + final Fq p1x = + Fq.create( + new BigInteger( + "10744596414106452074759370245733544594153395043370666422502510773307029471145")); + final Fq p1y = + Fq.create( + new BigInteger( + "21039565435327757486054843320102702720990930294403178719740356721829973864651")); + final AltBn128Point p1 = new AltBn128Point(p1x, p1y); + + assertThat(p0.add(p1).equals(AltBn128Point.INFINITY)).isTrue(); + } + + @Test + public void shouldReturnTrueAddAndComputeSlope() { + final Fq p0x = + Fq.create( + new BigInteger( + "10744596414106452074759370245733544594153395043370666422502510773307029471145")); + final Fq p0y = + Fq.create( + new BigInteger( + "848677436511517736191562425154572367705380862894644942948681172815252343932")); + final AltBn128Point p0 = new AltBn128Point(p0x, p0y); + + final Fq p1x = + Fq.create( + new BigInteger( + "1624070059937464756887933993293429854168590106605707304006200119738501412969")); + final Fq p1y = + Fq.create( + new BigInteger( + "3269329550605213075043232856820720631601935657990457502777101397807070461336")); + final AltBn128Point p1 = new AltBn128Point(p1x, p1y); + + final Fq sumX = + Fq.create( + new BigInteger( + "9836339169314901400584090930519505895878753154116006108033708428907043344230")); + final Fq sumY = + Fq.create( + new BigInteger( + "2085718088180884207082818799076507077917184375787335400014805976331012093279")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(p0.add(p1).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnInfinityWhenMultiplierIsInfinity() { + final Fq px = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq py = + Fq.create( + new BigInteger( + "11843594000332171325303933275547366297934113019079887694534126289021216356598")); + final AltBn128Point p = new AltBn128Point(px, py); + final BigInteger multiplier = BigInteger.ZERO; + assertThat(p.multiply(multiplier).equals(AltBn128Point.INFINITY)).isTrue(); + } + + @Test + public void shouldReturnTrueMultiplyScalarAndPoint() { + final AltBn128Point multiplicand = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + final BigInteger multiplier = + new BigInteger( + "115792089237316195423570985008687907853269984665640564039457584007913129639935"); + + final Fq sumX = + Fq.create( + new BigInteger( + "21415159568991615317144600033915305503576371596506956373206836402282692989778")); + final Fq sumY = + Fq.create( + new BigInteger( + "8573070896319864868535933562264623076420652926303237982078693068147657243287")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnIdentityWhenMultipliedByScalarValueOne() { + final Fq multiplicandX = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq multiplicandY = + Fq.create( + new BigInteger( + "11843594000332171325303933275547366297934113019079887694534126289021216356598")); + final AltBn128Point multiplicand = new AltBn128Point(multiplicandX, multiplicandY); + + final BigInteger multiplier = BigInteger.valueOf(1); + + final Fq sumX = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq sumY = + Fq.create( + new BigInteger( + "11843594000332171325303933275547366297934113019079887694534126289021216356598")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnTrueMultiplyPointByScalar() { + final AltBn128Point multiplicand = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + + final BigInteger multiplier = BigInteger.valueOf(9); + + final Fq sumX = + Fq.create( + new BigInteger( + "1624070059937464756887933993293429854168590106605707304006200119738501412969")); + final Fq sumY = + Fq.create( + new BigInteger( + "3269329550605213075043232856820720631601935657990457502777101397807070461336")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnInfinityMultiplyPointByFieldModulus() { + final Fq multiplicandX = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq multiplicandY = + Fq.create( + new BigInteger( + "11843594000332171325303933275547366297934113019079887694534126289021216356598")); + final AltBn128Point multiplicand = new AltBn128Point(multiplicandX, multiplicandY); + + final BigInteger multiplier = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495617"); + + assertThat(multiplicand.multiply(multiplier).equals(AltBn128Point.INFINITY)).isTrue(); + } + + @Test + public void shouldReturnSumMultiplyPointByScalar_0() { + final Fq multiplicandX = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq multiplicandY = + Fq.create( + new BigInteger( + "11843594000332171325303933275547366297934113019079887694534126289021216356598")); + final AltBn128Point multiplicand = new AltBn128Point(multiplicandX, multiplicandY); + + final BigInteger multiplier = BigInteger.valueOf(2); + + final Fq sumX = + Fq.create( + new BigInteger( + "1735584146725871897168740753407579795109098319299249929076571979506257370192")); + final Fq sumY = + Fq.create( + new BigInteger( + "6064265680718387101814183009970545962379849150498077125987052947023017993936")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnSumMultiplyPointByScalar_1() { + final Fq multiplicandX = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq multiplicandY = + Fq.create( + new BigInteger( + "11843594000332171325303933275547366297934113019079887694534126289021216356598")); + final AltBn128Point multiplicand = new AltBn128Point(multiplicandX, multiplicandY); + + final BigInteger multiplier = BigInteger.valueOf(9); + + final Fq sumX = + Fq.create( + new BigInteger( + "13447195743588318540108422034660542894354216867239950480700468911927695682420")); + final Fq sumY = + Fq.create( + new BigInteger( + "20282243652944194694550455553589850678366346583698568858716117082144718267765")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnSumMultiplyPointByScalar_2() { + final AltBn128Point multiplicand = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + + final BigInteger multiplier = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495616"); + + final Fq sumX = Fq.create(BigInteger.valueOf(1)); + final Fq sumY = + Fq.create( + new BigInteger( + "21888242871839275222246405745257275088696311157297823662689037894645226208581")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnSumMultiplyPointByScalar_3() { + final Fq multiplicandX = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq multiplicandY = + Fq.create( + new BigInteger( + "11843594000332171325303933275547366297934113019079887694534126289021216356598")); + final AltBn128Point multiplicand = new AltBn128Point(multiplicandX, multiplicandY); + + final BigInteger multiplier = + new BigInteger( + "21888242871839275222246405745257275088548364400416034343698204186575808495616"); + + final Fq sumX = + Fq.create( + new BigInteger( + "11999875504842010600789954262886096740416429265635183817701593963271973497827")); + final Fq sumY = + Fq.create( + new BigInteger( + "10044648871507103896942472469709908790762198138217935968154911605624009851985")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnSumMultiplyPointByScalar_4() { + final AltBn128Point multiplicand = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + + final BigInteger multiplier = new BigInteger("340282366920938463463374607431768211456"); + + final Fq sumX = + Fq.create( + new BigInteger( + "8920802327774939509523725599419958131004060744305956036272850138837360588708")); + final Fq sumY = + Fq.create( + new BigInteger( + "15515729996153051217274459095713198084165220977632053298080637275617709055542")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } + + @Test + public void shouldReturnSumMultiplyPointByScalar_5() { + final AltBn128Point multiplicand = + new AltBn128Point(Fq.create(BigInteger.valueOf(1)), Fq.create(BigInteger.valueOf(2))); + + final BigInteger multiplier = BigInteger.valueOf(2); + + final Fq sumX = + Fq.create( + new BigInteger( + "1368015179489954701390400359078579693043519447331113978918064868415326638035")); + final Fq sumY = + Fq.create( + new BigInteger( + "9918110051302171585080402603319702774565515993150576347155970296011118125764")); + final AltBn128Point sum = new AltBn128Point(sumX, sumY); + + assertThat(multiplicand.multiply(multiplier).equals(sum)).isTrue(); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq12Test.java b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq12Test.java new file mode 100755 index 00000000000..cac2a7d0d2c --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq12Test.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class Fq12Test { + + @Test + public void shouldBeTheSumWhenAdded() { + final Fq12 x = Fq12.create(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + final Fq12 f = Fq12.create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + final Fq12 fpx = Fq12.create(2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + + assertThat(x.add(f)).isEqualTo(fpx); + } + + @Test + public void shouldBeOneWhenPointIsDividedByItself() { + final Fq12 f = Fq12.create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + final Fq12 one = Fq12.create(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + + assertThat(f.divide(f)).isEqualTo(one); + } + + @Test + public void shouldBeALinearDivide() { + final Fq12 x = Fq12.create(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + final Fq12 f = Fq12.create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + final Fq12 one = Fq12.create(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + + assertThat(one.divide(f).add(x.divide(f))).isEqualTo(one.add(x).divide(f)); + } + + @Test + public void shouldBeALinearMultiply() { + final Fq12 x = Fq12.create(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + final Fq12 f = Fq12.create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); + final Fq12 one = Fq12.create(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + + assertThat(one.multiply(f).add(x.multiply(f))).isEqualTo(one.add(x).multiply(f)); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq2Test.java b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq2Test.java new file mode 100755 index 00000000000..eb8ccb6fe58 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/Fq2Test.java @@ -0,0 +1,57 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.junit.Test; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class Fq2Test { + + @Test + public void shouldBeTheSumWhenAdded() { + final Fq2 x = Fq2.create(1, 0); + final Fq2 f = Fq2.create(1, 2); + final Fq2 fpx = Fq2.create(2, 2); + + assertThat(x.add(f)).isEqualTo(fpx); + } + + @Test + public void shouldBeOneWhenPointIsDividedByItself() { + final Fq2 f = Fq2.create(1, 2); + final Fq2 one = Fq2.create(1, 0); + + assertThat(f.divide(f)).isEqualTo(one); + } + + @Test + public void shouldBeALinearDivide() { + final Fq2 x = Fq2.create(1, 0); + final Fq2 f = Fq2.create(1, 2); + final Fq2 one = Fq2.create(1, 0); + + assertThat(one.divide(f).add(x.divide(f))).isEqualTo(one.add(x).divide(f)); + } + + @Test + public void shouldBeALinearMultiply() { + final Fq2 x = Fq2.create(1, 0); + final Fq2 f = Fq2.create(1, 2); + final Fq2 one = Fq2.create(1, 0); + + assertThat(one.multiply(f).add(x.multiply(f))).isEqualTo(one.add(x).multiply(f)); + } + + @Test + public void shouldEqualOneWhenRaisedToFieldModulus() { + final Fq2 x = Fq2.create(1, 0); + final Fq2 one = Fq2.create(1, 0); + + assertThat(x.power(FieldElement.FIELD_MODULUS.pow(2).subtract(BigInteger.ONE))).isEqualTo(one); + } +} diff --git a/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/FqTest.java b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/FqTest.java new file mode 100755 index 00000000000..22d7ab4ae98 --- /dev/null +++ b/crypto/src/test/java/net/consensys/pantheon/crypto/altbn128/FqTest.java @@ -0,0 +1,117 @@ +package net.consensys.pantheon.crypto.altbn128; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; + +import org.junit.Test; + +/** + * Adapted from the pc_ecc (Apache 2 License) implementation: + * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py + */ +public class FqTest { + + @Test + public void shouldBeValidWhenLessThanFieldModulus() { + final Fq fq = Fq.create(FieldElement.FIELD_MODULUS.subtract(BigInteger.ONE)); + assertThat(fq.isValid()).isTrue(); + } + + @Test + public void shouldBeInvalidWhenEqualToFieldModulus() { + final Fq fq = Fq.create(FieldElement.FIELD_MODULUS); + assertThat(fq.isValid()).isFalse(); + } + + @Test + public void shouldBeInvalidWhenGreaterThanFieldModulus() { + final Fq fq = Fq.create(FieldElement.FIELD_MODULUS.add(BigInteger.ONE)); + assertThat(fq.isValid()).isFalse(); + } + + @Test + public void shouldBeAbleToAddNumbersWithoutOverflow() { + final Fq a = Fq.create(1); + final Fq b = Fq.create(1); + final Fq c = a.add(b); + assertThat(c).isEqualTo(Fq.create(2)); + } + + @Test + public void shouldBeAbleToAddNumbersWithOverflow() { + final Fq a = Fq.create(FieldElement.FIELD_MODULUS.subtract(BigInteger.ONE)); + final Fq b = Fq.create(2); + final Fq c = a.add(b); + assertThat(c).isEqualTo(Fq.one()); + } + + @Test + public void shouldBeAbleToSubtractNumbers() { + final Fq a = Fq.create(5); + final Fq b = Fq.create(3); + final Fq c = a.subtract(b); + assertThat(c).isEqualTo(Fq.create(2)); + } + + @Test + public void shouldBeAbleToMultiplyNumbersWithoutOverflow() { + final Fq a = Fq.create(2); + final Fq b = Fq.create(3); + final Fq c = a.multiply(b); + assertThat(c).isEqualTo(Fq.create(6)); + } + + @Test + public void shouldBeAbleToMultiplyNumbersWithOverflow() { + // FIELD_MODULUS is odd so (FIELD_MODULUS + 1) / 2 => FIELD_MODULUS / 2 + 1 (with int types). + final Fq a = + Fq.create(FieldElement.FIELD_MODULUS.add(BigInteger.ONE).divide(BigInteger.valueOf(2))); + final Fq b = Fq.create(2); + final Fq c = a.multiply(b); + assertThat(c).isEqualTo(Fq.one()); + } + + @Test + public void shouldNegatePositiveNumberToNegative() { + final Fq a = Fq.create(1); + assertThat(a.negate()).isEqualTo(Fq.create(-1)); + } + + @Test + public void shouldNegateNegativeNumberToPositive() { + final Fq a = Fq.create(-1); + assertThat(a.negate()).isEqualTo(Fq.create(1)); + } + + @Test + public void shouldBeProductWhenMultiplied() { + final Fq fq2 = Fq.create(2); + final Fq fq4 = Fq.create(4); + assertThat(fq2.multiply(fq2)).isEqualTo(fq4); + } + + @Test + public void shouldBeALinearDivide() { + final Fq fq2 = Fq.create(2); + final Fq fq7 = Fq.create(7); + final Fq fq9 = Fq.create(9); + final Fq fq11 = Fq.create(11); + assertThat(fq2.divide(fq7).add(fq9.divide(fq7))).isEqualTo(fq11.divide(fq7)); + } + + @Test + public void shouldBeALinearMultiply() { + final Fq fq2 = Fq.create(2); + final Fq fq7 = Fq.create(7); + final Fq fq9 = Fq.create(9); + final Fq fq11 = Fq.create(11); + assertThat(fq2.multiply(fq7).add(fq9.multiply(fq7))).isEqualTo(fq11.multiply(fq7)); + } + + @Test + public void shouldEqualItselWhenRaisedToFieldModulus() { + final Fq fq9 = Fq.create(9); + assertThat(fq9.power(FieldElement.FIELD_MODULUS)).isEqualTo(fq9); + } +} diff --git a/crypto/src/test/resources/log4j2.xml b/crypto/src/test/resources/log4j2.xml new file mode 100755 index 00000000000..3ea5233a1ca --- /dev/null +++ b/crypto/src/test/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + INFO + + + + + + + + + + + + \ No newline at end of file diff --git a/crypto/src/test/resources/net/consensys/pantheon/crypto/validPrivateKey.txt b/crypto/src/test/resources/net/consensys/pantheon/crypto/validPrivateKey.txt new file mode 100755 index 00000000000..c498f25bf94 --- /dev/null +++ b/crypto/src/test/resources/net/consensys/pantheon/crypto/validPrivateKey.txt @@ -0,0 +1 @@ +000000000000000000000000000000000000000000000000000000000000000A \ No newline at end of file diff --git a/errorprone-checks/README.md b/errorprone-checks/README.md new file mode 100755 index 00000000000..34f336cbc95 --- /dev/null +++ b/errorprone-checks/README.md @@ -0,0 +1,9 @@ +The creation of custom errorprone checkers was largely derived from: +* https://github.com/tbroyer/gradle-errorprone-plugin +* https://errorprone.info/docs/installation +* https://github.com/google/error-prone/wiki/Writing-a-check + +To allow for debugging from within intellij, the following must be added to the VM args +in the run/debug configuration (this assumes your gradle cache is at the default location under +your home): +-Xbootclasspath/p:${HOME}/.gradle/caches/./modules-2/files-2.1/com.google.errorprone/javac/9+181-r4173-1/bdf4c0aa7d540ee1f7bf14d47447aea4bbf450c5/javac-9+181-r4173-1.jar diff --git a/errorprone-checks/build.gradle b/errorprone-checks/build.gradle new file mode 100755 index 00000000000..e1e46a588a8 --- /dev/null +++ b/errorprone-checks/build.gradle @@ -0,0 +1,37 @@ +// See https://github.com/tbroyer/gradle-errorprone-plugin +// See https://github.com/tbroyer/gradle-apt-plugin +plugins { id 'net.ltgt.apt' version '0.14' apply false } + +// we use this config to get the path of the JDK 9 javac jar, to +// stick it in the bootclasspath when running tests +configurations.maybeCreate("epJavac") + + +apply plugin: 'java' +apply plugin: 'net.ltgt.errorprone' +apply plugin: 'net.ltgt.apt' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + + implementation "com.google.errorprone:error_prone_core" + implementation "com.google.errorprone:error_prone_annotation" + + implementation "com.google.auto.service:auto-service:1.0-rc4" + annotationProcessor "com.google.auto.service:auto-service:1.0-rc4" + + testImplementation "junit:junit" + testImplementation "org.assertj:assertj-core" + testImplementation 'com.google.errorprone:error_prone_test_helpers' + + epJavac "com.google.errorprone:error_prone_check_api" +} + +test { + if (JavaVersion.current().isJava8()) { + jvmArgs "-Xbootclasspath/p:${configurations.epJavac.asPath}" + } + testLogging { showStandardStreams = true } +} diff --git a/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectly.java b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectly.java new file mode 100755 index 00000000000..726bfbae8b0 --- /dev/null +++ b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectly.java @@ -0,0 +1,47 @@ +package net.consensys.errorpronechecks; + +import static com.google.errorprone.BugPattern.Category.JDK; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; +import com.sun.tools.javac.code.Symbol; + +@AutoService(BugChecker.class) +@BugPattern( + name = "DoNotCreateSecureRandomDirectly", + summary = "Do not create SecureRandom directly.", + category = JDK, + severity = WARNING +) +public class DoNotCreateSecureRandomDirectly extends BugChecker + implements MethodInvocationTreeMatcher, NewClassTreeMatcher { + + @Override + public Description matchMethodInvocation( + final MethodInvocationTree tree, final VisitorState state) { + if (tree.getMethodSelect().toString().equals("SecureRandom.getInstance")) { + return describeMatch(tree); + } + + return Description.NO_MATCH; + } + + @Override + public Description matchNewClass(final NewClassTree tree, final VisitorState state) { + final Symbol sym = ASTHelpers.getSymbol(tree.getIdentifier()); + if (sym != null && sym.toString().equals("java.security.SecureRandom")) { + return describeMatch(tree); + } + + return Description.NO_MATCH; + } +} diff --git a/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectly.java b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectly.java new file mode 100755 index 00000000000..5c1067f1804 --- /dev/null +++ b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectly.java @@ -0,0 +1,32 @@ +package net.consensys.errorpronechecks; + +import static com.google.errorprone.BugPattern.Category.JDK; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.MethodInvocationTree; + +@AutoService(BugChecker.class) +@BugPattern( + name = "DoNotInvokeMessageDigestDirectly", + summary = "Do not invoke MessageDigest.getInstance directly.", + category = JDK, + severity = WARNING +) +public class DoNotInvokeMessageDigestDirectly extends BugChecker + implements MethodInvocationTreeMatcher { + + @Override + public Description matchMethodInvocation( + final MethodInvocationTree tree, final VisitorState state) { + if (tree.getMethodSelect().toString().equals("MessageDigest.getInstance")) { + return describeMatch(tree); + } + return Description.NO_MATCH; + } +} diff --git a/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotReturnNullOptionals.java b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotReturnNullOptionals.java new file mode 100755 index 00000000000..2671a0d8193 --- /dev/null +++ b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/DoNotReturnNullOptionals.java @@ -0,0 +1,57 @@ +package net.consensys.errorpronechecks; + +import static com.google.errorprone.BugPattern.Category.JDK; +import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; +import static com.google.errorprone.matchers.Matchers.contains; +import static com.sun.source.tree.Tree.Kind.NULL_LITERAL; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; + +/* + * This is reworked from an example found at: + * https://github.com/google/error-prone/wiki/Writing-a-check + */ + +@AutoService(BugChecker.class) // the service descriptor +@BugPattern( + name = "DoNotReturnNullOptionals", + summary = "Do not return null optionals.", + category = JDK, + severity = SUGGESTION +) +public class DoNotReturnNullOptionals extends BugChecker implements MethodTreeMatcher { + + private static class ReturnNullMatcher implements Matcher { + + @Override + public boolean matches(final Tree tree, final VisitorState state) { + if ((tree instanceof ReturnTree) && (((ReturnTree) tree).getExpression() != null)) { + return ((ReturnTree) tree).getExpression().getKind() == NULL_LITERAL; + } + return false; + } + } + + private static final Matcher RETURN_NULL = new ReturnNullMatcher(); + private static final Matcher CONTAINS_RETURN_NULL = contains(RETURN_NULL); + + @Override + public Description matchMethod(final MethodTree tree, final VisitorState state) { + if ((tree.getReturnType() == null) + || !tree.getReturnType().toString().startsWith("Optional<") + || (tree.getBody() == null) + || (!CONTAINS_RETURN_NULL.matches(tree.getBody(), state))) { + return Description.NO_MATCH; + } + return describeMatch(tree); + } +} diff --git a/errorprone-checks/src/main/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinal.java b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinal.java new file mode 100755 index 00000000000..d4122c1d6f5 --- /dev/null +++ b/errorprone-checks/src/main/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinal.java @@ -0,0 +1,97 @@ +package net.consensys.errorpronechecks; + +import static com.google.errorprone.BugPattern.Category.JDK; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; + +import javax.lang.model.element.Modifier; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ModifiersTree; +import com.sun.source.tree.VariableTree; + +@AutoService(BugChecker.class) +@BugPattern( + name = "MethodInputParametersMustBeFinal", + summary = "Method input parameters must be final.", + category = JDK, + severity = WARNING +) +public class MethodInputParametersMustBeFinal extends BugChecker + implements MethodTreeMatcher, ClassTreeMatcher { + + private boolean isAbstraction = false; + + @Override + public Description matchClass(final ClassTree tree, final VisitorState state) { + isAbstraction = + isInterface(tree.getModifiers()) + || isAnonymousClassInAbstraction(tree) + || isEnumInAbstraction(tree); + return Description.NO_MATCH; + } + + @Override + public Description matchMethod(final MethodTree tree, final VisitorState state) { + final ModifiersTree mods = tree.getModifiers(); + + if (isAbstraction) { + if (isConcreteMethod(mods)) { + return matchParameters(tree); + } + } else if (isNotAbstract(mods)) { + return matchParameters(tree); + } + + return Description.NO_MATCH; + } + + private Description matchParameters(final MethodTree tree) { + for (final VariableTree inputParameter : tree.getParameters()) { + if (isMissingFinalModifier(inputParameter)) { + return describeMatch(tree); + } + } + + return Description.NO_MATCH; + } + + private boolean isMissingFinalModifier(final VariableTree inputParameter) { + return !inputParameter.getModifiers().getFlags().contains(Modifier.FINAL); + } + + private boolean isNotAbstract(final ModifiersTree mods) { + return !mods.getFlags().contains(Modifier.ABSTRACT); + } + + private boolean isInterface(final ModifiersTree mods) { + return mods.toString().contains("interface"); + } + + private boolean isConcreteMethod(final ModifiersTree mods) { + return mods.getFlags().contains(Modifier.DEFAULT) || mods.getFlags().contains(Modifier.STATIC); + } + + private boolean isAnonymousClassInAbstraction(final ClassTree tree) { + return isAbstraction && isAnonymousClass(tree); + } + + private boolean isAnonymousClass(final ClassTree tree) { + return tree.getSimpleName().contentEquals(""); + } + + private boolean isEnumInAbstraction(final ClassTree tree) { + return isAbstraction && isEnum(tree); + } + + private boolean isEnum(final ClassTree tree) { + return tree.toString().contains("enum"); + } +} diff --git a/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyTest.java b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyTest.java new file mode 100755 index 00000000000..62fb346c5da --- /dev/null +++ b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyTest.java @@ -0,0 +1,26 @@ +package net.consensys.errorpronechecks; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.Before; +import org.junit.Test; + +public class DoNotCreateSecureRandomDirectlyTest { + + private CompilationTestHelper compilationHelper; + + @Before + public void setup() { + compilationHelper = + CompilationTestHelper.newInstance(DoNotCreateSecureRandomDirectly.class, getClass()); + } + + @Test + public void doNotCreateSecureRandomDirectlyPositiveCases() { + compilationHelper.addSourceFile("DoNotCreateSecureRandomDirectlyPositiveCases.java").doTest(); + } + + @Test + public void doNotCreateSecureRandomDirectlyNegativeCases() { + compilationHelper.addSourceFile("DoNotCreateSecureRandomDirectlyNegativeCases.java").doTest(); + } +} diff --git a/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyTest.java b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyTest.java new file mode 100755 index 00000000000..35ef9898a8c --- /dev/null +++ b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyTest.java @@ -0,0 +1,26 @@ +package net.consensys.errorpronechecks; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.Before; +import org.junit.Test; + +public class DoNotInvokeMessageDigestDirectlyTest { + + private CompilationTestHelper compilationHelper; + + @Before + public void setup() { + compilationHelper = + CompilationTestHelper.newInstance(DoNotInvokeMessageDigestDirectly.class, getClass()); + } + + @Test + public void doNotInvokeMessageDigestDirectlyPositiveCases() { + compilationHelper.addSourceFile("DoNotInvokeMessageDigestDirectlyPositiveCases.java").doTest(); + } + + @Test + public void doNotInvokeMessageDigestDirectlyNegativeCases() { + compilationHelper.addSourceFile("DoNotInvokeMessageDigestDirectlyNegativeCases.java").doTest(); + } +} diff --git a/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotReturnNullOptionalsTest.java b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotReturnNullOptionalsTest.java new file mode 100755 index 00000000000..1772ebc29d5 --- /dev/null +++ b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/DoNotReturnNullOptionalsTest.java @@ -0,0 +1,26 @@ +package net.consensys.errorpronechecks; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.Before; +import org.junit.Test; + +public class DoNotReturnNullOptionalsTest { + + private CompilationTestHelper compilationHelper; + + @Before + public void setup() { + compilationHelper = + CompilationTestHelper.newInstance(DoNotReturnNullOptionals.class, getClass()); + } + + @Test + public void doNotReturnNullPositiveCases() { + compilationHelper.addSourceFile("DoNotReturnNullOptionalsPositiveCases.java").doTest(); + } + + @Test + public void doNotReturnNullNegativeCases() { + compilationHelper.addSourceFile("DoNotReturnNullOptionalsNegativeCases.java").doTest(); + } +} diff --git a/errorprone-checks/src/test/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalTest.java b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalTest.java new file mode 100755 index 00000000000..b0aa7823752 --- /dev/null +++ b/errorprone-checks/src/test/java/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalTest.java @@ -0,0 +1,40 @@ +package net.consensys.errorpronechecks; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.Before; +import org.junit.Test; + +public class MethodInputParametersMustBeFinalTest { + + private CompilationTestHelper compilationHelper; + + @Before + public void setup() { + compilationHelper = + CompilationTestHelper.newInstance(MethodInputParametersMustBeFinal.class, getClass()); + } + + @Test + public void methodInputParametersMustBeFinalPositiveCases() { + compilationHelper.addSourceFile("MethodInputParametersMustBeFinalPositiveCases.java").doTest(); + } + + @Test + public void methodInputParametersMustBeFinalInterfacePositiveCases() { + compilationHelper + .addSourceFile("MethodInputParametersMustBeFinalInterfacePositiveCases.java") + .doTest(); + } + + @Test + public void methodInputParametersMustBeFinalNegativeCases() { + compilationHelper.addSourceFile("MethodInputParametersMustBeFinalNegativeCases.java").doTest(); + } + + @Test + public void methodInputParametersMustBeFinalInterfaceNegativeCases() { + compilationHelper + .addSourceFile("MethodInputParametersMustBeFinalInterfaceNegativeCases.java") + .doTest(); + } +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyNegativeCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyNegativeCases.java new file mode 100755 index 00000000000..9071bc5337e --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyNegativeCases.java @@ -0,0 +1,19 @@ +package net.consensys.errorpronechecks; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DoNotCreateSecureRandomDirectlyNegativeCases { + + public void callsNonJRESecureRandomGetInstance() throws Exception { + TestSecureRandom.getInstance(""); + TestSecureRandom.getInstance("", ""); + TestSecureRandom.getInstance("", new Provider("", 0, "") {}); + } + + public void invokesNonJRESecureRandomConstructor() throws Exception { + new TestSecureRandom(); + } + + private class TestSecureRandom extends SecureRandom {} +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyPositiveCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyPositiveCases.java new file mode 100755 index 00000000000..d82c18d4d21 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotCreateSecureRandomDirectlyPositiveCases.java @@ -0,0 +1,26 @@ +package net.consensys.errorpronechecks; + +import java.security.Provider; +import java.security.SecureRandom; + +public class DoNotCreateSecureRandomDirectlyPositiveCases { + + public void callsSecureRandomGetInstance() throws Exception { + // BUG: Diagnostic contains: Do not create SecureRandom directly. + SecureRandom.getInstance(""); + + // BUG: Diagnostic contains: Do not create SecureRandom directly. + SecureRandom.getInstance("", ""); + + // BUG: Diagnostic contains: Do not create SecureRandom directly. + SecureRandom.getInstance("", new Provider("", 0, "") {}); + } + + public void invokesSecureRandomConstructor() throws Exception { + // BUG: Diagnostic contains: Do not create SecureRandom directly. + new SecureRandom(); + + // BUG: Diagnostic contains: Do not create SecureRandom directly. + new SecureRandom(new byte[] {}); + } +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyNegativeCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyNegativeCases.java new file mode 100755 index 00000000000..4b32dec6a14 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyNegativeCases.java @@ -0,0 +1,11 @@ +package net.consensys.errorpronechecks; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class DoNotInvokeMessageDigestDirectlyNegativeCases { + + public void callsMessageDigestGetInstance() throws NoSuchAlgorithmException { + MessageDigest dig = null; + } +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyPositiveCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyPositiveCases.java new file mode 100755 index 00000000000..3238e482206 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotInvokeMessageDigestDirectlyPositiveCases.java @@ -0,0 +1,12 @@ +package net.consensys.errorpronechecks; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class DoNotInvokeMessageDigestDirectlyPositiveCases { + + public void callsMessageDigestGetInstance() throws NoSuchAlgorithmException { + // BUG: Diagnostic contains: Do not invoke MessageDigest.getInstance directly. + MessageDigest dig = MessageDigest.getInstance("SHA-256"); + } +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsNegativeCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsNegativeCases.java new file mode 100755 index 00000000000..88100dd9946 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsNegativeCases.java @@ -0,0 +1,22 @@ +package net.consensys.errorpronechecks; + +import java.util.Optional; +import javax.annotation.Nullable; + +public class DoNotReturnNullOptionalsNegativeCases { + + public interface allInterfacesAreValid { + public Optional ExpectToBeOverridden(); + } + + public DoNotReturnNullOptionalsNegativeCases() {} + + public Optional doesNotReturnNull() { + return Optional.of(3L); + } + + @Nullable + public Optional returnsNullButAnnotatedWithNullable() { + return Optional.empty(); + } +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsPositiveCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsPositiveCases.java new file mode 100755 index 00000000000..50dccc69185 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/DoNotReturnNullOptionalsPositiveCases.java @@ -0,0 +1,20 @@ +package net.consensys.errorpronechecks; + +import java.util.Optional; + +public class DoNotReturnNullOptionalsPositiveCases { + + // BUG: Diagnostic contains: Do not return null optionals. + public Optional returnsNull() { + return null; + } + + // BUG: Diagnostic contains: Do not return null optionals. + public Optional sometimesReturnsNull(boolean random) { + if (random) { + + return null; + } + return Optional.of(2L); + } +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfaceNegativeCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfaceNegativeCases.java new file mode 100755 index 00000000000..f66af5d3ff0 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfaceNegativeCases.java @@ -0,0 +1,26 @@ +package net.consensys.errorpronechecks; + +import java.util.Observable; +import java.util.Observer; + +public interface MethodInputParametersMustBeFinalInterfaceNegativeCases { + + void parameterCannotBeFinal(int value); + + default void concreteMethod(final long value) {} + + static void anotherConcreteMethod(final double value) {} + + static Observer annonymousClass() { + return new Observer() { + @Override + public void update(final Observable o, final Object arg) {} + }; + } + + void methodAfterAnnonymousClass(int value); + + enum Status {} + + void methodAfterEnum(int value); +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfacePositiveCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfacePositiveCases.java new file mode 100755 index 00000000000..5044d206105 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalInterfacePositiveCases.java @@ -0,0 +1,10 @@ +package net.consensys.errorpronechecks; + +public interface MethodInputParametersMustBeFinalInterfacePositiveCases { + + // BUG: Diagnostic contains: Method input parameters must be final. + default void concreteMethod(int value) {} + + // BUG: Diagnostic contains: Method input parameters must be final. + static void concreteStaticMethodsAreIncluded(int value) {} +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalNegativeCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalNegativeCases.java new file mode 100755 index 00000000000..68aac775f2a --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalNegativeCases.java @@ -0,0 +1,16 @@ +package net.consensys.errorpronechecks; + +public class MethodInputParametersMustBeFinalNegativeCases { + + public void noInputParameters() {} + + public void onlyPrimativeInputParameters(final long value) {} + + public void onlyObjectInputParameters(final Object value) {} + + public void mixedInputParameters(final Object value, final int anotherValue) {} + + public interface allInterfacesAreValid { + void parameterCannotBeFinal(int value); + } +} diff --git a/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalPositiveCases.java b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalPositiveCases.java new file mode 100755 index 00000000000..19522faa813 --- /dev/null +++ b/errorprone-checks/src/test/resources/net/consensys/errorpronechecks/MethodInputParametersMustBeFinalPositiveCases.java @@ -0,0 +1,27 @@ +package net.consensys.errorpronechecks; + +public class MethodInputParametersMustBeFinalPositiveCases { + + // BUG: Diagnostic contains: Method input parameters must be final. + public void primativeInputMethod(int value) {} + + // BUG: Diagnostic contains: Method input parameters must be final. + public void objectInputMethod(Object value) {} + + // BUG: Diagnostic contains: Method input parameters must be final. + public void mixedInputMethod(Object value, int anotherValue) {} + + // BUG: Diagnostic contains: Method input parameters must be final. + public void mixedInputMethodFirstFinal(final Object value, int anotherValue) {} + + // BUG: Diagnostic contains: Method input parameters must be final. + public void mixedInputMethodSecondFinal(Object value, final int anotherValue) {} + + // BUG: Diagnostic contains: Method input parameters must be final. + public void varArgsInputMethod(String... value) {} + + public abstract class abstractClassDefinition { + // BUG: Diagnostic contains: Method input parameters must be final. + public void concreteMethodsAreIncluded(int value) {} + } +} diff --git a/ethereum/build.gradle b/ethereum/build.gradle new file mode 100755 index 00000000000..4e71f006a0b --- /dev/null +++ b/ethereum/build.gradle @@ -0,0 +1 @@ +jar { enabled = false } diff --git a/ethereum/core/build.gradle b/ethereum/core/build.gradle new file mode 100755 index 00000000000..39e317af58a --- /dev/null +++ b/ethereum/core/build.gradle @@ -0,0 +1,151 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-core' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + implementation project(':crypto') + implementation project(':ethereum:rlp') + implementation project(':ethereum:trie') + implementation project(':services:kvstore') + + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.google.guava:guava' + implementation 'io.vertx:vertx-core' + implementation 'org.apache.logging.log4j:log4j-api' + + runtime 'org.apache.logging.log4j:log4j-core' + + testImplementation project(path:':ethereum:referencetests', configuration: 'testOutput') + testImplementation project(':testutil') + + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' + testImplementation 'junit:junit' + + integrationTestImplementation 'org.assertj:assertj-core' + integrationTestImplementation 'org.mockito:mockito-core' + integrationTestImplementation 'junit:junit' + + testSupportImplementation project(':testutil') + + testSupportImplementation 'org.assertj:assertj-core' + testSupportImplementation 'org.mockito:mockito-core' + testSupportImplementation 'junit:junit' +} + +configurations { testArtifacts } +task testJar (type: Jar) { + baseName = "${project.name}-test" + from sourceSets.test.output +} + +test { + exclude 'net/consensys/pantheon/ethereum/vm/**ReferenceTest.class' + exclude 'net/consensys/pantheon/ethereum/vm/blockchain/**.class' + exclude 'net/consensys/pantheon/ethereum/vm/generalstate/**.class' +} + +def generateTestFiles(FileTree jsonPath, File resourcesPath, File templateFile, String pathstrip, String destination, String namePrefix) { + def referenceTestTemplate = templateFile.text + + // This is how many json files to include in each test file + def fileSets = jsonPath.getFiles().collate(5) + + fileSets.each { fileSet -> + def resPath = resourcesPath.getPath().replaceAll("\\\\", "/") + def name = fileSet.first().getPath().toString() + .replaceAll("\\\\", "/") + .replaceAll(resPath + "/", "") + .replaceAll(pathstrip, "") + .replaceAll(".json", "") + .replaceAll("/", "_") + .replaceAll("\\^", "") + .replaceAll("\\-", "") + .replaceAll("\\+", "") + + def paths = [] + fileSet.each { testJsonFile -> + if (!testJsonFile.getName().toString().startsWith(".")) { + paths << testJsonFile.getPath().toString() + .replaceAll(resPath + "/", "") + } + } + + def testFile = file(destination + "/" + namePrefix + "_" + name + ".java") + + def allPaths = '"' + paths.join('", "') + '"'; + + def testFileContents = referenceTestTemplate + .replaceAll("%%TESTS_FILE%%", allPaths) + .replaceAll("%%TESTS_NAME%%", namePrefix + "_" + name) + testFile.newWriter().withWriter { w -> w << testFileContents } + } +} + +task blockchainReferenceTestsSetup { + generateTestFiles( + fileTree('../referencetests/src/test/resources/BlockchainTests'), + file("../referencetests/src/test/resources"), + file("./src/test/resources/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTest.java.template"), + "BlockchainTests/", + "./src/test/java/net/consensys/pantheon/ethereum/vm/blockchain", + "BlockchainReferenceTest" + ) +} + +compileTestJava.dependsOn(blockchainReferenceTestsSetup) + +task generalstateReferenceTestsSetup { + generateTestFiles( + fileTree("../referencetests/src/test/resources/GeneralStateTests"), + file("../referencetests/src/test/resources"), + file("./src/test/resources/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTest.java.template"), + "GeneralStateTests/", + "./src/test/java/net/consensys/pantheon/ethereum/vm/generalstate", + "GeneralStateReferenceTest" + ) +} + +compileTestJava.dependsOn(generalstateReferenceTestsSetup) + +task generalstateRegressionReferenceTestsSetup { + generateTestFiles( + fileTree("./src/test/resources/regressions/generalstate"), + file("./src/test/resources"), + file("./src/test/resources/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTest.java.template"), + "regressions/generalstate/", + "./src/test/java/net/consensys/pantheon/ethereum/vm/generalstate", + "GeneralStateRegressionReferenceTest" + ) +} + +compileTestJava.dependsOn(generalstateRegressionReferenceTestsSetup) + +task cleanupReferenceTests(type: Delete) { + delete fileTree("./src/test/java/net/consensys/pantheon/ethereum/vm/generalstate/") { + include("**/GeneralStateReferenceTest*.java") + include("**/GeneralStateRegressionReferenceTest*.java") + } + delete fileTree("./src/test/java/net/consensys/pantheon/ethereum/vm/blockchain/") { include("**/BlockchainReferenceTest*.java") } +} + +clean.dependsOn(cleanupReferenceTests) + +task referenceTests(type: Test, dependsOn: ["compileTestJava"]) { + scanForTestClasses = false + enableAssertions = true + include 'net/consensys/pantheon/ethereum/vm/**ReferenceTest.class' + include 'net/consensys/pantheon/ethereum/vm/blockchain/**.class' + include 'net/consensys/pantheon/ethereum/vm/generalstate/**.class' +} + +artifacts { + testArtifacts testJar + testSupportArtifacts testSupportJar +} diff --git a/ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/EntriesFromIntegrationTest.java b/ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/EntriesFromIntegrationTest.java new file mode 100755 index 00000000000..975f2a3742f --- /dev/null +++ b/ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/EntriesFromIntegrationTest.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.vm; + +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; + +import org.junit.Test; + +public class EntriesFromIntegrationTest { + + @Test + public void shouldCollectStateEntries() { + final MutableWorldState worldState = createInMemoryWorldStateArchive().getMutable(); + final WorldUpdater updater = worldState.updater(); + MutableAccount account = updater.getOrCreate(Address.fromHexString("0x56")); + final Map expectedValues = new TreeMap<>(); + final int nodeCount = 100_000; + final Random random = new Random(42989428249L); + + // Create some storage entries in the committed, underlying account. + for (int i = 0; i <= nodeCount; i++) { + addExpectedValue( + account, expectedValues, UInt256.of(Math.abs(random.nextLong())), UInt256.of(i * 10 + 1)); + } + updater.commit(); + + // Add some changes on top that AbstractWorldUpdater.UpdateTrackingAccount will have to merge. + account = worldState.updater().getOrCreate(Address.fromHexString("0x56")); + for (int i = 0; i <= nodeCount; i++) { + addExpectedValue( + account, expectedValues, UInt256.of(Math.abs(random.nextLong())), UInt256.of(i * 10 + 1)); + } + + final Map values = + account.storageEntriesFrom(Bytes32.ZERO, Integer.MAX_VALUE); + assertThat(values).isEqualTo(expectedValues); + } + + private void addExpectedValue( + final MutableAccount account, + final Map expectedValues, + final UInt256 key, + final UInt256 value) { + account.setStorageValue(key, value); + expectedValues.put(Hash.hash(key.getBytes()), value); + } +} diff --git a/ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/TraceTransactionIntegrationTest.java b/ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/TraceTransactionIntegrationTest.java new file mode 100755 index 00000000000..44463f52302 --- /dev/null +++ b/ethereum/core/src/integration-test/java/net/consensys/pantheon/ethereum/vm/TraceTransactionIntegrationTest.java @@ -0,0 +1,248 @@ +package net.consensys.pantheon.ethereum.vm; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.debug.TraceOptions; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.Map.Entry; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; + +public class TraceTransactionIntegrationTest { + + private static final String CONTRACT_CREATION_TX = + "0xf9014880808347b7608080b8fb608060405234801561001057600080fd5b5060dc8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633fa4f24514604e57806355241077146076575b600080fd5b348015605957600080fd5b50606060a0565b6040518082815260200191505060405180910390f35b348015608157600080fd5b50609e6004803603810190808035906020019092919050505060a6565b005b60005481565b80600081905550505600a165627a7a723058202bdbba2e694dba8fff33d9d0976df580f57bff0a40e25a46c398f8063b4c003600291ca057095e0bd8b08b1311ce81cf202fbaebf1f5bafeadbe9870cec3c0f5dd93bd0ea03639e8e40eedd640eb3565097a96c6c0c553d7e74ecc7b4b571eb9875d23d871"; + + private static final String CONTRACT_CREATION_DATA = + "608060405234801561001057600080fd5b50610228806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063368b87721461005157806360029940146100ba575b600080fd5b34801561005d57600080fd5b506100b8600480360381019080803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509192919290505050610123565b005b3480156100c657600080fd5b50610121600480360381019080803590602001908201803590602001908080601f016020809104026020016040519081016040528093929190818152602001838380828437820191505050505050919291929050505061013d565b005b8060009080519060200190610139929190610157565b5050565b8060019080519060200190610153929190610157565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061019857805160ff19168380011785556101c6565b828001600101855582156101c6579182015b828111156101c55782518255916020019190600101906101aa565b5b5090506101d391906101d7565b5090565b6101f991905b808211156101f55760008160009055506001016101dd565b5090565b905600a165627a7a72305820e01bbaa933adf9c8cbeaa62f07aa3c6349ae777dcb32ae0bbb4c1a4809204aef0029"; + private static final String CALL_SET_OTHER = + "0x60029940000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000036261720000000000000000000000000000000000000000000000000000000000"; + private MutableBlockchain blockchain; + private WorldStateArchive worldStateArchive; + private Block genesisBlock; + private TransactionProcessor transactionProcessor; + + @Before + public void setUp() { + final GenesisConfig genesisConfig = GenesisConfig.development(); + genesisBlock = genesisConfig.getBlock(); + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockchain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + worldStateArchive = + new WorldStateArchive(new KeyValueStorageWorldStateStorage(keyValueStorage)); + final ProtocolSchedule protocolSchedule = genesisConfig.getProtocolSchedule(); + genesisConfig.writeStateTo(worldStateArchive.getMutable(Hash.EMPTY_TRIE_HASH)); + transactionProcessor = protocolSchedule.getByBlockNumber(0).getTransactionProcessor(); + } + + @Test + public void shouldTraceSStoreOperation() { + final KeyPair keyPair = KeyPair.generate(); + final Transaction createTransaction = + Transaction.builder() + .gasLimit(300_000) + .gasPrice(Wei.ZERO) + .nonce(0) + .payload(BytesValue.fromHexString(CONTRACT_CREATION_DATA)) + .value(Wei.ZERO) + .signAndBuild(keyPair); + + final MutableWorldState worldState = + worldStateArchive.getMutable(genesisBlock.getHeader().getStateRoot()); + final WorldUpdater createTransactionUpdater = worldState.updater(); + Result result = + transactionProcessor.processTransaction( + blockchain, + createTransactionUpdater, + genesisBlock.getHeader(), + createTransaction, + genesisBlock.getHeader().getCoinbase()); + assertThat(result.isSuccessful()).isTrue(); + final Account createdContract = + createTransactionUpdater + .getTouchedAccounts() + .stream() + .filter(account -> !account.getCode().isEmpty()) + .findAny() + .get(); + createTransactionUpdater.commit(); + + // Now call the transaction to execute the SSTORE. + final DebugOperationTracer tracer = + new DebugOperationTracer(new TraceOptions(true, true, true)); + final Transaction executeTransaction = + Transaction.builder() + .gasLimit(300_000) + .gasPrice(Wei.ZERO) + .nonce(1) + .payload(BytesValue.fromHexString(CALL_SET_OTHER)) + .to(createdContract.getAddress()) + .value(Wei.ZERO) + .signAndBuild(keyPair); + final WorldUpdater storeUpdater = worldState.updater(); + result = + transactionProcessor.processTransaction( + blockchain, + storeUpdater, + genesisBlock.getHeader(), + executeTransaction, + genesisBlock.getHeader().getCoinbase(), + tracer); + + assertThat(result.isSuccessful()).isTrue(); + + // No storage changes before the SSTORE call. + TraceFrame frame = tracer.getTraceFrames().get(170); + assertThat(frame.getOpcode()).isEqualTo("DUP6"); + assertStorageContainsExactly(frame); + + // Storage changes show up in the SSTORE frame. + frame = tracer.getTraceFrames().get(171); + assertThat(frame.getOpcode()).isEqualTo("SSTORE"); + assertStorageContainsExactly( + frame, entry("0x01", "0x6261720000000000000000000000000000000000000000000000000000000006")); + + // And storage changes are still present in future frames. + frame = tracer.getTraceFrames().get(172); + assertThat(frame.getOpcode()).isEqualTo("PUSH2"); + assertStorageContainsExactly( + frame, entry("0x01", "0x6261720000000000000000000000000000000000000000000000000000000006")); + } + + @Test + public void shouldTraceContractCreation() { + final DebugOperationTracer tracer = + new DebugOperationTracer(new TraceOptions(true, true, true)); + final Transaction transaction = + Transaction.readFrom( + new BytesValueRLPInput(BytesValue.fromHexString(CONTRACT_CREATION_TX), false)); + transactionProcessor.processTransaction( + blockchain, + worldStateArchive.getMutable(genesisBlock.getHeader().getStateRoot()).updater(), + genesisBlock.getHeader(), + transaction, + genesisBlock.getHeader().getCoinbase(), + tracer); + + final int expectedDepth = 0; // Reference impl returned 1. Why the difference? + + final List traceFrames = tracer.getTraceFrames(); + assertThat(traceFrames).hasSize(17); + + TraceFrame frame = traceFrames.get(0); + assertThat(frame.getDepth()).isEqualTo(expectedDepth); + assertThat(frame.getGasRemaining()).isEqualTo(Gas.of(4632748)); + assertThat(frame.getGasCost()).contains(Gas.of(3)); + assertThat(frame.getOpcode()).isEqualTo("PUSH1"); + assertThat(frame.getPc()).isEqualTo(0); + assertStackContainsExactly(frame); + assertMemoryContainsExactly(frame); + assertStorageContainsExactly(frame); + + frame = traceFrames.get(1); + assertThat(frame.getDepth()).isEqualTo(expectedDepth); + assertThat(frame.getGasRemaining()).isEqualTo(Gas.of(4632745)); + assertThat(frame.getGasCost()).contains(Gas.of(3)); + assertThat(frame.getOpcode()).isEqualTo("PUSH1"); + assertThat(frame.getPc()).isEqualTo(2); + assertStackContainsExactly( + frame, "0000000000000000000000000000000000000000000000000000000000000080"); + assertMemoryContainsExactly(frame); + assertStorageContainsExactly(frame); + + frame = traceFrames.get(2); + assertThat(frame.getDepth()).isEqualTo(expectedDepth); + assertThat(frame.getGasRemaining()).isEqualTo(Gas.of(4632742)); + assertThat(frame.getGasCost()).contains(Gas.of(12)); + assertThat(frame.getOpcode()).isEqualTo("MSTORE"); + assertThat(frame.getPc()).isEqualTo(4); + assertStackContainsExactly( + frame, + "0000000000000000000000000000000000000000000000000000000000000080", + "0000000000000000000000000000000000000000000000000000000000000040"); + assertMemoryContainsExactly(frame); + assertStorageContainsExactly(frame); + // Reference implementation actually records the memory after expansion but before the store. + // assertMemoryContainsExactly(frame, + // "0000000000000000000000000000000000000000000000000000000000000000", + // "0000000000000000000000000000000000000000000000000000000000000000", + // "0000000000000000000000000000000000000000000000000000000000000000"); + + frame = traceFrames.get(3); + assertThat(frame.getDepth()).isEqualTo(expectedDepth); + assertThat(frame.getGasRemaining()).isEqualTo(Gas.of(4632730)); + assertThat(frame.getGasCost()).contains(Gas.of(2)); + assertThat(frame.getOpcode()).isEqualTo("CALLVALUE"); + assertThat(frame.getPc()).isEqualTo(5); + assertStackContainsExactly(frame); + assertMemoryContainsExactly( + frame, + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000080"); + assertStorageContainsExactly(frame); + } + + private void assertStackContainsExactly( + final TraceFrame frame, final String... stackEntriesAsHex) { + assertThat(frame.getStack()).isPresent(); + final Bytes32[] stackEntries = + Stream.of(stackEntriesAsHex).map(Bytes32::fromHexString).toArray(Bytes32[]::new); + assertThat(frame.getStack().get()).containsExactly(stackEntries); + } + + private void assertMemoryContainsExactly( + final TraceFrame frame, final String... memoryEntriesAsHex) { + assertThat(frame.getMemory()).isPresent(); + final Bytes32[] memoryEntries = + Stream.of(memoryEntriesAsHex).map(Bytes32::fromHexString).toArray(Bytes32[]::new); + assertThat(frame.getMemory().get()).containsExactly(memoryEntries); + } + + @SuppressWarnings("unchecked") + @SafeVarargs + private final void assertStorageContainsExactly( + final TraceFrame frame, final Entry... memoryEntriesAsHex) { + assertThat(frame.getMemory()).isPresent(); + final Entry[] memoryEntries = + Stream.of(memoryEntriesAsHex) + .map( + entry -> + entry( + UInt256.fromHexString(entry.getKey()), + UInt256.fromHexString(entry.getValue()))) + .toArray(Entry[]::new); + assertThat(frame.getStorage().get()).containsExactly(memoryEntries); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/ProtocolContext.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/ProtocolContext.java new file mode 100755 index 00000000000..f68f5b0f655 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/ProtocolContext.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum; + +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; + +/** + * Holds the mutable state used to track the current context of the protocol. This is primarily the + * blockchain and world state archive, but can also hold arbitrary context required by a particular + * consensus algorithm. + * + * @param the type of the consensus algorithm context + */ +public class ProtocolContext { + private final MutableBlockchain blockchain; + private final WorldStateArchive worldStateArchive; + private final C consensusState; + + public ProtocolContext( + final MutableBlockchain blockchain, + final WorldStateArchive worldStateArchive, + final C consensusState) { + this.blockchain = blockchain; + this.worldStateArchive = worldStateArchive; + this.consensusState = consensusState; + } + + public MutableBlockchain getBlockchain() { + return blockchain; + } + + public WorldStateArchive getWorldStateArchive() { + return worldStateArchive; + } + + public C getConsensusState() { + return consensusState; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java new file mode 100755 index 00000000000..29893629aee --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AbstractBlockCreator.java @@ -0,0 +1,269 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.core.SealableBlockHeader; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.mainnet.BodyValidation; +import net.consensys.pantheon.ethereum.mainnet.DifficultyCalculator; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockProcessor.TransactionReceiptFactory; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +import com.google.common.collect.Lists; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public abstract class AbstractBlockCreator implements AsyncBlockCreator { + + public interface ExtraDataCalculator { + + BytesValue get(final BlockHeader parent); + } + + private static final Logger LOGGER = LogManager.getLogger(AbstractBlockCreator.class); + + protected final Address coinbase; + + private final Function gasLimitCalculator; + + private final ExtraDataCalculator extraDataCalculator; + private final PendingTransactions pendingTransactions; + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final Wei minTransactionGasPrice; + private final Address miningBeneficiary; + private final BlockHeader parentHeader; + + private final AtomicBoolean isCancelled = new AtomicBoolean(false); + + public AbstractBlockCreator( + final Address coinbase, + final ExtraDataCalculator extraDataCalculator, + final PendingTransactions pendingTransactions, + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final Function gasLimitCalculator, + final Wei minTransactionGasPrice, + final Address miningBeneficiary, + final BlockHeader parentHeader) { + this.coinbase = coinbase; + this.extraDataCalculator = extraDataCalculator; + this.pendingTransactions = pendingTransactions; + this.protocolContext = protocolContext; + this.protocolSchedule = protocolSchedule; + this.gasLimitCalculator = gasLimitCalculator; + this.minTransactionGasPrice = minTransactionGasPrice; + this.miningBeneficiary = miningBeneficiary; + this.parentHeader = parentHeader; + } + + /** + * Create block will create a new block at the head of the blockchain specified in the + * protocolContext. + * + *

It will select transactions from the PendingTransaction list for inclusion in the Block + * body, and will supply an empty Ommers list. + * + *

Once transactions have been selected and applied to a disposable/temporary world state, the + * block reward is paid to the relevant coinbase, and a sealable header is constucted. + * + *

The sealableHeader is then provided to child instances for sealing (i.e. proof of work or + * otherwise). + * + *

The constructed block is then returned. + * + * @return a block with appropriately selected transactions, seals and ommers. + */ + @Override + public Block createBlock(final long timestamp) { + try { + final ProcessableBlockHeader processableBlockHeader = createPendingBlockHeader(timestamp); + + throwIfStopped(); + + final MutableWorldState disposableWorldState = duplicateWorldStateAtParent(); + + throwIfStopped(); + + final List ommers = selectOmmers(); + + throwIfStopped(); + + final BlockTransactionSelector.TransactionSelectionResults transactionResults = + selectTransactions(processableBlockHeader, disposableWorldState); + + throwIfStopped(); + + final Wei blockReward = + protocolSchedule.getByBlockNumber(processableBlockHeader.getNumber()).getBlockReward(); + + if (!rewardBeneficiary(disposableWorldState, processableBlockHeader, ommers, blockReward)) { + LOGGER.trace("Failed to apply mining reward, exiting."); + throw new RuntimeException("Failed to apply mining reward."); + } + + throwIfStopped(); + + final BlockHeader parentHeader = + protocolContext + .getBlockchain() + .getBlockHeader(processableBlockHeader.getParentHash()) + .get(); + + final SealableBlockHeader sealableBlockHeader = + BlockHeaderBuilder.create() + .populateFrom(processableBlockHeader) + .ommersHash(BodyValidation.ommersHash(ommers)) + .stateRoot(disposableWorldState.rootHash()) + .transactionsRoot( + BodyValidation.transactionsRoot(transactionResults.getTransactions())) + .receiptsRoot(BodyValidation.receiptsRoot(transactionResults.getReceipts())) + .logsBloom(BodyValidation.logsBloom(transactionResults.getReceipts())) + .gasUsed(transactionResults.getCumulativeGasUsed()) + .extraData(extraDataCalculator.get(parentHeader)) + .buildSealableBlockHeader(); + + final BlockHeader blockHeader = createFinalBlockHeader(sealableBlockHeader); + + return new Block(blockHeader, new BlockBody(transactionResults.getTransactions(), ommers)); + + } catch (final CancellationException ex) { + LOGGER.trace("Attempt to create block was interrupted."); + throw ex; + } catch (final Exception ex) { + // TODO(tmm): How are we going to know this has exploded, and thus restart it? + LOGGER.trace( + "Block creation failed unexpectedly. Will restart on next block added to chain."); + throw ex; + } + } + + private BlockTransactionSelector.TransactionSelectionResults selectTransactions( + final ProcessableBlockHeader processableBlockHeader, + final MutableWorldState disposableWorldState) + throws RuntimeException { + final long blockNumber = processableBlockHeader.getNumber(); + + final TransactionProcessor transactionProcessor = + protocolSchedule.getByBlockNumber(blockNumber).getTransactionProcessor(); + + final TransactionReceiptFactory transactionReceiptFactory = + protocolSchedule.getByBlockNumber(blockNumber).getTransactionReceiptFactory(); + + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + protocolContext.getBlockchain(), + disposableWorldState, + pendingTransactions, + processableBlockHeader, + transactionReceiptFactory, + minTransactionGasPrice, + isCancelled::get, + miningBeneficiary); + + return selector.buildTransactionListForBlock(); + } + + private MutableWorldState duplicateWorldStateAtParent() { + final Hash parentStateRoot = parentHeader.getStateRoot(); + final MutableWorldState worldState = + protocolContext.getWorldStateArchive().getMutable(parentStateRoot); + + return worldState.copy(); + } + + private List selectOmmers() { + return Lists.newArrayList(); + } + + private ProcessableBlockHeader createPendingBlockHeader(final long timestamp) { + final long newBlockNumber = parentHeader.getNumber() + 1; + final long gasLimit = gasLimitCalculator.apply(parentHeader.getGasLimit()); + + final DifficultyCalculator difficultyCalculator = + protocolSchedule.getByBlockNumber(newBlockNumber).getDifficultyCalculator(); + final BigInteger difficulty = + difficultyCalculator.nextDifficulty(timestamp, parentHeader, protocolContext); + + return BlockHeaderBuilder.create() + .parentHash(parentHeader.getHash()) + .coinbase(coinbase) + .difficulty(UInt256.of(difficulty)) + .number(newBlockNumber) + .gasLimit(gasLimit) + .timestamp(timestamp) + .buildProcessableBlockHeader(); + } + + @Override + public void cancel() { + isCancelled.set(true); + } + + protected void throwIfStopped() throws CancellationException { + if (isCancelled.get()) { + throw new CancellationException(); + } + } + + /* Copied from BlockProcessor (with modifications). */ + private boolean rewardBeneficiary( + final MutableWorldState worldState, + final ProcessableBlockHeader header, + final List ommers, + final Wei blockReward) { + + // TODO(tmm): Added to make this work, should come from blockProcessor. + final int MAX_GENERATION = 6; + if (blockReward.isZero()) { + return true; + } + final Wei coinbaseReward = blockReward.plus(blockReward.times(ommers.size()).dividedBy(32)); + final WorldUpdater updater = worldState.updater(); + final MutableAccount beneficiary = updater.getOrCreate(miningBeneficiary); + + beneficiary.incrementBalance(coinbaseReward); + for (final BlockHeader ommerHeader : ommers) { + if (ommerHeader.getNumber() - header.getNumber() > MAX_GENERATION) { + LOGGER.trace( + "Block processing error: ommer block number {} more than {} generations current block number {}", + ommerHeader.getNumber(), + MAX_GENERATION, + header.getNumber()); + return false; + } + + final MutableAccount ommerCoinbase = updater.getOrCreate(ommerHeader.getCoinbase()); + final long distance = header.getNumber() - ommerHeader.getNumber(); + final Wei ommerReward = blockReward.minus(blockReward.times(distance).dividedBy(8)); + ommerCoinbase.incrementBalance(ommerReward); + } + + updater.commit(); + + return true; + } + + protected abstract BlockHeader createFinalBlockHeader( + final SealableBlockHeader sealableBlockHeader); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AsyncBlockCreator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AsyncBlockCreator.java new file mode 100755 index 00000000000..b676fba879d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/AsyncBlockCreator.java @@ -0,0 +1,6 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +public interface AsyncBlockCreator extends BlockCreator { + + void cancel(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BaseBlockScheduler.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BaseBlockScheduler.java new file mode 100755 index 00000000000..6394fa9ef12 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BaseBlockScheduler.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.time.Clock; + +public abstract class BaseBlockScheduler { + + protected final Clock clock; + + public BaseBlockScheduler(final Clock clock) { + this.clock = clock; + } + + public long waitUntilNextBlockCanBeMined(final BlockHeader parentHeader) + throws InterruptedException { + final BlockCreationTimeResult result = getNextTimestamp(parentHeader); + + Thread.sleep(result.millisecondsUntilValid); + + return result.timestampForHeader; + } + + public abstract BlockCreationTimeResult getNextTimestamp(final BlockHeader parentHeader); + + public static class BlockCreationTimeResult { + + private final long timestampForHeader; + private final long millisecondsUntilValid; + + public BlockCreationTimeResult( + final long timestampForHeader, final long millisecondsUntilValid) { + this.timestampForHeader = timestampForHeader; + this.millisecondsUntilValid = millisecondsUntilValid; + } + + public long getTimestampForHeader() { + return timestampForHeader; + } + + public long getMillisecondsUntilValid() { + return millisecondsUntilValid; + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockCreator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockCreator.java new file mode 100755 index 00000000000..cc1dc48da44 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockCreator.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.core.Block; + +public interface BlockCreator { + + Block createBlock(final long timestamp); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockMiner.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockMiner.java new file mode 100755 index 00000000000..36750b16b57 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockMiner.java @@ -0,0 +1,102 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator.MinedBlockObserver; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.util.Subscribers; + +import java.util.concurrent.CancellationException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Responsible for creating a block, and importing it to the blockchain. This is specifically a + * mainnet capability (as IBFT would then use the block as part of a proposal round). + * + *

While the capability is largely functional, it has been wrapped in an object to allow it to be + * cancelled safely. + * + *

This class is responsible for mining a single block only - the AbstractBlockCreator maintains + * state so must be destroyed between block mining activities. + */ +public class BlockMiner implements Runnable { + + private static final Logger LOG = LogManager.getLogger(); + + private final AbstractBlockCreator blockCreator; + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final Subscribers observers; + private final BaseBlockScheduler scheduler; + private final BlockHeader parentHeader; + + public BlockMiner( + final AbstractBlockCreator blockCreator, + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final Subscribers observers, + final BaseBlockScheduler scheduler, + final BlockHeader parentHeader) { + this.blockCreator = blockCreator; + this.protocolContext = protocolContext; + this.protocolSchedule = protocolSchedule; + this.observers = observers; + this.scheduler = scheduler; + this.parentHeader = parentHeader; + } + + @Override + public void run() { + boolean blockMined = false; + while (!blockMined) { + try { + blockMined = mineBlock(); + } catch (final CancellationException ex) { + LOG.info("Block creation process cancelled."); + break; + } catch (final InterruptedException ex) { + LOG.error("Block mining was interrupted {}", ex); + } catch (final Exception ex) { + LOG.error("Blocking mining threw an exception {}", ex); + } + } + } + + private 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. + LOG.trace("Waiting for next block timestamp to be valid."); + long newBlockTimestamp = scheduler.waitUntilNextBlockCanBeMined(parentHeader); + + LOG.trace("Started a mining operation."); + Block block = blockCreator.createBlock(newBlockTimestamp); + LOG.info( + "Block created, importing to local chain, block includes {} transactions", + block.getBody().getTransactions().size()); + + final BlockImporter importer = + protocolSchedule.getByBlockNumber(block.getHeader().getNumber()).getBlockImporter(); + final boolean blockImported = + importer.importBlock(protocolContext, block, HeaderValidationMode.FULL); + if (blockImported) { + notifyNewBlockListeners(block); + LOG.trace("Block {} imported to block chain.", block.getHeader().getNumber()); + } else { + LOG.error("Illegal block mined, could not be imported to local chain."); + } + return blockImported; + } + + public void cancel() { + blockCreator.cancel(); + } + + private void notifyNewBlockListeners(final Block block) { + observers.forEach(obs -> obs.blockMined(block)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java new file mode 100755 index 00000000000..0529ceca8f2 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelector.java @@ -0,0 +1,194 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.PendingTransactions.TransactionSelectionResult; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockProcessor; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockProcessor.TransactionReceiptFactory; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; + +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.function.Supplier; + +import com.google.common.collect.Lists; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Responsible for extracting transactions from PendingTransactions and determining if the + * transaction is suitable for inclusion in the block defined by the provided + * ProcessableBlockHeader. + * + *

If a transaction is suitable for inclusion, the world state must be updated, and a receipt + * generated. + * + *

The output from this class's exeuction will be: + * + *

    + *
  • A list of transactions to include in the block being constructed. + *
  • A list of receipts for inclusion in the block. + *
  • The root hash of the world state at the completion of transaction execution. + *
  • The amount of gas consumed when executing all transactions. + *
+ * + * Once "used" this class must be discarded and another created. This class contains state which is + * not cleared between executions of buildTransactionListForBlock(). + */ +public class BlockTransactionSelector { + + private static final Logger LOGGER = LogManager.getLogger(MainnetBlockProcessor.class); + private final Wei minTransactionGasPrice; + + private static final double MIN_BLOCK_OCCUPANCY_RATIO = 0.8; + + public static class TransactionSelectionResults { + private final List transactions = Lists.newArrayList(); + private final List receipts = Lists.newArrayList(); + private long cumulativeGasUsed = 0; + + private void update( + final Transaction transaction, final TransactionReceipt receipt, final long gasUsed) { + transactions.add(transaction); + receipts.add(receipt); + cumulativeGasUsed += gasUsed; + } + + public List getTransactions() { + return transactions; + } + + public List getReceipts() { + return receipts; + } + + public long getCumulativeGasUsed() { + return cumulativeGasUsed; + } + } + + private final Supplier isCancelled; + private final TransactionProcessor transactionProcessor; + private final ProcessableBlockHeader processableBlockHeader; + private final Blockchain blockchain; + private final MutableWorldState worldState; + private final PendingTransactions pendingTransactions; + private final TransactionReceiptFactory transactionReceiptFactory; + private final Address miningBeneficiary; + + private final TransactionSelectionResults transactionSelectionResult = + new TransactionSelectionResults(); + + public BlockTransactionSelector( + final TransactionProcessor transactionProcessor, + final Blockchain blockchain, + final MutableWorldState worldState, + final PendingTransactions pendingTransactions, + final ProcessableBlockHeader processableBlockHeader, + final TransactionReceiptFactory transactionReceiptFactory, + final Wei minTransactionGasPrice, + final Supplier isCancelled, + final Address miningBeneficiary) { + this.transactionProcessor = transactionProcessor; + this.blockchain = blockchain; + this.worldState = worldState; + this.pendingTransactions = pendingTransactions; + this.processableBlockHeader = processableBlockHeader; + this.transactionReceiptFactory = transactionReceiptFactory; + this.isCancelled = isCancelled; + this.minTransactionGasPrice = minTransactionGasPrice; + this.miningBeneficiary = miningBeneficiary; + } + + /* + This function iterates over (potentially) all transactions in the PendingTransactions, this is a + long running process. + If running in a thread, it can be cancelled via the isCancelled supplier (which will result + in this throwing an CancellationException). + */ + public TransactionSelectionResults buildTransactionListForBlock() { + pendingTransactions.selectTransactions(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. + * + * This function will continue to be called until the block under construction is suitably + * full (in terms of gasLimit) and the provided transaction's gasLimit does not fit within + * the space remaining in the block. + * + */ + private TransactionSelectionResult evaluateTransaction(final Transaction transaction) { + if (isCancelled.get()) { + throw new CancellationException("Cancelled during transaction selection."); + } + + if (transactionTooLargeForBlock(transaction)) { + if (blockOccupancyAboveThreshold()) { + return TransactionSelectionResult.COMPLETE_OPERATION; + } else { + return TransactionSelectionResult.CONTINUE; + } + } + + // If the gas price specified by the transaction is less than this node is willing to accept, + // do not include it in the block. + if (minTransactionGasPrice.compareTo(transaction.getGasPrice()) > 0) { + return TransactionSelectionResult.DELETE_TRANSACTION_AND_CONTINUE; + } + + final WorldUpdater worldStateUpdater = worldState.updater(); + + final TransactionProcessor.Result result = + transactionProcessor.processTransaction( + blockchain, worldStateUpdater, processableBlockHeader, transaction, miningBeneficiary); + + if (!result.isInvalid()) { + worldStateUpdater.commit(); + updateTransactionResultTracking(transaction, result); + } else { + // Remove invalid transactions from the transaction pool but continue looking for valid ones + // as the block may not yet be full. + return TransactionSelectionResult.DELETE_TRANSACTION_AND_CONTINUE; + } + return TransactionSelectionResult.CONTINUE; + } + + /* + Responsible for updating the state maintained between transaction validation (i.e. receipts, + cumulative gas, world state root hash.). + */ + private void updateTransactionResultTracking( + final Transaction transaction, final TransactionProcessor.Result result) { + final long gasUsedByTransaction = transaction.getGasLimit() - result.getGasRemaining(); + final long cumulativeGasUsed = + transactionSelectionResult.cumulativeGasUsed + gasUsedByTransaction; + + transactionSelectionResult.update( + transaction, + transactionReceiptFactory.create(result, worldState, cumulativeGasUsed), + gasUsedByTransaction); + } + + private boolean transactionTooLargeForBlock(final Transaction transaction) { + final long blockGasRemaining = + processableBlockHeader.getGasLimit() - transactionSelectionResult.getCumulativeGasUsed(); + return (transaction.getGasLimit() > blockGasRemaining); + } + + private boolean blockOccupancyAboveThreshold() { + final double gasUsed = transactionSelectionResult.getCumulativeGasUsed(); + final double gasAvailable = processableBlockHeader.getGasLimit(); + + return (gasUsed / gasAvailable) >= MIN_BLOCK_OCCUPANCY_RATIO; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/CoinbaseNotSetException.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/CoinbaseNotSetException.java new file mode 100755 index 00000000000..acf898ba8c7 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/CoinbaseNotSetException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +public class CoinbaseNotSetException extends RuntimeException { + + public CoinbaseNotSetException(final String message) { + super(message); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockScheduler.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockScheduler.java new file mode 100755 index 00000000000..382075e4d1b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockScheduler.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.time.Clock; + +import java.util.concurrent.TimeUnit; + +import com.google.common.annotations.VisibleForTesting; + +public class DefaultBlockScheduler extends BaseBlockScheduler { + + private final long acceptableClockDriftSeconds; + private final long minimumSecondsSinceParent; + + public DefaultBlockScheduler( + final long minimumSecondsSinceParent, + final long acceptableClockDriftSeconds, + final Clock clock) { + super(clock); + this.acceptableClockDriftSeconds = acceptableClockDriftSeconds; + this.minimumSecondsSinceParent = minimumSecondsSinceParent; + } + + @Override + @VisibleForTesting + public BlockCreationTimeResult getNextTimestamp(final BlockHeader parentHeader) { + final long msSinceEpoch = clock.millisecondsSinceEpoch(); + final long secondsSinceEpoch = TimeUnit.SECONDS.convert(msSinceEpoch, TimeUnit.MILLISECONDS); + final long parentTimestamp = parentHeader.getTimestamp(); + + final long nextHeaderTimestamp = + Long.max(parentTimestamp + minimumSecondsSinceParent, secondsSinceEpoch); + + final long millisecondsUntilHeaderTimeStampIsValid = + (nextHeaderTimestamp * 1000) - (msSinceEpoch + (acceptableClockDriftSeconds * 1000)); + + return new BlockCreationTimeResult( + nextHeaderTimestamp, Math.max(0, millisecondsUntilHeaderTimeStampIsValid)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java new file mode 100755 index 00000000000..3bb2536a198 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMiner.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator.MinedBlockObserver; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.EthHashBlockCreator; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolution; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolverInputs; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.util.Subscribers; + +import java.util.Optional; + +/** + * Provides the EthHash specific aspects of the mining operation - i.e. getting the work definition, + * reporting the hashrate of the miner and accepting work submissions. + * + *

All other aspects of mining (i.e. pre-block delays, block creation and importing to the chain) + * are all conducted by the parent class. + */ +public class EthHashBlockMiner extends BlockMiner { + + private final EthHashBlockCreator blockCreator; + + public EthHashBlockMiner( + final EthHashBlockCreator blockCreator, + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final Subscribers observers, + final BaseBlockScheduler scheduler, + final BlockHeader parentHeader) { + super(blockCreator, protocolSchedule, protocolContext, observers, scheduler, parentHeader); + this.blockCreator = blockCreator; + } + + public Optional getWorkDefinition() { + return blockCreator.getWorkDefinition(); + } + + public Optional getHashesPerSecond() { + return blockCreator.getHashesPerSecond(); + } + + public boolean submitWork(final EthHashSolution solution) { + return blockCreator.submitWork(solution); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java new file mode 100755 index 00000000000..6f1125332fe --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutor.java @@ -0,0 +1,102 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator.MinedBlockObserver; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.EthHashBlockCreator; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolver; +import net.consensys.pantheon.ethereum.mainnet.EthHasher; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.util.Subscribers; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +public class EthHashMinerExecutor { + + private final ProtocolContext protocolContext; + private final ExecutorService executorService; + private final ProtocolSchedule protocolSchedule; + private final PendingTransactions pendingTransactions; + private volatile BytesValue extraData; + private volatile Optional

coinbase; + private volatile Wei minTransactionGasPrice; + private final BaseBlockScheduler blockScheduler; + + public EthHashMinerExecutor( + final ProtocolContext protocolContext, + final ExecutorService executorService, + final ProtocolSchedule protocolSchedule, + final PendingTransactions pendingTransactions, + final MiningParameters miningParams, + final BaseBlockScheduler blockScheduler) { + this.protocolContext = protocolContext; + this.executorService = executorService; + this.protocolSchedule = protocolSchedule; + this.pendingTransactions = pendingTransactions; + this.coinbase = miningParams.getCoinbase(); + this.extraData = miningParams.getExtraData(); + this.minTransactionGasPrice = miningParams.getMinTransactionGasPrice(); + this.blockScheduler = blockScheduler; + } + + public EthHashBlockMiner startAsyncMining( + final Subscribers observers, final BlockHeader parentHeader) { + 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); + executorService.execute(currentRunningMiner); + return currentRunningMiner; + } + } + + public void setExtraData(final BytesValue extraData) { + this.extraData = extraData.copy(); + } + + public void setCoinbase(final Address coinbase) { + if (coinbase == null) { + throw new IllegalArgumentException("Coinbase cannot be unset."); + } else { + this.coinbase = Optional.of(coinbase.copy()); + } + } + + public Optional
getCoinbase() { + return coinbase; + } + + public void setMinTransactionGasPrice(final Wei minTransactionGasPrice) { + this.minTransactionGasPrice = minTransactionGasPrice.copy(); + } + + public Wei getMinTransactionGasPrice() { + return minTransactionGasPrice; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGenerator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGenerator.java new file mode 100755 index 00000000000..91657681df7 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGenerator.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import java.util.Iterator; + +public class IncrementingNonceGenerator implements Iterable { + + private long nextValue; + + public IncrementingNonceGenerator(final long nextValue) { + this.nextValue = nextValue; + } + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Long next() { + return nextValue++; + } + }; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinator.java new file mode 100755 index 00000000000..13872d17bc0 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinator.java @@ -0,0 +1,146 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent.EventType; +import net.consensys.pantheon.ethereum.chain.BlockAddedObserver; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolution; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolverInputs; +import net.consensys.pantheon.util.Subscribers; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +/** + * Responsible for determining when a block mining operation should be started/stopped, then + * creating an appropriate miner and starting it running in a thread. + */ +public class MiningCoordinator implements BlockAddedObserver { + + private final Subscribers minedBlockObservers = new Subscribers<>(); + + private final EthHashMinerExecutor executor; + + private volatile Optional currentRunningMiner = Optional.empty(); + private volatile Optional cachedHashesPerSecond = Optional.empty(); + private boolean isEnabled = false; + private final Blockchain blockchain; + + public MiningCoordinator(final Blockchain blockchain, final EthHashMinerExecutor executor) { + this.executor = executor; + this.blockchain = blockchain; + this.blockchain.observeBlockAdded(this); + } + + public void enable() { + synchronized (this) { + if (isEnabled) { + return; + } + startAsyncMiningOperation(); + isEnabled = true; + } + } + + public void disable() { + synchronized (this) { + if (!isEnabled) { + return; + } + haltCurrentMiningOperation(); + isEnabled = false; + } + } + + public boolean isRunning() { + synchronized (this) { + return isEnabled; + } + } + + public void setCoinbase(final Address coinbase) { + executor.setCoinbase(coinbase); + } + + public Optional
getCoinbase() { + return executor.getCoinbase(); + } + + public void setMinTransactionGasPrice(final Wei minGasPrice) { + executor.setMinTransactionGasPrice(minGasPrice); + } + + public Wei getMinTransactionGasPrice() { + return executor.getMinTransactionGasPrice(); + } + + public void setExtraData(final BytesValue extraData) { + executor.setExtraData(extraData); + } + + public Optional hashesPerSecond() { + final Optional currentHashesPerSecond = + currentRunningMiner.flatMap(EthHashBlockMiner::getHashesPerSecond); + + if (currentHashesPerSecond.isPresent()) { + cachedHashesPerSecond = currentHashesPerSecond; + return currentHashesPerSecond; + } else { + return cachedHashesPerSecond; + } + } + + public Optional getWorkDefinition() { + return currentRunningMiner.flatMap(EthHashBlockMiner::getWorkDefinition); + } + + public boolean submitWork(final EthHashSolution solution) { + synchronized (this) { + return currentRunningMiner.map(miner -> miner.submitWork(solution)).orElse(false); + } + } + + @Override + public void onBlockAdded(final BlockAddedEvent event, final Blockchain blockchain) { + synchronized (this) { + if (isEnabled && shouldStartNewMiner(event)) { + haltCurrentMiningOperation(); + startAsyncMiningOperation(); + } + } + } + + private boolean shouldStartNewMiner(final BlockAddedEvent event) { + return event.getEventType() != EventType.FORK; + } + + private void startAsyncMiningOperation() { + final BlockHeader parentHeader = blockchain.getChainHeadHeader(); + currentRunningMiner = Optional.of(executor.startAsyncMining(minedBlockObservers, parentHeader)); + } + + private void haltCurrentMiningOperation() { + currentRunningMiner.ifPresent( + miner -> { + miner.cancel(); + miner.getHashesPerSecond().ifPresent(val -> cachedHashesPerSecond = Optional.of(val)); + }); + } + + public long addMinedBlockObserver(final MinedBlockObserver obs) { + return minedBlockObservers.subscribe(obs); + } + + public void removeMinedBlockObserver(final long id) { + minedBlockObservers.unsubscribe(id); + } + + public interface MinedBlockObserver { + + void blockMined(Block block); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningParameters.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningParameters.java new file mode 100755 index 00000000000..34ba9eea4f8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/MiningParameters.java @@ -0,0 +1,42 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +public class MiningParameters { + + private final Optional
coinbase; + private final Wei minTransactionGasPrice; + private final BytesValue extraData; + private final Boolean enabled; + + public MiningParameters( + final Address coinbase, + final Wei minTransactionGasPrice, + final BytesValue extraData, + final Boolean enabled) { + this.coinbase = Optional.ofNullable(coinbase); + this.minTransactionGasPrice = minTransactionGasPrice; + this.extraData = extraData; + this.enabled = enabled; + } + + public Optional
getCoinbase() { + return coinbase; + } + + public Wei getMinTransactionGasPrice() { + return minTransactionGasPrice; + } + + public BytesValue getExtraData() { + return extraData; + } + + public Boolean isMiningEnabled() { + return enabled; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/RandomNonceGenerator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/RandomNonceGenerator.java new file mode 100755 index 00000000000..709808db9c1 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/blockcreation/RandomNonceGenerator.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import net.consensys.pantheon.crypto.SecureRandomProvider; + +import java.util.Iterator; +import java.util.Random; + +/** Creates an everlasting random long value (for use in nonces). */ +public class RandomNonceGenerator implements Iterable { + + private final Random longGenerator; + + public RandomNonceGenerator() { + this.longGenerator = SecureRandomProvider.publicSecureRandom(); + } + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Long next() { + return longGenerator.nextLong(); + } + }; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedEvent.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedEvent.java new file mode 100755 index 00000000000..93128d3c3ee --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedEvent.java @@ -0,0 +1,66 @@ +package net.consensys.pantheon.ethereum.chain; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Transaction; + +import java.util.Collections; +import java.util.List; + +public class BlockAddedEvent { + + private final Block block; + private final List addedTransactions; + private final List removedTransactions; + private final EventType eventType; + + public enum EventType { + HEAD_ADVANCED, + FORK, + CHAIN_REORG + } + + private BlockAddedEvent( + final EventType eventType, + final Block block, + final List addedTransactions, + final List removedTransactions) { + this.eventType = eventType; + this.block = block; + this.addedTransactions = addedTransactions; + this.removedTransactions = removedTransactions; + } + + public static BlockAddedEvent createForHeadAdvancement(final Block block) { + return new BlockAddedEvent( + EventType.HEAD_ADVANCED, block, block.getBody().getTransactions(), Collections.emptyList()); + } + + public static BlockAddedEvent createForChainReorg( + final Block block, + final List addedTransactions, + final List removedTransactions) { + return new BlockAddedEvent( + EventType.CHAIN_REORG, block, addedTransactions, removedTransactions); + } + + public static BlockAddedEvent createForFork(final Block block) { + return new BlockAddedEvent( + EventType.FORK, block, Collections.emptyList(), Collections.emptyList()); + } + + public Block getBlock() { + return block; + } + + public EventType getEventType() { + return eventType; + } + + public List getAddedTransactions() { + return addedTransactions; + } + + public List getRemovedTransactions() { + return removedTransactions; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedObserver.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedObserver.java new file mode 100755 index 00000000000..4d697aab143 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/BlockAddedObserver.java @@ -0,0 +1,6 @@ +package net.consensys.pantheon.ethereum.chain; + +@FunctionalInterface +public interface BlockAddedObserver { + void onBlockAdded(BlockAddedEvent event, Blockchain blockchain); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/Blockchain.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/Blockchain.java new file mode 100755 index 00000000000..d0e4551ccf8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/Blockchain.java @@ -0,0 +1,163 @@ +package net.consensys.pantheon.ethereum.chain; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.Optional; + +/** An interface for reading data from the blockchain. */ +public interface Blockchain { + /** + * Return the head (latest block hash and total difficulty) of the local canonical blockchain. + * + * @return The head of the blockchain. + */ + ChainHead getChainHead(); + + /** + * Return the block number of the head of the canonical chain. + * + * @return The block number of the head of the chain. + */ + long getChainHeadBlockNumber(); + + /** + * Return the hash of the head of the canonical chain. + * + * @return The hash of the head of the chain. + */ + Hash getChainHeadHash(); + + /** @return the header for the current chain head */ + default BlockHeader getChainHeadHeader() { + return getBlockHeader(getChainHeadHash()) + .orElseThrow(() -> new IllegalStateException("Missing chain head header.")); + } + + default Block getChainHeadBlock() { + final Hash chainHeadHash = getChainHeadHash(); + final BlockHeader header = + getBlockHeader(chainHeadHash) + .orElseThrow(() -> new IllegalStateException("Missing chain head header.")); + final BlockBody body = + getBlockBody(chainHeadHash) + .orElseThrow(() -> new IllegalStateException("Missing chain head body.")); + return new Block(header, body); + } + + /** + * Checks whether the block corresponding to the given hash is on the canonical chain. + * + * @param blockHeaderHash The hash of the block to check. + * @return true if the block corresponding to the hash is on the canonical chain. + */ + default boolean blockIsOnCanonicalChain(final Hash blockHeaderHash) { + return getBlockHeader(blockHeaderHash) + .flatMap(h -> getBlockHashByNumber(h.getNumber())) + .map(h -> h.equals(blockHeaderHash)) + .orElse(false); + } + + /** + * Returns the block header corresponding to the given block number on the canonical chain. + * + * @param blockNumber The reference block number whose header we want to retrieve. + * @return The block header corresponding to this block number. + */ + Optional getBlockHeader(long blockNumber); + + /** + * Return true if the block corresponding the hash is present. + * + * @param blockHash The hash of the block to check. + * @return true if the block is tracked. + */ + default boolean contains(final Hash blockHash) { + return getBlockHeader(blockHash).isPresent(); + } + + /** + * Returns the block header corresponding to the given block hash. Associated block is not + * necessarily on the canonical chain. + * + * @param blockHeaderHash The hash of the block whose header we want to retrieve. + * @return The block header corresponding to this block hash. + */ + Optional getBlockHeader(Hash blockHeaderHash); + + /** + * Returns the block body corresponding to the given block header hash. Associated block is not + * necessarily on the canonical chain. + * + * @param blockHeaderHash The block header hash identifying the block whose body should be + * returned. + * @return The block body corresponding to the target block. + */ + Optional getBlockBody(Hash blockHeaderHash); + + /** + * Given a block's hash, returns the list of transaction receipts associated with this block's + * transactions. Associated block is not necessarily on the canonical chain. + * + * @param blockHeaderHash The header hash of the block we're querying. + * @return The transaction receipts corresponding to block hash. + */ + Optional> getTxReceipts(Hash blockHeaderHash); + + /** + * Retrieves the header hash of the block at the given height in the canonical chain. + * + * @param number The height of the block whose hash should be retrieved. + * @return The hash of the block at the given height. + */ + Optional getBlockHashByNumber(long number); + + /** + * Returns the total difficulty (cumulative difficulty up to and including the target block) of + * the block corresponding to the given hash. Associated block is not necessarily on the canonical + * chain. + * + * @param blockHeaderHash The hash of the block header being queried. + * @return The total difficulty of the corresponding block. + */ + Optional getTotalDifficultyByHash(Hash blockHeaderHash); + + /** + * Given a transaction hash, returns the location (block number and transaction index) of the + * hashed transaction on the canonical chain. + * + * @param transactionHash A transaction hash. + * @return The location of the hashed transaction. + */ + Optional getTransactionByHash(Hash transactionHash); + + /** + * @param transactionHash A transaction hash. + * @return The transaction location associated with the corresponding hash. + */ + Optional getTransactionLocation(Hash transactionHash); + + /** + * Adds an observer that will get called when a new block is added. + * + *

No guarantees are made about the order in which observers are invoked. + * + * @param observer the observer to call + * @return the observer ID that can be used to remove it later. + */ + long observeBlockAdded(BlockAddedObserver observer); + + /** + * Removes an previously added observer of any type. + * + * @param observerId the ID of the observer to remove + * @return {@code true} if the observer was removed; otherwise {@code false} + */ + boolean removeObserver(long observerId); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/ChainHead.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/ChainHead.java new file mode 100755 index 00000000000..a163a69e528 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/ChainHead.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.chain; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.uint.UInt256; + +/** Head of a blockchain. */ +public final class ChainHead { + + private final Hash hash; + + private final UInt256 totalDifficulty; + + public ChainHead(final Hash hash, final UInt256 totalDifficulty) { + this.hash = hash; + this.totalDifficulty = totalDifficulty; + } + + public Hash getHash() { + return hash; + } + + public UInt256 getTotalDifficulty() { + return totalDifficulty; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/GenesisConfig.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/GenesisConfig.java new file mode 100755 index 00000000000..3aaa2f6555f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/GenesisConfig.java @@ -0,0 +1,321 @@ +package net.consensys.pantheon.ethereum.chain; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.development.DevelopmentProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.worldstate.DefaultMutableWorldState; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.base.MoreObjects; +import com.google.common.io.Resources; +import io.vertx.core.json.JsonObject; + +public final class GenesisConfig { + + private static final BlockBody BODY = + new BlockBody(Collections.emptyList(), Collections.emptyList()); + private static final String MAINNET_FILE = "mainnet.json"; + + private final Block block; + private final int chainId; + private final ProtocolSchedule protocolSchedule; + private final List genesisAccounts; + + private GenesisConfig( + final Block block, + final int chainId, + final ProtocolSchedule protocolSchedule, + final List genesisAccounts) { + this.block = block; + this.chainId = chainId; + this.protocolSchedule = protocolSchedule; + this.genesisAccounts = genesisAccounts; + } + + public static GenesisConfig mainnet() { + try { + final JsonObject config = + new JsonObject( + Resources.toString(Resources.getResource(MAINNET_FILE), StandardCharsets.UTF_8)); + return GenesisConfig.fromConfig( + config, MainnetProtocolSchedule.fromConfig(config.getJsonObject("config"))); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } + } + + public static GenesisConfig development() { + try { + final JsonObject config = + new JsonObject( + Resources.toString(Resources.getResource("dev.json"), StandardCharsets.UTF_8)); + return GenesisConfig.fromConfig( + config, DevelopmentProtocolSchedule.create(config.getJsonObject("config"))); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Construct a {@link GenesisConfig} from a JSON string. + * + * @param json A JSON string describing the genesis block + * @param protocolSchedule A protocol Schedule associated with + * @param The consensus context type + * @return A new genesis block. + */ + public static GenesisConfig fromJson( + final String json, final ProtocolSchedule protocolSchedule) { + return fromConfig(new JsonObject(json), protocolSchedule); + } + + /** + * Construct a {@link GenesisConfig} from a JSON object. + * + * @param jsonConfig A {@link JsonObject} describing the genesis block. + * @param protocolSchedule A protocol Schedule associated with + * @param The consensus context type + * @return A new genesis block. + */ + @SuppressWarnings("unchecked") + public static GenesisConfig fromConfig( + final JsonObject jsonConfig, final ProtocolSchedule protocolSchedule) { + final Map definition = toNormalizedMap(jsonConfig); + final List genesisAccounts = + parseAllocations(definition).collect(Collectors.toList()); + final Block block = + new Block( + buildHeader(definition, calculateGenesisStateHash(genesisAccounts), protocolSchedule), + BODY); + + final Map config = + (Map) definition.getOrDefault("config", Collections.emptyMap()); + final int chainId = (int) config.getOrDefault("chainId", 1); + return new GenesisConfig<>(block, chainId, protocolSchedule, genesisAccounts); + } + + public Block getBlock() { + return block; + } + + public int getChainId() { + return chainId; + } + + /** + * Writes the genesis block's world state to the given {@link MutableWorldState}. + * + * @param target WorldView to write genesis state to + */ + public void writeStateTo(final MutableWorldState target) { + writeAccountsTo(target, genesisAccounts); + } + + private static void writeAccountsTo( + final MutableWorldState target, final List genesisAccounts) { + final WorldUpdater updater = target.updater(); + genesisAccounts.forEach( + account -> updater.getOrCreate(account.address).setBalance(account.balance)); + updater.commit(); + target.persist(); + } + + public ProtocolSchedule getProtocolSchedule() { + return protocolSchedule; + } + + private static Hash calculateGenesisStateHash(final List genesisAccounts) { + final MutableWorldState worldState = + new DefaultMutableWorldState( + new KeyValueStorageWorldStateStorage(new InMemoryKeyValueStorage())); + writeAccountsTo(worldState, genesisAccounts); + return worldState.rootHash(); + } + + private static BlockHeader buildHeader( + final Map genesis, + final Hash genesisRootHash, + final ProtocolSchedule protocolSchedule) { + + return BlockHeaderBuilder.create() + .parentHash(parseParentHash(genesis)) + .ommersHash(Hash.EMPTY_LIST_HASH) + .coinbase(parseCoinbase(genesis)) + .stateRoot(genesisRootHash) + .transactionsRoot(Hash.EMPTY_TRIE_HASH) + .receiptsRoot(Hash.EMPTY_TRIE_HASH) + .logsBloom(LogsBloomFilter.empty()) + .difficulty(parseDifficulty(genesis)) + .number(BlockHeader.GENESIS_BLOCK_NUMBER) + .gasLimit(parseGasLimit(genesis)) + .gasUsed(0L) + .timestamp(parseTimestamp(genesis)) + .extraData(parseExtraData(genesis)) + .mixHash(parseMixHash(genesis)) + .nonce(parseNonce(genesis)) + .blockHashFunction(ScheduleBasedBlockHashFunction.create(protocolSchedule)) + .buildBlockHeader(); + } + + /* Converts the {@link JsonObject} describing the Genesis Block to a {@link Map}. This method + * converts all nested {@link JsonObject} to {@link Map} as well. Also, note that all keys are + * converted to lowercase for easier lookup since the keys in a 'genesis.json' file are assumed + * case insensitive. + */ + private static Map toNormalizedMap(final JsonObject genesis) { + final Map normalized = new HashMap<>(); + genesis + .getMap() + .forEach( + (key, value) -> { + final String normalizedKey = key.toLowerCase(Locale.US); + if (value instanceof JsonObject) { + normalized.put(normalizedKey, toNormalizedMap((JsonObject) value)); + } else { + normalized.put(normalizedKey, value); + } + }); + return normalized; + } + + private static String getString( + final Map map, final String key, final String def) { + return entryAsString(map.getOrDefault(key.toLowerCase(Locale.US), def)); + } + + private static String getString(final Map map, final String key) { + final String keyy = key.toLowerCase(Locale.US); + if (!map.containsKey(keyy)) { + throw new IllegalArgumentException( + String.format("Invalid Genesis block configuration, missing value for '%s'", key)); + } + return entryAsString(map.get(keyy)); + } + + private static String entryAsString(final Object value) { + return ((CharSequence) value).toString(); + } + + private static long parseTimestamp(final Map genesis) { + return Long.parseLong(getString(genesis, "timestamp", "0x0").substring(2), 16); + } + + private static Address parseCoinbase(final Map genesis) { + final Address coinbase; + final String key = "coinbase"; + if (genesis.containsKey(key)) { + coinbase = Address.fromHexString(getString(genesis, key)); + } else { + coinbase = Address.wrap(BytesValue.wrap(new byte[Address.SIZE])); + } + return coinbase; + } + + private static Hash parseParentHash(final Map genesis) { + return Hash.wrap(Bytes32.fromHexString(getString(genesis, "parentHash", ""))); + } + + private static BytesValue parseExtraData(final Map genesis) { + return BytesValue.fromHexString(getString(genesis, "extraData", "")); + } + + private static UInt256 parseDifficulty(final Map genesis) { + return UInt256.fromHexString(getString(genesis, "difficulty")); + } + + private static long parseGasLimit(final Map genesis) { + return Long.decode(getString(genesis, "gasLimit")); + } + + private static Hash parseMixHash(final Map genesis) { + return Hash.wrap(Bytes32.fromHexString(getString(genesis, "mixHash", ""))); + } + + private static long parseNonce(final Map genesis) { + String nonce = getString(genesis, "nonce", "").toLowerCase(); + if (nonce.startsWith("0x")) { + nonce = nonce.substring(2); + } + return Long.parseUnsignedLong(nonce, 16); + } + + @SuppressWarnings("unchecked") + private static Stream parseAllocations(final Map genesis) { + final Map alloc = (Map) genesis.get("alloc"); + return alloc + .entrySet() + .stream() + .map( + entry -> { + final Address address = Address.fromHexString(entry.getKey()); + final String balance = getString((Map) entry.getValue(), "balance"); + return new GenesisAccount(address, balance); + }); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("block", block) + .add("chainId", chainId) + .add("protocolSchedule", protocolSchedule) + .add("genesisAccounts", genesisAccounts) + .toString(); + } + + private static final class GenesisAccount { + + final Address address; + final Wei balance; + + GenesisAccount(final Address address, final String balance) { + this.address = address; + this.balance = parseBalance(balance); + } + + private Wei parseBalance(final String balance) { + BigInteger val; + if (balance.startsWith("0x")) { + val = new BigInteger(1, BytesValue.fromHexStringLenient(balance).extractArray()); + } else { + val = new BigInteger(balance); + } + + return Wei.of(val); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("address", address) + .add("balance", balance) + .toString(); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/MutableBlockchain.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/MutableBlockchain.java new file mode 100755 index 00000000000..d3faf68bd1c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/MutableBlockchain.java @@ -0,0 +1,21 @@ +package net.consensys.pantheon.ethereum.chain; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; + +import java.util.List; + +public interface MutableBlockchain extends Blockchain { + + /** + * Adds a block to the blockchain. + * + *

Block must be connected to the existing blockchain (its parent must already be stored), + * otherwise an {@link IllegalArgumentException} is thrown. Blocks representing forks are allowed + * as long as they are connected. + * + * @param block The block to append. + * @param receipts The list of receipts associated with this block's transactions. + */ + void appendBlock(Block block, List receipts); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/TransactionLocation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/TransactionLocation.java new file mode 100755 index 00000000000..96c5beb2717 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/chain/TransactionLocation.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.ethereum.chain; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import java.util.Objects; + +/** + * Specifies the location of a transaction within the blockchain (where the transaction was included + * in the block and location within this block's transactions list). + */ +public class TransactionLocation { + + private final Hash blockHash; + private final int transactionIndex; + + public TransactionLocation(final Hash blockHash, final int transactionIndex) { + this.blockHash = blockHash; + this.transactionIndex = transactionIndex; + } + + public Hash getBlockHash() { + return blockHash; + } + + public int getTransactionIndex() { + return transactionIndex; + } + + public void writeTo(final RLPOutput out) { + out.startList(); + + out.writeBytesValue(blockHash); + out.writeIntScalar(transactionIndex); + + out.endList(); + } + + public static TransactionLocation readFrom(final RLPInput input) { + input.enterList(); + final TransactionLocation txLocation = + new TransactionLocation(Hash.wrap(input.readBytes32()), input.readIntScalar()); + input.leaveList(); + return txLocation; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TransactionLocation)) { + return false; + } + final TransactionLocation other = (TransactionLocation) obj; + return getTransactionIndex() == other.getTransactionIndex() + && getBlockHash().equals(other.getBlockHash()); + } + + @Override + public int hashCode() { + return Objects.hash(getBlockHash(), getTransactionIndex()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AbstractWorldUpdater.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AbstractWorldUpdater.java new file mode 100755 index 00000000000..8ae4b38c4e5 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AbstractWorldUpdater.java @@ -0,0 +1,378 @@ +package net.consensys.pantheon.ethereum.core; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import javax.annotation.Nullable; + +/** + * An abstract implementation of a {@link WorldUpdater} that buffers update over the {@link + * WorldView} provided in the constructor in memory. + * + *

Concrete implementation have to implement the {@link #commit()} method. + */ +public abstract class AbstractWorldUpdater + implements WorldUpdater { + + private final W world; + + private final Map> updatedAccounts = new HashMap<>(); + private final Set

deletedAccounts = new HashSet<>(); + + protected AbstractWorldUpdater(final W world) { + this.world = world; + } + + protected abstract A getForMutation(Address address); + + private UpdateTrackingAccount track(final UpdateTrackingAccount account) { + final Address address = account.getAddress(); + updatedAccounts.put(address, account); + deletedAccounts.remove(address); + return account; + } + + @Override + public MutableAccount createAccount(final Address address, final long nonce, final Wei balance) { + final UpdateTrackingAccount account = new UpdateTrackingAccount<>(address); + account.setNonce(nonce); + account.setBalance(balance); + return track(account); + } + + @Override + public Account get(final Address address) { + // We may have updated it already, so check that first. + final MutableAccount existing = updatedAccounts.get(address); + if (existing != null) { + return existing; + } + if (deletedAccounts.contains(address)) { + return null; + } + return world.get(address); + } + + @Override + public MutableAccount getMutable(final Address address) { + // We may have updated it already, so check that first. + final MutableAccount existing = updatedAccounts.get(address); + if (existing != null) { + return existing; + } + if (deletedAccounts.contains(address)) { + return null; + } + + // Otherwise, get it from our wrapped view and create a new update tracker. + final A origin = getForMutation(address); + if (origin == null) { + return null; + } else { + return track(new UpdateTrackingAccount<>(origin)); + } + } + + @Override + public void deleteAccount(final Address address) { + deletedAccounts.add(address); + updatedAccounts.remove(address); + } + + /** + * Creates an updater that buffer updates on top of this updater. + * + *

+ * + * @return a new updater on top of this updater. Updates made to the returned object will become + * visible on this updater when the returned updater is committed. Note however that updates + * to this updater may or may not be reflected to the created updater, so it is + * strongly advised to not update this updater until the returned one is discarded + * (either after having been committed, or because the updates it represent are meant to be + * discarded). + */ + @Override + public WorldUpdater updater() { + return new StackedUpdater<>(this); + } + + /** + * The world view on top of which this buffer updates. + * + * @return The world view on top of which this buffer updates. + */ + protected W wrappedWorldView() { + return world; + } + + /** + * The accounts modified in this updater. + * + * @return The accounts modified in this updater. + */ + protected Collection> updatedAccounts() { + return updatedAccounts.values(); + } + + /** + * The accounts deleted as part of this updater. + * + * @return The accounts deleted as part of this updater. + */ + protected Collection

deletedAccounts() { + return deletedAccounts; + } + + /** + * A implementation of {@link MutableAccount} that tracks updates made to the account since the + * creation of the updater this is linked to. + * + *

Note that in practice this only track the modified value of the nonce and balance, but + * doesn't remind if those were modified or not (the reason being that any modification of an + * account imply the underlying trie node will have to be updated, and so knowing if the nonce and + * balance where updated or not doesn't matter, we just need their new value). + */ + public static class UpdateTrackingAccount implements MutableAccount { + private final Address address; + + @Nullable private final A account; // null if this is a new account. + + private long nonce; + private Wei balance; + + @Nullable private BytesValue updatedCode; // Null if the underlying code has not been updated. + + // Only contains update storage entries, but may contains entry with a value of 0 to signify + // deletion. + private final SortedMap updatedStorage; + + UpdateTrackingAccount(final Address address) { + checkNotNull(address); + this.address = address; + this.account = null; + + this.nonce = 0; + this.balance = Wei.ZERO; + + this.updatedCode = BytesValue.EMPTY; + this.updatedStorage = new TreeMap<>(); + } + + UpdateTrackingAccount(final A account) { + checkNotNull(account); + + this.address = account.getAddress(); + this.account = account; + + this.nonce = account.getNonce(); + this.balance = account.getBalance(); + + this.updatedStorage = new TreeMap<>(); + } + + /** + * The original account over which this tracks updates. + * + * @return The original account over which this tracks updates, or {@code null} if this is a + * newly created account. + */ + public A getWrappedAccount() { + return account; + } + + /** + * Whether the code of the account was modified. + * + * @return {@code true} if the code was updated. + */ + public boolean codeWasUpdated() { + return updatedCode != null; + } + + /** + * A map of the storage entries that were modified. + * + * @return a map containing all entries that have been modified. This may contain entries + * with a value of 0 to signify deletion. + */ + @Override + public SortedMap getUpdatedStorage() { + return updatedStorage; + } + + @Override + public Address getAddress() { + return address; + } + + @Override + public long getNonce() { + return nonce; + } + + @Override + public void setNonce(final long value) { + this.nonce = value; + } + + @Override + public Wei getBalance() { + return balance; + } + + @Override + public void setBalance(final Wei value) { + this.balance = value; + } + + @Override + public BytesValue getCode() { + // Note that we set code for new account, so it's only null if account isn't. + return updatedCode == null ? account.getCode() : updatedCode; + } + + @Override + public boolean hasCode() { + // Note that we set code for new account, so it's only null if account isn't. + return updatedCode == null ? account.hasCode() : !updatedCode.isEmpty(); + } + + @Override + public void setCode(final BytesValue code) { + this.updatedCode = code; + } + + @Override + public UInt256 getStorageValue(final UInt256 key) { + final UInt256 value = updatedStorage.get(key); + if (value != null) { + return value; + } + + // We haven't updated the key-value yet, so either it's a new account and it doesn't have the + // key, or we should query the underlying storage for its existing value (which might be 0). + return account == null ? UInt256.ZERO : account.getStorageValue(key); + } + + @Override + public NavigableMap storageEntriesFrom( + final Bytes32 startKeyHash, final int limit) { + NavigableMap entries; + if (account != null) { + entries = account.storageEntriesFrom(startKeyHash, limit); + } else { + entries = new TreeMap<>(); + } + updatedStorage.forEach( + (key, value) -> { + final Hash hashedKey = Hash.hash(key.getBytes()); + if (hashedKey.compareTo(startKeyHash) >= 0) { + entries.put(hashedKey, value); + } + }); + + while (entries.size() > limit) { + entries.remove(entries.lastKey()); + } + return entries; + } + + @Override + public void setStorageValue(final UInt256 key, final UInt256 value) { + updatedStorage.put(key, value); + } + + @Override + public String toString() { + return String.format( + "%s -> {nonce: %s, balance:%s, code:%s, storage:%s }", + address, + nonce, + balance, + updatedCode == null ? "[not updated]" : updatedCode, + updatedStorage.isEmpty() ? "[not updated]" : updatedStorage); + } + } + + static class StackedUpdater + extends AbstractWorldUpdater, UpdateTrackingAccount> { + + protected StackedUpdater(final AbstractWorldUpdater world) { + super(world); + } + + @Override + protected UpdateTrackingAccount getForMutation(final Address address) { + final AbstractWorldUpdater wrapped = wrappedWorldView(); + final UpdateTrackingAccount wrappedTracker = wrapped.updatedAccounts.get(address); + if (wrappedTracker != null) { + return wrappedTracker; + } + if (wrapped.deletedAccounts.contains(address)) { + return null; + } + // The wrapped one isn't tracking that account. We're creating a tracking "for him" (but + // don't add him yet to his tracking map) because we need it to satisfy the type system. + // We will recognize this case in commit below and use that tracker "pay back" our + // allocation, so this isn't lost. + final A account = wrappedWorldView().getForMutation(address); + return account == null ? null : new UpdateTrackingAccount<>(account); + } + + @Override + public Collection getTouchedAccounts() { + return new ArrayList<>(updatedAccounts()); + } + + @Override + public void revert() { + deletedAccounts().clear(); + updatedAccounts().clear(); + } + + @Override + public void commit() { + final AbstractWorldUpdater wrapped = wrappedWorldView(); + // Our own updates should apply on top of the updates we're stacked on top, so our deletions + // may kill some of "their" updates, and our updates may review some of the account "they" + // deleted. + deletedAccounts().forEach(wrapped.updatedAccounts::remove); + updatedAccounts().forEach(a -> wrapped.deletedAccounts.remove(a.getAddressHash())); + + // Then push our deletes and updates to the stacked ones. + wrapped.deletedAccounts.addAll(deletedAccounts()); + + for (final UpdateTrackingAccount> update : updatedAccounts()) { + UpdateTrackingAccount existing = wrapped.updatedAccounts.get(update.getAddress()); + if (existing == null) { + // If we don't track this account, it's either a new one or getForMutation above had + // created a tracker to satisfy the type system above and we can reuse that now. + existing = update.getWrappedAccount(); + if (existing == null) { + // Brand new account, create our own version + existing = new UpdateTrackingAccount(update.address); + } + wrapped.updatedAccounts.put(existing.address, existing); + } + existing.setNonce(update.getNonce()); + existing.setBalance(update.getBalance()); + if (update.codeWasUpdated()) { + existing.setCode(update.getCode()); + } + update.getUpdatedStorage().forEach(existing::setStorageValue); + } + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Account.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Account.java new file mode 100755 index 00000000000..96d972004ff --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Account.java @@ -0,0 +1,116 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.NavigableMap; + +/** + * A world state account. + * + *

An account has four properties associated with it: + * + *

+ */ +public interface Account { + + /** + * The Keccak-256 hash of the account address. + * + *

Note that the world state does not store account addresses, only their hashes, and so this + * is how account are truly identified. So while accounts can be queried by their address (through + * first computing their hash), one cannot infer the address from an account simply from the world + * state. + * + * @return the Keccak-256 hash of the account address. + */ + default Hash getAddressHash() { + return Hash.hash(getAddress()); + } + + /** + * The account address. + * + * @return the account address + */ + Address getAddress(); + + /** + * The account nonce, that is the number of transactions sent from that account. + * + * @return the account nonce. + */ + long getNonce(); + + /** + * The available balance of that account. + * + * @return the balance, in Wei, of the account. + */ + Wei getBalance(); + + /** + * The EVM bytecode associated with this account. + * + * @return the account code (which can be empty). + */ + BytesValue getCode(); + + /** + * Whether the account has (non empty) EVM bytecode associated to it. + * + *

This is functionally equivalent to {@code !code().isEmpty()}, though could be implemented + * more efficiently. + * + * @return Whether the account has EVM bytecode associated to it. + */ + default boolean hasCode() { + return !getCode().isEmpty(); + } + + /** + * Retrieves a value in the account storage given its key. + * + * @param key the key to retrieve in the account storage. + * @return the value associated to {@code key} in the account storage. Note that this is never + * {@code null}, but 0 acts as a default value. + */ + UInt256 getStorageValue(UInt256 key); + + /** + * Whether the account is "empty". + * + *

An account is defined as empty if: + * + *

    + *
  • its nonce is 0; + *
  • its balance is 0; + *
  • its associated code is empty (the zero-length byte sequence). + *
+ * + * @return {@code true} if the account is empty with regard to the definition above, {@code false} + * otherwise. + */ + default boolean isEmpty() { + return getNonce() == 0 && getBalance().isZero() && !hasCode(); + } + + /** + * Retrieve up to {@code limit} storage entries beginning from the first entry with hash equal to + * or greater than {@code startKeyHash}. + * + * @param startKeyHash the first key hash to return. + * @param limit the maximum number of entries to return. + * @return the requested storage entries as a map of key hash to value. + */ + NavigableMap storageEntriesFrom(Bytes32 startKeyHash, int limit); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrder.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrder.java new file mode 100755 index 00000000000..3927e76ecf9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrder.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.ethereum.core; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Stream; + +public class AccountTransactionOrder { + + private static final Comparator SORT_BY_NONCE = + Comparator.comparing(Transaction::getNonce); + private final SortedSet transactionsForSender = new TreeSet<>(SORT_BY_NONCE); + private final SortedSet deferredTransactions = new TreeSet<>(SORT_BY_NONCE); + + public AccountTransactionOrder(final Stream senderTransactions) { + senderTransactions.forEach(this.transactionsForSender::add); + } + + /** + * Determine the transactions from this sender that are able to be processed given that + * nextTransactionInPriorityOrder has been reached in the normal priority order. + * + *

Transactions may be deferred from their place in normal priority order if the sender has + * other transactions in the pool with lower nonces. Deferred transactions are delayed until the + * transactions preceding them are reached. + * + * @param nextTransactionInPriorityOrder the next transaction to be processed in normal priority + * order. Must be from the sender this instance is ordering. + * @return the transactions from this sender that are now due to be processed, in order. + */ + public Iterable transactionsToProcess( + final Transaction nextTransactionInPriorityOrder) { + deferredTransactions.add(nextTransactionInPriorityOrder); + final List transactionsToApply = new ArrayList<>(); + while (!deferredTransactions.isEmpty() + && !transactionsForSender.isEmpty() + && deferredTransactions.first().equals(transactionsForSender.first())) { + final Transaction transaction = deferredTransactions.first(); + transactionsToApply.add(transaction); + deferredTransactions.remove(transaction); + transactionsForSender.remove(transaction); + } + return transactionsToApply; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Address.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Address.java new file mode 100755 index 00000000000..cba12a0974b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Address.java @@ -0,0 +1,118 @@ +package net.consensys.pantheon.ethereum.core; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.DelegatingBytesValue; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** A 160-bits account address. */ +public class Address extends DelegatingBytesValue { + + public static final int SIZE = 20; + + /** Specific addresses of the "precompiled" contracts. */ + public static final Address ECREC = Address.precompiled(1); + + public static final Address SHA256 = Address.precompiled(2); + public static final Address RIPEMD160 = Address.precompiled(3); + public static final Address ID = Address.precompiled(4); + public static final Address MODEXP = Address.precompiled(5); + public static final Address ALTBN128_ADD = Address.precompiled(6); + public static final Address ALTBN128_MUL = Address.precompiled(7); + public static final Address ALTBN128_PAIRING = Address.precompiled(8); + + protected Address(final BytesValue bytes) { + super(bytes); + checkArgument( + bytes.size() == SIZE, + "An account address must be be %s bytes long, got %s", + SIZE, + bytes.size()); + } + + public static Address wrap(final BytesValue value) { + return new Address(value); + } + + /** + * Creates an address from the given RLP-encoded input. + * + * @param input The input to read from + * @return the input's corresponding address + */ + public static Address readFrom(final RLPInput input) { + final BytesValue bytes = input.readBytesValue(); + if (bytes.size() != SIZE) { + throw new RLPException( + String.format("Address unexpected size of %s (needs %s)", bytes.size(), SIZE)); + } + return Address.wrap(bytes); + } + + /** + * @param hash A hash that has been obtained through hashing the return of the + * ECDSARECOVER function from Appendix F (Signing Transactions) of the Ethereum + * Yellow Paper. + * @return The ethereum address from the provided hash. + */ + public static Address extract(final Hash hash) { + return wrap(hash.slice(12, 20)); + } + + /** + * Parse an hexadecimal string representing an account address. + * + * @param str An hexadecimal string (with or without the leading '0x') representing a valid + * account address. + * @return The parsed address. + * @throws NullPointerException if the provided string is {@code null}. + * @throws IllegalArgumentException if the string is either not hexadecimal, or not the valid + * representation of an address. + */ + @JsonCreator + public static Address fromHexString(final String str) { + if (str == null) return null; + + return new Address(BytesValue.fromHexStringLenient(str, SIZE)); + } + + private static Address precompiled(final int value) { + // Keep it simple while we don't need precompiled above 127. + checkArgument(value < Byte.MAX_VALUE); + final byte[] address = new byte[SIZE]; + address[SIZE - 1] = (byte) value; + return new Address(BytesValue.wrap(address)); + } + + /** + * Address of the created contract. + * + *

This implement equation (86) in Section 7 of the Yellow Paper (rev. a91c29c). + * + * @param senderAddress the address of the transaction sender. + * @param nonce the nonce of this transaction. + * @return The generated address of the created contract. + */ + public static Address contractAddress(final Address senderAddress, final long nonce) { + return Address.extract( + Hash.hash( + RLP.encode( + out -> { + out.startList(); + out.writeBytesValue(senderAddress); + out.writeLongScalar(nonce); + out.endList(); + }))); + } + + @Override + public Address copy() { + final BytesValue copiedStorage = wrapped.copy(); + return Address.wrap(copiedStorage); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Block.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Block.java new file mode 100755 index 00000000000..1d5991ed661 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Block.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Objects; + +public class Block { + + private final BlockHeader header; + private final BlockBody body; + + public Block(final BlockHeader header, final BlockBody body) { + this.header = header; + this.body = body; + } + + public BlockHeader getHeader() { + return header; + } + + public BlockBody getBody() { + return body; + } + + public Hash getHash() { + return header.getHash(); + } + + public BytesValue toRlp() { + return RLP.encode(this::writeTo); + } + + public int calculateSize() { + return toRlp().size(); + } + + public void writeTo(final RLPOutput out) { + out.startList(); + + header.writeTo(out); + out.writeList(body.getTransactions(), Transaction::writeTo); + out.writeList(body.getOmmers(), BlockHeader::writeTo); + + out.endList(); + } + + public static Block readFrom(final RLPInput in, final BlockHashFunction hashFunction) { + in.enterList(); + final BlockHeader header = BlockHeader.readFrom(in, hashFunction); + final List transactions = in.readList(Transaction::readFrom); + final List ommers = in.readList(rlp -> BlockHeader.readFrom(rlp, hashFunction)); + in.leaveList(); + + return new Block(header, new BlockBody(transactions, ommers)); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Block)) { + return false; + } + final Block other = (Block) obj; + return header.equals(other.header) && body.equals(other.body); + } + + @Override + public int hashCode() { + return Objects.hash(header, body); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Block{"); + sb.append("header=").append(header).append(", "); + sb.append("body=").append(body); + return sb.append("}").toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockBody.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockBody.java new file mode 100755 index 00000000000..1ece9d6e40e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockBody.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class BlockBody { + + private static final BlockBody EMPTY = + new BlockBody(Collections.emptyList(), Collections.emptyList()); + + private final List transactions; + private final List ommers; + + public BlockBody(final List transactions, final List ommers) { + this.transactions = transactions; + this.ommers = ommers; + } + + public static BlockBody empty() { + return EMPTY; + } + + /** @return The list of transactions of the block. */ + public List getTransactions() { + return transactions; + } + + /** @return The list of ommers of the block. */ + public List getOmmers() { + return ommers; + } + + /** + * Writes Block to {@link RLPOutput}. + * + * @param output Output to write to + */ + public void writeTo(final RLPOutput output) { + output.startList(); + output.writeList(getTransactions(), Transaction::writeTo); + output.writeList(getOmmers(), BlockHeader::writeTo); + output.endList(); + } + + public static BlockBody readFrom( + final RLPInput input, final BlockHashFunction blockHashFunction) { + input.enterList(); + // TODO: Support multiple hard fork transaction formats. + final BlockBody body = + new BlockBody( + input.readList(Transaction::readFrom), + input.readList(rlp -> BlockHeader.readFrom(rlp, blockHashFunction))); + input.leaveList(); + return body; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof BlockBody)) { + return false; + } + final BlockBody other = (BlockBody) obj; + return transactions.equals(other.transactions) && ommers.equals(other.ommers); + } + + @Override + public int hashCode() { + return Objects.hash(transactions, ommers); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("BlockBody{"); + sb.append("transactions=").append(transactions).append(", "); + sb.append("ommers=").append(ommers); + return sb.append("}").toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHashFunction.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHashFunction.java new file mode 100755 index 00000000000..fb10dce14bb --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHashFunction.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.core; + +/** + * An interface for creating the block hash given a {@link BlockHeader}. + * + *

The algorithm to create the block hash may vary depending on the consensus mechanism used by + * the chain. + */ +@FunctionalInterface +public interface BlockHashFunction { + + /** + * Create the hash for a given BlockHeader. + * + * @param header the header to create the block hash from + * @return a {@link Hash} containing the block hash. + */ + Hash apply(BlockHeader header); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeader.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeader.java new file mode 100755 index 00000000000..570a4961f27 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeader.java @@ -0,0 +1,180 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Objects; + +/** A mined Ethereum block header. */ +public class BlockHeader extends SealableBlockHeader { + + public static final int MAX_EXTRA_DATA_BYTES = 32; + + public static final long GENESIS_BLOCK_NUMBER = 0L; + + private final Hash mixHash; + + private final long nonce; + private final BlockHashFunction hashFunction; + + private Hash hash; + + public BlockHeader( + final Hash parentHash, + final Hash ommersHash, + final Address coinbase, + final Hash stateRoot, + final Hash transactionsRoot, + final Hash receiptsRoot, + final LogsBloomFilter logsBloom, + final UInt256 difficulty, + final long number, + final long gasLimit, + final long gasUsed, + final long timestamp, + final BytesValue extraData, + final Hash mixHash, + final long nonce, + final BlockHashFunction hashFunction) { + super( + parentHash, + ommersHash, + coinbase, + stateRoot, + transactionsRoot, + receiptsRoot, + logsBloom, + difficulty, + number, + gasLimit, + gasUsed, + timestamp, + extraData); + this.mixHash = mixHash; + this.nonce = nonce; + this.hashFunction = hashFunction; + } + + /** + * Returns the block mixed hash. + * + * @return the block mixed hash + */ + public Hash getMixHash() { + return mixHash; + } + + /** + * Returns the block nonce. + * + * @return the block nonce + */ + public long getNonce() { + return nonce; + } + + /** + * Returns the block header hash. + * + * @return the block header hash + */ + public Hash getHash() { + if (hash == null) { + hash = hashFunction.apply(this); + } + return hash; + } + + /** + * Write an RLP representation. + * + * @param out The RLP output to write to + */ + public void writeTo(final RLPOutput out) { + out.startList(); + + out.writeBytesValue(parentHash); + out.writeBytesValue(ommersHash); + out.writeBytesValue(coinbase); + out.writeBytesValue(stateRoot); + out.writeBytesValue(transactionsRoot); + out.writeBytesValue(receiptsRoot); + out.writeBytesValue(logsBloom.getBytes()); + out.writeUInt256Scalar(difficulty); + out.writeLongScalar(number); + out.writeLongScalar(gasLimit); + out.writeLongScalar(gasUsed); + out.writeLongScalar(timestamp); + out.writeBytesValue(extraData); + out.writeBytesValue(mixHash); + out.writeLong(nonce); + + out.endList(); + } + + public static BlockHeader readFrom(final RLPInput input, final BlockHashFunction hashFunction) { + input.enterList(); + final BlockHeader header = + new BlockHeader( + Hash.wrap(input.readBytes32()), + Hash.wrap(input.readBytes32()), + Address.readFrom(input), + Hash.wrap(input.readBytes32()), + Hash.wrap(input.readBytes32()), + Hash.wrap(input.readBytes32()), + LogsBloomFilter.readFrom(input), + input.readUInt256Scalar(), + input.readLongScalar(), + input.readLongScalar(), + input.readLongScalar(), + input.readLongScalar(), + input.readBytesValue(), + Hash.wrap(input.readBytes32()), + input.readLong(), + hashFunction); + input.leaveList(); + return header; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof BlockHeader)) { + return false; + } + final BlockHeader other = (BlockHeader) obj; + return getHash().equals(other.getHash()); + } + + @Override + public int hashCode() { + return Objects.hash(getHash()); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("BlockHeader{"); + sb.append("hash=").append(getHash()).append(", "); + sb.append("parentHash=").append(parentHash).append(", "); + sb.append("ommersHash=").append(ommersHash).append(", "); + sb.append("coinbase=").append(coinbase).append(", "); + sb.append("stateRoot=").append(stateRoot).append(", "); + sb.append("transactionsRoot=").append(transactionsRoot).append(", "); + sb.append("receiptsRoot=").append(receiptsRoot).append(", "); + sb.append("logsBloom=").append(logsBloom).append(", "); + sb.append("difficulty=").append(difficulty).append(", "); + sb.append("number=").append(number).append(", "); + sb.append("gasLimit=").append(gasLimit).append(", "); + sb.append("gasUsed=").append(gasUsed).append(", "); + sb.append("timestamp=").append(timestamp).append(", "); + sb.append("extraData=").append(extraData).append(", "); + sb.append("mixHash=").append(mixHash).append(", "); + sb.append("nonce=").append(nonce); + return sb.append("}").toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeaderBuilder.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeaderBuilder.java new file mode 100755 index 00000000000..80ac6477589 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockHeaderBuilder.java @@ -0,0 +1,252 @@ +package net.consensys.pantheon.ethereum.core; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.time.Instant; +import java.util.OptionalLong; + +/** A utility class for building block headers. */ +public class BlockHeaderBuilder { + + private Hash parentHash; + + private Hash ommersHash; + + private Address coinbase; + + private Hash stateRoot; + + private Hash transactionsRoot; + + private Hash receiptsRoot; + + private LogsBloomFilter logsBloom; + + private UInt256 difficulty; + + private long number = -1L; + + private long gasLimit = -1L; + + private long gasUsed = -1L; + + private long timestamp = -1L; + + private BytesValue extraData; + + private Hash mixHash; + + private BlockHashFunction blockHashFunction; + + // A nonce can be any value so we use the OptionalLong + // instead of an invalid identifier such as -1. + private OptionalLong nonce = OptionalLong.empty(); + + public static BlockHeaderBuilder create() { + return new BlockHeaderBuilder(); + } + + public BlockHeader buildBlockHeader() { + validateBlockHeader(); + + return new BlockHeader( + parentHash, + ommersHash, + coinbase, + stateRoot, + transactionsRoot, + receiptsRoot, + logsBloom, + difficulty, + number, + gasLimit, + gasUsed, + timestamp < 0 ? Instant.now().getEpochSecond() : timestamp, + extraData, + mixHash, + nonce.getAsLong(), + blockHashFunction); + } + + public ProcessableBlockHeader buildProcessableBlockHeader() { + validateProcessableBlockHeader(); + + return new ProcessableBlockHeader( + parentHash, coinbase, difficulty, number, gasLimit, timestamp); + } + + public SealableBlockHeader buildSealableBlockHeader() { + validateSealableBlockHeader(); + + return new SealableBlockHeader( + parentHash, + ommersHash, + coinbase, + stateRoot, + transactionsRoot, + receiptsRoot, + logsBloom, + difficulty, + number, + gasLimit, + gasUsed, + timestamp, + extraData); + } + + private void validateBlockHeader() { + validateSealableBlockHeader(); + checkState(this.mixHash != null, "Missing mixHash"); + checkState(this.nonce.isPresent(), "Missing nonce"); + checkState(this.blockHashFunction != null, "Missing blockHashFunction"); + } + + private void validateProcessableBlockHeader() { + checkState(this.parentHash != null, "Missing parent hash"); + checkState(this.coinbase != null, "Missing coinbase"); + checkState(this.difficulty != null, "Missing block difficulty"); + checkState(this.number > -1L, "Missing block number"); + checkState(this.gasLimit > -1L, "Missing gas limit"); + checkState(this.timestamp > -1L, "Missing timestamp"); + } + + private void validateSealableBlockHeader() { + validateProcessableBlockHeader(); + checkState(this.ommersHash != null, "Missing ommers hash"); + checkState(this.stateRoot != null, "Missing state root"); + checkState(this.transactionsRoot != null, "Missing transaction root"); + checkState(this.receiptsRoot != null, "Missing receipts root"); + checkState(this.logsBloom != null, "Missing logs bloom filter"); + checkState(this.gasUsed > -1L, "Missing gas used"); + checkState(this.extraData != null, "Missing extra data field"); + } + + public BlockHeaderBuilder populateFrom(final ProcessableBlockHeader processableBlockHeader) { + checkNotNull(processableBlockHeader); + parentHash(processableBlockHeader.getParentHash()); + coinbase(processableBlockHeader.getCoinbase()); + difficulty(processableBlockHeader.getDifficulty()); + number(processableBlockHeader.getNumber()); + gasLimit(processableBlockHeader.getGasLimit()); + timestamp(processableBlockHeader.getTimestamp()); + return this; + } + + public BlockHeaderBuilder populateFrom(final SealableBlockHeader sealableBlockHeader) { + checkNotNull(sealableBlockHeader); + parentHash(sealableBlockHeader.getParentHash()); + ommersHash(sealableBlockHeader.getOmmersHash()); + coinbase(sealableBlockHeader.getCoinbase()); + stateRoot(sealableBlockHeader.getStateRoot()); + transactionsRoot(sealableBlockHeader.getTransactionsRoot()); + receiptsRoot(sealableBlockHeader.getReceiptsRoot()); + logsBloom(sealableBlockHeader.getLogsBloom()); + difficulty(sealableBlockHeader.getDifficulty()); + number(sealableBlockHeader.getNumber()); + gasLimit(sealableBlockHeader.getGasLimit()); + gasUsed(sealableBlockHeader.getGasUsed()); + timestamp(sealableBlockHeader.getTimestamp()); + extraData(sealableBlockHeader.getExtraData()); + return this; + } + + public BlockHeaderBuilder parentHash(final Hash hash) { + checkNotNull(hash); + this.parentHash = hash; + return this; + } + + public BlockHeaderBuilder ommersHash(final Hash hash) { + checkNotNull(hash); + this.ommersHash = hash; + return this; + } + + public BlockHeaderBuilder coinbase(final Address address) { + checkNotNull(address); + this.coinbase = address; + return this; + } + + public BlockHeaderBuilder stateRoot(final Hash hash) { + checkNotNull(hash); + this.stateRoot = hash; + return this; + } + + public BlockHeaderBuilder transactionsRoot(final Hash hash) { + checkNotNull(hash); + this.transactionsRoot = hash; + return this; + } + + public BlockHeaderBuilder receiptsRoot(final Hash hash) { + checkNotNull(hash); + this.receiptsRoot = hash; + return this; + } + + public BlockHeaderBuilder logsBloom(final LogsBloomFilter filter) { + checkNotNull(filter); + this.logsBloom = filter; + return this; + } + + public BlockHeaderBuilder difficulty(final UInt256 difficulty) { + checkNotNull(difficulty); + this.difficulty = difficulty; + return this; + } + + public BlockHeaderBuilder number(final long number) { + checkArgument(number >= 0L); + this.number = number; + return this; + } + + public BlockHeaderBuilder gasLimit(final long gasLimit) { + checkArgument(gasLimit >= 0L); + this.gasLimit = gasLimit; + return this; + } + + public BlockHeaderBuilder gasUsed(final long gasUsed) { + checkArgument(gasUsed > -1L); + this.gasUsed = gasUsed; + return this; + } + + public BlockHeaderBuilder timestamp(final long timestamp) { + checkArgument(timestamp >= 0); + this.timestamp = timestamp; + return this; + } + + public BlockHeaderBuilder extraData(final BytesValue data) { + checkNotNull(data); + + this.extraData = data; + return this; + } + + public BlockHeaderBuilder mixHash(final Hash mixHash) { + checkNotNull(mixHash); + this.mixHash = mixHash; + return this; + } + + public BlockHeaderBuilder nonce(final long nonce) { + this.nonce = OptionalLong.of(nonce); + return this; + } + + public BlockHeaderBuilder blockHashFunction(final BlockHashFunction blockHashFunction) { + this.blockHashFunction = blockHashFunction; + return this; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockImporter.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockImporter.java new file mode 100755 index 00000000000..cb1fa3963e6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockImporter.java @@ -0,0 +1,45 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; + +import java.util.List; + +/** + * An interface for a block importer. + * + *

The block importer is responsible for assessing whether a candidate block can be added to a + * given blockchain given the block history and its corresponding state. If the block is able to be + * successfully added, the corresponding blockchain and world state will be updated as well. + */ +public interface BlockImporter { + + /** + * Attempts to import the given block to the specificed blockchain and world state. + * + * @param context The context to attempt to update + * @param block The block + * @param headerValidationMode Determines the validation to perform on this header. + * @return {@code true} if the block was added somewhere in the blockchain; otherwise {@code + * false} + */ + boolean importBlock( + ProtocolContext context, Block block, HeaderValidationMode headerValidationMode); + + /** + * Attempts to import the given block. Uses "fast" validation. Performs light validation using the + * block's receipts rather than processing all transactions and fully validating world state. + * + * @param context The context to attempt to update + * @param block The block + * @param receipts The receipts associated with this block. + * @param headerValidationMode Determines the validation to perform on this header. + * @return {@code true} if the block was added somewhere in the blockchain; otherwise {@code + * false} + */ + boolean fastImportBlock( + ProtocolContext context, + Block block, + List receipts, + HeaderValidationMode headerValidationMode); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockMetadata.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockMetadata.java new file mode 100755 index 00000000000..0c4e3ec5a76 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/BlockMetadata.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +public class BlockMetadata { + private static final BlockMetadata EMPTY = new BlockMetadata(null); + private final UInt256 totalDifficulty; + + public BlockMetadata(final UInt256 totalDifficulty) { + this.totalDifficulty = totalDifficulty; + } + + public static BlockMetadata empty() { + return EMPTY; + } + + public static BlockMetadata fromRlp(final BytesValue bytes) { + return readFrom(RLP.input(bytes)); + } + + public static BlockMetadata readFrom(final RLPInput in) throws RLPException { + in.enterList(); + + final UInt256 totalDifficulty = in.readUInt256Scalar(); + + in.leaveList(); + + return new BlockMetadata(totalDifficulty); + } + + public UInt256 getTotalDifficulty() { + return totalDifficulty; + } + + public BytesValue toRlp() { + return RLP.encode(this::writeTo); + } + + public void writeTo(final RLPOutput out) { + out.startList(); + + out.writeUInt256Scalar(totalDifficulty); + + out.endList(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Gas.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Gas.java new file mode 100755 index 00000000000..7becc866ff8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Gas.java @@ -0,0 +1,140 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import javax.annotation.concurrent.Immutable; + +import com.google.common.primitives.Longs; + +/** A particular quantity of Gas as used by the Ethereum VM. */ +@Immutable +public final class Gas { + + private final long value; + + public static final Gas ZERO = of(0); + + public static final Gas MAX_VALUE = Gas.of(Long.MAX_VALUE); + + private static final BigInteger MAX_VALUE_BIGINT = BigInteger.valueOf(Long.MAX_VALUE); + + protected Gas(final long value) { + this.value = value; + } + + public static Gas of(final long value) { + return new Gas(value); + } + + public static Gas of(final BigInteger value) { + if (value.compareTo(MAX_VALUE_BIGINT) > 0) { + return MAX_VALUE; + } + + return of(value.longValue()); + } + + public static Gas of(final UInt256 value) { + if (value.fitsLong()) { + return of(value.toLong()); + } else { + return MAX_VALUE; + } + } + + public static Gas of(final Bytes32 value) { + return Gas.of(UInt256.wrap(value)); + } + + public static Gas fromHexString(final String str) { + try { + final long value = Long.decode(str); + return Gas.of(value); + } catch (final NumberFormatException e) { + return MAX_VALUE; + } + } + + /** + * The price of this amount of gas given the provided price per unit of gas. + * + * @param gasPrice The price per unit of gas. + * @return The price of this amount of gas for a per unit of gas price of {@code gasPrice}. + */ + public Wei priceFor(final Wei gasPrice) { + return gasPrice.times(Wei.of(value)); + } + + public Gas max(final Gas other) { + return of(Long.max(value, other.value)); + } + + public Gas min(final Gas other) { + return of(Long.min(value, other.value)); + } + + public Gas dividedBy(final long other) { + return Gas.of(value / other); + } + + public Gas plus(final Gas amount) { + try { + return of(Math.addExact(value, amount.value)); + } catch (final ArithmeticException e) { + return MAX_VALUE; + } + } + + public Gas minus(final Gas amount) { + return of(value - amount.value); + } + + public Gas times(final Gas amount) { + try { + return of(Math.multiplyExact(value, amount.value)); + } catch (final ArithmeticException e) { + return MAX_VALUE; + } + } + + public Gas times(final long amount) { + return times(Gas.of(amount)); + } + + public UInt256 asUInt256() { + return UInt256.of(value); + } + + public int compareTo(final Gas other) { + return Long.compare(value, other.value); + } + + public byte[] getBytes() { + return Longs.toByteArray(value); + } + + public long toLong() { + return value; + } + + @Override + public int hashCode() { + return Longs.hashCode(value); + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof Gas)) { + return false; + } + final Gas other = (Gas) obj; + return value == other.value; + } + + @Override + public String toString() { + return Long.toString(value); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Hash.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Hash.java new file mode 100755 index 00000000000..578195eec90 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Hash.java @@ -0,0 +1,53 @@ +package net.consensys.pantheon.ethereum.core; + +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.DelegatingBytes32; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** A 32-bytes hash value as used in Ethereum blocks, that is the result of the KEC algorithm. */ +public class Hash extends DelegatingBytes32 { + + public static final Hash ZERO = new Hash(Bytes32.ZERO); + + public static final Hash EMPTY_TRIE_HASH = Hash.hash(RLP.NULL); + + public static final Hash EMPTY_LIST_HASH = Hash.hash(RLP.EMPTY_LIST); + + public static final Hash EMPTY = hash(BytesValue.EMPTY); + + private Hash(final Bytes32 bytes) { + super(bytes); + } + + public static Hash hash(final BytesValue value) { + return new Hash(keccak256(value)); + } + + public static Hash wrap(final Bytes32 bytes) { + return new Hash(bytes); + } + + /** + * Parse an hexadecimal string representing a hash value. + * + * @param str An hexadecimal string (with or without the leading '0x') representing a valid hash + * value. + * @return The parsed hash. + * @throws NullPointerException if the provided string is {@code null}. + * @throws IllegalArgumentException if the string is either not hexadecimal, or not the valid + * representation of a hash (not 32 bytes). + */ + @JsonCreator + public static Hash fromHexString(final String str) { + return new Hash(Bytes32.fromHexStringStrict(str)); + } + + public static Hash fromHexStringLenient(final String str) { + return new Hash(Bytes32.fromHexStringLenient(str)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Log.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Log.java new file mode 100755 index 00000000000..d9a29e4ef02 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Log.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Objects; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; + +/** + * A log entry is a tuple of a logger’s address (the address of the contract that added the logs), a + * series of 32-bytes log topics, and some number of bytes of data. + */ +public class Log { + + private final Address logger; + private final BytesValue data; + private final ImmutableList topics; + + /** + * @param logger The address of the contract that produced this log. + * @param data Data associated with this log. + * @param topics Indexable topics associated with this log. + */ + public Log(final Address logger, final BytesValue data, final List topics) { + this.logger = logger; + this.data = data; + this.topics = ImmutableList.copyOf(topics); + } + + /** + * Writes the log entry to the provided RLP output. + * + * @param out the output in which to encode the log entry. + */ + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeBytesValue(logger); + out.writeList(topics, LogTopic::writeTo); + out.writeBytesValue(data); + out.endList(); + } + + /** + * Reads the log entry from the provided RLP input. + * + * @param in the input from which to decode the log entry. + * @return the read log entry. + */ + public static Log readFrom(final RLPInput in) { + in.enterList(); + final Address logger = Address.wrap(in.readBytesValue()); + final List topics = in.readList(LogTopic::readFrom); + final BytesValue data = in.readBytesValue(); + in.leaveList(); + return new Log(logger, data, topics); + } + + public Address getLogger() { + return logger; + } + + public BytesValue getData() { + return data; + } + + public List getTopics() { + return topics; + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof Log)) return false; + + // Compare data + final Log that = (Log) other; + return this.data.equals(that.data) + && this.logger.equals(that.logger) + && this.topics.equals(that.topics); + } + + @Override + public int hashCode() { + return Objects.hash(data, logger, topics); + } + + @Override + public String toString() { + final String joinedTopics = Joiner.on("\n").join(topics); + return String.format("Data: %s\nLogger: %s\nTopics: %s", data, logger, joinedTopics); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogSeries.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogSeries.java new file mode 100755 index 00000000000..97b2883ddde --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogSeries.java @@ -0,0 +1,84 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A series of {@link Log} entries accrued during the execution of a transaction. + * + *

Note that this class is essentially a list of {@link Log} with maintain a bloom filter of + * those logs. Note however that while it is a mutable list, only additions are allowed: trying to + * set or remove an element will throw {@link UnsupportedOperationException} (as we cannot update + * the bloom filter properly on such operations). + */ +public class LogSeries extends AbstractList { + + public static LogSeries empty() { + return new LogSeries(new ArrayList<>()); + } + + private final List series; + private final LogsBloomFilter bloomFilter; + + public LogSeries(final Collection logs) { + this.series = new ArrayList<>(logs); + this.bloomFilter = new LogsBloomFilter(); + + logs.forEach(bloomFilter::insertLog); + } + + /** + * The bloom filter composed of the content of this log series. + * + * @return the log series bloom filter. + */ + public LogsBloomFilter getBloomFilter() { + return bloomFilter; + } + + /** + * Writes the log series to the provided RLP output. + * + * @param out the output in which to encode the log series. + */ + public void writeTo(final RLPOutput out) { + out.writeList(series, Log::writeTo); + } + + /** + * Reads the log series from the provided RLP input. + * + * @param in the input from which to decode the log series. + * @return the read logs series. + */ + public static LogSeries readFrom(final RLPInput in) { + final List logs = in.readList(Log::readFrom); + return new LogSeries(logs); + } + + @Override + public Log get(final int i) { + return series.get(i); + } + + @Override + public int size() { + return series.size(); + } + + @Override + public void add(final int index, final Log log) { + series.add(index, log); + bloomFilter.insertLog(log); + } + + @Override + public void clear() { + series.clear(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogTopic.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogTopic.java new file mode 100755 index 00000000000..5ce5e9272e4 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogTopic.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.ethereum.core; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.DelegatingBytesValue; + +public class LogTopic extends DelegatingBytesValue { + + public static final int SIZE = 32; + + private LogTopic(final BytesValue bytes) { + super(bytes); + checkArgument( + bytes.size() == SIZE, "A log topic must be be %s bytes long, got %s", SIZE, bytes.size()); + } + + public static LogTopic create(final BytesValue bytes) { + return new LogTopic(bytes); + } + + public static LogTopic wrap(final BytesValue bytes) { + return new LogTopic(bytes); + } + + public static LogTopic of(final BytesValue bytes) { + return new LogTopic(bytes.copy()); + } + + public static LogTopic fromHexString(final String str) { + return new LogTopic(BytesValue.fromHexString(str)); + } + + /** + * Reads the log topic from the provided RLP input. + * + * @param in the input from which to decode the log topic. + * @return the read log topic. + */ + public static LogTopic readFrom(final RLPInput in) { + return new LogTopic(in.readBytesValue()); + } + + /** + * Writes the log topic to the provided RLP output. + * + * @param out the output in which to encode the log topic. + */ + public void writeTo(final RLPOutput out) { + out.writeBytesValue(this); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogsBloomFilter.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogsBloomFilter.java new file mode 100755 index 00000000000..4f7b040dc4f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/LogsBloomFilter.java @@ -0,0 +1,140 @@ +package net.consensys.pantheon.ethereum.core; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.util.Collection; + +/* + * Bloom filter implementation for storing persistent logs, describes a 2048-bit representation of + * all log entries of a transaction, except data. Sets the bits of the 2048 byte array, where + * indices are given by: The lower order 11-bits, of the first three double-bytes, of the SHA3, of + * each value. For instance the address "0x0F572E5295C57F15886F9B263E2F6D2D6C7B5EC6" results in the + * KECCAK256 hash "bd2b01afcd27800b54d2179edc49e2bffde5078bb6d0b204694169b1643fb108", of which the + * corresponding double-bytes are: bd2b, 01af, cd27, corresponding to the following bits in the + * bloom filter: 1323, 431, 1319 + */ +public class LogsBloomFilter { + + public static final int BYTE_SIZE = 256; + private static final int LEAST_SIGNIFICANT_BYTE = 0xFF; + private static final int LEAST_SIGNIFICANT_THREE_BITS = 0x7; + private static final int BITS_IN_BYTE = 8; + + private final MutableBytesValue data; + + public LogsBloomFilter() { + this.data = MutableBytesValue.create(BYTE_SIZE); + } + + public LogsBloomFilter(final BytesValue data) { + checkArgument( + data.size() == BYTE_SIZE, + "Invalid size for bloom filter backing array: expected %s but got %s", + BYTE_SIZE, + data.size()); + this.data = data.mutableCopy(); + } + + public static LogsBloomFilter fromHexString(final String hexString) { + return new LogsBloomFilter(BytesValue.fromHexString(hexString)); + } + + public static LogsBloomFilter empty() { + return new LogsBloomFilter(BytesValue.wrap(new byte[LogsBloomFilter.BYTE_SIZE])); + } + + /** + * Creates a bloom filter corresponding to the provide log series. + * + * @param logs the logs to populate the bloom filter with. + * @return the newly created bloom filter populated with the logs from {@code logs}. + */ + public static LogsBloomFilter compute(final Collection logs) { + final LogsBloomFilter bloom = new LogsBloomFilter(); + logs.forEach(bloom::insertLog); + return bloom; + } + + /** + * Creates a bloom filter from the given RLP-encoded input. + * + * @param input The input to read from + * @return the input's corresponding bloom filter + */ + public static LogsBloomFilter readFrom(final RLPInput input) { + final BytesValue bytes = input.readBytesValue(); + if (bytes.size() != BYTE_SIZE) { + throw new RLPException( + String.format( + "LogsBloomFilter unexpected size of %s (needs %s)", bytes.size(), BYTE_SIZE)); + } + return new LogsBloomFilter(bytes); + } + + /** + * Discover the low order 11-bits, of the first three double-bytes, of the SHA3 hash, of each + * value and update the bloom filter accordingly. + * + * @param hashValue The hash of the log item. + */ + private void setBits(final BytesValue hashValue) { + for (int counter = 0; counter < 6; counter += 2) { + final int setBloomBit = + ((hashValue.get(counter) & LEAST_SIGNIFICANT_THREE_BITS) << BITS_IN_BYTE) + + (hashValue.get(counter + 1) & LEAST_SIGNIFICANT_BYTE); + setBit(setBloomBit); + } + } + + @Override + public final boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof LogsBloomFilter)) { + return false; + } + final LogsBloomFilter other = (LogsBloomFilter) obj; + return data.equals(other.data); + } + + @Override + public int hashCode() { + return data.hashCode(); + } + + public BytesValue getBytes() { + return data; + } + + public void insertLog(final Log log) { + setBits(keccak256(log.getLogger())); + + for (final LogTopic topic : log.getTopics()) { + setBits(keccak256(topic)); + } + } + + private void setBit(final int index) { + final int byteIndex = BYTE_SIZE - 1 - index / 8; + final int bitIndex = index % 8; + data.set(byteIndex, (byte) (data.get(byteIndex) | (1 << bitIndex))); + } + + public void digest(final LogsBloomFilter other) { + for (int i = 0; i < data.size(); ++i) { + data.set(i, (byte) ((data.get(i) | other.data.get(i)) & 0xff)); + } + } + + @Override + public String toString() { + return data.toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableAccount.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableAccount.java new file mode 100755 index 00000000000..dea2f5ccc6f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableAccount.java @@ -0,0 +1,89 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Map; + +/** A mutable world state account. */ +public interface MutableAccount extends Account { + + /** + * Increments (by 1) the nonce of this account. + * + * @return the previous value of the nonce. + */ + default long incrementNonce() { + final long current = getNonce(); + setNonce(current + 1); + return current; + } + + /** + * Sets the nonce of this account to the provide value. + * + * @param value the value to set the nonce to. + */ + void setNonce(long value); + + /** + * Increments the account balance by the provided amount. + * + * @param value The amount to increment + * @return the previous balance (before increment). + */ + default Wei incrementBalance(final Wei value) { + final Wei current = getBalance(); + setBalance(current.plus(value)); + return current; + } + + /** + * Decrements the account balance by the provided amount. + * + * @param value The amount to decrement + * @return the previous balance (before decrement). The account must have enough funds or an + * exception is thrown. + * @throws IllegalStateException if the account balance is strictly less than {@code value}. + */ + default Wei decrementBalance(final Wei value) { + final Wei current = getBalance(); + if (current.compareTo(value) < 0) { + throw new IllegalStateException( + String.format("Cannot remove %s wei from account, balance is only %s", value, current)); + } + setBalance(current.minus(value)); + return current; + } + + /** + * Sets the balance of the account to the provided amount. + * + * @param value the amount to set. + */ + void setBalance(Wei value); + + /** + * Sets the code for the account. + * + * @param code the code to set for the account. + */ + void setCode(BytesValue code); + + /** + * Sets a particular key-value pair in the account storage. + * + *

Note that setting the value of an entry to 0 is basically equivalent to deleting that entry. + * + * @param key the key to set. + * @param value the value to set {@code key} to. + */ + void setStorageValue(UInt256 key, UInt256 value); + + /** + * Returns the storage entries that have been set through the updater this instance came from. + * + * @return a map of storage that has been modified. + */ + Map getUpdatedStorage(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldState.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldState.java new file mode 100755 index 00000000000..14fee55b626 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldState.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.ethereum.core; + +public interface MutableWorldState extends WorldState, MutableWorldView { + + /** + * Creates an independent copy of this world state initially equivalent to this world state. + * + * @return a copy of this world state. + */ + MutableWorldState copy(); + + /** Persist accumulated changes to underlying storage. */ + void persist(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldView.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldView.java new file mode 100755 index 00000000000..12286edc5c1 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/MutableWorldView.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.ethereum.core; + +public interface MutableWorldView extends WorldView { + + /** + * Creates a updater for this mutable world view. + * + * @return a new updater for this mutable world view. On commit, change made to this updater will + * become visible on this view. + */ + WorldUpdater updater(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactionListener.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactionListener.java new file mode 100755 index 00000000000..aca49c5e672 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactionListener.java @@ -0,0 +1,7 @@ +package net.consensys.pantheon.ethereum.core; + +@FunctionalInterface +public interface PendingTransactionListener { + + void onTransactionAdded(Transaction transaction); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactions.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactions.java new file mode 100755 index 00000000000..43472a3f3a6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/PendingTransactions.java @@ -0,0 +1,243 @@ +package net.consensys.pantheon.ethereum.core; + +import static java.util.Collections.newSetFromMap; +import static java.util.Comparator.comparing; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Holds the current set of pending transactions with the ability to iterate them based on priority + * for mining or look-up by hash. + * + *

This class is safe for use across multiple threads. + */ +public class PendingTransactions { + public static final int MAX_PENDING_TRANSACTIONS = 30_000; + + private final Map pendingTransactions = new HashMap<>(); + private final SortedSet prioritizedTransactions = + new TreeSet<>( + comparing(TransactionInfo::isReceivedFromLocalSource) + .thenComparing(TransactionInfo::getSequence) + .reversed()); + private final Map> transactionsBySender = + new HashMap<>(); + + private final Collection listeners = + newSetFromMap(new ConcurrentHashMap<>()); + + private final int maxPendingTransactions; + + public PendingTransactions(final int maxPendingTransactions) { + this.maxPendingTransactions = maxPendingTransactions; + } + + public boolean addRemoteTransaction(final Transaction transaction) { + final TransactionInfo transactionInfo = new TransactionInfo(transaction, false); + return addTransaction(transactionInfo); + } + + boolean addLocalTransaction(final Transaction transaction) { + return addTransaction(new TransactionInfo(transaction, true)); + } + + public void removeTransaction(final Transaction transaction) { + synchronized (pendingTransactions) { + final TransactionInfo removedTransactionInfo = pendingTransactions.remove(transaction.hash()); + if (removedTransactionInfo != null) { + prioritizedTransactions.remove(removedTransactionInfo); + Optional.ofNullable(transactionsBySender.get(transaction.getSender())) + .ifPresent( + transactionsForSender -> { + transactionsForSender.remove(transaction.getNonce()); + if (transactionsForSender.isEmpty()) { + transactionsBySender.remove(transaction.getSender()); + } + }); + } + } + } + + /* + * The BlockTransaction selection process (part of block mining) requires synchronised access to + * all pendingTransactions - this allows it to iterate over the available transactions without + * releasing the lock in between items. + * + */ + public void selectTransactions(final TransactionSelector selector) { + synchronized (pendingTransactions) { + final Map accountTransactions = new HashMap<>(); + final List transactionsToRemove = new ArrayList<>(); + for (final TransactionInfo transactionInfo : prioritizedTransactions) { + final AccountTransactionOrder accountTransactionOrder = + accountTransactions.computeIfAbsent( + transactionInfo.getSender(), this::createSenderTransactionOrder); + + for (final Transaction transactionToProcess : + accountTransactionOrder.transactionsToProcess(transactionInfo.getTransaction())) { + final TransactionSelectionResult result = + selector.evaluateTransaction(transactionToProcess); + switch (result) { + case DELETE_TRANSACTION_AND_CONTINUE: + transactionsToRemove.add(transactionToProcess); + break; + case CONTINUE: + break; + case COMPLETE_OPERATION: + return; + default: + throw new RuntimeException("Illegal value for TransactionSelectionResult."); + } + } + } + transactionsToRemove.forEach(this::removeTransaction); + } + } + + private AccountTransactionOrder createSenderTransactionOrder(final Address address) { + return new AccountTransactionOrder( + transactionsBySender.get(address).values().stream().map(TransactionInfo::getTransaction)); + } + + private boolean addTransaction(final TransactionInfo transactionInfo) { + synchronized (pendingTransactions) { + if (pendingTransactions.containsKey(transactionInfo.getHash())) { + return false; + } + + if (!addTransactionForSenderAndNonce(transactionInfo)) { + return false; + } + prioritizedTransactions.add(transactionInfo); + pendingTransactions.put(transactionInfo.getHash(), transactionInfo); + + notifyTransactionAdded(transactionInfo.getTransaction()); + if (pendingTransactions.size() > maxPendingTransactions) { + final TransactionInfo toRemove = prioritizedTransactions.last(); + removeTransaction(toRemove.getTransaction()); + } + return true; + } + } + + private boolean addTransactionForSenderAndNonce(final TransactionInfo transactionInfo) { + final Map transactionsForSender = + transactionsBySender.computeIfAbsent(transactionInfo.getSender(), key -> new TreeMap<>()); + final TransactionInfo existingTransaction = + transactionsForSender.get(transactionInfo.getNonce()); + if (existingTransaction != null) { + if (!shouldReplace(existingTransaction, transactionInfo)) { + return false; + } + removeTransaction(existingTransaction.getTransaction()); + } + transactionsForSender.put(transactionInfo.getNonce(), transactionInfo); + return true; + } + + private boolean shouldReplace( + final TransactionInfo existingTransaction, final TransactionInfo newTransaction) { + return newTransaction + .getTransaction() + .getGasPrice() + .compareTo(existingTransaction.getTransaction().getGasPrice()) + > 0; + } + + private void notifyTransactionAdded(final Transaction transaction) { + listeners.forEach(listener -> listener.onTransactionAdded(transaction)); + } + + public int size() { + synchronized (pendingTransactions) { + return pendingTransactions.size(); + } + } + + public Optional getTransactionByHash(final Hash transactionHash) { + synchronized (pendingTransactions) { + return Optional.ofNullable(pendingTransactions.get(transactionHash)) + .map(TransactionInfo::getTransaction); + } + } + + public void addTransactionListener(final PendingTransactionListener listener) { + listeners.add(listener); + } + + public OptionalLong getNextNonceForSender(final Address sender) { + synchronized (pendingTransactions) { + final SortedMap transactionsForSender = + transactionsBySender.get(sender); + if (transactionsForSender == null) { + return OptionalLong.empty(); + } + return OptionalLong.of(transactionsForSender.lastKey() + 1); + } + } + + /** + * Tracks the additional metadata associated with transactions to enable prioritization for mining + * and deciding which transactions to drop when the transaction pool reaches its size limit. + */ + private static class TransactionInfo { + + private static final AtomicLong TRANSACTIONS_ADDED = new AtomicLong(); + private final Transaction transaction; + private final boolean receivedFromLocalSource; + private final long sequence; // Allows prioritization based on order transactions are added + + private TransactionInfo(final Transaction transaction, final boolean receivedFromLocalSource) { + this.transaction = transaction; + this.receivedFromLocalSource = receivedFromLocalSource; + this.sequence = TRANSACTIONS_ADDED.getAndIncrement(); + } + + public Transaction getTransaction() { + return transaction; + } + + public long getSequence() { + return sequence; + } + + public long getNonce() { + return transaction.getNonce(); + } + + public Address getSender() { + return transaction.getSender(); + } + + public boolean isReceivedFromLocalSource() { + return receivedFromLocalSource; + } + + public Hash getHash() { + return transaction.hash(); + } + } + + public enum TransactionSelectionResult { + DELETE_TRANSACTION_AND_CONTINUE, + CONTINUE, + COMPLETE_OPERATION + } + + @FunctionalInterface + public interface TransactionSelector { + TransactionSelectionResult evaluateTransaction(final Transaction transaction); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/ProcessableBlockHeader.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/ProcessableBlockHeader.java new file mode 100755 index 00000000000..2f721cc7240 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/ProcessableBlockHeader.java @@ -0,0 +1,89 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.util.uint.UInt256; + +/** A block header capable of being processed. */ +public class ProcessableBlockHeader { + + protected final Hash parentHash; + + protected final Address coinbase; + + protected final UInt256 difficulty; + + protected final long number; + + protected final long gasLimit; + + // The block creation timestamp (seconds since the unix epoch) + protected final long timestamp; + + protected ProcessableBlockHeader( + final Hash parentHash, + final Address coinbase, + final UInt256 difficulty, + final long number, + final long gasLimit, + final long timestamp) { + this.parentHash = parentHash; + this.coinbase = coinbase; + this.difficulty = difficulty; + this.number = number; + this.gasLimit = gasLimit; + this.timestamp = timestamp; + } + + /** + * Returns the block parent block hash. + * + * @return the block parent block hash + */ + public Hash getParentHash() { + return parentHash; + } + + /** + * Returns the block coinbase address. + * + * @return the block coinbase address + */ + public Address getCoinbase() { + return coinbase; + } + + /** + * Returns the block difficulty. + * + * @return the block difficulty + */ + public UInt256 getDifficulty() { + return difficulty; + } + + /** + * Returns the block number. + * + * @return the block number + */ + public long getNumber() { + return number; + } + + /** + * Return the block gas limit. + * + * @return the block gas limit + */ + public long getGasLimit() { + return gasLimit; + } + + /** + * Return the block timestamp. + * + * @return the block timestamp + */ + public long getTimestamp() { + return timestamp; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SealableBlockHeader.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SealableBlockHeader.java new file mode 100755 index 00000000000..9187a2ad63f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SealableBlockHeader.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +/** A block header capable of being sealed. */ +public class SealableBlockHeader extends ProcessableBlockHeader { + + protected final Hash ommersHash; + + protected final Hash stateRoot; + + protected final Hash transactionsRoot; + + protected final Hash receiptsRoot; + + protected final LogsBloomFilter logsBloom; + + protected final long gasUsed; + + protected final BytesValue extraData; + + protected SealableBlockHeader( + final Hash parentHash, + final Hash ommersHash, + final Address coinbase, + final Hash stateRoot, + final Hash transactionsRoot, + final Hash receiptsRoot, + final LogsBloomFilter logsBloom, + final UInt256 difficulty, + final long number, + final long gasLimit, + final long gasUsed, + final long timestamp, + final BytesValue extraData) { + super(parentHash, coinbase, difficulty, number, gasLimit, timestamp); + this.ommersHash = ommersHash; + this.stateRoot = stateRoot; + this.transactionsRoot = transactionsRoot; + this.receiptsRoot = receiptsRoot; + this.logsBloom = logsBloom; + this.gasUsed = gasUsed; + this.extraData = extraData; + } + + /** + * Returns the block ommers list hash. + * + * @return the block ommers list hash + */ + public Hash getOmmersHash() { + return ommersHash; + } + + /** + * Returns the block world state root hash. + * + * @return the block world state root hash + */ + public Hash getStateRoot() { + return stateRoot; + } + + /** + * Returns the block transaction root hash. + * + * @return the block transaction root hash + */ + public Hash getTransactionsRoot() { + return transactionsRoot; + } + + /** + * Returns the block transaction receipt root hash. + * + * @return the block transaction receipt root hash + */ + public Hash getReceiptsRoot() { + return receiptsRoot; + } + + /** + * Returns the block logs bloom filter. + * + * @return the block logs bloom filter + */ + public LogsBloomFilter getLogsBloom() { + return logsBloom; + } + + /** + * Returns the total gas consumed by the executing the block. + * + * @return the total gas consumed by the executing the block + */ + public long getGasUsed() { + return gasUsed; + } + + /** + * Returns the block extra data field. + * + * @return the block extra data field + */ + public BytesValue getExtraData() { + return extraData; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SyncStatus.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SyncStatus.java new file mode 100755 index 00000000000..85ee4205f9c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/SyncStatus.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.core; + +public final class SyncStatus { + + private final long startingBlock; + private final long currentBlock; + private final long highestBlock; + + public SyncStatus(final long startingBlock, final long currentBlock, final long highestBlock) { + this.startingBlock = startingBlock; + this.currentBlock = currentBlock; + this.highestBlock = highestBlock; + } + + public long getStartingBlock() { + return startingBlock; + } + + public long getCurrentBlock() { + return currentBlock; + } + + public long getHighestBlock() { + return highestBlock; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Synchronizer.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Synchronizer.java new file mode 100755 index 00000000000..3f198a5a37c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Synchronizer.java @@ -0,0 +1,15 @@ +package net.consensys.pantheon.ethereum.core; + +import java.util.Optional; + +/** Provides an interface to block synchronization processes. */ +public interface Synchronizer { + + public void start(); + + /** + * @return the status, based on SyncingResult When actively synchronizing blocks, alternatively + * empty + */ + Optional getSyncStatus(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Transaction.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Transaction.java new file mode 100755 index 00000000000..e56b0e4f054 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Transaction.java @@ -0,0 +1,493 @@ +package net.consensys.pantheon.ethereum.core; + +import static com.google.common.base.Preconditions.checkState; +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; + +/** An operation submitted by an external actor to be applied to the system. */ +public class Transaction { + + // Used for transactions that are not tied to a specific chain + // (e.g. does not have a chain id associated with it). + private static final int REPLAY_UNPROTECTED_V_BASE = 27; + + private static final int REPLAY_PROTECTED_V_BASE = 35; + + // The v signature parameter starts at 36 because 1 is the first valid chainId so: + // chainId > 1 implies that 2 * chainId + V_BASE > 36. + private static final int REPLAY_PROTECTED_V_MIN = 36; + + private final long nonce; + + private final Wei gasPrice; + + private final long gasLimit; + + private final Optional

to; + + private final Wei value; + + private final SECP256K1.Signature signature; + + private final BytesValue payload; + + private final OptionalInt chainId; + + // Caches a "hash" of a portion of the transaction used for sender recovery. + // Note that this hash does not include the transaction signature so it does not + // fully identify the transaction (use the result of the {@code hash()} for that). + // It is only used to compute said signature and recover the sender from it. + protected volatile Bytes32 hashNoSignature; + + // Caches the transaction sender. + protected volatile Address sender; + + // Caches the hash used to uniquely identify the transaction. + protected volatile Hash hash; + + public static Builder builder() { + return new Builder(); + } + + public static Transaction readFrom(final RLPInput input) throws RLPException { + input.enterList(); + + final Builder builder = + builder() + .nonce(input.readLongScalar()) + .gasPrice(input.readUInt256Scalar(Wei::wrap)) + .gasLimit(input.readLongScalar()) + .to(input.readBytesValue(v -> v.size() == 0 ? null : Address.wrap(v))) + .value(input.readUInt256Scalar(Wei::wrap)) + .payload(input.readBytesValue()); + + final int v = input.readIntScalar(); + byte recId; + int chainId = -1; + if (v == REPLAY_UNPROTECTED_V_BASE || v == REPLAY_UNPROTECTED_V_BASE + 1) { + recId = (byte) (v - REPLAY_UNPROTECTED_V_BASE); + } else if (v > REPLAY_PROTECTED_V_MIN) { + chainId = (v - REPLAY_PROTECTED_V_BASE) / 2; + recId = (byte) (v - (2 * chainId + REPLAY_PROTECTED_V_BASE)); + } else { + throw new RuntimeException( + String.format("An unsupported encoded `v` value of %s was found", v)); + } + final BigInteger r = BytesValues.asUnsignedBigInteger(input.readUInt256Scalar().getBytes()); + final BigInteger s = BytesValues.asUnsignedBigInteger(input.readUInt256Scalar().getBytes()); + final SECP256K1.Signature signature = SECP256K1.Signature.create(r, s, recId); + + input.leaveList(); + + return builder.chainId(chainId).signature(signature).build(); + } + + /** + * Instantiates a transaction instance. + * + * @param nonce the nonce + * @param gasPrice the gas price + * @param gasLimit the gas limit + * @param to the transaction recipient + * @param value the value being transferred to the recipient + * @param signature the signature + * @param payload the payload + * @param sender the transaction sender + * @param chainId the chain id to apply the transaction to + *

The {@code to} will be an {@code Optional.empty()} for a contract creation transaction; + * otherwise it should contain an address. + *

The {@code chainId} must be greater than 0 to be applied to a specific chain; otherwise + * it will default to any chain. + */ + protected Transaction( + final long nonce, + final Wei gasPrice, + final long gasLimit, + final Optional

to, + final Wei value, + final SECP256K1.Signature signature, + final BytesValue payload, + final Address sender, + final int chainId) { + this.nonce = nonce; + this.gasPrice = gasPrice; + this.gasLimit = gasLimit; + this.to = to; + this.value = value; + this.signature = signature; + this.payload = payload; + this.sender = sender; + this.chainId = chainId > 0 ? OptionalInt.of(chainId) : OptionalInt.empty(); + } + + /** + * Returns the transaction nonce. + * + * @return the transaction nonce + */ + public long getNonce() { + return nonce; + } + + /** + * Return the transaction gas price. + * + * @return the transaction gas price + */ + public Wei getGasPrice() { + return gasPrice; + } + + /** + * Returns the transaction gas limit. + * + * @return the transaction gas limit + */ + public long getGasLimit() { + return gasLimit; + } + + /** + * Returns the transaction recipient. + * + *

The {@code Optional

} will be {@code Optional.empty()} if the transaction is a + * contract creation; otherwise it will contain the message call transaction recipient. + * + * @return the transaction recipient if a message call; otherwise {@code Optional.empty()} + */ + public Optional
getTo() { + return to; + } + + /** + * Returns the value transferred in the transaction. + * + * @return the value transferred in the transaction + */ + public Wei getValue() { + return value; + } + + /** + * Returns the signature used to sign the transaction. + * + * @return the signature used to sign the transaction + */ + public SECP256K1.Signature getSignature() { + return signature; + } + + /** + * Returns the transaction payload. + * + * @return the transaction payload + */ + public BytesValue getPayload() { + return payload; + } + + /** + * Return the transaction chain id (if it exists) + * + *

The {@code OptionalInt} will be {@code OptionalInt.empty()} if the transaction is not tied + * to a specific chain. + * + * @return the transaction chain id if it exists; otherwise {@code OptionalInt.empty()} + */ + public OptionalInt getChainId() { + return chainId; + } + + /** + * Returns the transaction sender. + * + * @return the transaction sender + */ + public Address getSender() { + if (sender == null) { + final SECP256K1.PublicKey publicKey = + SECP256K1.PublicKey.recoverFromSignature(getOrComputeSenderRecoveryHash(), signature) + .orElseThrow( + () -> + new IllegalStateException( + "Cannot recover public key from " + "signature for " + this)); + sender = Address.extract(Hash.hash(publicKey.getEncodedBytes())); + } + return sender; + } + + private Bytes32 getOrComputeSenderRecoveryHash() { + if (hashNoSignature == null) { + hashNoSignature = + computeSenderRecoveryHash( + nonce, gasPrice, gasLimit, to.isPresent() ? to.get() : null, value, payload, chainId); + } + return hashNoSignature; + } + + /** + * Writes the transaction to RLP + * + * @param out the output to write the transaction to + */ + public void writeTo(final RLPOutput out) { + out.startList(); + + out.writeLongScalar(getNonce()); + out.writeUInt256Scalar(getGasPrice()); + out.writeLongScalar(getGasLimit()); + out.writeBytesValue(getTo().isPresent() ? getTo().get() : BytesValue.EMPTY); + out.writeUInt256Scalar(getValue()); + out.writeBytesValue(getPayload()); + writeSignature(out); + + out.endList(); + } + + private void writeSignature(final RLPOutput out) { + out.writeIntScalar(getV()); + out.writeBigIntegerScalar(getSignature().getR()); + out.writeBigIntegerScalar(getSignature().getS()); + } + + public BigInteger getR() { + return signature.getR(); + } + + public BigInteger getS() { + return signature.getS(); + } + + public int getV() { + int v; + if (!chainId.isPresent()) { + v = signature.getRecId() + REPLAY_UNPROTECTED_V_BASE; + } else { + v = (getSignature().getRecId() + REPLAY_PROTECTED_V_BASE + 2 * chainId.getAsInt()); + } + return v; + } + + /** + * Returns the transaction hash. + * + * @return the transaction hash + */ + public Hash hash() { + if (hash == null) { + final BytesValue rlp = RLP.encode(this::writeTo); + hash = Hash.hash(rlp); + } + return hash; + } + + /** + * Returns whether the transaction is a contract creation + * + * @return {@code true} if this is a contract-creation transaction; otherwise {@code false} + */ + public boolean isContractCreation() { + return !getTo().isPresent(); + } + + /** + * Calculates the up-front cost for the gas the transaction can use. + * + * @return the up-front cost for the gas the transaction can use. + */ + public Wei getUpfrontGasCost() { + return Wei.of(getGasLimit()).times(getGasPrice()); + } + + /** + * Calculates the up-front cost for the transaction. + * + *

The up-front cost is paid by the sender account before the transaction is executed. The + * sender must have the amount in its account balance to execute and some of this amount may be + * refunded after the transaction has executed. + * + * @return the up-front gas cost for the transaction + */ + public Wei getUpfrontCost() { + return getUpfrontGasCost().plus(getValue()); + } + + private static Bytes32 computeSenderRecoveryHash( + final long nonce, + final Wei gasPrice, + final long gasLimit, + final Address to, + final Wei value, + final BytesValue payload, + final OptionalInt chainId) { + return keccak256( + RLP.encode( + out -> { + out.startList(); + out.writeLongScalar(nonce); + out.writeUInt256Scalar(gasPrice); + out.writeLongScalar(gasLimit); + out.writeBytesValue(to == null ? BytesValue.EMPTY : to); + out.writeUInt256Scalar(value); + out.writeBytesValue(payload); + if (chainId.isPresent()) { + out.writeIntScalar(chainId.getAsInt()); + out.writeUInt256Scalar(UInt256.ZERO); + out.writeUInt256Scalar(UInt256.ZERO); + } + out.endList(); + })); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof Transaction)) { + return false; + } + final Transaction that = (Transaction) other; + return this.chainId.equals(that.chainId) + && this.gasLimit == that.gasLimit + && this.gasPrice.equals(that.gasPrice) + && this.nonce == that.nonce + && this.payload.equals(that.payload) + && this.signature.equals(that.signature) + && this.to.equals(that.to) + && this.value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(nonce, gasPrice, gasLimit, to, value, payload, signature, chainId); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(isContractCreation() ? "ContractCreation" : "MessageCall").append("{"); + sb.append("nonce=").append(getNonce()).append(", "); + sb.append("gasPrice=").append(getGasPrice()).append(", "); + sb.append("gasLimit=").append(getGasLimit()).append(", "); + if (getTo().isPresent()) sb.append("to=").append(getTo().get()).append(", "); + sb.append("value=").append(getValue()).append(", "); + sb.append("sig=").append(getSignature()).append(", "); + if (chainId.isPresent()) sb.append("chainId=").append(getChainId().getAsInt()).append(", "); + sb.append("payload=").append(getPayload()); + return sb.append("}").toString(); + } + + public Optional

contractAddress() { + if (isContractCreation()) { + return Optional.of(Address.contractAddress(getSender(), getNonce())); + } + return Optional.empty(); + } + + public static class Builder { + + protected long nonce = -1L; + + protected Wei gasPrice; + + protected long gasLimit = -1L; + + protected Address to; + + protected Wei value; + + protected SECP256K1.Signature signature; + + protected BytesValue payload; + + protected Address sender; + + protected int chainId = -1; + + public Builder chainId(final int chainId) { + this.chainId = chainId; + return this; + } + + public Builder gasPrice(final Wei gasPrice) { + this.gasPrice = gasPrice; + return this; + } + + public Builder gasLimit(final long gasLimit) { + this.gasLimit = gasLimit; + return this; + } + + public Builder nonce(final long nonce) { + this.nonce = nonce; + return this; + } + + public Builder value(final Wei value) { + this.value = value; + return this; + } + + public Builder to(final Address to) { + this.to = to; + return this; + } + + public Builder payload(final BytesValue payload) { + this.payload = payload; + return this; + } + + public Builder sender(final Address sender) { + this.sender = sender; + return this; + } + + public Builder signature(final SECP256K1.Signature signature) { + this.signature = signature; + return this; + } + + public Transaction build() { + return new Transaction( + nonce, + gasPrice, + gasLimit, + Optional.ofNullable(to), + value, + signature, + payload, + sender, + chainId); + } + + public Transaction signAndBuild(final SECP256K1.KeyPair keys) { + checkState( + signature == null, "The transaction signature has already been provided to this builder"); + signature(computeSignature(keys)); + sender(Address.extract(Hash.hash(keys.getPublicKey().getEncodedBytes()))); + return build(); + } + + protected SECP256K1.Signature computeSignature(final SECP256K1.KeyPair keys) { + final OptionalInt optionalChainId = + chainId > 0 ? OptionalInt.of(chainId) : OptionalInt.empty(); + final Bytes32 hash = + computeSenderRecoveryHash(nonce, gasPrice, gasLimit, to, value, payload, optionalChainId); + return SECP256K1.sign(hash, keys); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionBuilder.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionBuilder.java new file mode 100755 index 00000000000..24c4a0b8a35 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionBuilder.java @@ -0,0 +1,106 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +/** Convenience object for building {@link Transaction}s. */ +public interface TransactionBuilder { + + /** @return A {@link Transaction} populated with the accumulated state. */ + Transaction build(); + + /** + * Constructs a {@link SECP256K1.Signature} based on the accumulated state and then builds a + * corresponding {@link Transaction}. + * + * @param keys The keys to construct the transaction signature with. + * @return A {@link Transaction} populated with the accumulated state. + */ + Transaction signAndBuild(SECP256K1.KeyPair keys); + + /** + * Populates the {@link TransactionBuilder} based on the RLP-encoded transaction and builds a + * {@link Transaction}. + * + *

Note: the fields from the RLP-transaction will be extracted and replace any previously + * populated fields. + * + * @param in The RLP-encoded transaction. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder populateFrom(RLPInput in); + + /** + * Sets the chain id for the {@link Transaction}. + * + * @param chainId The chain id. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder chainId(int chainId); + + /** + * Sets the gas limit for the {@link Transaction}. + * + * @param gasLimit The gas limit. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder gasLimit(long gasLimit); + + /** + * Sets the gas price for the {@link Transaction}. + * + * @param gasPrice The gas price. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder gasPrice(Wei gasPrice); + + /** + * Sets the nonce for the {@link Transaction}. + * + * @param nonce The nonce. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder nonce(long nonce); + + /** + * Sets the payload for the {@link Transaction}. + * + * @param payload The payload. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder payload(BytesValue payload); + + /** + * Sets the sender of the {@link Transaction}. + * + * @param sender The sender. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder sender(Address sender); + + /** + * Sets the signature of the {@link Transaction}. + * + * @param signature The signature. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder signature(Signature signature); + + /** + * Sets the recipient of the {@link Transaction}. + * + * @param to The recipent (can be null). + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder to(Address to); + + /** + * Sets the {@link Wei} transfer value of the {@link Transaction}. + * + * @param value The transfer value. + * @return The updated {@link TransactionBuilder}. + */ + TransactionBuilder value(Wei value); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionPool.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionPool.java new file mode 100755 index 00000000000..76139e38cf3 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionPool.java @@ -0,0 +1,154 @@ +package net.consensys.pantheon.ethereum.core; + +import static java.util.Collections.singletonList; +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.BlockAddedObserver; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.logging.log4j.Logger; + +/** + * Maintains the set of pending transactions received from JSON-RPC or other nodes. Transactions are + * removed automatically when they are included in a block on the canonical chain and re-added if a + * re-org removes them from the canonical chain again. + * + *

This class is safe for use across multiple threads. + */ +public class TransactionPool implements BlockAddedObserver { + private static final Logger LOG = getLogger(); + private final PendingTransactions pendingTransactions; + private final ProtocolSchedule protocolSchedule; + private final ProtocolContext protocolContext; + private final TransactionBatchAddedListener transactionBatchAddedListener; + + public TransactionPool( + final PendingTransactions pendingTransactions, + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final TransactionBatchAddedListener transactionBatchAddedListener) { + this.pendingTransactions = pendingTransactions; + this.protocolSchedule = protocolSchedule; + this.protocolContext = protocolContext; + this.transactionBatchAddedListener = transactionBatchAddedListener; + } + + public ValidationResult addLocalTransaction( + final Transaction transaction) { + final ValidationResult validationResult = + validateTransaction(transaction); + + validationResult.ifValid( + () -> { + final boolean added = pendingTransactions.addLocalTransaction(transaction); + if (added) { + transactionBatchAddedListener.onTransactionsAdded(singletonList(transaction)); + } + }); + return validationResult; + } + + public void addRemoteTransactions(final Collection transactions) { + final Set addedTransactions = new HashSet<>(); + for (final Transaction transaction : sortByNonce(transactions)) { + final ValidationResult validationResult = + validateTransaction(transaction); + if (validationResult.isValid()) { + final boolean added = pendingTransactions.addRemoteTransaction(transaction); + if (added) { + addedTransactions.add(transaction); + } + } else { + LOG.debug( + "Validation failed ({}) for transaction {}. Discarding.", + validationResult.getInvalidReason(), + transaction); + } + } + if (!addedTransactions.isEmpty()) { + transactionBatchAddedListener.onTransactionsAdded(addedTransactions); + } + } + + // Sort transactions by nonce to ensure we import sequences of transactions correctly + private List sortByNonce(final Collection transactions) { + final List sortedTransactions = new ArrayList<>(transactions); + sortedTransactions.sort(Comparator.comparing(Transaction::getNonce)); + return sortedTransactions; + } + + public void addTransactionListener(final PendingTransactionListener listener) { + pendingTransactions.addTransactionListener(listener); + } + + @Override + public void onBlockAdded(final BlockAddedEvent event, final Blockchain blockchain) { + event.getAddedTransactions().forEach(pendingTransactions::removeTransaction); + addRemoteTransactions(event.getRemovedTransactions()); + } + + private TransactionValidator getTransactionValidator() { + return protocolSchedule + .getByBlockNumber(protocolContext.getBlockchain().getChainHeadBlockNumber()) + .getTransactionValidator(); + } + + public PendingTransactions getPendingTransactions() { + return pendingTransactions; + } + + private ValidationResult validateTransaction( + final Transaction transaction) { + final ValidationResult basicValidationResult = + getTransactionValidator().validate(transaction); + if (!basicValidationResult.isValid()) { + return basicValidationResult; + } + + final BlockHeader chainHeadBlockHeader = getChainHeadBlockHeader(); + if (transaction.getGasLimit() > chainHeadBlockHeader.getGasLimit()) { + return ValidationResult.invalid( + TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT, + String.format( + "Transaction gas limit of %s exceeds block gas limit of %s", + transaction.getGasLimit(), chainHeadBlockHeader.getGasLimit())); + } + + return getTransactionValidator() + .validateForSender( + transaction, + getSenderAccount(transaction, chainHeadBlockHeader), + pendingTransactions.getNextNonceForSender(transaction.getSender())); + } + + private Account getSenderAccount( + final Transaction transaction, final BlockHeader chainHeadHeader) { + final WorldState worldState = + protocolContext.getWorldStateArchive().get(chainHeadHeader.getStateRoot()); + return worldState.get(transaction.getSender()); + } + + private BlockHeader getChainHeadBlockHeader() { + final MutableBlockchain blockchain = protocolContext.getBlockchain(); + return blockchain.getBlockHeader(blockchain.getChainHeadHash()).get(); + } + + public interface TransactionBatchAddedListener { + + void onTransactionsAdded(Iterable transactions); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionReceipt.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionReceipt.java new file mode 100755 index 00000000000..55076368259 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/TransactionReceipt.java @@ -0,0 +1,203 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.mainnet.TransactionReceiptType; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import java.util.List; +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +/** + * A transaction receipt, containing information pertaining a transaction execution. + * + *

Transaction receipts have two different formats: state root-encoded and status-encoded. The + * difference between these two formats is that the state root-encoded transaction receipt contains + * the state root for world state after the transaction has been processed (e.g. not invalid) and + * the status-encoded transaction receipt instead has contains the status of the transaction (e.g. 1 + * for success and 0 for failure). The other transaction receipt fields are the same for both + * formats: logs, logs bloom, and cumulative gas used in the block. The TransactionReceiptType + * attribute is the best way to check which format has been used. + */ +public class TransactionReceipt { + + private static final int NONEXISTENT = -1; + + private final Hash stateRoot; + private final long cumulativeGasUsed; + private final List logs; + private final LogsBloomFilter bloomFilter; + private final int status; + private final TransactionReceiptType transactionReceiptType; + + /** + * Creates an instance of a state root-encoded transaction receipt. + * + * @param stateRoot the state root for the world state after the transaction has been processed + * @param cumulativeGasUsed the total amount of gas consumed in the block after this transaction + * @param logs the logs generated within the transaction + */ + public TransactionReceipt( + final Hash stateRoot, final long cumulativeGasUsed, final List logs) { + this(stateRoot, NONEXISTENT, cumulativeGasUsed, logs); + } + + /** + * Creates an instance of a status-encoded transaction receipt. + * + * @param status the status code for the transaction (1 for success and 0 for failure) + * @param cumulativeGasUsed the total amount of gas consumed in the block after this transaction + * @param logs the logs generated within the transaction + */ + public TransactionReceipt(final int status, final long cumulativeGasUsed, final List logs) { + this(null, status, cumulativeGasUsed, logs); + } + + private TransactionReceipt( + final Hash stateRoot, final int status, final long cumulativeGasUsed, final List logs) { + this.stateRoot = stateRoot; + this.cumulativeGasUsed = cumulativeGasUsed; + this.status = status; + this.logs = logs; + this.bloomFilter = LogsBloomFilter.compute(logs); + transactionReceiptType = + stateRoot == null ? TransactionReceiptType.STATUS : TransactionReceiptType.ROOT; + } + + /** + * Write an RLP representation. + * + * @param out The RLP output to write to + */ + public void writeTo(final RLPOutput out) { + out.startList(); + + // Determine whether it's a state root-encoded transaction receipt + // or is a status code-encoded transaction receipt. + if (stateRoot != null) { + out.writeBytesValue(stateRoot); + } else { + out.writeLongScalar(status); + } + out.writeLongScalar(cumulativeGasUsed); + out.writeBytesValue(bloomFilter.getBytes()); + out.writeList(logs, Log::writeTo); + + out.endList(); + } + + /** + * Creates a transaction receipt for the given RLP + * + * @param input the RLP-encoded transaction receipt + * @return the transaction receipt + */ + public static TransactionReceipt readFrom(final RLPInput input) { + input.enterList(); + + try { + // Get the first element to check later to determine the + // correct transaction receipt encoding to use. + final RLPInput firstElement = input.readAsRlp(); + final long cumulativeGas = input.readLongScalar(); + // The logs below will populate the bloom filter upon construction. + // TODO consider validating that the logs and bloom filter match. + input.skipNext(); + final List logs = input.readList(Log::readFrom); + + // Status code-encoded transaction receipts have a single + // byte for success (0x01) or failure (0x80). + if (firstElement.raw().size() == 1) { + final int status = firstElement.readIntScalar(); + return new TransactionReceipt(status, cumulativeGas, logs); + } else { + final Hash stateRoot = Hash.wrap(firstElement.readBytes32()); + return new TransactionReceipt(stateRoot, cumulativeGas, logs); + } + } finally { + input.leaveList(); + } + } + + /** + * Returns the state root for a state root-encoded transaction receipt + * + * @return the state root if the transaction receipt is state root-encoded; otherwise {@code null} + */ + public Hash getStateRoot() { + return stateRoot; + } + + /** + * Returns the total amount of gas consumed in the block after the transaction has been processed. + * + * @return the total amount of gas consumed in the block after the transaction has been processed + */ + public long getCumulativeGasUsed() { + return cumulativeGasUsed; + } + + /** + * Returns the logs generated by the transaction. + * + * @return the logs generated by the transaction + */ + public List getLogs() { + return logs; + } + + /** + * Returns the logs bloom filter for the logs generated by the transaction + * + * @return the logs bloom filter for the logs generated by the transaction + */ + public LogsBloomFilter getBloomFilter() { + return bloomFilter; + } + + /** + * Returns the status code for the status-encoded transaction receipt + * + * @return the status code if the transaction receipt is status-encoded; otherwise {@code -1} + */ + public int getStatus() { + return status; + } + + public TransactionReceiptType getTransactionReceiptType() { + return transactionReceiptType; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof TransactionReceipt)) { + return false; + } + final TransactionReceipt other = (TransactionReceipt) obj; + return logs.equals(other.getLogs()) + && stateRoot.equals(other.stateRoot) + && cumulativeGasUsed == other.getCumulativeGasUsed() + && status == other.status; + } + + @Override + public int hashCode() { + return Objects.hash(logs, stateRoot, cumulativeGasUsed); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("stateRoot", stateRoot) + .add("cumulativeGasUsed", cumulativeGasUsed) + .add("logs", logs) + .add("bloomFilter", bloomFilter) + .add("status", status) + .add("transactionReceiptType", transactionReceiptType) + .toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Util.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Util.java new file mode 100755 index 00000000000..3a336bc7b17 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Util.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.crypto.SECP256K1.PublicKey; +import net.consensys.pantheon.crypto.SECP256K1.Signature; + +public class Util { + + /** + * Converts a Signature to an Address, underlying math requires the hash of the data used to + * create the signature. + * + * @param seal the signature from which an address is to be extracted + * @param dataHash the hash of the data which was signed. + * @return The Address of the Ethereum node which signed the data defined by the supplied dataHash + */ + public static Address signatureToAddress(final Signature seal, final Hash dataHash) { + return PublicKey.recoverFromSignature(dataHash, seal) + .map(Util::publicKeyToAddress) + .orElse(null); + } + + public static Address publicKeyToAddress(final PublicKey publicKey) { + return Address.extract(Hash.hash(publicKey.getEncodedBytes())); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Wei.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Wei.java new file mode 100755 index 00000000000..bb39b70c83a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/Wei.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.BaseUInt256Value; +import net.consensys.pantheon.util.uint.Counter; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; + +/** A particular quantity of Wei, the Ethereum currency. */ +public final class Wei extends BaseUInt256Value { + + public static final Wei ZERO = of(0); + + protected Wei(final Bytes32 bytes) { + super(bytes, WeiCounter::new); + } + + private Wei(final long v) { + super(v, WeiCounter::new); + } + + private Wei(final BigInteger v) { + super(v, WeiCounter::new); + } + + private Wei(final String hexString) { + super(hexString, WeiCounter::new); + } + + public static Wei of(final long value) { + return new Wei(value); + } + + public static Wei of(final BigInteger value) { + return new Wei(value); + } + + public static Wei of(final UInt256 value) { + return new Wei(value.getBytes().copy()); + } + + public static Wei wrap(final Bytes32 value) { + return new Wei(value); + } + + public static Wei fromHexString(final String str) { + return new Wei(str); + } + + public static Wei fromEth(final long eth) { + return Wei.of(BigInteger.valueOf(eth).multiply(BigInteger.TEN.pow(18))); + } + + public static Counter newCounter() { + return new WeiCounter(); + } + + private static class WeiCounter extends Counter { + private WeiCounter() { + super(Wei::new); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldState.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldState.java new file mode 100755 index 00000000000..b7df8a03a74 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldState.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.core; + +import java.util.stream.Stream; + +/** + * A specific state of the world. + * + *

Note that while this interface represents an immutable view of a world state (it doesn't have + * mutation methods), it does not guarantee in and of itself that the underlying implementation is + * not mutable. In other words, objects implementing this interface are not guaranteed to be + * thread-safe, though some particular implementations may provide such guarantees. + */ +public interface WorldState extends WorldView { + + /** + * The root hash of the world state this represents. + * + * @return the world state root hash. + */ + Hash rootHash(); + + /** + * A stream of all the accounts in this world state. + * + * @return a stream of all the accounts (in no particular order) contained in the world state + * represented by the root hash of this object at the time of the call. + */ + Stream accounts(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldUpdater.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldUpdater.java new file mode 100755 index 00000000000..7156b70d6a6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldUpdater.java @@ -0,0 +1,91 @@ +package net.consensys.pantheon.ethereum.core; + +import java.util.Collection; + +/** + * An object that buffers updates made over a particular {@link WorldView}. + * + *

All changes made to this object, being it account creation/deletion or account modifications + * through {@link MutableAccount}, are immediately reflected on this object (so for instance, + * deleting an account and trying to get it afterwards will return {@code null}) but do not impact + * whichever {@link WorldView} this is an updater for until the {@link #commit} method is called. + */ +public interface WorldUpdater extends MutableWorldView { + + /** + * Creates a new account, or reset it (that is, act as if it was deleted and created anew) if it + * already exists. + * + *

After this call, the account will exists and will have the provided nonce and balance. His + * code and storage will be empty. + * + * @param address the address of the account to create (or reset). + * @param nonce the nonce for created/reset account. + * @param balance the balance for created/reset account. + * @return the account {@code address}, which will have nonce {@code nonce}, balance {@code + * balance} and empty code and storage. + */ + MutableAccount createAccount(Address address, long nonce, Wei balance); + + /** + * Creates a new account, or reset it (that is, act as if it was deleted and created anew) if it + * already exists. + * + *

This call is equivalent to {@link #createAccount(Address, long, Wei)} but defaults both the + * nonce and balance to zero. + * + * @param address the address of the account to create (or reset). + * @return the account {@code address}, which will have 0 for the nonce and balance and empty code + * and storage. + */ + default MutableAccount createAccount(final Address address) { + return createAccount(address, 0L, Wei.ZERO); + } + + /** + * Retrieves the provided account if it exists, or create it if it doesn't. + * + * @param address the address of the account. + * @return the account {@code address}. If that account exists, it is returned as if by {@link + * #getMutable(Address)}, otherwise, it is created and returned as if by {@link + * #createAccount(Address)} (and thus all his fields will be zero/empty). + */ + default MutableAccount getOrCreate(final Address address) { + final MutableAccount account = getMutable(address); + return account == null ? createAccount(address) : account; + } + + /** + * Retrieves the provided account, returning a modifiable object (whose updates are accumulated by + * this updater). + * + * @param address the address of the account. + * @return the account {@code address} as modifiable object, or {@code null} if the account does + * not exists. + */ + MutableAccount getMutable(Address address); + + /** + * Deletes the provided account. + * + * @param address the address of the account to delete. If that account doesn't exists prior to + * this call, this is a no-op. + */ + void deleteAccount(Address address); + + /** + * Returns the accounts that have been touched within the scope of this updater. + * + * @return the accounts that have been touched within the scope of this updater + */ + Collection getTouchedAccounts(); + + /** Removes the changes that were made to this updater. */ + void revert(); + + /** + * Commits the changes made to this updater to the underlying {@link WorldView} this is an updater + * of. + */ + void commit(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldView.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldView.java new file mode 100755 index 00000000000..97ef4d2cd00 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/core/WorldView.java @@ -0,0 +1,15 @@ +package net.consensys.pantheon.ethereum.core; + +/** Generic interface for a view over the accounts of the world state. */ +public interface WorldView { + WorldView EMPTY = address -> null; + + /** + * Get an account provided it's address. + * + * @param address the address of the account to retrieve. + * @return the {@link Account} corresponding to {@code address} or {@code null} if there is no + * such account. + */ + Account get(Address address); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/BlockchainStorage.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/BlockchainStorage.java new file mode 100755 index 00000000000..bbedf1c331d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/BlockchainStorage.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.db; + +import net.consensys.pantheon.ethereum.chain.TransactionLocation; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface BlockchainStorage { + + Optional getChainHead(); + + Collection getForkHeads(); + + Optional getBlockHeader(Hash blockHash); + + Optional getBlockBody(Hash blockHash); + + Optional> getTransactionReceipts(Hash blockHash); + + Optional getBlockHash(long blockNumber); + + Optional getTotalDifficulty(Hash blockHash); + + Optional getTransactionLocation(Hash transactionHash); + + Updater updater(); + + interface Updater { + + void putBlockHeader(Hash blockHash, BlockHeader blockHeader); + + void putBlockBody(Hash blockHash, BlockBody blockBody); + + void putTransactionLocation(Hash transactionHash, TransactionLocation transactionLocation); + + void putTransactionReceipts(Hash blockHash, List transactionReceipts); + + void putBlockHash(long blockNumber, Hash blockHash); + + void putTotalDifficulty(Hash blockHash, UInt256 totalDifficulty); + + void setChainHead(Hash blockHash); + + void setForkHeads(Collection forkHeadHashes); + + void removeBlockHash(long blockNumber); + + void removeTransactionLocation(Hash transactionHash); + + void commit(); + + void rollback(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchain.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchain.java new file mode 100755 index 00000000000..f969f3f0b32 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchain.java @@ -0,0 +1,376 @@ +package net.consensys.pantheon.ethereum.db; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.BlockAddedObserver; +import net.consensys.pantheon.ethereum.chain.ChainHead; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.chain.TransactionLocation; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.Subscribers; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; + +public class DefaultMutableBlockchain implements MutableBlockchain { + + private final BlockchainStorage blockchainStorage; + + private final Subscribers blockAddedObservers = new Subscribers<>(); + + public DefaultMutableBlockchain( + final Block genesisBlock, + final KeyValueStorage keyValueStorage, + final BlockHashFunction blockHashFunction) { + checkArgument(genesisBlock != null, "Missing required KeyValueStorage"); + this.blockchainStorage = + new KeyValueStoragePrefixedKeyBlockchainStorage(keyValueStorage, blockHashFunction); + this.setGenesis(genesisBlock); + } + + @Override + public ChainHead getChainHead() { + return blockchainStorage + .getChainHead() + .flatMap(h -> blockchainStorage.getTotalDifficulty(h).map(td -> new ChainHead(h, td))) + .get(); + } + + @Override + public Hash getChainHeadHash() { + return blockchainStorage.getChainHead().get(); + } + + @Override + public long getChainHeadBlockNumber() { + // Head should always be set, so we can call get() + return blockchainStorage + .getChainHead() + .flatMap(blockchainStorage::getBlockHeader) + .map(BlockHeader::getNumber) + .get(); + } + + @Override + public Optional getBlockHeader(final long blockNumber) { + return blockchainStorage.getBlockHash(blockNumber).flatMap(blockchainStorage::getBlockHeader); + } + + @Override + public Optional getBlockHeader(final Hash blockHeaderHash) { + return blockchainStorage.getBlockHeader(blockHeaderHash); + } + + @Override + public Optional getBlockBody(final Hash blockHeaderHash) { + return blockchainStorage.getBlockBody(blockHeaderHash); + } + + @Override + public Optional> getTxReceipts(final Hash blockHeaderHash) { + return blockchainStorage.getTransactionReceipts(blockHeaderHash); + } + + @Override + public Optional getBlockHashByNumber(final long number) { + return blockchainStorage.getBlockHash(number); + } + + @Override + public Optional getTotalDifficultyByHash(final Hash blockHeaderHash) { + return blockchainStorage.getTotalDifficulty(blockHeaderHash); + } + + @Override + public Optional getTransactionByHash(final Hash transactionHash) { + return blockchainStorage + .getTransactionLocation(transactionHash) + .flatMap( + l -> + blockchainStorage + .getBlockBody(l.getBlockHash()) + .map(b -> b.getTransactions().get(l.getTransactionIndex()))); + } + + @Override + public Optional getTransactionLocation(final Hash transactionHash) { + return blockchainStorage.getTransactionLocation(transactionHash); + } + + @Override + public synchronized void appendBlock(final Block block, final List receipts) { + checkArgument( + block.getBody().getTransactions().size() == receipts.size(), + "Supplied receipts do not match block transactions."); + if (blockIsAlreadyTracked(block)) { + return; + } + if (!blockIsConnected(block)) { + throw new IllegalArgumentException("Attempt to append non-connected block."); + } + + final BlockAddedEvent blockAddedEvent = appendBlockHelper(block, receipts); + notifyBlockAdded(blockAddedEvent); + } + + private BlockAddedEvent appendBlockHelper( + final Block block, final List receipts) { + final Hash hash = block.getHash(); + final UInt256 td = calculateTotalDifficulty(block); + + final BlockchainStorage.Updater updater = blockchainStorage.updater(); + + updater.putBlockHeader(hash, block.getHeader()); + updater.putBlockBody(hash, block.getBody()); + updater.putTransactionReceipts(hash, receipts); + updater.putTotalDifficulty(hash, td); + + // Update canonical chain data + final BlockAddedEvent blockAddedEvent = updateCanonicalChainData(updater, block, td); + + updater.commit(); + + return blockAddedEvent; + } + + private UInt256 calculateTotalDifficulty(final Block block) { + if (block.getHeader().getNumber() == BlockHeader.GENESIS_BLOCK_NUMBER) { + return block.getHeader().getDifficulty(); + } + + final Optional maybeParentId = + blockchainStorage.getTotalDifficulty(block.getHeader().getParentHash()); + if (!maybeParentId.isPresent()) { + throw new IllegalStateException("Blockchain is missing total difficulty data."); + } + final UInt256 parentTd = maybeParentId.get(); + return block.getHeader().getDifficulty().plus(parentTd); + } + + private BlockAddedEvent updateCanonicalChainData( + final BlockchainStorage.Updater updater, + final Block newBlock, + final UInt256 totalDifficulty) { + final Hash chainHead = blockchainStorage.getChainHead().orElse(null); + if (newBlock.getHeader().getNumber() != BlockHeader.GENESIS_BLOCK_NUMBER && chainHead == null) { + throw new IllegalStateException("Blockchain is missing chain head."); + } + + final Hash newBlockHash = newBlock.getHash(); + try { + if (chainHead == null || newBlock.getHeader().getParentHash().equals(chainHead)) { + // This block advances the chain, update the chain head + updater.putBlockHash(newBlock.getHeader().getNumber(), newBlockHash); + updater.setChainHead(newBlockHash); + indexTransactionForBlock(updater, newBlockHash, newBlock.getBody().getTransactions()); + return BlockAddedEvent.createForHeadAdvancement(newBlock); + } else if (totalDifficulty.compareTo(blockchainStorage.getTotalDifficulty(chainHead).get()) + > 0) { + // New block represents a chain reorganization + return handleChainReorg(updater, newBlock); + } else { + // New block represents a fork + return handleFork(updater, newBlock); + } + } catch (final NoSuchElementException e) { + // Any Optional.get() calls in this block should be present, missing data means data + // corruption or a bug. + updater.rollback(); + throw new IllegalStateException("Blockchain is missing data that should be present.", e); + } + } + + private BlockAddedEvent handleFork(final BlockchainStorage.Updater updater, final Block fork) { + final Collection forkHeads = blockchainStorage.getForkHeads(); + + // Check to see if this block advances any existing fork. + final Hash parentHash = fork.getHeader().getParentHash(); + final Optional parent = + forkHeads.stream().filter(head -> head.equals(parentHash)).findAny(); + // This block will replace its parent + parent.ifPresent(forkHeads::remove); + + forkHeads.add(fork.getHash()); + + updater.setForkHeads(forkHeads); + return BlockAddedEvent.createForFork(fork); + } + + private BlockAddedEvent handleChainReorg( + final BlockchainStorage.Updater updater, final Block newChainHead) { + final Hash oldChainHead = blockchainStorage.getChainHead().get(); + BlockHeader oldChain = blockchainStorage.getBlockHeader(oldChainHead).get(); + BlockHeader newChain = newChainHead.getHeader(); + + // Update chain head + updater.setChainHead(newChain.getHash()); + + // Track transactions to be added and removed + final Map> newTransactions = new HashMap<>(); + final List removedTransactions = new ArrayList<>(); + + while (newChain.getNumber() > oldChain.getNumber()) { + // If new chain is longer than old chain, walk back until we meet the old chain by number + // adding indexing for new chain along the way. + final Hash blockHash = newChain.getHash(); + updater.putBlockHash(newChain.getNumber(), blockHash); + final List newTxs = + blockHash.equals(newChainHead.getHash()) + ? newChainHead.getBody().getTransactions() + : blockchainStorage.getBlockBody(blockHash).get().getTransactions(); + newTransactions.put(blockHash, newTxs); + + newChain = blockchainStorage.getBlockHeader(newChain.getParentHash()).get(); + } + + while (oldChain.getNumber() > newChain.getNumber()) { + // If oldChain is longer than new chain, walk back until we meet the new chain by number, + // updating as we go. + updater.removeBlockHash(oldChain.getNumber()); + removedTransactions.addAll( + blockchainStorage.getBlockBody(oldChain.getHash()).get().getTransactions()); + + oldChain = blockchainStorage.getBlockHeader(oldChain.getParentHash()).get(); + } + + while (!oldChain.getHash().equals(newChain.getHash())) { + // Walk back until we meet the common ancestor between the two chains, updating as we go. + final Hash newBlockHash = newChain.getHash(); + updater.putBlockHash(newChain.getNumber(), newBlockHash); + + // Collect transaction to be updated + final List newTxs = + newBlockHash.equals(newChainHead.getHash()) + ? newChainHead.getBody().getTransactions() + : blockchainStorage.getBlockBody(newBlockHash).get().getTransactions(); + newTransactions.put(newBlockHash, newTxs); + removedTransactions.addAll( + blockchainStorage.getBlockBody(oldChain.getHash()).get().getTransactions()); + + newChain = blockchainStorage.getBlockHeader(newChain.getParentHash()).get(); + oldChain = blockchainStorage.getBlockHeader(oldChain.getParentHash()).get(); + } + + // Update indexed transactions + newTransactions.forEach( + (blockHash, transactionsInBlock) -> { + indexTransactionForBlock(updater, blockHash, transactionsInBlock); + // Don't remove transactions that are being re-indexed. + removedTransactions.removeAll(transactionsInBlock); + }); + clearIndexedTransactionsForBlock(updater, removedTransactions); + + // Update tracked forks + final Collection forks = blockchainStorage.getForkHeads(); + // Old head is now a fork + forks.add(oldChainHead); + // Remove new chain head's parent if it was tracked as a fork + final Optional parentFork = + forks.stream().filter(f -> f.equals(newChainHead.getHeader().getParentHash())).findAny(); + parentFork.ifPresent(forks::remove); + updater.setForkHeads(forks); + return BlockAddedEvent.createForChainReorg( + newChainHead, + newTransactions.values().stream().flatMap(Collection::stream).collect(toList()), + removedTransactions); + } + + private static void indexTransactionForBlock( + final BlockchainStorage.Updater updater, final Hash hash, final List txs) { + for (int i = 0; i < txs.size(); i++) { + final Hash txHash = txs.get(i).hash(); + final TransactionLocation loc = new TransactionLocation(hash, i); + updater.putTransactionLocation(txHash, loc); + } + } + + private static void clearIndexedTransactionsForBlock( + final BlockchainStorage.Updater updater, final List txs) { + for (final Transaction tx : txs) { + updater.removeTransactionLocation(tx.hash()); + } + } + + @VisibleForTesting + Set getForks() { + return new HashSet<>(blockchainStorage.getForkHeads()); + } + + protected void setGenesis(final Block genesisBlock) { + checkArgument( + genesisBlock.getHeader().getNumber() == BlockHeader.GENESIS_BLOCK_NUMBER, + "Invalid genesis block."); + final Optional maybeHead = blockchainStorage.getChainHead(); + if (!maybeHead.isPresent()) { + // Initialize blockchain store with genesis block. + final BlockchainStorage.Updater updater = blockchainStorage.updater(); + final Hash hash = genesisBlock.getHash(); + updater.putBlockHeader(hash, genesisBlock.getHeader()); + updater.putBlockBody(hash, genesisBlock.getBody()); + updater.putTransactionReceipts(hash, emptyList()); + updater.putTotalDifficulty(hash, calculateTotalDifficulty(genesisBlock)); + updater.putBlockHash(genesisBlock.getHeader().getNumber(), hash); + updater.setChainHead(hash); + updater.commit(); + } else { + // Verify genesis block is consistent with stored blockchain. + final Optional genesisHash = getBlockHashByNumber(BlockHeader.GENESIS_BLOCK_NUMBER); + if (!genesisHash.isPresent()) { + throw new IllegalStateException("Blockchain is missing genesis block data."); + } + if (!genesisHash.get().equals(genesisBlock.getHash())) { + throw new IllegalArgumentException( + "Supplied genesis block does not match stored chain data."); + } + } + } + + protected boolean blockIsAlreadyTracked(final Block block) { + return blockchainStorage.getBlockHeader(block.getHash()).isPresent(); + } + + protected boolean blockIsConnected(final Block block) { + return blockchainStorage.getBlockHeader(block.getHeader().getParentHash()).isPresent(); + } + + @Override + public long observeBlockAdded(final BlockAddedObserver observer) { + checkNotNull(observer); + return blockAddedObservers.subscribe(observer); + } + + @Override + public boolean removeObserver(final long observerId) { + return blockAddedObservers.unsubscribe(observerId); + } + + @VisibleForTesting + int observerCount() { + return blockAddedObservers.getSubscriberCount(); + } + + private void notifyBlockAdded(final BlockAddedEvent event) { + blockAddedObservers.forEach(observer -> observer.onBlockAdded(event, this)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/KeyValueStoragePrefixedKeyBlockchainStorage.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/KeyValueStoragePrefixedKeyBlockchainStorage.java new file mode 100755 index 00000000000..705d74dfbc1 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/KeyValueStoragePrefixedKeyBlockchainStorage.java @@ -0,0 +1,194 @@ +package net.consensys.pantheon.ethereum.db; + +import net.consensys.pantheon.ethereum.chain.TransactionLocation; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.Lists; + +public class KeyValueStoragePrefixedKeyBlockchainStorage implements BlockchainStorage { + + private static final BytesValue CHAIN_HEAD_KEY = + BytesValue.wrap("chainHeadHash".getBytes(StandardCharsets.UTF_8)); + private static final BytesValue FORK_HEADS_KEY = + BytesValue.wrap("forkHeads".getBytes(StandardCharsets.UTF_8)); + + private static final BytesValue CONSTANTS_PREFIX = BytesValue.of(1); + private static final BytesValue BLOCK_HEADER_PREFIX = BytesValue.of(2); + private static final BytesValue BLOCK_BODY_PREFIX = BytesValue.of(3); + private static final BytesValue TRANSACTION_RECEIPTS_PREFIX = BytesValue.of(4); + private static final BytesValue BLOCK_HASH_PREFIX = BytesValue.of(5); + private static final BytesValue TOTAL_DIFFICULTY_PREFIX = BytesValue.of(6); + private static final BytesValue TRANSACTION_LOCATION_PREFIX = BytesValue.of(7); + + private final KeyValueStorage storage; + private final BlockHashFunction blockHashFunction; + + public KeyValueStoragePrefixedKeyBlockchainStorage( + final KeyValueStorage storage, final BlockHashFunction blockHashFunction) { + this.storage = storage; + this.blockHashFunction = blockHashFunction; + } + + @Override + public Optional getChainHead() { + return get(CONSTANTS_PREFIX, CHAIN_HEAD_KEY).map(this::bytesToHash); + } + + @Override + public Collection getForkHeads() { + return get(CONSTANTS_PREFIX, FORK_HEADS_KEY) + .map(bytes -> RLP.input(bytes).readList(in -> this.bytesToHash(in.readBytes32()))) + .orElse(Lists.newArrayList()); + } + + @Override + public Optional getBlockHeader(final Hash blockHash) { + return get(BLOCK_HEADER_PREFIX, blockHash) + .map(b -> BlockHeader.readFrom(RLP.input(b), blockHashFunction)); + } + + @Override + public Optional getBlockBody(final Hash blockHash) { + return get(BLOCK_BODY_PREFIX, blockHash) + .map(bytesValue -> BlockBody.readFrom(RLP.input(bytesValue), blockHashFunction)); + } + + @Override + public Optional> getTransactionReceipts(final Hash blockHash) { + return get(TRANSACTION_RECEIPTS_PREFIX, blockHash).map(this::rlpDecodeTransactionReceipts); + } + + @Override + public Optional getBlockHash(final long blockNumber) { + return get(BLOCK_HASH_PREFIX, UInt256Bytes.of(blockNumber)).map(this::bytesToHash); + } + + @Override + public Optional getTotalDifficulty(final Hash blockHash) { + return get(TOTAL_DIFFICULTY_PREFIX, blockHash).map(b -> UInt256.wrap(Bytes32.wrap(b, 0))); + } + + @Override + public Optional getTransactionLocation(final Hash transactionHash) { + return get(TRANSACTION_LOCATION_PREFIX, transactionHash) + .map(bytesValue -> TransactionLocation.readFrom(RLP.input(bytesValue))); + } + + @Override + public Updater updater() { + return new Updater(storage.getStartTransaction()); + } + + private List rlpDecodeTransactionReceipts(final BytesValue bytes) { + return RLP.input(bytes).readList(TransactionReceipt::readFrom); + } + + private Hash bytesToHash(final BytesValue bytesValue) { + return Hash.wrap(Bytes32.wrap(bytesValue, 0)); + } + + private Optional get(final BytesValue prefix, final BytesValue key) { + return storage.get(BytesValues.concatenate(prefix, key)); + } + + public static class Updater implements BlockchainStorage.Updater { + + private final KeyValueStorage.Transaction transaction; + + private Updater(final KeyValueStorage.Transaction transaction) { + this.transaction = transaction; + } + + @Override + public void putBlockHeader(final Hash blockHash, final BlockHeader blockHeader) { + set(BLOCK_HEADER_PREFIX, blockHash, RLP.encode(blockHeader::writeTo)); + } + + @Override + public void putBlockBody(final Hash blockHash, final BlockBody blockBody) { + set(BLOCK_BODY_PREFIX, blockHash, RLP.encode(blockBody::writeTo)); + } + + @Override + public void putTransactionLocation( + final Hash transactionHash, final TransactionLocation transactionLocation) { + set(TRANSACTION_LOCATION_PREFIX, transactionHash, RLP.encode(transactionLocation::writeTo)); + } + + @Override + public void putTransactionReceipts( + final Hash blockHash, final List transactionReceipts) { + set(TRANSACTION_RECEIPTS_PREFIX, blockHash, rlpEncode(transactionReceipts)); + } + + @Override + public void putBlockHash(final long blockNumber, final Hash blockHash) { + set(BLOCK_HASH_PREFIX, UInt256Bytes.of(blockNumber), blockHash); + } + + @Override + public void putTotalDifficulty(final Hash blockHash, final UInt256 totalDifficulty) { + set(TOTAL_DIFFICULTY_PREFIX, blockHash, totalDifficulty.getBytes()); + } + + @Override + public void setChainHead(final Hash blockHash) { + set(CONSTANTS_PREFIX, CHAIN_HEAD_KEY, blockHash); + } + + @Override + public void setForkHeads(final Collection forkHeadHashes) { + final BytesValue data = + RLP.encode(o -> o.writeList(forkHeadHashes, (val, out) -> out.writeBytesValue(val))); + set(CONSTANTS_PREFIX, FORK_HEADS_KEY, data); + } + + @Override + public void removeBlockHash(final long blockNumber) { + remove(BLOCK_HASH_PREFIX, UInt256Bytes.of(blockNumber)); + } + + @Override + public void removeTransactionLocation(final Hash transactionHash) { + remove(TRANSACTION_LOCATION_PREFIX, transactionHash); + } + + @Override + public void commit() { + transaction.commit(); + } + + @Override + public void rollback() { + transaction.rollback(); + } + + private void set(final BytesValue prefix, final BytesValue key, final BytesValue value) { + transaction.put(BytesValues.concatenate(prefix, key), value); + } + + private void remove(final BytesValue prefix, final BytesValue key) { + transaction.remove(BytesValues.concatenate(prefix, key)); + } + + private BytesValue rlpEncode(final List receipts) { + return RLP.encode(o -> o.writeList(receipts, TransactionReceipt::writeTo)); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/WorldStateArchive.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/WorldStateArchive.java new file mode 100755 index 00000000000..ea43346c5ae --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/db/WorldStateArchive.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.db; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.trie.MerklePatriciaTrie; +import net.consensys.pantheon.ethereum.worldstate.DefaultMutableWorldState; +import net.consensys.pantheon.ethereum.worldstate.WorldStateStorage; + +public class WorldStateArchive { + private final WorldStateStorage storage; + private static final Hash EMPTY_ROOT_HASH = Hash.wrap(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH); + + public WorldStateArchive(final WorldStateStorage storage) { + this.storage = storage; + } + + public WorldState get(final Hash rootHash) { + return getMutable(rootHash); + } + + public MutableWorldState getMutable(final Hash rootHash) { + return new DefaultMutableWorldState(rootHash, storage); + } + + public WorldState get() { + return get(EMPTY_ROOT_HASH); + } + + public MutableWorldState getMutable() { + return getMutable(EMPTY_ROOT_HASH); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceFrame.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceFrame.java new file mode 100755 index 00000000000..d87439db871 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceFrame.java @@ -0,0 +1,97 @@ +package net.consensys.pantheon.ethereum.debug; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Optional; + +import com.google.common.base.MoreObjects; + +public class TraceFrame { + + private final int pc; + private final String opcode; + private final Gas gasRemaining; + private final Optional gasCost; + private final int depth; + private final EnumSet exceptionalHaltReasons; + private final Optional stack; + private final Optional memory; + private final Optional> storage; + + public TraceFrame( + final int pc, + final String opcode, + final Gas gasRemaining, + final Optional gasCost, + final int depth, + final EnumSet exceptionalHaltReasons, + final Optional stack, + final Optional memory, + final Optional> storage) { + this.pc = pc; + this.opcode = opcode; + this.gasRemaining = gasRemaining; + this.gasCost = gasCost; + this.depth = depth; + this.exceptionalHaltReasons = exceptionalHaltReasons; + this.stack = stack; + this.memory = memory; + this.storage = storage; + } + + public int getPc() { + return pc; + } + + public String getOpcode() { + return opcode; + } + + public Gas getGasRemaining() { + return gasRemaining; + } + + public Optional getGasCost() { + return gasCost; + } + + public int getDepth() { + return depth; + } + + public EnumSet getExceptionalHaltReasons() { + return exceptionalHaltReasons; + } + + public Optional getStack() { + return stack; + } + + public Optional getMemory() { + return memory; + } + + public Optional> getStorage() { + return storage; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("pc", pc) + .add("opcode", opcode) + .add("getGasRemaining", gasRemaining) + .add("gasCost", gasCost) + .add("depth", depth) + .add("exceptionalHaltReasons", exceptionalHaltReasons) + .add("stack", stack) + .add("memory", memory) + .add("storage", storage) + .toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceOptions.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceOptions.java new file mode 100755 index 00000000000..c7df5bb2146 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/debug/TraceOptions.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.debug; + +public class TraceOptions { + + private final boolean traceStorage; + private final boolean traceMemory; + private final boolean traceStack; + + public static final TraceOptions DEFAULT = new TraceOptions(true, true, true); + + public TraceOptions( + final boolean traceStorage, final boolean traceMemory, final boolean traceStack) { + this.traceStorage = traceStorage; + this.traceMemory = traceMemory; + this.traceStack = traceStack; + } + + public boolean isStorageEnabled() { + return traceStorage; + } + + public boolean isMemoryEnabled() { + return traceMemory; + } + + public boolean isStackEnabled() { + return traceStack; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentDifficultyCalculators.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentDifficultyCalculators.java new file mode 100755 index 00000000000..8e76aa4ed54 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentDifficultyCalculators.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.development; + +import net.consensys.pantheon.ethereum.mainnet.DifficultyCalculator; + +import java.math.BigInteger; + +/** + * This provides a difficulty calculator that can be used during development efforts; given + * development (typically) uses CPU based mining, a negligible difficulty ensures tests etc. execute + * quickly. + */ +public class DevelopmentDifficultyCalculators { + + public static final BigInteger MINIMUM_DIFFICULTY = BigInteger.valueOf(500L); + + public static DifficultyCalculator DEVELOPER = + (time, parent, context) -> { + return MINIMUM_DIFFICULTY; + }; +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSchedule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSchedule.java new file mode 100755 index 00000000000..da0978d19cd --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSchedule.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.ethereum.development; + +import static net.consensys.pantheon.ethereum.mainnet.MainnetTransactionValidator.NO_CHAIN_ID; + +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +import io.vertx.core.json.JsonObject; + +/** + * A mock ProtocolSchedule which behaves similarly to Frontier (but for all blocks), albeit with a + * much reduced difficulty (which supports testing on CPU alone). + */ +public class DevelopmentProtocolSchedule { + + public static ProtocolSchedule create(final JsonObject config) { + final Integer chainId = config.getInteger("chainId", NO_CHAIN_ID); + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone(0, DevelopmentProtocolSpecs.first(chainId, protocolSchedule)); + return protocolSchedule; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSpecs.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSpecs.java new file mode 100755 index 00000000000..2d6f64c6db6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolSpecs.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.development; + +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; + +/** + * Provides a protocol specification which is suitable for use on private, PoW networks, where block + * mining is performed on CPUs alone. + */ +public class DevelopmentProtocolSpecs { + + /* + * The DevelopmentProtocolSpecification is the same as the byzantium spec, but with a much reduced + * difficulty calculator (to support CPU mining). + */ + public static ProtocolSpec first( + final Integer chainId, final ProtocolSchedule protocolSchedule) { + return MainnetProtocolSpecs.byzantiumDefinition(chainId) + .difficultyCalculator(DevelopmentDifficultyCalculators.DEVELOPER) + .name("first") + .build(protocolSchedule); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractMessageProcessor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractMessageProcessor.java new file mode 100755 index 00000000000..52966f6f482 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractMessageProcessor.java @@ -0,0 +1,182 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.OperationTracer; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * A skeletal class for instantiating message processors. + * + *

The following methods have been created to be invoked when the message state changes via the + * {@link MessageFrame.State}. Note that some of these methods are abstract while others have + * default behaviors. There is currently no method for responding to a {@link + * MessageFrame.State#CODE_SUSPENDED}. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
{@code MessageFrame.State}Method
{@link MessageFrame.State#NOT_STARTED}{@link AbstractMessageProcessor#start(MessageFrame)}
{@link MessageFrame.State#CODE_EXECUTING}{@link AbstractMessageProcessor#codeExecute(MessageFrame, OperationTracer)}
{@link MessageFrame.State#CODE_SUCCESS}{@link AbstractMessageProcessor#codeSuccess(MessageFrame)}
{@link MessageFrame.State#COMPLETED_FAILED}{@link AbstractMessageProcessor#completedFailed(MessageFrame)}
{@link MessageFrame.State#COMPLETED_SUCCESS}{@link AbstractMessageProcessor#completedSuccess(MessageFrame)}
+ */ +public abstract class AbstractMessageProcessor { + + // List of addresses to force delete when they are touched but empty + // when the state changes in the message are were not meant to be committed. + private final Collection

forceDeleteAccountsWhenEmpty; + private final EVM evm; + + protected AbstractMessageProcessor( + final EVM evm, final Collection
forceDeleteAccountsWhenEmpty) { + this.evm = evm; + this.forceDeleteAccountsWhenEmpty = forceDeleteAccountsWhenEmpty; + } + + protected abstract void start(MessageFrame frame); + + /** + * Gets called when the message frame code executes successfully. + * + * @param frame The message frame + */ + protected abstract void codeSuccess(MessageFrame frame); + + private void clearAccumulatedStateBesidesGasAndOutput(final MessageFrame frame) { + final Collection
addressesToForceCommit = + frame + .getWorldState() + .getTouchedAccounts() + .stream() + .filter(a -> forceDeleteAccountsWhenEmpty.contains(a.getAddress()) && a.isEmpty()) + .map(Account::getAddress) + .collect(Collectors.toCollection(ArrayList::new)); + + // Clear any pending changes. + frame.getWorldState().revert(); + + // Force delete any requested accounts and commit the changes. + addressesToForceCommit.forEach(h -> frame.getWorldState().deleteAccount(h)); + frame.getWorldState().commit(); + + frame.clearLogs(); + frame.clearSelfDestructs(); + frame.clearGasRefund(); + } + + /** + * Gets called when the message frame encounters an exceptional halt. + * + * @param frame The message frame + */ + protected void exceptionalHalt(final MessageFrame frame) { + clearAccumulatedStateBesidesGasAndOutput(frame); + frame.clearGasRemaining(); + frame.clearOutputData(); + frame.setState(MessageFrame.State.COMPLETED_FAILED); + } + + /** + * Gets called when the message frame requests a revert. + * + * @param frame The message frame + */ + protected void revert(final MessageFrame frame) { + clearAccumulatedStateBesidesGasAndOutput(frame); + frame.setState(MessageFrame.State.COMPLETED_FAILED); + } + + /** + * Gets called when the message frame completes successfully. + * + * @param frame The message frame + */ + protected void completedSuccess(final MessageFrame frame) { + frame.getWorldState().commit(); + frame.getMessageFrameStack().removeFirst(); + frame.notifyCompletion(); + } + + /** + * Gets called when the message frame execution fails. + * + * @param frame The message frame + */ + protected void completedFailed(final MessageFrame frame) { + frame.getMessageFrameStack().removeFirst(); + frame.notifyCompletion(); + } + + /** + * Executes the message frame code until it halts. + * + * @param frame The message frame + * @param operationTracer The tracer recording execution + */ + private void codeExecute(final MessageFrame frame, final OperationTracer operationTracer) { + try { + evm.runToHalt(frame, operationTracer); + } catch (final ExceptionalHaltException e) { + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + } + } + + public void process(final MessageFrame frame, final OperationTracer operationTracer) { + if (frame.getState() == MessageFrame.State.NOT_STARTED) { + start(frame); + } + + if (frame.getState() == MessageFrame.State.CODE_EXECUTING) { + codeExecute(frame, operationTracer); + + if (frame.getState() == MessageFrame.State.CODE_SUSPENDED) { + return; + } + + if (frame.getState() == MessageFrame.State.CODE_SUCCESS) { + codeSuccess(frame); + } + } + + if (frame.getState() == MessageFrame.State.EXCEPTIONAL_HALT) { + exceptionalHalt(frame); + } + + if (frame.getState() == MessageFrame.State.REVERT) { + revert(frame); + } + + if (frame.getState() == MessageFrame.State.COMPLETED_SUCCESS) { + completedSuccess(frame); + } + + if (frame.getState() == MessageFrame.State.COMPLETED_FAILED) { + completedFailed(frame); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractPrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractPrecompiledContract.java new file mode 100755 index 00000000000..4b511e2c669 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AbstractPrecompiledContract.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.BytesValue; + +/** Skeleton class for @{link PrecompileContract} implementations. */ +public abstract class AbstractPrecompiledContract implements PrecompiledContract { + + private final GasCalculator gasCalculator; + + private final String name; + + public AbstractPrecompiledContract(final String name, final GasCalculator gasCalculator) { + this.name = name; + this.gasCalculator = gasCalculator; + } + + protected GasCalculator gasCalculator() { + return gasCalculator; + } + + @Override + public String getName() { + return name; + } + + @Override + public abstract Gas gasRequirement(BytesValue input); + + @Override + public abstract BytesValue compute(BytesValue input); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AttachedBlockHeaderValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AttachedBlockHeaderValidationRule.java new file mode 100755 index 00000000000..c44bac4f48f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/AttachedBlockHeaderValidationRule.java @@ -0,0 +1,17 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.BlockHeader; + +public interface AttachedBlockHeaderValidationRule { + + /** + * Validates a block header against its ancestors. + * + * @param header the block header to validate + * @param parent the block header corresponding to the parent of the header being validated. + * @param protocolContext the protocol context + * @return {@code true} if valid; otherwise {@code false} + */ + boolean validate(BlockHeader header, BlockHeader parent, ProtocolContext protocolContext); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockBodyValidator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockBodyValidator.java new file mode 100755 index 00000000000..e40254f6d5c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockBodyValidator.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; + +import java.util.List; + +/** Validates block bodies. */ +public interface BlockBodyValidator { + + /** + * Validates that the block body is valid. + * + * @param context The context to validate against + * @param block The block to validate + * @param receipts The receipts that correspond to the blocks transactions + * @param worldStateRootHash The rootHash defining the world state after processing this block and + * all of its transactions. + * @return {@code true} if valid; otherwise {@code false} + */ + boolean validateBody( + ProtocolContext context, + Block block, + List receipts, + Hash worldStateRootHash); + + /** + * Validates that the block body is valid, but skips state root validation. + * + * @param context The context to validate against + * @param block The block to validate + * @param receipts The receipts that correspond to the blocks transactions + * @return {@code true} if valid; otherwise {@code false} + */ + boolean validateBodyLight( + ProtocolContext context, Block block, List receipts); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidator.java new file mode 100755 index 00000000000..70326b3e37a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidator.java @@ -0,0 +1,128 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.BlockHeader; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class BlockHeaderValidator { + + private static final Logger LOGGER = LogManager.getLogger(BlockHeaderValidator.class); + + private final List> rules; + + private BlockHeaderValidator(final List> rules) { + this.rules = rules; + } + + public boolean validateHeader( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext protocolContext, + final HeaderValidationMode mode) { + switch (mode) { + case NONE: + return true; + case LIGHT: + return applyRules(header, parent, protocolContext, Rule::includeInLightValidation); + case DETACHED_ONLY: + return applyRules(header, parent, protocolContext, Rule::isDetachedSupported); + case SKIP_DETACHED: + return applyRules(header, parent, protocolContext, rule -> !rule.isDetachedSupported()); + case FULL: + return applyRules(header, parent, protocolContext, rule -> true); + } + throw new IllegalArgumentException("Unknown HeaderValidationMode: " + mode); + } + + public boolean validateHeader( + final BlockHeader header, + final ProtocolContext protocolContext, + final HeaderValidationMode mode) { + if (mode == HeaderValidationMode.NONE) { + return true; + } + return getParent(header, protocolContext) + .map(parentHeader -> validateHeader(header, parentHeader, protocolContext, mode)) + .orElse(false); + } + + private boolean applyRules( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext protocolContext, + final Predicate> filter) { + return rules + .stream() + .filter(filter) + .allMatch(rule -> rule.validate(header, parent, protocolContext)); + } + + private Optional getParent( + final BlockHeader header, final ProtocolContext context) { + final Optional parent = + context.getBlockchain().getBlockHeader(header.getParentHash()); + if (!parent.isPresent()) { + LOGGER.trace("Invalid block header: cannot determine parent header"); + } + return parent; + } + + private static class Rule { + private final boolean detachedSupported; + private final AttachedBlockHeaderValidationRule rule; + private final boolean includeInLightValidation; + + private Rule( + final boolean detachedSupported, + final AttachedBlockHeaderValidationRule rule, + final boolean includeInLightValidation) { + this.detachedSupported = detachedSupported; + this.rule = rule; + this.includeInLightValidation = includeInLightValidation; + } + + public boolean isDetachedSupported() { + return detachedSupported; + } + + public boolean validate( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext protocolContext) { + return this.rule.validate(header, parent, protocolContext); + } + + public boolean includeInLightValidation() { + return includeInLightValidation; + } + } + + public static class Builder { + private final List> rules = new ArrayList<>(); + + public Builder addRule(final AttachedBlockHeaderValidationRule rule) { + this.rules.add(new Rule<>(false, rule, true)); + return this; + } + + public Builder addRule(final DetachedBlockHeaderValidationRule rule) { + this.rules.add( + new Rule<>( + true, + (header, parent, protocolContext) -> rule.validate(header, parent), + rule.includeInLightValidation())); + return this; + } + + public BlockHeaderValidator build() { + return new BlockHeaderValidator<>(rules); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockProcessor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockProcessor.java new file mode 100755 index 00000000000..9ce38495522 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BlockProcessor.java @@ -0,0 +1,69 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; + +import java.util.List; + +/** Processes a block. */ +public interface BlockProcessor { + + /** A block processing result. */ + interface Result { + + /** + * The receipts generated for the transactions in a block + * + *

This is only valid when {@code BlockProcessor#isSuccessful} returns {@code true}. + * + * @return the receipts generated for the transactions the a block + */ + List getReceipts(); + + /** + * Returns whether the block was successfully processed. + * + * @return {@code true} if the block was processed successfully; otherwise {@code false} + */ + boolean isSuccessful(); + } + + /** + * Processes the block. + * + * @param blockchain the blockchain to append the block to + * @param worldState the world state to apply changes to + * @param block the block to process + * @return the block processing result + */ + default Result processBlock( + final Blockchain blockchain, final MutableWorldState worldState, final Block block) { + return processBlock( + blockchain, + worldState, + block.getHeader(), + block.getBody().getTransactions(), + block.getBody().getOmmers()); + } + + /** + * Processes the block. + * + * @param blockchain the blockchain to append the block to + * @param worldState the world state to apply changes to + * @param blockHeader the block header for the block + * @param transactions the transactions in the block + * @param ommers the block ommers + * @return the block processing result + */ + Result processBlock( + Blockchain blockchain, + MutableWorldState worldState, + BlockHeader blockHeader, + List transactions, + List ommers); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BodyValidation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BodyValidation.java new file mode 100755 index 00000000000..707ccbd5219 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/BodyValidation.java @@ -0,0 +1,91 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static net.consensys.pantheon.crypto.Hash.keccak256; +import static net.consensys.pantheon.util.bytes.BytesValues.trimLeadingZeros; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.trie.MerklePatriciaTrie; +import net.consensys.pantheon.ethereum.trie.SimpleMerklePatriciaTrie; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; + +/** A utility class for body validation tasks. */ +public final class BodyValidation { + + private BodyValidation() { + // Utility Class + } + + private static BytesValue indexKey(final int i) { + return RLP.encodeOne(trimLeadingZeros(UInt256.of(i).getBytes())); + } + + private static MerklePatriciaTrie trie() { + return new SimpleMerklePatriciaTrie<>(b -> b); + } + + /** + * Generates the transaction root for a list of transactions + * + * @param transactions the transactions + * @return the transaction root + */ + public static Hash transactionsRoot(final List transactions) { + final MerklePatriciaTrie trie = trie(); + + for (int i = 0; i < transactions.size(); ++i) { + trie.put(indexKey(i), RLP.encode(transactions.get(i)::writeTo)); + } + + return Hash.wrap(trie.getRootHash()); + } + + /** + * Generates the receipt root for a list of receipts + * + * @param receipts the receipts + * @return the receipt root + */ + public static Hash receiptsRoot(final List receipts) { + final MerklePatriciaTrie trie = trie(); + + for (int i = 0; i < receipts.size(); ++i) { + trie.put(indexKey(i), RLP.encode(receipts.get(i)::writeTo)); + } + + return Hash.wrap(trie.getRootHash()); + } + + /** + * Generates the ommers hash for a list of ommer block headers + * + * @param ommers the ommer block headers + * @return the ommers hash + */ + public static Hash ommersHash(final List ommers) { + return Hash.wrap(keccak256(RLP.encode(out -> out.writeList(ommers, BlockHeader::writeTo)))); + } + + /** + * Generates the logs bloom filter for a list of transaction receipts + * + * @param receipts the transaction receipts + * @return the logs bloom filter + */ + public static LogsBloomFilter logsBloom(final List receipts) { + final LogsBloomFilter logsBloom = new LogsBloomFilter(); + + for (final TransactionReceipt receipt : receipts) { + logsBloom.digest(receipt.getBloomFilter()); + } + + return logsBloom; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ConstantinopleGasCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ConstantinopleGasCalculator.java new file mode 100755 index 00000000000..c0a7fb65a4a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ConstantinopleGasCalculator.java @@ -0,0 +1,17 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class ConstantinopleGasCalculator extends SpuriousDragonGasCalculator { + + @Override + public Gas create2OperationGasCost(final MessageFrame frame) { + final UInt256 initCodeLength = frame.getStackItem(2).asUInt256(); + final UInt256 numWords = initCodeLength.dividedCeilBy(Bytes32.SIZE); + final Gas initCodeHashCost = SHA3_OPERATION_WORD_GAS_COST.times(Gas.of(numWords)); + return createOperationGasCost(frame).plus(initCodeHashCost); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DetachedBlockHeaderValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DetachedBlockHeaderValidationRule.java new file mode 100755 index 00000000000..5fa6e5ecd31 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DetachedBlockHeaderValidationRule.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHeader; + +public interface DetachedBlockHeaderValidationRule { + + /** + * Validates a block header against its parent. + * + * @param header the block header to validate + * @param parent the block header corresponding to the parent of the header being validated. + * @return {@code true} if valid; otherwise {@code false} + */ + boolean validate(BlockHeader header, BlockHeader parent); + + default boolean includeInLightValidation() { + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DifficultyCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DifficultyCalculator.java new file mode 100755 index 00000000000..290e1927fd7 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/DifficultyCalculator.java @@ -0,0 +1,21 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.BlockHeader; + +import java.math.BigInteger; + +/** Calculates block difficulties. */ +@FunctionalInterface +public interface DifficultyCalculator { + + /** + * Calculates the block difficulty for a block. + * + * @param time the time the block was generated + * @param parent the block's parent block header + * @param context the context in which the difficulty calculator should operate + * @return the block difficulty + */ + BigInteger nextDifficulty(long time, BlockHeader parent, ProtocolContext context); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHash.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHash.java new file mode 100755 index 00000000000..af71d222e94 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHash.java @@ -0,0 +1,329 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.crypto.BouncyCastleMessageDigestFactory; +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.ethereum.core.SealableBlockHeader; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.function.BiConsumer; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; +import org.bouncycastle.jcajce.provider.digest.Keccak; + +/** Implementation of EthHash. */ +public final class EthHash { + + public static final int HASH_BYTES = 64; + + public static final BigInteger TARGET_UPPER_BOUND = BigInteger.valueOf(2).pow(256); + + private static final int EPOCH_LENGTH = 30000; + + private static final int DATASET_INIT_BYTES = 1 << 30; + + private static final int DATASET_GROWTH_BYTES = 1 << 23; + + private static final int CACHE_INIT_BYTES = 1 << 24; + + private static final int CACHE_GROWTH_BYTES = 1 << 17; + + private static final int MIX_BYTES = 128; + + private static final int HASH_WORDS = 16; + + private static final int CACHE_ROUNDS = 3; + + private static final int WORD_BYTES = 4; + + private static final int DATASET_PARENTS = 256; + + private static final int ACCESSES = 64; + + private static final ThreadLocal KECCAK_256 = + ThreadLocal.withInitial( + () -> { + try { + return BouncyCastleMessageDigestFactory.create(Hash.KECCAK256_ALG); + } catch (final NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + }); + + private static final ThreadLocal KECCAK_512 = + ThreadLocal.withInitial(Keccak.Digest512::new); + + /** + * Hashimoto Light Implementation. + * + * @param size Dataset size for the given header hash + * @param cache EthHash Cache + * @param header Truncated BlockHeader hash + * @param nonce Nonce to use for hashing + * @return A byte array holding MixHash in its first 32 bytes and the EthHash result in the in + * bytes 32 to 63 + */ + public static byte[] hashimotoLight( + final long size, final int[] cache, final byte[] header, final long nonce) { + return hashimoto(header, size, nonce, (target, ind) -> calcDatasetItem(target, cache, ind)); + } + + public static byte[] hashimoto( + final byte[] header, + final long size, + final long nonce, + final BiConsumer datasetLookup) { + final int n = (int) Long.divideUnsigned(size, MIX_BYTES); + final MessageDigest keccak512 = KECCAK_512.get(); + keccak512.update(header); + keccak512.update(Longs.toByteArray(Long.reverseBytes(nonce))); + final byte[] seed = keccak512.digest(); + final ByteBuffer mixBuffer = ByteBuffer.allocate(MIX_BYTES).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < MIX_BYTES / HASH_BYTES; ++i) { + mixBuffer.put(seed); + } + mixBuffer.position(0); + final int[] mix = new int[MIX_BYTES / 4]; + for (int i = 0; i < MIX_BYTES / 4; ++i) { + mix[i] = mixBuffer.getInt(); + } + final byte[] lookupResult = new byte[HASH_BYTES]; + final byte[] temp = new byte[MIX_BYTES]; + for (int i = 0; i < ACCESSES; ++i) { + final int p = + Integer.remainderUnsigned( + fnv(i ^ readLittleEndianInt(seed, 0), mix[i % (MIX_BYTES / WORD_BYTES)]), n); + for (int j = 0; j < MIX_BYTES / HASH_BYTES; ++j) { + datasetLookup.accept(lookupResult, 2 * p + j); + System.arraycopy(lookupResult, 0, temp, j * HASH_BYTES, HASH_BYTES); + } + fnvHash(mix, temp); + } + final int[] cmix = new int[mix.length / 4]; + for (int i = 0; i < mix.length; i += 4) { + cmix[i / 4] = fnv(fnv(fnv(mix[i], mix[i + 1]), mix[i + 2]), mix[i + 3]); + } + final byte[] result = new byte[32 + 32]; + intToByte(result, cmix); + final MessageDigest keccak256 = KECCAK_256.get(); + keccak256.update(seed); + keccak256.update(result, 0, 32); + try { + keccak256.digest(result, 32, 32); + } catch (final DigestException ex) { + throw new IllegalStateException(ex); + } + return result; + } + + /** + * Calculates a dataset item and writes it to a given buffer. + * + * @param buffer Buffer to store dataset item in + * @param cache EthHash Cache + * @param index Index of the dataset item to calculate + */ + public static void calcDatasetItem(final byte[] buffer, final int[] cache, final int index) { + final int rows = cache.length / HASH_WORDS; + final int[] mixInts = new int[HASH_BYTES / 4]; + final int offset = index % rows * HASH_WORDS; + mixInts[0] = cache[offset] ^ index; + System.arraycopy(cache, offset + 1, mixInts, 1, HASH_WORDS - 1); + intToByte(buffer, mixInts); + final MessageDigest keccak512 = KECCAK_512.get(); + keccak512.update(buffer); + try { + keccak512.digest(buffer, 0, HASH_BYTES); + ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(mixInts); + for (int i = 0; i < DATASET_PARENTS; ++i) { + fnvHash( + mixInts, + cache, + Integer.remainderUnsigned(fnv(index ^ i, mixInts[i % 16]), rows) * HASH_WORDS); + } + intToByte(buffer, mixInts); + keccak512.update(buffer); + keccak512.digest(buffer, 0, HASH_BYTES); + } catch (final DigestException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Hashes a BlockHeader without its nonce and MixHash. + * + * @param header Block Header + * @return Truncated BlockHeader hash + */ + public static byte[] hashHeader(final SealableBlockHeader header) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.writeBytesValue(header.getParentHash()); + out.writeBytesValue(header.getOmmersHash()); + out.writeBytesValue(header.getCoinbase()); + out.writeBytesValue(header.getStateRoot()); + out.writeBytesValue(header.getTransactionsRoot()); + out.writeBytesValue(header.getReceiptsRoot()); + out.writeBytesValue(header.getLogsBloom().getBytes()); + out.writeUInt256Scalar(header.getDifficulty()); + out.writeLongScalar(header.getNumber()); + out.writeLongScalar(header.getGasLimit()); + out.writeLongScalar(header.getGasUsed()); + out.writeLongScalar(header.getTimestamp()); + out.writeBytesValue(header.getExtraData()); + out.endList(); + return KECCAK_256.get().digest(out.encoded().extractArray()); + } + + /** + * Calculates the EthHash Epoch for a given block number. + * + * @param block Block Number + * @return EthHash Epoch + */ + public static long epoch(final long block) { + return Long.divideUnsigned(block, EPOCH_LENGTH); + } + + /** + * Generates the EthHash cache for given parameters. + * + * @param cacheSize Size of the cache to generate + * @param block Block Number to generate cache for + * @return EthHash Cache + */ + public static int[] mkCache(final int cacheSize, final long block) { + final MessageDigest keccak512 = KECCAK_512.get(); + keccak512.update(seed(block)); + final int rows = cacheSize / HASH_BYTES; + final byte[] cache = new byte[rows * HASH_BYTES]; + try { + keccak512.digest(cache, 0, HASH_BYTES); + } catch (final DigestException ex) { + throw new IllegalStateException(ex); + } + for (int i = 1; i < rows; ++i) { + keccak512.update(cache, (i - 1) * HASH_BYTES, HASH_BYTES); + try { + keccak512.digest(cache, i * HASH_BYTES, HASH_BYTES); + } catch (final DigestException ex) { + throw new IllegalStateException(ex); + } + } + final byte[] temp = new byte[HASH_BYTES]; + for (int i = 0; i < CACHE_ROUNDS; ++i) { + for (int j = 0; j < rows; ++j) { + final int offset = j * HASH_BYTES; + for (int k = 0; k < HASH_BYTES; ++k) { + temp[k] = + (byte) + (cache[(j - 1 + rows) % rows * HASH_BYTES + k] + ^ cache[ + Integer.remainderUnsigned(readLittleEndianInt(cache, offset), rows) + * HASH_BYTES + + k]); + } + keccak512.update(temp); + try { + keccak512.digest(temp, 0, HASH_BYTES); + } catch (final DigestException ex) { + throw new IllegalStateException(ex); + } + System.arraycopy(temp, 0, cache, offset, HASH_BYTES); + } + } + final int[] result = new int[cache.length / 4]; + ByteBuffer.wrap(cache).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().get(result); + return result; + } + + /** + * Calculates EthHash Cache size at a given epoch. + * + * @param epoch EthHash Epoch + * @return Cache size + */ + public static long cacheSize(final long epoch) { + long size = epoch * CACHE_GROWTH_BYTES + CACHE_INIT_BYTES - HASH_BYTES; + while (!isPrime(Long.divideUnsigned(size, HASH_BYTES))) { + size -= 2 * HASH_BYTES; + } + return size; + } + + /** + * Calculates EthHash DataSet size at a given epoch. + * + * @param epoch EthHash Epoch + * @return DataSet size + */ + public static long datasetSize(final long epoch) { + long size = epoch * DATASET_GROWTH_BYTES + DATASET_INIT_BYTES - MIX_BYTES; + while (!isPrime(Long.divideUnsigned(size, MIX_BYTES))) { + size -= 2 * MIX_BYTES; + } + return size; + } + + private static boolean isPrime(final long num) { + if (num > 2 && (num & 1) == 0) { + return false; + } + for (int i = 3; i * i <= num; i += 2) { + if (num % i == 0) { + return false; + } + } + return true; + } + + private static byte[] seed(final long block) { + final byte[] seed = new byte[32]; + if (Long.compareUnsigned(block, EPOCH_LENGTH) >= 0) { + final MessageDigest keccak256 = KECCAK_256.get(); + for (int i = 0; i < Long.divideUnsigned(block, EPOCH_LENGTH); ++i) { + keccak256.update(seed); + try { + keccak256.digest(seed, 0, seed.length); + } catch (final DigestException ex) { + throw new IllegalStateException(ex); + } + } + } + return seed; + } + + private static int readLittleEndianInt(final byte[] buffer, final int offset) { + return Ints.fromBytes( + buffer[offset + 3], buffer[offset + 2], buffer[offset + 1], buffer[offset]); + } + + private static void intToByte(final byte[] target, final int[] ints) { + final ByteBuffer buffer = ByteBuffer.wrap(target).order(ByteOrder.LITTLE_ENDIAN); + for (final int i : ints) { + buffer.putInt(i); + } + } + + private static void fnvHash(final int[] mix, final byte[] cache) { + for (int i = 0; i < mix.length; i++) { + mix[i] = fnv(mix[i], readLittleEndianInt(cache, i * Integer.BYTES)); + } + } + + private static void fnvHash(final int[] mix, final int[] cache, final int offset) { + for (int i = 0; i < mix.length; i++) { + mix[i] = fnv(mix[i], cache[offset + i]); + } + } + + private static int fnv(final int a, final int b) { + return a * 0x01000193 ^ b; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreator.java new file mode 100755 index 00000000000..0882f68b9f8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreator.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.AbstractBlockCreator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.SealableBlockHeader; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolver.EthHashSolverJob; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +public class EthHashBlockCreator extends AbstractBlockCreator { + + private final EthHashSolver nonceSolver; + + public EthHashBlockCreator( + final Address coinbase, + final ExtraDataCalculator extraDataCalculator, + final PendingTransactions pendingTransactions, + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final Function gasLimitCalculator, + final EthHashSolver nonceSolver, + final Wei minTransactionGasPrice, + final BlockHeader parentHeader) { + super( + coinbase, + extraDataCalculator, + pendingTransactions, + protocolContext, + protocolSchedule, + gasLimitCalculator, + minTransactionGasPrice, + coinbase, + parentHeader); + + this.nonceSolver = nonceSolver; + } + + @Override + protected BlockHeader createFinalBlockHeader(final SealableBlockHeader sealableBlockHeader) { + final EthHashSolverInputs workDefinition = generateNonceSolverInputs(sealableBlockHeader); + EthHashSolution solution; + try { + solution = nonceSolver.solveFor(EthHashSolverJob.createFromInputs(workDefinition)); + } catch (final InterruptedException ex) { + throw new CancellationException(); + } catch (final ExecutionException ex) { + throw new RuntimeException("Failure occurred during nonce calculations."); + } + return BlockHeaderBuilder.create() + .populateFrom(sealableBlockHeader) + .mixHash(solution.getMixHash()) + .nonce(solution.getNonce()) + .blockHashFunction(MainnetBlockHashFunction::createHash) + .buildBlockHeader(); + } + + private EthHashSolverInputs generateNonceSolverInputs( + final SealableBlockHeader sealableBlockHeader) { + final BigInteger difficulty = + BytesValues.asUnsignedBigInteger(sealableBlockHeader.getDifficulty().getBytes()); + final UInt256 target = UInt256.of(EthHash.TARGET_UPPER_BOUND.divide(difficulty)); + + return new EthHashSolverInputs( + target, EthHash.hashHeader(sealableBlockHeader), sealableBlockHeader.getNumber()); + } + + public Optional getWorkDefinition() { + return nonceSolver.getWorkDefinition(); + } + + public Optional getHashesPerSecond() { + return nonceSolver.hashesPerSecond(); + } + + public boolean submitWork(final EthHashSolution solution) { + return nonceSolver.submitSolution(solution); + } + + @Override + public void cancel() { + super.cancel(); + nonceSolver.cancel(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashCacheFactory.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashCacheFactory.java new file mode 100755 index 00000000000..3d7bbe3d8ec --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashCacheFactory.java @@ -0,0 +1,49 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import java.util.concurrent.ExecutionException; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.primitives.Ints; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class EthHashCacheFactory { + + private static final Logger LOGGER = LogManager.getLogger(); + + public static class EthHashDescriptor { + private final long datasetSize; + private final int[] cache; + + public EthHashDescriptor(final long datasetSize, final int[] cache) { + this.datasetSize = datasetSize; + this.cache = cache; + } + + public long getDatasetSize() { + return datasetSize; + } + + public int[] getCache() { + return cache; + } + } + + Cache descriptorCache = CacheBuilder.newBuilder().maximumSize(5).build(); + + public EthHashDescriptor ethHashCacheFor(final long blockNumber) { + final Long epochIndex = EthHash.epoch(blockNumber); + try { + return descriptorCache.get(epochIndex, () -> createHashCache(epochIndex, blockNumber)); + } catch (final ExecutionException ex) { + throw new RuntimeException("Failed to create a suitable cache for EthHash calculations.", ex); + } + } + + private EthHashDescriptor createHashCache(final long epochIndex, final long blockNumber) { + final int[] cache = + EthHash.mkCache(Ints.checkedCast(EthHash.cacheSize(epochIndex)), blockNumber); + return new EthHashDescriptor(EthHash.datasetSize(epochIndex), cache); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolution.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolution.java new file mode 100755 index 00000000000..1ca3bbae01e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolution.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Hash; + +public class EthHashSolution { + private final long nonce; + private final Hash mixHash; + private final byte[] powHash; + + public EthHashSolution(final long nonce, final Hash mixHash, final byte[] powHash) { + this.nonce = nonce; + this.mixHash = mixHash; + this.powHash = powHash; + } + + public long getNonce() { + return nonce; + } + + public Hash getMixHash() { + return mixHash; + } + + public byte[] getPowHash() { + return powHash; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolver.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolver.java new file mode 100755 index 00000000000..6eb0ca2aa57 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolver.java @@ -0,0 +1,147 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Stopwatch; + +public class EthHashSolver { + + public static class EthHashSolverJob { + + private final EthHashSolverInputs inputs; + private final CompletableFuture nonceFuture; + + public EthHashSolverJob( + final EthHashSolverInputs inputs, final CompletableFuture nonceFuture) { + this.inputs = inputs; + this.nonceFuture = nonceFuture; + } + + public static EthHashSolverJob createFromInputs(final EthHashSolverInputs inputs) { + return new EthHashSolverJob(inputs, new CompletableFuture<>()); + } + + public EthHashSolverInputs getInputs() { + return inputs; + } + + public boolean isDone() { + return nonceFuture.isDone(); + } + + public void solvedWith(final EthHashSolution solution) { + nonceFuture.complete(solution); + } + + public void cancel() { + nonceFuture.cancel(false); + } + + public void failed(final Throwable ex) { + nonceFuture.completeExceptionally(ex); + } + + public EthHashSolution getSolution() throws InterruptedException, ExecutionException { + return nonceFuture.get(); + } + } + + private final long NO_MINING_CONDUCTED = -1; + + private final Iterable nonceGenerator; + private final EthHasher ethHasher; + private volatile long hashesPerSecond = NO_MINING_CONDUCTED; + + private volatile Optional currentJob = Optional.empty(); + + public EthHashSolver(final Iterable nonceGenerator, final EthHasher ethHasher) { + this.nonceGenerator = nonceGenerator; + this.ethHasher = ethHasher; + } + + public EthHashSolution solveFor(final EthHashSolverJob job) + throws InterruptedException, ExecutionException { + currentJob = Optional.of(job); + findValidNonce(); + return currentJob.get().getSolution(); + } + + private void findValidNonce() { + final Stopwatch operationTimer = Stopwatch.createStarted(); + final EthHashSolverJob job = currentJob.get(); + long hashesExecuted = 0; + final byte[] hashBuffer = new byte[64]; + for (final Long n : nonceGenerator) { + + if (job.isDone()) { + return; + } + + final Optional solution = testNonce(job.getInputs(), n, hashBuffer); + solution.ifPresent(job::solvedWith); + + hashesExecuted++; + final double operationDurationSeconds = operationTimer.elapsed(TimeUnit.NANOSECONDS) / 1e9; + hashesPerSecond = (long) (hashesExecuted / operationDurationSeconds); + } + job.failed(new IllegalStateException("No valid nonce found.")); + } + + private Optional testNonce( + final EthHashSolverInputs inputs, final long nonce, final byte[] hashBuffer) { + ethHasher.hash(hashBuffer, nonce, inputs.getDagSeed(), inputs.getPrePowHash()); + final UInt256 x = UInt256.wrap(Bytes32.wrap(hashBuffer, 32)); + if (x.compareTo(inputs.getTarget()) <= 0) { + final Hash mixedHash = + Hash.wrap(Bytes32.leftPad(BytesValue.wrap(hashBuffer).slice(0, Bytes32.SIZE))); + return Optional.of(new EthHashSolution(nonce, mixedHash, inputs.getPrePowHash())); + } + return Optional.empty(); + } + + public void cancel() { + currentJob.ifPresent(job -> job.cancel()); + } + + public Optional getWorkDefinition() { + return currentJob.flatMap(job -> Optional.of(job.getInputs())); + } + + public Optional hashesPerSecond() { + if (hashesPerSecond == NO_MINING_CONDUCTED) { + return Optional.empty(); + } + return Optional.of(hashesPerSecond); + } + + public boolean submitSolution(final EthHashSolution solution) { + final Optional jobSnapshot = currentJob; + if (!jobSnapshot.isPresent()) { + return false; + } + + final EthHashSolverJob job = jobSnapshot.get(); + final EthHashSolverInputs inputs = job.getInputs(); + if (!Arrays.equals(inputs.getPrePowHash(), solution.getPowHash())) { + return false; + } + final byte[] hashBuffer = new byte[64]; + final Optional calculatedSolution = + testNonce(inputs, solution.getNonce(), hashBuffer); + + if (calculatedSolution.isPresent()) { + currentJob.get().solvedWith(solution); + return true; + } + return false; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverInputs.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverInputs.java new file mode 100755 index 00000000000..5265643ee72 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverInputs.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.util.uint.UInt256; + +public class EthHashSolverInputs { + private final UInt256 target; + private final byte[] prePowHash; + private final long dagSeed; // typically block number + + public EthHashSolverInputs(final UInt256 target, final byte[] prePowHash, final long dagSeed) { + this.target = target; + this.prePowHash = prePowHash; + this.dagSeed = dagSeed; + } + + public UInt256 getTarget() { + return target; + } + + public byte[] getPrePowHash() { + return prePowHash; + } + + public long getDagSeed() { + return dagSeed; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHasher.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHasher.java new file mode 100755 index 00000000000..98c00b329f1 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/EthHasher.java @@ -0,0 +1,167 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import java.io.Closeable; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.google.common.primitives.Ints; + +public interface EthHasher { + + /** + * @param buffer At least 64 bytes long buffer to store EthHash result in + * @param nonce Block Nonce + * @param number Block Number + * @param headerHash Block Header (without mix digest and nonce) Hash + */ + void hash(byte[] buffer, long nonce, long number, byte[] headerHash); + + final class Light implements EthHasher { + + private static final EthHashCacheFactory cacheFactory = new EthHashCacheFactory(); + + @Override + public void hash( + final byte[] buffer, final long nonce, final long number, final byte[] headerHash) { + final EthHashCacheFactory.EthHashDescriptor cache = cacheFactory.ethHashCacheFor(number); + final byte[] hash = + EthHash.hashimotoLight(cache.getDatasetSize(), cache.getCache(), headerHash, nonce); + System.arraycopy(hash, 0, buffer, 0, hash.length); + } + } + + final class Full implements EthHasher, Closeable { + + private static final int HASHERS = Runtime.getRuntime().availableProcessors(); + + private long epoch = -1L; + + private long datasetSize; + + private final RandomAccessFile cacheFile; + + private final ExecutorService hashers = Executors.newFixedThreadPool(HASHERS); + + public Full(final Path cacheFile) throws IOException { + this.cacheFile = new RandomAccessFile(cacheFile.toFile(), "rw"); + datasetSize = this.cacheFile.length(); + } + + @Override + public void hash( + final byte[] buffer, final long nonce, final long number, final byte[] headerHash) { + final long newEpoch = EthHash.epoch(number); + if (epoch != newEpoch) { + updateCache(number, newEpoch); + } + final byte[] hash = + EthHash.hashimoto( + headerHash, + datasetSize, + nonce, + (bytes, integer) -> { + try { + cacheFile.seek(integer * EthHash.HASH_BYTES); + cacheFile.readFully(bytes); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } + }); + System.arraycopy(hash, 0, buffer, 0, hash.length); + } + + private void updateCache(final long number, final long newEpoch) { + final int[] cache = EthHash.mkCache(Ints.checkedCast(EthHash.cacheSize(newEpoch)), number); + epoch = newEpoch; + final long newDatasetSize = EthHash.datasetSize(epoch); + if (newDatasetSize != datasetSize) { + datasetSize = newDatasetSize; + final CountDownLatch doneLatch = new CountDownLatch(HASHERS); + final int upperBound = Ints.checkedCast(datasetSize / EthHash.HASH_BYTES); + final int partitionSize = upperBound / HASHERS; + for (int partition = 0; partition < HASHERS; ++partition) { + hashers.execute( + new EthHasher.Full.HasherTask( + partition * partitionSize, + partition == HASHERS - 1 ? upperBound : (partition + 1) * partitionSize, + cache, + doneLatch, + cacheFile)); + } + try { + doneLatch.await(); + } catch (final InterruptedException ex) { + throw new IllegalStateException(ex); + } + } + } + + @Override + public void close() throws IOException { + cacheFile.close(); + hashers.shutdownNow(); + } + + private static final class HasherTask implements Runnable { + + private static final int DISK_BATCH_SIZE = 256; + + private final int start; + private final int end; + private final int[] cache; + private final CountDownLatch doneLatch; + private final RandomAccessFile cacheFile; + + HasherTask( + final int start, + final int upperBound, + final int[] cache, + final CountDownLatch doneLatch, + final RandomAccessFile cacheFile) { + this.end = upperBound; + this.cache = cache; + this.start = start; + this.doneLatch = doneLatch; + this.cacheFile = cacheFile; + } + + @Override + public void run() { + try { + final byte[] itemBuffer = new byte[EthHash.HASH_BYTES]; + final byte[] writeBuffer = new byte[EthHash.HASH_BYTES * DISK_BATCH_SIZE]; + int buffered = 0; + long lastOffset = 0; + for (int i = start; i < end; ++i) { + EthHash.calcDatasetItem(itemBuffer, cache, i); + System.arraycopy( + itemBuffer, 0, writeBuffer, buffered * EthHash.HASH_BYTES, EthHash.HASH_BYTES); + ++buffered; + if (buffered == DISK_BATCH_SIZE) { + synchronized (cacheFile) { + lastOffset = + (long) ((i - DISK_BATCH_SIZE + 1) * EthHash.HASH_BYTES) + writeBuffer.length; + cacheFile.seek(lastOffset - writeBuffer.length); + cacheFile.write(writeBuffer); + } + buffered = 0; + } + } + if (buffered > 0) { + synchronized (cacheFile) { + cacheFile.seek(lastOffset); + cacheFile.write(writeBuffer, 0, buffered * EthHash.HASH_BYTES); + } + } + doneLatch.countDown(); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } + } + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/FrontierGasCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/FrontierGasCalculator.java new file mode 100755 index 00000000000..b57fa2c3e1a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/FrontierGasCalculator.java @@ -0,0 +1,443 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.ethereum.vm.operations.ExpOperation; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +public class FrontierGasCalculator implements GasCalculator { + + private static final Gas TX_DATA_ZERO_COST = Gas.of(4L); + + private static final Gas TX_DATA_NON_ZERO_COST = Gas.of(68L); + + private static final Gas TX_BASE_COST = Gas.of(21_000L); + + private static final Gas TX_CREATE_EXTRA_COST = Gas.of(0L); + + private static final Gas CODE_DEPOSIT_BYTE_COST = Gas.of(200L); + + private static final Gas ID_PRECOMPILED_BASE_GAS_COST = Gas.of(15L); + + private static final Gas ID_PRECOMPILED_WORD_GAS_COST = Gas.of(3L); + + private static final Gas ECREC_PRECOMPILED_GAS_COST = Gas.of(3_000L); + + private static final Gas SHA256_PRECOMPILED_BASE_GAS_COST = Gas.of(60L); + + private static final Gas SHA256_PRECOMPILED_WORD_GAS_COST = Gas.of(12L); + + private static final Gas RIPEMD160_PRECOMPILED_WORD_GAS_COST = Gas.of(120L); + + private static final Gas RIPEMD160_PRECOMPILED_BASE_GAS_COST = Gas.of(600L); + + private static final Gas VERY_LOW_TIER_GAS_COST = Gas.of(3L); + + private static final Gas LOW_TIER_GAS_COST = Gas.of(5L); + + private static final Gas BASE_TIER_GAS_COST = Gas.of(2L); + + private static final Gas MID_TIER_GAS_COST = Gas.of(8L); + + private static final Gas HIGH_TIER_GAS_COST = Gas.of(10L); + + private static final Gas CALL_OPERATION_BASE_GAS_COST = Gas.of(40L); + + private static final Gas CALL_VALUE_TRANSFER_GAS_COST = Gas.of(9_000L); + + private static final Gas ADDITIONAL_CALL_STIPEND = Gas.of(2_300L); + + private static final Gas NEW_ACCOUNT_GAS_COST = Gas.of(25_000L); + + private static final Gas CREATE_OPERATION_GAS_COST = Gas.of(32_000L); + + private static final Gas COPY_WORD_GAS_COST = Gas.of(3L); + + private static final Gas MEMORY_WORD_GAS_COST = Gas.of(3L); + + private static final Gas BALANCE_OPERATION_GAS_COST = Gas.of(20L); + + private static final Gas BLOCKHASH_OPERATION_GAS_COST = Gas.of(20L); + + private static final Gas EXP_OPERATION_BASE_GAS_COST = Gas.of(10); + + private static final Gas EXP_OPERATION_BYTE_GAS_COST = Gas.of(10); + + private static final Gas EXT_CODE_BASE_GAS_COST = Gas.of(20L); + + private static final Gas JUMPDEST_OPERATION_GAS_COST = Gas.of(1); + + private static final Gas LOG_OPERATION_BASE_GAS_COST = Gas.of(375L); + + private static final Gas LOG_OPERATION_DATA_BYTE_GAS_COST = Gas.of(8L); + + private static final Gas LOG_OPERATION_TOPIC_GAS_COST = Gas.of(375L); + + private static final Gas SELFDESTRUCT_OPERATION_GAS_COST = Gas.of(0); + + private static final Gas SHA3_OPERATION_BASE_GAS_COST = Gas.of(30L); + + static final Gas SHA3_OPERATION_WORD_GAS_COST = Gas.of(6L); + + private static final Gas SLOAD_OPERATION_GAS_COST = Gas.of(50); + + private static final Gas STORAGE_SET_GAS_COST = Gas.of(20_000L); + + private static final Gas STORAGE_RESET_GAS_COST = Gas.of(5_000L); + + private static final Gas STORAGE_RESET_REFUND_AMOUNT = Gas.of(15_000L); + + private static final Gas SELF_DESTRUCT_REFUND_AMOUNT = Gas.of(24_000L); + + @Override + public Gas transactionIntrinsicGasCost(final Transaction transaction) { + final BytesValue payload = transaction.getPayload(); + int zeros = 0; + for (int i = 0; i < payload.size(); i++) { + if (payload.get(i) == 0) { + ++zeros; + } + } + final int nonZeros = payload.size() - zeros; + + Gas cost = + Gas.ZERO + .plus(TX_BASE_COST) + .plus(TX_DATA_ZERO_COST.times(zeros)) + .plus(TX_DATA_NON_ZERO_COST.times(nonZeros)); + + if (transaction.isContractCreation()) { + cost = cost.plus(txCreateExtraGasCost()); + } + + return cost; + } + + /** + * Returns the additional gas cost for contract creation transactions + * + * @return the additional gas cost for contract creation transactions + */ + protected Gas txCreateExtraGasCost() { + return TX_CREATE_EXTRA_COST; + } + + @Override + public Gas codeDepositGasCost(final int codeSize) { + return CODE_DEPOSIT_BYTE_COST.times(codeSize); + } + + @Override + public Gas idPrecompiledContractGasCost(final BytesValue input) { + return ID_PRECOMPILED_WORD_GAS_COST + .times(Words.numWords(input)) + .plus(ID_PRECOMPILED_BASE_GAS_COST); + } + + @Override + public Gas getEcrecPrecompiledContractGasCost() { + return ECREC_PRECOMPILED_GAS_COST; + } + + @Override + public Gas sha256PrecompiledContractGasCost(final BytesValue input) { + return SHA256_PRECOMPILED_WORD_GAS_COST + .times(Words.numWords(input)) + .plus(SHA256_PRECOMPILED_BASE_GAS_COST); + } + + @Override + public Gas ripemd160PrecompiledContractGasCost(final BytesValue input) { + return RIPEMD160_PRECOMPILED_WORD_GAS_COST + .times(Words.numWords(input)) + .plus(RIPEMD160_PRECOMPILED_BASE_GAS_COST); + } + + @Override + public Gas getZeroTierGasCost() { + return Gas.ZERO; + } + + @Override + public Gas getVeryLowTierGasCost() { + return VERY_LOW_TIER_GAS_COST; + } + + @Override + public Gas getLowTierGasCost() { + return LOW_TIER_GAS_COST; + } + + @Override + public Gas getBaseTierGasCost() { + return BASE_TIER_GAS_COST; + } + + @Override + public Gas getMidTierGasCost() { + return MID_TIER_GAS_COST; + } + + @Override + public Gas getHighTierGasCost() { + return HIGH_TIER_GAS_COST; + } + + /** + * Returns the base gas cost to execute a call operation. + * + * @return the base gas cost to execute a call operation + */ + protected Gas callOperationBaseGasCost() { + return CALL_OPERATION_BASE_GAS_COST; + } + + /** + * Returns the gas cost to transfer funds in a call operation. + * + * @return the gas cost to transfer funds in a call operation + */ + protected Gas callValueTransferGasCost() { + return CALL_VALUE_TRANSFER_GAS_COST; + } + + /** + * Returns the gas cost to create a new account. + * + * @return the gas cost to create a new account + */ + protected Gas newAccountGasCost() { + return NEW_ACCOUNT_GAS_COST; + } + + @Override + public Gas callOperationGasCost( + final MessageFrame frame, + final Gas stipend, + final UInt256 inputDataOffset, + final UInt256 inputDataLength, + final UInt256 outputDataOffset, + final UInt256 outputDataLength, + final Wei transferValue, + final Account recipient) { + final Gas inputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, inputDataOffset, inputDataLength); + final Gas outputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, outputDataOffset, outputDataLength); + final Gas memoryExpansionCost = inputDataMemoryExpansionCost.max(outputDataMemoryExpansionCost); + + Gas cost = callOperationBaseGasCost().plus(stipend).plus(memoryExpansionCost); + + if (!transferValue.isZero()) { + cost = cost.plus(callValueTransferGasCost()); + } + + if (recipient == null) { + cost = cost.plus(newAccountGasCost()); + } + + return cost; + } + + /** + * Returns the additional call stipend for calls with value transfers. + * + * @return the additional call stipend for calls with value transfers + */ + protected Gas additionalCallStipend() { + return ADDITIONAL_CALL_STIPEND; + } + + @Override + public Gas gasAvailableForChildCall( + final MessageFrame frame, final Gas stipend, final boolean transfersValue) { + if (transfersValue) { + return stipend.plus(additionalCallStipend()); + } else { + return stipend; + } + } + + @Override + public Gas createOperationGasCost(final MessageFrame frame) { + final UInt256 initCodeOffset = frame.getStackItem(1).asUInt256(); + final UInt256 initCodeLength = frame.getStackItem(2).asUInt256(); + + final Gas memoryGasCost = memoryExpansionGasCost(frame, initCodeOffset, initCodeLength); + return CREATE_OPERATION_GAS_COST.plus(memoryGasCost); + } + + @Override + public Gas gasAvailableForChildCreate(final Gas stipend) { + return stipend; + } + + @Override + public Gas dataCopyOperationGasCost( + final MessageFrame frame, final UInt256 offset, final UInt256 length) { + return copyWordsToMemoryGasCost( + frame, VERY_LOW_TIER_GAS_COST, COPY_WORD_GAS_COST, offset, length); + } + + @Override + public Gas memoryExpansionGasCost( + final MessageFrame frame, final UInt256 offset, final UInt256 length) { + + final Gas pre = memoryCost(frame.memoryWordSize()); + final Gas post = memoryCost(frame.calculateMemoryExpansion(offset, length)); + + return post.minus(pre); + } + + @Override + public Gas getBalanceOperationGasCost() { + return BALANCE_OPERATION_GAS_COST; + } + + @Override + public Gas getBlockHashOperationGasCost() { + return BLOCKHASH_OPERATION_GAS_COST; + } + + /** + * Returns the gas cost for a byte in the {@link ExpOperation}. + * + * @return the gas cost for a byte in the exponent operation + */ + protected Gas expOperationByteGasCost() { + return EXP_OPERATION_BYTE_GAS_COST; + } + + @Override + public Gas expOperationGasCost(final int numBytes) { + return expOperationByteGasCost().times(Gas.of(numBytes)).plus(EXP_OPERATION_BASE_GAS_COST); + } + + /** + * Returns the base gas cost for external code accesses. + * + * @return the base gas cost for external code accesses + */ + protected Gas extCodeBaseGasCost() { + return EXT_CODE_BASE_GAS_COST; + } + + @Override + public Gas extCodeCopyOperationGasCost( + final MessageFrame frame, final UInt256 offset, final UInt256 length) { + return copyWordsToMemoryGasCost( + frame, extCodeBaseGasCost(), COPY_WORD_GAS_COST, offset, length); + } + + @Override + public Gas getExtCodeSizeOperationGasCost() { + return extCodeBaseGasCost(); + } + + @Override + public Gas getJumpDestOperationGasCost() { + return JUMPDEST_OPERATION_GAS_COST; + } + + @Override + public Gas logOperationGasCost( + final MessageFrame frame, + final UInt256 dataOffset, + final UInt256 dataLength, + final int numTopics) { + return Gas.ZERO + .plus(LOG_OPERATION_BASE_GAS_COST) + .plus(LOG_OPERATION_DATA_BYTE_GAS_COST.times(Gas.of(dataLength))) + .plus(LOG_OPERATION_TOPIC_GAS_COST.times(numTopics)) + .plus(memoryExpansionGasCost(frame, dataOffset, dataLength)); + } + + @Override + public Gas mLoadOperationGasCost(final MessageFrame frame, final UInt256 offset) { + return VERY_LOW_TIER_GAS_COST.plus(memoryExpansionGasCost(frame, offset, UInt256.U_32)); + } + + @Override + public Gas mStoreOperationGasCost(final MessageFrame frame, final UInt256 offset) { + return VERY_LOW_TIER_GAS_COST.plus(memoryExpansionGasCost(frame, offset, UInt256.U_32)); + } + + @Override + public Gas mStore8OperationGasCost(final MessageFrame frame, final UInt256 offset) { + return VERY_LOW_TIER_GAS_COST.plus(memoryExpansionGasCost(frame, offset, UInt256.ONE)); + } + + @Override + public Gas selfDestructOperationGasCost(final Account recipient, final Wei inheritance) { + return SELFDESTRUCT_OPERATION_GAS_COST; + } + + @Override + public Gas sha3OperationGasCost( + final MessageFrame frame, final UInt256 offset, final UInt256 length) { + return copyWordsToMemoryGasCost( + frame, SHA3_OPERATION_BASE_GAS_COST, SHA3_OPERATION_WORD_GAS_COST, offset, length); + } + + @Override + public Gas create2OperationGasCost(final MessageFrame frame) { + throw new UnsupportedOperationException( + "CREATE2 operation not supported by " + getClass().getSimpleName()); + } + + @Override + public Gas getSloadOperationGasCost() { + return SLOAD_OPERATION_GAS_COST; + } + + @Override + public Gas getStorageResetGasCost() { + return STORAGE_RESET_GAS_COST; + } + + @Override + public Gas getStorageSetGasCost() { + return STORAGE_SET_GAS_COST; + } + + @Override + public Gas getStorageResetRefundAmount() { + return STORAGE_RESET_REFUND_AMOUNT; + } + + @Override + public Gas getSelfDestructRefundAmount() { + return SELF_DESTRUCT_REFUND_AMOUNT; + } + + private Gas copyWordsToMemoryGasCost( + final MessageFrame frame, + final Gas baseGasCost, + final Gas wordGasCost, + final UInt256 offset, + final UInt256 length) { + final UInt256 numWords = length.dividedCeilBy(Bytes32.SIZE); + + final Gas copyCost = wordGasCost.times(Gas.of(numWords)).plus(baseGasCost); + final Gas memoryCost = memoryExpansionGasCost(frame, offset, length); + + return copyCost.plus(memoryCost); + } + + private static Gas memoryCost(final UInt256 length) { + if (!length.fitsInt()) { + return Gas.MAX_VALUE; + } + final Gas len = Gas.of(length); + final Gas base = len.times(len).dividedBy(512); + + return MEMORY_WORD_GAS_COST.times(len).plus(base); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HeaderValidationMode.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HeaderValidationMode.java new file mode 100755 index 00000000000..1b75428c53a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HeaderValidationMode.java @@ -0,0 +1,18 @@ +package net.consensys.pantheon.ethereum.mainnet; + +public enum HeaderValidationMode { + /** No Validation. data must be pre-validated */ + NONE, + + /** Skip proof of work validation */ + LIGHT, + + /** Skip rules that can be applied when the parent is already on the blockchain */ + DETACHED_ONLY, + + /** Skip rules that can be applied before the parent is added to the block chain */ + SKIP_DETACHED, + + /** Fully validate the header */ + FULL +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HomesteadGasCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HomesteadGasCalculator.java new file mode 100755 index 00000000000..1890debbe4d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/HomesteadGasCalculator.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Gas; + +public class HomesteadGasCalculator extends FrontierGasCalculator { + + private static final Gas TX_CREATE_EXTRA = Gas.of(32_000L); + + @Override + protected Gas txCreateExtraGasCost() { + return TX_CREATE_EXTRA; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockBodyValidator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockBodyValidator.java new file mode 100755 index 00000000000..7ab4ab99113 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockBodyValidator.java @@ -0,0 +1,221 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class MainnetBlockBodyValidator implements BlockBodyValidator { + + private static final Logger LOGGER = LogManager.getLogger(MainnetBlockBodyValidator.class); + + private static final int MAX_OMMERS = 2; + + private static final int MAX_GENERATION = 6; + private final ProtocolSchedule protocolSchedule; + + public MainnetBlockBodyValidator(final ProtocolSchedule protocolSchedule) { + this.protocolSchedule = protocolSchedule; + } + + @Override + public boolean validateBody( + final ProtocolContext context, + final Block block, + final List receipts, + final Hash worldStateRootHash) { + + if (!validateBodyLight(context, block, receipts)) { + return false; + } + + if (!validateStateRoot(block.getHeader().getStateRoot(), worldStateRootHash)) { + return false; + } + + return true; + } + + @Override + public boolean validateBodyLight( + final ProtocolContext context, + final Block block, + final List receipts) { + final BlockHeader header = block.getHeader(); + final BlockBody body = block.getBody(); + + final Bytes32 transactionsRoot = BodyValidation.transactionsRoot(body.getTransactions()); + if (!validateTransactionsRoot(header.getTransactionsRoot(), transactionsRoot)) { + return false; + } + + final Bytes32 receiptsRoot = BodyValidation.receiptsRoot(receipts); + if (!validateReceiptsRoot(header.getReceiptsRoot(), receiptsRoot)) { + return false; + } + + final long gasUsed = + receipts.isEmpty() ? 0 : receipts.get(receipts.size() - 1).getCumulativeGasUsed(); + if (!validateGasUsed(header.getGasUsed(), gasUsed)) { + return false; + } + + if (!validateLogsBloom(header.getLogsBloom(), BodyValidation.logsBloom(receipts))) { + return false; + } + + if (!validateEthHash(context, block)) { + return false; + } + + return true; + } + + private static boolean validateTransactionsRoot(final Bytes32 expected, final Bytes32 actual) { + if (!expected.equals(actual)) { + LOGGER.warn( + "Invalid block: transaction root mismatch (expected={}, actual={})", expected, actual); + return false; + } + + return true; + } + + private static boolean validateLogsBloom( + final LogsBloomFilter expected, final LogsBloomFilter actual) { + if (!expected.equals(actual)) { + LOGGER.warn( + "Invalid block: logs bloom filter mismatch (expected={}, actual={})", expected, actual); + return false; + } + + return true; + } + + private static boolean validateGasUsed(final long expected, final long actual) { + if (expected != actual) { + LOGGER.warn("Invalid block: gas used mismatch (expected={}, actual={})", expected, actual); + return false; + } + + return true; + } + + private static boolean validateReceiptsRoot(final Bytes32 expected, final Bytes32 actual) { + if (!expected.equals(actual)) { + LOGGER.warn( + "Invalid block: receipts root mismatch (expected={}, actual={})", expected, actual); + return false; + } + + return true; + } + + private static boolean validateStateRoot(final Bytes32 expected, final Bytes32 actual) { + if (!expected.equals(actual)) { + LOGGER.warn("Invalid block: state root mismatch (expected={}, actual={})", expected, actual); + return false; + } + + return true; + } + + private boolean validateEthHash(final ProtocolContext context, final Block block) { + final BlockHeader header = block.getHeader(); + final BlockBody body = block.getBody(); + + final Bytes32 ommerHash = BodyValidation.ommersHash(body.getOmmers()); + if (!validateOmmersHash(header.getOmmersHash(), ommerHash)) { + return false; + } + + if (!validateOmmers(context, header, body.getOmmers())) { + return false; + } + + return true; + } + + private static boolean validateOmmersHash(final Bytes32 expected, final Bytes32 actual) { + if (!expected.equals(actual)) { + LOGGER.warn("Invalid block: ommers hash mismatch (expected={}, actual={})", expected, actual); + return false; + } + + return true; + } + + private boolean validateOmmers( + final ProtocolContext context, final BlockHeader header, final List ommers) { + if (ommers.size() > MAX_OMMERS) { + LOGGER.warn( + "Invalid block: ommer count {} exceeds ommer limit {}", ommers.size(), MAX_OMMERS); + return false; + } + + if (!areOmmersUnique(ommers)) { + LOGGER.warn("Invalid block: ommers are not unique"); + return false; + } + + for (final BlockHeader ommer : ommers) { + if (!isOmmerValid(context, header, ommer)) { + LOGGER.warn("Invalid block: ommer is invalid"); + return false; + } + } + + return true; + } + + private static boolean areOmmersUnique(final List ommers) { + final Set uniqueOmmers = new HashSet<>(ommers); + + return uniqueOmmers.size() == ommers.size(); + } + + private static boolean areSiblings(final BlockHeader a, final BlockHeader b) { + // Siblings cannot be the same. + if (a.equals(b)) { + return false; + } + + return a.getParentHash().equals(b.getParentHash()); + } + + private boolean isOmmerValid( + final ProtocolContext context, final BlockHeader current, final BlockHeader ommer) { + final ProtocolSpec protocolSpec = protocolSchedule.getByBlockNumber(ommer.getNumber()); + if (!protocolSpec + .getBlockHeaderValidator() + .validateHeader(ommer, context, HeaderValidationMode.FULL)) { + return false; + } + + // The current block is guaranteed to have a parent because it's a valid header. + final long lastAncestorBlockNumber = Math.max(current.getNumber() - MAX_GENERATION, 0); + + BlockHeader previous = current; + while (previous.getNumber() > lastAncestorBlockNumber) { + final BlockHeader ancestor = + context.getBlockchain().getBlockHeader(previous.getParentHash()).get(); + if (areSiblings(ommer, ancestor)) { + return true; + } + previous = ancestor; + } + + return false; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHashFunction.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHashFunction.java new file mode 100755 index 00000000000..24772681c8f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHashFunction.java @@ -0,0 +1,15 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.BytesValue; + +/** Implements the block hashing algorithm for MainNet as per the yellow paper. */ +public class MainnetBlockHashFunction { + + public static Hash createHash(final BlockHeader header) { + final BytesValue rlp = RLP.encode(header::writeTo); + return Hash.hash(rlp); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidator.java new file mode 100755 index 00000000000..ff7c8b2b3d4 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidator.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.AncestryValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.CalculatedDifficultyValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.ConstantFieldValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.ExtraDataMaxLengthValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasLimitRangeAndDeltaValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.GasUsageValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.ProofOfWorkValidationRule; +import net.consensys.pantheon.ethereum.mainnet.headervalidationrules.TimestampValidationRule; +import net.consensys.pantheon.util.bytes.BytesValue; + +public final class MainnetBlockHeaderValidator { + + private static final BytesValue DAO_EXTRA_DATA = + BytesValue.fromHexString("0x64616f2d686172642d666f726b"); + private static final int MIN_GAS_LIMIT = 5000; + private static final long MAX_GAS_LIMIT = 0x7fffffffffffffffL; + public static final int TIMESTAMP_TOLERANCE_S = 15; + public static final int MINIMUM_SECONDS_SINCE_PARENT = 1; + + public static BlockHeaderValidator create( + final DifficultyCalculator difficultyCalculator) { + return createValidator(difficultyCalculator).build(); + } + + public static BlockHeaderValidator createDaoValidator( + final DifficultyCalculator difficultyCalculator) { + return createValidator(difficultyCalculator) + .addRule( + new ConstantFieldValidationRule<>( + "extraData", BlockHeader::getExtraData, DAO_EXTRA_DATA)) + .build(); + } + + private static BlockHeaderValidator.Builder createValidator( + final DifficultyCalculator difficultyCalculator) { + return new BlockHeaderValidator.Builder() + .addRule(new CalculatedDifficultyValidationRule<>(difficultyCalculator)) + .addRule(new AncestryValidationRule()) + .addRule(new GasLimitRangeAndDeltaValidationRule(MIN_GAS_LIMIT, MAX_GAS_LIMIT)) + .addRule(new GasUsageValidationRule()) + .addRule(new TimestampValidationRule(TIMESTAMP_TOLERANCE_S, MINIMUM_SECONDS_SINCE_PARENT)) + .addRule(new ExtraDataMaxLengthValidationRule(BlockHeader.MAX_EXTRA_DATA_BYTES)) + .addRule(new ProofOfWorkValidationRule()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockImporter.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockImporter.java new file mode 100755 index 00000000000..891f7456744 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockImporter.java @@ -0,0 +1,97 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; + +import java.util.List; +import java.util.Optional; + +import org.apache.logging.log4j.Logger; + +public class MainnetBlockImporter implements BlockImporter { + private static final Logger LOG = getLogger(); + + private final BlockHeaderValidator blockHeaderValidator; + + private final BlockBodyValidator blockBodyValidator; + + private final BlockProcessor blockProcessor; + + public MainnetBlockImporter( + final BlockHeaderValidator blockHeaderValidator, + final BlockBodyValidator blockBodyValidator, + final BlockProcessor blockProcessor) { + this.blockHeaderValidator = blockHeaderValidator; + this.blockBodyValidator = blockBodyValidator; + this.blockProcessor = blockProcessor; + } + + @Override + public synchronized boolean importBlock( + final ProtocolContext context, + final Block block, + final HeaderValidationMode headerValidationMode) { + final BlockHeader header = block.getHeader(); + + final Optional maybeParentHeader = + context.getBlockchain().getBlockHeader(header.getParentHash()); + if (!maybeParentHeader.isPresent()) { + LOG.error( + "Attempted to import block {} with hash {} but parent block {} was not present", + header.getNumber(), + header.getHash(), + header.getParentHash()); + return false; + } + final BlockHeader parentHeader = maybeParentHeader.get(); + + if (!blockHeaderValidator.validateHeader(header, parentHeader, context, headerValidationMode)) { + return false; + } + + final MutableBlockchain blockchain = context.getBlockchain(); + final MutableWorldState worldState = + context.getWorldStateArchive().getMutable(parentHeader.getStateRoot()); + final BlockProcessor.Result result = blockProcessor.processBlock(blockchain, worldState, block); + if (!result.isSuccessful()) { + return false; + } + + final List receipts = result.getReceipts(); + if (!blockBodyValidator.validateBody(context, block, receipts, worldState.rootHash())) { + return false; + } + + blockchain.appendBlock(block, receipts); + + return true; + } + + @Override + public boolean fastImportBlock( + final ProtocolContext context, + final Block block, + final List receipts, + final HeaderValidationMode headerValidationMode) { + final BlockHeader header = block.getHeader(); + + if (!blockHeaderValidator.validateHeader(header, context, headerValidationMode)) { + return false; + } + + if (!blockBodyValidator.validateBodyLight(context, block, receipts)) { + return false; + } + + context.getBlockchain().appendBlock(block, receipts); + + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessor.java new file mode 100755 index 00000000000..8de8e617086 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessor.java @@ -0,0 +1,170 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class MainnetBlockProcessor implements BlockProcessor { + + @FunctionalInterface + public interface TransactionReceiptFactory { + + TransactionReceipt create( + TransactionProcessor.Result result, WorldState worldState, long gasUsed); + } + + private static final Logger LOGGER = LogManager.getLogger(MainnetBlockProcessor.class); + + private static final int MAX_GENERATION = 6; + + public static class Result implements BlockProcessor.Result { + + private static final Result FAILED = new Result(false, null); + + private final boolean successful; + + private final List receipts; + + public static Result successful(final List receipts) { + return new Result(true, ImmutableList.copyOf(receipts)); + } + + public static Result failed() { + return FAILED; + } + + Result(final boolean successful, final List receipts) { + this.successful = successful; + this.receipts = receipts; + } + + @Override + public List getReceipts() { + return receipts; + } + + @Override + public boolean isSuccessful() { + return successful; + } + } + + private final TransactionProcessor transactionProcessor; + + private final TransactionReceiptFactory transactionReceiptFactory; + + private final Wei blockReward; + + private final MiningBeneficiaryCalculator miningBeneficiaryCalculator; + + public MainnetBlockProcessor( + final TransactionProcessor transactionProcessor, + final TransactionReceiptFactory transactionReceiptFactory, + final Wei blockReward, + final MiningBeneficiaryCalculator miningBeneficiaryCalculator) { + this.transactionProcessor = transactionProcessor; + this.transactionReceiptFactory = transactionReceiptFactory; + this.blockReward = blockReward; + this.miningBeneficiaryCalculator = miningBeneficiaryCalculator; + } + + @Override + public Result processBlock( + final Blockchain blockchain, + final MutableWorldState worldState, + final BlockHeader blockHeader, + final List transactions, + final List ommers) { + + long gasUsed = 0; + final List receipts = new ArrayList<>(); + + for (int i = 0; i < transactions.size(); ++i) { + final Transaction transaction = transactions.get(i); + + final long remainingGasBudget = blockHeader.getGasLimit() - gasUsed; + if (Long.compareUnsigned(transaction.getGasLimit(), remainingGasBudget) > 0) { + LOGGER.warn( + "Transaction processing error: transaction gas limit {} exceeds available block budget remaining {}", + transaction.getGasLimit(), + remainingGasBudget); + return Result.failed(); + } + + final WorldUpdater worldStateUpdater = worldState.updater(); + final Address miningBeneficiary = + miningBeneficiaryCalculator.calculateBeneficiary(blockHeader); + final TransactionProcessor.Result result = + transactionProcessor.processTransaction( + blockchain, worldStateUpdater, blockHeader, transaction, miningBeneficiary); + if (result.isInvalid()) { + return Result.failed(); + } + + worldStateUpdater.commit(); + gasUsed = transaction.getGasLimit() - result.getGasRemaining() + gasUsed; + final TransactionReceipt transactionReceipt = + transactionReceiptFactory.create(result, worldState, gasUsed); + receipts.add(transactionReceipt); + } + + if (!rewardCoinbase(worldState, blockHeader, ommers)) { + return Result.failed(); + } + + worldState.persist(); + return Result.successful(receipts); + } + + protected Address calculateFeeRecipient(final BlockHeader header) { + return header.getCoinbase(); + } + + private boolean rewardCoinbase( + final MutableWorldState worldState, + final ProcessableBlockHeader header, + final List ommers) { + if (blockReward.isZero()) { + return true; + } + final Wei coinbaseReward = blockReward.plus(blockReward.times(ommers.size()).dividedBy(32)); + final WorldUpdater updater = worldState.updater(); + final MutableAccount coinbase = updater.getOrCreate(header.getCoinbase()); + + coinbase.incrementBalance(coinbaseReward); + for (final BlockHeader ommerHeader : ommers) { + if (ommerHeader.getNumber() - header.getNumber() > MAX_GENERATION) { + LOGGER.warn( + "Block processing error: ommer block number {} more than {} generations current block number {}", + ommerHeader.getNumber(), + MAX_GENERATION, + header.getNumber()); + return false; + } + + final MutableAccount ommerCoinbase = updater.getOrCreate(ommerHeader.getCoinbase()); + final long distance = header.getNumber() - ommerHeader.getNumber(); + final Wei ommerReward = blockReward.minus(blockReward.times(distance).dividedBy(8)); + ommerCoinbase.incrementBalance(ommerReward); + } + + updater.commit(); + + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetContractCreationProcessor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetContractCreationProcessor.java new file mode 100755 index 00000000000..57db9b7a122 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetContractCreationProcessor.java @@ -0,0 +1,138 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collection; + +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** A contract creation message processor. */ +public class MainnetContractCreationProcessor extends AbstractMessageProcessor { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final boolean requireCodeDepositToSucceed; + + private final GasCalculator gasCalculator; + + private final long initialContractNonce; + + private final int codeSizeLimit; + + public MainnetContractCreationProcessor( + final GasCalculator gasCalculator, + final EVM evm, + final boolean requireCodeDepositToSucceed, + final int codeSizeLimit, + final long initialContractNonce, + final Collection

forceCommitAddresses) { + super(evm, forceCommitAddresses); + this.gasCalculator = gasCalculator; + this.requireCodeDepositToSucceed = requireCodeDepositToSucceed; + this.codeSizeLimit = codeSizeLimit; + this.initialContractNonce = initialContractNonce; + } + + public MainnetContractCreationProcessor( + final GasCalculator gasCalculator, + final EVM evm, + final boolean requireCodeDepositToSucceed, + final int codeSizeLimit, + final long initialContractNonce) { + this( + gasCalculator, + evm, + requireCodeDepositToSucceed, + codeSizeLimit, + initialContractNonce, + ImmutableSet.of()); + } + + private static boolean accountExists(final Account account) { + // The account exists if it has sent a transaction + // or already has its code initialized. + return account.getNonce() > 0 || !account.getCode().isEmpty(); + } + + protected GasCalculator gasCalculator() { + return gasCalculator; + } + + @Override + public void start(final MessageFrame frame) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Executing contract-creation"); + } + + final MutableAccount sender = frame.getWorldState().getMutable(frame.getSenderAddress()); + sender.decrementBalance(frame.getValue()); + + // TODO: Fix when tests are upstreamed or remove from test suit. + // EIP-68 mandates that contract creations cannot collide any more. + // While that EIP has been deferred, the General State reference tests + // incorrectly include this even in early hard forks. + final MutableAccount contract = frame.getWorldState().getOrCreate(frame.getContractAddress()); + if (accountExists(contract)) { + LOGGER.trace( + "Contract creation error: account as already been created for address {}", + frame.getContractAddress()); + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + } else { + contract.incrementBalance(frame.getValue()); + contract.setNonce(initialContractNonce); + frame.setState(MessageFrame.State.CODE_EXECUTING); + } + } + + @Override + protected void codeSuccess(final MessageFrame frame) { + final BytesValue contractCode = frame.getOutputData(); + + final Gas depositFee = gasCalculator.codeDepositGasCost(contractCode.size()); + + if (frame.getRemainingGas().compareTo(depositFee) < 0) { + LOGGER.trace( + "Not enough gas to pay the code deposit fee for {}: " + + "remaining gas = {} < {} = deposit fee", + frame.getContractAddress(), + frame.getRemainingGas(), + depositFee); + if (requireCodeDepositToSucceed) { + LOGGER.trace("Contract creation error: insufficient funds for code deposit"); + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + } else { + frame.setState(MessageFrame.State.COMPLETED_SUCCESS); + } + } else { + if (contractCode.size() > codeSizeLimit) { + LOGGER.trace( + "Contract creation error: code size {} exceeds code size limit {}", + contractCode.size(), + codeSizeLimit); + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + } else { + frame.decrementRemainingGas(depositFee); + + // Finalize contract creation, setting the contract code. + final MutableAccount contract = + frame.getWorldState().getOrCreate(frame.getContractAddress()); + contract.setCode(contractCode); + LOGGER.trace( + "Successful creation of contract {} with code of size {} (Gas remaining: {})", + frame.getContractAddress(), + contractCode.size(), + frame.getRemainingGas()); + frame.setState(MessageFrame.State.COMPLETED_SUCCESS); + } + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetDifficultyCalculators.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetDifficultyCalculators.java new file mode 100755 index 00000000000..e3ae7824f11 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetDifficultyCalculators.java @@ -0,0 +1,110 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; + +import com.google.common.primitives.Ints; + +/** Provides the various difficultly calculates used on mainnet hard forks. */ +public abstract class MainnetDifficultyCalculators { + + private static final BigInteger DIFFICULTY_BOUND_DIVISOR = BigInteger.valueOf(2_048L); + + private static final BigInteger MINIMUM_DIFFICULTY = BigInteger.valueOf(131_072L); + + private static final long EXPONENTIAL_DIFF_PERIOD = 100_000L; + + private static final int DURATION_LIMIT = 13; + + private static final BigInteger BIGINT_2 = BigInteger.valueOf(2L); + + private static final long BYZANTIUM_FAKE_BLOCK_OFFSET = 2_999_999L; + private static final long CONSTANTINOPLE_FAKE_BLOCK_OFFSET = 4_999_999L; + + private MainnetDifficultyCalculators() {} + + public static DifficultyCalculator FRONTIER = + (time, parent, protocolContext) -> { + final BigInteger parentDifficulty = difficulty(parent.getDifficulty()); + final BigInteger adjust = parentDifficulty.divide(DIFFICULTY_BOUND_DIVISOR); + BigInteger difficulty; + if (time - parent.getTimestamp() < DURATION_LIMIT) { + difficulty = adjust.add(parentDifficulty); + } else { + difficulty = parentDifficulty.subtract(adjust); + } + difficulty = ensureMinimumDifficulty(difficulty); + final long periodCount = (parent.getNumber() + 1) / EXPONENTIAL_DIFF_PERIOD; + return periodCount > 1 ? adjustForPeriod(periodCount, difficulty) : difficulty; + }; + + public static DifficultyCalculator HOMESTEAD = + (time, parent, protocolContext) -> { + final BigInteger parentDifficulty = difficulty(parent.getDifficulty()); + final BigInteger difficulty = + ensureMinimumDifficulty( + BigInteger.valueOf(Math.max(1 - (time - parent.getTimestamp()) / 10, -99L)) + .multiply(parentDifficulty.divide(DIFFICULTY_BOUND_DIVISOR)) + .add(parentDifficulty)); + final long periodCount = (parent.getNumber() + 1) / EXPONENTIAL_DIFF_PERIOD; + return periodCount > 1 ? adjustForPeriod(periodCount, difficulty) : difficulty; + }; + + public static DifficultyCalculator BYZANTIUM = + (time, parent, protocolContext) -> + calculateByzantiumDifficulty(time, parent, BYZANTIUM_FAKE_BLOCK_OFFSET); + + public static DifficultyCalculator CONSTANTINOPLE = + (time, parent, protocolContext) -> + calculateByzantiumDifficulty(time, parent, CONSTANTINOPLE_FAKE_BLOCK_OFFSET); + + private static BigInteger calculateByzantiumDifficulty( + final long time, final BlockHeader parent, final long fakeBlockOffset) { + final BigInteger parentDifficulty = difficulty(parent.getDifficulty()); + final boolean hasOmmers = !parent.getOmmersHash().equals(Hash.EMPTY_LIST_HASH); + final BigInteger difficulty = + ensureMinimumDifficulty( + BigInteger.valueOf(byzantiumX(time, parent.getTimestamp(), hasOmmers)) + .multiply(parentDifficulty.divide(DIFFICULTY_BOUND_DIVISOR)) + .add(parentDifficulty)); + final long periodCount = + fakeBlockNum(parent.getNumber(), fakeBlockOffset) / EXPONENTIAL_DIFF_PERIOD; + return periodCount > 1 ? adjustForPeriod(periodCount, difficulty) : difficulty; + } + + private static long fakeBlockNum(final long parentNum, final long fakeBlockOffset) { + final long fakeBlockNumber; + if (Long.compareUnsigned(parentNum, fakeBlockOffset) >= 0) { + fakeBlockNumber = parentNum - fakeBlockOffset; + } else { + fakeBlockNumber = 0L; + } + return fakeBlockNumber; + } + + private static long byzantiumX( + final long blockTime, final long parentTime, final boolean hasOmmers) { + long x = (blockTime - parentTime) / 9L; + if (hasOmmers) { + x = 2 - x; + } else { + x = 1 - x; + } + return Math.max(x, -99L); + } + + private static BigInteger adjustForPeriod(final long periodCount, final BigInteger difficulty) { + return difficulty.add(BIGINT_2.pow(Ints.checkedCast(periodCount - 2))); + } + + private static BigInteger ensureMinimumDifficulty(final BigInteger difficulty) { + return difficulty.compareTo(MINIMUM_DIFFICULTY) < 0 ? MINIMUM_DIFFICULTY : difficulty; + } + + private static BigInteger difficulty(final UInt256 value) { + return new BigInteger(1, value.getBytes().extractArray()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetEvmRegistries.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetEvmRegistries.java new file mode 100755 index 00000000000..e74562e3ea8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetEvmRegistries.java @@ -0,0 +1,262 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.Operation; +import net.consensys.pantheon.ethereum.vm.OperationRegistry; +import net.consensys.pantheon.ethereum.vm.operations.AddModOperation; +import net.consensys.pantheon.ethereum.vm.operations.AddOperation; +import net.consensys.pantheon.ethereum.vm.operations.AddressOperation; +import net.consensys.pantheon.ethereum.vm.operations.AndOperation; +import net.consensys.pantheon.ethereum.vm.operations.BalanceOperation; +import net.consensys.pantheon.ethereum.vm.operations.BlockHashOperation; +import net.consensys.pantheon.ethereum.vm.operations.ByteOperation; +import net.consensys.pantheon.ethereum.vm.operations.CallCodeOperation; +import net.consensys.pantheon.ethereum.vm.operations.CallDataCopyOperation; +import net.consensys.pantheon.ethereum.vm.operations.CallDataLoadOperation; +import net.consensys.pantheon.ethereum.vm.operations.CallDataSizeOperation; +import net.consensys.pantheon.ethereum.vm.operations.CallOperation; +import net.consensys.pantheon.ethereum.vm.operations.CallValueOperation; +import net.consensys.pantheon.ethereum.vm.operations.CallerOperation; +import net.consensys.pantheon.ethereum.vm.operations.CodeCopyOperation; +import net.consensys.pantheon.ethereum.vm.operations.CodeSizeOperation; +import net.consensys.pantheon.ethereum.vm.operations.CoinbaseOperation; +import net.consensys.pantheon.ethereum.vm.operations.Create2Operation; +import net.consensys.pantheon.ethereum.vm.operations.CreateOperation; +import net.consensys.pantheon.ethereum.vm.operations.DelegateCallOperation; +import net.consensys.pantheon.ethereum.vm.operations.DifficultyOperation; +import net.consensys.pantheon.ethereum.vm.operations.DivOperation; +import net.consensys.pantheon.ethereum.vm.operations.DupOperation; +import net.consensys.pantheon.ethereum.vm.operations.EqOperation; +import net.consensys.pantheon.ethereum.vm.operations.ExpOperation; +import net.consensys.pantheon.ethereum.vm.operations.ExtCodeCopyOperation; +import net.consensys.pantheon.ethereum.vm.operations.ExtCodeSizeOperation; +import net.consensys.pantheon.ethereum.vm.operations.GasLimitOperation; +import net.consensys.pantheon.ethereum.vm.operations.GasOperation; +import net.consensys.pantheon.ethereum.vm.operations.GasPriceOperation; +import net.consensys.pantheon.ethereum.vm.operations.GtOperation; +import net.consensys.pantheon.ethereum.vm.operations.InvalidOperation; +import net.consensys.pantheon.ethereum.vm.operations.IsZeroOperation; +import net.consensys.pantheon.ethereum.vm.operations.JumpDestOperation; +import net.consensys.pantheon.ethereum.vm.operations.JumpOperation; +import net.consensys.pantheon.ethereum.vm.operations.JumpiOperation; +import net.consensys.pantheon.ethereum.vm.operations.LogOperation; +import net.consensys.pantheon.ethereum.vm.operations.LtOperation; +import net.consensys.pantheon.ethereum.vm.operations.MLoadOperation; +import net.consensys.pantheon.ethereum.vm.operations.MSizeOperation; +import net.consensys.pantheon.ethereum.vm.operations.MStore8Operation; +import net.consensys.pantheon.ethereum.vm.operations.MStoreOperation; +import net.consensys.pantheon.ethereum.vm.operations.ModOperation; +import net.consensys.pantheon.ethereum.vm.operations.MulModOperation; +import net.consensys.pantheon.ethereum.vm.operations.MulOperation; +import net.consensys.pantheon.ethereum.vm.operations.NotOperation; +import net.consensys.pantheon.ethereum.vm.operations.NumberOperation; +import net.consensys.pantheon.ethereum.vm.operations.OrOperation; +import net.consensys.pantheon.ethereum.vm.operations.OriginOperation; +import net.consensys.pantheon.ethereum.vm.operations.PCOperation; +import net.consensys.pantheon.ethereum.vm.operations.PopOperation; +import net.consensys.pantheon.ethereum.vm.operations.PushOperation; +import net.consensys.pantheon.ethereum.vm.operations.ReturnDataCopyOperation; +import net.consensys.pantheon.ethereum.vm.operations.ReturnDataSizeOperation; +import net.consensys.pantheon.ethereum.vm.operations.ReturnOperation; +import net.consensys.pantheon.ethereum.vm.operations.RevertOperation; +import net.consensys.pantheon.ethereum.vm.operations.SDivOperation; +import net.consensys.pantheon.ethereum.vm.operations.SGtOperation; +import net.consensys.pantheon.ethereum.vm.operations.SLoadOperation; +import net.consensys.pantheon.ethereum.vm.operations.SLtOperation; +import net.consensys.pantheon.ethereum.vm.operations.SModOperation; +import net.consensys.pantheon.ethereum.vm.operations.SStoreOperation; +import net.consensys.pantheon.ethereum.vm.operations.SarOperation; +import net.consensys.pantheon.ethereum.vm.operations.SelfDestructOperation; +import net.consensys.pantheon.ethereum.vm.operations.Sha3Operation; +import net.consensys.pantheon.ethereum.vm.operations.ShlOperation; +import net.consensys.pantheon.ethereum.vm.operations.ShrOperation; +import net.consensys.pantheon.ethereum.vm.operations.SignExtendOperation; +import net.consensys.pantheon.ethereum.vm.operations.StaticCallOperation; +import net.consensys.pantheon.ethereum.vm.operations.StopOperation; +import net.consensys.pantheon.ethereum.vm.operations.SubOperation; +import net.consensys.pantheon.ethereum.vm.operations.SwapOperation; +import net.consensys.pantheon.ethereum.vm.operations.TimestampOperation; +import net.consensys.pantheon.ethereum.vm.operations.XorOperation; + +import java.util.List; +import java.util.function.Function; + +import com.google.common.collect.ImmutableList; + +/** Provides EVMs supporting the appropriate operations for mainnet hard forks. */ +public abstract class MainnetEvmRegistries { + + private interface OperationFactory extends Function {} + + private static final List FRONTIER_OPERATION_FACTORIES; + private static final List HOMESTEAD_OPERATION_FACTORIES; + private static final List BYZANTIUM_OPERATION_FACTORIES; + private static final List CONSTANTINOPLE_OPERATION_FACTORIES; + + static { + FRONTIER_OPERATION_FACTORIES = buildFrontierFactories(); + HOMESTEAD_OPERATION_FACTORIES = buildHomesteadFactories(FRONTIER_OPERATION_FACTORIES); + BYZANTIUM_OPERATION_FACTORIES = buildByzantiumFactories(HOMESTEAD_OPERATION_FACTORIES); + CONSTANTINOPLE_OPERATION_FACTORIES = + buildConstantinopleFactories(BYZANTIUM_OPERATION_FACTORIES); + } + + private static EVM createAndPopulate( + final List factories, final GasCalculator gasCalculator) { + final OperationRegistry registry = new OperationRegistry(); + + for (final OperationFactory factory : factories) { + final Operation operation = factory.apply(gasCalculator); + registry.put(operation.getOpcode(), operation); + } + + return new EVM(registry, new InvalidOperation(gasCalculator)); + } + + public static EVM frontier(final GasCalculator gasCalculator) { + return createAndPopulate(FRONTIER_OPERATION_FACTORIES, gasCalculator); + } + + public static EVM homestead(final GasCalculator gasCalculator) { + return createAndPopulate(HOMESTEAD_OPERATION_FACTORIES, gasCalculator); + } + + public static EVM byzantium(final GasCalculator gasCalculator) { + return createAndPopulate(BYZANTIUM_OPERATION_FACTORIES, gasCalculator); + } + + public static EVM constantinople(final GasCalculator gasCalculator) { + return createAndPopulate(CONSTANTINOPLE_OPERATION_FACTORIES, gasCalculator); + } + + private static List buildFrontierFactories() { + final ImmutableList.Builder builder = ImmutableList.builder(); + + builder.add(AddOperation::new); + builder.add(AddOperation::new); + builder.add(MulOperation::new); + builder.add(SubOperation::new); + builder.add(DivOperation::new); + builder.add(SDivOperation::new); + builder.add(ModOperation::new); + builder.add(SModOperation::new); + builder.add(ExpOperation::new); + builder.add(AddModOperation::new); + builder.add(MulModOperation::new); + builder.add(SignExtendOperation::new); + builder.add(LtOperation::new); + builder.add(GtOperation::new); + builder.add(SLtOperation::new); + builder.add(SGtOperation::new); + builder.add(EqOperation::new); + builder.add(IsZeroOperation::new); + builder.add(AndOperation::new); + builder.add(OrOperation::new); + builder.add(XorOperation::new); + builder.add(NotOperation::new); + builder.add(ByteOperation::new); + builder.add(Sha3Operation::new); + builder.add(AddressOperation::new); + builder.add(BalanceOperation::new); + builder.add(OriginOperation::new); + builder.add(CallerOperation::new); + builder.add(CallValueOperation::new); + builder.add(CallDataLoadOperation::new); + builder.add(CallDataSizeOperation::new); + builder.add(CallDataCopyOperation::new); + builder.add(CodeSizeOperation::new); + builder.add(CodeCopyOperation::new); + builder.add(GasPriceOperation::new); + builder.add(ExtCodeCopyOperation::new); + builder.add(ExtCodeSizeOperation::new); + builder.add(BlockHashOperation::new); + builder.add(CoinbaseOperation::new); + builder.add(TimestampOperation::new); + builder.add(NumberOperation::new); + builder.add(DifficultyOperation::new); + builder.add(GasLimitOperation::new); + builder.add(PopOperation::new); + builder.add(MLoadOperation::new); + builder.add(MStoreOperation::new); + builder.add(MStore8Operation::new); + builder.add(SLoadOperation::new); + builder.add(SStoreOperation::new); + builder.add(JumpOperation::new); + builder.add(JumpiOperation::new); + builder.add(PCOperation::new); + builder.add(MSizeOperation::new); + builder.add(GasOperation::new); + builder.add(JumpDestOperation::new); + builder.add(ReturnOperation::new); + builder.add(InvalidOperation::new); + builder.add(StopOperation::new); + builder.add(SelfDestructOperation::new); + builder.add(CreateOperation::new); + builder.add(CallOperation::new); + builder.add(CallCodeOperation::new); + + // Register the PUSH1, PUSH2, ..., PUSH32 operations. + for (int i = 1; i <= 32; ++i) { + final int n = i; + builder.add(f -> new PushOperation(n, f)); + } + + // Register the DUP1, DUP2, ..., DUP16 operations. + for (int i = 1; i <= 16; ++i) { + final int n = i; + builder.add(f -> new DupOperation(n, f)); + } + + // Register the SWAP1, SWAP2, ..., SWAP16 operations. + for (int i = 1; i <= 16; ++i) { + final int n = i; + builder.add(f -> new SwapOperation(n, f)); + } + + // Register the LOG0, LOG1, ..., LOG4 operations. + for (int i = 0; i < 5; ++i) { + final int n = i; + builder.add(f -> new LogOperation(n, f)); + } + + return builder.build(); + } + + private static List buildHomesteadFactories( + final List factories) { + final ImmutableList.Builder builder = ImmutableList.builder(); + + builder.addAll(factories); + builder.add(DelegateCallOperation::new); + + return builder.build(); + } + + private static List buildByzantiumFactories( + final List factories) { + final ImmutableList.Builder builder = ImmutableList.builder(); + + builder.addAll(factories); + builder.add(ReturnDataCopyOperation::new); + builder.add(ReturnDataSizeOperation::new); + builder.add(RevertOperation::new); + builder.add(StaticCallOperation::new); + + return builder.build(); + } + + private static List buildConstantinopleFactories( + final List factories) { + + final ImmutableList.Builder builder = ImmutableList.builder(); + + builder.addAll(factories); + builder.add(Create2Operation::new); + builder.add(SarOperation::new); + builder.add(ShlOperation::new); + builder.add(ShrOperation::new); + + return builder.build(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetMessageCallProcessor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetMessageCallProcessor.java new file mode 100755 index 00000000000..1442cb9c963 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetMessageCallProcessor.java @@ -0,0 +1,123 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collection; + +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class MainnetMessageCallProcessor extends AbstractMessageProcessor { + private static final Logger LOGGER = LogManager.getLogger(); + + private final PrecompileContractRegistry precompiles; + + public MainnetMessageCallProcessor( + final EVM evm, + final PrecompileContractRegistry precompiles, + final Collection
forceCommitAddresses) { + super(evm, forceCommitAddresses); + this.precompiles = precompiles; + } + + public MainnetMessageCallProcessor(final EVM evm, final PrecompileContractRegistry precompiles) { + super(evm, ImmutableSet.of()); + this.precompiles = precompiles; + } + + @Override + public void start(final MessageFrame frame) { + LOGGER.trace("Executing message-call"); + + transferValue(frame); + + // Check first if the message call is to a pre-compile contract + final PrecompiledContract precompile = precompiles.get(frame.getContractAddress()); + if (precompile != null) { + executePrecompile(precompile, frame); + } else { + frame.setState(MessageFrame.State.CODE_EXECUTING); + } + } + + @Override + protected void codeSuccess(final MessageFrame frame) { + LOGGER.trace( + "Successful message call of {} to {} (Gas remaining: {})", + frame.getSenderAddress(), + frame.getRecipientAddress(), + frame.getRemainingGas()); + frame.setState(MessageFrame.State.COMPLETED_SUCCESS); + } + + /** + * Transfers the message call value from the sender to the recipient. + * + *

Assumes that the transaction has been validated so that the sender has the required fund as + * of the world state of this executor. + */ + private void transferValue(final MessageFrame frame) { + final MutableAccount senderAccount = frame.getWorldState().getMutable(frame.getSenderAddress()); + // The yellow paper explicitly states that if the recipient account doesn't exist at this + // point, it is created. + final MutableAccount recipientAccount = + frame.getWorldState().getOrCreate(frame.getRecipientAddress()); + + if (frame.getRecipientAddress().equals(frame.getSenderAddress())) { + LOGGER.trace("Message call of {} to itself: no fund transferred", frame.getSenderAddress()); + } else { + final Wei prevSenderBalance = senderAccount.decrementBalance(frame.getValue()); + final Wei prevRecipientBalance = recipientAccount.incrementBalance(frame.getValue()); + + LOGGER.trace( + "Transferred value {} for message call from {} ({} -> {}) to {} ({} -> {})", + frame.getValue(), + frame.getSenderAddress(), + prevSenderBalance, + senderAccount.getBalance(), + frame.getRecipientAddress(), + prevRecipientBalance, + recipientAccount.getBalance()); + } + } + + /** + * Executes this message call knowing that it is a call to the provide pre-compiled contract. + * + * @param contract The contract this is a message call to. + */ + private void executePrecompile(final PrecompiledContract contract, final MessageFrame frame) { + final Gas gasRequirement = contract.gasRequirement(frame.getInputData()); + if (frame.getRemainingGas().compareTo(gasRequirement) < 0) { + LOGGER.trace( + "Not enough gas available for pre-compiled contract code {}: requiring " + + "{} but only {} gas available", + contract, + gasRequirement, + frame.getRemainingGas()); + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + } else { + frame.decrementRemainingGas(gasRequirement); + final BytesValue output = contract.compute(frame.getInputData()); + if (output != null) { + frame.setOutputData(output); + LOGGER.trace( + "Precompiled contract {} successfully executed (gas consumed: {})", + contract, + gasRequirement); + frame.setState(MessageFrame.State.COMPLETED_SUCCESS); + } else { + LOGGER.trace( + "Precompiled contract {} failed (gas consumed: {})", contract, gasRequirement); + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + } + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetPrecompiledContractRegistries.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetPrecompiledContractRegistries.java new file mode 100755 index 00000000000..a31edaa72e4 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetPrecompiledContractRegistries.java @@ -0,0 +1,43 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.mainnet.precompiles.AltBN128AddPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.AltBN128MulPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.AltBN128PairingPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.BigIntegerModularExponentiationPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.ECRECPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.IDPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.RIPEMD160PrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.SHA256PrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; + +/** Provides the various precompiled contracts used on mainnet hard forks. */ +public abstract class MainnetPrecompiledContractRegistries { + + private MainnetPrecompiledContractRegistries() {} + + private static void populateForFrontier( + final PrecompileContractRegistry registry, final GasCalculator gasCalculator) { + registry.put(Address.ECREC, new ECRECPrecompiledContract(gasCalculator)); + registry.put(Address.SHA256, new SHA256PrecompiledContract(gasCalculator)); + registry.put(Address.RIPEMD160, new RIPEMD160PrecompiledContract(gasCalculator)); + registry.put(Address.ID, new IDPrecompiledContract(gasCalculator)); + } + + public static PrecompileContractRegistry frontier(final GasCalculator gasCalculator) { + final PrecompileContractRegistry registry = new PrecompileContractRegistry(); + populateForFrontier(registry, gasCalculator); + return registry; + } + + public static PrecompileContractRegistry byzantium(final GasCalculator gasCalculator) { + final PrecompileContractRegistry registry = new PrecompileContractRegistry(); + populateForFrontier(registry, gasCalculator); + registry.put( + Address.MODEXP, new BigIntegerModularExponentiationPrecompiledContract(gasCalculator)); + registry.put(Address.ALTBN128_ADD, new AltBN128AddPrecompiledContract(gasCalculator)); + registry.put(Address.ALTBN128_MUL, new AltBN128MulPrecompiledContract(gasCalculator)); + registry.put(Address.ALTBN128_PAIRING, new AltBN128PairingPrecompiledContract(gasCalculator)); + return registry; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSchedule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSchedule.java new file mode 100755 index 00000000000..0e70e0006b2 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSchedule.java @@ -0,0 +1,105 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import io.vertx.core.json.JsonObject; + +/** Provides {@link ProtocolSpec} lookups for mainnet hard forks. */ +public class MainnetProtocolSchedule { + + private static final long DEFAULT_HOMESTEAD_BLOCK_NUMBER = 1_150_000L; + private static final long DEFAULT_DAO_BLOCK_NUMBER = 1_920_000L; + private static final long DEFAULT_TANGERINE_WHISTLE_BLOCK_NUMBER = 2_463_000L; + private static final long DEFAULT_SPURIOUS_DRAGON_BLOCK_NUMBER = 2_675_000L; + private static final long DEFAULT_BYZANTIUM_BLOCK_NUMBER = 4_730_000L; + // Start of Constantinople has not yet been set. + private static final long DEFAULT_CONSTANTINOPLE_BLOCK_NUMBER = -1L; + public static final int DEFAULT_CHAIN_ID = 1; + + /** + * Creates a mainnet protocol schedule with milestones starting at the specified block numbers + * + * @param homesteadBlockNumber Block number at which to start the homestead fork + * @param daoBlockNumber Block number at which to start the dao fork + * @param tangerineWhistleBlockNumber Block number at which to start the tangerine whistle fork + * @param spuriousDragonBlockNumber Block number at which to start the spurious dragon fork + * @param byzantiumBlockNumber Block number at which to start the byzantium fork + * @param constantinopleBlockNumber Block number at which to start the constantinople fork + * @param chainId ID of the blockchain + * @return MainnetProtocolSchedule return newly instantiated protocol schedule + */ + public static ProtocolSchedule create( + final long homesteadBlockNumber, + final long daoBlockNumber, + final long tangerineWhistleBlockNumber, + final long spuriousDragonBlockNumber, + final long byzantiumBlockNumber, + final long constantinopleBlockNumber, + final int chainId) { + + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone(0, MainnetProtocolSpecs.frontier(protocolSchedule)); + final ProtocolSpec homestead = MainnetProtocolSpecs.homestead(protocolSchedule); + protocolSchedule.putMilestone(homesteadBlockNumber, homestead); + if (daoBlockNumber != 0) { + protocolSchedule.putMilestone( + daoBlockNumber, MainnetProtocolSpecs.daoRecoveryInit(protocolSchedule)); + protocolSchedule.putMilestone( + daoBlockNumber + 1, MainnetProtocolSpecs.daoRecoveryTransition(protocolSchedule)); + protocolSchedule.putMilestone(daoBlockNumber + 10, homestead); + } + protocolSchedule.putMilestone( + tangerineWhistleBlockNumber, MainnetProtocolSpecs.tangerineWhistle(protocolSchedule)); + protocolSchedule.putMilestone( + spuriousDragonBlockNumber, MainnetProtocolSpecs.spuriousDragon(chainId, protocolSchedule)); + protocolSchedule.putMilestone( + byzantiumBlockNumber, MainnetProtocolSpecs.byzantium(chainId, protocolSchedule)); + + if (constantinopleBlockNumber >= 0) { + protocolSchedule.putMilestone( + constantinopleBlockNumber, + MainnetProtocolSpecs.constantinople(chainId, protocolSchedule)); + } + + return protocolSchedule; + } + + public static ProtocolSchedule create() { + return create( + DEFAULT_HOMESTEAD_BLOCK_NUMBER, + DEFAULT_DAO_BLOCK_NUMBER, + DEFAULT_TANGERINE_WHISTLE_BLOCK_NUMBER, + DEFAULT_SPURIOUS_DRAGON_BLOCK_NUMBER, + DEFAULT_BYZANTIUM_BLOCK_NUMBER, + DEFAULT_CONSTANTINOPLE_BLOCK_NUMBER, + DEFAULT_CHAIN_ID); + } + + /** + * Create a Mainnet protocol schedule from a config object + * + * @param config {@link JsonObject} containing the config options for the milestone starting + * points + * @return A configured mainnet protocol schedule + */ + public static ProtocolSchedule fromConfig(final JsonObject config) { + final long homesteadBlockNumber = + config.getLong("homesteadBlock", DEFAULT_HOMESTEAD_BLOCK_NUMBER); + final long daoBlockNumber = config.getLong("daoForkBlock", DEFAULT_DAO_BLOCK_NUMBER); + final long tangerineWhistleBlockNumber = + config.getLong("eip150Block", DEFAULT_TANGERINE_WHISTLE_BLOCK_NUMBER); + final long spuriousDragonBlockNumber = + config.getLong("eip158Block", DEFAULT_SPURIOUS_DRAGON_BLOCK_NUMBER); + final long byzantiumBlockNumber = + config.getLong("byzantiumBlock", DEFAULT_BYZANTIUM_BLOCK_NUMBER); + final long constantinopleBlockNumber = + config.getLong("constantinopleBlock", DEFAULT_CONSTANTINOPLE_BLOCK_NUMBER); + final int chainId = config.getInteger("chainId", DEFAULT_CHAIN_ID); + return create( + homesteadBlockNumber, + daoBlockNumber, + tangerineWhistleBlockNumber, + spuriousDragonBlockNumber, + byzantiumBlockNumber, + constantinopleBlockNumber, + chainId); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSpecs.java new file mode 100755 index 00000000000..178e4cf55f2 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolSpecs.java @@ -0,0 +1,320 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import io.vertx.core.json.JsonArray; + +/** Provides the various @{link ProtocolSpec}s on mainnet hard forks. */ +public abstract class MainnetProtocolSpecs { + + public static final int FRONTIER_CONTRACT_SIZE_LIMIT = Integer.MAX_VALUE; + + public static final int SPURIOUS_DRAGON_CONTRACT_SIZE_LIMIT = 24576; + + private static final Address RIPEMD160_PRECOMPILE = + Address.fromHexString("0x0000000000000000000000000000000000000003"); + + // A consensus bug at Ethereum mainnet transaction 0xcf416c53 + // deleted an empty account even when the message execution scope + // failed, but the transaction itself succeeded. + private static final ImmutableSet

SPURIOUS_DRAGON_FORCE_DELETE_WHEN_EMPTY_ADDRESSES = + ImmutableSet.of(RIPEMD160_PRECOMPILE); + + private static final Wei FRONTIER_BLOCK_REWARD = Wei.fromEth(5); + + private static final Wei BYZANTIUM_BLOCK_REWARD = Wei.fromEth(3); + + private static final Wei CONSTANTINOPLE_BLOCK_REWARD = Wei.fromEth(2); + + private MainnetProtocolSpecs() {} + + public static ProtocolSpecBuilder frontierDefinition() { + return new ProtocolSpecBuilder() + .gasCalculator(FrontierGasCalculator::new) + .evmBuilder(MainnetEvmRegistries::frontier) + .precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::frontier) + .messageCallProcessorBuilder(MainnetMessageCallProcessor::new) + .contractCreationProcessorBuilder( + (gasCalculator, evm) -> + new MainnetContractCreationProcessor( + gasCalculator, evm, false, FRONTIER_CONTRACT_SIZE_LIMIT, 0)) + .transactionValidatorBuilder( + gasCalculator -> new MainnetTransactionValidator(gasCalculator, false)) + .transactionProcessorBuilder( + (gasCalculator, + transactionValidator, + contractCreationProcessor, + messageCallProcessor) -> + new MainnetTransactionProcessor( + gasCalculator, + transactionValidator, + contractCreationProcessor, + messageCallProcessor, + false)) + .difficultyCalculator(MainnetDifficultyCalculators.FRONTIER) + .blockHeaderValidatorBuilder(MainnetBlockHeaderValidator::create) + .blockBodyValidatorBuilder(MainnetBlockBodyValidator::new) + .transactionReceiptFactory(MainnetProtocolSpecs::frontierTransactionReceiptFactory) + .blockReward(FRONTIER_BLOCK_REWARD) + .blockProcessorBuilder(MainnetBlockProcessor::new) + .blockImporterBuilder(MainnetBlockImporter::new) + .transactionReceiptType(TransactionReceiptType.ROOT) + .blockHashFunction(MainnetBlockHashFunction::createHash) + .miningBeneficiaryCalculator(BlockHeader::getCoinbase) + .name("Frontier"); + } + + /** + * Returns the Frontier milestone protocol spec. + * + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the Frontier milestone protocol spec + */ + public static ProtocolSpec frontier(final ProtocolSchedule protocolSchedule) { + return frontierDefinition().build(protocolSchedule); + } + + /** + * Returns the Homestead milestone protocol spec. + * + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the Homestead milestone protocol spec + */ + public static ProtocolSpec homestead(final ProtocolSchedule protocolSchedule) { + return homesteadDefinition().build(protocolSchedule); + } + + public static ProtocolSpecBuilder homesteadDefinition() { + return frontierDefinition() + .gasCalculator(HomesteadGasCalculator::new) + .evmBuilder(MainnetEvmRegistries::homestead) + .contractCreationProcessorBuilder( + (gasCalculator, evm) -> + new MainnetContractCreationProcessor( + gasCalculator, evm, true, FRONTIER_CONTRACT_SIZE_LIMIT, 0)) + .transactionValidatorBuilder( + gasCalculator -> new MainnetTransactionValidator(gasCalculator, true)) + .difficultyCalculator(MainnetDifficultyCalculators.HOMESTEAD) + .name("Homestead"); + } + + /** + * Returns the initial DAO block milestone protocol spec. + * + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the initial DAO block milestone protocol spec + */ + public static ProtocolSpec daoRecoveryInit(final ProtocolSchedule protocolSchedule) { + return daoRecoveryInitDefinition().build(protocolSchedule); + } + + private static ProtocolSpecBuilder daoRecoveryInitDefinition() { + return homesteadDefinition() + .blockHeaderValidatorBuilder(MainnetBlockHeaderValidator::createDaoValidator) + .blockProcessorBuilder( + (transactionProcessor, + transactionReceiptFactory, + blockReward, + miningBeneficiaryCalculator) -> + new DaoBlockProcessor( + new MainnetBlockProcessor( + transactionProcessor, + transactionReceiptFactory, + blockReward, + miningBeneficiaryCalculator))) + .name("DaoRecoveryInit"); + } + + /** + * Returns the DAO block transition segment milestone protocol spec. + * + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the DAO block transition segment milestone protocol spec + */ + public static ProtocolSpec daoRecoveryTransition( + final ProtocolSchedule protocolSchedule) { + return daoRecoveryInitDefinition() + .blockProcessorBuilder(MainnetBlockProcessor::new) + .name("DaoRecoveryTransition") + .build(protocolSchedule); + } + + /** + * Returns the Tangerine Whistle milestone protocol spec. + * + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the Tangerine Whistle milestone protocol spec + */ + public static ProtocolSpec tangerineWhistle(final ProtocolSchedule protocolSchedule) { + return tangerineWhistleDefinition().build(protocolSchedule); + } + + public static ProtocolSpecBuilder tangerineWhistleDefinition() { + return homesteadDefinition() + .gasCalculator(TangerineWhistleGasCalculator::new) + .name("TangerineWhistle"); + } + + /** + * Returns the Spurious Dragon milestone protocol spec. + * + * @param chainId ID of the blockchain + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the Spurious Dragon milestone protocol spec + */ + public static ProtocolSpec spuriousDragon( + final int chainId, final ProtocolSchedule protocolSchedule) { + return spuriousDragonDefinition(chainId).build(protocolSchedule); + } + + public static ProtocolSpecBuilder spuriousDragonDefinition(final int chainId) { + return tangerineWhistleDefinition() + .gasCalculator(SpuriousDragonGasCalculator::new) + .messageCallProcessorBuilder( + (evm, precompileContractRegistry) -> + new MainnetMessageCallProcessor( + evm, + precompileContractRegistry, + SPURIOUS_DRAGON_FORCE_DELETE_WHEN_EMPTY_ADDRESSES)) + .contractCreationProcessorBuilder( + (gasCalculator, evm) -> + new MainnetContractCreationProcessor( + gasCalculator, + evm, + true, + SPURIOUS_DRAGON_CONTRACT_SIZE_LIMIT, + 1, + SPURIOUS_DRAGON_FORCE_DELETE_WHEN_EMPTY_ADDRESSES)) + .transactionValidatorBuilder( + gasCalculator -> new MainnetTransactionValidator(gasCalculator, true, chainId)) + .transactionProcessorBuilder( + (gasCalculator, + transactionValidator, + contractCreationProcessor, + messageCallProcessor) -> + new MainnetTransactionProcessor( + gasCalculator, + transactionValidator, + contractCreationProcessor, + messageCallProcessor, + true)) + .name("SpuriousDragon"); + } + + /** + * Returns the Byzantium milestone protocol spec. + * + * @param chainId ID of the blockchain + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the Byzantium milestone protocol spec + */ + public static ProtocolSpec byzantium( + final int chainId, final ProtocolSchedule protocolSchedule) { + return byzantiumDefinition(chainId).build(protocolSchedule); + } + + public static ProtocolSpecBuilder byzantiumDefinition(final int chainId) { + return spuriousDragonDefinition(chainId) + .evmBuilder(MainnetEvmRegistries::byzantium) + .precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::byzantium) + .difficultyCalculator(MainnetDifficultyCalculators.BYZANTIUM) + .transactionReceiptFactory(MainnetProtocolSpecs::byzantiumTransactionReceiptFactory) + .blockReward(BYZANTIUM_BLOCK_REWARD) + .transactionReceiptType(TransactionReceiptType.STATUS) + .name("Byzantium"); + } + + /** + * Returns the Constantinople milestone protocol spec. + * + * @param chainId ID of the blockchain + * @param protocolSchedule the {@link ProtocolSchedule} this spec will be part of + * @return the Constantinople milestone protocol spec + */ + public static ProtocolSpec constantinople( + final int chainId, final ProtocolSchedule protocolSchedule) { + return byzantiumDefinition(chainId) + .difficultyCalculator(MainnetDifficultyCalculators.CONSTANTINOPLE) + .gasCalculator(ConstantinopleGasCalculator::new) + .evmBuilder(MainnetEvmRegistries::constantinople) + .blockReward(CONSTANTINOPLE_BLOCK_REWARD) + .name("Constantinople") + .build(protocolSchedule); + } + + private static TransactionReceipt frontierTransactionReceiptFactory( + final TransactionProcessor.Result result, final WorldState worldState, final long gasUsed) { + return new TransactionReceipt(worldState.rootHash(), gasUsed, result.getLogs()); + } + + private static TransactionReceipt byzantiumTransactionReceiptFactory( + final TransactionProcessor.Result result, final WorldState worldState, final long gasUsed) { + return new TransactionReceipt(result.isSuccessful() ? 1 : 0, gasUsed, result.getLogs()); + } + + private static class DaoBlockProcessor implements BlockProcessor { + + private final BlockProcessor wrapped; + + public DaoBlockProcessor(final BlockProcessor wrapped) { + this.wrapped = wrapped; + } + + @Override + public Result processBlock( + final Blockchain blockchain, + final MutableWorldState worldState, + final BlockHeader blockHeader, + final List transactions, + final List ommers) { + updateWorldStateForDao(worldState); + return wrapped.processBlock(blockchain, worldState, blockHeader, transactions, ommers); + } + + private static final Address DAO_REFUND_CONTRACT_ADDRESS = + Address.fromHexString("0xbf4ed7b27f1d666546e30d74d50d173d20bca754"); + + private void updateWorldStateForDao(final MutableWorldState worldState) { + try { + final JsonArray json = + new JsonArray( + Resources.toString( + Resources.getResource("daoAddresses.json"), StandardCharsets.UTF_8)); + final List
addresses = + IntStream.range(0, json.size()) + .mapToObj(json::getString) + .map(Address::fromHexString) + .collect(Collectors.toList()); + final WorldUpdater worldUpdater = worldState.updater(); + final MutableAccount daoRefundContract = + worldUpdater.getOrCreate(DAO_REFUND_CONTRACT_ADDRESS); + for (final Address address : addresses) { + final MutableAccount account = worldUpdater.getOrCreate(address); + final Wei balance = account.getBalance(); + account.decrementBalance(balance); + daoRefundContract.incrementBalance(balance); + } + worldUpdater.commit(); + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionProcessor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionProcessor.java new file mode 100755 index 00000000000..d161b4116e6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionProcessor.java @@ -0,0 +1,310 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.LogSeries; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import net.consensys.pantheon.ethereum.vm.Code; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.OperationTracer; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.OptionalLong; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class MainnetTransactionProcessor implements TransactionProcessor { + + private static final Logger LOGGER = LogManager.getLogger(MainnetTransactionProcessor.class); + + private final GasCalculator gasCalculator; + + private final TransactionValidator transactionValidator; + + private final AbstractMessageProcessor contractCreationProcessor; + + private final AbstractMessageProcessor messageCallProcessor; + + public static class Result implements TransactionProcessor.Result { + + private final Status status; + + private final long gasRemaining; + + private final LogSeries logs; + + private final BytesValue output; + + private final ValidationResult validationResult; + + public static Result invalid( + final ValidationResult validationResult) { + return new Result(Status.INVALID, LogSeries.empty(), -1, BytesValue.EMPTY, validationResult); + } + + public static Result failed( + final long gasRemaining, + final ValidationResult validationResult) { + return new Result( + Status.FAILED, LogSeries.empty(), gasRemaining, BytesValue.EMPTY, validationResult); + } + + public static Result successful( + final LogSeries logs, + final long gasRemaining, + final BytesValue output, + final ValidationResult validationResult) { + return new Result(Status.SUCCESSFUL, logs, gasRemaining, output, validationResult); + } + + Result( + final Status status, + final LogSeries logs, + final long gasRemaining, + final BytesValue output, + final ValidationResult validationResult) { + this.status = status; + this.logs = logs; + this.gasRemaining = gasRemaining; + this.output = output; + this.validationResult = validationResult; + } + + @Override + public LogSeries getLogs() { + return logs; + } + + @Override + public long getGasRemaining() { + return gasRemaining; + } + + @Override + public Status getStatus() { + return status; + } + + @Override + public BytesValue getOutput() { + return output; + } + + @Override + public ValidationResult getValidationResult() { + return validationResult; + } + } + + private final boolean clearEmptyAccounts; + + public MainnetTransactionProcessor( + final GasCalculator gasCalculator, + final TransactionValidator transactionValidator, + final AbstractMessageProcessor contractCreationProcessor, + final AbstractMessageProcessor messageCallProcessor, + final boolean clearEmptyAccounts) { + this.gasCalculator = gasCalculator; + this.transactionValidator = transactionValidator; + this.contractCreationProcessor = contractCreationProcessor; + this.messageCallProcessor = messageCallProcessor; + this.clearEmptyAccounts = clearEmptyAccounts; + } + + @Override + public Result processTransaction( + final Blockchain blockchain, + final WorldUpdater worldState, + final ProcessableBlockHeader blockHeader, + final Transaction transaction, + final Address miningBenficiary, + final OperationTracer operationTracer) { + LOGGER.trace("Starting execution of {}", transaction); + + ValidationResult validationResult = + transactionValidator.validate(transaction); + // Make sure the transaction is intrinsically valid before trying to + // compare against a sender account (because the transaction may not + // be signed correctly to extract the sender). + if (!validationResult.isValid()) { + LOGGER.warn("Invalid transaction: {}", validationResult.getErrorMessage()); + return Result.invalid(validationResult); + } + + final Address senderAddress = transaction.getSender(); + final MutableAccount sender = worldState.getOrCreate(senderAddress); + validationResult = + transactionValidator.validateForSender(transaction, sender, OptionalLong.empty()); + if (!validationResult.isValid()) { + LOGGER.warn("Invalid transaction: {}", validationResult.getErrorMessage()); + return Result.invalid(validationResult); + } + + final long previousNonce = sender.incrementNonce(); + LOGGER.trace( + "Incremented sender {} nonce ({} -> {})", senderAddress, previousNonce, sender.getNonce()); + + final Wei upfrontGasCost = transaction.getUpfrontGasCost(); + final Wei previousBalance = sender.decrementBalance(upfrontGasCost); + LOGGER.trace( + "Deducted sender {} upfront gas cost {} ({} -> {})", + senderAddress, + upfrontGasCost, + previousBalance, + sender.getBalance()); + + final Gas intrinsicGas = gasCalculator.transactionIntrinsicGasCost(transaction); + final Gas gasAvailable = Gas.of(transaction.getGasLimit()).minus(intrinsicGas); + LOGGER.trace( + "Gas available for execution {} = {} - {} (limit - intrinsic)", + gasAvailable, + transaction.getGasLimit(), + intrinsicGas); + + final WorldUpdater worldUpdater = worldState.updater(); + final MessageFrame initialFrame; + final Deque messageFrameStack = new ArrayDeque<>(); + if (transaction.isContractCreation()) { + final Address contractAddress = + Address.contractAddress(senderAddress, sender.getNonce() - 1L); + + initialFrame = + MessageFrame.builder() + .type(MessageFrame.Type.CONTRACT_CREATION) + .messageFrameStack(messageFrameStack) + .blockchain(blockchain) + .worldState(worldUpdater.updater()) + .initialGas(gasAvailable) + .address(contractAddress) + .originator(senderAddress) + .contract(contractAddress) + .gasPrice(transaction.getGasPrice()) + .inputData(BytesValue.EMPTY) + .sender(senderAddress) + .value(transaction.getValue()) + .apparentValue(transaction.getValue()) + .code(new Code(transaction.getPayload())) + .blockHeader(blockHeader) + .depth(0) + .completer(c -> {}) + .build(); + + } else { + final Address to = transaction.getTo().get(); + final Account contract = worldState.get(to); + + initialFrame = + MessageFrame.builder() + .type(MessageFrame.Type.MESSAGE_CALL) + .messageFrameStack(messageFrameStack) + .blockchain(blockchain) + .worldState(worldUpdater.updater()) + .initialGas(gasAvailable) + .address(to) + .originator(senderAddress) + .contract(to) + .gasPrice(transaction.getGasPrice()) + .inputData(transaction.getPayload()) + .sender(senderAddress) + .value(transaction.getValue()) + .apparentValue(transaction.getValue()) + .code(new Code(contract != null ? contract.getCode() : BytesValue.EMPTY)) + .blockHeader(blockHeader) + .depth(0) + .completer(c -> {}) + .build(); + } + + messageFrameStack.addFirst(initialFrame); + + while (!messageFrameStack.isEmpty()) { + process(messageFrameStack.peekFirst(), operationTracer); + } + + if (initialFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS) { + worldUpdater.commit(); + } + + if (LOGGER.isTraceEnabled()) { + LOGGER.trace( + "Gas used by transaction: {}, by message call/contract creation: {}", + () -> Gas.of(transaction.getGasLimit()).minus(initialFrame.getRemainingGas()), + () -> gasAvailable.minus(initialFrame.getRemainingGas())); + } + + // Refund the sender by what we should and pay the miner fee (note that we're doing them one + // after the other so that if it is the same account somehow, we end up with the right result) + final Gas selfDestructRefund = + gasCalculator.getSelfDestructRefundAmount().times(initialFrame.getSelfDestructs().size()); + final Gas refundGas = initialFrame.getGasRefund().plus(selfDestructRefund); + final Gas refunded = refunded(transaction, initialFrame.getRemainingGas(), refundGas); + final Wei refundedWei = refunded.priceFor(transaction.getGasPrice()); + sender.incrementBalance(refundedWei); + + final MutableAccount coinbase = worldState.getOrCreate(miningBenficiary); + final Gas coinbaseFee = Gas.of(transaction.getGasLimit()).minus(refunded); + final Wei coinbaseWei = coinbaseFee.priceFor(transaction.getGasPrice()); + coinbase.incrementBalance(coinbaseWei); + + initialFrame.getSelfDestructs().forEach(worldState::deleteAccount); + + if (clearEmptyAccounts) { + clearEmptyAccounts(worldState); + } + + if (initialFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS) { + return Result.successful( + initialFrame.getLogs(), + refunded.toLong(), + initialFrame.getOutputData(), + validationResult); + } else { + return Result.failed(refunded.toLong(), validationResult); + } + } + + private static void clearEmptyAccounts(final WorldUpdater worldState) { + worldState + .getTouchedAccounts() + .stream() + .filter(Account::isEmpty) + .forEach(a -> worldState.deleteAccount(a.getAddress())); + } + + private void process(final MessageFrame frame, final OperationTracer operationTracer) { + final AbstractMessageProcessor executor = getMessageProcessor(frame.getType()); + + executor.process(frame, operationTracer); + } + + private AbstractMessageProcessor getMessageProcessor(final MessageFrame.Type type) { + switch (type) { + case MESSAGE_CALL: + return messageCallProcessor; + case CONTRACT_CREATION: + return contractCreationProcessor; + default: + throw new IllegalStateException("Request for unsupported message processor type " + type); + } + } + + private static Gas refunded( + final Transaction transaction, final Gas gasRemaining, final Gas gasRefund) { + // Integer truncation takes care of the the floor calculation needed after the divide. + final Gas maxRefundAllowance = + Gas.of(transaction.getGasLimit()).minus(gasRemaining).dividedBy(2); + final Gas refundAllowance = maxRefundAllowance.min(gasRefund); + return gasRemaining.plus(refundAllowance); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java new file mode 100755 index 00000000000..4143b006105 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidator.java @@ -0,0 +1,151 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.INCORRECT_NONCE; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.INVALID_SIGNATURE; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.NONCE_TOO_LOW; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.WRONG_CHAIN_ID; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.vm.GasCalculator; + +import java.util.OptionalInt; +import java.util.OptionalLong; + +/** + * Validates a transaction based on Frontier protocol runtime requirements. + * + *

The {@link MainnetTransactionValidator} performs the intrinsic gas cost check on the given + * {@link Transaction}. + */ +public class MainnetTransactionValidator implements TransactionValidator { + + public static final int NO_CHAIN_ID = -1; + + public static MainnetTransactionValidator create() { + return new MainnetTransactionValidator(new FrontierGasCalculator(), false); + } + + private final GasCalculator gasCalculator; + + private final boolean disallowSignatureMalleability; + + private final OptionalInt chainId; + + public MainnetTransactionValidator( + final GasCalculator gasCalculator, final boolean checkSignatureMalleability) { + this(gasCalculator, checkSignatureMalleability, NO_CHAIN_ID); + } + + public MainnetTransactionValidator( + final GasCalculator gasCalculator, + final boolean checkSignatureMalleability, + final int chainId) { + this.gasCalculator = gasCalculator; + this.disallowSignatureMalleability = checkSignatureMalleability; + this.chainId = chainId > 0 ? OptionalInt.of(chainId) : OptionalInt.empty(); + } + + @Override + public ValidationResult validate(final Transaction transaction) { + final ValidationResult signatureResult = + validateTransactionSignature(transaction); + if (!signatureResult.isValid()) { + return signatureResult; + } + + final Gas intrinsicGasCost = gasCalculator.transactionIntrinsicGasCost(transaction); + if (intrinsicGasCost.compareTo(Gas.of(transaction.getGasLimit())) > 0) { + return ValidationResult.invalid( + INTRINSIC_GAS_EXCEEDS_GAS_LIMIT, + String.format( + "intrinsic gas cost %s exceeds gas limit %s", + intrinsicGasCost, transaction.getGasLimit())); + } + + return ValidationResult.valid(); + } + + @Override + public ValidationResult validateForSender( + final Transaction transaction, final Account sender, final OptionalLong maximumNonce) { + if (sender == null) { + return ValidationResult.invalid(UPFRONT_COST_EXCEEDS_BALANCE, "Unknown sender account"); + } + if (transaction.getUpfrontCost().compareTo(sender.getBalance()) > 0) { + return ValidationResult.invalid( + UPFRONT_COST_EXCEEDS_BALANCE, + String.format( + "transaction up-front cost %s exceeds transaction sender account balance %s", + transaction.getUpfrontCost(), sender.getBalance())); + } + + if (transaction.getNonce() < sender.getNonce()) { + return ValidationResult.invalid( + NONCE_TOO_LOW, + String.format( + "transaction nonce %s below sender account nonce %s", + transaction.getNonce(), sender.getNonce())); + } + + if (violatesMaximumNonce(transaction, maximumNonce) + && sender.getNonce() != transaction.getNonce()) { + return ValidationResult.invalid( + INCORRECT_NONCE, + String.format( + "transaction nonce %s does not match sender account nonce %s.", + transaction.getNonce(), sender.getNonce())); + } + + return ValidationResult.valid(); + } + + private boolean violatesMaximumNonce( + final Transaction transaction, final OptionalLong maximumNonce) { + return !maximumNonce.isPresent() || transaction.getNonce() > maximumNonce.getAsLong(); + } + + public ValidationResult validateTransactionSignature( + final Transaction transaction) { + if (chainId.isPresent() + && (transaction.getChainId().isPresent() && !transaction.getChainId().equals(chainId))) { + return ValidationResult.invalid( + WRONG_CHAIN_ID, + String.format( + "transaction was meant for chain id %s and not this chain id %s", + transaction.getChainId().getAsInt(), chainId.getAsInt())); + } + + if (!chainId.isPresent() && transaction.getChainId().isPresent()) { + return ValidationResult.invalid( + REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED, + "replay protected signatures is not supported"); + } + + final SECP256K1.Signature signature = transaction.getSignature(); + if (disallowSignatureMalleability + && signature.getS().compareTo(SECP256K1.HALF_CURVE_ORDER) > 0) { + return ValidationResult.invalid( + INVALID_SIGNATURE, + String.format( + "Signature s value should be less than %s, but got %s", + SECP256K1.HALF_CURVE_ORDER, signature.getS())); + } + + // org.bouncycastle.math.ec.ECCurve.AbstractFp.decompressPoint throws an + // IllegalArgumentException for "Invalid point compression" for bad signatures. + try { + transaction.getSender(); + } catch (final IllegalArgumentException e) { + return ValidationResult.invalid( + INVALID_SIGNATURE, "sender could not be extracted from transaction signature"); + } + + return ValidationResult.valid(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MiningBeneficiaryCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MiningBeneficiaryCalculator.java new file mode 100755 index 00000000000..1b71e77ec0e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MiningBeneficiaryCalculator.java @@ -0,0 +1,9 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; + +@FunctionalInterface +public interface MiningBeneficiaryCalculator { + Address calculateBeneficiary(BlockHeader header); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java new file mode 100755 index 00000000000..cad296e32a6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/MutableProtocolSchedule.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.Comparator; +import java.util.NavigableSet; +import java.util.TreeSet; + +public class MutableProtocolSchedule implements ProtocolSchedule { + + private final NavigableSet> protocolSpecs = + new TreeSet<>( + Comparator., Long>comparing(ScheduledProtocolSpec::getBlock) + .reversed()); + + public void putMilestone(final long blockNumber, final ProtocolSpec protocolSpec) { + final ScheduledProtocolSpec scheduledProtocolSpec = + new ScheduledProtocolSpec<>(blockNumber, protocolSpec); + // Ensure this replaces any existing spec at the same block number. + protocolSpecs.remove(scheduledProtocolSpec); + protocolSpecs.add(scheduledProtocolSpec); + } + + @Override + public ProtocolSpec getByBlockNumber(final long number) { + checkArgument(number >= 0, "number must be non-negative"); + checkArgument( + !protocolSpecs.isEmpty(), "At least 1 milestone must be provided to the protocol schedule"); + checkArgument( + protocolSpecs.last().getBlock() == 0, "There must be a milestone starting from block 0"); + // protocolSpecs is sorted in descending block order, so the first one we find that's lower than + // the requested level will be the most appropriate spec + for (final ScheduledProtocolSpec s : protocolSpecs) { + if (number >= s.getBlock()) { + return s.getSpec(); + } + } + return null; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompileContractRegistry.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompileContractRegistry.java new file mode 100755 index 00000000000..f52716b8fb9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompileContractRegistry.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Address; + +import java.util.HashMap; +import java.util.Map; + +/** Encapsulates a group of {@link PrecompiledContract}s used together. */ +public class PrecompileContractRegistry { + + private final Map precompiles; + + public PrecompileContractRegistry() { + this.precompiles = new HashMap<>(); + } + + public PrecompiledContract get(final Address address) { + return precompiles.get(address); + } + + public void put(final Address address, final PrecompiledContract precompile) { + precompiles.put(address, precompile); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompiledContract.java new file mode 100755 index 00000000000..09ab2ed4c46 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/PrecompiledContract.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.util.bytes.BytesValue; + +/** + * A pre-compiled contract. + * + *

It corresponds to one of the function defined in Appendix E of the Yellow Paper (rev. + * a91c29c). + */ +public interface PrecompiledContract { + + /** + * Returns the pre-compiled contract name. + * + * @return the pre-compiled contract name + */ + String getName(); + + /** + * Gas requirement for the contract. + * + * @param input the input for the pre-compiled contract (on which the gas requirement may or may + * not depend). + * @return the gas requirement (cost) for the pre-compiled contract. + */ + Gas gasRequirement(BytesValue input); + + /** + * Executes the pre-compiled contract. + * + * @param input the input for the pre-compiled contract. + * @return the output of the pre-compiled contract. + */ + BytesValue compute(BytesValue input); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSchedule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSchedule.java new file mode 100755 index 00000000000..91773d4cd0a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSchedule.java @@ -0,0 +1,6 @@ +package net.consensys.pantheon.ethereum.mainnet; + +public interface ProtocolSchedule { + + ProtocolSpec getByBlockNumber(long number); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpec.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpec.java new file mode 100755 index 00000000000..1bdb5ac7604 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpec.java @@ -0,0 +1,196 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockProcessor.TransactionReceiptFactory; +import net.consensys.pantheon.ethereum.vm.EVM; + +/** A protocol specification. */ +public class ProtocolSpec { + + private final String name; + private final EVM evm; + + private final TransactionValidator transactionValidator; + + private final TransactionProcessor transactionProcessor; + + private final BlockHeaderValidator blockHeaderValidator; + + private final BlockBodyValidator blockBodyValidator; + + private final BlockImporter blockImporter; + + private final BlockProcessor blockProcessor; + + private final BlockHashFunction blockHashFunction; + + private final TransactionReceiptFactory transactionReceiptFactory; + + private final DifficultyCalculator difficultyCalculator; + + private final Wei blockReward; + + private final MiningBeneficiaryCalculator miningBeneficiaryCalculator; + + /** + * Creates a new protocol specification instance. + * + * @param name the protocol specification name + * @param evm the EVM supporting the appropriate operations for this specification + * @param transactionValidator the transaction validator to use + * @param transactionProcessor the transaction processor to use + * @param blockHeaderValidator the block header validator to use + * @param blockBodyValidator the block body validator to use + * @param blockProcessor the block processor to use + * @param blockImporter the block importer to use + * @param blockHashFunction the block hash function to use + * @param transactionReceiptFactory the transactionReceiptFactory to use + * @param difficultyCalculator the difficultyCalculator to use + * @param blockReward the blockReward to use. + * @param transactionReceiptType the type of transaction receipt to use, one of + * @param miningBeneficiaryCalculator determines to whom mining proceeds are paid + */ + public ProtocolSpec( + final String name, + final EVM evm, + final TransactionValidator transactionValidator, + final TransactionProcessor transactionProcessor, + final BlockHeaderValidator blockHeaderValidator, + final BlockBodyValidator blockBodyValidator, + final BlockProcessor blockProcessor, + final BlockImporter blockImporter, + final BlockHashFunction blockHashFunction, + final TransactionReceiptFactory transactionReceiptFactory, + final DifficultyCalculator difficultyCalculator, + final Wei blockReward, + final TransactionReceiptType transactionReceiptType, + final MiningBeneficiaryCalculator miningBeneficiaryCalculator) { + this.name = name; + this.evm = evm; + this.transactionValidator = transactionValidator; + this.transactionProcessor = transactionProcessor; + this.blockHeaderValidator = blockHeaderValidator; + this.blockBodyValidator = blockBodyValidator; + this.blockProcessor = blockProcessor; + this.blockImporter = blockImporter; + this.blockHashFunction = blockHashFunction; + this.transactionReceiptFactory = transactionReceiptFactory; + this.difficultyCalculator = difficultyCalculator; + this.blockReward = blockReward; + this.miningBeneficiaryCalculator = miningBeneficiaryCalculator; + } + + /** + * Returns the protocol specification name. + * + * @return the protocol specification name + */ + public String getName() { + return name; + } + + /** + * Returns the transaction validator used in this specification. + * + * @return the transaction validator + */ + public TransactionValidator getTransactionValidator() { + return transactionValidator; + } + + /** + * Returns the transaction processor used in this specification. + * + * @return the transaction processor + */ + public TransactionProcessor getTransactionProcessor() { + return transactionProcessor; + } + + /** + * Returns the block processor used in this specification. + * + * @return the block processor + */ + public BlockProcessor getBlockProcessor() { + return blockProcessor; + } + + /** + * Returns the block importer used in this specification. + * + * @return the block importer + */ + public BlockImporter getBlockImporter() { + return blockImporter; + } + + /** + * Returns the block header validator used in this specification. + * + * @return the block header validator + */ + public BlockHeaderValidator getBlockHeaderValidator() { + return blockHeaderValidator; + } + + /** + * Returns the block body validator used in this specification. + * + * @return the block body validator + */ + public BlockBodyValidator getBlockBodyValidator() { + return blockBodyValidator; + } + + /** + * Returns the block hash function used in this specification. + * + * @return the block hash function + */ + public BlockHashFunction getBlockHashFunction() { + return blockHashFunction; + } + + /** + * Returns the EVM for this specification. + * + * @return the EVM + */ + public EVM getEvm() { + return evm; + } + + /** + * Returns the TransctionReceiptFactory used in this specification + * + * @return the transaction receipt factory + */ + public TransactionReceiptFactory getTransactionReceiptFactory() { + return transactionReceiptFactory; + } + + /** + * Returns the DifficultyCalculator used in this specification. + * + * @return the difficulty calculator. + */ + public DifficultyCalculator getDifficultyCalculator() { + return difficultyCalculator; + } + + /** + * Returns the blockReward used in this specification. + * + * @return the amount to be rewarded for block mining. + */ + public Wei getBlockReward() { + return blockReward; + } + + public MiningBeneficiaryCalculator getMiningBeneficiaryCalculator() { + return miningBeneficiaryCalculator; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpecBuilder.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpecBuilder.java new file mode 100755 index 00000000000..af8f754884e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ProtocolSpecBuilder.java @@ -0,0 +1,254 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockProcessor.TransactionReceiptFactory; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.GasCalculator; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ProtocolSpecBuilder { + private Supplier gasCalculatorBuilder; + private Wei blockReward; + private BlockHashFunction blockHashFunction; + private TransactionReceiptFactory transactionReceiptFactory; + private DifficultyCalculator difficultyCalculator; + private Function evmBuilder; + private Function transactionValidatorBuilder; + private Function, BlockHeaderValidator> blockHeaderValidatorBuilder; + private Function, BlockBodyValidator> blockBodyValidatorBuilder; + private BiFunction contractCreationProcessorBuilder; + private Function precompileContractRegistryBuilder; + private BiFunction + messageCallProcessorBuilder; + private TransactionProcessorBuilder transactionProcessorBuilder; + private BlockProcessorBuilder blockProcessorBuilder; + private BlockImporterBuilder blockImporterBuilder; + private TransactionReceiptType transactionReceiptType; + private String name; + private MiningBeneficiaryCalculator miningBeneficiaryCalculator; + + public ProtocolSpecBuilder gasCalculator(final Supplier gasCalculatorBuilder) { + this.gasCalculatorBuilder = gasCalculatorBuilder; + return this; + } + + public ProtocolSpecBuilder blockReward(final Wei blockReward) { + this.blockReward = blockReward; + return this; + } + + public ProtocolSpecBuilder blockHashFunction(final BlockHashFunction blockHashFunction) { + this.blockHashFunction = blockHashFunction; + return this; + } + + public ProtocolSpecBuilder transactionReceiptFactory( + final TransactionReceiptFactory transactionReceiptFactory) { + this.transactionReceiptFactory = transactionReceiptFactory; + return this; + } + + public ProtocolSpecBuilder difficultyCalculator( + final DifficultyCalculator difficultyCalculator) { + this.difficultyCalculator = difficultyCalculator; + return this; + } + + public ProtocolSpecBuilder evmBuilder(final Function evmBuilder) { + this.evmBuilder = evmBuilder; + return this; + } + + public ProtocolSpecBuilder transactionValidatorBuilder( + final Function transactionValidatorBuilder) { + this.transactionValidatorBuilder = transactionValidatorBuilder; + return this; + } + + public ProtocolSpecBuilder blockHeaderValidatorBuilder( + final Function, BlockHeaderValidator> + blockHeaderValidatorBuilder) { + this.blockHeaderValidatorBuilder = blockHeaderValidatorBuilder; + return this; + } + + public ProtocolSpecBuilder blockBodyValidatorBuilder( + final Function, BlockBodyValidator> blockBodyValidatorBuilder) { + this.blockBodyValidatorBuilder = blockBodyValidatorBuilder; + return this; + } + + public ProtocolSpecBuilder contractCreationProcessorBuilder( + final BiFunction + contractCreationProcessorBuilder) { + this.contractCreationProcessorBuilder = contractCreationProcessorBuilder; + return this; + } + + public ProtocolSpecBuilder precompileContractRegistryBuilder( + final Function precompileContractRegistryBuilder) { + this.precompileContractRegistryBuilder = precompileContractRegistryBuilder; + return this; + } + + public ProtocolSpecBuilder messageCallProcessorBuilder( + final BiFunction + messageCallProcessorBuilder) { + this.messageCallProcessorBuilder = messageCallProcessorBuilder; + return this; + } + + public ProtocolSpecBuilder transactionProcessorBuilder( + final TransactionProcessorBuilder transactionProcessorBuilder) { + this.transactionProcessorBuilder = transactionProcessorBuilder; + return this; + } + + public ProtocolSpecBuilder blockProcessorBuilder( + final BlockProcessorBuilder blockProcessorBuilder) { + this.blockProcessorBuilder = blockProcessorBuilder; + return this; + } + + public ProtocolSpecBuilder blockImporterBuilder( + final BlockImporterBuilder blockImporterBuilder) { + this.blockImporterBuilder = blockImporterBuilder; + return this; + } + + public ProtocolSpecBuilder transactionReceiptType( + final TransactionReceiptType transactionReceiptType) { + this.transactionReceiptType = transactionReceiptType; + return this; + } + + public ProtocolSpecBuilder miningBeneficiaryCalculator( + final MiningBeneficiaryCalculator miningBeneficiaryCalculator) { + this.miningBeneficiaryCalculator = miningBeneficiaryCalculator; + return this; + } + + public ProtocolSpecBuilder name(final String name) { + this.name = name; + return this; + } + + public ProtocolSpecBuilder changeConsensusContextType( + final Function, BlockHeaderValidator> blockHeaderValidatorBuilder, + final Function, BlockBodyValidator> blockBodyValidatorBuilder, + final BlockImporterBuilder blockImporterBuilder, + final DifficultyCalculator difficultyCalculator) { + return new ProtocolSpecBuilder() + .gasCalculator(gasCalculatorBuilder) + .evmBuilder(evmBuilder) + .transactionValidatorBuilder(transactionValidatorBuilder) + .contractCreationProcessorBuilder(contractCreationProcessorBuilder) + .precompileContractRegistryBuilder(precompileContractRegistryBuilder) + .messageCallProcessorBuilder(messageCallProcessorBuilder) + .transactionProcessorBuilder(transactionProcessorBuilder) + .blockHeaderValidatorBuilder(blockHeaderValidatorBuilder) + .blockBodyValidatorBuilder(blockBodyValidatorBuilder) + .blockProcessorBuilder(blockProcessorBuilder) + .blockImporterBuilder(blockImporterBuilder) + .blockHashFunction(blockHashFunction) + .blockReward(blockReward) + .difficultyCalculator(difficultyCalculator) + .transactionReceiptFactory(transactionReceiptFactory) + .transactionReceiptType(transactionReceiptType) + .miningBeneficiaryCalculator(miningBeneficiaryCalculator) + .name(name); + } + + public ProtocolSpec build(final ProtocolSchedule protocolSchedule) { + checkNotNull(gasCalculatorBuilder, "Missing gasCalculator"); + checkNotNull(evmBuilder, "Missing operation registry"); + checkNotNull(transactionValidatorBuilder, "Missing transaction validator"); + checkNotNull(contractCreationProcessorBuilder, "Missing contract creation processor"); + checkNotNull(precompileContractRegistryBuilder, "Missing precompile contract registry"); + checkNotNull(messageCallProcessorBuilder, "Missing message call processor"); + checkNotNull(transactionProcessorBuilder, "Missing transaction processor"); + checkNotNull(blockHeaderValidatorBuilder, "Missing block header validator"); + checkNotNull(blockBodyValidatorBuilder, "Missing block body validator"); + checkNotNull(blockProcessorBuilder, "Missing block processor"); + checkNotNull(blockImporterBuilder, "Missing block importer"); + checkNotNull(blockHashFunction, "Missing block hash function"); + checkNotNull(blockReward, "Missing block reward"); + checkNotNull(difficultyCalculator, "Missing difficulty calculator"); + checkNotNull(transactionReceiptFactory, "Missing transaction receipt factory"); + checkNotNull(transactionReceiptType, "Missing transaction receipt type"); + checkNotNull(name, "Missing name"); + checkNotNull(miningBeneficiaryCalculator, "Missing Mining Beneficiary Calculator"); + checkNotNull(protocolSchedule, "Missing protocol schedule"); + + final GasCalculator gasCalculator = gasCalculatorBuilder.get(); + final EVM evm = evmBuilder.apply(gasCalculator); + final TransactionValidator transactionValidator = + transactionValidatorBuilder.apply(gasCalculator); + final AbstractMessageProcessor contractCreationProcessor = + contractCreationProcessorBuilder.apply(gasCalculator, evm); + final PrecompileContractRegistry precompileContractRegistry = + precompileContractRegistryBuilder.apply(gasCalculator); + final AbstractMessageProcessor messageCallProcessor = + messageCallProcessorBuilder.apply(evm, precompileContractRegistry); + final TransactionProcessor transactionProcessor = + transactionProcessorBuilder.apply( + gasCalculator, transactionValidator, contractCreationProcessor, messageCallProcessor); + final BlockHeaderValidator blockHeaderValidator = + blockHeaderValidatorBuilder.apply(difficultyCalculator); + final BlockBodyValidator blockBodyValidator = + blockBodyValidatorBuilder.apply(protocolSchedule); + final BlockProcessor blockProcessor = + blockProcessorBuilder.apply( + transactionProcessor, + transactionReceiptFactory, + blockReward, + miningBeneficiaryCalculator); + final BlockImporter blockImporter = + blockImporterBuilder.apply(blockHeaderValidator, blockBodyValidator, blockProcessor); + return new ProtocolSpec<>( + name, + evm, + transactionValidator, + transactionProcessor, + blockHeaderValidator, + blockBodyValidator, + blockProcessor, + blockImporter, + blockHashFunction, + transactionReceiptFactory, + difficultyCalculator, + blockReward, + transactionReceiptType, + miningBeneficiaryCalculator); + } + + public interface TransactionProcessorBuilder { + TransactionProcessor apply( + GasCalculator gasCalculator, + TransactionValidator transactionValidator, + AbstractMessageProcessor contractCreationProcessor, + AbstractMessageProcessor messageCallProcessor); + } + + public interface BlockProcessorBuilder { + BlockProcessor apply( + TransactionProcessor transactionProcessor, + TransactionReceiptFactory transactionReceiptFactory, + Wei blockReward, + MiningBeneficiaryCalculator miningBeneficiaryCalculator); + } + + public interface BlockImporterBuilder { + BlockImporter apply( + BlockHeaderValidator blockHeaderValidator, + BlockBodyValidator blockBodyValidator, + BlockProcessor blockProcessor); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduleBasedBlockHashFunction.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduleBasedBlockHashFunction.java new file mode 100755 index 00000000000..b6f07e06bcb --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduleBasedBlockHashFunction.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; + +/** + * Looks up the correct {@link BlockHashFunction} to use based on a {@link ProtocolSchedule} to + * ensure that the correct hash is created given the block number. + */ +public class ScheduleBasedBlockHashFunction implements BlockHashFunction { + + private final ProtocolSchedule protocolSchedule; + + private ScheduleBasedBlockHashFunction(final ProtocolSchedule protocolSchedule) { + this.protocolSchedule = protocolSchedule; + } + + public static BlockHashFunction create(final ProtocolSchedule protocolSchedule) { + return new ScheduleBasedBlockHashFunction<>(protocolSchedule); + } + + @Override + public Hash apply(final BlockHeader header) { + return protocolSchedule + .getByBlockNumber(header.getNumber()) + .getBlockHashFunction() + .apply(header); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduledProtocolSpec.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduledProtocolSpec.java new file mode 100755 index 00000000000..eece91b655b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ScheduledProtocolSpec.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.mainnet; + +/** Tuple that associates a {@link ProtocolSpec} with a given block level starting point */ +public class ScheduledProtocolSpec { + private final long block; + private final ProtocolSpec spec; + + public ScheduledProtocolSpec(final long block, final ProtocolSpec spec) { + this.block = block; + this.spec = spec; + } + + public long getBlock() { + return block; + } + + public ProtocolSpec getSpec() { + return spec; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/SpuriousDragonGasCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/SpuriousDragonGasCalculator.java new file mode 100755 index 00000000000..9b72b7ba77f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/SpuriousDragonGasCalculator.java @@ -0,0 +1,59 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class SpuriousDragonGasCalculator extends TangerineWhistleGasCalculator { + + private static final Gas EXP_OPERATION_BYTE_GAS_COST = Gas.of(50L); + + @Override + public Gas callOperationGasCost( + final MessageFrame frame, + final Gas stipend, + final UInt256 inputDataOffset, + final UInt256 inputDataLength, + final UInt256 outputDataOffset, + final UInt256 outputDataLength, + final Wei transferValue, + final Account recipient) { + final Gas inputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, inputDataOffset, inputDataLength); + final Gas outputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, outputDataOffset, outputDataLength); + final Gas memoryExpansionCost = inputDataMemoryExpansionCost.max(outputDataMemoryExpansionCost); + + Gas cost = callOperationBaseGasCost().plus(memoryExpansionCost); + + if (!transferValue.isZero()) { + cost = cost.plus(callValueTransferGasCost()); + } + + if ((recipient == null || recipient.isEmpty()) && !transferValue.isZero()) { + cost = cost.plus(newAccountGasCost()); + } + + return cost; + } + + @Override + protected Gas expOperationByteGasCost() { + return EXP_OPERATION_BYTE_GAS_COST; + } + + private static final Gas SELFDESTRUCT_OPERATION_GAS_COST = Gas.of(5_000L); + + private static final Gas SELFDESTRUCT_OPERATION_CREATES_NEW_ACCOUNT = Gas.of(30_000L); + + @Override + public Gas selfDestructOperationGasCost(final Account recipient, final Wei inheritance) { + if ((recipient == null || recipient.isEmpty()) && !inheritance.isZero()) { + return SELFDESTRUCT_OPERATION_CREATES_NEW_ACCOUNT; + } else { + return SELFDESTRUCT_OPERATION_GAS_COST; + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TangerineWhistleGasCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TangerineWhistleGasCalculator.java new file mode 100755 index 00000000000..f7d53c87426 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TangerineWhistleGasCalculator.java @@ -0,0 +1,110 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class TangerineWhistleGasCalculator extends HomesteadGasCalculator { + + private static final Gas BALANCE_OPERATION_GAS_COST = Gas.of(400L); + + private static final Gas CALL_OPERATION_BASE_GAS_COST = Gas.of(700L); + + private static final Gas EXT_CODE_BASE_GAS_COST = Gas.of(700L); + + private static final Gas SELFDESTRUCT_OPERATION_GAS_COST = Gas.of(5_000L); + + private static final Gas SELFDESTRUCT_OPERATION_CREATES_NEW_ACCOUNT = Gas.of(30_000L); + + private static final Gas SLOAD_OPERATION_GAS_COST = Gas.of(200L); + + @Override + public Gas getBalanceOperationGasCost() { + return BALANCE_OPERATION_GAS_COST; + } + + // Returns all but 1/64 (n - floor(n /16)) of the provided value + private static Gas allButOneSixtyFourth(final Gas value) { + return value.minus(value.dividedBy(64)); + } + + @Override + protected Gas callOperationBaseGasCost() { + return CALL_OPERATION_BASE_GAS_COST; + } + + @Override + public Gas callOperationGasCost( + final MessageFrame frame, + final Gas stipend, + final UInt256 inputDataOffset, + final UInt256 inputDataLength, + final UInt256 outputDataOffset, + final UInt256 outputDataLength, + final Wei transferValue, + final Account recipient) { + final Gas inputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, inputDataOffset, inputDataLength); + final Gas outputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, outputDataOffset, outputDataLength); + final Gas memoryExpansionCost = inputDataMemoryExpansionCost.max(outputDataMemoryExpansionCost); + + Gas cost = callOperationBaseGasCost().plus(memoryExpansionCost); + + if (!transferValue.isZero()) { + cost = cost.plus(callValueTransferGasCost()); + } + + if (recipient == null) { + cost = cost.plus(newAccountGasCost()); + } + + return cost; + } + + private static Gas gasCap(final Gas remaining, final Gas stipend) { + return allButOneSixtyFourth(remaining).min(stipend); + } + + @Override + public Gas gasAvailableForChildCall( + final MessageFrame frame, final Gas stipend, final boolean transfersValue) { + final Gas gasCap = gasCap(frame.getRemainingGas(), stipend); + + // TODO: Integrate this into AbstractCallOperation since it's + // a little out of place to mutate the frame here. + frame.decrementRemainingGas(gasCap); + + if (transfersValue) { + return gasCap.plus(additionalCallStipend()); + } else { + return gasCap; + } + } + + @Override + public Gas gasAvailableForChildCreate(final Gas stipend) { + return allButOneSixtyFourth(stipend); + } + + @Override + protected Gas extCodeBaseGasCost() { + return EXT_CODE_BASE_GAS_COST; + } + + @Override + public Gas selfDestructOperationGasCost(final Account recipient, final Wei inheritance) { + if (recipient == null) { + return SELFDESTRUCT_OPERATION_CREATES_NEW_ACCOUNT; + } else { + return SELFDESTRUCT_OPERATION_GAS_COST; + } + } + + @Override + public Gas getSloadOperationGasCost() { + return SLOAD_OPERATION_GAS_COST; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionProcessor.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionProcessor.java new file mode 100755 index 00000000000..28e20e84dd4 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionProcessor.java @@ -0,0 +1,125 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static net.consensys.pantheon.ethereum.vm.OperationTracer.NO_TRACING; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.LogSeries; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import net.consensys.pantheon.ethereum.vm.OperationTracer; +import net.consensys.pantheon.util.bytes.BytesValue; + +/** Processes transactions. */ +public interface TransactionProcessor { + + /** A transaction processing result. */ + interface Result { + + /** The status of the transaction after being processed. */ + enum Status { + + /** The transaction was invalid for processing. */ + INVALID, + + /** The transaction was successfully processed. */ + SUCCESSFUL, + + /** The transaction failed to be completely processed. */ + FAILED + } + + /** + * Return the logs produced by the transaction. + * + *

This is only valid when {@code TransactionProcessor#isSuccessful} returns {@code true}. + * + * @return the logs produced by the transaction + */ + LogSeries getLogs(); + + /** + * Returns the status of the transaction after being processed. + * + * @return the status of the transaction after being processed + */ + Status getStatus(); + + /** + * Returns the gas remaining after the transaction was processed. + * + *

This is only valid when {@code TransactionProcessor#isSuccessful} returns {@code true}. + * + * @return the gas remaining after the transaction was processed + */ + long getGasRemaining(); + + BytesValue getOutput(); + + /** + * Returns whether or not the transaction was invalid. + * + * @return {@code true} if the transaction was invalid; otherwise {@code false} + */ + default boolean isInvalid() { + return getStatus() == Status.INVALID; + } + + /** + * Returns whether or not the transaction was successfully processed. + * + * @return {@code true} if the transaction was successfully processed; otherwise {@code false} + */ + default boolean isSuccessful() { + return getStatus() == Status.SUCCESSFUL; + } + + /** + * Returns the transaction validation result. + * + * @return the validation result, with the reason for failure (if applicable.) + */ + ValidationResult getValidationResult(); + } + + /** + * Applies a transaction to the current system state. + * + * @param blockchain The current blockchain + * @param worldState The current world state + * @param blockHeader The current block header + * @param transaction The transaction to process + * @param miningBeneficiary the address which is to receive the transaction fee + * @return the transaction result + */ + default Result processTransaction( + final Blockchain blockchain, + final WorldUpdater worldState, + final ProcessableBlockHeader blockHeader, + final Transaction transaction, + final Address miningBeneficiary) { + return processTransaction( + blockchain, worldState, blockHeader, transaction, miningBeneficiary, NO_TRACING); + } + + /** + * Applies a transaction to the current system state. + * + * @param blockchain The current blockchain + * @param worldState The current world state + * @param blockHeader The current block header + * @param transaction The transaction to process + * @param operationTracer The tracer to record results of each EVM operation + * @param miningBeneficiary the address which is to receive the transaction fee + * @return the transaction result + */ + Result processTransaction( + Blockchain blockchain, + WorldUpdater worldState, + ProcessableBlockHeader blockHeader, + Transaction transaction, + Address miningBeneficiary, + OperationTracer operationTracer); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionReceiptType.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionReceiptType.java new file mode 100755 index 00000000000..f75f35cdc3e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionReceiptType.java @@ -0,0 +1,6 @@ +package net.consensys.pantheon.ethereum.mainnet; + +public enum TransactionReceiptType { + ROOT, + STATUS +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionValidator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionValidator.java new file mode 100755 index 00000000000..faf3fe9e30d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/TransactionValidator.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Transaction; + +import java.util.OptionalLong; + +/** Validates transaction based on some criteria. */ +public interface TransactionValidator { + + /** + * Asserts whether a transaction is valid. + * + * @param transaction the transaction to validate + * @return An empty @{link Optional} if the transaction is considered valid; otherwise an @{code + * Optional} containing a {@link TransactionInvalidReason} that identifies why the transaction + * is invalid. + */ + ValidationResult validate(Transaction transaction); + + /** + * Asserts whether a transaction is valid for the sender accounts current state. + * + *

Note: {@code validate} should be called before getting the sender {@link Account} used in + * this method to ensure that a sender can be extracted from the {@link Transaction}. + * + * @param transaction the transaction to validate + * @param sender the sender account state to validate against + * @param maximumNonce the maximum transaction nonce. If not provided the transaction nonce must + * equal the sender's current account nonce + * @return An empty @{link Optional} if the transaction is considered valid; otherwise an @{code + * Optional} containing a {@link TransactionInvalidReason} that identifies why the transaction + * is invalid. + */ + ValidationResult validateForSender( + Transaction transaction, Account sender, OptionalLong maximumNonce); + + enum TransactionInvalidReason { + WRONG_CHAIN_ID, + REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED, + INVALID_SIGNATURE, + UPFRONT_COST_EXCEEDS_BALANCE, + NONCE_TOO_LOW, + INCORRECT_NONCE, + INTRINSIC_GAS_EXCEEDS_GAS_LIMIT, + EXCEEDS_BLOCK_GAS_LIMIT + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ValidationResult.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ValidationResult.java new file mode 100755 index 00000000000..b5da989801c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/ValidationResult.java @@ -0,0 +1,79 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.base.MoreObjects; + +public final class ValidationResult { + + private final Optional invalidReason; + private final Optional errorMessage; + + private ValidationResult(final Optional invalidReason, final Optional errorMessage) { + this.invalidReason = invalidReason; + this.errorMessage = errorMessage; + } + + public boolean isValid() { + return !invalidReason.isPresent(); + } + + public T getInvalidReason() throws NoSuchElementException { + return invalidReason.get(); + } + + public String getErrorMessage() { + return errorMessage.orElse(getInvalidReason().toString()); + } + + public void ifValid(final Runnable action) { + if (isValid()) { + action.run(); + } + } + + public R either(final Supplier whenValid, final Function whenInvalid) { + return invalidReason.map(whenInvalid).orElseGet(whenValid); + } + + public static ValidationResult valid() { + return new ValidationResult<>(Optional.empty(), Optional.empty()); + } + + public static ValidationResult invalid(final T reason, final String errorMessage) { + return new ValidationResult<>(Optional.of(reason), Optional.of(errorMessage)); + } + + public static ValidationResult invalid(final T reason) { + return new ValidationResult<>(Optional.of(reason), Optional.empty()); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ValidationResult that = (ValidationResult) o; + return Objects.equals(invalidReason, that.invalidReason); + } + + @Override + public int hashCode() { + return Objects.hash(invalidReason); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("invalidReason", invalidReason) + .add("errorMessage", errorMessage) + .toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRule.java new file mode 100755 index 00000000000..443a055a36a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRule.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Ensures the hash of the parent block matches that specified in the parent hash of the proposed + * header. + */ +public class AncestryValidationRule implements DetachedBlockHeaderValidationRule { + private final Logger LOGGER = LogManager.getLogger(AncestryValidationRule.class); + + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + if (!header.getParentHash().equals(parent.getHash())) { + LOGGER.trace( + "Invalid parent block header. Parent hash {} does not match " + + "supplied parent header {}.", + header.getParentHash(), + parent.getHash()); + return false; + } + + if (header.getNumber() != (parent.getNumber() + 1)) { + LOGGER.trace( + "Invalid block header: number {} is not one more than parent number {}", + header.getNumber(), + parent.getNumber()); + return false; + } + + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/CalculatedDifficultyValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/CalculatedDifficultyValidationRule.java new file mode 100755 index 00000000000..e8088195da3 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/CalculatedDifficultyValidationRule.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule; +import net.consensys.pantheon.ethereum.mainnet.DifficultyCalculator; + +import java.math.BigInteger; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class CalculatedDifficultyValidationRule implements AttachedBlockHeaderValidationRule { + private final Logger LOGGER = LogManager.getLogger(CalculatedDifficultyValidationRule.class); + private final DifficultyCalculator difficultyCalculator; + + public CalculatedDifficultyValidationRule(final DifficultyCalculator difficultyCalculator) { + this.difficultyCalculator = difficultyCalculator; + } + + @Override + public boolean validate( + final BlockHeader header, final BlockHeader parent, final ProtocolContext context) { + final BigInteger actualDifficulty = + new BigInteger(1, header.getDifficulty().getBytes().extractArray()); + final BigInteger expectedDifficulty = + difficultyCalculator.nextDifficulty(header.getTimestamp(), parent, context); + + if (actualDifficulty.compareTo(expectedDifficulty) != 0) { + LOGGER.trace( + "Invalid block header: difficulty {} does not equal expected difficulty {}", + actualDifficulty, + expectedDifficulty); + return false; + } + + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRule.java new file mode 100755 index 00000000000..738e6308843 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRule.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; + +import java.util.function.Function; + +import org.apache.logging.log4j.Logger; + +public class ConstantFieldValidationRule implements DetachedBlockHeaderValidationRule { + private static final Logger LOG = getLogger(); + private final T expectedValue; + private final Function accessor; + private final String fieldName; + + public ConstantFieldValidationRule( + final String fieldName, final Function accessor, final T expectedValue) { + this.expectedValue = expectedValue; + this.accessor = accessor; + this.fieldName = fieldName; + } + + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + final T actualValue = accessor.apply(header); + if (!actualValue.equals(expectedValue)) { + LOG.trace( + "{} failed validation. Actual != Expected ({} != {}).", + fieldName, + actualValue, + expectedValue); + return false; + } + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRule.java new file mode 100755 index 00000000000..6d4c84b4106 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRule.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Responsible for ensuring the extra data fields in the header contain the appropriate number of + * bytes. + */ +public class ExtraDataMaxLengthValidationRule implements DetachedBlockHeaderValidationRule { + + private final Logger LOGGER = LogManager.getLogger(ExtraDataMaxLengthValidationRule.class); + private final long maxExtraDataBytes; + + public ExtraDataMaxLengthValidationRule(final long maxExtraDataBytes) { + this.maxExtraDataBytes = maxExtraDataBytes; + } + + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + return validateExtraData(header.getExtraData()); + } + + private boolean validateExtraData(final BytesValue extraData) { + if (extraData.size() > maxExtraDataBytes) { + LOGGER.trace( + "Invalid block header: extra data field length {} is greater {}", + extraData.size(), + maxExtraDataBytes); + return false; + } + + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRule.java new file mode 100755 index 00000000000..2f37aab98d2 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRule.java @@ -0,0 +1,56 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Responsible for ensuring the gasLimit specified in the supplied block header is within bounds as + * specified at construction. And that the gasLimit for this block is within certain bounds of its + * parent block. + */ +public class GasLimitRangeAndDeltaValidationRule implements DetachedBlockHeaderValidationRule { + + private static final Logger LOGGER = + LogManager.getLogger(GasLimitRangeAndDeltaValidationRule.class); + private static final int GASLIMIT_BOUND_DIVISOR = 1024; + private final long minGasLimit; + private final long maxGasLimit; + + public GasLimitRangeAndDeltaValidationRule(final long minGasLimit, final long maxGasLimit) { + checkArgument( + minGasLimit >= GASLIMIT_BOUND_DIVISOR, + "minGasLimit of " + + minGasLimit + + " is below the bound divisor of " + + GASLIMIT_BOUND_DIVISOR); + this.minGasLimit = minGasLimit; + this.maxGasLimit = maxGasLimit; + } + + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + final long gasLimit = header.getGasLimit(); + + if ((gasLimit < minGasLimit) || (gasLimit > maxGasLimit)) { + LOGGER.trace( + "Header gasLimit = {}, outside range {} --> {}", gasLimit, minGasLimit, maxGasLimit); + return false; + } + + final long parentGasLimit = parent.getGasLimit(); + final long difference = Math.abs(parentGasLimit - gasLimit); + final long bounds = Long.divideUnsigned(parentGasLimit, GASLIMIT_BOUND_DIVISOR); + if (Long.compareUnsigned(difference, bounds) >= 0) { + LOGGER.trace( + "Invalid block header: gas limit delta {} is out of bounds of {}", gasLimit, bounds); + return false; + } + + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRule.java new file mode 100755 index 00000000000..30de661b20c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRule.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Validates the gas used in executing the block (defined by the supplied header) is less than or + * equal to the current gas limit. + */ +public class GasUsageValidationRule implements DetachedBlockHeaderValidationRule { + + private final Logger LOGGER = LogManager.getLogger(GasUsageValidationRule.class); + + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + if (header.getGasUsed() > header.getGasLimit()) { + LOGGER.trace( + "Invalid block header: gas used {} exceeds gas limit {}", + header.getGasUsed(), + header.getGasLimit()); + return false; + } + + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ProofOfWorkValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ProofOfWorkValidationRule.java new file mode 100755 index 00000000000..ea8756d5714 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ProofOfWorkValidationRule.java @@ -0,0 +1,111 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import net.consensys.pantheon.crypto.BouncyCastleMessageDigestFactory; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; +import net.consensys.pantheon.ethereum.mainnet.EthHasher; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class ProofOfWorkValidationRule implements DetachedBlockHeaderValidationRule { + + private static final Logger LOGGER = LogManager.getLogger(ProofOfWorkValidationRule.class); + + private static final int SERIALIZED_HASH_SIZE = 33; + + private static final int SERIALIZED_NONCE_SIZE = 9; + + private static final BigInteger ETHHASH_TARGET_UPPER_BOUND = BigInteger.valueOf(2).pow(256); + + private static final EthHasher HASHER = new EthHasher.Light(); + + private static final ThreadLocal KECCAK_256 = + ThreadLocal.withInitial( + () -> { + try { + return BouncyCastleMessageDigestFactory.create( + net.consensys.pantheon.crypto.Hash.KECCAK256_ALG); + } catch (final NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + }); + + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + final MessageDigest keccak256 = KECCAK_256.get(); + + final byte[] bytes = RLP.encode(header::writeTo).extractArray(); + final int listOffset = RlpUtils.decodeOffset(bytes, 0); + final int length = RlpUtils.decodeLength(bytes, 0); + + final byte[] listHeadBuff = new byte[10]; + final int newLength = length - SERIALIZED_HASH_SIZE - SERIALIZED_NONCE_SIZE; + final int sizeLen = writeListPrefix(newLength - listOffset, listHeadBuff); + + keccak256.update(listHeadBuff, 0, sizeLen); + keccak256.update(bytes, listOffset, newLength - sizeLen); + final byte[] hashBuffer = new byte[64]; + HASHER.hash(hashBuffer, header.getNonce(), header.getNumber(), keccak256.digest()); + + if (header.getDifficulty().isZero()) { + LOGGER.trace("Rejecting header because difficulty is 0"); + return false; + } + final BigInteger difficulty = + BytesValues.asUnsignedBigInteger(header.getDifficulty().getBytes()); + final UInt256 target = UInt256.of(ETHHASH_TARGET_UPPER_BOUND.divide(difficulty)); + + final UInt256 result = UInt256.wrap(Bytes32.wrap(hashBuffer, 32)); + if (result.compareTo(target) > 0) { + LOGGER.warn( + "Invalid block header: the EthHash result {} was greater than the target {}.\n" + + "Failing header:\n{}", + result, + target, + header); + return false; + } + + final Hash mixedHash = + Hash.wrap(Bytes32.leftPad(BytesValue.wrap(hashBuffer).slice(0, Bytes32.SIZE))); + if (!header.getMixHash().equals(mixedHash)) { + LOGGER.warn( + "Invalid block header: header mixed hash {} does not equal calculated mixed hash {}.\n" + + "Failing header:\n{}", + header.getMixHash(), + mixedHash, + header); + return false; + } + + return true; + } + + @Override + public boolean includeInLightValidation() { + return false; + } + + private static int writeListPrefix(final int size, final byte[] target) { + final int sizeLength = 4 - Integer.numberOfLeadingZeros(size) / 8; + target[0] = (byte) (0xf7 + sizeLength); + int shift = 0; + for (int i = 0; i < sizeLength; i++) { + target[sizeLength - i] = (byte) (size >> shift); + shift += 8; + } + return 1 + sizeLength; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRule.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRule.java new file mode 100755 index 00000000000..55fdfb725cb --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRule.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; + +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Responsible for ensuring the timestamp of a block is newer than its parent, but also that it has + * a timestamp not more than "acceptableClockDriftSeconds' into the future. + */ +public class TimestampValidationRule implements DetachedBlockHeaderValidationRule { + + private final Logger LOGGER = LogManager.getLogger(TimestampValidationRule.class); + private final long acceptableClockDriftSeconds; + private final long minimumSecondsSinceParent; + + public TimestampValidationRule( + final long acceptableClockDriftSeconds, final long minimumSecondsSinceParent) { + checkArgument(minimumSecondsSinceParent >= 0, "minimumSecondsSinceParent must be positive"); + this.acceptableClockDriftSeconds = acceptableClockDriftSeconds; + this.minimumSecondsSinceParent = minimumSecondsSinceParent; + } + + @Override + public boolean validate(final BlockHeader header, final BlockHeader parent) { + return validateTimestamp(header.getTimestamp(), parent.getTimestamp()); + } + + private boolean validateTimestamp(final long timestamp, final long parentTimestamp) { + boolean result = validateHeaderSufficientlyAheadOfParent(timestamp, parentTimestamp); + result &= validateHeaderNotAheadOfCurrentSystemTime(timestamp); + + return result; + } + + private boolean validateHeaderSufficientlyAheadOfParent( + final long timestamp, final long parentTimestamp) { + if ((timestamp - minimumSecondsSinceParent) < parentTimestamp) { + LOGGER.trace( + "Invalid block header: timestamp {} is not sufficiently newer than parent timestamp {}", + timestamp, + parentTimestamp); + return false; + } + + return true; + } + + private boolean validateHeaderNotAheadOfCurrentSystemTime(final long timestamp) { + final long timestampMargin = + TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + + acceptableClockDriftSeconds; + if (Long.compareUnsigned(timestamp, timestampMargin) > 0) { + LOGGER.trace( + "Invalid block header: timestamp {} is greater than the timestamp margin {}", + timestamp, + timestampMargin); + return false; + } + return true; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128AddPrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128AddPrecompiledContract.java new file mode 100755 index 00000000000..2134c3ba037 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128AddPrecompiledContract.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.crypto.altbn128.AltBn128Point; +import net.consensys.pantheon.crypto.altbn128.Fq; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.math.BigInteger; +import java.util.Arrays; + +public class AltBN128AddPrecompiledContract extends AbstractPrecompiledContract { + + public AltBN128AddPrecompiledContract(final GasCalculator gasCalculator) { + super("AltBN128Add", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + return Gas.of(500); + } + + @Override + public BytesValue compute(final BytesValue input) { + final BigInteger x1 = extractParameter(input, 0, 32); + final BigInteger y1 = extractParameter(input, 32, 32); + final BigInteger x2 = extractParameter(input, 64, 32); + final BigInteger y2 = extractParameter(input, 96, 32); + + final AltBn128Point p1 = new AltBn128Point(Fq.create(x1), Fq.create(y1)); + final AltBn128Point p2 = new AltBn128Point(Fq.create(x2), Fq.create(y2)); + if (!p1.isOnCurve() || !p2.isOnCurve()) { + return null; + } + final AltBn128Point sum = p1.add(p2); + final BytesValue x = sum.getX().toBytesValue(); + final BytesValue y = sum.getY().toBytesValue(); + final MutableBytesValue result = MutableBytesValue.create(64); + x.copyTo(result, 32 - x.size()); + y.copyTo(result, 64 - y.size()); + + return result; + } + + private static BigInteger extractParameter( + final BytesValue input, final int offset, final int length) { + if (offset > input.size() || length == 0) { + return BigInteger.ZERO; + } + final byte[] raw = Arrays.copyOfRange(input.extractArray(), offset, offset + length); + return new BigInteger(1, raw); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128MulPrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128MulPrecompiledContract.java new file mode 100755 index 00000000000..375215f630d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128MulPrecompiledContract.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.crypto.altbn128.AltBn128Point; +import net.consensys.pantheon.crypto.altbn128.Fq; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.math.BigInteger; +import java.util.Arrays; + +public class AltBN128MulPrecompiledContract extends AbstractPrecompiledContract { + + private static final BigInteger MAX_N = + new BigInteger( + "115792089237316195423570985008687907853269984665640564039457584007913129639935"); + + public AltBN128MulPrecompiledContract(final GasCalculator gasCalculator) { + super("AltBn128Mul", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + return Gas.of(40_000L); + } + + @Override + public BytesValue compute(final BytesValue input) { + final BigInteger x = extractParameter(input, 0, 32); + final BigInteger y = extractParameter(input, 32, 32); + final BigInteger n = extractParameter(input, 64, 32); + + final AltBn128Point p = new AltBn128Point(Fq.create(x), Fq.create(y)); + if (!p.isOnCurve() || n.compareTo(MAX_N) > 0) { + return null; + } + final AltBn128Point product = p.multiply(n); + + final BytesValue xResult = product.getX().toBytesValue(); + final BytesValue yResult = product.getY().toBytesValue(); + final MutableBytesValue result = MutableBytesValue.create(64); + xResult.copyTo(result, 32 - xResult.size()); + yResult.copyTo(result, 64 - yResult.size()); + + return result; + } + + private static BigInteger extractParameter( + final BytesValue input, final int offset, final int length) { + if (offset > input.size() || length == 0) { + return BigInteger.ZERO; + } + final byte[] raw = Arrays.copyOfRange(input.extractArray(), offset, offset + length); + return new BigInteger(1, raw); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128PairingPrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128PairingPrecompiledContract.java new file mode 100755 index 00000000000..0f968ccec63 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/AltBN128PairingPrecompiledContract.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.crypto.altbn128.AltBn128Fq12Pairer; +import net.consensys.pantheon.crypto.altbn128.AltBn128Fq2Point; +import net.consensys.pantheon.crypto.altbn128.AltBn128Point; +import net.consensys.pantheon.crypto.altbn128.Fq; +import net.consensys.pantheon.crypto.altbn128.Fq12; +import net.consensys.pantheon.crypto.altbn128.Fq2; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AltBN128PairingPrecompiledContract extends AbstractPrecompiledContract { + + private static final int FIELD_LENGTH = 32; + private static final int PARAMETER_LENGTH = 192; + + private static final BytesValue FALSE = + BytesValue.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000000"); + private static final BytesValue TRUE = + BytesValue.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000001"); + + public AltBN128PairingPrecompiledContract(final GasCalculator gasCalculator) { + super("AltBN128Pairing", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + final int parameters = input.size() / PARAMETER_LENGTH; + return Gas.of(80_000L).times(Gas.of(parameters)).plus(Gas.of(100_000L)); + } + + @Override + public BytesValue compute(final BytesValue input) { + if (input.isEmpty()) { + return TRUE; + } + if (input.size() % PARAMETER_LENGTH != 0) { + return null; + } + + final int parameters = input.size() / PARAMETER_LENGTH; + final List a = new ArrayList<>(); + final List b = new ArrayList<>(); + for (int i = 0; i < parameters; ++i) { + final BigInteger p1_x = extractParameter(input, i * PARAMETER_LENGTH, FIELD_LENGTH); + final BigInteger p1_y = extractParameter(input, i * PARAMETER_LENGTH + 32, FIELD_LENGTH); + final AltBn128Point p1 = new AltBn128Point(Fq.create(p1_x), Fq.create(p1_y)); + if (!p1.isOnCurve()) { + return null; + } + a.add(p1); + + final BigInteger p2_xImag = extractParameter(input, i * PARAMETER_LENGTH + 64, FIELD_LENGTH); + final BigInteger p2_xReal = extractParameter(input, i * PARAMETER_LENGTH + 96, FIELD_LENGTH); + final BigInteger p2_yImag = extractParameter(input, i * PARAMETER_LENGTH + 128, FIELD_LENGTH); + final BigInteger p2_yReal = extractParameter(input, i * PARAMETER_LENGTH + 160, FIELD_LENGTH); + final Fq2 p2_x = Fq2.create(p2_xReal, p2_xImag); + final Fq2 p2_y = Fq2.create(p2_yReal, p2_yImag); + final AltBn128Fq2Point p2 = new AltBn128Fq2Point(p2_x, p2_y); + if (!p2.isOnCurve()) { + return null; + } + b.add(p2); + } + + Fq12 exponent = Fq12.one(); + for (int i = 0; i < parameters; ++i) { + exponent = exponent.multiply(AltBn128Fq12Pairer.pair(a.get(i), b.get(i))); + } + + if (AltBn128Fq12Pairer.finalize(exponent).equals(Fq12.one())) { + return TRUE; + } else { + return FALSE; + } + } + + private static BigInteger extractParameter( + final BytesValue input, final int offset, final int length) { + if (offset > input.size() || length == 0) { + return BigInteger.ZERO; + } + final byte[] raw = Arrays.copyOfRange(input.extractArray(), offset, offset + length); + return new BigInteger(1, raw); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/BigIntegerModularExponentiationPrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/BigIntegerModularExponentiationPrecompiledContract.java new file mode 100755 index 00000000000..77f887e8641 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/BigIntegerModularExponentiationPrecompiledContract.java @@ -0,0 +1,155 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.math.BigInteger; +import java.util.Arrays; + +// The big integer modular exponentiation precompiled contract defined in EIP-198. +public class BigIntegerModularExponentiationPrecompiledContract + extends AbstractPrecompiledContract { + + private static final BigInteger WORD_SIZE = BigInteger.valueOf(32); + private static final BigInteger BITS_IN_BYTE = BigInteger.valueOf(8); + private static final BigInteger BASE_OFFSET = BigInteger.valueOf(96); + private static final BigInteger MAX_FIRST_EXPONENT_BYTES = BigInteger.valueOf(32); + private static final BigInteger GQUADDIVISOR = BigInteger.valueOf(20); + private static final int PARAMETER_LENGTH = 32; + private static final int BASE_LENGTH_OFFSET = 0; + private static final int EXPONENT_LENGTH_OFFSET = 32; + private static final int MODULUS_LENGTH_OFFSET = 64; + private static final int MAX_GAS_BITS = 255; + + private static final BigInteger BIGINT_4 = BigInteger.valueOf(4); + private static final BigInteger BIGINT_16 = BigInteger.valueOf(16); + private static final BigInteger BIGINT_64 = BigInteger.valueOf(64); + private static final BigInteger BIGINT_96 = BigInteger.valueOf(96); + private static final BigInteger BIGINT_480 = BigInteger.valueOf(480); + private static final BigInteger BIGINT_1024 = BigInteger.valueOf(1_024L); + private static final BigInteger BIGINT_3072 = BigInteger.valueOf(3_072L); + private static final BigInteger BIGINT_199680 = BigInteger.valueOf(199_680L); + + public BigIntegerModularExponentiationPrecompiledContract(final GasCalculator gasCalculator) { + super("BigIntModExp", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + // Typically gas calculations are delegated to a GasCalculator instance, + // but the complexity and coupling wih other parts of the precompile seem + // like reasonable reasons to do the math here instead. + final BigInteger baseLength = baseLength(input); + final BigInteger exponentLength = exponentLength(input); + final BigInteger modulusLength = modulusLength(input); + final BigInteger exponentOffset = BASE_OFFSET.add(baseLength); + final int firstExponentBytesCap = exponentLength.min(MAX_FIRST_EXPONENT_BYTES).intValue(); + final BigInteger firstExpBytes = extractParameter(input, exponentOffset, firstExponentBytesCap); + final BigInteger adjustedExponentLength = adjustedExponentLength(exponentLength, firstExpBytes); + final BigInteger multiplicationComplexity = + multiplicationComplexity(baseLength.max(modulusLength)); + final BigInteger gasRequirement = + multiplicationComplexity + .multiply(adjustedExponentLength.max(BigInteger.ONE)) + .divide(GQUADDIVISOR); + + // Gas price is so large it will not fit in a Gas type, so an + // very very very unlikely high gas price is used instead. + if (gasRequirement.bitLength() > MAX_GAS_BITS) { + return Gas.of(Long.MAX_VALUE); + } else { + return Gas.of(gasRequirement); + } + } + + @Override + public BytesValue compute(final BytesValue input) { + final BigInteger baseLength = baseLength(input); + final BigInteger exponentLength = exponentLength(input); + final BigInteger modulusLength = modulusLength(input); + final BigInteger exponentOffset = BASE_OFFSET.add(baseLength); + final BigInteger modulusOffset = exponentOffset.add(exponentLength); + final BigInteger base = extractParameter(input, BASE_OFFSET, baseLength.intValue()); + final BigInteger exp = extractParameter(input, exponentOffset, exponentLength.intValue()); + final BigInteger mod = extractParameter(input, modulusOffset, modulusLength.intValue()); + + final BytesValue modExp; + // Result must be the length of the modulus. + final MutableBytesValue result = MutableBytesValue.create(modulusLength.intValue()); + if (mod.compareTo(BigInteger.ZERO) == 0) { + modExp = MutableBytesValue.EMPTY; + } else { + // BigInteger zero-pads positive values whose most significant bit is a 1 if + // the padding was not there. + modExp = + BytesValues.trimLeadingZeros(MutableBytesValue.wrap(base.modPow(exp, mod).toByteArray())); + } + + modExp.copyTo(result, result.size() - modExp.size()); + return result; + } + + // Equation to estimate the multiplication complexity. + private static BigInteger multiplicationComplexity(final BigInteger x) { + if (x.compareTo(BIGINT_64) <= 0) { + return square(x); + } else if (x.compareTo(BIGINT_1024) <= 0) { + return square(x).divide(BIGINT_4).add(BIGINT_96.multiply(x)).subtract(BIGINT_3072); + } else { + return square(x).divide(BIGINT_16).add(BIGINT_480.multiply(x)).subtract(BIGINT_199680); + } + } + + private static BigInteger bitLength(final BigInteger n) { + return n.compareTo(BigInteger.ZERO) == 0 + ? BigInteger.ZERO + : BigInteger.valueOf(n.bitLength() - 1); + } + + private static BigInteger adjustedExponentLength( + final BigInteger exponentLength, final BigInteger firstExpBytes) { + final BigInteger bitLength = bitLength(firstExpBytes); + if (exponentLength.compareTo(WORD_SIZE) <= 0) { + return bitLength; + } else { + return BITS_IN_BYTE.multiply(exponentLength.subtract(WORD_SIZE)).add(bitLength); + } + } + + private static final BigInteger baseLength(final BytesValue input) { + return extractParameter(input, BASE_LENGTH_OFFSET, PARAMETER_LENGTH); + } + + private static final BigInteger exponentLength(final BytesValue input) { + return extractParameter(input, EXPONENT_LENGTH_OFFSET, PARAMETER_LENGTH); + } + + private static final BigInteger modulusLength(final BytesValue input) { + return extractParameter(input, MODULUS_LENGTH_OFFSET, PARAMETER_LENGTH); + } + + private static BigInteger extractParameter( + final BytesValue input, final int offset, final int length) { + if (offset > input.size() || length == 0) { + return BigInteger.ZERO; + } + final byte[] raw = Arrays.copyOfRange(input.extractArray(), offset, offset + length); + return new BigInteger(1, raw); + } + + private static BigInteger extractParameter( + final BytesValue input, final BigInteger offset, final int length) { + if (BigInteger.valueOf(input.size()).compareTo(offset) <= 0) { + return BigInteger.ZERO; + } + return extractParameter(input, offset.intValue(), length); + } + + private static BigInteger square(final BigInteger n) { + return n.multiply(n); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/ECRECPrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/ECRECPrecompiledContract.java new file mode 100755 index 00000000000..8209a911f80 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/ECRECPrecompiledContract.java @@ -0,0 +1,74 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.crypto.SECP256K1.PublicKey; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.bytes.MutableBytes32; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.math.BigInteger; +import java.util.Optional; + +public class ECRECPrecompiledContract extends AbstractPrecompiledContract { + + private static final int V_BASE = 27; + + public ECRECPrecompiledContract(final GasCalculator gasCalculator) { + super("ECREC_PRECOMPILED_GAS_COST", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + return gasCalculator().getEcrecPrecompiledContractGasCost(); + } + + @Override + public BytesValue compute(final BytesValue input) { + final int size = input.size(); + final BytesValue d = + size >= 128 ? input : BytesValue.wrap(input, MutableBytesValue.create(128 - size)); + final Bytes32 h = Bytes32.wrap(d, 0); + // Note that the Yellow Paper defines v as the next 32 bytes (so 32..63). Yet, v is a simple + // byte in ECDSARECOVER and the Yellow Paper is not very clear on this mismatch but it appears + // it is simply the last byte of those 32 bytes that needs to be used. It does appear we need + // to check the rest of the bytes are zero though. + if (!d.slice(32, 31).isZero()) { + return BytesValue.EMPTY; + } + + final int recId = d.get(63) - V_BASE; + final BigInteger r = BytesValues.asUnsignedBigInteger(d.slice(64, 32)); + final BigInteger s = BytesValues.asUnsignedBigInteger(d.slice(96, 32)); + + Signature signature; + try { + signature = Signature.create(r, s, (byte) recId); + } catch (final IllegalArgumentException e) { + return BytesValue.EMPTY; + } + + // SECP256K1#PublicKey#recoverFromSignature throws an Illegal argument exception + // when it is unable to recover the key. There is not a straightforward way to + // check the arguments ahead of time to determine if the fail will happen and + // the library needs to be updated. + try { + final Optional recovered = PublicKey.recoverFromSignature(h, signature); + if (!recovered.isPresent()) { + return BytesValue.EMPTY; + } + + final Bytes32 hashed = Hash.hash(recovered.get().getEncodedBytes()); + final MutableBytes32 result = MutableBytes32.create(); + hashed.slice(12).copyTo(result, 12); + return result; + } catch (final IllegalArgumentException e) { + return BytesValue.EMPTY; + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/IDPrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/IDPrecompiledContract.java new file mode 100755 index 00000000000..e9262661399 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/IDPrecompiledContract.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class IDPrecompiledContract extends AbstractPrecompiledContract { + + public IDPrecompiledContract(final GasCalculator gasCalculator) { + super("ID", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + return gasCalculator().idPrecompiledContractGasCost(input); + } + + @Override + public BytesValue compute(final BytesValue input) { + return input; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/RIPEMD160PrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/RIPEMD160PrecompiledContract.java new file mode 100755 index 00000000000..90a63cb24a7 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/RIPEMD160PrecompiledContract.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class RIPEMD160PrecompiledContract extends AbstractPrecompiledContract { + + public RIPEMD160PrecompiledContract(final GasCalculator gasCalculator) { + super("RIPEMD160", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + return gasCalculator().ripemd160PrecompiledContractGasCost(input); + } + + @Override + public BytesValue compute(final BytesValue input) { + return Bytes32.leftPad(Hash.ripemd160(input)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/SHA256PrecompiledContract.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/SHA256PrecompiledContract.java new file mode 100755 index 00000000000..6e71c0f8d2d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/mainnet/precompiles/SHA256PrecompiledContract.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.mainnet.precompiles; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.AbstractPrecompiledContract; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class SHA256PrecompiledContract extends AbstractPrecompiledContract { + + public SHA256PrecompiledContract(final GasCalculator gasCalculator) { + super("SHA256", gasCalculator); + } + + @Override + public Gas gasRequirement(final BytesValue input) { + return gasCalculator().sha256PrecompiledContractGasCost(input); + } + + @Override + public BytesValue compute(final BytesValue input) { + return Hash.sha256(input); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/BlockchainUtil.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/BlockchainUtil.java new file mode 100755 index 00000000000..ca40dc26197 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/BlockchainUtil.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.util; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockHeader; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.OptionalInt; + +public class BlockchainUtil { + + private BlockchainUtil() {} + + public static OptionalInt findHighestKnownBlockIndex( + final Blockchain blockchain, + final List headers, + final boolean ascendingHeaderOrder) { + int offset = ascendingHeaderOrder ? -1 : 0; + Comparator comparator = knownBlockComparator(blockchain, ascendingHeaderOrder); + + int insertionIndex = -Collections.binarySearch(headers, null, comparator) - 1; + int ancestorIndex = insertionIndex + offset; + if (ancestorIndex < 0 || ancestorIndex >= headers.size()) { + return OptionalInt.empty(); + } + return OptionalInt.of(ancestorIndex); + } + + private static Comparator knownBlockComparator( + final Blockchain blockchain, final boolean ascendingHeaderOrder) { + Comparator comparator = + (final BlockHeader element0, final BlockHeader element1) -> { + if (element0 == null) { + return blockchain.contains(element1.getHash()) ? -1 : 1; + } + return blockchain.contains(element0.getHash()) ? 1 : -1; + }; + return ascendingHeaderOrder ? comparator.reversed() : comparator; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/ByteArrayUtil.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/ByteArrayUtil.java new file mode 100755 index 00000000000..720d3c66b77 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/ByteArrayUtil.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.ethereum.util; + +import com.google.common.primitives.Longs; + +public final class ByteArrayUtil { + + private ByteArrayUtil() { + // Utility Class + } + + public static int compare( + final byte[] buffer1, + final int offset1, + final int length1, + final byte[] buffer2, + final int offset2, + final int length2) { + if (buffer1 == buffer2 && offset1 == offset2 && length1 == length2) { + return 0; + } + final int end1 = offset1 + length1; + final int end2 = offset2 + length2; + for (int i = offset1, j = offset2; i < end1 && j < end2; i++, j++) { + final int a = buffer1[i] & 0xff; + final int b = buffer2[j] & 0xff; + if (a != b) { + return a - b; + } + } + return length1 - length2; + } + + public static long readLong(final int index, final byte[] buffer) { + return Longs.fromBytes( + buffer[index], + buffer[index + 1], + buffer[index + 2], + buffer[index + 3], + buffer[index + 4], + buffer[index + 5], + buffer[index + 6], + buffer[index + 7]); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/RawBlockIterator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/RawBlockIterator.java new file mode 100755 index 00000000000..4453f1d5b2b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/util/RawBlockIterator.java @@ -0,0 +1,93 @@ +package net.consensys.pantheon.ethereum.util; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.function.Function; + +public final class RawBlockIterator implements Iterator, Closeable { + + private final FileChannel fileChannel; + private final Function headerReader; + + private ByteBuffer readBuffer = ByteBuffer.allocate(2 << 15); + + private Block next; + + public RawBlockIterator(final Path file, final Function headerReader) + throws IOException { + fileChannel = FileChannel.open(file); + this.headerReader = headerReader; + nextBlock(); + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Block next() { + if (next == null) { + throw new NoSuchElementException("No more blocks in found in the file."); + } + final Block result = next; + try { + nextBlock(); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } + return result; + } + + @Override + public void close() throws IOException { + fileChannel.close(); + } + + private void nextBlock() throws IOException { + fillReadBuffer(); + int initial = readBuffer.position(); + if (initial > 0) { + final int length = RlpUtils.decodeLength(readBuffer, 0); + if (length > readBuffer.capacity()) { + readBuffer.flip(); + final ByteBuffer newBuffer = ByteBuffer.allocate(2 * length); + newBuffer.put(readBuffer); + readBuffer = newBuffer; + fillReadBuffer(); + initial = readBuffer.position(); + } + final RLPInput rlp = + new BytesValueRLPInput(BytesValue.wrap(Arrays.copyOf(readBuffer.array(), length)), false); + rlp.enterList(); + final BlockHeader header = headerReader.apply(rlp); + final BlockBody body = + new BlockBody(rlp.readList(Transaction::readFrom), rlp.readList(headerReader)); + next = new Block(header, body); + readBuffer.position(length); + readBuffer.compact(); + readBuffer.position(initial - length); + } else { + next = null; + } + } + + private void fillReadBuffer() throws IOException { + fileChannel.read(readBuffer); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractCallOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractCallOperation.java new file mode 100755 index 00000000000..6c199d24473 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractCallOperation.java @@ -0,0 +1,205 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +/** + * A skeleton class for implementing call operations. + * + *

A call operation creates a child message call from the current message context, allows it to + * execute, and then updates the current message context based on its execution. + */ +public abstract class AbstractCallOperation extends AbstractOperation { + + public AbstractCallOperation( + final int opcode, + final String name, + final int stackItemsConsumed, + final int stackItemsProduced, + final boolean updatesProgramCounter, + final int opSize, + final GasCalculator gasCalculator) { + super( + opcode, + name, + stackItemsConsumed, + stackItemsProduced, + updatesProgramCounter, + opSize, + gasCalculator); + } + + /** + * Returns the additional gas to provide the call operation. + * + * @param frame The current message frame + * @return the additional gas to provide the call operation + */ + protected abstract Gas gas(MessageFrame frame); + + /** + * Returns the account the call is being made to. + * + * @param frame The current message frame + * @return the account the call is being made to + */ + protected abstract Address to(MessageFrame frame); + + /** + * Returns the value being transferred in the call + * + * @param frame The current message frame + * @return the value being transferred in the call + */ + protected abstract Wei value(MessageFrame frame); + + /** + * Returns the apparent value being transferred in the call + * + * @param frame The current message frame + * @return the apparent value being transferred in the call + */ + protected abstract Wei apparentValue(MessageFrame frame); + + /** + * Returns the memory offset the input data starts at. + * + * @param frame The current message frame + * @return the memory offset the input data starts at + */ + protected abstract UInt256 inputDataOffset(MessageFrame frame); + + /** + * Returns the length of the input data to read from memory. + * + * @param frame The current message frame + * @return the length of the input data to read from memory. + */ + protected abstract UInt256 inputDataLength(MessageFrame frame); + + /** + * Returns the memory offset the offset data starts at. + * + * @param frame The current message frame + * @return the memory offset the offset data starts at + */ + protected abstract UInt256 outputDataOffset(MessageFrame frame); + + /** + * Returns the length of the output data to read from memory. + * + * @param frame The current message frame + * @return the length of the output data to read from memory. + */ + protected abstract UInt256 outputDataLength(MessageFrame frame); + + /** + * Returns the account address the call operation is being performed on + * + * @param frame The current message frame + * @return the account address the call operation is being performed on + */ + protected abstract Address address(MessageFrame frame); + + /** + * Returns the account address the call operation is being sent from + * + * @param frame The current message frame + * @return the account address the call operation is being sent from + */ + protected abstract Address sender(MessageFrame frame); + + /** + * Returns the gas available to execute the child message call. + * + * @param frame The current message frame + * @return the gas available to execute the child message call + */ + protected abstract Gas gasAvailableForChildCall(MessageFrame frame); + + /** + * Returns whether or not the child message call should be static. + * + * @param frame The current message frame + * @return {@code true} if the child message call should be static; otherwise {@code false} + */ + protected abstract boolean isStatic(MessageFrame frame); + + @Override + public void execute(final MessageFrame frame) { + frame.clearReturnData(); + + final Address to = to(frame); + final Account contract = frame.getWorldState().get(to); + + final Account account = frame.getWorldState().get(frame.getRecipientAddress()); + final Wei balance = account.getBalance(); + if (value(frame).compareTo(balance) > 0 || frame.getMessageStackDepth() >= 1024) { + frame.expandMemory(inputDataOffset(frame).toLong(), inputDataLength(frame).toInt()); + frame.expandMemory(outputDataOffset(frame).toLong(), outputDataLength(frame).toInt()); + frame.incrementRemainingGas(gasAvailableForChildCall(frame)); + frame.popStackItems(getStackItemsConsumed()); + frame.pushStackItem(Bytes32.ZERO); + return; + } + + final BytesValue inputData = frame.readMemory(inputDataOffset(frame), inputDataLength(frame)); + + final MessageFrame childFrame = + MessageFrame.builder() + .type(MessageFrame.Type.MESSAGE_CALL) + .messageFrameStack(frame.getMessageFrameStack()) + .blockchain(frame.getBlockchain()) + .worldState(frame.getWorldState().updater()) + .initialGas(gasAvailableForChildCall(frame)) + .address(address(frame)) + .originator(frame.getOriginatorAddress()) + .contract(to) + .gasPrice(frame.getGasPrice()) + .inputData(inputData) + .sender(sender(frame)) + .value(value(frame)) + .apparentValue(apparentValue(frame)) + .code(new Code(contract != null ? contract.getCode() : BytesValue.EMPTY)) + .blockHeader(frame.getBlockHeader()) + .depth(frame.getMessageStackDepth() + 1) + .isStatic(isStatic(frame)) + .completer(child -> complete(frame, child)) + .build(); + + frame.getMessageFrameStack().addFirst(childFrame); + frame.setState(MessageFrame.State.CODE_SUSPENDED); + } + + public void complete(final MessageFrame frame, final MessageFrame childFrame) { + frame.setState(MessageFrame.State.CODE_EXECUTING); + + final UInt256 outputOffset = outputDataOffset(frame); + final UInt256 outputSize = outputDataLength(frame); + frame.writeMemory(outputOffset, outputSize, childFrame.getOutputData()); + + frame.setReturnData(childFrame.getOutputData()); + frame.addLogs(childFrame.getLogs()); + frame.addSelfDestructs(childFrame.getSelfDestructs()); + frame.incrementGasRefund(childFrame.getGasRefund()); + + final Gas gasRemaining = childFrame.getRemainingGas(); + frame.incrementRemainingGas(gasRemaining); + + frame.popStackItems(getStackItemsConsumed()); + + if (childFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS) { + frame.pushStackItem(UInt256.ONE.getBytes()); + } else { + frame.pushStackItem(Bytes32.ZERO); + } + + final int currentPC = frame.getPC(); + frame.setPC(currentPC + 1); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractOperation.java new file mode 100755 index 00000000000..9ccabcefc4d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/AbstractOperation.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.ethereum.vm; + +/** + * All {@link Operation} implementations should inherit from this class to get the setting of some + * members for free. + */ +public abstract class AbstractOperation implements Operation { + private final int opcode; + private final String name; + private final int stackItemsConsumed; + private final int stackItemsProduced; + private final boolean updatesProgramCounter; + private final int opSize; + private final GasCalculator gasCalculator; + + public AbstractOperation( + final int opcode, + final String name, + final int stackItemsConsumed, + final int stackItemsProduced, + final boolean updatesProgramCounter, + final int opSize, + final GasCalculator gasCalculator) { + this.opcode = opcode & 0xff; + this.name = name; + this.stackItemsConsumed = stackItemsConsumed; + this.stackItemsProduced = stackItemsProduced; + this.updatesProgramCounter = updatesProgramCounter; + this.opSize = opSize; + this.gasCalculator = gasCalculator; + } + + protected GasCalculator gasCalculator() { + return gasCalculator; + } + + @Override + public int getOpcode() { + return opcode; + } + + @Override + public String getName() { + return name; + } + + @Override + public int getStackItemsConsumed() { + return stackItemsConsumed; + } + + @Override + public int getStackItemsProduced() { + return stackItemsProduced; + } + + @Override + public int getStackSizeChange() { + return stackItemsProduced - stackItemsConsumed; + } + + @Override + public boolean getUpdatesProgramCounter() { + return updatesProgramCounter; + } + + @Override + public int getOpSize() { + return opSize; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Code.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Code.java new file mode 100755 index 00000000000..4d450ff47bf --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Code.java @@ -0,0 +1,94 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.vm.operations.JumpDestOperation; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.BitSet; + +import com.google.common.base.MoreObjects; + +/** Represents EVM code associated with an account. */ +public class Code { + + /** The bytes representing the code. */ + private final BytesValue bytes; + + /** Used to cache valid jump destinations. */ + private BitSet validJumpDestinations; + + /** + * Public constructor. + * + * @param bytes The byte representation of the code. + */ + public Code(final BytesValue bytes) { + this.bytes = bytes; + } + + public Code() { + this(BytesValue.EMPTY); + } + + /** + * Returns true if the object is equal to this; otherwise false. + * + * @param other The object to compare this with. + * @return True if the object is equal to this; otherwise false. + */ + @Override + public boolean equals(final Object other) { + if (other == null) return false; + if (other == this) return true; + if (!(other instanceof Code)) return false; + + final Code that = (Code) other; + return this.bytes.equals(that.bytes); + } + + @Override + public int hashCode() { + return bytes.hashCode(); + } + + /** @return The number of bytes in the code. */ + public int getSize() { + return bytes.size(); + } + + /** + * Determine whether a specified destination is a valid jump target. + * + * @param evm the EVM executing this code + * @param destination The destination we're checking for validity. + * @return Whether or not this location is a valid jump destination. + */ + public boolean isValidJumpDestination(final EVM evm, final UInt256 destination) { + if (!destination.fitsInt()) return false; + + final int jumpDestination = destination.toInt(); + if (jumpDestination > getSize()) return false; + + if (validJumpDestinations == null) { + // Calculate valid jump destinations + validJumpDestinations = new BitSet(getSize()); + evm.forEachOperation( + this, + (final Operation op, final Integer offset) -> { + if (op.getOpcode() == JumpDestOperation.OPCODE) { + validJumpDestinations.set(offset); + } + }); + } + return validJumpDestinations.get(jumpDestination); + } + + public BytesValue getBytes() { + return bytes; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("bytes", bytes).toString(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracer.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracer.java new file mode 100755 index 00000000000..7cdd08c59f8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracer.java @@ -0,0 +1,98 @@ +package net.consensys.pantheon.ethereum.vm; + +import static net.consensys.pantheon.util.uint.UInt256.U_32; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.debug.TraceOptions; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltException; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +public class DebugOperationTracer implements OperationTracer { + + private final TraceOptions options; + private final List traceFrames = new ArrayList<>(); + + public DebugOperationTracer(final TraceOptions options) { + this.options = options; + } + + @Override + public void traceExecution( + final MessageFrame frame, + final Optional currentGasCost, + final ExecuteOperation executeOperation) + throws ExceptionalHaltException { + final int depth = frame.getMessageStackDepth(); + final String opcode = frame.getCurrentOperation().getName(); + final int pc = frame.getPC(); + final Gas gasRemaining = frame.getRemainingGas(); + final EnumSet exceptionalHaltReasons = + EnumSet.copyOf(frame.getExceptionalHaltReasons()); + final Optional stack = captureStack(frame); + final Optional memory = captureMemory(frame); + + try { + executeOperation.execute(); + } finally { + final Optional> storage = captureStorage(frame); + + traceFrames.add( + new TraceFrame( + pc, + opcode, + gasRemaining, + currentGasCost, + depth, + exceptionalHaltReasons, + stack, + memory, + storage)); + } + } + + private Optional> captureStorage(final MessageFrame frame) { + if (!options.isStorageEnabled()) { + return Optional.empty(); + } + final Map storageContents = + new TreeMap<>( + frame.getWorldState().getMutable(frame.getRecipientAddress()).getUpdatedStorage()); + return Optional.of(storageContents); + } + + private Optional captureMemory(final MessageFrame frame) { + if (!options.isMemoryEnabled()) { + return Optional.empty(); + } + final Bytes32[] memoryContents = new Bytes32[frame.memoryWordSize().toInt()]; + for (int i = 0; i < memoryContents.length; i++) { + memoryContents[i] = Bytes32.wrap(frame.readMemory(UInt256.of(i).times(U_32), U_32), 0); + } + return Optional.of(memoryContents); + } + + private Optional captureStack(final MessageFrame frame) { + if (!options.isStackEnabled()) { + return Optional.empty(); + } + final Bytes32[] stackContents = new Bytes32[frame.stackSize()]; + for (int i = 0; i < stackContents.length; i++) { + // Record stack contents in reverse + stackContents[i] = frame.getStackItem(stackContents.length - i - 1); + } + return Optional.of(stackContents); + } + + public List getTraceFrames() { + return traceFrames; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/EVM.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/EVM.java new file mode 100755 index 00000000000..7207bc1b6b6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/EVM.java @@ -0,0 +1,133 @@ +package net.consensys.pantheon.ethereum.vm; + +import static net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS; +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.MessageFrame.State; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltException; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltManager; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.function.BiConsumer; + +import org.apache.logging.log4j.Logger; + +public class EVM { + private static final Logger LOG = getLogger(); + + private static final int STOP_OPCODE = 0x00; + private final OperationRegistry operations; + private final Operation invalidOperation; + + public EVM(final OperationRegistry operations, final Operation invalidOperation) { + this.operations = operations; + this.invalidOperation = invalidOperation; + } + + public void runToHalt(final MessageFrame frame, final OperationTracer operationTracer) + throws ExceptionalHaltException { + while (frame.getState() == MessageFrame.State.CODE_EXECUTING) { + executeNextOperation(frame, operationTracer); + } + } + + public void forEachOperation( + final Code code, final BiConsumer operationDelegate) { + int pc = 0; + final int length = code.getSize(); + + while (pc < length) { + final Operation curOp = operationAtOffset(code, pc); + operationDelegate.accept(curOp, pc); + pc += curOp.getOpSize(); + } + } + + private void executeNextOperation(final MessageFrame frame, final OperationTracer operationTracer) + throws ExceptionalHaltException { + frame.setCurrentOperation(operationAtOffset(frame.getCode(), frame.getPC())); + evaluateExceptionalHaltReasons(frame); + final Optional currentGasCost = calculateGasCost(frame); + operationTracer.traceExecution( + frame, + currentGasCost, + () -> { + checkForExceptionalHalt(frame); + logState(frame); + decrementRemainingGas(frame, currentGasCost); + frame.getCurrentOperation().execute(frame); + incrementProgramCounter(frame); + }); + } + + private void evaluateExceptionalHaltReasons(final MessageFrame frame) { + final EnumSet haltReasons = + ExceptionalHaltManager.evaluateAll(frame, this); + frame.getExceptionalHaltReasons().addAll(haltReasons); + } + + private Optional calculateGasCost(final MessageFrame frame) { + // Calculate the cost if, and only if, we are not halting as a result of a stack underflow, as + // the operation may need all its stack items to calculate gas. + // This is how existing EVM implementations behave. + if (!frame.getExceptionalHaltReasons().contains(INSUFFICIENT_STACK_ITEMS)) { + try { + return Optional.ofNullable(frame.getCurrentOperation().cost(frame)); + } catch (final IllegalArgumentException e) { + // TODO: Figure out a better way to handle gas overflows. + } + } + return Optional.empty(); + } + + private void decrementRemainingGas(final MessageFrame frame, final Optional currentGasCost) { + frame.decrementRemainingGas( + currentGasCost.orElseThrow(() -> new IllegalStateException("Gas overflow detected"))); + } + + private void checkForExceptionalHalt(final MessageFrame frame) throws ExceptionalHaltException { + final EnumSet exceptionalHaltReasons = frame.getExceptionalHaltReasons(); + if (!exceptionalHaltReasons.isEmpty()) { + frame.setState(State.EXCEPTIONAL_HALT); + frame.setOutputData(BytesValue.EMPTY); + throw new ExceptionalHaltException(exceptionalHaltReasons); + } + } + + private void incrementProgramCounter(final MessageFrame frame) { + final Operation operation = frame.getCurrentOperation(); + if (frame.getState() == State.CODE_EXECUTING && !operation.getUpdatesProgramCounter()) { + final int currentPC = frame.getPC(); + final int opSize = operation.getOpSize(); + frame.setPC(currentPC + opSize); + } + } + + private static void logState(final MessageFrame frame) { + if (LOG.isTraceEnabled()) { + final StringBuilder builder = new StringBuilder(); + builder.append("Depth: ").append(frame.getMessageStackDepth()).append("\n"); + builder.append("Operation: ").append(frame.getCurrentOperation().getName()).append("\n"); + builder.append(" PC: ").append(frame.getPC()).append("\n"); + builder.append("Gas Remaining: ").append(frame.getRemainingGas()).append("\n"); + builder.append("Stack:"); + for (int i = 0; i < frame.stackSize(); ++i) { + builder.append("\n\t").append(i).append(" ").append(frame.getStackItem(i)); + } + LOG.trace(builder.toString()); + } + } + + private Operation operationAtOffset(final Code code, final int offset) { + final BytesValue bytecode = code.getBytes(); + // If the length of the program code is shorter than the required offset, halt execution. + if (offset >= bytecode.size()) { + return operations.get(STOP_OPCODE); + } + + return operations.getOrDefault(bytecode.get(offset), invalidOperation); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ExceptionalHaltReason.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ExceptionalHaltReason.java new file mode 100755 index 00000000000..991c3a7a241 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ExceptionalHaltReason.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.ethereum.vm; + +public enum ExceptionalHaltReason { + NONE, + INSUFFICIENT_GAS, + INSUFFICIENT_STACK_ITEMS, + INVALID_JUMP_DESTINATION, + INVALID_OPERATION, + INVALID_RETURN_DATA_BUFFER_ACCESS, + TOO_MANY_STACK_ITEMS, + ILLEGAL_STATE_CHANGE +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/GasCalculator.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/GasCalculator.java new file mode 100755 index 00000000000..e15f231d9e8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/GasCalculator.java @@ -0,0 +1,360 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.AbstractMessageProcessor; +import net.consensys.pantheon.ethereum.mainnet.precompiles.ECRECPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.IDPrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.RIPEMD160PrecompiledContract; +import net.consensys.pantheon.ethereum.mainnet.precompiles.SHA256PrecompiledContract; +import net.consensys.pantheon.ethereum.vm.operations.BalanceOperation; +import net.consensys.pantheon.ethereum.vm.operations.BlockHashOperation; +import net.consensys.pantheon.ethereum.vm.operations.ExpOperation; +import net.consensys.pantheon.ethereum.vm.operations.ExtCodeCopyOperation; +import net.consensys.pantheon.ethereum.vm.operations.ExtCodeSizeOperation; +import net.consensys.pantheon.ethereum.vm.operations.JumpDestOperation; +import net.consensys.pantheon.ethereum.vm.operations.LogOperation; +import net.consensys.pantheon.ethereum.vm.operations.MLoadOperation; +import net.consensys.pantheon.ethereum.vm.operations.MStore8Operation; +import net.consensys.pantheon.ethereum.vm.operations.MStoreOperation; +import net.consensys.pantheon.ethereum.vm.operations.SLoadOperation; +import net.consensys.pantheon.ethereum.vm.operations.SStoreOperation; +import net.consensys.pantheon.ethereum.vm.operations.SelfDestructOperation; +import net.consensys.pantheon.ethereum.vm.operations.Sha3Operation; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +/** + * Provides various gas cost lookups and calculations used during block processing. + * + *

The {@code GasCalculator} is meant to encapsulate all {@link Gas}-related calculations except + * for the following "safe" operations: + * + *

    + *
  • Operation Gas Deductions: Deducting the operation's gas cost from the VM's current + * message frame because the + *
+ */ +public interface GasCalculator { + + // Transaction Gas Calculations + + /** + * Returns a {@link Transaction}s intrinisic gas cost + * + * @param transaction The transaction + * @return the transaction's intrinsic gas cost + */ + Gas transactionIntrinsicGasCost(Transaction transaction); + + // Contract Creation Gas Calculations + + /** + * Returns the cost for a {@link AbstractMessageProcessor} to deposit the code in storage + * + * @param codeSize The size of the code in bytes + * @return the code deposit cost + */ + Gas codeDepositGasCost(int codeSize); + + // Precompiled Contract Gas Calculations + + /** + * Returns the gas cost to execute the {@link IDPrecompiledContract}. + * + * @param input The input to the ID precompiled contract + * @return the gas cost to execute the ID precompiled contract + */ + Gas idPrecompiledContractGasCost(BytesValue input); + + /** + * Returns the gas cost to execute the {@link ECRECPrecompiledContract}. + * + * @return the gas cost to execute the ECREC precompiled contract + */ + Gas getEcrecPrecompiledContractGasCost(); + + /** + * Returns the gas cost to execute the {@link SHA256PrecompiledContract}. + * + * @param input The input to the SHA256 precompiled contract + * @return the gas cost to execute the SHA256 precompiled contract + */ + Gas sha256PrecompiledContractGasCost(BytesValue input); + + /** + * Returns the gas cost to execute the {@link RIPEMD160PrecompiledContract}. + * + * @param input The input to the RIPEMD160 precompiled contract + * @return the gas cost to execute the RIPEMD160 precompiled contract + */ + Gas ripemd160PrecompiledContractGasCost(BytesValue input); + + // Gas Tier Lookups + + /** + * Returns the gas cost for the zero gas tier. + * + * @return the gas cost for the zero gas tier + */ + Gas getZeroTierGasCost(); + + /** + * Returns the gas cost for the very low gas tier. + * + * @return the gas cost for the very low gas tier + */ + Gas getVeryLowTierGasCost(); + + /** + * Returns the gas cost for the low gas tier. + * + * @return the gas cost for the low gas tier + */ + Gas getLowTierGasCost(); + + /** + * Returns the gas cost for the base gas tier. + * + * @return the gas cost for the base gas tier + */ + Gas getBaseTierGasCost(); + + /** + * Returns the gas cost for the mid gas tier. + * + * @return the gas cost for the mid gas tier + */ + Gas getMidTierGasCost(); + + /** + * Returns the gas cost for the high gas tier. + * + * @return the gas cost for the high gas tier + */ + Gas getHighTierGasCost(); + + // Call/Create Operation Calculations + + /** + * Returns the gas cost for one of the various CALL operations. + * + * @param frame The current frame + * @param stipend The gas stipend being provided by the CALL caller + * @param inputDataOffset The offset in memory to retrieve the CALL input data + * @param inputDataLength The CALL input data length + * @param outputDataOffset The offset in memory to place the CALL output data + * @param outputDataLength The CALL output data length + * @param transferValue The wei being transferred + * @param recipient The CALL recipient + * @return The gas cost for the CALL operation + */ + Gas callOperationGasCost( + MessageFrame frame, + Gas stipend, + UInt256 inputDataOffset, + UInt256 inputDataLength, + UInt256 outputDataOffset, + UInt256 outputDataLength, + Wei transferValue, + Account recipient); + + /** + * Returns the amount of gas parent will provide its child CALL. + * + * @param frame The current frame + * @param stipend The gas stipend being provided by the CALL caller + * @param transfersValue Whether or not the call transfers any wei + * @return the amount of gas parent will provide its child CALL + */ + Gas gasAvailableForChildCall(MessageFrame frame, Gas stipend, boolean transfersValue); + + /** + * Returns the amount of gas the CREATE operation will consume. + * + * @param frame The current frame + * @return the amount of gas the CREATE operation will consume + */ + Gas createOperationGasCost(MessageFrame frame); + + /** + * Returns the amount of gas the CREATE2 operation will consume. + * + * @param frame The current frame + * @return the amount of gas the CREATE2 operation will consume + */ + Gas create2OperationGasCost(MessageFrame frame); + + /** + * Returns the amount of gas parent will provide its child CREATE. + * + * @param stipend The gas stipend being provided by the CREATE caller + * @return the amount of gas parent will provide its child CREATE + */ + Gas gasAvailableForChildCreate(Gas stipend); + + // Re-used Operation Calculations + + /** + * Returns the amount of gas consumed by the data copy operation. + * + * @param frame The current frame + * @param offset The offset in memory to copy the data to + * @param length The length of the data being copied into memory + * @return the amount of gas consumed by the data copy operation + */ + Gas dataCopyOperationGasCost(MessageFrame frame, UInt256 offset, UInt256 length); + + /** + * Returns the cost of expanding memory for the specified access. + * + * @param frame The current frame + * @param offset The offset in memory where the access occurs + * @param length the length of the memory access + * @return The gas required to expand memory for the specified access + */ + Gas memoryExpansionGasCost(MessageFrame frame, UInt256 offset, UInt256 length); + + // Specific Non-call Operation Calculations + + /** + * Returns the cost for executing a {@link BalanceOperation}. + * + * @return the cost for executing the balance operation + */ + Gas getBalanceOperationGasCost(); + + /** + * Returns the cost for executing a {@link BlockHashOperation}. + * + * @return the cost for executing the block hash operation + */ + Gas getBlockHashOperationGasCost(); + + /** + * Returns the cost for executing a {@link ExpOperation}. + * + * @param numBytes The number of bytes for the exponent parameter + * @return the cost for executing the exp operation + */ + Gas expOperationGasCost(int numBytes); + + /** + * Returns the cost for executing a {@link ExtCodeCopyOperation}. + * + * @param frame The current frame + * @param offset The offset in memory to external code copy the data to + * @param length The length of the code being copied into memory + * @return the cost for executing the external code size operation + */ + Gas extCodeCopyOperationGasCost(MessageFrame frame, UInt256 offset, UInt256 length); + + /** + * Returns the cost for executing a {@link ExtCodeSizeOperation}. + * + * @return the cost for executing the external code size operation + */ + Gas getExtCodeSizeOperationGasCost(); + + /** + * Returns the cost for executing a {@link JumpDestOperation}. + * + * @return the cost for executing the jump destination operation + */ + Gas getJumpDestOperationGasCost(); + + /** + * Returns the cost for executing a {@link LogOperation}. + * + * @param frame The current frame + * @param dataOffset The offset in memory where the log data exists + * @param dataLength The length of the log data to read from memory + * @param numTopics The number of topics in the log + * @return the cost for executing the external code size operation + */ + Gas logOperationGasCost( + MessageFrame frame, UInt256 dataOffset, UInt256 dataLength, int numTopics); + + /** + * Returns the cost for executing a {@link MLoadOperation}. + * + * @param frame The current frame + * @param offset The offset in memory where the access takes place + * @return the cost for executing the memory load operation + */ + Gas mLoadOperationGasCost(MessageFrame frame, UInt256 offset); + + /** + * Returns the cost for executing a {@link MStoreOperation}. + * + * @param frame The current frame + * @param offset The offset in memory where the access takes place + * @return the cost for executing the memory store operation + */ + Gas mStoreOperationGasCost(MessageFrame frame, UInt256 offset); + + /** + * Returns the cost for executing a {@link MStore8Operation}. + * + * @param frame The current frame + * @param offset The offset in memory where the access takes place + * @return the cost for executing the memory byte store operation + */ + Gas mStore8OperationGasCost(MessageFrame frame, UInt256 offset); + + /** + * Returns the cost for executing a {@link SelfDestructOperation}. + * + * @param recipient The recipient of the self destructed inheritance (may be null) + * @param inheritance The amount the recipient will receive + * @return the cost for executing the self destruct operation + */ + Gas selfDestructOperationGasCost(Account recipient, Wei inheritance); + + /** + * Returns the cost for executing a {@link Sha3Operation}. + * + * @param frame The current frame + * @param offset The offset in memory where the data to be hashed exists + * @param length The hashed data length + * @return the cost for executing the memory byte store operation + */ + Gas sha3OperationGasCost(MessageFrame frame, UInt256 offset, UInt256 length); + + /** + * Returns the cost for executing a {@link SLoadOperation}. + * + * @return the cost for executing the storage load operation + */ + Gas getSloadOperationGasCost(); + + /** + * Returns the cost for clearing a value in a {@link SStoreOperation}. + * + * @return the cost for clearing a value in a storage store operation + */ + Gas getStorageResetGasCost(); + + /** + * Returns the cost for setting a value in a {@link SStoreOperation}. + * + * @return the cost for setting a value in a storage store operation + */ + Gas getStorageSetGasCost(); + + /** + * Returns the refund amount for clearing a storage location in a {@link SStoreOperation}. + * + * @return the refund amount for clearing a storage store operation + */ + Gas getStorageResetRefundAmount(); + + /** + * Returns the refund amount for deleting an account in a {@link SelfDestructOperation}. + * + * @return the refund amount for deleting an account in a self destruct operation + */ + Gas getSelfDestructRefundAmount(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Memory.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Memory.java new file mode 100755 index 00000000000..e3724eabe70 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Memory.java @@ -0,0 +1,529 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.bytes.MutableBytes32; +import net.consensys.pantheon.util.bytes.MutableBytesValue; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Value; +import net.consensys.pantheon.util.uint.UInt256s; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Objects; + +import com.google.common.base.Joiner; + +/** + * A EVM memory implementation. + * + *

Note: this is meant to map to I in Section 9.1 "Basics" and Section 9.4.1 "Machine State" in + * the Yellow Paper Revision 59dccd. + */ +public class Memory { + + // See below. + private static final long MAX_BYTES = 32L * Integer.MAX_VALUE; + + /** + * The data stored within the memory. + * + *

Note that the current Ethereum spec don't put a limit on memory, so we could theoretically + * overflow this. That said we can already store up to 64GB and: + * + *

    + *
  • that's 64GB of underlying bytes, but *a lot* more physical memory use in practice, + * because ... Java; worth testing but I suspect all but the beefiest servers would OOM + * before we come close to his in the first place. + *
  • the price of a transaction needing more than that is likely prohibitive. + *
+ * + * So this is likely a reasonable limitation, at least at first (and possibly ever if I'm to bet). + */ + /* + * Implementation note: using an array of word have a bunch of advantages: - it can make + * expansions cheaper (less bytes to copy on resize). - it makes word-related operations simple. - + * it's an easy way to put a higher limit on addressable memory (Integer.MAX_VALUE word is 32 + * times more capacity than Integer.MAX_VALUE bytes). but it's not without downsides either: - + * non-word aligned operations (on more than 1 byte) are currently more expansive (could be + * improved, but with more works). - sequential access, even word-aligned, might be slower than if + * we allocated larger byte arrays underneath due to cache effects. This is likely good enough + * initially, but a page-based design (with a page being X word, X to be determined) could be + * worth exploring as a future improvement. + * + * Lastly note that we may want to share the underlying implementation with the VM Stack: the + * stack is really just growable memory that is grown/accessed from the end and on by fully word, + * but the same page-based design probably make sense. + */ + private final ArrayList data; + + // Really data.size(), but cached as a UInt256 to avoid recomputing it each time. + private UInt256 activeWords = UInt256.ZERO; + + public Memory() { + this(new ArrayList<>()); + } + + private Memory(final ArrayList data) { + this.data = data; + this.activeWords = UInt256.of(data.size()); + } + + private static RuntimeException overflow(final long v) { + return overflow(String.valueOf(v)); + } + + private static RuntimeException overflow(final String v) { + // TODO: we should probably have another specific exception so this properly end up as an + // exceptional halt condition with a clear message (message that can indicate that if anyone + // runs into this, he should contact us so we know it's a case we do need to handle). + final String msg = "Memory index or length %s too large, cannot be larger than %d"; + throw new IllegalStateException(String.format(msg, v, MAX_BYTES)); + } + + private void checkByteIndex(final long v) { + // We can have at most MAX_BYTES, so an index can only at most MAX_BYTES - 1. + if (v < 0 || v >= MAX_BYTES) throw overflow(v); + } + + private long asByteIndex(final UInt256 w) { + try { + final long v = w.toLong(); + checkByteIndex(v); + return v; + } catch (final IllegalStateException e) { + throw overflow(w.toString()); + } + } + + private static int asByteLength(final UInt256 l) { + try { + // While we can theoretically support up to 32 * Integer.MAX_VALUE due to storing words, and + // so an index in memory need to be a long internally, we simply cannot load/store more than + // Integer.MAX_VALUE bytes at a time (BytesValue has an int size). + return l.toInt(); + } catch (final IllegalStateException e) { + throw overflow(l.toString()); + } + } + + private int wordForByte(final long byteIndex) { + checkByteIndex(byteIndex); + return (int) (byteIndex / Bytes32.SIZE); + } + + private int indexInWord(final long byteIndex) { + checkByteIndex(byteIndex); + return (int) (byteIndex - ((byteIndex / Bytes32.SIZE) * Bytes32.SIZE)); + } + + /** + * For use in memoryExpansionGasCost() of GasCost. Returns the number of new active words that + * accommodate at least the number of specified bytes from the provide memory offset. + * + *

Not that this has to return a UInt256 for Gas calculation, in case someone writes code that + * require a crazy amount of data. Such allocation should get prohibitive however and we will end + * up with an Out-of-Gas error. + * + * @param location The offset in memory from which we want to accommodate {@code numBytes}. + * @param numBytes The minimum number of bytes in memory. + * @return The number of active words that accommodate at least the number of specified bytes. + */ + public UInt256 calculateNewActiveWords( + final UInt256Value location, final UInt256Value numBytes) { + if (numBytes.isZero()) { + return activeWords; + } + + if (location.fitsInt() && numBytes.fitsInt()) { + // Fast common path (note that we work on int but use long arithmetic to avoid issues) + final long byteSize = (long) location.toInt() + (long) numBytes.toInt(); + int wordSize = (int) (byteSize / Bytes32.SIZE); + if (byteSize % Bytes32.SIZE != 0) wordSize += 1; + return wordSize > data.size() ? UInt256.of(wordSize) : activeWords; + } else { + // Slow, rare path + + // Note that this is one place where, while the result will fit UInt256, we should compute + // without modulo for extreme cases. + final BigInteger byteSize = + BytesValues.asUnsignedBigInteger(location.getBytes()) + .add(BytesValues.asUnsignedBigInteger(numBytes.getBytes())); + final BigInteger[] result = byteSize.divideAndRemainder(BigInteger.valueOf(Bytes32.SIZE)); + BigInteger wordSize = result[0]; + if (!result[1].equals(BigInteger.ZERO)) { + wordSize = wordSize.add(BigInteger.ONE); + } + return UInt256s.max(activeWords, UInt256.of(wordSize)); + } + } + + /** + * Expands the active words to accommodate the specified byte position. + * + * @param address The location in memory to start with. + * @param numBytes The number of bytes to get. + */ + public void ensureCapacityForBytes(final long address, final int numBytes) { + // Do not increase the memory capacity if no bytes are being written + // regardless of what the address may be. + if (numBytes == 0) { + return; + } + final int lastWordRequired = wordForByte(address + numBytes - 1); + maybeExpandCapacity(lastWordRequired + 1); + } + + /** + * Expands the memory to the specified number of active words. + * + * @param newActiveWords The new number of active words to expand to. + */ + private void maybeExpandCapacity(final int newActiveWords) { + if (data.size() >= newActiveWords) return; + + // Require full capacity to guarantee we don't resize more than once. + data.ensureCapacity(newActiveWords); + final int toAdd = newActiveWords - data.size(); + for (int i = 0; i < toAdd; i++) { + data.add(MutableBytes32.create()); + } + this.activeWords = UInt256.of(data.size()); + } + + /** + * Returns true if the object is equal to this memory instance; otherwise false. + * + * @param other The object to compare this memory instance with. + * @return True if the object is equal to this memory instance. + */ + @Override + public boolean equals(final Object other) { + if (other == null) return false; + if (other == this) return true; + if (!(other instanceof Memory)) return false; + + final Memory that = (Memory) other; + return this.data.equals(that.data); + } + + @Override + public int hashCode() { + return Objects.hash(data); + } + + /** + * Returns the current number of active bytes stored in memory. + * + * @return The current number of active bytes stored in memory. + */ + public long getActiveBytes() { + return (long) data.size() * Bytes32.SIZE; + } + + /** + * Returns the current number of active words stored in memory. + * + * @return The current number of active words stored in memory. + */ + public UInt256 getActiveWords() { + return activeWords; + } + + /** + * Returns a copy of bytes from memory. + * + * @param location The location in memory to start with. + * @param numBytes The number of bytes to get. + * @return A fresh copy of the bytes from memory starting at {@code location} and extending {@code + * numBytes}. + */ + public BytesValue getBytes(final UInt256 location, final UInt256 numBytes) { + // Note: if length == 0, we don't require any memory expansion, whatever location is. So + // we we must call asByteIndex(location) after this check so as it doesn't throw if the location + // is too big but the length is 0 (which is somewhat nonsensical, but is exercise by some + // tests). + final int length = asByteLength(numBytes); + if (length == 0) { + return BytesValue.EMPTY; + } + + final long start = asByteIndex(location); + + ensureCapacityForBytes(start, length); + + // Index of last byte to set. + final long end = start + length - 1; + + final int startWord = wordForByte(start); + final int idxInStart = indexInWord(start); + final int endWord = wordForByte(end); + final int idxInEnd = indexInWord(end); + + if (startWord == endWord) { + // Bytes within a word, fast-path. + final MutableBytesValue bytes = data.get(startWord); + return idxInStart == 0 && length == Bytes32.SIZE + ? bytes.copy() + : bytes.slice(idxInStart, length).copy(); + } + + // Spans multiple word, slower path. + final int bytesInStartWord = Bytes32.SIZE - idxInStart; + final int bytesInEndWord = idxInEnd + 1; + + final MutableBytesValue result = MutableBytesValue.create(length); + int resultIdx = 0; + data.get(startWord).slice(idxInStart).copyTo(result, resultIdx); + resultIdx += bytesInStartWord; + for (int i = startWord + 1; i < endWord; i++) { + data.get(i).copyTo(result, resultIdx); + resultIdx += Bytes32.SIZE; + } + data.get(endWord).slice(0, bytesInEndWord).copyTo(result, resultIdx); + return result; + } + + /** + * Copy the bytes from the provided number of bytes from the provided value to memory from the + * provided offset. + * + *

Note that this method will extend memory to accommodate the location assigned and bytes + * copied and so never fails. + * + * @param memOffset the location in memory at which to start copying the bytes of {@code value}. + * @param sourceOffset the location in the source to start copying. + * @param numBytes the number of bytes to set in memory. Note that this value may differ from + * {@code value.size()}: if {@code numBytes < value.size()} bytes, only {@code numBytes} will + * be copied from {@code value}; if {@code numBytes < value.size()}, then only the bytes in + * {@code value} will be copied, but the memory will be expanded if necessary to cover {@code + * numBytes} (in other words, {@link #getActiveWords()} will return a value consistent with + * having set {@code numBytes} bytes, even if less than that have been concretely set due to + * {@code value} being smaller). + * @param bytes the bytes to copy to memory from {@code location}. + */ + public void setBytes( + final UInt256 memOffset, + final UInt256 sourceOffset, + final UInt256 numBytes, + final BytesValue bytes) { + final int offset = sourceOffset.fitsInt() ? sourceOffset.toInt() : Integer.MAX_VALUE; + final int length = numBytes.fitsInt() ? numBytes.toInt() : Integer.MAX_VALUE; + + if (offset >= bytes.size()) { + clearBytes(memOffset, numBytes); + return; + } + + final BytesValue toCopy = bytes.slice(offset, Math.min(length, bytes.size() - offset)); + setBytes(memOffset, numBytes, toCopy); + } + + /** + * Copy the bytes from the provided number of bytes from the provided value to memory from the + * provided offset. + * + *

Note that this method will extend memory to accommodate the location assigned and bytes + * copied and so never fails. + * + * @param location the location in memory at which to start copying the bytes of {@code value}. + * @param numBytes the number of bytes to set in memory. Note that this value may differ from + * {@code value.size()}: if {@code numBytes < value.size()} bytes, only {@code numBytes} will + * be copied from {@code value}; if {@code numBytes < value.size()}, then only the bytes in + * {@code value} will be copied, but the memory will be expanded if necessary to cover {@code + * numBytes} (in other words, {@link #getActiveWords()} will return a value consistent with + * having set {@code numBytes} bytes, even if less than that have been concretely set due to + * {@code value} being smaller). + * @param taintedValue the bytes to copy to memory from {@code location}. + */ + public void setBytes( + final UInt256 location, final UInt256 numBytes, final BytesValue taintedValue) { + if (numBytes.isZero()) { + return; + } + + final long start = asByteIndex(location); + final int length = asByteLength(numBytes); + + ensureCapacityForBytes(start, length); + + // We've properly expanded memory as needed. We now have simply have to copy the + // min(length, value.size()) first bytes of value. + if (taintedValue.isEmpty()) { + return; + } + final BytesValue value; + if (taintedValue.size() > length) { + value = taintedValue.slice(0, length); + } else if (taintedValue.size() < length) { + value = taintedValue; + clearBytes(location.plus(taintedValue.size()), numBytes.minus(taintedValue.size())); + } else { + value = taintedValue; + } + + // Index of last byte to set. + final long end = start + value.size() - 1; + + final int startWord = wordForByte(start); + final int idxInStart = indexInWord(start); + final int endWord = wordForByte(end); + + if (startWord == endWord) { + // Bytes within a word, fast-path. + value.copyTo(data.get(startWord), idxInStart); + return; + } + + // Spans multiple word, slower path. + final int bytesInStartWord = Bytes32.SIZE - idxInStart; + + int valueIdx = 0; + value.slice(valueIdx, bytesInStartWord).copyTo(data.get(startWord), idxInStart); + valueIdx += bytesInStartWord; + for (int i = startWord + 1; i < endWord; i++) { + value.slice(valueIdx, Bytes32.SIZE).copyTo(data.get(i)); + valueIdx += Bytes32.SIZE; + } + value.slice(valueIdx).copyTo(data.get(endWord), 0); + } + + /** + * Clears (set to 0) some contiguous number of bytes in memory. + * + * @param location The location in memory from which to start clearing the bytes. + * @param numBytes The number of bytes to clear. + */ + public void clearBytes(final UInt256 location, final UInt256 numBytes) { + // See getBytes for why we checki length == 0 first, before calling asByteIndex(location). + final int length = asByteLength(numBytes); + if (length == 0) { + return; + } + clearBytes(asByteIndex(location), length); + } + + /** + * Clears (set to 0) some contiguous number of bytes in memory. + * + * @param location The location in memory from which to start clearing the bytes. + * @param numBytes The number of bytes to clear. + */ + public void clearBytes(final long location, final int numBytes) { + if (numBytes == 0) { + return; + } + + ensureCapacityForBytes(location, numBytes); + + // Index of last byte to set. + final long end = location + numBytes - 1; + + final int startWord = wordForByte(location); + final int idxInStart = indexInWord(location); + final int endWord = wordForByte(end); + final int idxInEnd = indexInWord(end); + + if (startWord == endWord) { + // Bytes within a word, fast-path. + MutableBytesValue bytes = data.get(startWord); + if (idxInStart != 0 || numBytes != Bytes32.SIZE) { + bytes = bytes.mutableSlice(idxInStart, numBytes); + } + bytes.clear(); + return; + } + + // Spans multiple word, slower path. + final int bytesInStartWord = Bytes32.SIZE - idxInStart; + final int bytesInEndWord = idxInEnd + 1; + + data.get(startWord).mutableSlice(idxInStart, bytesInStartWord).clear(); + for (int i = startWord + 1; i < endWord; i++) { + data.get(i).clear(); + } + data.get(endWord).mutableSlice(0, bytesInEndWord).clear(); + } + + /** + * Sets a single byte in memory at the provided location. + * + * @param location the location of the byte to set. + * @param value the value to set for the byte at {@code location}. + */ + public void setByte(final UInt256 location, final byte value) { + final long start = asByteIndex(location); + ensureCapacityForBytes(start, 1); + + final int word = wordForByte(start); + final int idxInWord = indexInWord(start); + + data.get(word).set(idxInWord, value); + } + + /** + * Returns a copy of the 32-bytes word that begins at the specified memory location. + * + * @param location The memory location the 256-bit word begins at. + * @return a copy of the 32-bytes word that begins at the specified memory location. + */ + public Bytes32 getWord(final UInt256 location) { + final long start = asByteIndex(location); + ensureCapacityForBytes(start, Bytes32.SIZE); + + final int startWord = wordForByte(start); + final int idxInStart = indexInWord(start); + + if (idxInStart == 0) { + // Word-aligned. Fast-path. + return data.get(startWord).copy(); + } + + // Spans 2 memory word, slower path. + final MutableBytes32 result = MutableBytes32.create(); + final int sizeInFirstWord = Bytes32.SIZE - idxInStart; + data.get(startWord).slice(idxInStart, sizeInFirstWord).copyTo(result, 0); + data.get(startWord + 1) + .slice(0, Bytes32.SIZE - sizeInFirstWord) + .copyTo(result, sizeInFirstWord); + return result; + } + + /** + * Sets a 32-bytes word in memory at the provided location. + * + *

Note that this method will extend memory to accommodate the location assigned and bytes + * copied and so never fails. + * + * @param location the location at which to start setting the bytes. + * @param bytes the 32 bytes to copy at {@code location}. + */ + public void setWord(final UInt256 location, final Bytes32 bytes) { + final long start = asByteIndex(location); + ensureCapacityForBytes(start, Bytes32.SIZE); + + final int startWord = wordForByte(start); + final int idxInStart = indexInWord(start); + + if (idxInStart == 0) { + // Word-aligned. Fast-path. + bytes.copyTo(data.get(startWord)); + return; + } + + // Spans 2 memory word, slower path. + final int sizeInFirstWord = Bytes32.SIZE - idxInStart; + bytes.slice(0, sizeInFirstWord).copyTo(data.get(startWord), idxInStart); + bytes.slice(sizeInFirstWord).copyTo(data.get(startWord + 1), 0); + } + + @Override + public String toString() { + if (data.isEmpty()) { + return ""; + } + + return '\n' + Joiner.on("\n").join(data); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/MessageFrame.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/MessageFrame.java new file mode 100755 index 00000000000..64081cfdfd9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/MessageFrame.java @@ -0,0 +1,937 @@ +package net.consensys.pantheon.ethereum.vm; + +import static com.google.common.base.Preconditions.checkState; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.LogSeries; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.mainnet.AbstractMessageProcessor; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Value; + +import java.util.Deque; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +/** + * A container object for all of the state associated with a message. + * + *

A message corresponds to an interaction between two accounts. A {@link Transaction} spawns at + * least one message when its processed. Messages can also spawn messages depending on the code + * executed within a message. + * + *

Note that there is no specific Message object in the code base. Instead message executions + * correspond to a {@code MessageFrame} and a specific {@link AbstractMessageProcessor}. Currently + * there are two such {@link AbstractMessageProcessor} types: + * + *

Message Call ({@code MESSAGE_CALL}) + * + *

A message call consists of applying a set of changes to an account on behalf of another + * account. At the minimal end of changes is a value transfer between a sender and recipient + * account. If the recipient account contains code, that code is also executed. + * + *

Contract Creation ({@code CONTRACT_CREATION}) + * + *

A contract creation, as its name suggests, creates contract accounts. Contract initialization + * code and a value are supplied to initialize the contract account code and balance, respectively. + */ +public class MessageFrame { + + /** + * Message Frame State. + * + *

Message Frame Lifecycle

+ * + *

The diagram below presents the message frame lifecycle: + * + *

+   *            ------------------------------------------------------
+   *            |                                                    |
+   *            |                                                    v
+   *            |               ---------------------     ---------------------
+   *            |               |                   |     |                   |
+   *            |               |    CODE_SUCCESS   | --> | COMPLETED_SUCCESS |
+   *            |               |                   |     |                   |
+   *            |               ---------------------     ---------------------
+   *            |                         ^
+   *            |                         |
+   *  ---------------------     ---------------------     ---------------------
+   *  |                   |     |                   | --> |                   |
+   *  |    NOT_STARTED    | --> |   CODE_EXECUTING  |     |   CODE_SUSPENDED  |
+   *  |                   |     |                   | <-- |                   |
+   *  ---------------------     ---------------------     ---------------------
+   *            |                         |
+   *            |                         |
+   *            |                         |                 ---------------------
+   *            |                         |                 |                   |
+   *            |                         |------------> |      REVERTED     |
+   *            |                         |                 |                   |
+   *            |                         |                 ---------------------
+   *            |                         |
+   *            |                         v
+   *            |               ---------------------     ---------------------
+   *            |               |                   |     |                   |
+   *            |-------------> |  EXCEPTIONAL_HALT | --> | COMPLETED_FAILURE |
+   *                            |                   |     |                   |
+   *                            ---------------------     ---------------------
+   * 
+ * + *

Message Not Started ({@link #NOT_STARTED})

+ * + *

The message has not begun to execute yet. + * + *

Code Executing ({@link #CODE_EXECUTING})

+ * + *

The message contains code and has begun executing it. The execution will continue until it + * is halted due to (1) spawning a child message (2) encountering an exceptional halting condition + * (2) completing successfully. + * + *

Code Suspended Execution ({@link #CODE_SUSPENDED})

+ * + *

The message has spawned a child message and has suspended its execution until the child + * message has completed and notified its parent message. The message will then continue executing + * code ({@link #CODE_EXECUTING}) again. + * + *

Code Execution Completed Successfully ({@link #CODE_SUSPENDED})

+ * + *

The code within the message has executed to completion successfully. + * + *

Message Exceptionally Halted ({@link #EXCEPTIONAL_HALT})

+ * + *

The message execution has encountered an exceptional halting condition at some point during + * its execution. + * + *

Message Reverted ({@link #REVERT})

+ * + *

The message execution has requested to revert state during execution. + * + *

Message Execution Failed ({@link #COMPLETED_FAILED})

+ * + *

The message execution failed to execute successfully; most likely due to encountering an + * exceptoinal halting condition. At this point the message frame is finalized and the parent is + * notified. + * + *

Message Execution Completed Successfully ({@link #COMPLETED_SUCCESS})

+ * + *

The message execution completed successfully and needs to finalized and propagated to the + * parent message that spawned it. + */ + public enum State { + + /** Message execution has not started. */ + NOT_STARTED, + + /** Code execution within the message is in progress. */ + CODE_EXECUTING, + + /** Code execution within the message has finished successfully. */ + CODE_SUCCESS, + + /** Code execution within the message has been suspended. */ + CODE_SUSPENDED, + + /** An exceptional halting condition has occurred. */ + EXCEPTIONAL_HALT, + + /** State changes were reverted during execution. */ + REVERT, + + /** The message execution has failed to complete successfully. */ + COMPLETED_FAILED, + + /** The message execution has completed successfully. */ + COMPLETED_SUCCESS, + } + + /** The message type the frame corresponds to. */ + public enum Type { + + /** A Contract creation message. */ + CONTRACT_CREATION, + + /** A message call message. */ + MESSAGE_CALL, + } + + private static final int MAX_STACK_SIZE = 1024; + + // Global data fields. + private final WorldUpdater worldState; + private final Blockchain blockchain; + + // Metadata fields. + private final Type type; + private State state; + + // Machine state fields. + private Gas gasRemaining; + private int pc; + private final Memory memory; + private final OperandStack stack; + private BytesValue output; + private BytesValue returnData; + private final boolean isStatic; + + // Transaction substate fields. + private final LogSeries logs; + private Gas gasRefund; + private final Set

selfDestructs; + + // Execution Environment fields. + private final Address recipient; + private final Address originator; + private final Address contract; + private final Wei gasPrice; + private final BytesValue inputData; + private final Address sender; + private final Wei value; + private final Wei apparentValue; + private final Code code; + private final ProcessableBlockHeader blockHeader; + private final int depth; + private final Deque messageFrameStack; + + // Miscellaneous fields. + private final EnumSet exceptionalHaltReasons = + EnumSet.noneOf(ExceptionalHaltReason.class); + private Operation currentOperation; + private final Consumer completer; + + public static Builder builder() { + return new Builder(); + } + + private MessageFrame( + final Type type, + final Blockchain blockchain, + final Deque messageFrameStack, + final WorldUpdater worldState, + final Gas initialGas, + final Address recipient, + final Address originator, + final Address contract, + final Wei gasPrice, + final BytesValue inputData, + final Address sender, + final Wei value, + final Wei apparentValue, + final Code code, + final ProcessableBlockHeader blockHeader, + final int depth, + final boolean isStatic, + final Consumer completer) { + this.type = type; + this.blockchain = blockchain; + this.messageFrameStack = messageFrameStack; + this.worldState = worldState; + this.gasRemaining = initialGas; + this.pc = 0; + this.memory = new Memory(); + this.stack = new PreAllocatedOperandStack(MAX_STACK_SIZE); + this.output = BytesValue.EMPTY; + this.returnData = BytesValue.EMPTY; + this.logs = LogSeries.empty(); + this.gasRefund = Gas.ZERO; + this.selfDestructs = new HashSet<>(); + this.recipient = recipient; + this.originator = originator; + this.contract = contract; + this.gasPrice = gasPrice; + this.inputData = inputData; + this.sender = sender; + this.value = value; + this.apparentValue = apparentValue; + this.code = code; + this.blockHeader = blockHeader; + this.depth = depth; + this.state = State.NOT_STARTED; + this.isStatic = isStatic; + this.completer = completer; + } + + /** + * Return the program counter. + * + * @return the program counter + */ + public int getPC() { + return pc; + } + + /** + * Set the program counter. + * + * @param pc The new program counter value + */ + public void setPC(final int pc) { + this.pc = pc; + } + + /** Deducts the remainging gas. */ + public void clearGasRemaining() { + this.gasRemaining = Gas.ZERO; + } + + /** + * Decrement the amount of remaining gas. + * + * @param amount The amount of gas to deduct + */ + public void decrementRemainingGas(final Gas amount) { + this.gasRemaining = gasRemaining.minus(amount); + } + + /** + * Return the amount of remaining gas. + * + * @return the amount of remaining gas + */ + public Gas getRemainingGas() { + return gasRemaining; + } + + /** + * Increment the amount of remaining gas. + * + * @param amount The amount of gas to increment + */ + public void incrementRemainingGas(final Gas amount) { + this.gasRemaining = gasRemaining.plus(amount); + } + + /** + * Set the amount of remaining gas. + * + * @param amount The amount of remainging gas + */ + public void setGasRemaining(final Gas amount) { + this.gasRemaining = amount; + } + + /** + * Return the output data. + * + * @return the output data + */ + public BytesValue getOutputData() { + return output; + } + + /** + * Set the output data. + * + * @param output The output data + */ + public void setOutputData(final BytesValue output) { + this.output = output; + } + + /** Clears the output data buffer. */ + public void clearOutputData() { + setOutputData(BytesValue.EMPTY); + } + + /** + * Return the return data. + * + * @return the return data + */ + public BytesValue getReturnData() { + return returnData; + } + + /** + * Set the return data. + * + * @param returnData The return data + */ + public void setReturnData(final BytesValue returnData) { + this.returnData = returnData; + } + + /** Clear the return data buffer. */ + public void clearReturnData() { + setReturnData(BytesValue.EMPTY); + } + + /** + * Returns the item at the specified offset in the stack. + * + * @param offset The item's position relative to the top of the stack + * @return The item at the specified offset in the stack + * @throws IndexOutOfBoundsException if the offset is out of range + */ + public Bytes32 getStackItem(final int offset) { + return stack.get(offset); + } + + /** + * Removes the item at the top of the stack. + * + * @return the item at the top of the stack + * @throws IllegalStateException if the stack is empty + */ + public Bytes32 popStackItem() { + return stack.pop(); + } + + /** + * Removes the corresponding number of items from the top of the stack. + * + * @param n The number of items to pop off the stack + * @throws IllegalStateException if the stack does not contain enough items + */ + public void popStackItems(final int n) { + stack.bulkPop(n); + } + + /** + * Pushes the corresponding item onto the top of the stack + * + * @param value The value to push onto the stack. + * @throws IllegalStateException if the stack is full + */ + public void pushStackItem(final Bytes32 value) { + stack.push(value); + } + + /** + * Sets the stack item at the specified offset from the top of the stack to the value + * + * @param offset The item's position relative to the top of the stack + * @param value The value to set the stack item to + * @throws IllegalStateException if the stack is too small + */ + public void setStackItem(final int offset, final Bytes32 value) { + stack.set(offset, value); + } + + /** + * Return the current stack size. + * + * @return The current stack size + */ + public int stackSize() { + return stack.size(); + } + + /** + * Returns whether or not the message frame is static or not. + * + * @return {@code} true if the frame is static; otherwise {@code false} + */ + public boolean isStatic() { + return isStatic; + } + + /** + * Returns the memory size for specified memory access. + * + * @param offset The offset in memory + * @param length The length of the memory access + * @return the memory size for specified memory access + */ + public UInt256 calculateMemoryExpansion( + final UInt256Value offset, final UInt256Value length) { + return memory.calculateNewActiveWords(offset, length); + } + + /** + * Expands memory to accomodate the specified memory access. + * + * @param offset The offset in memory + * @param length The length of the memory access + */ + public void expandMemory(final long offset, final int length) { + memory.ensureCapacityForBytes(offset, length); + } + + /** + * Returns the number of bytes in memory. + * + * @return the number of bytes in memory + */ + public long memoryByteSize() { + return memory.getActiveBytes(); + } + + /** + * Returns the number of words in memory. + * + * @return the number of words in memory + */ + public UInt256 memoryWordSize() { + return memory.getActiveWords(); + } + + /** + * Read bytes in memory. + * + * @param offset The offset in memory + * @param length The length of the bytes to read + * @return The bytes in the specified range + */ + public BytesValue readMemory(final UInt256 offset, final UInt256 length) { + return memory.getBytes(offset, length); + } + + /** + * Write byte to memory + * + * @param offset The offset in memory + * @param value The value to set in memory + */ + public void writeMemory(final UInt256 offset, final byte value) { + memory.setByte(offset, value); + } + + /** + * Write bytes to memory + * + * @param offset The offset in memory + * @param length The length of the bytes to write + * @param value The value to write + */ + public void writeMemory(final UInt256 offset, final UInt256 length, final BytesValue value) { + memory.setBytes(offset, length, value); + } + + /** + * Write bytes to memory + * + * @param offset The offset in memory to start the write + * @param sourceOffset The offset in the source value to start the write + * @param length The length of the bytes to write + * @param value The value to write + */ + public void writeMemory( + final UInt256 offset, + final UInt256 sourceOffset, + final UInt256 length, + final BytesValue value) { + memory.setBytes(offset, sourceOffset, length, value); + } + + /** + * Accumulate a log. + * + * @param log The log to accumulate + */ + public void addLog(final Log log) { + logs.add(log); + } + + /** + * Accumulate logs. + * + * @param logs The logs to accumulate + */ + public void addLogs(final LogSeries logs) { + this.logs.addAll(logs); + } + + /** Clear the accumulated logs. */ + public void clearLogs() { + logs.clear(); + } + + /** + * Return the accumulated logs. + * + * @return the accumulated logs + */ + public LogSeries getLogs() { + return logs; + } + + /** + * Increment the gas refund. + * + * @param amount The amount to increment the refund + */ + public void incrementGasRefund(final Gas amount) { + this.gasRefund = gasRefund.plus(amount); + } + + /** Clear the accumulated gas refund. */ + public void clearGasRefund() { + gasRefund = Gas.ZERO; + } + + /** + * Return the accumulated gas refund. + * + * @return accumulated gas refund + */ + public Gas getGasRefund() { + return gasRefund; + } + + /** + * Add recipient to the self-destruct set if not already present. + * + * @param address The recipient to self-destruct + */ + public void addSelfDestruct(final Address address) { + selfDestructs.add(address); + } + + /** + * Add addresses to the self-destruct set if they are not already present. + * + * @param addresses The addresses to self-destruct + */ + public void addSelfDestructs(final Set
addresses) { + selfDestructs.addAll(addresses); + } + + /** Removes all entries in the self-destruct set. */ + public void clearSelfDestructs() { + selfDestructs.clear(); + } + + /** + * Returns the self-destruct set. + * + * @return the self-destruct set + */ + public Set
getSelfDestructs() { + return selfDestructs; + } + + /** + * Returns the current blockchain. + * + * @return the current blockchain + */ + public Blockchain getBlockchain() { + return blockchain; + } + + /** + * Return the world state. + * + * @return the world state + */ + public WorldUpdater getWorldState() { + return worldState; + } + + /** + * Returns the message frame type. + * + * @return the message frame type + */ + public Type getType() { + return type; + } + + /** + * Returns the current execution state. + * + * @return the current execution state + */ + public State getState() { + return state; + } + + /** + * Sets the current execution state. + * + * @param state The new execution state + */ + public void setState(final State state) { + this.state = state; + } + + /** + * Returns the code currently being executed. + * + * @return the code currently being executed + */ + public Code getCode() { + return code; + } + + /** + * Returns the current input data. + * + * @return the current input data + */ + public BytesValue getInputData() { + return inputData; + } + + /** + * Returns the recipient account recipient + * + * @return the callee account recipient + */ + public Address getRecipientAddress() { + return recipient; + } + + /** + * Returns the message stack depth. + * + * @return the message stack depth + */ + public int getMessageStackDepth() { + return depth; + } + + /** + * Returns the recipient that originated the message. + * + * @return the recipient that originated the message + */ + public Address getOriginatorAddress() { + return originator; + } + + /** + * Returns the recipient of the code currently executing. + * + * @return the recipient of the code currently executing + */ + public Address getContractAddress() { + return contract; + } + + /** + * Returns the current gas price. + * + * @return the current gas price + */ + public Wei getGasPrice() { + return gasPrice; + } + + /** + * Returns the recipient of the sender. + * + * @return the recipient of the sender + */ + public Address getSenderAddress() { + return sender; + } + + /** + * Returns the value being transferred. + * + * @return the value being transferred + */ + public Wei getValue() { + return value; + } + + /** + * Returns the apparent value being transferred. + * + * @return the apparent value being transferred + */ + public Wei getApparentValue() { + return apparentValue; + } + + /** + * Returns the current block header. + * + * @return the current block header + */ + public ProcessableBlockHeader getBlockHeader() { + return blockHeader; + } + + /** Performs updates based on the message frame's execution. */ + public void notifyCompletion() { + completer.accept(this); + } + + /** + * Returns the current message frame stack. + * + * @return the current message frame stack + */ + public Deque getMessageFrameStack() { + return messageFrameStack; + } + + public EnumSet getExceptionalHaltReasons() { + return exceptionalHaltReasons; + } + + public Operation getCurrentOperation() { + return currentOperation; + } + + public void setCurrentOperation(final Operation currentOperation) { + this.currentOperation = currentOperation; + } + + public static class Builder { + + private Type type; + private Blockchain blockchain; + private Deque messageFrameStack; + private WorldUpdater worldState; + private Gas initialGas; + private Address address; + private Address originator; + private Address contract; + private Wei gasPrice; + private BytesValue inputData; + private Address sender; + private Wei value; + private Wei apparentValue; + private Code code; + private ProcessableBlockHeader blockHeader; + private int depth = -1; + private boolean isStatic = false; + private Consumer completer; + + public Builder type(final Type type) { + this.type = type; + return this; + } + + public Builder messageFrameStack(final Deque messageFrameStack) { + this.messageFrameStack = messageFrameStack; + return this; + } + + public Builder blockchain(final Blockchain blockchain) { + this.blockchain = blockchain; + return this; + } + + public Builder worldState(final WorldUpdater worldState) { + this.worldState = worldState; + return this; + } + + public Builder initialGas(final Gas initialGas) { + this.initialGas = initialGas; + return this; + } + + public Builder address(final Address address) { + this.address = address; + return this; + } + + public Builder originator(final Address originator) { + this.originator = originator; + return this; + } + + public Builder contract(final Address contract) { + this.contract = contract; + return this; + } + + public Builder gasPrice(final Wei gasPrice) { + this.gasPrice = gasPrice; + return this; + } + + public Builder inputData(final BytesValue inputData) { + this.inputData = inputData; + return this; + } + + public Builder sender(final Address sender) { + this.sender = sender; + return this; + } + + public Builder value(final Wei value) { + this.value = value; + return this; + } + + public Builder apparentValue(final Wei apparentValue) { + this.apparentValue = apparentValue; + return this; + } + + public Builder code(final Code code) { + this.code = code; + return this; + } + + public Builder blockHeader(final ProcessableBlockHeader blockHeader) { + this.blockHeader = blockHeader; + return this; + } + + public Builder depth(final int depth) { + this.depth = depth; + return this; + } + + public Builder isStatic(final boolean isStatic) { + this.isStatic = isStatic; + return this; + } + + public Builder completer(final Consumer completer) { + this.completer = completer; + return this; + } + + private void validate() { + checkState(type != null, "Missing message frame type"); + checkState(blockchain != null, "Missing message frame blockchain"); + checkState(messageFrameStack != null, "Missing message frame message frame stack"); + checkState(worldState != null, "Missing message frame world state"); + checkState(initialGas != null, "Missing message frame initial getGasRemaining"); + checkState(address != null, "Missing message frame recipient"); + checkState(originator != null, "Missing message frame originator"); + checkState(contract != null, "Missing message frame contract"); + checkState(gasPrice != null, "Missing message frame getGasRemaining price"); + checkState(inputData != null, "Missing message frame input data"); + checkState(sender != null, "Missing message frame sender"); + checkState(value != null, "Missing message frame value"); + checkState(apparentValue != null, "Missing message frame apparent value"); + checkState(code != null, "Missing message frame code"); + checkState(blockHeader != null, "Missing message frame block header"); + checkState(depth > -1, "Missing message frame depth"); + checkState(completer != null, "Missing message frame completer"); + } + + public MessageFrame build() { + validate(); + + return new MessageFrame( + type, + blockchain, + messageFrameStack, + worldState, + initialGas, + address, + originator, + contract, + gasPrice, + inputData, + sender, + value, + apparentValue, + code, + blockHeader, + depth, + isStatic, + completer); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperandStack.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperandStack.java new file mode 100755 index 00000000000..07f25596b9d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperandStack.java @@ -0,0 +1,80 @@ +package net.consensys.pantheon.ethereum.vm; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.util.bytes.Bytes32; + +/** + * An operand stack for the Ethereum Virtual machine (EVM). + * + *

The operand stack is responsible for storing the current operands that the EVM can execute. It + * is assumed to have a fixed size. + */ +public interface OperandStack { + + /** + * Returns the operand located at the offset from the top of the stack. + * + * @param offset the position relative to the top of the stack of the operand to return + * @return the operand located at the specified offset + * @throws IndexOutOfBoundsException if the offset is out of range (offset < 0 || offset >= + * {@link #size()}) + */ + Bytes32 get(int offset); + + /** + * Removes the operand at the top of the stack. + * + * @return the operand removed from the top of the stack + * @throws IllegalStateException if the stack is empty (e.g. a stack underflow occurs) + */ + Bytes32 pop(); + + /** + * Pops the specified number of operands from the stack. + * + * @param items the number of operands to pop off the stack + * @throws IllegalArgumentException if the items to pop is negative. + * @throws IllegalStateException when the items to pop is greater than {@link #size()} + */ + default void bulkPop(final int items) { + if (items < 0) { + throw new IllegalArgumentException( + String.format("requested number of items to bulk pop (%d) is negative", items)); + } + checkArgument(items > 0, "number of items to pop must be greater than 0"); + if (items > size()) { + throw new IllegalStateException( + String.format("requested to bulk pop %d items off a stack of size %d", items, size())); + } + + for (int i = 0; i < items; ++i) { + pop(); + } + } + + /** + * Pushes the operand onto the stack. + * + * @param operand the operand to push on the stack + * @throws IllegalStateException when the stack is at capacity (e.g. a stack overflow occurs) + */ + public void push(Bytes32 operand); + + /** + * Sets the ith item from the top of the stack to the value. + * + * @param index the position relative to the top of the stack to set + * @param operand the new operand that replaces the operand at the current offset + * @throws IndexOutOfBoundsException if the offset is out of range (offset < 0 || offset >= + * {@link #size()}) + */ + void set(int index, Bytes32 operand); + + /** + * Returns the current number of operands in the stack. + * + * @return the current number of operands in the stack + */ + int size(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Operation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Operation.java new file mode 100755 index 00000000000..612cfcdf413 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Operation.java @@ -0,0 +1,57 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltPredicate; + +import java.util.EnumSet; +import java.util.Optional; + +public interface Operation extends ExceptionalHaltPredicate { + + /** + * @param frame The frame for execution of this operation. + * @return The gas cost associated with executing this operation given the current {@link + * MessageFrame}. + */ + Gas cost(MessageFrame frame); + + /** + * Executes the logic behind this operation. + * + * @param frame The frame for execution of this operation. + */ + void execute(MessageFrame frame); + + /** + * Check if an exceptional halt condition should apply + * + * @param frame the current frame + * @param previousReasons any existing exceptional halt conditions + * @param evm the currently executing EVM + * @return an {@link Optional} containing the {@link ExceptionalHaltReason} that applies or empty + * if no exceptional halt condition applies. + */ + @Override + default Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + return Optional.empty(); + } + + int getOpcode(); + + String getName(); + + int getStackItemsConsumed(); + + int getStackItemsProduced(); + + default int getStackSizeChange() { + return getStackItemsProduced() - getStackItemsConsumed(); + } + + boolean getUpdatesProgramCounter(); + + int getOpSize(); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationRegistry.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationRegistry.java new file mode 100755 index 00000000000..fd2d05fd4c0 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationRegistry.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.vm; + +/** Encapsulates a group of {@link Operation}s used together. */ +public class OperationRegistry { + + private static final int NUM_OPERATIONS = 256; + + private final Operation[] operations; + + public OperationRegistry() { + this.operations = new Operation[NUM_OPERATIONS]; + } + + public Operation get(final byte opcode) { + return get(opcode & 0xff); + } + + public Operation get(final int opcode) { + return operations[opcode]; + } + + public void put(final int opcode, final Operation operation) { + operations[opcode] = operation; + } + + public Operation getOrDefault(final byte opcode, final Operation defaultOperation) { + final Operation operation = get(opcode); + + if (operation == null) { + return defaultOperation; + } + + return operation; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationTracer.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationTracer.java new file mode 100755 index 00000000000..770442b67a7 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/OperationTracer.java @@ -0,0 +1,21 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltException; + +import java.util.Optional; + +public interface OperationTracer { + + OperationTracer NO_TRACING = + ((frame, currentGasCost, executeOperation) -> executeOperation.execute()); + + void traceExecution( + MessageFrame frame, Optional currentGasCost, ExecuteOperation executeOperation) + throws ExceptionalHaltException; + + interface ExecuteOperation { + + void execute() throws ExceptionalHaltException; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStack.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStack.java new file mode 100755 index 00000000000..0960a270735 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStack.java @@ -0,0 +1,98 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.Arrays; + +/** + * An {@link OperandStack} implementations whose capacity is pre-allocated. + * + *

The {@code PreAllocatedOperandStack} pre-allocates its internal storage to hold the max number + * it is capable of storing. + */ +public class PreAllocatedOperandStack implements OperandStack { + + private final Bytes32[] entries; + + private final int maxSize; + + private int top; + + public PreAllocatedOperandStack(final int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException( + String.format("max size (%d) must be non-negative", maxSize)); + } + this.entries = new Bytes32[maxSize]; + this.maxSize = maxSize; + this.top = -1; + } + + @Override + public Bytes32 get(final int offset) { + if (offset < 0 || offset >= size()) { + throw new IndexOutOfBoundsException(); + } + + return entries[top - offset]; + } + + @Override + public Bytes32 pop() { + if (top < 0) { + throw new IllegalStateException("operand stack underflow"); + } + + final Bytes32 removed = entries[top]; + entries[top--] = null; + return removed; + } + + @Override + public void push(final Bytes32 operand) { + final int nextTop = top + 1; + if (nextTop == maxSize) { + throw new IllegalStateException("operand stack overflow"); + } + entries[nextTop] = operand; + top = nextTop; + } + + @Override + public void set(final int offset, final Bytes32 operand) { + if (offset < 0 || offset >= size()) { + throw new IndexOutOfBoundsException(); + } + + entries[top - offset] = operand; + } + + @Override + public int size() { + return top + 1; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < size(); ++i) { + builder.append(String.format("\n0x%04X ", i)).append(get(i)); + } + return builder.toString(); + } + + @Override + public int hashCode() { + return Arrays.hashCode(entries); + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof PreAllocatedOperandStack)) { + return false; + } + + final PreAllocatedOperandStack that = (PreAllocatedOperandStack) other; + return Arrays.deepEquals(this.entries, that.entries); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Words.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Words.java new file mode 100755 index 00000000000..c3c67d24688 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/Words.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytes32; + +/** Static utility methods to work with VM words (that is, {@link Bytes32} values). */ +public abstract class Words { + private Words() {} + + /** + * Creates a new word containing the provided address. + * + * @param address The address to convert to a word. + * @return A VM word containing {@code address} (left-padded as according to the VM specification + * (Appendix H. of the Yellow paper)). + */ + public static Bytes32 fromAddress(final Address address) { + final MutableBytes32 bytes = MutableBytes32.create(); + address.copyTo(bytes, bytes.size() - Address.SIZE); + return bytes; + } + + /** + * Extract an address from the the provided address. + * + * @param bytes The word to extract the address from. + * @return An address build from the right-most 160-bits of the {@code bytes} (as according to the + * VM specification (Appendix H. of the Yellow paper)). + */ + public static Address toAddress(final Bytes32 bytes) { + return Address.wrap(bytes.slice(bytes.size() - Address.SIZE, Address.SIZE).copy()); + } + + /** + * The number of words corresponding to the provided input. + * + *

In other words, this compute {@code input.size() / 32} but rounded up. + * + * @param input the input to check. + * @return the number of (32 bytes) words that {@code input} spans. + */ + public static int numWords(final BytesValue input) { + // m/n round up == (m + n - 1)/n: http://www.cs.nott.ac.uk/~psarb2/G51MPC/slides/NumberLogic.pdf + return (input.size() + Bytes32.SIZE - 1) / Bytes32.SIZE; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltException.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltException.java new file mode 100755 index 00000000000..93f1831e212 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltException.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.vm.ehalt; + +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; + +import java.util.EnumSet; + +/** An exception to signal that an exceptional halt has occurred. */ +public class ExceptionalHaltException extends Exception { + private final EnumSet reasons; + + public ExceptionalHaltException(final EnumSet reasons) { + this.reasons = reasons; + } + + @Override + public String getMessage() { + return "Exceptional halt condition(s) triggered: " + this.reasons; + } + + public EnumSet getReasons() { + return reasons; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltManager.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltManager.java new file mode 100755 index 00000000000..2f1e66499d9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltManager.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.vm.ehalt; + +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +public class ExceptionalHaltManager { + + private static final List GLOBAL = + Arrays.asList( + new InvalidOperationExceptionalHaltPredicate(), + new StackOverflowExceptionalHaltPredicate(), + new StackUnderflowExceptionalHaltPredicate(), + new InsufficientGasExceptionalHaltPredicate()); + + public static EnumSet evaluateAll( + final MessageFrame frame, final EVM evm) { + final EnumSet answer = EnumSet.noneOf(ExceptionalHaltReason.class); + for (final ExceptionalHaltPredicate predicate : GLOBAL) { + predicate.exceptionalHaltCondition(frame, answer, evm).ifPresent(answer::add); + } + + // TODO: Determine whether or not to short-circuit here. + // Several operations (e.g. JUMP and JUMPI) have stack dependent checks. + + if (!answer.contains(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS)) { + // Evaluate any operation specific halt conditions too. + frame + .getCurrentOperation() + .exceptionalHaltCondition(frame, answer, evm) + .ifPresent(answer::add); + } + + return answer; + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltPredicate.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltPredicate.java new file mode 100755 index 00000000000..f824b68a293 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/ExceptionalHaltPredicate.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.ethereum.vm.ehalt; + +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +import java.util.EnumSet; +import java.util.Optional; + +public interface ExceptionalHaltPredicate { + + Optional exceptionalHaltCondition( + MessageFrame frame, EnumSet previousReasons, EVM evm); +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InsufficientGasExceptionalHaltPredicate.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InsufficientGasExceptionalHaltPredicate.java new file mode 100755 index 00000000000..2e9f1b9950e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InsufficientGasExceptionalHaltPredicate.java @@ -0,0 +1,34 @@ +package net.consensys.pantheon.ethereum.vm.ehalt; + +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +import java.util.EnumSet; +import java.util.Optional; + +public class InsufficientGasExceptionalHaltPredicate implements ExceptionalHaltPredicate { + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, final EnumSet prevReasons, final EVM evm) { + // Should not execute, as the cost function could depend on the stack items. + if (prevReasons.contains(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS)) { + return Optional.empty(); + } + + try { + return Optional.ofNullable(frame.getCurrentOperation().cost(frame)) + .filter(cost -> frame.getRemainingGas().compareTo(cost) < 0) + .map(cost -> ExceptionalHaltReason.INSUFFICIENT_GAS); + } catch (final IllegalArgumentException e) { + // TODO: Figure out a better way to handle gas overflows. + if (e.getMessage().contains("Gas too large")) { + return Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS); + } + + // throw e; + return Optional.of(ExceptionalHaltReason.INSUFFICIENT_GAS); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InvalidOperationExceptionalHaltPredicate.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InvalidOperationExceptionalHaltPredicate.java new file mode 100755 index 00000000000..fdc389f986d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/InvalidOperationExceptionalHaltPredicate.java @@ -0,0 +1,21 @@ +package net.consensys.pantheon.ethereum.vm.ehalt; + +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +import java.util.EnumSet; +import java.util.Optional; + +public class InvalidOperationExceptionalHaltPredicate implements ExceptionalHaltPredicate { + + private static final int INVALID_OPCODE = 0xfe; + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, final EnumSet prevReasons, final EVM evm) { + return frame.getCurrentOperation().getOpcode() == INVALID_OPCODE + ? Optional.of(ExceptionalHaltReason.INVALID_OPERATION) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackOverflowExceptionalHaltPredicate.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackOverflowExceptionalHaltPredicate.java new file mode 100755 index 00000000000..cf6adea387e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackOverflowExceptionalHaltPredicate.java @@ -0,0 +1,21 @@ +package net.consensys.pantheon.ethereum.vm.ehalt; + +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Operation; + +import java.util.EnumSet; +import java.util.Optional; + +public class StackOverflowExceptionalHaltPredicate implements ExceptionalHaltPredicate { + public static final int MAX_STACK_SIZE = 1024; + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, final EnumSet prevReasons, final EVM evm) { + final Operation op = frame.getCurrentOperation(); + final boolean condition = frame.stackSize() + op.getStackSizeChange() > MAX_STACK_SIZE; + return condition ? Optional.of(ExceptionalHaltReason.TOO_MANY_STACK_ITEMS) : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackUnderflowExceptionalHaltPredicate.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackUnderflowExceptionalHaltPredicate.java new file mode 100755 index 00000000000..ea8b174a49b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/ehalt/StackUnderflowExceptionalHaltPredicate.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.vm.ehalt; + +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Operation; + +import java.util.EnumSet; +import java.util.Optional; + +public class StackUnderflowExceptionalHaltPredicate implements ExceptionalHaltPredicate { + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, final EnumSet prevReasons, final EVM evm) { + final Operation op = frame.getCurrentOperation(); + final boolean condition = frame.stackSize() < op.getStackItemsConsumed(); + + return condition + ? Optional.of(ExceptionalHaltReason.INSUFFICIENT_STACK_ITEMS) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AbstractCreateOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AbstractCreateOperation.java new file mode 100755 index 00000000000..4c488dba57f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AbstractCreateOperation.java @@ -0,0 +1,137 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.Code; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Optional; + +public abstract class AbstractCreateOperation extends AbstractOperation { + + public AbstractCreateOperation( + final int opcode, + final String name, + final int stackItemsConsumed, + final int stackItemsProduced, + final boolean updatesProgramCounter, + final int opSize, + final GasCalculator gasCalculator) { + super( + opcode, + name, + stackItemsConsumed, + stackItemsProduced, + updatesProgramCounter, + opSize, + gasCalculator); + } + + @Override + public void execute(final MessageFrame frame) { + final Wei value = Wei.wrap(frame.getStackItem(0)); + + final Address address = frame.getRecipientAddress(); + final MutableAccount account = frame.getWorldState().getMutable(address); + + frame.clearReturnData(); + + if (value.compareTo(account.getBalance()) > 0 || frame.getMessageStackDepth() >= 1024) { + fail(frame); + } else { + spawnChildMessage(frame); + } + } + + protected abstract Address targetContractAddress(MessageFrame frame); + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + return frame.isStatic() + ? Optional.of(ExceptionalHaltReason.ILLEGAL_STATE_CHANGE) + : Optional.empty(); + } + + private void fail(final MessageFrame frame) { + final UInt256 inputOffset = frame.getStackItem(1).asUInt256(); + final UInt256 inputSize = frame.getStackItem(2).asUInt256(); + frame.readMemory(inputOffset, inputSize); + frame.popStackItems(getStackItemsConsumed()); + frame.pushStackItem(Bytes32.ZERO); + } + + private void spawnChildMessage(final MessageFrame frame) { + final Address address = frame.getRecipientAddress(); + final MutableAccount account = frame.getWorldState().getMutable(address); + account.incrementNonce(); + + final Wei value = Wei.wrap(frame.getStackItem(0)); + final UInt256 inputOffset = frame.getStackItem(1).asUInt256(); + final UInt256 inputSize = frame.getStackItem(2).asUInt256(); + final BytesValue inputData = frame.readMemory(inputOffset, inputSize); + + final Address contractAddress = targetContractAddress(frame); + + final Gas childGasStipend = gasCalculator().gasAvailableForChildCreate(frame.getRemainingGas()); + frame.decrementRemainingGas(childGasStipend); + + final MessageFrame childFrame = + MessageFrame.builder() + .type(MessageFrame.Type.CONTRACT_CREATION) + .messageFrameStack(frame.getMessageFrameStack()) + .blockchain(frame.getBlockchain()) + .worldState(frame.getWorldState().updater()) + .initialGas(childGasStipend) + .address(contractAddress) + .originator(frame.getOriginatorAddress()) + .contract(contractAddress) + .gasPrice(frame.getGasPrice()) + .inputData(BytesValue.EMPTY) + .sender(frame.getRecipientAddress()) + .value(value) + .apparentValue(value) + .code(new Code(inputData)) + .blockHeader(frame.getBlockHeader()) + .depth(frame.getMessageStackDepth() + 1) + .completer(child -> complete(frame, child)) + .build(); + + frame.getMessageFrameStack().addFirst(childFrame); + frame.setState(MessageFrame.State.CODE_SUSPENDED); + } + + private void complete(final MessageFrame frame, final MessageFrame childFrame) { + frame.setState(MessageFrame.State.CODE_EXECUTING); + + frame.incrementRemainingGas(childFrame.getRemainingGas()); + frame.addLogs(childFrame.getLogs()); + frame.addSelfDestructs(childFrame.getSelfDestructs()); + frame.incrementGasRefund(childFrame.getGasRefund()); + + frame.popStackItems(getStackItemsConsumed()); + + if (childFrame.getState() == MessageFrame.State.COMPLETED_SUCCESS) { + frame.pushStackItem(Words.fromAddress(childFrame.getContractAddress())); + } else { + frame.setReturnData(childFrame.getOutputData()); + frame.pushStackItem(Bytes32.ZERO); + } + + final int currentPC = frame.getPC(); + frame.setPC(currentPC + 1); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddModOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddModOperation.java new file mode 100755 index 00000000000..a17e1cb939b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddModOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class AddModOperation extends AbstractOperation { + + public AddModOperation(final GasCalculator gasCalculator) { + super(0x08, "ADDMOD", 3, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getMidTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + final UInt256 value2 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.plusModulo(value1, value2); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddOperation.java new file mode 100755 index 00000000000..38a5e1ea73c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class AddOperation extends AbstractOperation { + + public AddOperation(final GasCalculator gasCalculator) { + super(0x01, "ADD", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.plus(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddressOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddressOperation.java new file mode 100755 index 00000000000..462ead94664 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AddressOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; + +public class AddressOperation extends AbstractOperation { + + public AddressOperation(final GasCalculator gasCalculator) { + super(0x30, "ADDRESS", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Address address = frame.getRecipientAddress(); + frame.pushStackItem(Words.fromAddress(address)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AndOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AndOperation.java new file mode 100755 index 00000000000..e72ad827fa9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/AndOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class AndOperation extends AbstractOperation { + + public AndOperation(final GasCalculator gasCalculator) { + super(0x16, "AND", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.and(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BalanceOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BalanceOperation.java new file mode 100755 index 00000000000..74c81796ca6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BalanceOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.bytes.Bytes32; + +public class BalanceOperation extends AbstractOperation { + + public BalanceOperation(final GasCalculator gasCalculator) { + super(0x31, "BALANCE", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBalanceOperationGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Address accountAddress = Words.toAddress(frame.popStackItem()); + final Account account = frame.getWorldState().get(accountAddress); + frame.pushStackItem(account == null ? Bytes32.ZERO : account.getBalance().getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BlockHashOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BlockHashOperation.java new file mode 100755 index 00000000000..c9b76cbb669 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/BlockHashOperation.java @@ -0,0 +1,53 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Optional; + +public class BlockHashOperation extends AbstractOperation { + private static final int MAX_RELATIVE_BLOCK = 255; + + public BlockHashOperation(final GasCalculator gasCalculator) { + super(0x40, "BLOCKHASH", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBlockHashOperationGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 blockArg = frame.popStackItem().asUInt256(); + + // Short-circuit if value is unreasonably large + if (!blockArg.fitsLong()) { + frame.pushStackItem(Bytes32.ZERO); + return; + } + + final long soughtBlock = blockArg.toLong(); + final ProcessableBlockHeader blockHeader = frame.getBlockHeader(); + final long currentBlockNumber = blockHeader.getNumber(); + final long mostRecentBlockNumber = currentBlockNumber - 1; + + // If the current block is the genesis block or the sought block is + // not within the last 256 completed blocks, zero is returned. + if (currentBlockNumber == 0 + || soughtBlock < (mostRecentBlockNumber - MAX_RELATIVE_BLOCK) + || soughtBlock > mostRecentBlockNumber) { + frame.pushStackItem(Bytes32.ZERO); + } else { + final Optional maybeBlockHash = frame.getBlockchain().getBlockHashByNumber(soughtBlock); + assert maybeBlockHash.isPresent(); + frame.pushStackItem(maybeBlockHash.get()); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ByteOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ByteOperation.java new file mode 100755 index 00000000000..a7089c065fb --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ByteOperation.java @@ -0,0 +1,49 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.Counter; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Value; + +public class ByteOperation extends AbstractOperation { + + public ByteOperation(final GasCalculator gasCalculator) { + super(0x1A, "BYTE", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + private UInt256 getByte(final UInt256 seq, final UInt256 offset) { + if (!offset.fitsInt()) { + return UInt256.ZERO; + } + + final int index = offset.toInt(); + if (index >= 32) { + return UInt256.ZERO; + } + + final byte b = seq.getBytes().get(index); + final Counter res = UInt256.newCounter(); + res.getBytes().set(UInt256Value.SIZE - 1, b); + return res.get(); + } + + @Override + public void execute(final MessageFrame frame) { + + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + // Stack items are reversed for the BYTE operation. + final UInt256 result = getByte(value1, value0); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallCodeOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallCodeOperation.java new file mode 100755 index 00000000000..9da93a66720 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallCodeOperation.java @@ -0,0 +1,99 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractCallOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.uint.UInt256; + +public class CallCodeOperation extends AbstractCallOperation { + + public CallCodeOperation(final GasCalculator gasCalculator) { + super(0xF2, "CALLCODE", 7, 1, false, 1, gasCalculator); + } + + @Override + protected Gas gas(final MessageFrame frame) { + return Gas.of(frame.getStackItem(0)); + } + + @Override + protected Address to(final MessageFrame frame) { + return Words.toAddress(frame.getStackItem(1)); + } + + @Override + protected Wei value(final MessageFrame frame) { + return Wei.wrap(frame.getStackItem(2)); + } + + @Override + protected Wei apparentValue(final MessageFrame frame) { + return value(frame); + } + + @Override + protected UInt256 inputDataOffset(final MessageFrame frame) { + return frame.getStackItem(3).asUInt256(); + } + + @Override + protected UInt256 inputDataLength(final MessageFrame frame) { + return frame.getStackItem(4).asUInt256(); + } + + @Override + protected UInt256 outputDataOffset(final MessageFrame frame) { + return frame.getStackItem(5).asUInt256(); + } + + @Override + protected UInt256 outputDataLength(final MessageFrame frame) { + return frame.getStackItem(6).asUInt256(); + } + + @Override + protected Address address(final MessageFrame frame) { + return frame.getRecipientAddress(); + } + + @Override + protected Address sender(final MessageFrame frame) { + return frame.getRecipientAddress(); + } + + @Override + public Gas gasAvailableForChildCall(final MessageFrame frame) { + return gasCalculator().gasAvailableForChildCall(frame, gas(frame), !value(frame).isZero()); + } + + @Override + protected boolean isStatic(final MessageFrame frame) { + return frame.isStatic(); + } + + @Override + public Gas cost(final MessageFrame frame) { + final Gas stipend = gas(frame); + final UInt256 inputDataOffset = inputDataOffset(frame).asUInt256(); + final UInt256 inputDataLength = inputDataLength(frame).asUInt256(); + final UInt256 outputDataOffset = outputDataOffset(frame).asUInt256(); + final UInt256 outputDataLength = outputDataLength(frame).asUInt256(); + final Account recipient = frame.getWorldState().get(address(frame)); + + return gasCalculator() + .callOperationGasCost( + frame, + stipend, + inputDataOffset, + inputDataLength, + outputDataOffset, + outputDataLength, + value(frame), + recipient); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataCopyOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataCopyOperation.java new file mode 100755 index 00000000000..cc6c510bb0e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataCopyOperation.java @@ -0,0 +1,34 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +public class CallDataCopyOperation extends AbstractOperation { + + public CallDataCopyOperation(final GasCalculator gasCalculator) { + super(0x37, "CALLDATACOPY", 3, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + final UInt256 length = frame.getStackItem(2).asUInt256(); + + return gasCalculator().dataCopyOperationGasCost(frame, offset, length); + } + + @Override + public void execute(final MessageFrame frame) { + final BytesValue callData = frame.getInputData(); + + final UInt256 memOffset = frame.popStackItem().asUInt256(); + final UInt256 sourceOffset = frame.popStackItem().asUInt256(); + final UInt256 numBytes = frame.popStackItem().asUInt256(); + + frame.writeMemory(memOffset, sourceOffset, numBytes, callData); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataLoadOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataLoadOperation.java new file mode 100755 index 00000000000..032d5a717ad --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataLoadOperation.java @@ -0,0 +1,43 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class CallDataLoadOperation extends AbstractOperation { + + public CallDataLoadOperation(final GasCalculator gasCalculator) { + super(0x35, "CALLDATALOAD", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 startWord = frame.popStackItem().asUInt256(); + + // If the start index doesn't fit a int, it comes after anything in data, and so the returned + // word should be zero. + if (!startWord.fitsInt()) { + frame.pushStackItem(Bytes32.ZERO); + return; + } + + final int offset = startWord.toInt(); + final BytesValue data = frame.getInputData(); + final MutableBytes32 res = MutableBytes32.create(); + if (offset < data.size()) { + final BytesValue toCopy = data.slice(offset, Math.min(Bytes32.SIZE, data.size() - offset)); + toCopy.copyTo(res, 0); + } + frame.pushStackItem(res); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataSizeOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataSizeOperation.java new file mode 100755 index 00000000000..d12b000bd69 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallDataSizeOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class CallDataSizeOperation extends AbstractOperation { + + public CallDataSizeOperation(final GasCalculator gasCalculator) { + super(0x36, "CALLDATASIZE", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final BytesValue callData = frame.getInputData(); + frame.pushStackItem(UInt256Bytes.of(callData.size())); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallOperation.java new file mode 100755 index 00000000000..5222a84df78 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallOperation.java @@ -0,0 +1,114 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractCallOperation; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Optional; + +public class CallOperation extends AbstractCallOperation { + + public CallOperation(final GasCalculator gasCalculator) { + super(0xF1, "CALL", 7, 1, false, 1, gasCalculator); + } + + @Override + protected Gas gas(final MessageFrame frame) { + return Gas.of(frame.getStackItem(0)); + } + + @Override + protected Address to(final MessageFrame frame) { + return Words.toAddress(frame.getStackItem(1)); + } + + @Override + protected Wei value(final MessageFrame frame) { + return Wei.wrap(frame.getStackItem(2)); + } + + @Override + protected Wei apparentValue(final MessageFrame frame) { + return value(frame); + } + + @Override + protected UInt256 inputDataOffset(final MessageFrame frame) { + return frame.getStackItem(3).asUInt256(); + } + + @Override + protected UInt256 inputDataLength(final MessageFrame frame) { + return frame.getStackItem(4).asUInt256(); + } + + @Override + protected UInt256 outputDataOffset(final MessageFrame frame) { + return frame.getStackItem(5).asUInt256(); + } + + @Override + protected UInt256 outputDataLength(final MessageFrame frame) { + return frame.getStackItem(6).asUInt256(); + } + + @Override + protected Address address(final MessageFrame frame) { + return to(frame); + } + + @Override + protected Address sender(final MessageFrame frame) { + return frame.getRecipientAddress(); + } + + @Override + public Gas gasAvailableForChildCall(final MessageFrame frame) { + return gasCalculator().gasAvailableForChildCall(frame, gas(frame), !value(frame).isZero()); + } + + @Override + protected boolean isStatic(final MessageFrame frame) { + return frame.isStatic(); + } + + @Override + public Gas cost(final MessageFrame frame) { + final Gas stipend = gas(frame); + final UInt256 inputDataOffset = inputDataOffset(frame).asUInt256(); + final UInt256 inputDataLength = inputDataLength(frame).asUInt256(); + final UInt256 outputDataOffset = outputDataOffset(frame).asUInt256(); + final UInt256 outputDataLength = outputDataLength(frame).asUInt256(); + final Account recipient = frame.getWorldState().get(address(frame)); + + return gasCalculator() + .callOperationGasCost( + frame, + stipend, + inputDataOffset, + inputDataLength, + outputDataOffset, + outputDataLength, + value(frame), + recipient); + } + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + return frame.isStatic() && !value(frame).isZero() + ? Optional.of(ExceptionalHaltReason.ILLEGAL_STATE_CHANGE) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallValueOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallValueOperation.java new file mode 100755 index 00000000000..aadddaebf0b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallValueOperation.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +public class CallValueOperation extends AbstractOperation { + + public CallValueOperation(final GasCalculator gasCalculator) { + super(0x34, "CALLVALUE", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Wei value = frame.getApparentValue(); + frame.pushStackItem(value.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallerOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallerOperation.java new file mode 100755 index 00000000000..b549caf3e8b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CallerOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; + +public class CallerOperation extends AbstractOperation { + + public CallerOperation(final GasCalculator gasCalculator) { + super(0x33, "CALLER", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Address callerAddress = frame.getSenderAddress(); + frame.pushStackItem(Words.fromAddress(callerAddress)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeCopyOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeCopyOperation.java new file mode 100755 index 00000000000..80a14b8b1f9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeCopyOperation.java @@ -0,0 +1,34 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.Code; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class CodeCopyOperation extends AbstractOperation { + + public CodeCopyOperation(final GasCalculator gasCalculator) { + super(0x39, "CODECOPY", 3, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + final UInt256 length = frame.getStackItem(2).asUInt256(); + + return gasCalculator().dataCopyOperationGasCost(frame, offset, length); + } + + @Override + public void execute(final MessageFrame frame) { + final Code code = frame.getCode(); + + final UInt256 memOffset = frame.popStackItem().asUInt256(); + final UInt256 sourceOffset = frame.popStackItem().asUInt256(); + final UInt256 numBytes = frame.popStackItem().asUInt256(); + + frame.writeMemory(memOffset, sourceOffset, numBytes, code.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeSizeOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeSizeOperation.java new file mode 100755 index 00000000000..e4b7c6b4aaa --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CodeSizeOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.Code; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class CodeSizeOperation extends AbstractOperation { + + public CodeSizeOperation(final GasCalculator gasCalculator) { + super(0x38, "CODESIZE", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Code code = frame.getCode(); + frame.pushStackItem(UInt256Bytes.of(code.getSize())); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CoinbaseOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CoinbaseOperation.java new file mode 100755 index 00000000000..a79ff51ba23 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CoinbaseOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; + +public class CoinbaseOperation extends AbstractOperation { + + public CoinbaseOperation(final GasCalculator gasCalculator) { + super(0x41, "COINBASE", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Address coinbase = frame.getBlockHeader().getCoinbase(); + frame.pushStackItem(Words.fromAddress(coinbase)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Create2Operation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Create2Operation.java new file mode 100755 index 00000000000..935d11c6995 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Create2Operation.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +public class Create2Operation extends AbstractCreateOperation { + + private static final BytesValue PREFIX = BytesValue.fromHexString("0xFF"); + + public Create2Operation(final GasCalculator gasCalculator) { + super(0xF5, "CREATE2", 4, 1, false, 1, gasCalculator); + } + + @Override + protected Address targetContractAddress(final MessageFrame frame) { + final Address sender = frame.getSenderAddress(); + final UInt256 offset = frame.getStackItem(1).asUInt256(); + final UInt256 length = frame.getStackItem(2).asUInt256(); + final Bytes32 salt = frame.getStackItem(3); + final BytesValue initCode = frame.readMemory(offset, length); + final Hash hash = Hash.hash(PREFIX.concat(sender).concat(salt).concat(Hash.hash(initCode))); + return Address.extract(hash); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().create2OperationGasCost(frame); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CreateOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CreateOperation.java new file mode 100755 index 00000000000..868088a0411 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/CreateOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +public class CreateOperation extends AbstractCreateOperation { + + public CreateOperation(final GasCalculator gasCalculator) { + super(0xF0, "CREATE", 3, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().createOperationGasCost(frame); + } + + @Override + protected Address targetContractAddress(final MessageFrame frame) { + final Account sender = frame.getWorldState().get(frame.getRecipientAddress()); + // Decrement nonce by 1 to normalize the effect of transaction execution + return Address.contractAddress(frame.getRecipientAddress(), sender.getNonce() - 1L); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DelegateCallOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DelegateCallOperation.java new file mode 100755 index 00000000000..2992e0fc3ef --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DelegateCallOperation.java @@ -0,0 +1,99 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractCallOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.uint.UInt256; + +public class DelegateCallOperation extends AbstractCallOperation { + + public DelegateCallOperation(final GasCalculator gasCalculator) { + super(0xF4, "DELEGATECALL", 6, 1, false, 1, gasCalculator); + } + + @Override + protected Gas gas(final MessageFrame frame) { + return Gas.of(frame.getStackItem(0)); + } + + @Override + protected Address to(final MessageFrame frame) { + return Words.toAddress(frame.getStackItem(1)); + } + + @Override + protected Wei value(final MessageFrame frame) { + return Wei.ZERO; + } + + @Override + protected Wei apparentValue(final MessageFrame frame) { + return frame.getApparentValue(); + } + + @Override + protected UInt256 inputDataOffset(final MessageFrame frame) { + return frame.getStackItem(2).asUInt256(); + } + + @Override + protected UInt256 inputDataLength(final MessageFrame frame) { + return frame.getStackItem(3).asUInt256(); + } + + @Override + protected UInt256 outputDataOffset(final MessageFrame frame) { + return frame.getStackItem(4).asUInt256(); + } + + @Override + protected UInt256 outputDataLength(final MessageFrame frame) { + return frame.getStackItem(5).asUInt256(); + } + + @Override + protected Address address(final MessageFrame frame) { + return frame.getRecipientAddress(); + } + + @Override + protected Address sender(final MessageFrame frame) { + return frame.getSenderAddress(); + } + + @Override + public Gas gasAvailableForChildCall(final MessageFrame frame) { + return gasCalculator().gasAvailableForChildCall(frame, gas(frame), false); + } + + @Override + protected boolean isStatic(final MessageFrame frame) { + return frame.isStatic(); + } + + @Override + public Gas cost(final MessageFrame frame) { + final Gas stipend = gas(frame); + final UInt256 inputDataOffset = inputDataOffset(frame).asUInt256(); + final UInt256 inputDataLength = inputDataLength(frame).asUInt256(); + final UInt256 outputDataOffset = outputDataOffset(frame).asUInt256(); + final UInt256 outputDataLength = outputDataLength(frame).asUInt256(); + final Account recipient = frame.getWorldState().get(address(frame)); + + return gasCalculator() + .callOperationGasCost( + frame, + stipend, + inputDataOffset, + inputDataLength, + outputDataOffset, + outputDataLength, + Wei.ZERO, + recipient); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DifficultyOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DifficultyOperation.java new file mode 100755 index 00000000000..dfa415f29ad --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DifficultyOperation.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class DifficultyOperation extends AbstractOperation { + + public DifficultyOperation(final GasCalculator gasCalculator) { + super(0x44, "DIFFICULTY", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 difficulty = frame.getBlockHeader().getDifficulty(); + frame.pushStackItem(difficulty.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DivOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DivOperation.java new file mode 100755 index 00000000000..ccbe0655128 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DivOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class DivOperation extends AbstractOperation { + + public DivOperation(final GasCalculator gasCalculator) { + super(0x04, "DIV", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.dividedBy(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DupOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DupOperation.java new file mode 100755 index 00000000000..3a92f026b84 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/DupOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +public class DupOperation extends AbstractOperation { + + private final int index; + + public DupOperation(final int index, final GasCalculator gasCalculator) { + super(0x80 + index - 1, "DUP" + index, index, index + 1, false, 1, gasCalculator); + this.index = index; + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + frame.pushStackItem(frame.getStackItem(index - 1)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/EqOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/EqOperation.java new file mode 100755 index 00000000000..4d0c99f1e0f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/EqOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class EqOperation extends AbstractOperation { + + public EqOperation(final GasCalculator gasCalculator) { + super(0x14, "EQ", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final Bytes32 result = value0.equals(value1) ? Bytes32.TRUE : Bytes32.FALSE; + + frame.pushStackItem(result); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExpOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExpOperation.java new file mode 100755 index 00000000000..77aa872a49e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExpOperation.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class ExpOperation extends AbstractOperation { + + public ExpOperation(final GasCalculator gasCalculator) { + super(0x0A, "EXP", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 power = frame.getStackItem(1).asUInt256(); + + final int numBytes = (power.bitLength() + 7) / 8; + return gasCalculator().expOperationGasCost(numBytes); + // return FrontierGasCosts.EXP.plus(FrontierGasCosts.EXP_BYTE.times(numBytes)); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.pow(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeCopyOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeCopyOperation.java new file mode 100755 index 00000000000..25383b68894 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeCopyOperation.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +public class ExtCodeCopyOperation extends AbstractOperation { + + public ExtCodeCopyOperation(final GasCalculator gasCalculator) { + super(0x3C, "EXTCODECOPY", 4, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(1).asUInt256(); + final UInt256 length = frame.getStackItem(3).asUInt256(); + + return gasCalculator().extCodeCopyOperationGasCost(frame, offset, length); + } + + @Override + public void execute(final MessageFrame frame) { + final Address address = Words.toAddress(frame.popStackItem()); + final Account account = frame.getWorldState().get(address); + final BytesValue code = account != null ? account.getCode() : BytesValue.EMPTY; + + final UInt256 memOffset = frame.popStackItem().asUInt256(); + final UInt256 sourceOffset = frame.popStackItem().asUInt256(); + final UInt256 numBytes = frame.popStackItem().asUInt256(); + + frame.writeMemory(memOffset, sourceOffset, numBytes, code); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeSizeOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeSizeOperation.java new file mode 100755 index 00000000000..4c433fdee89 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ExtCodeSizeOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class ExtCodeSizeOperation extends AbstractOperation { + + public ExtCodeSizeOperation(final GasCalculator gasCalculator) { + super(0x3B, "EXTCODESIZE", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getExtCodeSizeOperationGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Address address = Words.toAddress(frame.popStackItem()); + final Account account = frame.getWorldState().get(address); + frame.pushStackItem(account == null ? Bytes32.ZERO : UInt256Bytes.of(account.getCode().size())); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasLimitOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasLimitOperation.java new file mode 100755 index 00000000000..d0707a60516 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasLimitOperation.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class GasLimitOperation extends AbstractOperation { + + public GasLimitOperation(final GasCalculator gasCalculator) { + super(0x45, "GASLIMIT", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Gas gasLimit = Gas.of(frame.getBlockHeader().getGasLimit()); + final Bytes32 value = Bytes32.leftPad(BytesValue.of(gasLimit.getBytes())); + frame.pushStackItem(value); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasOperation.java new file mode 100755 index 00000000000..0851cd3c511 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasOperation.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class GasOperation extends AbstractOperation { + + public GasOperation(final GasCalculator gasCalculator) { + super(0x5A, "GAS", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Gas gasRemaining = frame.getRemainingGas(); + final Bytes32 value = Bytes32.leftPad(BytesValue.of(gasRemaining.getBytes())); + frame.pushStackItem(value); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasPriceOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasPriceOperation.java new file mode 100755 index 00000000000..81f1093a1d4 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GasPriceOperation.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +public class GasPriceOperation extends AbstractOperation { + + public GasPriceOperation(final GasCalculator gasCalculator) { + super(0x3A, "GASPRICE", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Wei gasPrice = frame.getGasPrice(); + frame.pushStackItem(gasPrice.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GtOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GtOperation.java new file mode 100755 index 00000000000..6462dbada42 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/GtOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class GtOperation extends AbstractOperation { + + public GtOperation(final GasCalculator gasCalculator) { + super(0x11, "GT", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final Bytes32 result = value0.compareTo(value1) > 0 ? Bytes32.TRUE : Bytes32.FALSE; + + frame.pushStackItem(result); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/InvalidOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/InvalidOperation.java new file mode 100755 index 00000000000..fd7e89363db --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/InvalidOperation.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +public class InvalidOperation extends AbstractOperation { + + public InvalidOperation(final GasCalculator gasCalculator) { + super(0xFE, "INVALID", -1, -1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return null; + } + + @Override + public void execute(final MessageFrame frame) { + frame.setState(MessageFrame.State.EXCEPTIONAL_HALT); + frame.getExceptionalHaltReasons().add(ExceptionalHaltReason.INVALID_OPERATION); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/IsZeroOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/IsZeroOperation.java new file mode 100755 index 00000000000..0d04750e5a3 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/IsZeroOperation.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class IsZeroOperation extends AbstractOperation { + + public IsZeroOperation(final GasCalculator gasCalculator) { + super(0x15, "ISZERO", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value = frame.popStackItem().asUInt256(); + + frame.pushStackItem(value.isZero() ? Bytes32.TRUE : Bytes32.FALSE); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpDestOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpDestOperation.java new file mode 100755 index 00000000000..6d54450a3bd --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpDestOperation.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +public class JumpDestOperation extends AbstractOperation { + + public static final int OPCODE = 0x5B; + + public JumpDestOperation(final GasCalculator gasCalculator) { + super(OPCODE, "JUMPDEST", 0, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getJumpDestOperationGasCost(); + } + + @Override + public void execute(final MessageFrame frame) {} +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpOperation.java new file mode 100755 index 00000000000..487938d09f2 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpOperation.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.Code; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Optional; + +public class JumpOperation extends AbstractOperation { + + public JumpOperation(final GasCalculator gasCalculator) { + super(0x56, "JUMP", 1, 0, true, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getMidTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 jumpDestination = frame.popStackItem().asUInt256(); + frame.setPC(jumpDestination.toInt()); + } + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + final Code code = frame.getCode(); + + final UInt256 potentialJumpDestination = frame.getStackItem(0).asUInt256(); + return !code.isValidJumpDestination(evm, potentialJumpDestination) + ? Optional.of(ExceptionalHaltReason.INVALID_JUMP_DESTINATION) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpiOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpiOperation.java new file mode 100755 index 00000000000..5330885c903 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/JumpiOperation.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.Code; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Optional; + +public class JumpiOperation extends AbstractOperation { + + public JumpiOperation(final GasCalculator gasCalculator) { + super(0x57, "JUMPI", 2, 0, true, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getHighTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 jumpDestination = frame.popStackItem().asUInt256(); + final Bytes32 condition = frame.popStackItem(); + + if (!condition.isZero()) { + frame.setPC(jumpDestination.toInt()); + } else { + frame.setPC(frame.getPC() + getOpSize()); + } + } + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + // If condition is zero (false), no jump is will be performed. Therefore skip the test. + if (frame.getStackItem(1).isZero()) { + return Optional.empty(); + } + + final Code code = frame.getCode(); + final UInt256 potentialJumpDestination = frame.getStackItem(0).asUInt256(); + return !code.isValidJumpDestination(evm, potentialJumpDestination) + ? Optional.of(ExceptionalHaltReason.INVALID_JUMP_DESTINATION) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LogOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LogOperation.java new file mode 100755 index 00000000000..af1f22a6dbc --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LogOperation.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.LogTopic; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Optional; + +import com.google.common.collect.ImmutableList; + +public class LogOperation extends AbstractOperation { + + private final int numTopics; + + public LogOperation(final int numTopics, final GasCalculator gasCalculator) { + super(0xA0 + numTopics, "LOG" + numTopics, numTopics + 2, 0, false, 1, gasCalculator); + this.numTopics = numTopics; + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 dataOffset = frame.getStackItem(0).asUInt256(); + final UInt256 dataLength = frame.getStackItem(1).asUInt256(); + + return gasCalculator().logOperationGasCost(frame, dataOffset, dataLength, numTopics); + } + + @Override + public void execute(final MessageFrame frame) { + final Address address = frame.getRecipientAddress(); + + final UInt256 dataLocation = frame.popStackItem().asUInt256(); + final UInt256 numBytes = frame.popStackItem().asUInt256(); + final BytesValue data = frame.readMemory(dataLocation, numBytes); + + final ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(numTopics); + for (int i = 0; i < numTopics; i++) { + builder.add(LogTopic.of(frame.popStackItem())); + } + + frame.addLog(new Log(address, data, builder.build())); + } + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + return frame.isStatic() + ? Optional.of(ExceptionalHaltReason.ILLEGAL_STATE_CHANGE) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LtOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LtOperation.java new file mode 100755 index 00000000000..2424a9a4c66 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/LtOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class LtOperation extends AbstractOperation { + + public LtOperation(final GasCalculator gasCalculator) { + super(0x10, "LT", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final Bytes32 result = value0.compareTo(value1) < 0 ? Bytes32.TRUE : Bytes32.FALSE; + + frame.pushStackItem(result); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MLoadOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MLoadOperation.java new file mode 100755 index 00000000000..4bd02499f61 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MLoadOperation.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class MLoadOperation extends AbstractOperation { + + public MLoadOperation(final GasCalculator gasCalculator) { + super(0x51, "MLOAD", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + + return gasCalculator().mLoadOperationGasCost(frame, offset); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 location = frame.popStackItem().asUInt256(); + + final Bytes32 value = Bytes32.leftPad(frame.readMemory(location, UInt256.U_32)); + + frame.pushStackItem(value); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MSizeOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MSizeOperation.java new file mode 100755 index 00000000000..5195b6a3ec0 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MSizeOperation.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class MSizeOperation extends AbstractOperation { + + public MSizeOperation(final GasCalculator gasCalculator) { + super(0x59, "MSIZE", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + frame.pushStackItem(UInt256Bytes.of(frame.memoryByteSize())); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStore8Operation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStore8Operation.java new file mode 100755 index 00000000000..2c688249173 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStore8Operation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class MStore8Operation extends AbstractOperation { + + public MStore8Operation(final GasCalculator gasCalculator) { + super(0x53, "MSTORE8", 2, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + + return gasCalculator().mStore8OperationGasCost(frame, offset); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 location = frame.popStackItem().asUInt256(); + final Bytes32 value = frame.popStackItem(); + + frame.writeMemory(location, value.get(Bytes32.SIZE - 1)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStoreOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStoreOperation.java new file mode 100755 index 00000000000..baa1dba8d91 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MStoreOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +public class MStoreOperation extends AbstractOperation { + + public MStoreOperation(final GasCalculator gasCalculator) { + super(0x52, "MSTORE", 2, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + + return gasCalculator().mStoreOperationGasCost(frame, offset); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 location = frame.popStackItem().asUInt256(); + final Bytes32 value = frame.popStackItem(); + + frame.writeMemory(location, UInt256.U_32, value); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ModOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ModOperation.java new file mode 100755 index 00000000000..a8a373fee6a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ModOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class ModOperation extends AbstractOperation { + + public ModOperation(final GasCalculator gasCalculator) { + super(0x06, "MOD", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.mod(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulModOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulModOperation.java new file mode 100755 index 00000000000..6db1377f25e --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulModOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class MulModOperation extends AbstractOperation { + + public MulModOperation(final GasCalculator gasCalculator) { + super(0x09, "MULMOD", 3, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getMidTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + final UInt256 value2 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.timesModulo(value1, value2); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulOperation.java new file mode 100755 index 00000000000..77c5e4a6faf --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/MulOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class MulOperation extends AbstractOperation { + + public MulOperation(final GasCalculator gasCalculator) { + super(0x02, "MUL", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.times(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NotOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NotOperation.java new file mode 100755 index 00000000000..402e3d5ee24 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NotOperation.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class NotOperation extends AbstractOperation { + + public NotOperation(final GasCalculator gasCalculator) { + super(0x19, "NOT", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value = frame.popStackItem().asUInt256(); + + final UInt256 result = value.not(); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NumberOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NumberOperation.java new file mode 100755 index 00000000000..3d81ae5edb3 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/NumberOperation.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class NumberOperation extends AbstractOperation { + + public NumberOperation(final GasCalculator gasCalculator) { + super(0x43, "NUMBER", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final long number = frame.getBlockHeader().getNumber(); + frame.pushStackItem(UInt256Bytes.of(number)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OrOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OrOperation.java new file mode 100755 index 00000000000..3ba61125d7f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OrOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class OrOperation extends AbstractOperation { + + public OrOperation(final GasCalculator gasCalculator) { + super(0x17, "OR", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.or(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OriginOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OriginOperation.java new file mode 100755 index 00000000000..e6e723bd38f --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/OriginOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; + +public class OriginOperation extends AbstractOperation { + + public OriginOperation(final GasCalculator gasCalculator) { + super(0x32, "ORIGIN", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Address originAddress = frame.getOriginatorAddress(); + frame.pushStackItem(Words.fromAddress(originAddress)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PCOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PCOperation.java new file mode 100755 index 00000000000..7b0eb2f5edc --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PCOperation.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class PCOperation extends AbstractOperation { + + public PCOperation(final GasCalculator gasCalculator) { + super(0x58, "PC", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + frame.pushStackItem(UInt256Bytes.of(frame.getPC())); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PopOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PopOperation.java new file mode 100755 index 00000000000..d080e992263 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PopOperation.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; + +public class PopOperation extends AbstractOperation { + + public PopOperation(final GasCalculator gasCalculator) { + super(0x50, "POP", 1, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + frame.popStackItem(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PushOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PushOperation.java new file mode 100755 index 00000000000..bd26bb3ab55 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/PushOperation.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static java.lang.Math.min; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytes32; + +public class PushOperation extends AbstractOperation { + + private final int length; + + public PushOperation(final int length, final GasCalculator gasCalculator) { + super(0x60 + length - 1, "PUSH" + length, 0, 1, false, length + 1, gasCalculator); + this.length = length; + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final int pc = frame.getPC(); + final BytesValue code = frame.getCode().getBytes(); + + final int copyLength = min(length, code.size() - pc - 1); + final MutableBytes32 bytes = MutableBytes32.create(); + code.slice(pc + 1, copyLength).copyTo(bytes, bytes.size() - length); + frame.pushStackItem(bytes); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataCopyOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataCopyOperation.java new file mode 100755 index 00000000000..051f2c9f761 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataCopyOperation.java @@ -0,0 +1,59 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Optional; + +public class ReturnDataCopyOperation extends AbstractOperation { + + public ReturnDataCopyOperation(final GasCalculator gasCalculator) { + super(0x3E, "RETURNDATACOPY", 3, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + final UInt256 length = frame.getStackItem(2).asUInt256(); + + return gasCalculator().dataCopyOperationGasCost(frame, offset, length); + } + + @Override + public void execute(final MessageFrame frame) { + final BytesValue returnData = frame.getReturnData(); + + final UInt256 memOffset = frame.popStackItem().asUInt256(); + final UInt256 sourceOffset = frame.popStackItem().asUInt256(); + final UInt256 numBytes = frame.popStackItem().asUInt256(); + + frame.writeMemory(memOffset, sourceOffset, numBytes, returnData); + } + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + final BytesValue returnData = frame.getReturnData(); + + final UInt256 start = frame.getStackItem(1).asUInt256(); + final UInt256 length = frame.getStackItem(2).asUInt256(); + final UInt256 returnDataLength = UInt256.of(returnData.size()); + + if (!start.fitsInt() + || !length.fitsInt() + || start.plus(length).compareTo(returnDataLength) > 0) { + return Optional.of(ExceptionalHaltReason.INVALID_RETURN_DATA_BUFFER_ACCESS); + } else { + return Optional.empty(); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataSizeOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataSizeOperation.java new file mode 100755 index 00000000000..f6cc6e0124c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnDataSizeOperation.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class ReturnDataSizeOperation extends AbstractOperation { + + public ReturnDataSizeOperation(final GasCalculator gasCalculator) { + super(0x3D, "RETURNDATASIZE", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final BytesValue returnData = frame.getReturnData(); + frame.pushStackItem(UInt256Bytes.of(returnData.size())); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnOperation.java new file mode 100755 index 00000000000..d6e502acf65 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ReturnOperation.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class ReturnOperation extends AbstractOperation { + + public ReturnOperation(final GasCalculator gasCalculator) { + super(0xF3, "RETURN", 2, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + final UInt256 length = frame.getStackItem(1).asUInt256(); + + return gasCalculator().memoryExpansionGasCost(frame, offset, length); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 from = frame.popStackItem().asUInt256(); + final UInt256 length = frame.popStackItem().asUInt256(); + + frame.setOutputData(frame.readMemory(from, length)); + frame.setState(MessageFrame.State.CODE_SUCCESS); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/RevertOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/RevertOperation.java new file mode 100755 index 00000000000..b9098e0792d --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/RevertOperation.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class RevertOperation extends AbstractOperation { + + public RevertOperation(final GasCalculator gasCalculator) { + super(0xFD, "REVERT", 2, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + final UInt256 length = frame.getStackItem(1).asUInt256(); + + return gasCalculator().memoryExpansionGasCost(frame, offset, length); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 from = frame.popStackItem().asUInt256(); + final UInt256 length = frame.popStackItem().asUInt256(); + + frame.setOutputData(frame.readMemory(from, length)); + frame.setState(MessageFrame.State.REVERT); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SDivOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SDivOperation.java new file mode 100755 index 00000000000..37d1cc852b8 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SDivOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.Int256; + +public class SDivOperation extends AbstractOperation { + + public SDivOperation(final GasCalculator gasCalculator) { + super(0x05, "SDIV", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Int256 value0 = frame.popStackItem().asInt256(); + final Int256 value1 = frame.popStackItem().asInt256(); + + final Int256 result = value0.dividedBy(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SGtOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SGtOperation.java new file mode 100755 index 00000000000..abd4cc0c74c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SGtOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.Int256; + +public class SGtOperation extends AbstractOperation { + + public SGtOperation(final GasCalculator gasCalculator) { + super(0x13, "SGT", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Int256 value0 = frame.popStackItem().asInt256(); + final Int256 value1 = frame.popStackItem().asInt256(); + + final Bytes32 result = value0.compareTo(value1) > 0 ? Bytes32.TRUE : Bytes32.FALSE; + + frame.pushStackItem(result); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLoadOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLoadOperation.java new file mode 100755 index 00000000000..4d57c96fbd4 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLoadOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class SLoadOperation extends AbstractOperation { + + public SLoadOperation(final GasCalculator gasCalculator) { + super(0x54, "SLOAD", 1, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getSloadOperationGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 key = frame.popStackItem().asUInt256(); + + final Account account = frame.getWorldState().get(frame.getRecipientAddress()); + assert account != null : "VM account should exists"; + + frame.pushStackItem(account.getStorageValue(key).getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLtOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLtOperation.java new file mode 100755 index 00000000000..240bcdf7e8b --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SLtOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.Int256; + +public class SLtOperation extends AbstractOperation { + + public SLtOperation(final GasCalculator gasCalculator) { + super(0x12, "SLT", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Int256 value0 = frame.popStackItem().asInt256(); + final Int256 value1 = frame.popStackItem().asInt256(); + + final Bytes32 result = value0.compareTo(value1) < 0 ? Bytes32.TRUE : Bytes32.FALSE; + + frame.pushStackItem(result); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SModOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SModOperation.java new file mode 100755 index 00000000000..d41855be0e6 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SModOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.Int256; + +public class SModOperation extends AbstractOperation { + + public SModOperation(final GasCalculator gasCalculator) { + super(0x07, "SMOD", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Int256 value0 = frame.popStackItem().asInt256(); + final Int256 value1 = frame.popStackItem().asInt256(); + + final Int256 result = value0.mod(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SStoreOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SStoreOperation.java new file mode 100755 index 00000000000..a3f23286c8c --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SStoreOperation.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.EnumSet; +import java.util.Optional; + +public class SStoreOperation extends AbstractOperation { + + public SStoreOperation(final GasCalculator gasCalculator) { + super(0x55, "SSTORE", 2, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 key = frame.getStackItem(0).asUInt256(); + final UInt256 value = frame.getStackItem(1).asUInt256(); + + final Account account = frame.getWorldState().get(frame.getRecipientAddress()); + // Setting storage value to non-zero from zero (i.e. nothing currently at this location) vs. + // resetting an existing value. + final UInt256 storedValue = account.getStorageValue(key); + + if (!value.isZero() && storedValue.isZero()) { + return gasCalculator().getStorageSetGasCost(); + } else { + return gasCalculator().getStorageResetGasCost(); + } + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 key = frame.popStackItem().asUInt256(); + final UInt256 value = frame.popStackItem().asUInt256(); + + final MutableAccount account = frame.getWorldState().getMutable(frame.getRecipientAddress()); + assert account != null : "VM account should exists"; + + // Increment the refund counter. + if (value.isZero() && !account.getStorageValue(key).isZero()) { + frame.incrementGasRefund(gasCalculator().getStorageResetRefundAmount()); + } + + account.setStorageValue(key.copy(), value.copy()); + } + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + return frame.isStatic() + ? Optional.of(ExceptionalHaltReason.ILLEGAL_STATE_CHANGE) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SarOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SarOperation.java new file mode 100755 index 00000000000..e882ba1a4f4 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SarOperation.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static net.consensys.pantheon.util.uint.UInt256s.greaterThanOrEqualTo256; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.Bytes32s; +import net.consensys.pantheon.util.uint.UInt256; + +public class SarOperation extends AbstractOperation { + + private static final Bytes32 ALL_BITS = + Bytes32.fromHexString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + public SarOperation(final GasCalculator gasCalculator) { + super(0x1d, "SAR", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 shiftAmount = frame.popStackItem().asUInt256(); + Bytes32 value = frame.popStackItem(); + + final boolean negativeNumber = value.get(0) < 0; + + // short circuit result if we are shifting more than the width of the data. + if (greaterThanOrEqualTo256(shiftAmount)) { + final Bytes32 overflow = negativeNumber ? ALL_BITS : Bytes32.ZERO; + frame.pushStackItem(overflow); + return; + } + + // first perform standard shift right. + value = Bytes32s.shiftRight(value, shiftAmount.toInt()); + + // if a negative number, carry through the sign. + if (negativeNumber) { + final Bytes32 significantBits = Bytes32s.shiftLeft(ALL_BITS, 256 - shiftAmount.toInt()); + value = Bytes32s.or(value, significantBits); + } + frame.pushStackItem(value); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SelfDestructOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SelfDestructOperation.java new file mode 100755 index 00000000000..99f62ef6cb9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SelfDestructOperation.java @@ -0,0 +1,59 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.EVM; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; + +import java.util.EnumSet; +import java.util.Optional; + +public class SelfDestructOperation extends AbstractOperation { + + public SelfDestructOperation(final GasCalculator gasCalculator) { + super(0xFF, "SELFDESTRUCT", 1, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final Address recipientAddress = Words.toAddress(frame.getStackItem(0)); + + final Account recipient = frame.getWorldState().get(recipientAddress); + final Wei inheritance = frame.getWorldState().get(frame.getRecipientAddress()).getBalance(); + + return gasCalculator().selfDestructOperationGasCost(recipient, inheritance); + } + + @Override + public void execute(final MessageFrame frame) { + final Address address = frame.getRecipientAddress(); + final MutableAccount account = frame.getWorldState().getMutable(address); + + frame.addSelfDestruct(address); + + final MutableAccount recipient = + frame.getWorldState().getOrCreate(Words.toAddress(frame.popStackItem())); + + recipient.incrementBalance(account.getBalance()); + account.setBalance(Wei.ZERO); + + frame.setState(MessageFrame.State.CODE_SUCCESS); + } + + @Override + public Optional exceptionalHaltCondition( + final MessageFrame frame, + final EnumSet previousReasons, + final EVM evm) { + return frame.isStatic() + ? Optional.of(ExceptionalHaltReason.ILLEGAL_STATE_CHANGE) + : Optional.empty(); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Sha3Operation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Sha3Operation.java new file mode 100755 index 00000000000..cac05f28a26 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/Sha3Operation.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +public class Sha3Operation extends AbstractOperation { + + public Sha3Operation(final GasCalculator gasCalculator) { + super(0x20, "SHA3", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + final UInt256 offset = frame.getStackItem(0).asUInt256(); + final UInt256 length = frame.getStackItem(1).asUInt256(); + + return gasCalculator().sha3OperationGasCost(frame, offset, length); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 from = frame.popStackItem().asUInt256(); + final UInt256 length = frame.popStackItem().asUInt256(); + + final BytesValue bytes = frame.readMemory(from, length); + frame.pushStackItem(Hash.hash(bytes)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperation.java new file mode 100755 index 00000000000..58ca3d12488 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperation.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static net.consensys.pantheon.util.uint.UInt256s.greaterThanOrEqualTo256; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.Bytes32s; +import net.consensys.pantheon.util.uint.UInt256; + +public class ShlOperation extends AbstractOperation { + + public ShlOperation(final GasCalculator gasCalculator) { + super(0x1b, "SHL", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 shiftAmount = frame.popStackItem().asUInt256(); + final Bytes32 value = frame.popStackItem(); + + if (greaterThanOrEqualTo256(shiftAmount)) { + frame.pushStackItem(Bytes32.ZERO); + } else { + frame.pushStackItem(Bytes32s.shiftLeft(value, shiftAmount.toInt())); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperation.java new file mode 100755 index 00000000000..46ea129ec91 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperation.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static net.consensys.pantheon.util.uint.UInt256s.greaterThanOrEqualTo256; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.Bytes32s; +import net.consensys.pantheon.util.uint.UInt256; + +public class ShrOperation extends AbstractOperation { + + public ShrOperation(final GasCalculator gasCalculator) { + super(0x1c, "SHR", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 shiftAmount = frame.popStackItem().asUInt256(); + final Bytes32 value = frame.popStackItem(); + + if (greaterThanOrEqualTo256(shiftAmount)) { + frame.pushStackItem(Bytes32.ZERO); + } else { + frame.pushStackItem(Bytes32s.shiftRight(value, shiftAmount.toInt())); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SignExtendOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SignExtendOperation.java new file mode 100755 index 00000000000..5666b257d99 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SignExtendOperation.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class SignExtendOperation extends AbstractOperation { + + public SignExtendOperation(final GasCalculator gasCalculator) { + super(0x0B, "SIGNEXTEND", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + // Stack items are reversed for the SIGNEXTEND operation. + final UInt256 result = value1.signExtent(value0).asUnsigned(); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StaticCallOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StaticCallOperation.java new file mode 100755 index 00000000000..9fdb4b95257 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StaticCallOperation.java @@ -0,0 +1,99 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.AbstractCallOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.ethereum.vm.Words; +import net.consensys.pantheon.util.uint.UInt256; + +public class StaticCallOperation extends AbstractCallOperation { + + public StaticCallOperation(final GasCalculator gasCalculator) { + super(0xFA, "STATICCALL", 6, 1, false, 1, gasCalculator); + } + + @Override + protected Gas gas(final MessageFrame frame) { + return Gas.of(frame.getStackItem(0)); + } + + @Override + protected Address to(final MessageFrame frame) { + return Words.toAddress(frame.getStackItem(1)); + } + + @Override + protected Wei value(final MessageFrame frame) { + return Wei.ZERO; + } + + @Override + protected Wei apparentValue(final MessageFrame frame) { + return value(frame); + } + + @Override + protected UInt256 inputDataOffset(final MessageFrame frame) { + return frame.getStackItem(2).asUInt256(); + } + + @Override + protected UInt256 inputDataLength(final MessageFrame frame) { + return frame.getStackItem(3).asUInt256(); + } + + @Override + protected UInt256 outputDataOffset(final MessageFrame frame) { + return frame.getStackItem(4).asUInt256(); + } + + @Override + protected UInt256 outputDataLength(final MessageFrame frame) { + return frame.getStackItem(5).asUInt256(); + } + + @Override + protected Address address(final MessageFrame frame) { + return to(frame); + } + + @Override + protected Address sender(final MessageFrame frame) { + return frame.getRecipientAddress(); + } + + @Override + public Gas gasAvailableForChildCall(final MessageFrame frame) { + return gasCalculator().gasAvailableForChildCall(frame, gas(frame), !value(frame).isZero()); + } + + @Override + protected boolean isStatic(final MessageFrame frame) { + return true; + } + + @Override + public Gas cost(final MessageFrame frame) { + final Gas stipend = gas(frame); + final UInt256 inputDataOffset = inputDataOffset(frame).asUInt256(); + final UInt256 inputDataLength = inputDataLength(frame).asUInt256(); + final UInt256 outputDataOffset = outputDataOffset(frame).asUInt256(); + final UInt256 outputDataLength = outputDataLength(frame).asUInt256(); + final Account recipient = frame.getWorldState().get(address(frame)); + + return gasCalculator() + .callOperationGasCost( + frame, + stipend, + inputDataOffset, + inputDataLength, + outputDataOffset, + outputDataLength, + value(frame), + recipient); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StopOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StopOperation.java new file mode 100755 index 00000000000..9f08a89ed3a --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/StopOperation.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class StopOperation extends AbstractOperation { + + public StopOperation(final GasCalculator gasCalculator) { + super(0x00, "STOP", 0, 0, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getZeroTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + frame.setState(MessageFrame.State.CODE_SUCCESS); + frame.setOutputData(BytesValue.EMPTY); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SubOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SubOperation.java new file mode 100755 index 00000000000..97f6a995147 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SubOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class SubOperation extends AbstractOperation { + + public SubOperation(final GasCalculator gasCalculator) { + super(0x03, "SUB", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.minus(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SwapOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SwapOperation.java new file mode 100755 index 00000000000..a05d7a65475 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/SwapOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; + +public class SwapOperation extends AbstractOperation { + + private final int index; + + public SwapOperation(final int index, final GasCalculator gasCalculator) { + super(0x90 + index - 1, "SWAP" + index, index + 1, index + 1, false, 1, gasCalculator); + this.index = index; + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final Bytes32 tmp = frame.getStackItem(0); + frame.setStackItem(0, frame.getStackItem(index)); + frame.setStackItem(index, tmp); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/TimestampOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/TimestampOperation.java new file mode 100755 index 00000000000..34cb4d563fc --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/TimestampOperation.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +public class TimestampOperation extends AbstractOperation { + + public TimestampOperation(final GasCalculator gasCalculator) { + super(0x42, "TIMESTAMP", 0, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getBaseTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final long timestamp = frame.getBlockHeader().getTimestamp(); + frame.pushStackItem(UInt256Bytes.of(timestamp)); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/XorOperation.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/XorOperation.java new file mode 100755 index 00000000000..8b225bbc239 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/vm/operations/XorOperation.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.vm.AbstractOperation; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.uint.UInt256; + +public class XorOperation extends AbstractOperation { + + public XorOperation(final GasCalculator gasCalculator) { + super(0x18, "XOR", 2, 1, false, 1, gasCalculator); + } + + @Override + public Gas cost(final MessageFrame frame) { + return gasCalculator().getVeryLowTierGasCost(); + } + + @Override + public void execute(final MessageFrame frame) { + final UInt256 value0 = frame.popStackItem().asUInt256(); + final UInt256 value1 = frame.popStackItem().asUInt256(); + + final UInt256 result = value0.xor(value1); + + frame.pushStackItem(result.getBytes()); + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DebuggableMutableWorldState.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DebuggableMutableWorldState.java new file mode 100755 index 00000000000..1d054dca769 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DebuggableMutableWorldState.java @@ -0,0 +1,160 @@ +package net.consensys.pantheon.ethereum.worldstate; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +/** + * A simple extension of {@link DefaultMutableWorldState} that tracks in memory the mapping of hash + * to address for its accounts for debugging purposes. It also provides a full toString() method + * that display the content of the world state. It is obviously only mean for testing or debugging. + */ +public class DebuggableMutableWorldState extends DefaultMutableWorldState { + + // TODO: This is more complex than it should due to DefaultMutableWorldState.accounts() not being + // implmemented (pending NC-746). Once that is fixed, we won't need to keep the set of account + // hashes at all, just the hashtoAddress map (this is also why things are separated this way, + // it will make it easier to update later). + + private static class DebugInfo { + private final Set

accounts = new HashSet<>(); + + private void addAll(final DebugInfo other) { + this.accounts.addAll(other.accounts); + } + } + + private final DebugInfo info = new DebugInfo(); + + public DebuggableMutableWorldState() { + super(new KeyValueStorageWorldStateStorage(new InMemoryKeyValueStorage())); + } + + public DebuggableMutableWorldState(final WorldState worldState) { + super(worldState); + + if (worldState instanceof DebuggableMutableWorldState) { + final DebuggableMutableWorldState dws = ((DebuggableMutableWorldState) worldState); + info.addAll(dws.info); + } else { + // TODO: on NC-746 gets in, we can remove this. That is, post NC-746, we won't be relying + // on info.accounts to know that accounts exists, so the only thing we will not have in + // this branch is info.addressToHash, but that's not a huge deal. + throw new RuntimeException(worldState + " is not a debuggable word state"); + } + } + + @Override + public WorldUpdater updater() { + return new InfoCollectingUpdater(super.updater(), info); + } + + @Override + public Stream accounts() { + return info.accounts.stream().map(this::get).filter(Objects::nonNull); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(rootHash()).append(":\n"); + accounts() + .forEach( + account -> { + final Address address = account.getAddress(); + builder + .append(" ") + .append(address == null ? "" : address) + .append(" [") + .append(account.getAddressHash()) + .append("]:\n"); + builder.append(" nonce: ").append(account.getNonce()).append('\n'); + builder.append(" balance: ").append(account.getBalance()).append('\n'); + builder.append(" code: ").append(account.getCode()).append('\n'); + }); + return builder.toString(); + } + + private class InfoCollectingUpdater implements WorldUpdater { + private final WorldUpdater wrapped; + private final DebugInfo commitInfo; + private DebugInfo ownInfo = new DebugInfo(); + + InfoCollectingUpdater(final WorldUpdater wrapped, final DebugInfo info) { + this.wrapped = wrapped; + this.commitInfo = info; + } + + private void record(final Address address) { + ownInfo.accounts.add(address); + } + + @Override + public MutableAccount createAccount( + final Address address, final long nonce, final Wei balance) { + record(address); + return wrapped.createAccount(address, nonce, balance); + } + + @Override + public MutableAccount createAccount(final Address address) { + record(address); + return wrapped.createAccount(address); + } + + @Override + public MutableAccount getOrCreate(final Address address) { + record(address); + return wrapped.getOrCreate(address); + } + + @Override + public MutableAccount getMutable(final Address address) { + record(address); + return wrapped.getMutable(address); + } + + @Override + public void deleteAccount(final Address address) { + wrapped.deleteAccount(address); + } + + @Override + public Collection getTouchedAccounts() { + return wrapped.getTouchedAccounts(); + } + + @Override + public void revert() { + ownInfo = new DebugInfo(); + wrapped.revert(); + } + + @Override + public void commit() { + commitInfo.addAll(ownInfo); + wrapped.commit(); + } + + @Override + public WorldUpdater updater() { + return new InfoCollectingUpdater(wrapped.updater(), ownInfo); + } + + @Override + public Account get(final Address address) { + record(address); + return wrapped.get(address); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldState.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldState.java new file mode 100755 index 00000000000..f319896a917 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldState.java @@ -0,0 +1,377 @@ +package net.consensys.pantheon.ethereum.worldstate; + +import net.consensys.pantheon.ethereum.core.AbstractWorldUpdater; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.trie.MerklePatriciaTrie; +import net.consensys.pantheon.ethereum.trie.StoredMerklePatriciaTrie; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Stream; + +public class DefaultMutableWorldState implements MutableWorldState { + + private final MerklePatriciaTrie accountStateTrie; + private final Map> updatedStorageTries = + new HashMap<>(); + private final Map updatedAccountCode = new HashMap<>(); + private final WorldStateStorage worldStateStorage; + + public DefaultMutableWorldState(final WorldStateStorage storage) { + this(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, storage); + } + + public DefaultMutableWorldState( + final Bytes32 rootHash, final WorldStateStorage worldStateStorage) { + this.worldStateStorage = worldStateStorage; + this.accountStateTrie = newAccountStateTrie(rootHash); + } + + public DefaultMutableWorldState(final WorldState worldState) { + // TODO: this is an abstraction leak (and kind of incorrect in that we reuse the underlying + // storage), but the reason for this is that the accounts() method is unimplemented below and + // can't be until NC-754. + if (!(worldState instanceof DefaultMutableWorldState)) { + throw new UnsupportedOperationException(); + } + + final DefaultMutableWorldState other = (DefaultMutableWorldState) worldState; + this.worldStateStorage = other.worldStateStorage; + this.accountStateTrie = newAccountStateTrie(other.accountStateTrie.getRootHash()); + } + + private MerklePatriciaTrie newAccountStateTrie(final Bytes32 rootHash) { + return new StoredMerklePatriciaTrie<>( + worldStateStorage::getAccountStateTrieNode, rootHash, b -> b, b -> b); + } + + private MerklePatriciaTrie newAccountStorageTrie(final Bytes32 rootHash) { + return new StoredMerklePatriciaTrie<>( + worldStateStorage::getAccountStorageTrieNode, rootHash, b -> b, b -> b); + } + + @Override + public Hash rootHash() { + return Hash.wrap(accountStateTrie.getRootHash()); + } + + @Override + public MutableWorldState copy() { + return new DefaultMutableWorldState(rootHash(), worldStateStorage); + } + + @Override + public Account get(final Address address) { + final Hash addressHash = Hash.hash(address); + return accountStateTrie + .get(Hash.hash(address)) + .map(bytes -> deserializeAccount(address, addressHash, bytes)) + .orElse(null); + } + + private AccountState deserializeAccount( + final Address address, final Hash addressHash, final BytesValue encoded) throws RLPException { + final RLPInput in = RLP.input(encoded); + in.enterList(); + + final long nonce = in.readLongScalar(); + final Wei balance = in.readUInt256Scalar(Wei::wrap); + final Hash storageRoot = Hash.wrap(in.readBytes32()); + final Hash codeHash = Hash.wrap(in.readBytes32()); + + in.leaveList(); + + return new AccountState(address, addressHash, nonce, balance, storageRoot, codeHash); + } + + private static BytesValue serializeAccount( + final long nonce, final Wei balance, final Hash codeHash, final Hash storageRoot) { + return RLP.encode( + out -> { + out.startList(); + + out.writeLongScalar(nonce); + out.writeUInt256Scalar(balance); + out.writeBytesValue(storageRoot); + out.writeBytesValue(codeHash); + + out.endList(); + }); + } + + @Override + public WorldUpdater updater() { + return new Updater(this); + } + + @Override + public Stream accounts() { + // TODO: the current trie implementation doesn't have walking capability yet (pending NC-746) + // so this can't be implemented. + throw new UnsupportedOperationException("TODO"); + } + + @Override + public int hashCode() { + return Objects.hashCode(rootHash()); + } + + @Override + public final boolean equals(final Object other) { + if (!(other instanceof DefaultMutableWorldState)) { + return false; + } + + final DefaultMutableWorldState that = (DefaultMutableWorldState) other; + return this.rootHash().equals(that.rootHash()); + } + + @Override + public void persist() { + final WorldStateStorage.Updater updater = worldStateStorage.updater(); + // Store updated code + for (final BytesValue code : updatedAccountCode.values()) { + updater.putCode(code); + } + // Commit account storage tries + for (final MerklePatriciaTrie updatedStorage : + updatedStorageTries.values()) { + updatedStorage.commit(updater::putAccountStorageTrieNode); + } + // Commit account updates + accountStateTrie.commit(updater::putAccountStateTrieNode); + + // Clear pending changes that we just flushed + updatedStorageTries.clear(); + updatedAccountCode.clear(); + + // Push changes to underlying storage + updater.commit(); + } + + // An immutable class that represents an individual account as stored in + // in the world state's underlying merkle patricia trie. + protected class AccountState implements Account { + + private final Address address; + private final Hash addressHash; + + private final long nonce; + private final Wei balance; + private final Hash storageRoot; + private final Hash codeHash; + + // Lazily initialized since we don't always access storage. + private volatile MerklePatriciaTrie storageTrie; + + private AccountState( + final Address address, + final Hash addressHash, + final long nonce, + final Wei balance, + final Hash storageRoot, + final Hash codeHash) { + + this.address = address; + this.addressHash = addressHash; + this.nonce = nonce; + this.balance = balance; + this.storageRoot = storageRoot; + this.codeHash = codeHash; + } + + private MerklePatriciaTrie storageTrie() { + final MerklePatriciaTrie updatedTrie = updatedStorageTries.get(address); + if (updatedTrie != null) { + storageTrie = updatedTrie; + } + if (storageTrie == null) { + storageTrie = newAccountStorageTrie(storageRoot); + } + return storageTrie; + } + + @Override + public Address getAddress() { + return address; + } + + @Override + public Hash getAddressHash() { + return addressHash; + } + + @Override + public long getNonce() { + return nonce; + } + + @Override + public Wei getBalance() { + return balance; + } + + @Override + public BytesValue getCode() { + final BytesValue updatedCode = updatedAccountCode.get(address); + if (updatedCode != null) { + return updatedCode; + } + // No code is common, save the KV-store lookup. + if (codeHash.equals(Hash.EMPTY)) { + return BytesValue.EMPTY; + } + return worldStateStorage.getCode(codeHash).orElse(BytesValue.EMPTY); + } + + @Override + public boolean hasCode() { + return !getCode().isEmpty(); + } + + @Override + public UInt256 getStorageValue(final UInt256 key) { + final Optional val = storageTrie().get(Hash.hash(key.getBytes())); + if (!val.isPresent()) { + return UInt256.ZERO; + } + return convertToUInt256(val.get()); + } + + @Override + public NavigableMap storageEntriesFrom( + final Bytes32 startKeyHash, final int limit) { + final NavigableMap storageEntries = new TreeMap<>(); + storageTrie() + .entriesFrom(startKeyHash, limit) + .forEach((key, value) -> storageEntries.put(key, convertToUInt256(value))); + return storageEntries; + } + + private UInt256 convertToUInt256(final BytesValue value) { + // TODO: we could probably have an optimized method to decode a single scalar since it's used + // pretty often. + final RLPInput in = RLP.input(value); + return in.readUInt256Scalar(); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("AccountState").append("{"); + builder.append("address=").append(getAddress()).append(", "); + builder.append("nonce=").append(getNonce()).append(", "); + builder.append("balance=").append(getBalance()).append(", "); + builder.append("storageRoot=").append(storageRoot).append(", "); + builder.append("codeHash=").append(codeHash); + return builder.append("}").toString(); + } + } + + protected static class Updater + extends AbstractWorldUpdater { + + protected Updater(final DefaultMutableWorldState world) { + super(world); + } + + @Override + protected AccountState getForMutation(final Address address) { + final DefaultMutableWorldState wrapped = wrappedWorldView(); + final Hash addressHash = Hash.hash(address); + return wrapped + .accountStateTrie + .get(addressHash) + .map(bytes -> wrapped.deserializeAccount(address, addressHash, bytes)) + .orElse(null); + } + + @Override + public Collection getTouchedAccounts() { + return new ArrayList<>(updatedAccounts()); + } + + @Override + public void revert() { + deletedAccounts().clear(); + updatedAccounts().clear(); + } + + @Override + public void commit() { + final DefaultMutableWorldState wrapped = wrappedWorldView(); + + for (final Address address : deletedAccounts()) { + final Hash addressHash = Hash.hash(address); + wrapped.accountStateTrie.remove(addressHash); + wrapped.updatedStorageTries.remove(address); + wrapped.updatedAccountCode.remove(address); + } + + // TODO: this is inefficient with a real persistent world state as doing updates one by one + // might create a lot of garbage nodes that will be persisted without being needed. Also, if + // the state is big, doing update one by one is not algorithmically optimal in general. We + // should create a small in-memory trie of the updates first, and then apply this in bulk + // to the real world state as a merge of trie. + for (final UpdateTrackingAccount updated : updatedAccounts()) { + final AccountState origin = updated.getWrappedAccount(); + + // Save the code in key-value storage ... + Hash codeHash = origin == null ? Hash.EMPTY : origin.codeHash; + if (updated.codeWasUpdated()) { + codeHash = Hash.hash(updated.getCode()); + wrapped.updatedAccountCode.put(updated.getAddress(), updated.getCode()); + } + // ...and storage in the account trie first. + // TODO: same remark as above really, this could be make more efficient by "batching" + final SortedMap updatedStorage = updated.getUpdatedStorage(); + Hash storageRoot; + MerklePatriciaTrie storageTrie; + if (updatedStorage.isEmpty()) { + storageRoot = origin == null ? Hash.EMPTY_TRIE_HASH : origin.storageRoot; + } else { + storageTrie = + origin == null + ? wrapped.newAccountStorageTrie(Hash.EMPTY_TRIE_HASH) + : origin.storageTrie(); + wrapped.updatedStorageTries.put(updated.getAddress(), storageTrie); + for (final Map.Entry entry : updatedStorage.entrySet()) { + final UInt256 value = entry.getValue(); + final Hash keyHash = Hash.hash(entry.getKey().getBytes()); + if (value.isZero()) { + storageTrie.remove(keyHash); + } else { + storageTrie.put(keyHash, RLP.encode(out -> out.writeUInt256Scalar(entry.getValue()))); + } + } + storageRoot = Hash.wrap(storageTrie.getRootHash()); + } + + // Lastly, save the new account. + final BytesValue account = + serializeAccount(updated.getNonce(), updated.getBalance(), codeHash, storageRoot); + + wrapped.accountStateTrie.put(updated.getAddressHash(), account); + } + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/KeyValueStorageWorldStateStorage.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/KeyValueStorageWorldStateStorage.java new file mode 100755 index 00000000000..b42ce4dd429 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/KeyValueStorageWorldStateStorage.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.ethereum.worldstate; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +public class KeyValueStorageWorldStateStorage implements WorldStateStorage { + + private final KeyValueStorage keyValueStorage; + + public KeyValueStorageWorldStateStorage(final KeyValueStorage keyValueStorage) { + this.keyValueStorage = keyValueStorage; + } + + @Override + public Optional getCode(final Hash codeHash) { + return keyValueStorage.get(codeHash); + } + + @Override + public Optional getAccountStateTrieNode(final Bytes32 nodeHash) { + return keyValueStorage.get(nodeHash); + } + + @Override + public Optional getAccountStorageTrieNode(final Bytes32 nodeHash) { + return keyValueStorage.get(nodeHash); + } + + @Override + public Updater updater() { + return new Updater(keyValueStorage.getStartTransaction()); + } + + public static class Updater implements WorldStateStorage.Updater { + + private final KeyValueStorage.Transaction transaction; + + public Updater(final KeyValueStorage.Transaction transaction) { + this.transaction = transaction; + } + + @Override + public void putCode(final BytesValue code) { + transaction.put(Hash.hash(code), code); + } + + @Override + public void putAccountStateTrieNode(final Bytes32 nodeHash, final BytesValue node) { + transaction.put(nodeHash, node); + } + + @Override + public void putAccountStorageTrieNode(final Bytes32 nodeHash, final BytesValue node) { + transaction.put(nodeHash, node); + } + + @Override + public void commit() { + transaction.commit(); + } + + @Override + public void rollback() { + transaction.rollback(); + } + } +} diff --git a/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/WorldStateStorage.java b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/WorldStateStorage.java new file mode 100755 index 00000000000..3c7da3b40c9 --- /dev/null +++ b/ethereum/core/src/main/java/net/consensys/pantheon/ethereum/worldstate/WorldStateStorage.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.worldstate; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +public interface WorldStateStorage { + + Optional getCode(Hash codeHash); + + Optional getAccountStateTrieNode(Bytes32 nodeHash); + + Optional getAccountStorageTrieNode(Bytes32 nodeHash); + + Updater updater(); + + interface Updater { + + void putCode(BytesValue code); + + void putAccountStateTrieNode(Bytes32 nodeHash, BytesValue node); + + void putAccountStorageTrieNode(Bytes32 nodeHash, BytesValue node); + + void commit(); + + void rollback(); + } +} diff --git a/ethereum/core/src/main/resources/daoAddresses.json b/ethereum/core/src/main/resources/daoAddresses.json new file mode 100755 index 00000000000..52f48ab6fc9 --- /dev/null +++ b/ethereum/core/src/main/resources/daoAddresses.json @@ -0,0 +1,118 @@ +[ + "0xd4fe7bc31cedb7bfb8a345f31e668033056b2728", + "0xb3fb0e5aba0e20e5c49d252dfd30e102b171a425", + "0x2c19c7f9ae8b751e37aeb2d93a699722395ae18f", + "0xecd135fa4f61a655311e86238c92adcd779555d2", + "0x1975bd06d486162d5dc297798dfc41edd5d160a7", + "0xa3acf3a1e16b1d7c315e23510fdd7847b48234f6", + "0x319f70bab6845585f412ec7724b744fec6095c85", + "0x06706dd3f2c9abf0a21ddcc6941d9b86f0596936", + "0x5c8536898fbb74fc7445814902fd08422eac56d0", + "0x6966ab0d485353095148a2155858910e0965b6f9", + "0x779543a0491a837ca36ce8c635d6154e3c4911a6", + "0x2a5ed960395e2a49b1c758cef4aa15213cfd874c", + "0x5c6e67ccd5849c0d29219c4f95f1a7a93b3f5dc5", + "0x9c50426be05db97f5d64fc54bf89eff947f0a321", + "0x200450f06520bdd6c527622a273333384d870efb", + "0xbe8539bfe837b67d1282b2b1d61c3f723966f049", + "0x6b0c4d41ba9ab8d8cfb5d379c69a612f2ced8ecb", + "0xf1385fb24aad0cd7432824085e42aff90886fef5", + "0xd1ac8b1ef1b69ff51d1d401a476e7e612414f091", + "0x8163e7fb499e90f8544ea62bbf80d21cd26d9efd", + "0x51e0ddd9998364a2eb38588679f0d2c42653e4a6", + "0x627a0a960c079c21c34f7612d5d230e01b4ad4c7", + "0xf0b1aa0eb660754448a7937c022e30aa692fe0c5", + "0x24c4d950dfd4dd1902bbed3508144a54542bba94", + "0x9f27daea7aca0aa0446220b98d028715e3bc803d", + "0xa5dc5acd6a7968a4554d89d65e59b7fd3bff0f90", + "0xd9aef3a1e38a39c16b31d1ace71bca8ef58d315b", + "0x63ed5a272de2f6d968408b4acb9024f4cc208ebf", + "0x6f6704e5a10332af6672e50b3d9754dc460dfa4d", + "0x77ca7b50b6cd7e2f3fa008e24ab793fd56cb15f6", + "0x492ea3bb0f3315521c31f273e565b868fc090f17", + "0x0ff30d6de14a8224aa97b78aea5388d1c51c1f00", + "0x9ea779f907f0b315b364b0cfc39a0fde5b02a416", + "0xceaeb481747ca6c540a000c1f3641f8cef161fa7", + "0xcc34673c6c40e791051898567a1222daf90be287", + "0x579a80d909f346fbfb1189493f521d7f48d52238", + "0xe308bd1ac5fda103967359b2712dd89deffb7973", + "0x4cb31628079fb14e4bc3cd5e30c2f7489b00960c", + "0xac1ecab32727358dba8962a0f3b261731aad9723", + "0x4fd6ace747f06ece9c49699c7cabc62d02211f75", + "0x440c59b325d2997a134c2c7c60a8c61611212bad", + "0x4486a3d68fac6967006d7a517b889fd3f98c102b", + "0x9c15b54878ba618f494b38f0ae7443db6af648ba", + "0x27b137a85656544b1ccb5a0f2e561a5703c6a68f", + "0x21c7fdb9ed8d291d79ffd82eb2c4356ec0d81241", + "0x23b75c2f6791eef49c69684db4c6c1f93bf49a50", + "0x1ca6abd14d30affe533b24d7a21bff4c2d5e1f3b", + "0xb9637156d330c0d605a791f1c31ba5890582fe1c", + "0x6131c42fa982e56929107413a9d526fd99405560", + "0x1591fc0f688c81fbeb17f5426a162a7024d430c2", + "0x542a9515200d14b68e934e9830d91645a980dd7a", + "0xc4bbd073882dd2add2424cf47d35213405b01324", + "0x782495b7b3355efb2833d56ecb34dc22ad7dfcc4", + "0x58b95c9a9d5d26825e70a82b6adb139d3fd829eb", + "0x3ba4d81db016dc2890c81f3acec2454bff5aada5", + "0xb52042c8ca3f8aa246fa79c3feaa3d959347c0ab", + "0xe4ae1efdfc53b73893af49113d8694a057b9c0d1", + "0x3c02a7bc0391e86d91b7d144e61c2c01a25a79c5", + "0x0737a6b837f97f46ebade41b9bc3e1c509c85c53", + "0x97f43a37f595ab5dd318fb46e7a155eae057317a", + "0x52c5317c848ba20c7504cb2c8052abd1fde29d03", + "0x4863226780fe7c0356454236d3b1c8792785748d", + "0x5d2b2e6fcbe3b11d26b525e085ff818dae332479", + "0x5f9f3392e9f62f63b8eac0beb55541fc8627f42c", + "0x057b56736d32b86616a10f619859c6cd6f59092a", + "0x9aa008f65de0b923a2a4f02012ad034a5e2e2192", + "0x304a554a310c7e546dfe434669c62820b7d83490", + "0x914d1b8b43e92723e64fd0a06f5bdb8dd9b10c79", + "0x4deb0033bb26bc534b197e61d19e0733e5679784", + "0x07f5c1e1bc2c93e0402f23341973a0e043f7bf8a", + "0x35a051a0010aba705c9008d7a7eff6fb88f6ea7b", + "0x4fa802324e929786dbda3b8820dc7834e9134a2a", + "0x9da397b9e80755301a3b32173283a91c0ef6c87e", + "0x8d9edb3054ce5c5774a420ac37ebae0ac02343c6", + "0x0101f3be8ebb4bbd39a2e3b9a3639d4259832fd9", + "0x5dc28b15dffed94048d73806ce4b7a4612a1d48f", + "0xbcf899e6c7d9d5a215ab1e3444c86806fa854c76", + "0x12e626b0eebfe86a56d633b9864e389b45dcb260", + "0xa2f1ccba9395d7fcb155bba8bc92db9bafaeade7", + "0xec8e57756626fdc07c63ad2eafbd28d08e7b0ca5", + "0xd164b088bd9108b60d0ca3751da4bceb207b0782", + "0x6231b6d0d5e77fe001c2a460bd9584fee60d409b", + "0x1cba23d343a983e9b5cfd19496b9a9701ada385f", + "0xa82f360a8d3455c5c41366975bde739c37bfeb8a", + "0x9fcd2deaff372a39cc679d5c5e4de7bafb0b1339", + "0x005f5cee7a43331d5a3d3eec71305925a62f34b6", + "0x0e0da70933f4c7849fc0d203f5d1d43b9ae4532d", + "0xd131637d5275fd1a68a3200f4ad25c71a2a9522e", + "0xbc07118b9ac290e4622f5e77a0853539789effbe", + "0x47e7aa56d6bdf3f36be34619660de61275420af8", + "0xacd87e28b0c9d1254e868b81cba4cc20d9a32225", + "0xadf80daec7ba8dcf15392f1ac611fff65d94f880", + "0x5524c55fb03cf21f549444ccbecb664d0acad706", + "0x40b803a9abce16f50f36a77ba41180eb90023925", + "0xfe24cdd8648121a43a7c86d289be4dd2951ed49f", + "0x17802f43a0137c506ba92291391a8a8f207f487d", + "0x253488078a4edf4d6f42f113d1e62836a942cf1a", + "0x86af3e9626fce1957c82e88cbf04ddf3a2ed7915", + "0xb136707642a4ea12fb4bae820f03d2562ebff487", + "0xdbe9b615a3ae8709af8b93336ce9b477e4ac0940", + "0xf14c14075d6c4ed84b86798af0956deef67365b5", + "0xca544e5c4687d109611d0f8f928b53a25af72448", + "0xaeeb8ff27288bdabc0fa5ebb731b6f409507516c", + "0xcbb9d3703e651b0d496cdefb8b92c25aeb2171f7", + "0x6d87578288b6cb5549d5076a207456a1f6a63dc0", + "0xb2c6f0dfbb716ac562e2d85d6cb2f8d5ee87603e", + "0xaccc230e8a6e5be9160b8cdf2864dd2a001c28b6", + "0x2b3455ec7fedf16e646268bf88846bd7a2319bb2", + "0x4613f3bca5c44ea06337a9e439fbc6d42e501d0a", + "0xd343b217de44030afaa275f54d31a9317c7f441e", + "0x84ef4b2357079cd7a7c69fd7a37cd0609a679106", + "0xda2fef9e4a3230988ff17df2165440f37e8b1708", + "0xf4c64518ea10f995918a454158c6b61407ea345c", + "0x7602b46df5390e432ef1c307d4f2c9ff6d65cc97", + "0xbb9bc244d798123fde783fcc1c72d3bb8c189413", + "0x807640a13483f8ac783c557fcdf27be11ea4ac7a" +] \ No newline at end of file diff --git a/ethereum/core/src/main/resources/dev.json b/ethereum/core/src/main/resources/dev.json new file mode 100755 index 00000000000..6f75c23acd8 --- /dev/null +++ b/ethereum/core/src/main/resources/dev.json @@ -0,0 +1,34 @@ +{ + "config": { + "chainId": 2018, + "ethash": { + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x1000000", + "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/ethereum/core/src/main/resources/infura.json b/ethereum/core/src/main/resources/infura.json new file mode 100755 index 00000000000..1775579f290 --- /dev/null +++ b/ethereum/core/src/main/resources/infura.json @@ -0,0 +1,35 @@ +{ + "config": { + "chainId": 1337, + "homesteadBlock":0, + "eip150Block":0, + "eip155Block":10, + "eip158Block":10, + "eip160Block":10 + }, + "nonce": "0x0000000000000042", + "alloc": { + "0x0000000000000000000000000000000000000001": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000002": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000003": { + "balance": "1" + }, + "0x0000000000000000000000000000000000000004": { + "balance": "1" + }, + "118ca24d0e3e20ec641bf8795765e96dea762b2b": { + "balance": "10000000000000000000" + } + }, + "timestamp": "0x00", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0x3535353535353535353535353535353535353535353535353535353535353535", + "gasLimit": "0x1000000", + "difficulty": "0x10000", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000" +} diff --git a/ethereum/core/src/main/resources/mainnet.json b/ethereum/core/src/main/resources/mainnet.json new file mode 100755 index 00000000000..b853e7bcf66 --- /dev/null +++ b/ethereum/core/src/main/resources/mainnet.json @@ -0,0 +1,26707 @@ +{ + "config": { + "chainId": 1, + "homesteadBlock": 1150000, + "daoForkBlock": 1920000, + "daoForkSupport": true, + "eip150Block": 2463000, + "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0", + "eip155Block": 2675000, + "eip158Block": 2675000, + "byzantiumBlock": 4370000, + "ethash": { + + } + }, + "nonce": "0x42", + "timestamp": "0x0", + "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "gasLimit": "0x1388", + "difficulty": "0x400000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "000d836201318ec6899a67540690382780743280": { + "balance": "0xad78ebc5ac6200000" + }, + "001762430ea9c3a26e5749afdb70da5f78ddbb8c": { + "balance": "0xad78ebc5ac6200000" + }, + "001d14804b399c6ef80e64576f657660804fec0b": { + "balance": "0xe3aeb5737240a00000" + }, + "0032403587947b9f15622a68d104d54d33dbd1cd": { + "balance": "0x433874f632cc60000" + }, + "00497e92cdc0e0b963d752b2296acb87da828b24": { + "balance": "0xa8f649fe7c6180000" + }, + "004bfbe1546bc6c65b5c7eaa55304b38bbfec6d3": { + "balance": "0x6c6b935b8bbd400000" + }, + "005a9c03f69d17d66cbb8ad721008a9ebbb836fb": { + "balance": "0x6c6b935b8bbd400000" + }, + "005d0ee8155ec0a6ff6808552ca5f16bb5be323a": { + "balance": "0xaadec983fcff40000" + }, + "007622d84a234bb8b078230fcf84b67ae9a8acae": { + "balance": "0x25e1cc519952f80000" + }, + "007b9fc31905b4994b04c9e2cfdc5e2770503f42": { + "balance": "0x6c5db2a4d815dc0000" + }, + "007f4a23ca00cd043d25c2888c1aa5688f81a344": { + "balance": "0x29f0a95bfbf7290000" + }, + "008639dabbe3aeac887b5dc0e43e13bcd287d76c": { + "balance": "0x10d0e3c87d6e2c0000" + }, + "0089508679abf8c71bf6781687120e3e6a84584d": { + "balance": "0x6194049f30f7200000" + }, + "008fc7cbadffbd0d7fe44f8dfd60a79d721a1c9c": { + "balance": "0x3635c9adc5dea00000" + }, + "009560a3de627868f91fa8bfe1c1b7afaf08186b": { + "balance": "0x1c67f5f7baa0b00000" + }, + "00969747f7a5b30645fe00e44901435ace24cc37": { + "balance": "0x5c283d410394100000" + }, + "009a6d7db326679b77c90391a7476d238f3ba33e": { + "balance": "0xada55474b81340000" + }, + "009eef0a0886056e3f69211853b9b7457f3782e4": { + "balance": "0xa2a878069b28e00000" + }, + "009fdbf44e1f4a6362b769c39a475f95a96c2bc7": { + "balance": "0x1e931283ccc8500000" + }, + "00a5797f52c9d58f189f36b1d45d1bf6041f2f6b": { + "balance": "0x127d1b3461acd1a0000" + }, + "00aa5381b2138ebeffc191d5d8c391753b7098d2": { + "balance": "0x35abb09ffedeb68000" + }, + "00aada25ea2286709abb422d41923fd380cd04c7": { + "balance": "0x233df3299f61720000" + }, + "00acbfb2f25a5485c739ef70a44eeeeb7c65a66f": { + "balance": "0x56bc75e2d63100000" + }, + "00acc6f082a442828764d11f58d6894ae408f073": { + "balance": "0xcb49b44ba602d800000" + }, + "00b277b099a8e866ca0ec65bcb87284fd142a582": { + "balance": "0x6acb3df27e1f880000" + }, + "00bdd4013aa31c04616c2bc9785f2788f915679b": { + "balance": "0xb9f65d00f63c0000" + }, + "00c27d63fde24b92ee8a1e7ed5d26d8dc5c83b03": { + "balance": "0x6c6b935b8bbd400000" + }, + "00c40fe2095423509b9fd9b754323158af2310f3": { + "balance": "0x0" + }, + "00d75ed60c774f8b3a5a5173fb1833ad7105a2d9": { + "balance": "0x6cb7e74867d5e60000" + }, + "00d78d89b35f472716eceafebf600527d3a1f969": { + "balance": "0x5e0549c9632e1d80000" + }, + "00dae27b350bae20c5652124af5d8b5cba001ec1": { + "balance": "0x22b1c8c1227a00000" + }, + "00dc01cbf44978a42e8de8e436edf94205cfb6ec": { + "balance": "0x4f0febbcda8cb40000" + }, + "00e681bc2d10db62de85848324492250348e90bf": { + "balance": "0x43c33c1937564800000" + }, + "00f463e137dcf625fbf3bca39eca98d2b968cf7f": { + "balance": "0x14061b9d77a5e980000" + }, + "010007394b8b7565a1658af88ce463499135d6b7": { + "balance": "0x56bc75e2d63100000" + }, + "010df1df4bed23760d2d1c03781586ddf7918e54": { + "balance": "0x340aad21b3b700000" + }, + "010f4a98dfa1d9799bf5c796fb550efbe7ecd877": { + "balance": "0x1b2f292236292c70000" + }, + "01155057002f6b0d18acb9388d3bc8129f8f7a20": { + "balance": "0x48a43c54602f700000" + }, + "01226e0ad8d62277b162621c62c928e96e0b9a8c": { + "balance": "0x6c6b935b8bbd400000" + }, + "0126e12ebc17035f35c0e9d11dd148393c405d7a": { + "balance": "0x6c660645aa47180000" + }, + "012f396a2b5eb83559bac515e5210df2c8c362ba": { + "balance": "0xad78ebc5ac6200000" + }, + "0134ff38155fabae94fd35c4ffe1d79de7ef9c59": { + "balance": "0x35659ef93f0fc40000" + }, + "0136a5af6c3299c6b5f005fdaddb148c070b299b": { + "balance": "0x11aa9ac15f1280000" + }, + "01488ad3da603c4cdd6cb0b7a1e30d2a30c8fc38": { + "balance": "0xad78ebc5ac6200000" + }, + "014974a1f46bf204944a853111e52f1602617def": { + "balance": "0x6c6b935b8bbd400000" + }, + "014b7f67b14f5d983d87014f570c8b993b9872b5": { + "balance": "0xad78ebc5ac6200000" + }, + "0151fa5d17a2dce2d7f1eb39ef7fe2ad213d5d89": { + "balance": "0xd8d726b7177a800000" + }, + "01577afd4e50890247c9b10d44af73229aec884f": { + "balance": "0x24dce54d34a1a00000" + }, + "015f097d9acddcddafaf2a107eb93a40fc94b04c": { + "balance": "0x43c33c1937564800000" + }, + "0169c1c210eae845e56840412e1f65993ea90fb4": { + "balance": "0x6c6b935b8bbd400000" + }, + "016b60bb6d67928c29fd0313c666da8f1698d9c5": { + "balance": "0x6c6b935b8bbd400000" + }, + "016c85e1613b900fa357b8283b120e65aefcdd08": { + "balance": "0x2b5d9784a97cd50000" + }, + "018492488ba1a292342247b31855a55905fef269": { + "balance": "0x796e3ea3f8ab00000" + }, + "018f20a27b27ec441af723fd9099f2cbb79d6263": { + "balance": "0x75792a8abdef7c0000" + }, + "0191eb547e7bf6976b9b1b577546761de65622e2": { + "balance": "0x6c6b4c4da6ddbe0000" + }, + "019d709579ff4bc09fdcdde431dc1447d2c260bc": { + "balance": "0x1158e460913d00000" + }, + "01a25a5f5af0169b30864c3be4d7563ccd44f09e": { + "balance": "0x4d853c8f8908980000" + }, + "01a7d9fa7d0eb1185c67e54da83c2e75db69e39f": { + "balance": "0x19d4addd0d8bc960000" + }, + "01a818135a414210c37c62b625aca1a54611ac36": { + "balance": "0xe18398e7601900000" + }, + "01b1cae91a3b9559afb33cdc6d689442fdbfe037": { + "balance": "0xad78ebc5ac6200000" + }, + "01b5b5bc5a117fa08b34ed1db9440608597ac548": { + "balance": "0xad78ebc5ac6200000" + }, + "01bbc14f67af0639aab1441e6a08d4ce7162090f": { + "balance": "0x46fcf68ff8be060000" + }, + "01d03815c61f416b71a2610a2daba59ff6a6de5b": { + "balance": "0x205dfe50b81c82e0000" + }, + "01d599ee0d5f8c38ab2d392e2c65b74c3ce31820": { + "balance": "0x1ba5abf9e779380000" + }, + "01e40521122530d9ac91113c06a0190b6d63850b": { + "balance": "0x487a9a304539440000" + }, + "01e6415d587b065490f1ed7f21d6e0f386ee6747": { + "balance": "0x6c6b935b8bbd400000" + }, + "01e864d354741b423e6f42851724468c74f5aa9c": { + "balance": "0x43c33c1937564800000" + }, + "01ed5fba8d2eab673aec042d30e4e8a611d8c55a": { + "balance": "0x6c6b935b8bbd400000" + }, + "01fb8ec12425a04f813e46c54c05748ca6b29aa9": { + "balance": "0xe15730385467c0000" + }, + "01ff1eb1dead50a7f2f9638fdee6eccf3a7b2ac8": { + "balance": "0x2086ac351052600000" + }, + "020362c3ade878ca90d6b2d889a4cc5510eed5f3": { + "balance": "0x3888e8b311adb38000" + }, + "0203ae01d4c41cae1865e04b1f5b53cdfaecae31": { + "balance": "0x3689cdceb28cd70000" + }, + "02089361a3fe7451fb1f87f01a2d866653dc0b07": { + "balance": "0x22ac74832b5040000" + }, + "021f69043de88c4917ca10f1842897eec0589c7c": { + "balance": "0x6b44cfb81487f40000" + }, + "02290fb5f9a517f82845acdeca0fc846039be233": { + "balance": "0x6c6b935b8bbd400000" + }, + "0239b4f21f8e05cd01512b2be7a0e18a6d974607": { + "balance": "0x3635c9adc5dea00000" + }, + "02477212ffdd75e5155651b76506b1646671a1eb": { + "balance": "0x5f68e8131ecf800000" + }, + "024a098ae702bef5406c9c22b78bd4eb2cc7a293": { + "balance": "0xd8d726b7177a800000" + }, + "024bdd2c7bfd500ee7404f7fb3e9fb31dd20fbd1": { + "balance": "0x9c2007651b2500000" + }, + "025367960304beee34591118e9ac2d1358d8021a": { + "balance": "0x6c6b935b8bbd400000" + }, + "0256149f5b5063bea14e15661ffb58f9b459a957": { + "balance": "0x2629f66e0c53000000" + }, + "02603d7a3bb297c67c877e5d34fbd5b913d4c63a": { + "balance": "0x1158e460913d00000" + }, + "0261ad3a172abf1315f0ffec3270986a8409cb25": { + "balance": "0xb08213bcf8ffe0000" + }, + "026432af37dc5113f1f46d480a4de0b28052237e": { + "balance": "0x1349b786e40bfc0000" + }, + "0266ab1c6b0216230b9395443d5fa75e684568c6": { + "balance": "0x3635c9adc5dea00000" + }, + "02751dc68cb5bd737027abf7ddb77390cd77c16b": { + "balance": "0x1158e460913d00000" + }, + "02778e390fa17510a3428af2870c4273547d386c": { + "balance": "0x36c3c66170c0d720000" + }, + "02ade5db22f8b758ee1443626c64ec2f32aa0a15": { + "balance": "0x43c33c1937564800000" + }, + "02af2459a93d0b3f4d062636236cd4b29e3bcecf": { + "balance": "0x678a932062e4180000" + }, + "02b1af72339b2a2256389fd64607de24f0de600a": { + "balance": "0x6c6b935b8bbd400000" + }, + "02b643d6fabd437a851accbe79abb7fde126dccf": { + "balance": "0x18650127cc3dc800000" + }, + "02b6d65cb00b7b36e1fb5ed3632c4cb20a894130": { + "balance": "0x43c33c1937564800000" + }, + "02b7b1d6b34ce053a40eb65cd4a4f7dddd0e9f30": { + "balance": "0x252248deb6e6940000" + }, + "02c9f7940a7b8b7a410bf83dc9c22333d4275dd3": { + "balance": "0x10f0cf064dd59200000" + }, + "02d4a30968a39e2b3498c3a6a4ed45c1c6646822": { + "balance": "0x6c6b935b8bbd400000" + }, + "02dfcb17a1b87441036374b762a5d3418b1cb4d4": { + "balance": "0x48b02ba9d1ba460000" + }, + "02e4cb22be46258a40e16d4338d802fffd00c151": { + "balance": "0x149696eaceba810000" + }, + "02e816afc1b5c0f39852131959d946eb3b07b5ad": { + "balance": "0x3635c9adc5dea00000" + }, + "02f7f67209b16a17550c694c72583819c80b54ad": { + "balance": "0x5559306a78a700000" + }, + "030973807b2f426914ad00181270acd27b8ff61f": { + "balance": "0x121ea68c114e5100000" + }, + "03097923ba155e16d82f3ad3f6b815540884b92c": { + "balance": "0x62a992e53a0af00000" + }, + "030fb3401f72bd3418b7d1da75bf8c519dd707dc": { + "balance": "0xa2a15d09519be00000" + }, + "031e25db516b0f099faebfd94f890cf96660836b": { + "balance": "0x6c6b935b8bbd400000" + }, + "0328510c09dbcd85194a98d67c33ac49f2f94d60": { + "balance": "0x2544faa778090e00000" + }, + "0329188f080657ab3a2afa522467178279832085": { + "balance": "0xbbf510ddfcb260000" + }, + "03317826d1f70aa4bddfa09be0c4105552d2358b": { + "balance": "0x21a754a6dc5280000" + }, + "03337012ae1d7ff3ee7f697c403e7780188bf0ef": { + "balance": "0xad78ebc5ac6200000" + }, + "03377c0e556b640103289a6189e1aeae63493467": { + "balance": "0x43c33c1937564800000" + }, + "0349634dc2a9e80c3f7721ee2b5046aeaaedfbb5": { + "balance": "0xd8d726b7177a800000" + }, + "0355bcacbd21441e95adeedc30c17218c8a408ce": { + "balance": "0x15af1d78b58c400000" + }, + "036eeff5ba90a6879a14dff4c5043b18ca0460c9": { + "balance": "0x56bc75e2d63100000" + }, + "03714b41d2a6f751008ef8dd4d2b29aecab8f36e": { + "balance": "0x14542ba12a337c00000" + }, + "0372e852582e0934344a0fed2178304df25d4628": { + "balance": "0x43c33c1937564800000" + }, + "0372ee5508bf8163ed284e5eef94ce4d7367e522": { + "balance": "0x56bc75e2d63100000" + }, + "037dd056e7fdbd641db5b6bea2a8780a83fae180": { + "balance": "0x796e3ea3f8ab00000" + }, + "038323b184cff7a82ae2e1bda7793fe4319ca0bf": { + "balance": "0x43c33c1937564800000" + }, + "038779ca2dbe663e63db3fe75683ea0ec62e2383": { + "balance": "0x5a87e7d7f5f6580000" + }, + "038e45eadd3d88b87fe4dab066680522f0dfc8f9": { + "balance": "0x21e19e0c9bab2400000" + }, + "0392549a727f81655429cb928b529f25df4d1385": { + "balance": "0x16c43a0eea0740000" + }, + "0394b90fadb8604f86f43fc1e35d3124b32a5989": { + "balance": "0x296aa140278e700000" + }, + "039e7a4ebc284e2ccd42b1bdd60bd6511c0f7706": { + "balance": "0xf015f25736420000" + }, + "039ef1ce52fe7963f166d5a275c4b1069fe3a832": { + "balance": "0x15af39e4aab2740000" + }, + "03a26cfc4c18316f70d59e9e1a79ee3e8b962f4c": { + "balance": "0x6c6b935b8bbd400000" + }, + "03aa622881236dd0f4940c24c324ff8b7b7e2186": { + "balance": "0xad78ebc5ac62000000" + }, + "03af7ad9d5223cf7c8c13f20df67ebe5ffc5bb41": { + "balance": "0xad78ebc5ac6200000" + }, + "03b0f17cd4469ddccfb7da697e82a91a5f9e7774": { + "balance": "0x1158e460913d00000" + }, + "03b41b51f41df20dd279bae18c12775f77ad771c": { + "balance": "0x3635c9adc5dea00000" + }, + "03be5b4629aefbbcab9de26d39576cb7f691d764": { + "balance": "0xadf30ba70c8970000" + }, + "03c647a9f929b0781fe9ae01caa3e183e876777e": { + "balance": "0x182ab7c20ce5240000" + }, + "03c91d92943603e752203e05340e566013b90045": { + "balance": "0x2b7cc2e9c3225c0000" + }, + "03cb4c4f4516c4ff79a1b6244fbf572e1c7fea79": { + "balance": "0x9489237adb9a500000" + }, + "03cb98d7acd817de9d886d22fab3f1b57d92a608": { + "balance": "0x56bc75e2d631000000" + }, + "03cc9d2d21f86b84ac8ceaf971dba78a90e62570": { + "balance": "0x57473d05dabae80000" + }, + "03d1724fd00e54aabcd2de2a91e8462b1049dd3a": { + "balance": "0x8f1d5c1cae37400000" + }, + "03dedfcd0b3c2e17c705da248790ef98a6bd5751": { + "balance": "0x487a9a304539440000" + }, + "03e8b084537557e709eae2e1e1a5a6bce1ef8314": { + "balance": "0x1158e460913d00000" + }, + "03ea6d26d080e57aee3926b18e8ed73a4e5b2826": { + "balance": "0xad78ebc5ac6200000" + }, + "03eb3cb860f6028da554d344a2bb5a500ae8b86f": { + "balance": "0x6c6b935b8bbd400000" + }, + "03ebc63fda6660a465045e235fbe6e5cf195735f": { + "balance": "0x7b06ce87fdd680000" + }, + "03ef6ad20ff7bd4f002bac58d47544cf879ae728": { + "balance": "0x175c758d0b96e5c0000" + }, + "03f7b92008813ae0a676eb212814afab35221069": { + "balance": "0x6c6b935b8bbd400000" + }, + "041170f581de80e58b2a045c8f7c1493b001b7cb": { + "balance": "0x303c74a1a336940000" + }, + "0413d0cf78c001898a378b918cd6e498ea773c4d": { + "balance": "0xf2dc7d47f15600000" + }, + "04241b41ecbd0bfdf1295e9d4fa59ea09e6c6186": { + "balance": "0x655f769450bc780000" + }, + "043707071e2ae21eed977891dc79cd5d8ee1c2da": { + "balance": "0x6c6b935b8bbd400000" + }, + "044e853144e3364495e7a69fa1d46abea3ac0964": { + "balance": "0x2ab2254b1dc9a8000" + }, + "0455dcec8a7fc4461bfd7f37456fce3f4c3caac7": { + "balance": "0x15af1d78b58c400000" + }, + "045ed7f6d9ee9f252e073268db022c6326adfc5b": { + "balance": "0x56bc75e2d63100000" + }, + "046377f864b0143f282174a892a73d3ec8ec6132": { + "balance": "0xa5aa85009e39c0000" + }, + "0469e8c440450b0e512626fe817e6754a8152830": { + "balance": "0x6c6b935b8bbd400000" + }, + "046d274b1af615fb505a764ad8dda770b1db2f3d": { + "balance": "0x6c6b935b8bbd400000" + }, + "047d5a26d7ad8f8e70600f70a398ddaa1c2db26f": { + "balance": "0x14542ba12a337c00000" + }, + "047e87c8f7d1fce3b01353a85862a948ac049f3e": { + "balance": "0x50c5e761a444080000" + }, + "047f9bf1529daf87d407175e6f171b5e59e9ff3e": { + "balance": "0x233c8fe42703e80000" + }, + "04852732b4c652f6c2e58eb36587e60a62da14db": { + "balance": "0x43c33c1937564800000" + }, + "048a8970ea4145c64d5517b8de5b46d0595aad06": { + "balance": "0x43c33c1937564800000" + }, + "049c5d4bc6f25d4e456c697b52a07811ccd19fb1": { + "balance": "0x104400a2470e680000" + }, + "04a1cada1cc751082ff8da928e3cfa000820a9e9": { + "balance": "0x22b1c8c1227a00000" + }, + "04a80afad53ef1f84165cfd852b0fdf1b1c24ba8": { + "balance": "0x324e964b3eca80000" + }, + "04aafc8ae5ce6f4903c89d7fac9cb19512224777": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "04ba4bb87140022c214a6fac42db5a16dd954045": { + "balance": "0x3635c9adc5dea00000" + }, + "04ba8a3f03f08b895095994dda619edaacee3e7a": { + "balance": "0x6c6b935b8bbd400000" + }, + "04c2c64bb54c3eccd05585e10ec6f99a0cdb01a3": { + "balance": "0x56bc75e2d63100000" + }, + "04ce45f600db18a9d0851b29d9393ebdaafe3dc5": { + "balance": "0x1158e460913d00000" + }, + "04d6b8d4da867407bb997749debbcdc0b358538a": { + "balance": "0x3635c9adc5dea00000" + }, + "04d73896cf6593a691972a13a6e4871ff2c42b13": { + "balance": "0x6c6b935b8bbd400000" + }, + "04d82af9e01a936d97f8f85940b970f9d4db9936": { + "balance": "0xad78ebc5ac6200000" + }, + "04e5f5bc7c923fd1e31735e72ef968fd67110c6e": { + "balance": "0x57551dbc8e624c0000" + }, + "04eca501630abce35218b174956b891ba25efb23": { + "balance": "0x36369ed7747d260000" + }, + "0505a08e22a109015a22f685305354662a5531d5": { + "balance": "0x8cf23f909c0fa00000" + }, + "0514954c3c2fb657f9a06f510ea22748f027cdd3": { + "balance": "0x15af1d78b58c400000" + }, + "051633080d07a557adde319261b074997f14692d": { + "balance": "0x13a6b2b564871a00000" + }, + "0517448dada761cc5ba4033ee881c83037036400": { + "balance": "0x6c4fd1ee246e780000" + }, + "051d424276b21239665186133d653bb8b1862f89": { + "balance": "0x3635c9adc5dea00000" + }, + "0521bc3a9f8711fecb10f50797d71083e341eb9d": { + "balance": "0x1158e460913d00000" + }, + "05236d4c90d065f9e3938358aaffd777b86aec49": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "052a58e035f1fe9cdd169bcf20970345d12b9c51": { + "balance": "0x50c5e761a444080000" + }, + "052eab1f61b6d45517283f41d1441824878749d0": { + "balance": "0xd8d726b7177a800000" + }, + "05336e9a722728d963e7a1cf2759fd0274530fca": { + "balance": "0x31a2443f888a798000" + }, + "053471cd9a41925b3904a5a8ffca3659e034be23": { + "balance": "0xad201a6794ff80000" + }, + "05361d8eb6941d4e90fb7e1418a95a32d5257732": { + "balance": "0x1158e460913d00000" + }, + "05423a54c8d0f9707e704173d923b946edc8e700": { + "balance": "0x6ea03c2bf8ba58000" + }, + "05440c5b073b529b4829209dff88090e07c4f6f5": { + "balance": "0x45d29737e22f200000" + }, + "055ab658c6f0ed4f875ed6742e4bc7292d1abbf0": { + "balance": "0x486cb9799191e0000" + }, + "055bd02caf19d6202bbcdc836d187bd1c01cf261": { + "balance": "0x56bc75e2d63100000" + }, + "055eac4f1ad3f58f0bd024d68ea60dbe01c6afb3": { + "balance": "0x56bc75e2d63100000" + }, + "05665155cc49cbf6aabdd5ae92cbfaad82b8c0c1": { + "balance": "0x15af1d78b58c400000" + }, + "056686078fb6bcf9ba0a8a8dc63a906f5feac0ea": { + "balance": "0x1b181e4bf2343c0000" + }, + "05696b73916bd3033e05521e3211dfec026e98e4": { + "balance": "0x6c6b935b8bbd400000" + }, + "056b1546894f9a85e203fb336db569b16c25e04f": { + "balance": "0x92edb09ff08d88000" + }, + "057949e1ca0570469e4ce3c690ae613a6b01c559": { + "balance": "0xad78ebc5ac6200000" + }, + "057dd29f2d19aa3da42327ea50bce86ff5c911d9": { + "balance": "0xd8d726b7177a800000" + }, + "057f7f81cd7a406fc45994408b5049912c566463": { + "balance": "0x5c283d410394100000" + }, + "05915d4e225a668162aee7d6c25fcfc6ed18db03": { + "balance": "0x398c37279259e0000" + }, + "0596a27dc3ee115fce2f94b481bc207a9e261525": { + "balance": "0x3635c9adc5dea00000" + }, + "05a830724302bc0f6ebdaa1ebeeeb46e6ce00b39": { + "balance": "0x556f64c1fe7fa0000" + }, + "05ae7fd4bbcc80ca11a90a1ec7a301f7cccc83db": { + "balance": "0x3154c9729d05780000" + }, + "05bb64a916be66f460f5e3b64332110d209e19ae": { + "balance": "0xe3aeb5737240a00000" + }, + "05bf4fcfe772e45b826443852e6c351350ce72a2": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "05c64004a9a826e94e5e4ee267fa2a7632dd4e6f": { + "balance": "0x36dc42ebff90b7f8000" + }, + "05c736d365aa37b5c0be9c12c8ad5cd903c32cf9": { + "balance": "0x1455e7b800a86880000" + }, + "05cb6c3b0072d3116761b532b218443b53e8f6c5": { + "balance": "0x1e02c3d7fca9b6280000" + }, + "05d0f4d728ebe82e84bf597515ad41b60bf28b39": { + "balance": "0xe3aeb5737240a00000" + }, + "05d68dad61d3bbdfb3f779265c49474aff3fcd30": { + "balance": "0x222c55dc1519d8000" + }, + "05e671de55afec964b074de574d5158d5d21b0a3": { + "balance": "0xd5967be4fc3f100000" + }, + "05e97b09492cd68f63b12b892ed1d11d152c0eca": { + "balance": "0x3708baed3d68900000" + }, + "05f3631f5664bdad5d0132c8388d36d7d8920918": { + "balance": "0x1158e460913d00000" + }, + "0609d83a6ce1ffc9b690f3e9a81e983e8bdc4d9d": { + "balance": "0xed2b525841adfc00000" + }, + "061ea4877cd08944eb64c2966e9db8dedcfec06b": { + "balance": "0x3635c9adc5dea00000" + }, + "0625d06056968b002206ff91980140242bfaa499": { + "balance": "0x3635c9adc5dea00000" + }, + "0628bfbe5535782fb588406bc96660a49b011af5": { + "balance": "0x52663ccab1e1c00000" + }, + "0631d18bbbbd30d9e1732bf36edae2ce8901ab80": { + "balance": "0xa3f98855ec39900000" + }, + "0631dc40d74e5095e3729eddf49544ecd4396f67": { + "balance": "0x8ac7230489e800000" + }, + "063759dd1c4e362eb19398951ff9f8fad1d31068": { + "balance": "0x21e19e0c9bab2400000" + }, + "065ff575fd9c16d3cb6fd68ffc8f483fc32ec835": { + "balance": "0xad78ebc5ac6200000" + }, + "06618e9d5762df62028601a81d4487d6a0ecb80e": { + "balance": "0x487a9a304539440000" + }, + "066647cfc85d23d37605573d208ca154b244d76c": { + "balance": "0x21e19e0c9bab2400000" + }, + "0678654ac6761db904a2f7e8595ec1eaac734308": { + "balance": "0x2f98b29c2818f80000" + }, + "06860a93525955ff624940fadcffb8e149fd599c": { + "balance": "0x6c68ccd09b022c0000" + }, + "068ce8bd6e902a45cb83b51541b40f39c4469712": { + "balance": "0x11c0f9bad4a46e00000" + }, + "068e29b3f191c812a6393918f71ab933ae6847f2": { + "balance": "0x6c6acc67d7b1d40000" + }, + "068e655766b944fb263619658740b850c94afa31": { + "balance": "0x1e87f85809dc00000" + }, + "06964e2d17e9189f88a8203936b40ac96e533c06": { + "balance": "0xfc936392801c0000" + }, + "06994cd83aa2640a97b2600b41339d1e0d3ede6c": { + "balance": "0xd8d726b7177a80000" + }, + "069ed0ab7aa77de571f16106051d92afe195f2d0": { + "balance": "0xad78ebc5ac6200000" + }, + "06ac26ad92cb859bd5905ddce4266aa0ec50a9c5": { + "balance": "0x2a034919dfbfbc0000" + }, + "06b0c1e37f5a5ec4bbf50840548f9d3ac0288897": { + "balance": "0xd8d882e1928e7d0000" + }, + "06b0ff834073cce1cbc9ea557ea87b605963e8b4": { + "balance": "0x1043561a8829300000" + }, + "06b106649aa8c421ddcd1b8c32cd0418cf30da1f": { + "balance": "0x878678326eac9000000" + }, + "06b5ede6fdf1d6e9a34721379aeaa17c713dd82a": { + "balance": "0x6c6b935b8bbd400000" + }, + "06cbfa08cdd4fba737bac407be8224f4eef35828": { + "balance": "0x202be5e8382e8b8000" + }, + "06d6cb308481c336a6e1a225a912f6e6355940a1": { + "balance": "0x5f68e8131ecf800000" + }, + "06dc7f18cee7edab5b795337b1df6a9e8bd8ae59": { + "balance": "0x15af1d78b58c400000" + }, + "06f68de3d739db41121eacf779aada3de8762107": { + "balance": "0x18493fba64ef00000" + }, + "06f7dc8d1b9462cef6feb13368a7e3974b097f9f": { + "balance": "0x6c6b935b8bbd400000" + }, + "0701f9f147ec486856f5e1b71de9f117e99e2105": { + "balance": "0x965da717fd5b80000" + }, + "070d5d364cb7bbf822fc2ca91a35bdd441b215d5": { + "balance": "0x6c6b935b8bbd400000" + }, + "071dd90d14d41f4ff7c413c24238d3359cd61a07": { + "balance": "0x7b53f79e888dac00000" + }, + "0726c42e00f45404836eb1e280d073e7059687f5": { + "balance": "0x58003e3fb947a38000" + }, + "0727be0a2a00212048b5520fbefb953ebc9d54a0": { + "balance": "0x21e19e0c9bab2400000" + }, + "0729a8a4a5ba23f579d0025b1ad0f8a0d35cdfd2": { + "balance": "0x20dd68aaf3289100000" + }, + "0729b4b47c09eb16158464c8aa7fd9690b438839": { + "balance": "0x6c68ccd09b022c0000" + }, + "0734a0a81c9562f4d9e9e10a8503da15db46d76e": { + "balance": "0xfc936392801c0000" + }, + "073c67e09b5c713c5221c8a0c7f3f74466c347b0": { + "balance": "0x41bad155e6512200000" + }, + "073f1ed1c9c3e9c52a9b0249a5c1caa0571fdf05": { + "balance": "0x3d0ff0b013b800000" + }, + "0748713145ef83c3f0ef4d31d823786f7e9cc689": { + "balance": "0xf3f20b8dfa69d00000" + }, + "075d15e2d33d8b4fa7dba8b9e607f04a261e340b": { + "balance": "0x678a932062e4180000" + }, + "076561a856455d7ef86e63f87c73dbb628a55f45": { + "balance": "0x30ca024f987b900000" + }, + "076ee99d3548623a03b5f99859d2d785a1778d48": { + "balance": "0xad78ebc5ac6200000" + }, + "0770b43dbae4b1f35a927b4fa8124d3866caf97b": { + "balance": "0x37193ea7ef5b470000" + }, + "0770c61be78772230cb5a3bb2429a72614a0b336": { + "balance": "0x16ee0a299b713418000" + }, + "07723e3c30e8b731ee456a291ee0e798b0204a77": { + "balance": "0x6c6b935b8bbd400000" + }, + "0773eeacc050f74720b4a1bd57895b1cceeb495d": { + "balance": "0x21e19e0c9bab2400000" + }, + "07800d2f8068e448c79a4f69b1f15ef682aae5f6": { + "balance": "0x41bad155e6512200000" + }, + "07a8dadec142571a7d53a4297051786d072cba55": { + "balance": "0x13b6da1139bda8000" + }, + "07af938c1237a27c9030094dcf240750246e3d2c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "07b1a306cb4312df66482c2cae72d1e061400fcd": { + "balance": "0x43c33c1937564800000" + }, + "07b7a57033f8f11330e4665e185d234e83ec140b": { + "balance": "0xea7ee92a0c9a0b8000" + }, + "07bc2cc8eedc01970700efc9c4fb36735e98cd71": { + "balance": "0xd8d726b7177a800000" + }, + "07d41217badca5e0e60327d845a3464f0f27f84a": { + "balance": "0xd8d726b7177a800000" + }, + "07d4334ec385e8aa54eedaeadb30022f0cdfa4ab": { + "balance": "0x8e91d520f2eb790000" + }, + "07dae622630d1136381933d2ad6b22b839d82102": { + "balance": "0xad78ebc5ac6200000" + }, + "07dc2bf83bc6af19a842ffea661af5b41b67fda1": { + "balance": "0x5150ae84a8cdf00000" + }, + "07dc8c8b927adbedfa8f5d639b4352351f2f36d2": { + "balance": "0x110aed3b5530db0000" + }, + "07ddd0422c86ef65bf0c7fc3452862b1228b08b8": { + "balance": "0x6ff5d2aa8f9fcf0000" + }, + "07e1162ceae3cf21a3f62d105990302e307f4e3b": { + "balance": "0x52f103edb66ba80000" + }, + "07e2b4cdeed9d087b12e556d9e770c13c099615f": { + "balance": "0x243d4d18229ca20000" + }, + "07feef54c136850829badc4b49c3f2a73c89fb9e": { + "balance": "0x6685ac1bfe32c0000" + }, + "080546508a3d2682c8b9884f13637b8847b44db3": { + "balance": "0x6c6b935b8bbd400000" + }, + "08090876baadfee65c3d363ba55312748cfa873d": { + "balance": "0x5c2a99371cffe10000" + }, + "08166f02313feae18bb044e7877c808b55b5bf58": { + "balance": "0x6acb3df27e1f880000" + }, + "0829d0f7bb7c446cfbb0deadb2394d9db7249a87": { + "balance": "0x22ca3587cf4eb0000" + }, + "08306de51981e7aca1856859b7c778696a6b69f9": { + "balance": "0xad78ebc5ac62000000" + }, + "0837539b5f6a522a482cdcd3a9bb7043af39bdd2": { + "balance": "0x14542ba12a337c00000" + }, + "0838a7768d9c2aca8ba279adfee4b1f491e326f1": { + "balance": "0xad78ebc5ac6200000" + }, + "08411652c871713609af0062a8a1281bf1bbcfd9": { + "balance": "0x4be4e7267b6ae00000" + }, + "084d103254759b343cb2b9c2d8ff9e1ac5f14596": { + "balance": "0x19bff2ff57968c00000" + }, + "08504f05643fab5919f5eea55925d7a3ed7d807a": { + "balance": "0x1158e460913d00000" + }, + "085b4ab75d8362d914435cedee1daa2b1ee1a23b": { + "balance": "0xd255d112e103a00000" + }, + "085ba65febe23eefc2c802666ab1262382cfc494": { + "balance": "0x15af1d78b58c400000" + }, + "087498c0464668f31150f4d3c4bcdda5221ba102": { + "balance": "0x1158e460913d00000" + }, + "0877eeaeab78d5c00e83c32b2d98fa79ad51482f": { + "balance": "0x17d22d71da62260000" + }, + "08936a37df85b3a158cafd9de021f58137681347": { + "balance": "0xfc936392801c0000" + }, + "08a9a44e1f41de3dbba7a363a3ab412c124cd15e": { + "balance": "0xad78ebc5ac6200000" + }, + "08b7bdcf944d5570838be70460243a8694485858": { + "balance": "0x6c6b935b8bbd400000" + }, + "08b84536b74c8c01543da88b84d78bb95747d822": { + "balance": "0xad78ebc5ac6200000" + }, + "08c2f236ac4adcd3fda9fbc6e4532253f9da3bec": { + "balance": "0x1158e460913d00000" + }, + "08c802f87758349fa03e6bc2e2fd0791197eea9a": { + "balance": "0x6c6b935b8bbd400000" + }, + "08c9f1bfb689fdf804d769f82123360215aff93b": { + "balance": "0x6acb3df27e1f880000" + }, + "08cac8952641d8fc526ec1ab4f2df826a5e7710f": { + "balance": "0x1043561a8829300000" + }, + "08ccda50e4b26a0ffc0ef92e9205310706bec2c7": { + "balance": "0x149756c3857c6000000" + }, + "08d0864dc32f9acb36bf4ea447e8dd6726906a15": { + "balance": "0x6c6e59e67c78540000" + }, + "08d4267feb15da9700f7ccc3c84a8918bf17cfde": { + "balance": "0x61093d7c2c6d380000" + }, + "08d4311c9c1bbaf87fabe1a1d01463828d5d98ce": { + "balance": "0x130ee8e7179044400000" + }, + "08d54e83ad486a934cfaeae283a33efd227c0e99": { + "balance": "0x38530583245edc0000" + }, + "08d97eadfcb7b064e1ccd9c8979fbee5e77a9719": { + "balance": "0xe6c5da8d67ac18000" + }, + "08da3a7a0f452161cfbcec311bb68ebfdee17e88": { + "balance": "0x6c6b935b8bbd400000" + }, + "08e38ee0ce48c9ca645c1019f73b5355581c56e6": { + "balance": "0x56bc75e2d631000000" + }, + "08ef3fa4c43ccdc57b22a4b9b2331a82e53818f2": { + "balance": "0xd8d726b7177a800000" + }, + "0909648c18a3ce5bae7a047ec2f868d24cdda81d": { + "balance": "0xcf152640c5c8300000" + }, + "090cd67b60e81d54e7b5f6078f3e021ba65b9a1e": { + "balance": "0x3635c9adc5dea00000" + }, + "090cebef292c3eb081a05fd8aaf7d39bf07b89d4": { + "balance": "0xd8d726b7177a800000" + }, + "090fa9367bda57d0d3253a0a8ff76ce0b8e19a73": { + "balance": "0x3635c9adc5dea00000" + }, + "09146ea3885176f07782e1fe30dce3ce24c49e1f": { + "balance": "0x1158e460913d00000" + }, + "0921605f99164e3bcc28f31caece78973182561d": { + "balance": "0x2b07692a9065a80000" + }, + "09261f9acb451c3788844f0c1451a35bad5098e3": { + "balance": "0x1d5ad27502920600000" + }, + "0927220492194b2eda9fc4bbe38f25d681dfd36c": { + "balance": "0x14542ba12a337c00000" + }, + "092acb624b08c05510189bbbe21e6524d644ccad": { + "balance": "0xfc936392801c0000" + }, + "092e815558402d67f90d6bfe6da0b2fffa91455a": { + "balance": "0x340aad21b3b700000" + }, + "095030e4b82692dcf8b8d0912494b9b378ec9328": { + "balance": "0x48a43c54602f700000" + }, + "095270cc42141dd998ad2862dbd1fe9b44e7e650": { + "balance": "0x410d586a20a4c00000" + }, + "095457f8ef8e2bdc362196b9a9125da09c67e3ab": { + "balance": "0xad78ebc5ac6200000" + }, + "0954a8cb5d321fc3351a7523a617d0f58da676a7": { + "balance": "0x87d9bc7aa498e80000" + }, + "095b0ea2b218d82e0aea7c2889238a39c9bf9077": { + "balance": "0x43c33c1937564800000" + }, + "095b949de3333a377d5019d893754a5e4656ff97": { + "balance": "0x126e72a69a50d00000" + }, + "095e0174829f34c3781be1a5e38d1541ea439b7f": { + "balance": "0x14542ba12a337c00000" + }, + "095f5a51d06f6340d80b6d29ea2e88118ad730fe": { + "balance": "0x6c6e59e67c78540000" + }, + "0968ee5a378f8cadb3bafdbed1d19aaacf936711": { + "balance": "0x3635c9adc5dea00000" + }, + "0977bfba038a44fb49b03970d8d8cf2cb61f8b25": { + "balance": "0x16c4abbebea0100000" + }, + "097da12cfc1f7c1a2464def08c29bed5e2f851e9": { + "balance": "0x1158e460913d00000" + }, + "097ecda22567c2d91cb03f8c5215c22e9dcda949": { + "balance": "0x11651ac3e7a758000" + }, + "0989c200440b878991b69d6095dfe69e33a22e70": { + "balance": "0x678a932062e4180000" + }, + "0990e81cd785599ea236bd1966cf526302c35b9c": { + "balance": "0x3635c9adc5dea00000" + }, + "0998d8273115b56af43c505e087aff0676ed3659": { + "balance": "0xd8d6eddf2d2e180000" + }, + "09a025316f967fa8b9a1d60700063f5a68001caa": { + "balance": "0x21221a99b93ec0000" + }, + "09a928d528ec1b3e25ffc83e218c1e0afe8928c7": { + "balance": "0xfc936392801c0000" + }, + "09ae49e37f121df5dc158cfde806f173a06b0c7f": { + "balance": "0xd8309e26aba1d00000" + }, + "09afa73bc047ef46b977fd9763f87286a6be68c6": { + "balance": "0x1b2fb5e8f06a660000" + }, + "09b4668696f86a080f8bebb91db8e6f87015915a": { + "balance": "0x238ff7b34f60010000" + }, + "09b59b8698a7fbd3d2f8c73a008988de3e406b2b": { + "balance": "0x878678326eac9000000" + }, + "09b7a988d13ff89186736f03fdf46175b53d16e0": { + "balance": "0x14542ba12a337c00000" + }, + "09c177f1ae442411ddacf187d46db956148360e7": { + "balance": "0x1e52e336cde22180000" + }, + "09c88f917e4d6ad473fa12e98ea3c4472a5ed6da": { + "balance": "0x21e19e0c9bab2400000" + }, + "09d0b8cd077c69d9f32d9cca43b3c208a21ed48b": { + "balance": "0x821d221b5291f8000" + }, + "09d6cefd75b0c4b3f8f1d687a522c96123f1f539": { + "balance": "0x14542ba12a337c00000" + }, + "09e437d448861228a232b62ee8d37965a904ed9c": { + "balance": "0x498cf401df8842e8000" + }, + "09ee12b1b42b05af9cf207d5fcac255b2ec411f2": { + "balance": "0x331cddd47e0fe8000" + }, + "09f3f601f605441140586ce0656fa24aa5b1d9ae": { + "balance": "0x5373776fe8c4540000" + }, + "09f9575be57d004793c7a4eb84b71587f97cbb6a": { + "balance": "0xad78ebc5ac6200000" + }, + "0a0650861f785ed8e4bf1005c450bbd06eb48fb6": { + "balance": "0xa6413b79144e7e0000" + }, + "0a06fad7dcd7a492cbc053eeabde6934b39d8637": { + "balance": "0x1158e460913d00000" + }, + "0a077db13ffeb09484c217709d5886b8bf9c5a8b": { + "balance": "0xd8d726b7177a800000" + }, + "0a0ecda6636f7716ef1973614687fd89a820a706": { + "balance": "0x155bd9307f9fe80000" + }, + "0a29a8a4d5fd950075ffb34d77afeb2d823bd689": { + "balance": "0xad78ebc5ac6200000" + }, + "0a2ade95b2e8c66d8ae6f0ba64ca57d783be6d44": { + "balance": "0xd8d726b7177a800000" + }, + "0a2b4fc5d81ace67dc4bba03f7b455413d46fe3d": { + "balance": "0xaadec983fcff40000" + }, + "0a2dcb7a671701dbb8f495728088265873356c8e": { + "balance": "0x83f16ce08a06c0000" + }, + "0a3de155d5ecd8e81c1ff9bbf0378301f8d4c623": { + "balance": "0xd8d726b7177a800000" + }, + "0a47ad9059a249fc936b2662353da6905f75c2b9": { + "balance": "0x6c6b935b8bbd400000" + }, + "0a48296f7631708c95d2b74975bc4ab88ac1392a": { + "balance": "0x10f0cf064dd59200000" + }, + "0a4a011995c681bc999fdd79754e9a324ae3b379": { + "balance": "0x8c19ab06eb89af60000" + }, + "0a58fddd71898de773a74fdae45e7bd84ef43646": { + "balance": "0x1158e460913d00000" + }, + "0a5b79d8f23b6483dbe2bdaa62b1064cc76366ae": { + "balance": "0x6ac882100952c78000" + }, + "0a652e2a8b77bd97a790d0e91361c98890dbb04e": { + "balance": "0x3635c9adc5dea00000" + }, + "0a6ebe723b6ed1f9a86a69ddda68dc47465c2b1b": { + "balance": "0x403d2db599d5e40000" + }, + "0a77e7f72b437b574f00128b21f2ac265133528c": { + "balance": "0x6c6b935b8bbd400000" + }, + "0a917f3b5cb0b883047fd9b6593dbcd557f453b9": { + "balance": "0x3635c9adc5dea00000" + }, + "0a931b449ea8f12cdbd5e2c8cc76bad2c27c0639": { + "balance": "0x13f9e8c79fe058000" + }, + "0a9804137803ba6868d93a55f9985fcd540451e4": { + "balance": "0xb98bc829a6f90000" + }, + "0a9ab2638b1cfd654d25dab018a0aebddf85fd55": { + "balance": "0x12e8cb5fe4c4a8000" + }, + "0ab366e6e7d5abbce6b44a438d69a1cabb90d133": { + "balance": "0x1158e460913d000000" + }, + "0ab4281ebb318590abb89a81df07fa3af904258a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "0ab59d390702c9c059db148eb4f3fcfa7d04c7e7": { + "balance": "0xfc936392801c0000" + }, + "0abfb39b11486d79572866195ba26c630b6784db": { + "balance": "0x19ba8737f96928f00000" + }, + "0aca9a5626913b08cfc9a66d40508dce52b60f87": { + "balance": "0x678a932062e4180000" + }, + "0ad3e44d3c001fa290b393617030544108ac6eb9": { + "balance": "0x6abda0bc30b2df8000" + }, + "0aec2e426ed6cc0cf3c249c1897eac47a7faa9bd": { + "balance": "0xad78ebc5ac6200000" + }, + "0af65f14784e55a6f95667fd73252a1c94072d2a": { + "balance": "0xa763b8e02d44f8000" + }, + "0af6c8d539c96d50259e1ba6719e9c8060f388c2": { + "balance": "0x3635c9adc5dea00000" + }, + "0b06390f2437b20ec4a3d3431b3279c6583e5ed7": { + "balance": "0xa844a7424d9c80000" + }, + "0b0b3862112aeec3a03492b1b05f440eca54256e": { + "balance": "0xd8d726b7177a800000" + }, + "0b0e055b28cbd03dc5ff44aa64f3dce04f5e63fb": { + "balance": "0x6c6b935b8bbd400000" + }, + "0b119df99c6b8de58a1e2c3f297a6744bf552277": { + "balance": "0x6c6b935b8bbd400000" + }, + "0b14891999a65c9ef73308efe3100ca1b20e8192": { + "balance": "0x2b5e3af16b18800000" + }, + "0b2113504534642a1daf102eee10b9ebde76e261": { + "balance": "0x942cdd7c95f2bd8000" + }, + "0b288a5a8b75f3dc4191eb0457e1c83dbd204d25": { + "balance": "0x10714e77bb43ab40000" + }, + "0b369e002e1b4c7913fcf00f2d5e19c58165478f": { + "balance": "0x37f6516288c340000" + }, + "0b43bd2391025581d8956ce42a072579cbbfcb14": { + "balance": "0x104e70464b1580000" + }, + "0b507cf553568daaf65504ae4eaa17a8ea3cdbf5": { + "balance": "0x6c6b935b8bbd400000" + }, + "0b5d66b13c87b392e94d91d5f76c0d450a552843": { + "balance": "0x6c6b935b8bbd400000" + }, + "0b5e2011ebc25a007f21362960498afb8af280fb": { + "balance": "0x6c6b935b8bbd400000" + }, + "0b649da3b96a102cdc6db652a0c07d65b1e443e6": { + "balance": "0x6c6b935b8bbd400000" + }, + "0b6920a64b363b8d5d90802494cf564b547c430d": { + "balance": "0x410d586a20a4c00000" + }, + "0b701101a4109f9cb360dc57b77442673d5e5983": { + "balance": "0x6c6b935b8bbd400000" + }, + "0b71f554122469ef978e2f1fefd7cbb410982772": { + "balance": "0xd255d112e103a00000" + }, + "0b7bb342f01bc9888e6a9af4a887cbf4c2dd2caf": { + "balance": "0x3635c9adc5dea000000" + }, + "0b7d339371e5be6727e6e331b5821fa24bdb9d5a": { + "balance": "0x2e7f81868262010000" + }, + "0b7fc9ddf70576f6330669eaaa71b6a831e99528": { + "balance": "0x796e3ea3f8ab00000" + }, + "0b80fc70282cbdd5fde35bf78984db3bdb120188": { + "balance": "0x3638021cecdab00000" + }, + "0b924df007e9c0878417cfe63b976ea1a382a897": { + "balance": "0x22b1c8c1227a00000" + }, + "0b93fca4a4f09cac20db60e065edcccc11e0a5b6": { + "balance": "0xad78ebc5ac6200000" + }, + "0b9df80fbe232009dacf0aa8cac59376e2476203": { + "balance": "0x6c6b935b8bbd400000" + }, + "0ba6e46af25a13f57169255a34a4dac7ce12be04": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "0ba8705bf55cf219c0956b5e3fc01c4474a6cdc1": { + "balance": "0x525e0595d4d6b8000" + }, + "0baf6ecdb91acb3606a8357c0bc4f45cfd2d7e6f": { + "balance": "0x3635c9adc5dea00000" + }, + "0bb05f7224bb5804856556c07eeadbed87ba8f7c": { + "balance": "0x15be6174e1912e0000" + }, + "0bb0c12682a2f15c9b5741b2385cbe41f034068e": { + "balance": "0x5150ae84a8cdf00000" + }, + "0bb25ca7d188e71e4d693d7b170717d6f8f0a70a": { + "balance": "0x124302a82fadd70000" + }, + "0bb2650ea01aca755bc0c017b64b1ab5a66d82e3": { + "balance": "0x487a9a304539440000" + }, + "0bb54c72fd6610bfa4363397e020384b022b0c49": { + "balance": "0x487a9a304539440000" + }, + "0bb7160aba293762f8734f3e0326ffc9a4cac190": { + "balance": "0x3635c9adc5dea00000" + }, + "0bc95cb32dbb574c832fa8174a81356d38bc92ac": { + "balance": "0x6c6b935b8bbd400000" + }, + "0bd67dbde07a856ebd893b5edc4f3a5be4202616": { + "balance": "0x6c6b935b8bbd400000" + }, + "0bdbc54cc8bdbbb402a08911e2232a5460ce866b": { + "balance": "0xa2a15d09519be00000" + }, + "0bdd58b96e7c916dd2fb30356f2aebfaaf1d8630": { + "balance": "0x6c6b935b8bbd400000" + }, + "0be1bcb90343fae5303173f461bd914a4839056c": { + "balance": "0x14542ba12a337c00000" + }, + "0be1fdf626ee6189102d70d13b31012c95cd1cd6": { + "balance": "0x6c6b935b8bbd400000" + }, + "0be2b94ad950a2a62640c35bfccd6c67dae450f6": { + "balance": "0x692ae8897081d00000" + }, + "0be6a09e4307fe48d412b8d1a1a8284dce486261": { + "balance": "0x40fbff85c0138300000" + }, + "0befb54707f61b2c9fb04715ab026e1bb72042bd": { + "balance": "0xd8d726b7177a800000" + }, + "0bf064428f83626722a7b5b26a9ab20421a7723e": { + "balance": "0x73f75d1a085ba0000" + }, + "0bfbb6925dc75e52cf2684224bbe0550fea685d3": { + "balance": "0x6acb3df27e1f880000" + }, + "0c088006c64b30c4ddafbc36cb5f05469eb62834": { + "balance": "0x6c6b935b8bbd400000" + }, + "0c2073ba44d3ddbdb639c04e191039a71716237f": { + "balance": "0x4d853c8f8908980000" + }, + "0c222c7c41c9b048efcce0a232434362e12d673b": { + "balance": "0x21e8359697677380000" + }, + "0c2808b951ed9e872d7b32790fcc5994ae41ffdc": { + "balance": "0x15996e5b3cd6b3c00000" + }, + "0c28847e4f09dfce5f9b25af7c4e530f59c880fe": { + "balance": "0x3635c9adc5dea00000" + }, + "0c2d5c920538e953caaf24f0737f554cc6927742": { + "balance": "0x3635c9adc5dea00000" + }, + "0c30cacc3f72269f8b4f04cf073d2b05a83d9ad1": { + "balance": "0x6c7974123f64a40000" + }, + "0c3239e2e841242db989a61518c22247e8c55208": { + "balance": "0xe4af6471734640000" + }, + "0c480de9f7461002908b49f60fc61e2b62d3140b": { + "balance": "0x21e19e0c9bab2400000" + }, + "0c48ae62d1539788eba013d75ea60b64eeba4e80": { + "balance": "0x77fbdc43e030998000" + }, + "0c5589a7a89b9ad15b02751930415948a875fbef": { + "balance": "0x6d499ec6c63380000" + }, + "0c67033dd8ee7f0c8ae534d42a51f7d9d4f7978f": { + "balance": "0xad78ebc5ac6200000" + }, + "0c6845bf41d5ee273c3ee6b5b0d69f6fd5eabbf7": { + "balance": "0xa2a1b9682e58090000" + }, + "0c7f869f8e90d53fdc03e8b2819b016b9d18eb26": { + "balance": "0x43c33c1937564800000" + }, + "0c8692eeff2a53d6d1688ed56a9ddbbd68dabba1": { + "balance": "0x6c6b935b8bbd400000" + }, + "0c8f66c6017bce5b20347204b602b743bad78d60": { + "balance": "0x6c6b935b8bbd400000" + }, + "0c8fd7775e54a6d9c9a3bf890e761f6577693ff0": { + "balance": "0x215f835bc769da80000" + }, + "0c925ad5eb352c8ef76d0c222d115b0791b962a1": { + "balance": "0xac635d7fa34e300000" + }, + "0c967e3061b87a753e84507eb60986782c8f3013": { + "balance": "0x56bc75e2d63100000" + }, + "0ca12ab0b9666cf0cec6671a15292f2653476ab2": { + "balance": "0x2c7827c42d22d07c0000" + }, + "0ca670eb2c8b96cba379217f5929c2b892f39ef6": { + "balance": "0x6c6b935b8bbd400000" + }, + "0cae108e6db99b9e637876b064c6303eda8a65c8": { + "balance": "0xa2a15d09519be00000" + }, + "0cbd921dbe121563b98a6871fecb14f1cc7e88d7": { + "balance": "0xad78ebc5ac6200000" + }, + "0cbf8770f0d1082e5c20c5aead34e5fca9ae7ae2": { + "balance": "0x3635c9adc5dea00000" + }, + "0cc67f8273e1bae0867fd42e8b8193d72679dbf8": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "0cd6a141918d126b106d9f2ebf69e102de4d3277": { + "balance": "0x1158e460913d00000" + }, + "0cda12bf72d461bbc479eb92e6491d057e6b5ad1": { + "balance": "0x21e19e0c9bab2400000" + }, + "0cdc960b998c141998160dc179b36c15d28470ed": { + "balance": "0x1b1b6bd7af64c70000" + }, + "0cfb172335b16c87d519cd1475530d20577f5e0e": { + "balance": "0x152d02c7e14af6800000" + }, + "0d1f2a57713ebc6e94de29846e8844d376665763": { + "balance": "0x10f0cf064dd59200000" + }, + "0d3265d3e7bdb93d5e8e8b1ca47f210a793ecc8e": { + "balance": "0xad78ebc5ac6200000" + }, + "0d35408f226566116fb8acdaa9e2c9d59b76683f": { + "balance": "0x32f51edbaaa3300000" + }, + "0d551ec1a2133c981d5fc6a8c8173f9e7c4f47af": { + "balance": "0x6c6b935b8bbd400000" + }, + "0d5d98565c647ca5f177a2adb9d3022fac287f21": { + "balance": "0xad78ebc5ac6200000" + }, + "0d658014a199061cf6b39433140303c20ffd4e5a": { + "balance": "0x1bc85dc2a89bb200000" + }, + "0d678706d037187f3e22e6f69b99a592d11ebc59": { + "balance": "0x55a6e79ccd1d300000" + }, + "0d69100c395ce6c5eaadf95d05d872837ededd21": { + "balance": "0x15af1d78b58c400000" + }, + "0d747ee5969bf79d57381d6fe3a2406cd0d8ce27": { + "balance": "0x152d02c7e14af6800000" + }, + "0d8023929d917234ae40512b1aabb5e8a4512771": { + "balance": "0x805e99fdcc5d00000" + }, + "0d8aab8f74ea862cdf766805009d3f3e42d8d00b": { + "balance": "0x13b80b99c5185700000" + }, + "0d8c40a79e18994ff99ec251ee10d088c3912e80": { + "balance": "0x63664fcd2bbc40000" + }, + "0d8ed7d0d15638330ed7e4eaccab8a458d75737e": { + "balance": "0x6c6b935b8bbd400000" + }, + "0d92582fdba05eabc3e51538c56db8813785b328": { + "balance": "0xa5aa85009e39c0000" + }, + "0d9443a79468a5bbf7c13c6e225d1de91aee07df": { + "balance": "0x3cb71f51fc5580000" + }, + "0d9a825ff2bcd397cbad5b711d9dcc95f1cc112d": { + "balance": "0x2b5e3af16b188000000" + }, + "0d9d3f9bc4a4c6efbd59679b69826bc1f63d9916": { + "balance": "0x2086ac351052600000" + }, + "0da532c910e3ac0dfb14db61cd739a93353fd05f": { + "balance": "0x4878be1ffaf95d0000" + }, + "0da7401262384e2e8b4b26dd154799b55145efa0": { + "balance": "0x1043561a8829300000" + }, + "0dae3ee5b915b36487f9161f19846d101433318a": { + "balance": "0x678a932062e4180000" + }, + "0dbd417c372b8b0d01bcd944706bd32e60ae28d1": { + "balance": "0x126e72a69a50d00000" + }, + "0dc100b107011c7fc0a1339612a16ccec3285208": { + "balance": "0x6c6b935b8bbd400000" + }, + "0dcf9d8c9804459f647c14138ed50fad563b4154": { + "balance": "0x960db77681e940000" + }, + "0dcfe837ea1cf28c65fccec3bef1f84e59d150c0": { + "balance": "0xad78ebc5ac6200000" + }, + "0dd4e674bbadb1b0dc824498713dce3b5156da29": { + "balance": "0x93739534d28680000" + }, + "0dfbd4817050d91d9d625c02053cf61a3ee28572": { + "balance": "0x126e72a69a50d00000" + }, + "0e024e7f029c6aaf3a8b910f5e080873b85795aa": { + "balance": "0x3635c9adc5dea00000" + }, + "0e09646c99af438e99fa274cb2f9c856cb65f736": { + "balance": "0x678a932062e4180000" + }, + "0e0c9d005ea016c295cd795cc9213e87febc33eb": { + "balance": "0xabbcd4ef377580000" + }, + "0e0d6633db1e0c7f234a6df163a10e0ab39c200f": { + "balance": "0xad78ebc5ac6200000" + }, + "0e11d77a8977fac30d268445e531149b31541a24": { + "balance": "0x6c6b935b8bbd400000" + }, + "0e123d7da6d1e6fac2dcadd27029240bb39052fe": { + "balance": "0x3635c9adc5dea00000" + }, + "0e1801e70b6262861b1134ccbc391f568afc92f7": { + "balance": "0xd8d726b7177a800000" + }, + "0e2094ac1654a46ba1c4d3a40bb8c17da7f39688": { + "balance": "0x13683f7f3c15d80000" + }, + "0e21af1b8dbf27fcf63f37e047b87a825cbe7c27": { + "balance": "0xa2a15d09519be00000" + }, + "0e2e504a2d1122b5a9feee5cb1451bf4c2ace87b": { + "balance": "0xd5967be4fc3f100000" + }, + "0e2f8e28a681f77c583bd0ecde16634bdd7e00cd": { + "balance": "0x52738f659bca20000" + }, + "0e320219838e859b2f9f18b72e3d4073ca50b37d": { + "balance": "0x6c6b935b8bbd400000" + }, + "0e33fcbbc003510be35785b52a9c5d216bc005f4": { + "balance": "0x65ea3db75546600000" + }, + "0e3696cf1f4217b163d1bc12a5ea730f1c32a14a": { + "balance": "0xd8d726b7177a800000" + }, + "0e390f44053ddfcef0d608b35e4d9c2cbe9871bb": { + "balance": "0x6acb3df27e1f880000" + }, + "0e3a28c1dfafb0505bdce19fe025f506a6d01ceb": { + "balance": "0x6c6b935b8bbd400000" + }, + "0e3dd7d4e429fe3930a6414035f52bdc599d784d": { + "balance": "0x22ca3587cf4eb0000" + }, + "0e4765790352656bc656682c24fc5ef3e76a23c7": { + "balance": "0x286d7fc0cb4f50000" + }, + "0e498800447177b8c8afc3fdfa7f69f4051bb629": { + "balance": "0x7405b69b8de5610000" + }, + "0e6baaa3deb989f289620076668618e9ac332865": { + "balance": "0xad78ebc5ac6200000" + }, + "0e6cd664ad9c1ed64bf98749f40644b626e3792c": { + "balance": "0xcb49b44ba602d800000" + }, + "0e6dfd553b2e873d2aec15bd5fbb3f8472d8d394": { + "balance": "0x28a857425466f800000" + }, + "0e6ec313376271dff55423ab5422cc3a8b06b22b": { + "balance": "0xd8d726b7177a800000" + }, + "0e6ece99111cad1961c748ed3df51edd69d2a3b1": { + "balance": "0x152d02c7e14af6800000" + }, + "0e83b850481ab44d49e0a229a2e464902c69539b": { + "balance": "0x56bc75e2d63100000" + }, + "0e89eddd3fa0d71d8ab0ff8da5580686e3d4f74f": { + "balance": "0x6c6b935b8bbd400000" + }, + "0e9096d343c060db581a120112b278607ec6e52b": { + "balance": "0x1158e460913d00000" + }, + "0e9c511864a177f49be78202773f60489fe04e52": { + "balance": "0x14542ba12a337c00000" + }, + "0ea2a210312b3e867ee0d1cc682ce1d666f18ed5": { + "balance": "0x21e19e0c9bab2400000" + }, + "0eb189ef2c2d5762a963d6b7bdf9698ea8e7b48a": { + "balance": "0x487a9a304539440000" + }, + "0eb5b662a1c718608fd52f0c25f9378830178519": { + "balance": "0x14a37281a612e740000" + }, + "0ec46696ffac1f58005fa8439824f08eed1df89b": { + "balance": "0x21e19e0c9bab2400000" + }, + "0ec50aa823f465b9464b0bc0c4a57724a555f5d6": { + "balance": "0xc83d1426ac7b1f00000" + }, + "0ec5308b31282e218fc9e759d4fec5db3708cec4": { + "balance": "0x3643aa647986040000" + }, + "0eccf617844fd61fba62cb0e445b7ac68bcc1fbe": { + "balance": "0x14fe4fe63565c60000" + }, + "0ed3bb3a4eb554cfca97947d575507cdfd6d21d8": { + "balance": "0x1db3205fcc23d58000" + }, + "0ed76c2c3b5d50ff8fb50b3eeacd681590be1c2d": { + "balance": "0x56bc75e2d63100000" + }, + "0eda80f4ed074aea697aeddf283b63dbca3dc4da": { + "balance": "0x6c6b935b8bbd400000" + }, + "0edd4b580ff10fe06c4a03116239ef96622bae35": { + "balance": "0xaadec983fcff40000" + }, + "0ee391f03c765b11d69026fd1ab35395dc3802a0": { + "balance": "0xad78ebc5ac6200000" + }, + "0ee414940487fd24e390378285c5d7b9334d8b65": { + "balance": "0x914878a8c05ee00000" + }, + "0ef54ac7264d2254abbb5f8b41adde875157db7c": { + "balance": "0x22b1c8c1227a00000" + }, + "0ef85b49d08a75198692914eddb4b22cf5fa4450": { + "balance": "0x6cae30621d47200000" + }, + "0efd1789eb1244a3dede0f5de582d8963cb1f39f": { + "balance": "0x5150ae84a8cdf00000" + }, + "0f042c9c2fb18766f836bb59f735f27dc329fe3c": { + "balance": "0x21e19e0c9bab2400000" + }, + "0f049a8bdfd761de8ec02cee2829c4005b23c06b": { + "balance": "0xda933d8d8c6700000" + }, + "0f05f120c89e9fbc93d4ab0c5e2b4a0df092b424": { + "balance": "0x65a4da25d3016c00000" + }, + "0f127bbf8e311caea2ba502a33feced3f730ba42": { + "balance": "0xa31062beeed700000" + }, + "0f1c249cd962b00fd114a9349f6a6cc778d76c4d": { + "balance": "0x6c6b935b8bbd400000" + }, + "0f206e1a1da7207ea518b112418baa8b06260328": { + "balance": "0x2086ac351052600000" + }, + "0f24105abbdaa03fa6309ef6c188e51f714a6e59": { + "balance": "0xad78ebc5ac6200000" + }, + "0f26480a150961b8e30750713a94ee6f2e47fc00": { + "balance": "0x3635c9adc5dea00000" + }, + "0f2d8daf04b5414a0261f549ff6477b80f2f1d07": { + "balance": "0x2a5a058fc295ed000000" + }, + "0f2fb884c8aaff6f543ac6228bd08e4f60b0a5fd": { + "balance": "0xaa7da485136b840000" + }, + "0f32d9cb4d0fdaa0150656bb608dcc43ed7d9301": { + "balance": "0x28df8bf440db790000" + }, + "0f3665d48e9f1419cd984fc7fa92788710c8f2e4": { + "balance": "0x6c6b935b8bbd400000" + }, + "0f3a1023cac04dbf44f5a5fa6a9cf8508cd4fddf": { + "balance": "0x62a992e53a0af00000" + }, + "0f4073c1b99df60a1549d69789c7318d9403a814": { + "balance": "0x43c33c1937564800000" + }, + "0f46c81db780c1674ac73d314f06539ee56ebc83": { + "balance": "0x215f835bc769da80000" + }, + "0f4f94b9191bb7bb556aaad7c74ddb288417a50b": { + "balance": "0x4be4e7267b6ae00000" + }, + "0f6000de1578619320aba5e392706b131fb1de6f": { + "balance": "0x1b1ab319f5ec750000" + }, + "0f6e840a3f2a24647d8e43e09d45c7c335df4248": { + "balance": "0x878678326eac900000" + }, + "0f7515ff0e808f695e0c20485ff96ed2f7b79310": { + "balance": "0x3638221660a5aa8000" + }, + "0f789e30397c53bf256fc364e6ef39f853504114": { + "balance": "0xc55325ca7415e00000" + }, + "0f7b61c59b016322e8226cafaee9d9e76d50a1b3": { + "balance": "0xd8d726b7177a800000" + }, + "0f7bea4ef3f73ae0233df1e100718cbe29310bb0": { + "balance": "0x6c6b935b8bbd400000" + }, + "0f7bf6373f771a4601762c4dae5fbbf4fedd9cc9": { + "balance": "0x6c6b935b8bbd400000" + }, + "0f832a93df9d7f74cd0fb8546b7198bf5377d925": { + "balance": "0x7c0860e5a80dc0000" + }, + "0f83461ba224bb1e8fdd9dae535172b735acb4e0": { + "balance": "0xad78ebc5ac6200000" + }, + "0f85e42b1df321a4b3e835b50c00b06173968436": { + "balance": "0x35659ef93f0fc40000" + }, + "0f88aac9346cb0e7347fba70905475ba8b3e5ece": { + "balance": "0x21e19e0c9bab2400000" + }, + "0f929cf895db017af79f3ead2216b1bd69c37dc7": { + "balance": "0x6c6b935b8bbd400000" + }, + "0fa010ce0c731d3b628e36b91f571300e49dbeab": { + "balance": "0x36330322d5238c0000" + }, + "0fa5d8c5b3f294efd495ab69d768f81872508548": { + "balance": "0x6c6b935b8bbd400000" + }, + "0fa6c7b0973d0bae2940540e247d3627e37ca347": { + "balance": "0x3635c9adc5dea00000" + }, + "0fad05507cdc8f24b2be4cb7fa5d927ddb911b88": { + "balance": "0xa2df13f441f0098000" + }, + "0fb5d2c673bfb1ddca141b9894fd6d3f05da6720": { + "balance": "0x56bc75e2d63100000" + }, + "0fc9a0e34145fbfdd2c9d2a499b617d7a02969b9": { + "balance": "0x9c2007651b2500000" + }, + "0fcfc4065008cfd323305f6286b57a4dd7eee23b": { + "balance": "0x43c33c1937564800000" + }, + "0fdd65402395df9bd19fee4507ef5345f745104c": { + "balance": "0x10f0cf064dd59200000" + }, + "0fec4ee0d7ca180290b6bd20f9992342f60ff68d": { + "balance": "0x12207f0edce9718000" + }, + "0fee81ac331efd8f81161c57382bb4507bb9ebec": { + "balance": "0x15af880d8cdb830000" + }, + "0ffea06d7113fb6aec2869f4a9dfb09007facef4": { + "balance": "0xc384681b1e1740000" + }, + "10097198b4e7ee91ff82cc2f3bd95fed73c540c0": { + "balance": "0x6c6b935b8bbd400000" + }, + "100b4d0977fcbad4debd5e64a0497aeae5168fab": { + "balance": "0x110c9073b5245a0000" + }, + "101a0a64f9afcc448a8a130d4dfcbee89537d854": { + "balance": "0x337fe5feaf2d1800000" + }, + "102c477d69aadba9a0b0f62b7459e17fbb1c1561": { + "balance": "0x6c6b935b8bbd400000" + }, + "1031e0ecb54985ae21af1793950dc811888fde7c": { + "balance": "0x1158e460913d00000" + }, + "10346414bec6d3dcc44e50e54d54c2b8c3734e3e": { + "balance": "0xd8d726b7177a800000" + }, + "10389858b800e8c0ec32f51ed61a355946cc409b": { + "balance": "0xad78ebc5ac6200000" + }, + "1059cbc63e36c43e88f30008aca7ce058eeaa096": { + "balance": "0x152d02c7e14af6800000" + }, + "106ed5c719b5261477890425ae7551dc59bd255c": { + "balance": "0x2896a58c95be5880000" + }, + "10711c3dda32317885f0a2fd8ae92e82069b0d0b": { + "balance": "0xd8d726b7177a800000" + }, + "107379d4c467464f235bc18e55938aad3e688ad7": { + "balance": "0x2b5e3af16b1880000" + }, + "1076212d4f758c8ec7121c1c7d74254926459284": { + "balance": "0x7695b59b5c17b4c0000" + }, + "1078d7f61b0e56c74ee6635b2e1819ef1e3d8785": { + "balance": "0x3635c9adc5dea00000" + }, + "107a03cf0842dbdeb0618fb587ca69189ec92ff5": { + "balance": "0x6acb3df27e1f880000" + }, + "1080c1d8358a15bc84dac8253c6883319020df2c": { + "balance": "0x90f534608a72880000" + }, + "108a2b7c336f784779d8b54d02a8d31d9a139c0a": { + "balance": "0x21e19e0c9bab2400000" + }, + "108ba7c2895c50e072dc6f964932d50c282d3034": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "108fe8ee2a13da487b22c6ab6d582ea71064d98c": { + "balance": "0x15ac56edc4d12c0000" + }, + "1091176be19b9964a8f72e0ece6bf8e3cfad6e9c": { + "balance": "0x21f2f6f0fc3c6100000" + }, + "1098c774c20ca1daac5ddb620365316d353f109c": { + "balance": "0x56bc75e2d63100000" + }, + "1098cc20ef84bad5146639c4cd1ca6c3996cb99b": { + "balance": "0xfc936392801c0000" + }, + "10a1c42dc1ba746986b985a522a73c93eae64c63": { + "balance": "0x3635c9adc5dea00000" + }, + "10a93457496f1108cd98e140a1ecdbae5e6de171": { + "balance": "0x15a99062d416180000" + }, + "10b5b34d1248fcf017f8c8ffc408ce899ceef92f": { + "balance": "0xe7eeba3410b740000" + }, + "10cf560964ff83c1c9674c783c0f73fcd89943fc": { + "balance": "0x878678326eac9000000" + }, + "10d32416722ca4e648630548ead91edd79c06aff": { + "balance": "0x56bc75e2d63100000" + }, + "10d945334ecde47beb9ca3816c173dfbbd0b5333": { + "balance": "0x4be4e7267b6ae00000" + }, + "10df681506e34930ac7a5c67a54c3e89ce92b981": { + "balance": "0x74c1fab8adb4540000" + }, + "10e1e3377885c42d7df218522ee7766887c05e6a": { + "balance": "0x1043c43cde1d398000" + }, + "10e390ad2ba33d82b37388d09c4544c6b0225de5": { + "balance": "0xad78ebc5ac6200000" + }, + "10f4bff0caa5027c0a6a2dcfc952824de2940909": { + "balance": "0x6c6b935b8bbd400000" + }, + "11001b89ed873e3aaec1155634b4681643986323": { + "balance": "0x3635c9adc5dea00000" + }, + "110237cf9117e767922fc4a1b78d7964da82df20": { + "balance": "0xd5967be4fc3f100000" + }, + "1111e5dbf45e6f906d62866f1708101788ddd571": { + "balance": "0x467be6533ec2e40000" + }, + "11172b278ddd44eea2fdf4cb1d16962391c453d9": { + "balance": "0xc62f3d9bfd4895f00000" + }, + "112634b4ec30ff786e024159f796a57939ea144e": { + "balance": "0x6c6acc67d7b1d40000" + }, + "11306c7d57588637780fc9fde8e98ecb008f0164": { + "balance": "0x6c6acc67d7b1d40000" + }, + "113612bc3ba0ee4898b49dd20233905f2f458f62": { + "balance": "0x2f6f10780d22cc00000" + }, + "11415fab61e0dfd4b90676141a557a869ba0bde9": { + "balance": "0x6f05b59d3b20000000" + }, + "114cbbbf6fb52ac414be7ec61f7bb71495ce1dfa": { + "balance": "0xa2a15d09519be00000" + }, + "114cfefe50170dd97ae08f0a44544978c599548d": { + "balance": "0x2ec887e7a14a1c0000" + }, + "116108c12084612eeda7a93ddcf8d2602e279e5c": { + "balance": "0x6c6b935b8bbd400000" + }, + "1164caaa8cc5977afe1fad8a7d6028ce2d57299b": { + "balance": "0x15af1d78b58c400000" + }, + "11675a25554607a3b6c92a9ee8f36f75edd3e336": { + "balance": "0x8a9aba557e36c0000" + }, + "116a09df66cb150e97578e297fb06e13040c893c": { + "balance": "0x6c6b935b8bbd400000" + }, + "116fef5e601642c918cb89160fc2293ba71da936": { + "balance": "0x2b7cc2e9c3225c0000" + }, + "1178501ff94add1c5881fe886136f6dfdbe61a94": { + "balance": "0x890b0c2e14fb80000" + }, + "1179c60dbd068b150b074da4be23033b20c68558": { + "balance": "0x24dce54d34a1a00000" + }, + "117d9aa3c4d13bee12c7500f09f5dd1c66c46504": { + "balance": "0xb2ad30490b2780000" + }, + "117db836377fe15455e02c2ebda40b1ceb551b19": { + "balance": "0x14542ba12a337c00000" + }, + "118c18b2dce170e8f445753ba5d7513cb7636d2d": { + "balance": "0x1dd0c885f9a0d800000" + }, + "118fbd753b9792395aef7a4d78d263cdcaabd4f7": { + "balance": "0x36330322d5238c0000" + }, + "11928378d27d55c520ceedf24ceb1e822d890df0": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "119aa64d5b7d181dae9d3cb449955c89c1f963fa": { + "balance": "0x25f273933db5700000" + }, + "11c0358aa6479de21866fe21071924b65e70f8b9": { + "balance": "0x7b53f79e888dac00000" + }, + "11d2247a221e70c2d66d17ee138d38c55ffb8640": { + "balance": "0x21e19e0c9bab2400000" + }, + "11d7844a471ef89a8d877555583ceebd1439ea26": { + "balance": "0x22369e6ba80c6880000" + }, + "11dd6185d9a8d73ddfdaa71e9b7774431c4dfec2": { + "balance": "0x3635c9adc5dea00000" + }, + "11e7997edd904503d77da6038ab0a4c834bbd563": { + "balance": "0x150894e849b3900000" + }, + "11ec00f849b6319cf51aa8dd8f66b35529c0be77": { + "balance": "0x6c6b935b8bbd400000" + }, + "11efb8a20451161b644a8ccebbc1d343a3bbcb52": { + "balance": "0xad78ebc5ac62000000" + }, + "11fefb5dc1a4598aa712640c517775dfa1d91f8c": { + "balance": "0x21e19e0c9bab2400000" + }, + "120f9de6e0af7ec02a07c609ca8447f157e6344c": { + "balance": "0xe7eeba3410b740000" + }, + "1210f80bdb826c175462ab0716e69e46c24ad076": { + "balance": "0x56bc75e2d63100000" + }, + "12134e7f6b017bf48e855a399ca58e2e892fa5c8": { + "balance": "0x3635c9adc5dea00000" + }, + "12173074980153aeaa4b0dcbc7132eadcec21b64": { + "balance": "0xd02ab486cedc00000" + }, + "121f855b70149ac83473b9706fb44d47828b983b": { + "balance": "0x4be4e7267b6ae00000" + }, + "1227e10a4dbf9caca31b1780239f557615fc35c1": { + "balance": "0xad78ebc5ac6200000" + }, + "122dcfd81addb97d1a0e4925c4b549806e9f3beb": { + "balance": "0x522035cc6e01210000" + }, + "122f56122549d168a5c5e267f52662e5c5cce5c8": { + "balance": "0xa076407d3f7440000" + }, + "12316fc7f178eac22eb2b25aedeadf3d75d00177": { + "balance": "0x43c33be05f6bfb98000" + }, + "123759f333e13e3069e2034b4f05398918119d36": { + "balance": "0x43c33c1937564800000" + }, + "125cc5e4d56b2bcc2ee1c709fb9e68fb177440bd": { + "balance": "0x6c6b935b8bbd400000" + }, + "12632388b2765ee4452b50161d1fffd91ab81f4a": { + "balance": "0x281d901f4fdd100000" + }, + "126897a311a14ad43b78e0920100c4426bfd6bdd": { + "balance": "0x34c726893f2d948000" + }, + "126d91f7ad86debb0557c612ca276eb7f96d00a1": { + "balance": "0x56bc75e2d63100000" + }, + "127d3fc5003bf63c0d83e93957836515fd279045": { + "balance": "0x610c9222e6e750000" + }, + "127db1cadf1b771cbd7475e1b272690f558c8565": { + "balance": "0x2f6f10780d22cc00000" + }, + "1284f0cee9d2ff2989b65574d06ffd9ab0f7b805": { + "balance": "0x15af1d78b58c400000" + }, + "128b908fe743a434203de294c441c7e20a86ea67": { + "balance": "0x26ab14e0c0e13c0000" + }, + "1293c78c7d6a443b9d74b0ba5ee7bb47fd418588": { + "balance": "0x16a6502f15a1e540000" + }, + "1296acded1e063af39fe8ba0b4b63df789f70517": { + "balance": "0x56bf91b1a65eb0000" + }, + "12aa7d86ddfbad301692feac8a08f841cb215c37": { + "balance": "0x76d41c62494840000" + }, + "12afbcba1427a6a39e7ba4849f7ab1c4358ac31b": { + "balance": "0x43c33c1937564800000" + }, + "12b5e28945bb2969f9c64c63cc05b6f1f8d6f4d5": { + "balance": "0x1a29e86913b74050000" + }, + "12cf8b0e465213211a5b53dfb0dd271a282c12c9": { + "balance": "0xd2f13f7789f00000" + }, + "12d20790b7d3dbd88c81a279b812039e8a603bd0": { + "balance": "0x56f985d38644b80000" + }, + "12d60d65b7d9fc48840be5f891c745ce76ee501e": { + "balance": "0x485e5388d0c76840000" + }, + "12d91a92d74fc861a729646db192a125b79f5374": { + "balance": "0xfc936392801c0000" + }, + "12e9a4ad2ad57484dd700565bddb46423bd9bd31": { + "balance": "0x43c30fb0884a96c0000" + }, + "12f32c0a1f2daab676fe69abd9e018352d4ccd45": { + "balance": "0x2b5e3af16b1880000" + }, + "12f460ae646cd2780fd35c50a6af4b9accfa85c6": { + "balance": "0x3635c9adc5dea00000" + }, + "12ffc1128605cb0c13709a7290506f2690977193": { + "balance": "0xb50fcfafebecb00000" + }, + "13032446e7d610aa00ec8c56c9b574d36ca1c016": { + "balance": "0x6c6b935b8bbd400000" + }, + "131c792c197d18bd045d7024937c1f84b60f4438": { + "balance": "0xd8d726b7177a800000" + }, + "131df8d330eb7cc7147d0a55576f05de8d26a8b7": { + "balance": "0xa31062beeed700000" + }, + "131faed12561bb7aee04e5185af802b1c3438d9b": { + "balance": "0xbdf3c4bb0328c0000" + }, + "1321b605026f4ffb296a3e0edcb390c9c85608b7": { + "balance": "0x6c6b935b8bbd400000" + }, + "1321ccf29739b974e5a516f18f3a843671e39642": { + "balance": "0xd8d726b7177a800000" + }, + "1327d759d56e0ab87af37ecf63fe01f310be100a": { + "balance": "0x23bc3cdb68a1800000" + }, + "1329dd19cd4baa9fc64310efeceab22117251f12": { + "balance": "0xad78ebc5ac6200000" + }, + "13371f92a56ea8381e43059a95128bdc4d43c5a6": { + "balance": "0x3635c9adc5dea00000" + }, + "133c490fa5bf7f372888e607d958fab7f955bae1": { + "balance": "0x55a6e79ccd1d300000" + }, + "133e4f15e1e39c53435930aaedf3e0fe56fde843": { + "balance": "0x1158e460913d00000" + }, + "134163be9fbbe1c5696ee255e90b13254395c318": { + "balance": "0xad78ebc5ac6200000" + }, + "135cecd955e5798370769230159303d9b1839f66": { + "balance": "0x10f0cf064dd59200000" + }, + "135d1719bf03e3f866312479fe338118cd387e70": { + "balance": "0x6c6b935b8bbd400000" + }, + "135eb8c0e9e101deedec11f2ecdb66ae1aae8867": { + "balance": "0x43c33c1937564800000" + }, + "1360e87df24c69ee6d51c76e73767ffe19a2131c": { + "balance": "0x4fcc1a89027f00000" + }, + "136c834bf111326d207395295b2e583ea7f33572": { + "balance": "0x56bc75e2d63100000" + }, + "136d4b662bbd1080cfe4445b0fa213864435b7f1": { + "balance": "0xd8d726b7177a800000" + }, + "136f4907cab41e27084b9845069ff2fd0c9ade79": { + "balance": "0xd8d726b7177a800000" + }, + "1374facd7b3f8d68649d60d4550ee69ff0484133": { + "balance": "0xe9ed6e11172da0000" + }, + "137cf341e8516c815814ebcd73e6569af14cf7bc": { + "balance": "0x3635c9adc5dea00000" + }, + "13848b46ea75beb7eaa85f59d866d77fd24cf21a": { + "balance": "0xa968163f0a57b400000" + }, + "139d3531c9922ad56269f6309aa789fb2485f98c": { + "balance": "0xd8d726b7177a800000" + }, + "139e479764b499d666208c4a8a047a97043163dd": { + "balance": "0x2077212aff6df00000" + }, + "13a5eecb38305df94971ef2d9e179ae6cebab337": { + "balance": "0x11e3ab8395c6e80000" + }, + "13acada8980affc7504921be84eb4944c8fbb2bd": { + "balance": "0x56d2aa3a5c09a00000" + }, + "13b9b10715714c09cfd610cf9c9846051cb1d513": { + "balance": "0x6acb3df27e1f880000" + }, + "13ce332dff65a6ab933897588aa23e000980fa82": { + "balance": "0xe020536f028f00000" + }, + "13d67a7e25f2b12cdb85585009f8acc49b967301": { + "balance": "0x6c6acc67d7b1d40000" + }, + "13dee03e3799952d0738843d4be8fc0a803fb20e": { + "balance": "0x6c6b935b8bbd400000" + }, + "13e02fb448d6c84ae17db310ad286d056160da95": { + "balance": "0x6c6b935b8bbd400000" + }, + "13e321728c9c57628058e93fc866a032dd0bda90": { + "balance": "0x26bcca23fe2ea20000" + }, + "13ec812284026e409bc066dfebf9d5a4a2bf801e": { + "balance": "0x57473d05dabae80000" + }, + "140129eaa766b5a29f5b3af2574e4409f8f6d3f1": { + "balance": "0x15af1d78b58c4000000" + }, + "140518a3194bad1350b8949e650565debe6db315": { + "balance": "0x6c6b935b8bbd400000" + }, + "1406854d149e081ac09cb4ca560da463f3123059": { + "balance": "0x487a9a304539440000" + }, + "140ca28ff33b9f66d7f1fc0078f8c1eef69a1bc0": { + "balance": "0x56bc75e2d631000000" + }, + "140fba58dbc04803d84c2130f01978f9e0c73129": { + "balance": "0x15af1d78b58c400000" + }, + "141a5e39ee2f680a600fbf6fa297de90f3225cdd": { + "balance": "0x21e19e0c9bab2400000" + }, + "14254ea126b52d0142da0a7e188ce255d8c47178": { + "balance": "0x2a034919dfbfbc0000" + }, + "142b87c5043ffb5a91df18c2e109ced6fe4a71db": { + "balance": "0xad78ebc5ac6200000" + }, + "143c639752caeecf6a997d39709fc8f19878c7e8": { + "balance": "0x6acb3df27e1f880000" + }, + "143d536b8b1cb84f56a39e0bc81fd5442bcacce1": { + "balance": "0x56bc75e2d63100000" + }, + "143f5f1658d9e578f4f3d95f80c0b1bd3933cbda": { + "balance": "0x50c5e761a444080000" + }, + "14410fb310711be074a80883c635d0ef6afb2539": { + "balance": "0x6c6b935b8bbd400000" + }, + "144b19f1f66cbe318347e48d84b14039466c5909": { + "balance": "0x6c6b935b8bbd400000" + }, + "145250b06e4fa7cb2749422eb817bdda8b54de5f": { + "balance": "0xbdf3c4bb0328c0000" + }, + "145e0600e2a927b2dd8d379356b45a2e7d51d3ae": { + "balance": "0x8a02ab400bb2cb8000" + }, + "145e1de0147911ccd880875fbbea61f6a142d11d": { + "balance": "0xd8d726b7177a800000" + }, + "1463a873555bc0397e575c2471cf77fa9db146e0": { + "balance": "0x21e19e0c9bab2400000" + }, + "1479a9ec7480b74b5db8fc499be352da7f84ee9c": { + "balance": "0x3635c9adc5dea00000" + }, + "147af46ae9ccd18bb35ca01b353b51990e49dce1": { + "balance": "0xd8d726b7177a800000" + }, + "147f4210ab5804940a0b7db8c14c28396b62a6bf": { + "balance": "0x6c6b935b8bbd400000" + }, + "14830704e99aaad5c55e1f502b27b22c12c91933": { + "balance": "0x219c3a7b1966300000" + }, + "149b6dbde632c19f5af47cb493114bebd9b03c1f": { + "balance": "0x28a857425466f800000" + }, + "149ba10f0da2725dc704733e87f5a524ca88515e": { + "balance": "0x1ab2cf7c9f87e200000" + }, + "14a7352066364404db50f0d0d78d754a22198ef4": { + "balance": "0x65ea3db75546600000" + }, + "14ab164b3b524c82d6abfbc0de831126ae8d1375": { + "balance": "0x6c6b935b8bbd400000" + }, + "14b1603ec62b20022033eec4d6d6655ac24a015a": { + "balance": "0x2b5e3af16b1880000" + }, + "14c63ba2dcb1dd4df33ddab11c4f0007fa96a62d": { + "balance": "0x34841b6057afab00000" + }, + "14cdddbc8b09e6675a9e9e05091cb92238c39e1e": { + "balance": "0x11478b7c30abc300000" + }, + "14d00aad39a0a7d19ca05350f7b03727f08dd82e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "14eec09bf03e352bd6ff1b1e876be664ceffd0cf": { + "balance": "0x116dc3a8994b30000" + }, + "14f221159518783bc4a706676fc4f3c5ee405829": { + "balance": "0xad78ebc5ac6200000" + }, + "14fcd1391e7d732f41766cdacd84fa1deb9ffdd2": { + "balance": "0x6c6b935b8bbd400000" + }, + "150e3dbcbcfc84ccf89b73427763a565c23e60d0": { + "balance": "0x22b1c8c1227a00000" + }, + "1518627b88351fede796d3f3083364fbd4887b0c": { + "balance": "0x3635c9adc5dea000000" + }, + "15224ad1c0face46f9f556e4774a3025ad06bd52": { + "balance": "0xb98bc829a6f90000" + }, + "152f2bd229ddf3cb0fdaf455c183209c0e1e39a2": { + "balance": "0x6c6b935b8bbd400000" + }, + "152f4e860ef3ee806a502777a1b8dbc91a907668": { + "balance": "0x2086ac351052600000" + }, + "153c08aa8b96a611ef63c0253e2a4334829e579d": { + "balance": "0x155bd9307f9fe80000" + }, + "153cf2842cb9de876c276fa64767d1a8ecf573bb": { + "balance": "0x6c6b935b8bbd400000" + }, + "153ef58a1e2e7a3eb6b459a80ab2a547c94182a2": { + "balance": "0x14542ba12a337c000000" + }, + "154459fa2f21318e3434449789d826cdc1570ce5": { + "balance": "0x6c6b935b8bbd400000" + }, + "1547b9bf7ad66274f3413827231ba405ee8c88c1": { + "balance": "0x3a9d5baa4abf1d00000" + }, + "1548b770a5118ede87dba2f690337f616de683ab": { + "balance": "0x1c995685e0bf870000" + }, + "15528350e0d9670a2ea27f7b4a33b9c0f9621d21": { + "balance": "0xd8d8583fa2d52f0000" + }, + "155b3779bb6d56342e2fda817b5b2d81c7f41327": { + "balance": "0x2b8aa3a076c9c0000" + }, + "1565af837ef3b0bd4e2b23568d5023cd34b16498": { + "balance": "0x1551e9724ac4ba0000" + }, + "15669180dee29598869b08a721c7d24c4c0ee63f": { + "balance": "0x3635c9adc5dea00000" + }, + "1572cdfab72a01ce968e78f5b5448da29853fbdd": { + "balance": "0x112626c49060fa60000" + }, + "157559adc55764cc6df79323092534e3d6645a66": { + "balance": "0x14542ba12a337c00000" + }, + "1578bdbc371b4d243845330556fff2d5ef4dff67": { + "balance": "0x56bc75e2d63100000" + }, + "157eb3d3113bd3b597714d3a954edd018982a5cb": { + "balance": "0x6c6b935b8bbd400000" + }, + "1584a2c066b7a455dbd6ae2807a7334e83c35fa5": { + "balance": "0x70c1cc73b00c80000" + }, + "15874686b6733d10d703c9f9bec6c52eb8628d67": { + "balance": "0x6c6b935b8bbd400000" + }, + "158a0d619253bf4432b5cd02c7b862f7c2b75636": { + "balance": "0x75bac7c5b12188000" + }, + "1598127982f2f8ad3b6b8fc3cf27bf617801ba2b": { + "balance": "0x960db77681e940000" + }, + "159adce27aa10b47236429a34a5ac42cad5b6416": { + "balance": "0x6bf90a96edbfa718000" + }, + "15a0aec37ff9ff3d5409f2a4f0c1212aaccb0296": { + "balance": "0x3635c9adc5dea00000" + }, + "15aa530dc36958b4edb38eee6dd9e3c77d4c9145": { + "balance": "0x6c6b935b8bbd400000" + }, + "15acb61568ec4af7ea2819386181b116a6c5ee70": { + "balance": "0x690836c0af5f5600000" + }, + "15b96f30c23b8664e7490651066b00c4391fbf84": { + "balance": "0x1642e9df4876290000" + }, + "15c7edb8118ee27b342285eb5926b47a855bc7a5": { + "balance": "0x1158e460913d00000" + }, + "15d99468507aa0413fb60dca2adc7f569cb36b54": { + "balance": "0x6c6b935b8bbd400000" + }, + "15dbb48c98309764f99ced3692dcca35ee306bac": { + "balance": "0x1fc3842bd1f071c00000" + }, + "15dcafcc2bace7b55b54c01a1c514626bf61ebd8": { + "balance": "0x1fd933494aa5fe00000" + }, + "15e3b584056b62c973cf5eb096f1733e54c15c91": { + "balance": "0x32c75a0223ddf30000" + }, + "15ebd1c7cad2aff19275c657c4d808d010efa0f5": { + "balance": "0xadf30ba70c8970000" + }, + "15ee0fc63ebf1b1fc49d7bb38f8863823a2e17d2": { + "balance": "0x678a932062e4180000" + }, + "15f1b352110d68901d8f67aac46a6cfafe031477": { + "balance": "0xad78ebc5ac6200000" + }, + "15f2b7b16432ee50a5f55b41232f6334ed58bdc0": { + "balance": "0x15af1d78b58c400000" + }, + "16019a4dafab43f4d9bf4163fae0847d848afca2": { + "balance": "0x15bc70139f74a0000" + }, + "160226efe7b53a8af462d117a0108089bdecc2d1": { + "balance": "0xadf30ba70c8970000" + }, + "160ceb6f980e04315f53c4fc988b2bf69e284d7d": { + "balance": "0x10910d4cdc9f60000" + }, + "161caf5a972ace8379a6d0a04ae6e163fe21df2b": { + "balance": "0x152d02c7e14af6800000" + }, + "161d26ef6759ba5b9f20fdcd66f16132c352415e": { + "balance": "0x6c6b935b8bbd400000" + }, + "162110f29eac5f7d02b543d8dcd5bb59a5e33b73": { + "balance": "0x6c6b935b8bbd400000" + }, + "162ba503276214b509f97586bd842110d103d517": { + "balance": "0x1e7ffd8895c22680000" + }, + "162d76c2e6514a3afb6fe3d3cb93a35c5ae783f1": { + "balance": "0x6c6b935b8bbd400000" + }, + "163bad4a122b457d64e8150a413eae4d07023e6b": { + "balance": "0x104e70464b1580000" + }, + "163cc8be227646cb09719159f28ed09c5dc0dce0": { + "balance": "0x487a9a304539440000" + }, + "163dca73d7d6ea3f3e6062322a8734180c0b78ef": { + "balance": "0x9f742003cb7dfc0000" + }, + "164d7aac3eecbaeca1ad5191b753f173fe12ec33": { + "balance": "0x285652b8a468690000" + }, + "16526c9edf943efa4f6d0f0bae81e18b31c54079": { + "balance": "0x35659ef93f0fc40000" + }, + "165305b787322e25dc6ad0cefe6c6f334678d569": { + "balance": "0x6c6b935b8bbd400000" + }, + "1665ab1739d71119ee6132abbd926a279fe67948": { + "balance": "0x56bc75e2d63100000" + }, + "166bf6dab22d841b486c38e7ba6ab33a1487ed8c": { + "balance": "0x43c33c1937564800000" + }, + "167699f48a78c615512515739958993312574f07": { + "balance": "0x21d3bd55e803c0000" + }, + "1678c5f2a522393225196361894f53cc752fe2f3": { + "balance": "0x68f365aea1e4400000" + }, + "167ce7de65e84708595a525497a3eb5e5a665073": { + "balance": "0x1f314773666fc40000" + }, + "167e3e3ae2003348459392f7dfce44af7c21ad59": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "1680cec5021ee93050f8ae127251839e74c1f1fd": { + "balance": "0x2c61461e5d743d68000" + }, + "16816aac0ede0d2d3cd442da79e063880f0f1d67": { + "balance": "0x6c6b935b8bbd400000" + }, + "168b5019b818691644835fe69bf229e17112d52c": { + "balance": "0x5ede20f01a459800000" + }, + "168bdec818eafc6d2992e5ef54aa0e1601e3c561": { + "balance": "0x3637507a30abeb0000" + }, + "168d30e53fa681092b52e9bae15a0dcb41a8c9bb": { + "balance": "0x56bc75e2d63100000" + }, + "169bbefc41cfd7d7cbb8dfc63020e9fb06d49546": { + "balance": "0x6c6b935b8bbd400000" + }, + "16a58e985dccd707a594d193e7cca78b5d027849": { + "balance": "0x49b9ca9a6943400000" + }, + "16a9e9b73ae98b864d1728798b8766dbc6ea8d12": { + "balance": "0x33e7b44b0db5040000" + }, + "16aa52cb0b554723e7060f21f327b0a68315fea3": { + "balance": "0xd8d726b7177a80000" + }, + "16abb8b021a710bdc78ea53494b20614ff4eafe8": { + "balance": "0x890b0c2e14fb80000" + }, + "16afa787fc9f94bdff6976b1a42f430a8bf6fb0f": { + "balance": "0x6c6b935b8bbd400000" + }, + "16bae5d24eff91778cd98b4d3a1cc3162f44aa77": { + "balance": "0x15be6174e1912e0000" + }, + "16bc40215abbd9ae5d280b95b8010b4514ff1292": { + "balance": "0xad78ebc5ac6200000" + }, + "16be75e98a995a395222d00bd79ff4b6e638e191": { + "balance": "0x79f905c6fd34e800000" + }, + "16c1bf5b7dc9c83c179efacbcf2eb174e3561cb3": { + "balance": "0x3635c9adc5dea00000" + }, + "16c7b31e8c376282ac2271728c31c95e35d952c3": { + "balance": "0x6c6b935b8bbd400000" + }, + "16f313cf8ad000914a0a176dc6a4342b79ec2538": { + "balance": "0x6c6b935b8bbd400000" + }, + "16ffac84032940f0121a09668b858a7e79ffa3bb": { + "balance": "0xd24ada6e1087110000" + }, + "1703b4b292b8a9deddede81bb25d89179f6446b6": { + "balance": "0x42b65a455e8b1680000" + }, + "17049311101d817efb1d65910f663662a699c98c": { + "balance": "0x6c68ccd09b022c0000" + }, + "1704cefcfb1331ec7a78388b29393e85c1af7916": { + "balance": "0x15af1d78b58c400000" + }, + "170a88a8997f92d238370f1affdee6347050b013": { + "balance": "0xa2ac77351488300000" + }, + "17108dab2c50f99de110e1b3b3b4cd82f5df28e7": { + "balance": "0x35203b67bccad00000" + }, + "17125b59ac51cee029e4bd78d7f5947d1ea49bb2": { + "balance": "0x4a89f54ef0121c00000" + }, + "171ad9a04bedc8b861e8ed4bddf5717813b1bb48": { + "balance": "0x15af1d78b58c400000" + }, + "171ca02a8b6d62bf4ca47e906914079861972cb2": { + "balance": "0xad78ebc5ac6200000" + }, + "1722c4cbe70a94b6559d425084caeed4d6e66e21": { + "balance": "0xd8d726b7177a800000" + }, + "17580b766f7453525ca4c6a88b01b50570ea088c": { + "balance": "0x56bc75e2d63100000" + }, + "17589a6c006a54cad70103123aae0a82135fdeb4": { + "balance": "0xd8d726b7177a800000" + }, + "175a183a3a235ffbb03ba835675267229417a091": { + "balance": "0x3635c9adc5dea000000" + }, + "175feeea2aa4e0efda12e1588d2f483290ede81a": { + "balance": "0xad78ebc5ac6200000" + }, + "1765361c2ec2f83616ce8363aae21025f2566f40": { + "balance": "0x10f0cf064dd59200000" + }, + "1767525c5f5a22ed80e9d4d7710f0362d29efa33": { + "balance": "0x15af1d78b58c400000" + }, + "17762560e82a93b3f522e0e524adb8612c3a7470": { + "balance": "0x3635c9adc5dea00000" + }, + "177dae78bc0113d8d39c4402f2a641ae2a105ab8": { + "balance": "0x6292425620b4480000" + }, + "1784948bf99848c89e445638504dd698271b5924": { + "balance": "0x1474c410d87baee0000" + }, + "1788da9b57fd05edc4ff99e7fef301519c8a0a1e": { + "balance": "0x6c6b935b8bbd400000" + }, + "178eaf6b8554c45dfde16b78ce0c157f2ee31351": { + "balance": "0x1158e460913d000000" + }, + "17961d633bcf20a7b029a7d94b7df4da2ec5427f": { + "balance": "0xc6ff070f1938b8000" + }, + "1796bcc97b8abc717f4b4a7c6b1036ea2182639f": { + "balance": "0x1341f91cd8e3510000" + }, + "17993d312aa1106957868f6a55a5e8f12f77c843": { + "balance": "0x1865e814f4142e8000" + }, + "179a825e0f1f6e985309668465cffed436f6aea9": { + "balance": "0x1158e460913d00000" + }, + "17b2d6cf65c6f4a347ddc6572655354d8a412b29": { + "balance": "0x6c6b935b8bbd400000" + }, + "17b807afa3ddd647e723542e7b52fee39527f306": { + "balance": "0x15af40ffa7fc010000" + }, + "17c0478657e1d3d17aaa331dd429cecf91f8ae5d": { + "balance": "0x3634fb9f1489a70000" + }, + "17c0fef6986cfb2e4041f9979d9940b69dff3de2": { + "balance": "0xd8d726b7177a800000" + }, + "17d4918dfac15d77c47f9ed400a850190d64f151": { + "balance": "0x6c6b935b8bbd400000" + }, + "17d521a8d9779023f7164d233c3b6420ffd223ed": { + "balance": "0x1158e460913d00000" + }, + "17d931d4c56294dcbe77c8655be4695f006d4a3c": { + "balance": "0x6c6b935b8bbd400000" + }, + "17df49518d73b129f0da36b1c9b40cb66420fdc7": { + "balance": "0x21e19e0c9bab2400000" + }, + "17e4a0e52bac3ee44efe0954e753d4b85d644e05": { + "balance": "0x6c6b935b8bbd400000" + }, + "17e584e810e567702c61d55d434b34cdb5ee30f6": { + "balance": "0x10f0cf064dd59200000" + }, + "17e82e7078dc4fd9e879fb8a50667f53a5c54591": { + "balance": "0xad78ebc5ac6200000" + }, + "17e86f3b5b30c0ba59f2b2e858425ba89f0a10b0": { + "balance": "0x6c6b935b8bbd400000" + }, + "17ee9f54d4ddc84d670eff11e54a659fd72f4455": { + "balance": "0x3635c9adc5dea000000" + }, + "17ef4acc1bf147e326749d10e677dcffd76f9e06": { + "balance": "0x87751f4e0e1b5300000" + }, + "17f14632a7e2820be6e8f6df823558283dadab2d": { + "balance": "0x6c6b935b8bbd400000" + }, + "17f523f117bc9fe978aa481eb4f5561711371bc8": { + "balance": "0x6c69f73e29134e0000" + }, + "17fd9b551a98cb61c2e07fbf41d3e8c9a530cba5": { + "balance": "0x1768c308193048000" + }, + "180478a655d78d0f3b0c4f202b61485bc4002fd5": { + "balance": "0x6c6b935b8bbd400000" + }, + "18136c9df167aa17b6f18e22a702c88f4bc28245": { + "balance": "0xd8d726b7177a800000" + }, + "1815279dff9952da3be8f77249dbe22243377be7": { + "balance": "0x1017cb76e7b26640000" + }, + "181fbba852a7f50178b1c7f03ed9e58d54162929": { + "balance": "0x241a9b4f617a280000" + }, + "1827039f09570294088fddf047165c33e696a492": { + "balance": "0x205b4dfa1ee74780000" + }, + "182db85293f606e88988c3704cb3f0c0bbbfca5a": { + "balance": "0x73f75d1a085ba0000" + }, + "1848003c25bfd4aa90e7fcb5d7b16bcd0cffc0d8": { + "balance": "0x3635c9adc5dea00000" + }, + "184a4f0beb71ffd558a6b6e8f228b78796c4cf3e": { + "balance": "0x28a857425466f800000" + }, + "184d86f3466ae6683b19729982e7a7e1a48347b2": { + "balance": "0x21e19e0c9bab2400000" + }, + "1851a063ccdb30549077f1d139e72de7971197d5": { + "balance": "0x6c6b935b8bbd400000" + }, + "185546e8768d506873818ac9751c1f12116a3bef": { + "balance": "0xad78ebc5ac6200000" + }, + "1858cf11aea79f5398ad2bb22267b5a3c952ea74": { + "balance": "0x215f835bc769da80000" + }, + "185a7fc4ace368d233e620b2a45935661292bdf2": { + "balance": "0x43c33c1937564800000" + }, + "1864a3c7b48155448c54c88c708f166709736d31": { + "balance": "0x73f75d1a085ba0000" + }, + "186afdc085f2a3dce4615edffbadf71a11780f50": { + "balance": "0xad78ebc5ac6200000" + }, + "186b95f8e5effddcc94f1a315bf0295d3b1ea588": { + "balance": "0x6c6acc67d7b1d40000" + }, + "187d9f0c07f8eb74faaad15ebc7b80447417f782": { + "balance": "0x1158e460913d00000" + }, + "1895a0eb4a4372722fcbc5afe6936f289c88a419": { + "balance": "0x3154c9729d05780000" + }, + "1899f69f653b05a5a6e81f480711d09bbf97588c": { + "balance": "0x69fb133df750ac0000" + }, + "18a6d2fc52be73084023c91802f05bc24a4be09f": { + "balance": "0x6c6b935b8bbd400000" + }, + "18b0407cdad4ce52600623bd5e1f6a81ab61f026": { + "balance": "0x1151ccf0c654c68000" + }, + "18b8bcf98321da61fb4e3eacc1ec5417272dc27e": { + "balance": "0x2fb474098f67c00000" + }, + "18c6723a6753299cb914477d04a3bd218df8c775": { + "balance": "0x3635c9adc5dea00000" + }, + "18e113d8177c691a61be785852fa5bb47aeebdaf": { + "balance": "0x487a9a304539440000" + }, + "18e4ce47483b53040adbab35172c01ef64506e0c": { + "balance": "0x1e7e4171bf4d3a00000" + }, + "18e53243981aabc8767da10c73449f1391560eaa": { + "balance": "0x14542ba12a337c00000" + }, + "18fa8625c9dc843c78c7ab259ff87c9599e07f10": { + "balance": "0x3635c9adc5dea00000" + }, + "18fb09188f27f1038e654031924f628a2106703d": { + "balance": "0x6c6b935b8bbd400000" + }, + "18fccf62d2c3395453b7587b9e26f5cff9eb7482": { + "balance": "0x3635c9adc5dea00000" + }, + "191313525238a21c767457a91374f02200c55448": { + "balance": "0x64f5fdf494f780000" + }, + "1914f1eb95d1277e93b6e61b668b7d77f13a11a1": { + "balance": "0x34957444b840e80000" + }, + "1923cfc68b13ea7e2055803645c1e320156bd88d": { + "balance": "0x487a9a304539440000" + }, + "19336a236ded755872411f2e0491d83e3e00159e": { + "balance": "0x32f51edbaaa3300000" + }, + "1933e334c40f3acbad0c0b851158206924beca3a": { + "balance": "0x1995eaf01b896188000" + }, + "1937c5c515057553ccbd46d5866455ce66290284": { + "balance": "0xd3c21bcecceda1000000" + }, + "193ac65183651800e23580f8f0ead3bb597eb8a4": { + "balance": "0x2b62abcfb910a0000" + }, + "193d37ed347d1c2f4e35350d9a444bc57ca4db43": { + "balance": "0x340aad21b3b700000" + }, + "1940dc9364a852165f47414e27f5002445a4f143": { + "balance": "0x24c2dff6a3c7c480000" + }, + "1945fe377fe6d4b71e3e791f6f17db243c9b8b0f": { + "balance": "0x7679e7beb988360000" + }, + "194a6bb302b8aba7a5b579df93e0df1574967625": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "194cebb4929882bf3b4bf9864c2b1b0f62c283f9": { + "balance": "0x1ef861531f74aa0000" + }, + "194ff44aefc17bd20efd7a204c47d1620c86db5d": { + "balance": "0xa29909687f6aa40000" + }, + "194ffe78bbf5d20dd18a1f01da552e00b7b11db1": { + "balance": "0x17b7883c06916600000" + }, + "1953313e2ad746239cb2270f48af34d8bb9c4465": { + "balance": "0x6c6b935b8bbd400000" + }, + "19571a2b8f81c6bcf66ab3a10083295617150003": { + "balance": "0x1ab2cf7c9f87e20000" + }, + "19687daa39c368139b6e7be60dc1753a9f0cbea3": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "196c02210a450ab0b36370655f717aa87bd1c004": { + "balance": "0xe10ace157dbc00000" + }, + "196e85df7e732b4a8f0ed03623f4db9db0b8fa31": { + "balance": "0x125b92f5cef248000" + }, + "19732bf973055dbd91a4533adaa2149a91d38380": { + "balance": "0x6c6b935b8bbd400000" + }, + "197672fd39d6f246ce66a790d13aa922d70ea109": { + "balance": "0x3635c9adc5dea00000" + }, + "19798cbda715ea9a9b9d6aab942c55121e98bf91": { + "balance": "0x410d586a20a4c00000" + }, + "198bfcf1b07ae308fa2c02069ac9dafe7135fb47": { + "balance": "0x1158e460913d00000" + }, + "198ef1ec325a96cc354c7266a038be8b5c558f67": { + "balance": "0x80d1e4373e7f21da0000" + }, + "19918aa09e7d494e98ffa5db50350892f7156ac6": { + "balance": "0x21e19e0c9bab2400000" + }, + "19b36b0c87ea664ed80318dc77b688dde87d95a5": { + "balance": "0x699f499802303d0000" + }, + "19df9445a81c1b3d804aeaeb6f6e204e4236663f": { + "balance": "0x206d94e6a49878000" + }, + "19e5dea3370a2c746aae34a37c531f41da264e83": { + "balance": "0xad78ebc5ac6200000" + }, + "19e7f3eb7bf67f3599209ebe08b62ad3327f8cde": { + "balance": "0x6c6b935b8bbd400000" + }, + "19e94e620050aad766b9e1bad931238312d4bf49": { + "balance": "0x81e32df972abf00000" + }, + "19ecf2abf40c9e857b252fe1dbfd3d4c5d8f816e": { + "balance": "0x6c6b935b8bbd400000" + }, + "19f5caf4c40e6908813c0745b0aea9586d9dd931": { + "balance": "0x23fed9e1fa2b600000" + }, + "19f643e1a8fa04ae16006028138333a59a96de87": { + "balance": "0x1158e460913d00000" + }, + "19f99f2c0b46ce8906875dc9f90ae104dae35594": { + "balance": "0xf4575a5d4d162a0000" + }, + "19ff244fcfe3d4fa2f4fd99f87e55bb315b81eb6": { + "balance": "0xad78ebc5ac6200000" + }, + "1a04cec420ad432215246d77fe178d339ed0b595": { + "balance": "0x11216185c29f700000" + }, + "1a04d5389eb006f9ce880c30d15353f8d11c4b31": { + "balance": "0x39d84b2186dc9100000" + }, + "1a0841b92a7f7075569dc4627e6b76cab05ade91": { + "balance": "0x52663ccab1e1c00000" + }, + "1a085d43ec92414ea27b914fe767b6d46b1eef44": { + "balance": "0x641e8a13563d8f80000" + }, + "1a09fdc2c7a20e23574b97c69e93deba67d37220": { + "balance": "0x6c4fd1ee246e780000" + }, + "1a0a1ddfb031e5c8cc1d46cf05842d50fddc7130": { + "balance": "0x3635c9adc5dea00000" + }, + "1a1c9a26e0e02418a5cf687da75a275c622c9440": { + "balance": "0x10f0cf064dd59200000" + }, + "1a201b4327cea7f399046246a3c87e6e03a3cda8": { + "balance": "0x3635c9adc5dea00000" + }, + "1a2434cc774422d48d53d59c5d562cce8407c94b": { + "balance": "0x1a055690d9db80000" + }, + "1a25e1c5bc7e5f50ec16f8885f210ea1b938800e": { + "balance": "0xd8d726b7177a800000" + }, + "1a2694ec07cf5e4d68ba40f3e7a14c53f3038c6e": { + "balance": "0x3636cd06e2db3a8000" + }, + "1a3520453582c718a21c42375bc50773255253e1": { + "balance": "0x2ad373ce668e980000" + }, + "1a376e1b2d2f590769bb858d4575320d4e149970": { + "balance": "0x106712576391d180000" + }, + "1a3a330e4fcb69dbef5e6901783bf50fd1c15342": { + "balance": "0xe3aeb5737240a00000" + }, + "1a4ec6a0ae7f5a9427d23db9724c0d0cffb2ab2f": { + "balance": "0x9b41fbf9e0aec0000" + }, + "1a505e62a74e87e577473e4f3afa16bedd3cfa52": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "1a5ee533acbfb3a2d76d5b685277b796c56a052b": { + "balance": "0x6c6b935b8bbd400000" + }, + "1a644a50cbc2aee823bd2bf243e825be4d47df02": { + "balance": "0x56be03ca3e47d8000" + }, + "1a7044e2383f8708305b495bd1176b92e7ef043a": { + "balance": "0xad78ebc5ac6200000" + }, + "1a79c7f4039c67a39d7513884cdc0e2c34222490": { + "balance": "0x1158e460913d00000" + }, + "1a89899cbebdbb64bb26a195a63c08491fcd9eee": { + "balance": "0x6c6b935b8bbd400000" + }, + "1a8a5ce414de9cd172937e37f2d59cff71ce57a0": { + "balance": "0x21e19e0c9bab2400000" + }, + "1a95a8a8082e4652e4170df9271cb4bb4305f0b2": { + "balance": "0x2b5e3af16b1880000" + }, + "1a95c9b7546b5d1786c3858fb1236446bc0ca4ce": { + "balance": "0x6acb3df27e1f880000" + }, + "1a987e3f83de75a42f1bde7c997c19217b4a5f24": { + "balance": "0x6c6b935b8bbd400000" + }, + "1a9e702f385dcd105e8b9fa428eea21c57ff528a": { + "balance": "0x4be4e7267b6ae00000" + }, + "1aa1021f550af158c747668dd13b463160f95a40": { + "balance": "0x4fb0591b9b30380000" + }, + "1aa27699cada8dc3a76f7933aa66c71919040e88": { + "balance": "0x15af1d78b58c400000" + }, + "1aa40270d21e5cde86b6316d1ac3c533494b79ed": { + "balance": "0x1158e460913d00000" + }, + "1ab53a11bcc63ddfaa40a02b9e186496cdbb8aff": { + "balance": "0x6c3f2aac800c000000" + }, + "1abc4e253b080aeb437984ab05bca0979aa43e1c": { + "balance": "0x3635c9adc5dea00000" + }, + "1ac089c3bc4d82f06a20051a9d732dc0e734cb61": { + "balance": "0x25f69d63a6ce0e0000" + }, + "1ad4563ea5786be1159935abb0f1d5879c3e7372": { + "balance": "0x14542ba12a337c00000" + }, + "1ad72d20a76e7fcc6b764058f48d417d496fa6cd": { + "balance": "0x6c6b935b8bbd400000" + }, + "1adaf4abfa867db17f99af6abebf707a3cf55df6": { + "balance": "0x14542ba12a337c00000" + }, + "1af60343360e0b2d75255210375720df21db5c7d": { + "balance": "0x3635c9adc5dea00000" + }, + "1afcc585896cd0ede129ee2de5c19ea811540b64": { + "balance": "0xaf2aba0c8e5bef8000" + }, + "1b05ea6a6ac8af7cb6a8b911a8cce8fe1a2acfc8": { + "balance": "0x6c6b935b8bbd400000" + }, + "1b0b31afff4b6df3653a94d7c87978ae35f34aae": { + "balance": "0x133910453fa9840000" + }, + "1b0d076817e8d68ee2df4e1da1c1142d198c4435": { + "balance": "0x54069233bf7f780000" + }, + "1b130d6fa51d5c48ec8d1d52dc8a227be8735c8a": { + "balance": "0x6c6b935b8bbd400000" + }, + "1b23cb8663554871fbbe0d9e60397efb6faedc3e": { + "balance": "0xad78ebc5ac6200000" + }, + "1b2639588b55c344b023e8de5fd4087b1f040361": { + "balance": "0x5150ae84a8cdf00000" + }, + "1b3920d001c43e72b24e7ca46f0fd6e0c20a5ff2": { + "balance": "0x6c6b935b8bbd400000" + }, + "1b3cb81e51011b549d78bf720b0d924ac763a7c2": { + "balance": "0x7695a92c20d6fe000000" + }, + "1b43232ccd4880d6f46fa751a96cd82473315841": { + "balance": "0x4563918244f400000" + }, + "1b4bbcb18165211b265b280716cb3f1f212176e8": { + "balance": "0x199ad37d03d0608000" + }, + "1b4d07acd38183a61bb2783d2b7b178dd502ac8d": { + "balance": "0xad78ebc5ac6200000" + }, + "1b636b7a496f044d7359596e353a104616436f6b": { + "balance": "0x1388ea95c33f1d0000" + }, + "1b6495891240e64e594493c2662171db5e30ce13": { + "balance": "0x95887d695ed580000" + }, + "1b6610fb68bad6ed1cfaa0bbe33a24eb2e96fafb": { + "balance": "0x83d6c7aab63600000" + }, + "1b799033ef6dc7127822f74542bb22dbfc09a308": { + "balance": "0x56bc75e2d63100000" + }, + "1b7ed974b6e234ce81247498429a5bd4a0a2d139": { + "balance": "0x6c6b935b8bbd400000" + }, + "1b826fb3c012b0d159e294ba5b8a499ff3c0e03c": { + "balance": "0x6c6b935b8bbd400000" + }, + "1b8aa0160cd79f005f88510a714913d70ad3be33": { + "balance": "0xaeffb83079ad00000" + }, + "1b8bd6d2eca20185a78e7d98e8e185678dac4830": { + "balance": "0x3894f0e6f9b9f700000" + }, + "1b9b2dc2960e4cb9408f7405827c9b59071612fd": { + "balance": "0x3635c9adc5dea00000" + }, + "1ba9228d388727f389150ea03b73c82de8eb2e09": { + "balance": "0x18974fbe177c9280000" + }, + "1ba9f7997e5387b6b2aa0135ac2452fe36b4c20d": { + "balance": "0x2e141ea081ca080000" + }, + "1bba03ff6b4ad5bf18184acb21b188a399e9eb4a": { + "balance": "0x61093d7c2c6d380000" + }, + "1bbc199e586790be87afedc849c04726745c5d7b": { + "balance": "0xd8d726b7177a800000" + }, + "1bbc60bcc80e5cdc35c5416a1f0a40a83dae867b": { + "balance": "0x6c6b935b8bbd400000" + }, + "1bc44c8761231ba1f11f5faa40fa669a013e12ce": { + "balance": "0xb0952c45aeaad0000" + }, + "1bcf3441a866bdbe963009ce33c81cbb0261b02c": { + "balance": "0x9ddc1e3b901180000" + }, + "1bd28cd5c78aee51357c95c1ef9235e7c18bc854": { + "balance": "0x6c6b935b8bbd400000" + }, + "1bd8ebaa7674bb18e19198db244f570313075f43": { + "balance": "0x821ab0d4414980000" + }, + "1bd909ac0d4a1102ec98dcf2cca96a0adcd7a951": { + "balance": "0x11651ac3e7a758000" + }, + "1be3542c3613687465f15a70aeeb81662b65cca8": { + "balance": "0x6c6b935b8bbd400000" + }, + "1bea4df5122fafdeb3607eddda1ea4ffdb9abf2a": { + "balance": "0x12c1b6eed03d280000" + }, + "1bec4d02ce85fc48feb62489841d85b170586a9b": { + "balance": "0x821ab0d44149800000" + }, + "1bf974d9904f45ce81a845e11ef4cbcf27af719e": { + "balance": "0x56bc75e2d63100000" + }, + "1c045649cd53dc23541f8ed4d341812808d5dd9c": { + "balance": "0x17b7883c06916600000" + }, + "1c128bd6cda5fca27575e4b43b3253c8c4172afe": { + "balance": "0x6c6b935b8bbd400000" + }, + "1c13d38637b9a47ce79d37a86f50fb409c060728": { + "balance": "0x487a9a304539440000" + }, + "1c2010bd662df417f2a271879afb13ef4c88a3ae": { + "balance": "0xd8d726b7177a800000" + }, + "1c257ad4a55105ea3b58ed374b198da266c85f63": { + "balance": "0x21e19e0c9bab2400000" + }, + "1c2e3607e127caca0fbd5c5948adad7dd830b285": { + "balance": "0x42bf06b78ed3b500000" + }, + "1c356cfdb95febb714633b28d5c132dd84a9b436": { + "balance": "0x15af1d78b58c40000" + }, + "1c35aab688a0cd8ef82e76541ba7ac39527f743b": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "1c3ef05dae9dcbd489f3024408669de244c52a02": { + "balance": "0x43c33c1937564800000" + }, + "1c4af0e863d2656c8635bc6ffec8dd9928908cb5": { + "balance": "0x6c6b935b8bbd400000" + }, + "1c601993789207f965bb865cbb4cd657cce76fc0": { + "balance": "0x5541a7037503f0000" + }, + "1c63fa9e2cbbf23c49fcdef1cbabfe6e0d1e14c1": { + "balance": "0x3635c9adc5dea00000" + }, + "1c6702b3b05a5114bdbcaeca25531aeeb34835f4": { + "balance": "0x58556bead45dcae0000" + }, + "1c68a66138783a63c98cc675a9ec77af4598d35e": { + "balance": "0x2b746f48f0f120000" + }, + "1c73d00b6e25d8eb9c1ff4ad827b6b9e9cf6d20c": { + "balance": "0xad78ebc5ac6200000" + }, + "1c751e7f24df9d94a637a5dedeffc58277b5db19": { + "balance": "0xae8e7a0bb575d00000" + }, + "1c7cb2fe6bf3e09cbcdc187af38fa8f5053a70b6": { + "balance": "0x21c84f742d0cead8000" + }, + "1c89060f987c518fa079ec2c0a5ebfa30f5d20f7": { + "balance": "0x80bfbefcb5f0bc00000" + }, + "1c94d636e684eb155895ce6db4a2588fba1d001b": { + "balance": "0x6c6b935b8bbd400000" + }, + "1c99fe9bb6c6d1066d912099547fd1f4809eacd9": { + "balance": "0x6c6b935b8bbd400000" + }, + "1cb450920078aab2317c7db3b38af7dd298b2d41": { + "balance": "0x126e72a69a50d00000" + }, + "1cb5f33b4d488936d13e3161da33a1da7df70d1b": { + "balance": "0xad78ebc5ac6200000" + }, + "1cb6b2d7cfc559b7f41e6f56ab95c7c958cd0e4c": { + "balance": "0x487a9a304539440000" + }, + "1cc1d3c14f0fb8640e36724dc43229d2ea7a1e48": { + "balance": "0x5c283d410394100000" + }, + "1cc90876004109cd79a3dea866cb840ac364ba1b": { + "balance": "0x6c6b935b8bbd400000" + }, + "1cd1f0a314cbb200de0a0cb1ef97e920709d97c2": { + "balance": "0x6c6b935b8bbd400000" + }, + "1cda411bd5163baeca1e558563601ce720e24ee1": { + "balance": "0xfc936392801c0000" + }, + "1ce81d31a7923022e125bf48a3e03693b98dc9dd": { + "balance": "0x6c6b935b8bbd400000" + }, + "1cebf0985d7f680aaa915c44cc62edb49eab269e": { + "balance": "0x3635c9adc5dea00000" + }, + "1ced6715f862b1ff86058201fcce5082b36e62b2": { + "balance": "0x16a5e60bee273b10000" + }, + "1cf04cb14380059efd3f238b65d5beb86afa14d8": { + "balance": "0x1158e460913d00000" + }, + "1cf105ab23023b554c583e86d7921179ee83169f": { + "balance": "0x6acb3df27e1f880000" + }, + "1cf2eb7a8ccac2adeaef0ee87347d535d3b94058": { + "balance": "0x6c6b935b8bbd400000" + }, + "1cfcf7517f0c08459720942b647ad192aa9c8828": { + "balance": "0x2b5e3af16b18800000" + }, + "1d09ad2412691cc581c1ab36b6f9434cd4f08b54": { + "balance": "0x17b7883c06916600000" + }, + "1d157c5876c5cad553c912caf6ce2d5277e05c73": { + "balance": "0x6c6b935b8bbd400000" + }, + "1d2615f8b6ca5012b663bdd094b0c5137c778ddf": { + "balance": "0x21e19e0c9bab2400000" + }, + "1d29c7aab42b2048d2b25225d498dba67a03fbb2": { + "balance": "0xad78ebc5ac6200000" + }, + "1d341fa5a3a1bd051f7db807b6db2fc7ba4f9b45": { + "balance": "0xfc936392801c0000" + }, + "1d344e962567cb27e44db9f2fac7b68df1c1e6f7": { + "balance": "0x692ae8897081d00000" + }, + "1d36683063b7e9eb99462dabd569bddce71686f2": { + "balance": "0x3635c9adc5dea00000" + }, + "1d37616b793f94911838ac8e19ee9449df921ec4": { + "balance": "0x5150ae84a8cdf00000" + }, + "1d395b30adda1cf21f091a4f4a7b753371189441": { + "balance": "0x152d02c7e14af6800000" + }, + "1d45586eb803ca2190650bf748a2b174312bb507": { + "balance": "0x4be4e7267b6ae00000" + }, + "1d572edd2d87ca271a6714c15a3b37761dcca005": { + "balance": "0x6ebd52a8ddd390000" + }, + "1d633097a85225a1ff4321b12988fdd55c2b3844": { + "balance": "0xd8d726b7177a800000" + }, + "1d69c83d28ff0474ceebeacb3ad227a144ece7a3": { + "balance": "0x128cc03920a62d28000" + }, + "1d96bcd58457bbf1d3c2a46ffaf16dbf7d836859": { + "balance": "0x9497209d8467e8000" + }, + "1d9e6aaf8019a05f230e5def05af5d889bd4d0f2": { + "balance": "0x73f75d1a085ba0000" + }, + "1dab172effa6fbee534c94b17e794edac54f55f8": { + "balance": "0x6acb3df27e1f880000" + }, + "1db9ac9a9eaeec0a523757050c71f47278c72d50": { + "balance": "0x487a9a304539440000" + }, + "1dbe8e1c2b8a009f85f1ad3ce80d2e05350ee39c": { + "balance": "0x7570d6e9ebbe40000" + }, + "1dc7f7dad85df53f1271152403f4e1e4fdb3afa0": { + "balance": "0xad78ebc5ac6200000" + }, + "1dcebcb7656df5dcaa3368a055d22f9ed6cdd940": { + "balance": "0x1b181e4bf2343c0000" + }, + "1dd77441844afe9cc18f15d8c77bccfb655ee034": { + "balance": "0x106eb45579944880000" + }, + "1ddefefd35ab8f658b2471e54790bc17af98dea4": { + "balance": "0x3635c9adc5dea00000" + }, + "1deec01abe5c0d952de9106c3dc30639d85005d6": { + "balance": "0x6c6b935b8bbd400000" + }, + "1df6911672679bb0ef3509038c0c27e394fdfe30": { + "balance": "0x1d460162f516f00000" + }, + "1dfaee077212f1beaf0e6f2f1840537ae154ad86": { + "balance": "0x3635c9adc5dea00000" + }, + "1e060dc6c5f1cb8cc7e1452e02ee167508b56542": { + "balance": "0x2b14f02c864c77e0000" + }, + "1e13ec51142cebb7a26083412c3ce35144ba56a1": { + "balance": "0x10f0cf064dd59200000" + }, + "1e1a4828119be309bd88236e4d482b504dc55711": { + "balance": "0xa030dcebbd2f4c0000" + }, + "1e1aed85b86c6562cb8fa1eb6f8f3bc9dcae6e79": { + "balance": "0xf4d2dd84259b240000" + }, + "1e1c6351776ac31091397ecf16002d979a1b2d51": { + "balance": "0x4be4e7267b6ae00000" + }, + "1e1d7a5f2468b94ea826982dbf2125793c6e4a5a": { + "balance": "0x3634f48417401a0000" + }, + "1e210e7047886daa52aaf70f4b991dac68e3025e": { + "balance": "0xad78ebc5ac6200000" + }, + "1e2bf4ba8e5ef18d37de6d6ad636c4cae489d0cc": { + "balance": "0x6c6b935b8bbd400000" + }, + "1e2fe4e4a77d141ff49a0c7fbc95b0a2b283eeeb": { + "balance": "0x6c6b935b8bbd400000" + }, + "1e33d1c2fb5e084f2f1d54bc5267727fec3f985d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "1e381adcf801a3bf9fd7bfac9ccc2b8482ad5e66": { + "balance": "0x208972c0010d740000" + }, + "1e3badb1b6e1380e27039c576ae6222e963a5b53": { + "balance": "0x43c33c1937564800000" + }, + "1e484d0621f0f5331b35d5408d9aae4eb1acf21e": { + "balance": "0x1158e460913d00000" + }, + "1e5800227d4dcf75e30f5595c5bed3f72e341e3b": { + "balance": "0xd75dace73417e0000" + }, + "1e596a81b357c6f24970cc313df6dbdaabd0d09e": { + "balance": "0x6c6b935b8bbd400000" + }, + "1e6915ebd9a19c81b692ad99b1218a592c1ac7b1": { + "balance": "0xd8d726b7177a800000" + }, + "1e6e0153fc161bc05e656bbb144c7187bf4fe84d": { + "balance": "0x6c6b935b8bbd400000" + }, + "1e706655e284dcf0bb37fe075d613a18dc12ff4a": { + "balance": "0xed43bf1eee82ac0000" + }, + "1e783e522ab7df0acaac9eeed3593039e5ac7579": { + "balance": "0x2b1446dd6aefe41c0000" + }, + "1e7b5e4d1f572becf2c00fc90cb4767b4a6e33d4": { + "balance": "0x61fc6107593e10000" + }, + "1e8e689b02917cdc29245d0c9c68b094b41a9ed6": { + "balance": "0x6c6b935b8bbd400000" + }, + "1ea334b5750807ea74aac5ab8694ec5f28aa77cf": { + "balance": "0x1ab2cf7c9f87e20000" + }, + "1ea4715504c6af107b0194f4f7b1cb6fcccd6f4b": { + "balance": "0x20043197e0b0270000" + }, + "1ea492bce1ad107e337f4bd4a7ac9a7babcccdab": { + "balance": "0x56bc75e2d63100000" + }, + "1ea6bf2f15ae9c1dbc64daa7f8ea4d0d81aad3eb": { + "balance": "0xe3aeb5737240a00000" + }, + "1eb4bf73156a82a0a6822080c6edf49c469af8b9": { + "balance": "0x678a932062e4180000" + }, + "1ebacb7844fdc322f805904fbf1962802db1537c": { + "balance": "0x21e19e0c9bab2400000" + }, + "1ec4ec4b77bf19d091a868e6f49154180541f90e": { + "balance": "0x6c6b935b8bbd400000" + }, + "1ed06ee51662a86c634588fb62dc43c8f27e7c17": { + "balance": "0xad78ebc5ac6200000" + }, + "1ed8bb3f06778b039e9961d81cb71a73e6787c8e": { + "balance": "0x6c6b935b8bbd400000" + }, + "1eda084e796500ba14c5121c0d90846f66e4be62": { + "balance": "0x1cfdd7468216e80000" + }, + "1eee6cbee4fe96ad615a9cf5857a647940df8c78": { + "balance": "0x10d3aa536e2940000" + }, + "1ef2dcbfe0a500411d956eb8c8939c3d6cfe669d": { + "balance": "0x2a1129d09367200000" + }, + "1ef5c9c73650cfbbde5c885531d427c7c3fe5544": { + "balance": "0x14542ba12a337c00000" + }, + "1f0412bfedcd964e837d092c71a5fcbaf30126e2": { + "balance": "0x1158e460913d00000" + }, + "1f174f40a0447234e66653914d75bc003e5690dc": { + "balance": "0x8ac7230489e800000" + }, + "1f2186ded23e0cf9521694e4e164593e690a9685": { + "balance": "0x1043561a8829300000" + }, + "1f2afc0aed11bfc71e77a907657b36ea76e3fb99": { + "balance": "0xd8d726b7177a800000" + }, + "1f3959fc291110e88232c36b7667fc78a379613f": { + "balance": "0xfc936392801c0000" + }, + "1f3da68fe87eaf43a829ab6d7ec5a6e009b204fb": { + "balance": "0x1e1601758c2c7e0000" + }, + "1f49b86d0d3945590698a6aaf1673c37755ca80d": { + "balance": "0x25f273933db5700000" + }, + "1f5f3b34bd134b2781afe5a0424ac5846cdefd11": { + "balance": "0x55de6a779bbac0000" + }, + "1f6f0030349752061c96072bc3d6eb3549208d6b": { + "balance": "0x14b8de1eb88db8000" + }, + "1f7d8e86d6eeb02545aad90e91327bd369d7d2f3": { + "balance": "0x1158e460913d00000" + }, + "1f8116bd0af5570eaf0c56c49c7ab5e37a580458": { + "balance": "0x6c6b935b8bbd400000" + }, + "1f88f8a1338fc7c10976abcd3fb8d38554b5ec9c": { + "balance": "0xb9f65d00f63c0000" + }, + "1f9c3268458da301a2be5ab08257f77bb5a98aa4": { + "balance": "0xad78ebc5ac6200000" + }, + "1fa2319fed8c2d462adf2e17feec6a6f30516e95": { + "balance": "0x6cae30621d4720000" + }, + "1fb463a0389983df7d593f7bdd6d78497fed8879": { + "balance": "0x1158e460913d00000" + }, + "1fb7bd310d95f2a6d9baaf8a8a430a9a04453a8b": { + "balance": "0xa2a15d09519be00000" + }, + "1fcc7ce6a8485895a3199e16481f72e1f762defe": { + "balance": "0x3635c9adc5dea00000" + }, + "1fcfd1d57f872290560cb62d600e1defbefccc1c": { + "balance": "0x50c5e761a444080000" + }, + "1fd296be03ad737c92f9c6869e8d80a71c5714aa": { + "balance": "0xb98bc829a6f90000" + }, + "1fddd85fc98be9c4045961f40f93805ecc4549e5": { + "balance": "0x8e3f50b173c100000" + }, + "2001bef77b66f51e1599b02fb110194a0099b78d": { + "balance": "0x6c6b935b8bbd400000" + }, + "200264a09f8c68e3e6629795280f56254f8640d0": { + "balance": "0x1158e460913d00000" + }, + "2003717907a72560f4307f1beecc5436f43d21e7": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "200dfc0b71e359b2b465440a36a6cdc352773007": { + "balance": "0x5150ae84a8cdf00000" + }, + "20134cbff88bfadc466b52eceaa79857891d831e": { + "balance": "0x3635c9adc5dea00000" + }, + "2014261f01089f53795630ba9dd24f9a34c2d942": { + "balance": "0x487a9a304539440000" + }, + "2016895df32c8ed5478269468423aea7b7fbce50": { + "balance": "0x1158e460913d00000" + }, + "20181c4b41f6f972b66958215f19f570c15ddff1": { + "balance": "0x56bc75e2d631000000" + }, + "201864a8f784c2277b0b7c9ee734f7b377eab648": { + "balance": "0xf2281400d1d5ec0000" + }, + "2020b81ae53926ace9f7d7415a050c031d585f20": { + "balance": "0x127f19e83eb3480000" + }, + "203c6283f20df7bc86542fdfb4e763ecdbbbeef5": { + "balance": "0x54b40b1f852bda00000" + }, + "204ac98867a7c9c7ed711cb82f28a878caf69b48": { + "balance": "0x14542ba12a337c00000" + }, + "205237c4be146fba99478f3a7dad17b09138da95": { + "balance": "0x6c6b935b8bbd400000" + }, + "2053ac97548a0c4e8b80bc72590cd6a098fe7516": { + "balance": "0xa2325753b460c0000" + }, + "205f5166f12440d85762c967d3ae86184f8f4d98": { + "balance": "0x177224aa844c720000" + }, + "205fc843e19a4913d1881eb69b69c0fa3be5c50b": { + "balance": "0x20dd68aaf3289100000" + }, + "206482ee6f138a778fe1ad62b180ce856fbb23e6": { + "balance": "0x6c6b935b8bbd400000" + }, + "2066774d822793ff25f1760909479cf62491bf88": { + "balance": "0xbae3ac685cb72e00000" + }, + "206d55d5792a514ec108e090599f2a065e501185": { + "balance": "0xadf30ba70c8970000" + }, + "20707e425d2a11d2c89f391b2b809f556c592421": { + "balance": "0x6c6b935b8bbd400000" + }, + "207ef80b5d60b6fbffc51f3a64b8c72036a5abbd": { + "balance": "0x16a6502f15a1e540000" + }, + "20824ba1dbebbef9846ef3d0f6c1b017e6912ec4": { + "balance": "0x184b26e4daf1d350000" + }, + "2084fce505d97bebf1ad8c5ff6826fc645371fb2": { + "balance": "0x1a055690d9db80000" + }, + "208c45732c0a378f17ac8324926d459ba8b658b4": { + "balance": "0xa030dcebbd2f4c0000" + }, + "209377b6ad3fe101c9685b3576545c6b1684e73c": { + "balance": "0x62a992e53a0af00000" + }, + "209e8e29d33beae8fb6baa783d133e1d9ec1bc0b": { + "balance": "0x2d43f3ebfafb2c0000" + }, + "20a15256d50ce058bf0eac43aa533aa16ec9b380": { + "balance": "0x1158e460913d00000" + }, + "20a29c5079e26b3f18318bb2e50e8e8b346e5be8": { + "balance": "0x1b1ab319f5ec750000" + }, + "20a81680e465f88790f0074f60b4f35f5d1e6aa5": { + "balance": "0x456180278f0c778000" + }, + "20b9a9e6bd8880d9994ae00dd0b9282a0beab816": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "20c284ba10a20830fc3d699ec97d2dfa27e1b95e": { + "balance": "0x6c6b935b8bbd400000" + }, + "20d1417f99c569e3beb095856530fe12d0fceaaa": { + "balance": "0x4015f94b1183698000" + }, + "20dd8fcbb46ea46fe381a68b8ca0ea5be21fe9a5": { + "balance": "0x6c6b935b8bbd400000" + }, + "20ff3ede8cadb5c37b48cb14580fb65e23090a7b": { + "balance": "0x8e4d316827686400000" + }, + "2100381d60a5b54adc09d19683a8f6d5bb4bfbcb": { + "balance": "0x21e19e0c9bab2400000" + }, + "2118c116ab0cdf6fd11d54a4309307b477c3fc0f": { + "balance": "0x21e19e0c9bab2400000" + }, + "211b29cefc79ae976744fdebcebd3cbb32c51303": { + "balance": "0x2f6f10780d22cc00000" + }, + "21206ce22ea480e85940d31314e0d64f4e4d3a04": { + "balance": "0x3635c9adc5dea00000" + }, + "2132c0516a2e17174ac547c43b7b0020d1eb4c59": { + "balance": "0x35659ef93f0fc40000" + }, + "21408b4d7a2c0e6eca4143f2cacdbbccba121bd8": { + "balance": "0x43c33c1937564800000" + }, + "214b743955a512de6e0d886a8cbd0282bee6d2a2": { + "balance": "0x6c6b935b8bbd400000" + }, + "214c89c5bd8e7d22bc574bb35e48950211c6f776": { + "balance": "0x10654f258fd358000" + }, + "21546914dfd3af2add41b0ff3e83ffda7414e1e0": { + "balance": "0x14395e7385a502e0000" + }, + "21582e99e502cbf3d3c23bdffb76e901ac6d56b2": { + "balance": "0x56bc75e2d63100000" + }, + "2159240813a73095a7ebf7c3b3743e8028ae5f09": { + "balance": "0x6c6b935b8bbd400000" + }, + "2160b4c02cac0a81de9108de434590a8bfe68735": { + "balance": "0x6acb3df27e1f880000" + }, + "216e41864ef98f060da08ecae19ad1166a17d036": { + "balance": "0x1369fb96128ac480000" + }, + "21846f2fdf5a41ed8df36e5ed8544df75988ece3": { + "balance": "0x6c6acc67d7b1d40000" + }, + "21a6db6527467bc6dad54bc16e9fe2953b6794ed": { + "balance": "0x2f6f10780d22cc00000" + }, + "21a6feb6ab11c766fdd977f8df4121155f47a1c0": { + "balance": "0x319cf38f100580000" + }, + "21b182f2da2b384493cf5f35f83d9d1ee14f2a21": { + "balance": "0x6c6b935b8bbd400000" + }, + "21bfe1b45cacde6274fd8608d9a178bf3eeb6edc": { + "balance": "0x6cee06ddbe15ec0000" + }, + "21c07380484f6cbc8724ad32bc864c3b5ad500b7": { + "balance": "0x3635c9adc5dea00000" + }, + "21c3a8bba267c8cca27b1a9afabad86f607af708": { + "balance": "0x1e4a36c49d998300000" + }, + "21ce6d5b9018cec04ad6967944bea39e8030b6b8": { + "balance": "0x1158e460913d00000" + }, + "21d02705f3f64905d80ed9147913ea8c7307d695": { + "balance": "0x49edb1c09887360000" + }, + "21d13f0c4024e967d9470791b50f22de3afecf1b": { + "balance": "0xf15ad35e2e31e50000" + }, + "21dbdb817a0d8404c6bdd61504374e9c43c9210e": { + "balance": "0x21e18b9e9ab45e48000" + }, + "21df1ec24b4e4bfe79b0c095cebae198f291fbd1": { + "balance": "0x43c33c1937564800000" + }, + "21df2dcdaf74b2bf803404dd4de6a35eabec1bbd": { + "balance": "0x177224aa844c7200000" + }, + "21e219c89ca8ac14ae4cba6130eeb77d9e6d3962": { + "balance": "0x2acd9faaa038ee0000" + }, + "21e5d2bae995ccfd08a5c16bb524e1f630448f82": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "21e5d77320304c201c1e53b261a123d0a1063e81": { + "balance": "0x4b6fa9d33dd460000" + }, + "21eae6feffa9fbf4cd874f4739ace530ccbe5937": { + "balance": "0x10f0cf064dd59200000" + }, + "21ecb2dfa65779c7592d041cd2105a81f4fd4e46": { + "balance": "0x3635c9adc5dea00000" + }, + "21efbca09b3580b98e73f5b2f7f4dc0bf02c529c": { + "balance": "0x6c6b935b8bbd400000" + }, + "21fd0bade5f4ef7474d058b7f3d854cb1300524e": { + "balance": "0x1158e460913d00000" + }, + "21fd47c5256012198fa5abf131c06d6aa1965f75": { + "balance": "0x1ab2cf7c9f87e200000" + }, + "21fd6c5d97f9c600b76821ddd4e776350fce2be0": { + "balance": "0x6c6ad382d4fb610000" + }, + "220dc68df019b6b0ccbffb784b5a5ab4b15d4060": { + "balance": "0xd5967be4fc3f100000" + }, + "220e2b92c0f6c902b513d9f1e6fab6a8b0def3d7": { + "balance": "0x2b5e3af16b18800000" + }, + "22561c5931143536309c17e832587b625c390b9a": { + "balance": "0xd8d726b7177a800000" + }, + "2257fca16a6e5c2a647c3c29f36ce229ab93b17e": { + "balance": "0xd8d726b7177a800000" + }, + "225d35faedb391c7bc2db7fa9071160405996d00": { + "balance": "0x91854fc1862630000" + }, + "225f9eb3fb6ff3e9e3c8447e14a66e8d4f3779f6": { + "balance": "0x6c6b935b8bbd400000" + }, + "2272186ef27dcbe2f5fc373050fdae7f2ace2316": { + "balance": "0x368c8623a8b4d100000" + }, + "2273bad7bc4e487622d175ef7a66988b6a93c4ee": { + "balance": "0x1158e460913d00000" + }, + "2276264bec8526c0c0f270677abaf4f0e441e167": { + "balance": "0x3635c9adc5dea00000" + }, + "228242f8336eecd8242e1f000f41937e71dffbbf": { + "balance": "0x10f0cf064dd59200000" + }, + "22842ab830da509913f81dd1f04f10af9edd1c55": { + "balance": "0x6c6b935b8bbd400000" + }, + "22944fbca9b57963084eb84df7c85fb9bcdfb856": { + "balance": "0xfc118fef90ba388000" + }, + "229cc4711b62755ea296445ac3b77fc633821cf2": { + "balance": "0x223e8b05219328000" + }, + "229e430de2b74f442651ddcdb70176bc054cad54": { + "balance": "0xbbf981bc4aaa8000" + }, + "229f4f1a2a4f540774505b4707a81de44410255b": { + "balance": "0x6c6b935b8bbd400000" + }, + "229ff80bf5708009a9f739e0f8b560914016d5a6": { + "balance": "0x1211ecb56d13488000" + }, + "22a25812ab56dcc423175ed1d8adacce33cd1810": { + "balance": "0x6449e84e47a8a80000" + }, + "22b96ab2cad55db100b53001f9e4db378104c807": { + "balance": "0x21e19e0c9bab2400000" + }, + "22bdffc240a88ff7431af3bff50e14da37d5183e": { + "balance": "0x3635c9adc5dea00000" + }, + "22ce349159eeb144ef06ff2636588aef79f62832": { + "balance": "0xa31062beeed700000" + }, + "22db559f2c3c1475a2e6ffe83a5979599196a7fa": { + "balance": "0x3635c9adc5dea00000" + }, + "22e15158b5ee3e86eb0332e3e6a9ac6cd9b55ecd": { + "balance": "0x8ac7230489e800000" + }, + "22e2488e2da26a49ae84c01bd54b21f2947891c6": { + "balance": "0x5dc892aa1131c80000" + }, + "22e512149a18d369b73c71efa43e86c9edabaf1d": { + "balance": "0x4ee02e6714615c0000" + }, + "22eb7db0ba56b0f8b816ccb206e615d929185b0d": { + "balance": "0x45d29737e22f20000" + }, + "22eed327f8eb1d1338a3cb7b0f8a4baa5907cd95": { + "balance": "0x1455d5f4877088000" + }, + "22f004df8de9e6ebf523ccace457accb26f97281": { + "balance": "0x21e19e0c9bab2400000" + }, + "22f2dcff5ad78c3eb6850b5cb951127b659522e6": { + "balance": "0xbe202d6a0eda0000" + }, + "22f3c779dd79023ea92a78b65c1a1780f62d5c4a": { + "balance": "0x6acb3df27e1f880000" + }, + "22fe884d9037291b4d52e6285ae68dea0be9ffb5": { + "balance": "0x6c6b935b8bbd400000" + }, + "2306df931a940d58c01665fa4d0800802c02edfe": { + "balance": "0x3635c9adc5dea00000" + }, + "2309d34091445b3232590bd70f4f10025b2c9509": { + "balance": "0x21e19e0c9bab2400000" + }, + "23120046f6832102a752a76656691c863e17e59c": { + "balance": "0x11e0e4f8a50bd40000" + }, + "231a15acc199c89fa9cb22441cc70330bdcce617": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "231d94155dbcfe2a93a319b6171f63b20bd2b6fa": { + "balance": "0xcf147bb906e2f80000" + }, + "232832cd5977e00a4c30d0163f2e24f088a6cb09": { + "balance": "0xa2a15d09519be00000" + }, + "232c6d03b5b6e6711efff190e49c28eef36c82b0": { + "balance": "0x487a9a304539440000" + }, + "232cb1cd49993c144a3f88b3611e233569a86bd6": { + "balance": "0x34c606c42d0ac600000" + }, + "232ce782506225fd9860a2edc14a7a3047736da2": { + "balance": "0x1158e460913d00000" + }, + "232f525d55859b7d4e608d20487faadb00293135": { + "balance": "0xd8d726b7177a800000" + }, + "2334c590c7a48769103045c5b6534c8a3469f44a": { + "balance": "0x3b199073df72dc00000" + }, + "23376ecabf746ce53321cf42c86649b92b67b2ff": { + "balance": "0x6c6b935b8bbd400000" + }, + "23378f42926d0184b793b0c827a6dd3e3d334fcd": { + "balance": "0x30927f74c9de00000" + }, + "233842b1d0692fd11140cf5acda4bf9630bae5f8": { + "balance": "0x6c6b935b8bbd400000" + }, + "2339e9492870afea2537f389ac2f838302a33c06": { + "balance": "0x6c6b935b8bbd400000" + }, + "233bdddd5da94852f4ade8d212885682d9076bc6": { + "balance": "0xd8d726b7177a800000" + }, + "234f46bab73fe45d31bf87f0a1e0466199f2ebac": { + "balance": "0x1a4aba225c20740000" + }, + "23551f56975fe92b31fa469c49ea66ee6662f41e": { + "balance": "0x678a932062e4180000" + }, + "23569542c97d566018c907acfcf391d14067e87e": { + "balance": "0x6c6b935b8bbd400000" + }, + "235fa66c025ef5540070ebcf0d372d8177c467ab": { + "balance": "0x7129e1cdf373ee00000" + }, + "2372c4c1c9939f7aaf6cfac04090f00474840a09": { + "balance": "0x21e19e0c9bab2400000" + }, + "23730c357a91026e44b1d0e2fc2a51d071d8d77b": { + "balance": "0xd8d726b7177a800000" + }, + "2376ada90333b1d181084c97e645e810aa5b76f1": { + "balance": "0x28a857425466f80000" + }, + "2378fd4382511e968ed192106737d324f454b535": { + "balance": "0x3635c9adc5dea00000" + }, + "2382a9d48ec83ea3652890fd0ee79c907b5b2dc1": { + "balance": "0x73f75d1a085ba0000" + }, + "2383c222e67e969190d3219ef14da37850e26c55": { + "balance": "0x6c6b935b8bbd400000" + }, + "238a6b7635252f5244486c0af0a73a207385e039": { + "balance": "0x4a4491bd6dcd280000" + }, + "239a733e6b855ac592d663156186a8a174d2449e": { + "balance": "0x58be3758b241f60000" + }, + "23ab09e73f87aa0f3be0139df0c8eb6be5634f95": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "23abd9e93e7957e5b636be6579051c15e5ce0b0e": { + "balance": "0x3a3c8f7cbf42c380000" + }, + "23b1c4917fbd93ee3d48389306957384a5496cbf": { + "balance": "0xd8d8583fa2d52f0000" + }, + "23ba3864da583dab56f420873c37679690e02f00": { + "balance": "0x21342520d5fec200000" + }, + "23c55aeb5739876f0ac8d7ebea13be729685f000": { + "balance": "0x487a9a304539440000" + }, + "23c99ba087448e19c9701df66e0cab52368331fa": { + "balance": "0x6c6b935b8bbd400000" + }, + "23ccc3c6acd85c2e460c4ffdd82bc75dc849ea14": { + "balance": "0xd8d726b7177a800000" + }, + "23cd2598a20e149ead2ad69379576ecedb60e38e": { + "balance": "0x6c6b935b8bbd400000" + }, + "23df8f48ee009256ea797e1fa369beebcf6bc663": { + "balance": "0x7cd3fac26d19818000" + }, + "23e2c6a8be8e0acfa5c4df5e36058bb7cbac5a81": { + "balance": "0x6c6b935b8bbd400000" + }, + "23ea669e3564819a83b0c26c00a16d9e826f6c46": { + "balance": "0x4d8d6ca968ca130000" + }, + "23eb6fd85671a9063ab7678ebe265a20f61a02b3": { + "balance": "0x6c6b935b8bbd400000" + }, + "23f9ecf3e5dddca38815d3e59ed34b5b90b4a353": { + "balance": "0xb1781a3f0bb200000" + }, + "23fa7eb51a48229598f97e762be0869652dffc66": { + "balance": "0x3635c9adc5dea00000" + }, + "240305727313d01e73542c775ff59d11cd35f819": { + "balance": "0x141885666807f5c8000" + }, + "24046b91da9b61b629cb8b8ec0c351a07e0703e4": { + "balance": "0x6c6b935b8bbd400000" + }, + "240e559e274aaef0c258998c979f671d1173b88b": { + "balance": "0xd8d726b7177a800000" + }, + "241361559feef80ef137302153bd9ed2f25db3ef": { + "balance": "0x43c33c1937564800000" + }, + "243b3bca6a299359e886ce33a30341fafe4d573d": { + "balance": "0x43c33c1937564800000" + }, + "243c84d12420570cc4ef3baba1c959c283249520": { + "balance": "0x7f1f6993a853040000" + }, + "24434a3e32e54ecf272fe3470b5f6f512f675520": { + "balance": "0x14061b9d77a5e980000" + }, + "2448596f91c09baa30bc96106a2d37b5705e5d28": { + "balance": "0x6c6b935b8bbd400000" + }, + "24586ec5451735eeaaeb470dc8736aae752f82e5": { + "balance": "0xf43fc2c04ee00000" + }, + "2458d6555ff98a129cce4037953d00206eff4287": { + "balance": "0xaadec983fcff40000" + }, + "246291165b59332df5f18ce5c98856fae95897d6": { + "balance": "0x5c283d410394100000" + }, + "2467c6a5c696ede9a1e542bf1ad06bcc4b06aca0": { + "balance": "0x100bd33fb98ba0000" + }, + "2476b2bb751ce748e1a4c4ff7b230be0c15d2245": { + "balance": "0xd8d726b7177a800000" + }, + "247a0a11c57f0383b949de540b66dee68604b0a1": { + "balance": "0x39fbae8d042dd00000" + }, + "2487c3c4be86a2723d917c06b458550170c3edba": { + "balance": "0x3635c9adc5dea00000" + }, + "2489ac126934d4d6a94df08743da7b7691e9798e": { + "balance": "0x3635c9adc5dea00000" + }, + "249db29dbc19d1235da7298a04081c315742e9ac": { + "balance": "0x61acff81a78ad40000" + }, + "24a4eb36a7e498c36f99975c1a8d729fd6b305d7": { + "balance": "0xdfc78210eb2c80000" + }, + "24a750eae5874711116dd7d47b7186ce990d3103": { + "balance": "0xad78ebc5ac6200000" + }, + "24aa1151bb765fa3a89ca50eb6e1b1c706417fd4": { + "balance": "0xa80d24677efef00000" + }, + "24aca08d5be85ebb9f3132dfc1b620824edfedf9": { + "balance": "0xfc936392801c0000" + }, + "24b2be118b16d8b2174769d17b4cf84f07ca946d": { + "balance": "0x6c6b935b8bbd400000" + }, + "24b8b446debd1947955dd084f2c544933346d3ad": { + "balance": "0xea696d904039bd8000" + }, + "24b95ebef79500baa0eda72e77f877415df75c33": { + "balance": "0x3154c9729d05780000" + }, + "24b9e6644f6ba4cde126270d81f6ab60f286dff4": { + "balance": "0x73f75d1a085ba0000" + }, + "24bd5904059091d2f9e12d6a26a010ca22ab14e8": { + "balance": "0x65ea3db75546600000" + }, + "24c0c88b54a3544709828ab4ab06840559f6c5e2": { + "balance": "0x90f534608a72880000" + }, + "24c117d1d2b3a97ab11a4679c99a774a9eade8d1": { + "balance": "0x3635c9adc5dea00000" + }, + "24cff0e9336a9f80f9b1cb968caf6b1d1c4932a4": { + "balance": "0xada55474b81340000" + }, + "24daaaddf7b06bbcea9b80590085a88567682b4e": { + "balance": "0x114b2015d2bbd00000" + }, + "24dcc24bd9c7210ceacfb30da98ae04a4d7b8ab9": { + "balance": "0x3635c9adc5dea00000" + }, + "24f7450ddbf18b020feb1a2032d9d54b633edf37": { + "balance": "0x2b5e3af16b1880000" + }, + "24fc73d20793098e09ddab5798506224fa1e1850": { + "balance": "0xad78ebc5ac6200000" + }, + "24fd9a6c874c2fab3ff36e9afbf8ce0d32c7de92": { + "balance": "0x487a9a304539440000" + }, + "250a40cef3202397f240469548beb5626af4f23c": { + "balance": "0x503b203e9fba20000" + }, + "250a69430776f6347703f9529783955a6197b682": { + "balance": "0x692ae8897081d00000" + }, + "250eb7c66f869ddf49da85f3393e980c029aa434": { + "balance": "0xd8d726b7177a800000" + }, + "25106ab6755df86d6b63a187703b0cfea0e594a0": { + "balance": "0x17c405ad41db40000" + }, + "25185f325acf2d64500698f65c769ddf68301602": { + "balance": "0x10f0cf064dd59200000" + }, + "251c12722c6879227992a304eb3576cd18434ea5": { + "balance": "0x6c6b935b8bbd400000" + }, + "251e6838f7cec5b383c1d90146341274daf8e502": { + "balance": "0x7ff1ccb7561df0000" + }, + "25259d975a21d83ae30e33f800f53f37dfa01938": { + "balance": "0x1158e460913d00000" + }, + "25287b815f5c82380a73b0b13fbaf982be24c4d3": { + "balance": "0x22b1c8c1227a00000" + }, + "252b6555afdc80f2d96d972d17db84ea5ad521ac": { + "balance": "0x1ab2cf7c9f87e200000" + }, + "2538532936813c91e653284f017c80c3b8f8a36f": { + "balance": "0x6c8754c8f30c080000" + }, + "253e32b74ea4490ab92606fda0aa257bf23dcb8b": { + "balance": "0x21e19e0c9bab2400000" + }, + "253f1e742a2cec86b0d7b306e5eacb6ccb2f8554": { + "balance": "0x43e5ede1f878c200000" + }, + "2541314a0b408e95a694444977712a50713591ab": { + "balance": "0x589e1a5df4d7b50000" + }, + "254c1ecc630c2877de8095f0a8dba1e8bf1f550c": { + "balance": "0x5c283d410394100000" + }, + "255abc8d08a096a88f3d6ab55fbc7352bddcb9ce": { + "balance": "0x4743682313ede8000" + }, + "255bdd6474cc8262f26a22c38f45940e1ceea69b": { + "balance": "0xd8d726b7177a800000" + }, + "2560b09b89a4ae6849ed5a3c9958426631714466": { + "balance": "0x5c283d410394100000" + }, + "2561a138dcf83bd813e0e7f108642be3de3d6f05": { + "balance": "0x3634f48417401a0000" + }, + "2561ec0f379218fe5ed4e028a3f744aa41754c72": { + "balance": "0xb98bc829a6f90000" + }, + "256292a191bdda34c4da6b6bd69147bf75e2a9ab": { + "balance": "0xc2ff2e0dfb038000" + }, + "25697ef20cccaa70d32d376f8272d9c1070c3d78": { + "balance": "0xad78ebc5ac6200000" + }, + "256fa150cc87b5056a07d004efc84524739e62b5": { + "balance": "0xad78ebc5ac6200000" + }, + "25721c87b0dc21377c7200e524b14a22f0af69fb": { + "balance": "0xd8d726b7177a800000" + }, + "258939bbf00c9de9af5338f5d714abf6d0c1c671": { + "balance": "0x54069233bf7f780000" + }, + "2590126870e0bde8a663ab040a72a5573d8d41c2": { + "balance": "0x10f0cf064dd59200000" + }, + "259ec4d265f3ab536b7c70fa97aca142692c13fc": { + "balance": "0x11b1b5bea89f80000" + }, + "25a500eeec7a662a841552b5168b707b0de21e9e": { + "balance": "0x21f2f6f0fc3c6100000" + }, + "25a5a44d38a2f44c6a9db9cdbc6b1e2e97abb509": { + "balance": "0x39992648a23c8a00000" + }, + "25a74c2ac75dc8baa8b31a9c7cb4b7829b2456da": { + "balance": "0x6c6b935b8bbd400000" + }, + "25adb8f96f39492c9bb47c5edc88624e46075697": { + "balance": "0x5a9940bc56879500000" + }, + "25aee68d09afb71d8817f3f184ec562f7897b734": { + "balance": "0x6c6b935b8bbd400000" + }, + "25b0533b81d02a617b9229c7ec5d6f2f672e5b5a": { + "balance": "0x3635c9adc5dea00000" + }, + "25b78c9fad85b43343f0bfcd0fac11c9949ca5eb": { + "balance": "0x6c6b935b8bbd400000" + }, + "25bc49ef288cd165e525c661a812cf84fbec8f33": { + "balance": "0x125921aebda9d00000" + }, + "25bdfa3ee26f3849617b230062588a97e3cae701": { + "balance": "0x3635e619bb04d40000" + }, + "25c1a37ee5f08265a1e10d3d90d5472955f97806": { + "balance": "0x62a992e53a0af00000" + }, + "25c6e74ff1d928df98137af4df8430df24f07cd7": { + "balance": "0x15245655b102580000" + }, + "25cfc4e25c35c13b69f7e77dbfb08baf58756b8d": { + "balance": "0x878678326eac9000000" + }, + "25dad495a11a86b9eeece1eeec805e57f157faff": { + "balance": "0x3635c9adc5dea000000" + }, + "25e037f00a18270ba5ec3420229ddb0a2ce38fa2": { + "balance": "0x21e19e0c9bab2400000" + }, + "25e661c939863acc044e6f17b5698cce379ec3cc": { + "balance": "0x4a4491bd6dcd280000" + }, + "26048fe84d9b010a62e731627e49bc2eb73f408f": { + "balance": "0xd8d726b7177a800000" + }, + "2606c3b3b4ca1b091498602cb1978bf3b95221c0": { + "balance": "0x15af1d78b58c400000" + }, + "260a230e4465077e0b14ee4442a482d5b0c914bf": { + "balance": "0x5af606a06b5b118000" + }, + "260df8943a8c9a5dba7945327fd7e0837c11ad07": { + "balance": "0xad78ebc5ac6200000" + }, + "2614f42d5da844377578e6b448dc24305bef2b03": { + "balance": "0x6c6b935b8bbd400000" + }, + "2615100ea7e25bba9bca746058afbbb4ffbe4244": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "261575e9cf59c8226fa7aaf91de86fb70f5ac3ae": { + "balance": "0x1043a4436a523f0000" + }, + "261e0fa64c51137465eecf5b90f197f7937fdb05": { + "balance": "0x3cfc82e37e9a7400000" + }, + "262a8bfd7d9dc5dd3ad78161b6bb560824373655": { + "balance": "0x3f6a8384072b760000" + }, + "262aed4bc0f4a4b2c6fb35793e835a49189cdfec": { + "balance": "0x21e19e0c9bab2400000" + }, + "262dc1364ccf6df85c43268ee182554dae692e29": { + "balance": "0x10b202fec74ced80000" + }, + "263814309de4e635cf585e0d365477fc40e66cf7": { + "balance": "0x7ea28327577080000" + }, + "2639eee9873ceec26fcc9454b548b9e7c54aa65c": { + "balance": "0x3635c9adc5dea00000" + }, + "263e57dacbe0149f82fe65a2664898866ff5b463": { + "balance": "0x80bfbefcb5f0bc00000" + }, + "26475419c06d5f147aa597248eb46cf7befa64a5": { + "balance": "0x58e7926ee858a00000" + }, + "264cc8086a8710f91b21720905912cd7964ae868": { + "balance": "0x1731790534df20000" + }, + "265383d68b52d034161bfab01ae1b047942fbc32": { + "balance": "0x47271dee20d745c0000" + }, + "2659facb1e83436553b5b42989adb8075f9953ed": { + "balance": "0x1976576771a5e0000" + }, + "266f2da7f0085ef3f3fa09baee232b93c744db2e": { + "balance": "0xcb49b44ba602d800000" + }, + "267148fd72c54f620a592fb92799319cc4532b5c": { + "balance": "0x1639e49bba16280000" + }, + "26784ade91c8a83a8e39658c8d8277413ccc9954": { + "balance": "0x14542ba12a337c00000" + }, + "267a7e6e82e1b91d51deddb644f0e96dbb1f7f7e": { + "balance": "0x1158e460913d00000" + }, + "2680713d40808e2a50ed013150a2a694b96a7f1d": { + "balance": "0x61093d7c2c6d380000" + }, + "2697b339813b0c2d964b2471eb1c606f4ecb9616": { + "balance": "0x3e8ef795d890c80000" + }, + "26a68eab905a8b3dce00e317308225dab1b9f6b8": { + "balance": "0x6b56051582a9700000" + }, + "26b11d066588ce74a572a85a6328739212aa8b40": { + "balance": "0x6c6b935b8bbd400000" + }, + "26babf42b267fdcf3861fdd4236a5e474848b358": { + "balance": "0x3635c9adc5dea00000" + }, + "26c0054b700d3a7c2dcbe275689d4f4cad16a335": { + "balance": "0x6c6b935b8bbd400000" + }, + "26c2ffc30efdc5273e76183a16c2698d6e531286": { + "balance": "0x2a1129d09367200000" + }, + "26c99f8849c9802b83c861217fd07a9e84cdb79d": { + "balance": "0x1043561a8829300000" + }, + "26cfffd052152bb3f957b478d5f98b233a7c2b92": { + "balance": "0xd8d726b7177a800000" + }, + "26d4a16891f52922789217fcd886f7fce296d400": { + "balance": "0x6c6b935b8bbd400000" + }, + "26d4ec17d5ceb2c894bdc59d0a6a695dad2b43cc": { + "balance": "0x9f1f78761d341a0000" + }, + "26e801b62c827191dd68d31a011990947fd0ebe0": { + "balance": "0x1158e460913d00000" + }, + "26e9e2ad729702626417ef25de0dc800f7a779b3": { + "balance": "0x3635c9adc5dea00000" + }, + "26f9f7cefd7e394b9d3924412bf2c2831faf1f85": { + "balance": "0xd8d726b7177a800000" + }, + "26fe174cbf526650e0cd009bd6126502ce8e684d": { + "balance": "0x277017338a30ae00000" + }, + "26ff0a51e7cece8400276978dbd6236ef162c0e6": { + "balance": "0x152e185627540a500000" + }, + "27101a0f56d39a88c5a84f9b324cdde33e5cb68c": { + "balance": "0x6c6b935b8bbd400000" + }, + "27144ca9a7771a836ad50f803f64d869b2ae2b20": { + "balance": "0xd8d726b7177a800000" + }, + "27146913563aa745e2588430d9348e86ea7c3510": { + "balance": "0x15af1d78b58c400000" + }, + "271d3d481cb88e7671ad216949b6365e06303de0": { + "balance": "0xd8d726b7177a800000" + }, + "2720f9ca426ef2f2cbd2fecd39920c4f1a89e16d": { + "balance": "0x6c6b935b8bbd400000" + }, + "272a131a5a656a7a3aca35c8bd202222a7592258": { + "balance": "0x90f534608a72880000" + }, + "2744ff67464121e35afc2922177164fa2fcb0267": { + "balance": "0x56bc75e2d63100000" + }, + "274a3d771a3d709796fbc4d5f48fce2fe38c79d6": { + "balance": "0x1158e460913d00000" + }, + "274d69170fe7141401882b886ac4618c6ae40edb": { + "balance": "0x33c5499031720c0000" + }, + "27521deb3b6ef1416ea4c781a2e5d7b36ee81c61": { + "balance": "0x6c6b935b8bbd400000" + }, + "275875ff4fbb0cf3a430213127487f7608d04cba": { + "balance": "0x1b1c010e766d580000" + }, + "276a006e3028ecd44cdb62ba0a77ce94ebd9f10f": { + "balance": "0x6194049f30f7200000" + }, + "276b0521b0e68b277df0bb32f3fd48326350bfb2": { + "balance": "0x2b5e3af16b1880000" + }, + "276fd7d24f8f883f5a7a28295bf17151c7a84b03": { + "balance": "0x6c6b935b8bbd400000" + }, + "2770f14efb165ddeba79c10bb0af31c31e59334c": { + "balance": "0xa2a15d09519be00000" + }, + "277677aba1e52c3b53bfa2071d4e859a0af7e8e1": { + "balance": "0x3635c9adc5dea00000" + }, + "27824666d278d70423f03dfe1dc7a3f02f43e2b5": { + "balance": "0x3636c25e66ece70000" + }, + "27830c5f6023afaaf79745676c204a0faccda0ba": { + "balance": "0xd02ab486cedc00000" + }, + "2784903f1d7c1b5cd901f8875d14a79b3cbe2a56": { + "balance": "0x4bda7e9d74ad5500000" + }, + "278c0bde630ec393b1e7267fc9d7d97019e4145b": { + "balance": "0x6c6b935b8bbd400000" + }, + "27987110221a880826adb2e7ab5eca78c6e31aec": { + "balance": "0xd8d726b7177a800000" + }, + "27ac073be79ce657a93aa693ee43bf0fa41fef04": { + "balance": "0xa968163f0a57b400000" + }, + "27b1694eafa165ebd7cc7bc99e74814a951419dc": { + "balance": "0x2b5e3af16b18800000" + }, + "27b62816e1e3b8d19b79d1513d5dfa855b0c3a2a": { + "balance": "0x56af5c1fd69508000" + }, + "27bf943c1633fe32f8bcccdb6302b407a5724e44": { + "balance": "0x32f84c6df408c08000" + }, + "27bf9f44ba7d05c33540c3a53bb02cbbffe7c3c6": { + "balance": "0x6c6b935b8bbd400000" + }, + "27c2d7ca504daa3d9066dc09137dc42f3aaab452": { + "balance": "0x2086ac351052600000" + }, + "27d158ac3d3e1109ab6e570e90e85d3892cd7680": { + "balance": "0x56bc75e2d63100000" + }, + "27e63989ca1e903bc620cf1b9c3f67b9e2ae6581": { + "balance": "0x487a9a304539440000" + }, + "27f03cf1abc5e1b51dbc444b289e542c9ddfb0e6": { + "balance": "0x10f0cf064dd59200000" + }, + "27fc85a49cff90dbcfdadc9ddd40d6b9a2210a6c": { + "balance": "0x56bc75e2d63100000" + }, + "2805415e1d7fdec6dedfb89e521d10592d743c10": { + "balance": "0x56bc75e2d63100000" + }, + "28073efc17d05cab3195c2db332b61984777a612": { + "balance": "0x3635c9adc5dea00000" + }, + "281250a29121270a4ee5d78d24feafe82c70ba3a": { + "balance": "0x3635c9adc5dea00000" + }, + "2813d263fc5ff2479e970595d6b6b560f8d6d6d1": { + "balance": "0x6c6b935b8bbd400000" + }, + "282e80a554875a56799fa0a97f5510e795974c4e": { + "balance": "0x3635c9adc5dea00000" + }, + "283396ce3cac398bcbe7227f323e78ff96d08767": { + "balance": "0x15af1d78b58c400000" + }, + "28349f7ef974ea55fe36a1583b34cec3c45065f0": { + "balance": "0xcb633d49e65590000" + }, + "2836123046b284e5ef102bfd22b1765e508116ad": { + "balance": "0x1653fbb5c427e40000" + }, + "283c2314283c92d4b064f0aef9bb5246a7007f39": { + "balance": "0xad78ebc5ac6200000" + }, + "283e11203749b1fa4f32febb71e49d135919382a": { + "balance": "0x3635c9adc5dea00000" + }, + "283e6252b4efcf4654391acb75f903c59b78c5fb": { + "balance": "0x28a857425466f800000" + }, + "28510e6eff1fc829b6576f4328bc3938ec7a6580": { + "balance": "0x21e19e0c9bab2400000" + }, + "2858acacaf21ea81cab7598fdbd86b452e9e8e15": { + "balance": "0x241a9b4f617a280000" + }, + "285ae51b9500c58d541365d97569f14bb2a3709b": { + "balance": "0x6c6b935b8bbd400000" + }, + "2866b81decb02ee70ae250cee5cdc77b59d7b679": { + "balance": "0x6c6b935b8bbd400000" + }, + "286906b6bd4972e3c71655e04baf36260c7cb153": { + "balance": "0x126e72a69a50d00000" + }, + "286b186d61ea1fd78d9930fe12b06537b05c3d51": { + "balance": "0x3635c9adc5dea00000" + }, + "2874f3e2985d5f7b406627e17baa772b01abcc9e": { + "balance": "0x146050410765f380000" + }, + "287cf9d0902ef819a7a5f149445bf1775ee8c47c": { + "balance": "0x3635c9adc5dea000000" + }, + "28818e18b610001321b31df6fe7d2815cdadc9f5": { + "balance": "0x3635c9adc5dea00000" + }, + "28868324337e11ba106cb481da962f3a8453808d": { + "balance": "0x6c6b935b8bbd400000" + }, + "28904bb7c4302943b709b14d7970e42b8324e1a1": { + "balance": "0x21f97846a072d7e0000" + }, + "2895e80999d406ad592e2b262737d35f7db4b699": { + "balance": "0x692ae8897081d00000" + }, + "28967280214e218a120c5dda37041b111ea36d74": { + "balance": "0xad78ebc5ac6200000" + }, + "28a3da09a8194819ae199f2e6d9d1304817e28a5": { + "balance": "0x6c6b935b8bbd400000" + }, + "28ab165ffb69eda0c549ae38e9826f5f7f92f853": { + "balance": "0x464df6d7c844590000" + }, + "28b77585cb3d55a199ab291d3a18c68fe89a848a": { + "balance": "0x6a4076cf7995a00000" + }, + "28d4ebf41e3d3c451e943bdd7e1f175fae932a3d": { + "balance": "0x14542ba12a337c00000" + }, + "28d7e5866f1d85fd1ceb32bfbe1dfc36db434566": { + "balance": "0x1864231c610351c0000" + }, + "28d8c35fb7eea622582135e3ad47a227c9a663bd": { + "balance": "0xfc936392801c0000" + }, + "28e4af30cd93f686a122ad7bb19f8a8785eee342": { + "balance": "0x71e53b706cc7b40000" + }, + "28eaea78cd4d95faecfb68836eafe83520f3bbb7": { + "balance": "0xad78ebc5ac6200000" + }, + "28efae6356509edface89fc61a7fdcdb39eea8e5": { + "balance": "0x121ea68c114e5100000" + }, + "28fa2580f9ebe420f3e5eefdd371638e3b7af499": { + "balance": "0x14542ba12a337c00000" + }, + "2901f8077f34190bb47a8e227fa29b30ce113b31": { + "balance": "0x56bc75e2d63100000" + }, + "2905b192e83ce659aa355b9d0c204e3e95f9bb9a": { + "balance": "0x75235c1d00393e8000" + }, + "290a56d41f6e9efbdcea0342e0b7929a8cdfcb05": { + "balance": "0x12a5f58168ee600000" + }, + "2915624bcb679137b8dae9ab57d11b4905eaee4b": { + "balance": "0x1158e460913d00000" + }, + "291efe0081dce8c14799f7b2a43619c0c3b3fc1f": { + "balance": "0x410d586a20a4c00000" + }, + "291f929ca59b54f8443e3d4d75d95dee243cef78": { + "balance": "0x1b1a089237073d0000" + }, + "29298ccbdff689f87fe41aa6e98fdfb53deaf37a": { + "balance": "0x4315c32d71a9e600000" + }, + "292f228b0a94748c8eec612d246f989363e08f08": { + "balance": "0xa076407d3f7440000" + }, + "293384c42b6f8f2905ce52b7205c2274376c612b": { + "balance": "0x4be4e7267b6ae00000" + }, + "2934c0df7bbc172b6c186b0b72547ace8bf75454": { + "balance": "0x340aad21b3b700000" + }, + "293c2306df3604ae4fda0d207aba736f67de0792": { + "balance": "0xad78ebc5ac6200000" + }, + "2949fd1def5c76a286b3872424809a07db3966f3": { + "balance": "0x11bd906daa0c9438000" + }, + "294f494b3f2e143c2ffc9738cbfd9501850b874e": { + "balance": "0x796e3ea3f8ab000000" + }, + "2955c357fd8f75d5159a3dfa69c5b87a359dea8c": { + "balance": "0x6c6b935b8bbd400000" + }, + "2961fb391c61957cb5c9e407dda29338d3b92c80": { + "balance": "0x3634fb9f1489a70000" + }, + "29681d9912ddd07eaabb88d05d90f766e862417d": { + "balance": "0x3635c9adc5dea00000" + }, + "296b71c0015819c242a7861e6ff7eded8a5f71e3": { + "balance": "0x6c68ccd09b022c0000" + }, + "296d66b521571a4e4103a7f562c511e6aa732d81": { + "balance": "0x243d4d18229ca20000" + }, + "296f00de1dc3bb01d47a8ccd1e5d1dd9a1eb7791": { + "balance": "0x3635c9adc5dea00000" + }, + "297385e88634465685c231a314a0d5dcd146af01": { + "balance": "0x54069233bf7f780000" + }, + "29763dd6da9a7c161173888321eba6b63c8fb845": { + "balance": "0x11c7ea162e78200000" + }, + "2979741174a8c1ea0b7f9edf658177859417f512": { + "balance": "0x1901966c8496838000" + }, + "297a88921b5fca10e5bb9ded60025437ae221694": { + "balance": "0xad78ebc5ac6200000" + }, + "297d5dbe222f2fb52531acbd0b013dc446ac7368": { + "balance": "0x43c33c1937564800000" + }, + "29824e94cc4348bc963279dcdf47391715324cd3": { + "balance": "0x692ae8897081d00000" + }, + "2982d76a15f847dd41f1922af368fe678d0e681e": { + "balance": "0x56bc75e2d63100000" + }, + "298887bab57c5ba4f0615229d7525fa113b7ea89": { + "balance": "0x22b1c8c1227a00000" + }, + "298ec76b440d8807b3f78b5f90979bee42ed43db": { + "balance": "0x65a4da25d3016c00000" + }, + "299368609042a858d1ecdf1fc0ada5eaceca29cf": { + "balance": "0x6c6b935b8bbd400000" + }, + "299e0bca55e069de8504e89aca6eca21d38a9a5d": { + "balance": "0x302379bf2ca2e0000" + }, + "29ac2b458454a36c7e96c73a8667222a12242c71": { + "balance": "0xd8d726b7177a800000" + }, + "29adcf83b6b20ac6a434abb1993cbd05c60ea2e4": { + "balance": "0x21e19e0c9bab2400000" + }, + "29aef48de8c9fbad4b9e4ca970797a5533eb722d": { + "balance": "0x21e19e0c9bab2400000" + }, + "29b3f561ee7a6e25941e98a5325b78adc79785f3": { + "balance": "0x56bc75e2d63100000" + }, + "29bdc4f28de0180f433c2694eb74f5504ce94337": { + "balance": "0x6c6b935b8bbd400000" + }, + "29cc804d922be91f5909f348b0aaa5d21b607830": { + "balance": "0xd8d726b7177a800000" + }, + "29da3e35b23bb1f72f8e2258cf7f553359d24bac": { + "balance": "0x43c33c1937564800000" + }, + "29e67990e1b6d52e1055ffe049c53195a81542cf": { + "balance": "0x43c33c1937564800000" + }, + "29eaae82761762f4d2db53a9c68b0f6b0b6d4e66": { + "balance": "0x6c6b935b8bbd400000" + }, + "29eb7eefdae9feb449c63ff5f279d67510eb1422": { + "balance": "0x10d3aa536e2940000" + }, + "29f0edc60338e7112085a1d114da8c42ce8f55d6": { + "balance": "0xa05a7f0fd825780000" + }, + "29f8fba4c30772b057edbbe62ae7420c390572e1": { + "balance": "0x3635c9adc5dea00000" + }, + "29f9286c0e738d1721a691c6b95ab3d9a797ede8": { + "balance": "0x2a5a058fc295ed000000" + }, + "2a085e25b64862f5e68d768e2b0f7a8529858eee": { + "balance": "0x6b883acd5766cd0000" + }, + "2a2ab6b74c7af1d9476bb5bcb4524797bedc3552": { + "balance": "0x3635c9adc5dea00000" + }, + "2a39190a4fde83dfb3ddcb4c5fbb83ac6c49755c": { + "balance": "0x3635c9adc5dea00000" + }, + "2a400dff8594de7228b4fd15c32322b75bb87da8": { + "balance": "0x531a17f607a2d0000" + }, + "2a44a7218fe44d65a1b4b7a7d9b1c2c52c8c3e34": { + "balance": "0xd2d06c305a1eb578000" + }, + "2a46d353777176ff8e83ffa8001f4f70f9733aa5": { + "balance": "0x5bf0ba6634f680000" + }, + "2a595f16eee4cb0c17d9a2d939b3c10f6c677243": { + "balance": "0x3ba1910bf341b00000" + }, + "2a59e47ea5d8f0e7c028a3e8e093a49c1b50b9a3": { + "balance": "0x6c6b935b8bbd400000" + }, + "2a5ba9e34cd58da54c9a2712663a3be274c8e47b": { + "balance": "0xaadec983fcff40000" + }, + "2a5e3a40d2cd0325766de73a3d671896b362c73b": { + "balance": "0x152d02c7e14af6800000" + }, + "2a63590efe9986c3fee09b0a0a338b15bed91f21": { + "balance": "0x15e1c4e05ee26d00000" + }, + "2a67660a1368efcd626ef36b2b1b601980941c05": { + "balance": "0x73f75d1a085ba0000" + }, + "2a742b8910941e0932830a1d9692cfd28494cf40": { + "balance": "0x1b1ab319f5ec750000" + }, + "2a746cd44027af3ebd37c378c85ef7f754ab5f28": { + "balance": "0x155bd9307f9fe80000" + }, + "2a81d27cb6d4770ff4f3c4a3ba18e5e57f07517c": { + "balance": "0x6c6b935b8bbd400000" + }, + "2a91a9fed41b7d0e5cd2d83158d3e8a41a9a2d71": { + "balance": "0x692ae8897081d00000" + }, + "2a9c57fe7b6b138a920d676f3c76b6c2a0eef699": { + "balance": "0x1fd933494aa5fe00000" + }, + "2a9c96c19151ffcbe29a4616d0c52b3933b4659f": { + "balance": "0x3c1379b8765e18000" + }, + "2aa192777ca5b978b6b2c2ff800ac1860f753f47": { + "balance": "0x12290f15180bdc0000" + }, + "2aaa35274d742546670b7426264521032af4f4c3": { + "balance": "0x21e19e0c9bab2400000" + }, + "2aaea1f1046f30f109faec1c63ef5c7594eb08da": { + "balance": "0xd8d726b7177a800000" + }, + "2ab97e8d59eee648ab6caf8696f89937143864d6": { + "balance": "0xcf152640c5c8300000" + }, + "2abce1808940cd4ef5b5e05285f82df7a9ab5e03": { + "balance": "0x21342520d5fec200000" + }, + "2abdf1a637ef6c42a7e2fe217773d677e804ebdd": { + "balance": "0x10f0cf064dd59200000" + }, + "2ac1f8d7bf721f3cfe74d20fea9b87a28aaa982c": { + "balance": "0x8ba52e6fc45e40000" + }, + "2acc9c1a32240b4d5b2f777a2ea052b42fc1271c": { + "balance": "0x8d807ee14d836100000" + }, + "2ad6c9d10c261819a1a0ca2c48d8c7b2a71728df": { + "balance": "0x3635c9adc5dea00000" + }, + "2ae53866fc2d14d572ab73b4a065a1188267f527": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "2ae73a79aea0278533accf21070922b1613f8f32": { + "balance": "0xa7e94bbeae701a8000" + }, + "2ae82dab92a66389eea1abb901d1d57f5a7cca0b": { + "balance": "0x6c6b935b8bbd400000" + }, + "2aec809df9325b9f483996e99f7331097f08aa0e": { + "balance": "0xd8d726b7177a800000" + }, + "2aed2ce531c056b0097efc3c6de10c4762004ed9": { + "balance": "0x2356953ab7ddc380000" + }, + "2afb058c3d31032b353bf24f09ae20d54de57dbe": { + "balance": "0x3ba1910bf341b00000" + }, + "2b0362633614bfcb583569438ecc4ea57b1d337e": { + "balance": "0x43c33c1937564800000" + }, + "2b101e822cd962962a06800a2c08d3b15d82b735": { + "balance": "0x83d6c7aab63600000" + }, + "2b129c26b75dde127f8320bd0f63410c92a9f876": { + "balance": "0x77432217e683600000" + }, + "2b241f037337eb4acc61849bd272ac133f7cdf4b": { + "balance": "0x500b6bca962ab8400000" + }, + "2b3a68db6b0cae8a7c7a476bdfcfbd6205e10687": { + "balance": "0x821ab0d44149800000" + }, + "2b3cf97311ff30f460945a9d8099f4a88e26d456": { + "balance": "0x6c6b935b8bbd400000" + }, + "2b49fba29830360fcdb6da23bbfea5c0bbac5281": { + "balance": "0x1158e460913d00000" + }, + "2b4f4507bb6b9817942ce433781b708fbcd166fd": { + "balance": "0xfc936392801c0000" + }, + "2b5016e2457387956562587115aa8759d8695fdf": { + "balance": "0x2a5a058fc295ed000000" + }, + "2b5c60e84535eeb4d580de127a12eb2677ccb392": { + "balance": "0x43c33c1937564800000" + }, + "2b5ced9987c0765f900e49cf9da2d9f9c1138855": { + "balance": "0x15af1d78b58c400000" + }, + "2b5f4b3f1e11707a227aa5e69fa49dded33fb321": { + "balance": "0x14542ba12a337c00000" + }, + "2b68306ba7f8daaf73f4c644ef7d2743c0f26856": { + "balance": "0x2ee182ca17ddd00000" + }, + "2b6ed29a95753c3ad948348e3e7b1a251080ffb9": { + "balance": "0x34f086f3b33b68400000" + }, + "2b701d16c0d3cc1e4cd85445e6ad02eea4ac012d": { + "balance": "0x2086ac351052600000" + }, + "2b717cd432a323a4659039848d3b87de26fc9546": { + "balance": "0x69e10de76676d0800000" + }, + "2b74c373d04bfb0fd60a18a01a88fbe84770e58c": { + "balance": "0x22b1c8c1227a00000" + }, + "2b77a4d88c0d56a3dbe3bae04a05f4fcd1b757e1": { + "balance": "0x1043561a8829300000" + }, + "2b8488bd2d3c197a3d26151815b5a798d27168dc": { + "balance": "0x16a1f9f5fd7d9600000" + }, + "2b8a0dee5cb0e1e97e15cfca6e19ad21f995efad": { + "balance": "0x1b55438d9a249b0000" + }, + "2b8fe4166e23d11963c0932b8ade8e0145ea0770": { + "balance": "0x92896529baddc880000" + }, + "2b99b42e4f42619ee36baa7e4af2d65eacfcba35": { + "balance": "0x878678326eac9000000" + }, + "2bab0fbe28d58420b52036770a12f9952aea6911": { + "balance": "0xcf152640c5c8300000" + }, + "2bade91d154517620fd4b439ac97157a4102a9f7": { + "balance": "0xd8d726b7177a800000" + }, + "2baf8d6e221174124820ee492b9459ec4fadafbb": { + "balance": "0x6c6b935b8bbd400000" + }, + "2bafbf9e9ed2c219f7f2791374e7d05cb06777e7": { + "balance": "0xbed1d0263d9f00000" + }, + "2bb366b9edcb0da680f0e10b3b6e28748190d6c3": { + "balance": "0x13a62d7b57640640000" + }, + "2bb6f578adfbe7b2a116b3554facf9969813c319": { + "balance": "0x19127a1391ea2a00000" + }, + "2bbe62eac80ca7f4d6fdee7e7d8e28b63acf770e": { + "balance": "0x81e32df972abf00000" + }, + "2bbe672a1857508f630f2a5edb563d9e9de92815": { + "balance": "0x6c6b935b8bbd400000" + }, + "2bc429d618a66a4cf82dbb2d824e9356effa126a": { + "balance": "0x6c6acc67d7b1d40000" + }, + "2bd252e0d732ff1d7c78f0a02e6cb25423cf1b1a": { + "balance": "0x90f534608a72880000" + }, + "2bdd03bebbee273b6ca1059b34999a5bbd61bb79": { + "balance": "0x1158e460913d00000" + }, + "2c04115c3e52961b0dc0b0bf31fba4546f5966fd": { + "balance": "0xad78ebc5ac6200000" + }, + "2c06dd922b61514aafedd84488c0c28e6dcf0e99": { + "balance": "0x152d02c7e14af6800000" + }, + "2c0cc3f951482cc8a2925815684eb9f94e060200": { + "balance": "0x14542ba12a337c00000" + }, + "2c0ee134d8b36145b47beee7af8d2738dbda08e8": { + "balance": "0xae56f730e6d840000" + }, + "2c0f5b9df43625798e7e03c1a5fd6a6d091af82b": { + "balance": "0x1b0fcaab200300000" + }, + "2c128c95d957215101f043dd8fc582456d41016d": { + "balance": "0x2d43f3ebfafb2c0000" + }, + "2c1800f35fa02d3eb6ff5b25285f5e4add13b38d": { + "balance": "0x3122d3adafde100000" + }, + "2c1c19114e3d6de27851484b8d2715e50f8a1065": { + "balance": "0x56bc75e2d63100000" + }, + "2c1cc6e18c152488ba11c2cc1bcefa2df306abd1": { + "balance": "0x5a87e7d7f5f6580000" + }, + "2c1df8a76f48f6b54bcf9caf56f0ee1cf57ab33d": { + "balance": "0x2247f750089da580000" + }, + "2c2147947ae33fb098b489a5c16bfff9abcd4e2a": { + "balance": "0xad78ebc5ac6200000" + }, + "2c234f505ca8dcc77d9b7e01d257c318cc19396d": { + "balance": "0x56bc75e2d63100000" + }, + "2c2428e4a66974edc822d5dbfb241b2728075158": { + "balance": "0x6c6b935b8bbd400000" + }, + "2c2d15ff39561c1b72eda1cc027ffef23743a144": { + "balance": "0xd480ed9ef32b400000" + }, + "2c2db28c3309375eea3c6d72cd6d0eec145afcc0": { + "balance": "0x6c6b935b8bbd400000" + }, + "2c424ee47f583cdce07ae318b6fad462381d4d2b": { + "balance": "0xd8d726b7177a800000" + }, + "2c4b470307a059854055d91ec3794d80b53d0f4a": { + "balance": "0x43c33c1937564800000" + }, + "2c52c984102ee0cd3e31821b84d408930efa1ac7": { + "balance": "0x6c6b935b8bbd400000" + }, + "2c5a2d0abda03bbe215781b4ff296c8c61bdbaf6": { + "balance": "0x1a8e56f48c0228000" + }, + "2c5b7d7b195a371bf9abddb42fe04f2f1d9a9910": { + "balance": "0xad78ebc5ac6200000" + }, + "2c5df866666a194b26cebb407e4a1fd73e208d5e": { + "balance": "0x3635c9adc5dea00000" + }, + "2c603ff0fe93616c43573ef279bfea40888d6ae7": { + "balance": "0x100f4b6d66757900000" + }, + "2c6846a1aa999a2246a287056000ba4dcba8e63d": { + "balance": "0x21f2f6f0fc3c6100000" + }, + "2c6afcd4037c1ed14fa74ff6758e0945a185a8e8": { + "balance": "0xf43fc2c04ee00000" + }, + "2c6b699d9ead349f067f45711a074a641db6a897": { + "balance": "0x1158e460913d00000" + }, + "2c6f5c124cc789f8bb398e3f889751bc4b602d9e": { + "balance": "0x159f20bed00f00000" + }, + "2c83aeb02fcf067d65a47082fd977833ab1cec91": { + "balance": "0x8273823258ac00000" + }, + "2c89f5fdca3d155409b638b98a742e55eb4652b7": { + "balance": "0x14dbb2195ca228900000" + }, + "2c964849b1f69cc7cea4442538ed87fdf16cfc8f": { + "balance": "0x6c6b935b8bbd400000" + }, + "2c9fa72c95f37d08e9a36009e7a4b07f29bad41a": { + "balance": "0xdf6eb0b2d3ca0000" + }, + "2caf6bf4ec7d5a19c5e0897a5eeb011dcece4210": { + "balance": "0x7934835a031160000" + }, + "2cb4c3c16bb1c55e7c6b7a19b127a1ac9390cc09": { + "balance": "0xb82794a9244f0c8000" + }, + "2cb5495a505336c2465410d1cae095b8e1ba5cdd": { + "balance": "0x43c33c1937564800000" + }, + "2cb615073a40dcdb99faa848572e987b3b056efb": { + "balance": "0x2b58addb89a2580000" + }, + "2cba6d5d0dc204ea8a25ada2e26f5675bd5f2fdc": { + "balance": "0x4823ef7ddb9af38000" + }, + "2cbb0c73df91b91740b6693b774a7d05177e8e58": { + "balance": "0x6449e84e47a8a80000" + }, + "2ccb66494d0af689abf9483d365d782444e7dead": { + "balance": "0x3635c9adc5dea00000" + }, + "2ccc1f1cb5f4a8002e186b20885d9dbc030c0894": { + "balance": "0x6c6b935b8bbd400000" + }, + "2ccf80e21898125eb4e807cd82e09b9d28592f6e": { + "balance": "0x6c6b935b8bbd400000" + }, + "2cd19694d1926a0fa9189edebafc671cf1b2caa5": { + "balance": "0x3635c9adc5dea00000" + }, + "2cd39334ac7eac797257abe3736195f5b4b5ce0f": { + "balance": "0x56b47785e37260000" + }, + "2cd79eb52027b12c18828e3eaab2969bfcd287e9": { + "balance": "0x1158e460913d00000" + }, + "2cd87866568dd81ad47d9d3ad0846e5a65507373": { + "balance": "0x15af1d78b58c400000" + }, + "2cdb3944650616e47cb182e060322fa1487978ce": { + "balance": "0x62a992e53a0af00000" + }, + "2ce11a92fad024ff2b3e87e3b542e6c60dcbd996": { + "balance": "0xd8d726b7177a800000" + }, + "2d0326b23f0409c0c0e9236863a133075a94ba18": { + "balance": "0xb679be75be6ae0000" + }, + "2d0dec51a6e87330a6a8fa2a0f65d88d4abcdf73": { + "balance": "0xa076407d3f7440000" + }, + "2d23766b6f6b05737dad80a419c40eda4d77103e": { + "balance": "0xcf152640c5c8300000" + }, + "2d2b032359b363964fc11a518263bfd05431e867": { + "balance": "0x81c1df7629e700000" + }, + "2d3480bf0865074a72c7759ee5137b4d70c51ce9": { + "balance": "0xad78ebc5ac6200000" + }, + "2d35a9df62757f7ffad1049afb06ca4afc464c51": { + "balance": "0x1158e460913d00000" + }, + "2d40558b06f90a3923145592123b6774e46e31f4": { + "balance": "0x3635c9adc5dea00000" + }, + "2d426912d059fad9740b2e390a2eeac0546ff01b": { + "balance": "0x4be4e7267b6ae00000" + }, + "2d532df4c63911d1ce91f6d1fcbff7960f78a885": { + "balance": "0x5a85968a5878da8000" + }, + "2d5391e938b34858cf965b840531d5efda410b09": { + "balance": "0x4be4e7267b6ae00000" + }, + "2d5b42fc59ebda0dfd66ae914bc28c1b0a6ef83a": { + "balance": "0x2bc8b59fdcd836638000" + }, + "2d5d7335acb0362b47dfa3a8a4d3f5949544d380": { + "balance": "0xad78ebc5ac6200000" + }, + "2d61bfc56873923c2b00095dc3eaa0f590d8ae0f": { + "balance": "0x46566dff8ce55600000" + }, + "2d6511fd7a3800b26854c7ec39c0dcb5f4c4e8e8": { + "balance": "0x15adddba2f9e770000" + }, + "2d7d5c40ddafc450b04a74a4dabc2bb5d665002e": { + "balance": "0x6c6b935b8bbd400000" + }, + "2d89a8006a4f137a20dc2bec46fe2eb312ea9654": { + "balance": "0xad78ebc5ac6200000" + }, + "2d8c52329f38d2a2fa9cbaf5c583daf1490bb11c": { + "balance": "0x1158e460913d00000" + }, + "2d8e061892a5dcce21966ae1bb0788fd3e8ba059": { + "balance": "0xd8e5ce617f2d50000" + }, + "2d8e5bb8d3521695c77e7c834e0291bfacee7408": { + "balance": "0x6acb3df27e1f880000" + }, + "2d90b415a38e2e19cdd02ff3ad81a97af7cbf672": { + "balance": "0x5f3c7f64131e40000" + }, + "2d9bad6f1ee02a70f1f13def5cccb27a9a274031": { + "balance": "0x61093d7c2c6d380000" + }, + "2d9c5fecd2b44fbb6a1ec732ea059f4f1f9d2b5c": { + "balance": "0x36ca32661d1aa70000" + }, + "2da617695009cc57d26ad490b32a5dfbeb934e5e": { + "balance": "0x43c33c1937564800000" + }, + "2da76b7c39b420e388ba2c1020b0856b0270648a": { + "balance": "0x6c6b935b8bbd400000" + }, + "2dc79d6e7f55bce2e2d0c02ad07ceca8bb529354": { + "balance": "0x55a6e79ccd1d300000" + }, + "2dca0e449ab646dbdfd393a96662960bcab5ae1e": { + "balance": "0x878678326eac9000000" + }, + "2dd325fdffb97b19995284afa5abdb574a1df16a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "2dd578f7407dfbd548d05e95ccc39c485429626a": { + "balance": "0xe3aeb5737240a00000" + }, + "2dd8eeef87194abc2ce7585da1e35b7cea780cb7": { + "balance": "0x3635c6204739d98000" + }, + "2ddf40905769bcc426cb2c2938ffe077e1e89d98": { + "balance": "0xa2a15d09519be00000" + }, + "2de0964400c282bdd78a919c6bf77c6b5f796179": { + "balance": "0xad78ebc5ac6200000" + }, + "2de31afd189a13a76ff6fe73ead9f74bb5c4a629": { + "balance": "0x14542ba12a337c00000" + }, + "2dec98329d1f96c3a59caa7981755452d4da49d5": { + "balance": "0xad78ebc5ac6200000" + }, + "2dee90a28f192d676a8773232b56f18f239e2fad": { + "balance": "0x3efa7e747b6d1ad0000" + }, + "2e0880a34596230720f05ac8f065af8681dcb6c2": { + "balance": "0x152d02c7e14af6800000" + }, + "2e0c57b47150f95aa6a7e16ab9b1cbf54328979a": { + "balance": "0x56bc75e2d63100000" + }, + "2e10910ba6e0bc17e055556614cb87090f4d7e5b": { + "balance": "0xad78ebc5ac6200000" + }, + "2e24b597873bb141bdb237ea8a5ab747799af02d": { + "balance": "0x43c33c1937564800000" + }, + "2e2810dee44ae4dff3d86342ab126657d653c336": { + "balance": "0xad78ebc5ac6200000" + }, + "2e2cbd7ad82547b4f5ff8b3ab56f942a6445a3b0": { + "balance": "0xad78ebc5ac6200000" + }, + "2e2d7ea66b9f47d8cc52c01c52b6e191bc7d4786": { + "balance": "0xd8d4602c26bf6c0000" + }, + "2e439348df8a4277b22a768457d1158e97c40904": { + "balance": "0x2a1e9ff26fbf410000" + }, + "2e46fcee6a3bb145b594a243a3913fce5dad6fba": { + "balance": "0x21e19e0c9bab2400000" + }, + "2e47f287f498233713850d3126823cc67dcee255": { + "balance": "0xca9d9ea558b40000" + }, + "2e4ee1ae996aa0a1d92428d06652a6bea6d2d15d": { + "balance": "0x6c6b935b8bbd400000" + }, + "2e52912bc10ea39d54e293f7aed6b99a0f4c73be": { + "balance": "0x15af1d78b58c400000" + }, + "2e619f57abc1e987aa936ae3a2264962e7eb2d9a": { + "balance": "0x28fb9b8a8a53500000" + }, + "2e64a8d71111a22f4c5de1e039b336f68d398a7c": { + "balance": "0x6c6b935b8bbd400000" + }, + "2e6933543d4f2cc00b5350bd8068ba9243d6beb0": { + "balance": "0x6c6b935b8bbd400000" + }, + "2e7e05e29edda7e4ae25c5173543efd71f6d3d80": { + "balance": "0x14542ba12a337c00000" + }, + "2e7f465520ec35cc23d68e75651bb6689544a196": { + "balance": "0x38ec5b721a1a268000" + }, + "2e8eb30a716e5fe15c74233e039bfb1106e81d12": { + "balance": "0x56bc75e2d63100000" + }, + "2e9824b5c132111bca24ddfba7e575a5cd7296c1": { + "balance": "0x3a484516e6d7ffe0000" + }, + "2ea5fee63f337a376e4b918ea82148f94d48a626": { + "balance": "0x650f8e0dd293c50000" + }, + "2eaf4e2a46b789ccc288c8d1d9294e3fb0853896": { + "balance": "0x6c6b935b8bbd400000" + }, + "2eaff9f8f8113064d3957ac6d6e11eee42c8195d": { + "balance": "0x6acb3df27e1f880000" + }, + "2eba0c6ee5a1145c1c573984963a605d880a7a20": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "2ec95822eb887bc113b4712a4dfd7f13b097b5e7": { + "balance": "0x3635c9adc5dea00000" + }, + "2eca6a3c5d9f449d0956bd43fa7b4d7be8435958": { + "balance": "0x6c6bda69709cc20000" + }, + "2ecac504b233866eb5a4a99e7bd2901359e43b3d": { + "balance": "0x43c33c1937564800000" + }, + "2eebf59432b52892f9380bd140aa99dcf8ad0c0f": { + "balance": "0x83d6c7aab63600000" + }, + "2eeed50471a1a2bf53ee30b1232e6e9d80ef866d": { + "balance": "0x1158e460913d00000" + }, + "2eef6b1417d7b10ecfc19b123a8a89e73e526c58": { + "balance": "0x2086ac351052600000" + }, + "2ef869f0350b57d53478d701e3fee529bc911c75": { + "balance": "0x2b5e3af16b1880000" + }, + "2ef9e465716acacfb8c8252fa8e7bc7969ebf6e4": { + "balance": "0x959eb1c0e4ae200000" + }, + "2efc4c647dac6acac35577ad221758fef6616faa": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "2f13657526b177cad547c3908c840eff647b45d9": { + "balance": "0x3f76849cf1ee2c8000" + }, + "2f187d5a704d5a338c5b2876a090dce964284e29": { + "balance": "0xd8d726b7177a800000" + }, + "2f2523cc834f0086052402626296675186a8e582": { + "balance": "0x3635c9adc5dea000000" + }, + "2f282abbb6d4a3c3cd3b5ca812f7643e80305f06": { + "balance": "0x6449e84e47a8a80000" + }, + "2f2bba1b1796821a766fce64b84f28ec68f15aea": { + "balance": "0x1158e460913d00000" + }, + "2f315d9016e8ee5f536681202f9084b032544d4d": { + "balance": "0x383cd12b9e863c0000" + }, + "2f4da753430fc09e73acbccdcde9da647f2b5d37": { + "balance": "0xad78ebc5ac6200000" + }, + "2f5080b83f7e2dc0a1dd11b092ad042bff788f4c": { + "balance": "0xb4f8fb79231d2b8000" + }, + "2f61efa5819d705f2b1e4ee754aeb8a819506a75": { + "balance": "0x4f2591f896a6500000" + }, + "2f66bfbf2262efcc8d2bd0444fc5b0696298ff1e": { + "balance": "0x21ad935f79f76d00000" + }, + "2f6dce1330c59ef921602154572d4d4bacbd048a": { + "balance": "0x3635c9adc5dea00000" + }, + "2f7d3290851be5c6b4b43f7d4574329f61a792c3": { + "balance": "0x56bc75e2d63100000" + }, + "2f853817afd3b8f3b86e9f60ee77b5d97773c0e3": { + "balance": "0x4eaeea44e368b90000" + }, + "2fa491fb5920a6574ebd289f39c1b2430d2d9a6a": { + "balance": "0x6c6b935b8bbd400000" + }, + "2fb566c94bbba4e3cb67cdda7d5fad7131539102": { + "balance": "0x6c6b935b8bbd400000" + }, + "2fbb504a5dc527d3e3eb0085e2fc3c7dd538cb7a": { + "balance": "0x43c2b18aec3c0a8000" + }, + "2fbc85798a583598b522166d6e9dda121d627dbc": { + "balance": "0xad78ebc5ac6200000" + }, + "2fbcef3384d420e4bf61a0669990bc7054f1a5af": { + "balance": "0x6c6b935b8bbd400000" + }, + "2fc82ef076932341264f617a0c80dd571e6ae939": { + "balance": "0x18424f5f0b1b4e00000" + }, + "2fdd9b79df8df530ad63c20e62af431ae99216b8": { + "balance": "0x1236efcbcbb340000" + }, + "2fe0023f5722650f3a8ac01009125e74e3f82e9b": { + "balance": "0xa2a15d09519be00000" + }, + "2fe0cc424b53a31f0916be08ec81c50bf8eab0c1": { + "balance": "0x2086ac351052600000" + }, + "2fe13a8d0785de8758a5e41876c36e916cf75074": { + "balance": "0xd8d726b7177a800000" + }, + "2fea1b2f834f02fc54333f8a809f0438e5870aa9": { + "balance": "0x11854d0f9cee40000" + }, + "2fee36a49ee50ecf716f1047915646779f8ba03f": { + "balance": "0x394222c4da86d70000" + }, + "2fef81478a4b2e8098db5ff387ba2153f4e22b79": { + "balance": "0x3627e8f712373c0000" + }, + "2ff160c44f72a299b5ec2d71e28ce5446d2fcbaf": { + "balance": "0x138400eca364a00000" + }, + "2ff1ca55fd9cec1b1fe9f0a9abb74c513c1e2aaa": { + "balance": "0xa2a15d09519be00000" + }, + "2ff5cab12c0d957fd333f382eeb75107a64cb8e8": { + "balance": "0x21e19e0c9bab2400000" + }, + "2ff830cf55fb00d5a0e03514fecd44314bd6d9f1": { + "balance": "0x21e19e0c9bab2400000" + }, + "2ffe93ec1a5636e9ee34af70dff52682e6ff7079": { + "balance": "0x6c6b935b8bbd400000" + }, + "30037988702671acbe892c03fe5788aa98af287a": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "30248d58e414b20fed3a6c482b59d9d8f5a4b7e2": { + "balance": "0x340aad21b3b700000" + }, + "303139bc596403d5d3931f774c66c4ba467454db": { + "balance": "0x5c25e14aea283f0000" + }, + "30380087786965149e81423b15e313ba32c5c783": { + "balance": "0xfc936392801c0000" + }, + "303a30ac4286ae17cf483dad7b870c6bd64d7b4a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "303fbaebbe46b35b6e5b74946a5f99bc1585cae7": { + "balance": "0x2f9ac0695f5bba0000" + }, + "3041445a33ba158741160d9c344eb88e5c306f94": { + "balance": "0x340aad21b3b700000" + }, + "30480164bcd84974ebc0d90c9b9afab626cd1c73": { + "balance": "0x2b5e3af16b18800000" + }, + "304ec69a74545721d7316aef4dcfb41ac59ee2f0": { + "balance": "0xad78ebc5ac6200000" + }, + "30511832918d8034a7bee72ef2bfee440ecbbcf6": { + "balance": "0x368c8623a8b4d100000" + }, + "30513fca9f36fd788cfea7a340e86df98294a244": { + "balance": "0x183b5f03b1479c0000" + }, + "3055efd26029e0d11b930df4f53b162c8c3fd2ce": { + "balance": "0x1b1a089237073d0000" + }, + "305d26c10bdc103f6b9c21272eb7cb2d9108c47e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "305f78d618b990b4295bac8a2dfa262884f804ea": { + "balance": "0xd8d726b7177a800000" + }, + "3064899a963c4779cbf613cd6980846af1e6ec65": { + "balance": "0x17b773ce6e5df0a0000" + }, + "30730466b8eb6dc90d5496aa76a3472d7dbe0bbe": { + "balance": "0x6c68ccd09b022c0000" + }, + "30742ccdf4abbcd005681f8159345c9e79054b1a": { + "balance": "0x243d4d18229ca20000" + }, + "3083ef0ed4c4401196774a95cf4edc83edc1484f": { + "balance": "0x23ffb7ed6565d6400000" + }, + "308dd21cebe755126704b48c0f0dc234c60ba9b1": { + "balance": "0xad78ebc5ac6200000" + }, + "3090f8130ec44466afadb36ed3c926133963677b": { + "balance": "0xd8d726b7177a800000" + }, + "309544b6232c3dd737f945a03193d19b5f3f65b9": { + "balance": "0x3af342f67ef6c80000" + }, + "3096dca34108085bcf04ae72b94574a13e1a3e1d": { + "balance": "0xad78ebc5ac6200000" + }, + "3098b65db93ecacaf7353c48808390a223d57684": { + "balance": "0x186484cf7bb6a48000" + }, + "30a9da72574c51e7ee0904ba1f73a6b7b83b9b9d": { + "balance": "0x11854d0f9cee40000" + }, + "30acd858875fa24eef0d572fc7d62aad0ebddc35": { + "balance": "0x15af1d78b58c400000" + }, + "30b66150f1a63457023fdd45d0cc6cb54e0c0f06": { + "balance": "0x3635c9adc5dea00000" + }, + "30bb4357cd6910c86d2238bf727cbe8156680e62": { + "balance": "0x56bf91b1a65eb0000" + }, + "30bf61b2d877fe10635126326fa189e4b0b1c3b0": { + "balance": "0x37b48985a5d7e60000" + }, + "30c01142907acb1565f70438b9980ae731818738": { + "balance": "0x6c6b935b8bbd400000" + }, + "30c26a8e971baa1855d633ba703f028cc7873140": { + "balance": "0x21e19e0c9bab2400000" + }, + "30db6b9b107e62102f434a9dd0960c2021f5ce4c": { + "balance": "0x2083179b6e42530000" + }, + "30e33358fc21c85006e40f32357dc8895940aaf0": { + "balance": "0x678a932062e4180000" + }, + "30e60900cacc7203f314dc604347255167fc2a0f": { + "balance": "0x6c6b935b8bbd400000" + }, + "30e789b3d2465e946e6210fa5b35de4e8c93085f": { + "balance": "0x6c6b935b8bbd400000" + }, + "30e9698cf1e08a9d048bd8d8048f28be7ed9409f": { + "balance": "0x16a6502f15a1e540000" + }, + "30e9d5a0088f1ddb2fd380e2a049192266c51cbf": { + "balance": "0xaacacd9b9e22b0000" + }, + "30eac740e4f02cb56eef0526e5d300322600d03e": { + "balance": "0x6acb3df27e1f880000" + }, + "30ec9392244a2108c987bc5cdde0ed9f837a817b": { + "balance": "0x549925f6c9c5250000" + }, + "30ed11b77bc17e5e6694c8bc5b6e4798f68d9ca7": { + "balance": "0x1e6fb3421fe0299e0000" + }, + "30f7d025d16f7bee105580486f9f561c7bae3fef": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "30fbe5885f9fcce9ea5edb82ed4a1196dd259aed": { + "balance": "0x119e47f21381f400000" + }, + "31047d703f63b93424fbbd6e2f1f9e74de13e709": { + "balance": "0x9a8166f7e6b2a78000" + }, + "31313ffd635bf2f3324841a88c07ed146144ceeb": { + "balance": "0x6acb3df27e1f880000" + }, + "3159e90c48a915904adfe292b22fa5fd5e72796b": { + "balance": "0x36afe98f2606100000" + }, + "315db7439fa1d5b423afa7dd7198c1cf74c918bc": { + "balance": "0x2086ac351052600000" + }, + "315ef2da620fd330d12ee55de5f329a696e0a968": { + "balance": "0x821ab0d4414980000" + }, + "316e92a91bbda68b9e2f98b3c048934e3cc0b416": { + "balance": "0x6c6b935b8bbd400000" + }, + "316eb4e47df71b42e16d6fe46825b7327baf3124": { + "balance": "0xd8d726b7177a800000" + }, + "3171877e9d820cc618fc0919b29efd333fda4934": { + "balance": "0x3635c9adc5dea00000" + }, + "317cf4a23cb191cdc56312c29d15e210b3b9b784": { + "balance": "0x7ce66c50e28400000" + }, + "318b2ea5f0aaa879c4d5e548ac9d92a0c67487b7": { + "balance": "0xad78ebc5ac6200000" + }, + "318c76ecfd8af68d70555352e1f601e35988042d": { + "balance": "0x1b31192e68c7f00000" + }, + "318f1f8bd220b0558b95fb33100ffdbb640d7ca6": { + "balance": "0xd8d726b7177a800000" + }, + "31aa3b1ebe8c4dbcb6a708b1d74831e60e497660": { + "balance": "0x15af1d78b58c400000" + }, + "31ab088966ecc7229258f6098fce68cf39b38485": { + "balance": "0x3635c9adc5dea00000" + }, + "31ad4d9946ef09d8e988d946b1227f9141901736": { + "balance": "0x4d853c8f89089800000" + }, + "31b43b015d0081643c6cda46a7073a6dfdbca825": { + "balance": "0xa97916520cd18e80000" + }, + "31ccc616b3118268e75d9ab8996c8858ebd7f3c3": { + "balance": "0x15ae0f771ca1520000" + }, + "31d81d526c195e3f10b5c6db52b5e59afbe0a995": { + "balance": "0xe4fbc69449f200000" + }, + "31e9c00f0c206a4e4e7e0522170dc81e88f3eb70": { + "balance": "0x918ddc3a42a3d40000" + }, + "31ea12d49a35a740780ddeeaece84c0835b26270": { + "balance": "0xad78ebc5ac6200000" + }, + "31ea6eab19d00764e9a95e183f2b1b22fc7dc40f": { + "balance": "0x1158e460913d00000" + }, + "31eb123c95c82bf685ace7a75a1881a289efca10": { + "balance": "0x31e009607371bd0000" + }, + "31ed858788bda4d5270992221cc04206ec62610d": { + "balance": "0x3fc0474948f3600000" + }, + "31f006f3494ed6c16eb92aaf9044fa8abb5fd5a3": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "3201259caf734ad7581c561051ba0bca7fd6946b": { + "balance": "0x261dd1ce2f2088800000" + }, + "32034e8581d9484e8af42a28df190132ec29c466": { + "balance": "0xbb9125542263900000" + }, + "322021022678a0166d204b3aaa7ad4ec4b88b7d0": { + "balance": "0x15af1d78b58c400000" + }, + "3225c1ca5f2a9c88156bb7d9cdc44a326653c214": { + "balance": "0x15af1d78b58c400000" + }, + "322788b5e29bf4f5f55ae1ddb32085fda91b8ebe": { + "balance": "0xad78ebc5ac6200000" + }, + "322d6f9a140d213f4c80cd051afe25c620bf4c7d": { + "balance": "0x1158e460913d00000" + }, + "322e5c43b0f524389655a9b3ff24f2d4db3da10f": { + "balance": "0xfc13b69b3e7e680000" + }, + "323486ca64b375474fb2b759a9e7a135859bd9f6": { + "balance": "0x15af1d78b58c400000" + }, + "323749a3b971959e46c8b4822dcafaf7aaf9bd6e": { + "balance": "0x11671a5b245700000" + }, + "323aad41df4b6fc8fece8c93958aa901fa680843": { + "balance": "0x34957444b840e80000" + }, + "323b3cfe3ee62bbde2a261e53cb3ecc05810f2c6": { + "balance": "0x2eb8eb1a172dcb80000" + }, + "323fca5ed77f699f9d9930f5ceeff8e56f59f03c": { + "balance": "0x487a9a304539440000" + }, + "32485c818728c197fea487fbb6e829159eba8370": { + "balance": "0x3921b413bc4ec08000" + }, + "3250e3e858c26adeccadf36a5663c22aa84c4170": { + "balance": "0x10f0cf064dd59200000" + }, + "3259bd2fddfbbc6fbad3b6e874f0bbc02cda18b5": { + "balance": "0x2846056495b0d188000" + }, + "3275496fd4dd8931fd69fb0a0b04c4d1ff879ef5": { + "balance": "0x182d7e4cfda0380000" + }, + "327bb49e754f6fb4f733c6e06f3989b4f65d4bee": { + "balance": "0x1158e460913d00000" + }, + "3282791d6fd713f1e94f4bfd565eaa78b3a0599d": { + "balance": "0x487a9a304539440000" + }, + "3283eb7f9137dd39bed55ffe6b8dc845f3e1a079": { + "balance": "0x3970ae92155780000" + }, + "32860997d730b2d83b73241a25d3667d51c908ef": { + "balance": "0x1b1a089237073d0000" + }, + "3286d1bc657a312c8847d93cb3cb7950f2b0c6e3": { + "balance": "0x43c33c1937564800000" + }, + "32a20d028e2c6218b9d95b445c771524636a22ef": { + "balance": "0x202fefbf2d7c2f00000" + }, + "32a70691255c9fc9791a4f75c8b81f388e0a2503": { + "balance": "0x35659ef93f0fc40000" + }, + "32b7feebc5c59bf65e861c4c0be42a7611a5541a": { + "balance": "0x77e9aaa8525c100000" + }, + "32ba9a7d0423e03a525fe2ebeb661d2085778bd8": { + "balance": "0x43c33c1937564800000" + }, + "32bb2e9693e4e085344d2f0dbd46a283e3a087fd": { + "balance": "0x15af1d78b58c400000" + }, + "32c2fde2b6aabb80e5aea2b949a217f3cb092283": { + "balance": "0x1306160afdf20378000" + }, + "32d950d5e93ea1d5b48db4714f867b0320b31c0f": { + "balance": "0x3708baed3d68900000" + }, + "32dbb6716c54e83165829a4abb36757849b6e47d": { + "balance": "0x3635c9adc5dea00000" + }, + "32eb64be1b5dede408c6bdefbe6e405c16b7ed02": { + "balance": "0x6acb3df27e1f880000" + }, + "32ef5cdc671df5562a901aee5db716b9be76dcf6": { + "balance": "0x6c6b935b8bbd400000" + }, + "32f29e8727a74c6b4301e3ffff0687c1b870dae9": { + "balance": "0x3635c9adc5dea00000" + }, + "32fa0e86cd087dd68d693190f32d93310909ed53": { + "balance": "0xd8d726b7177a800000" + }, + "32fbeed6f626fcdfd51acafb730b9eeff612f564": { + "balance": "0x6c6b935b8bbd400000" + }, + "3300fb149aded65bcba6c04e9cd6b7a03b893bb1": { + "balance": "0xfc936392801c0000" + }, + "3301d9ca2f3bfe026279cd6819f79a293d98156e": { + "balance": "0xa968163f0a57b400000" + }, + "3308b03466c27a17dfe1aafceb81e16d2934566f": { + "balance": "0x39992648a23c8a00000" + }, + "331a1c26cc6994cdd3c14bece276ffff4b9df77c": { + "balance": "0xfa7aeddf4f068000" + }, + "3326b88de806184454c40b27f309d9dd6dcfb978": { + "balance": "0x3ca5c66d9bc44300000" + }, + "3329eb3baf4345d600ced40e6e9975656f113742": { + "balance": "0x10f08eda8e555098000" + }, + "33320dd90f2baa110dd334872a998f148426453c": { + "balance": "0x36356633ebd8ea0000" + }, + "3336c3ef6e8b50ee90e037b164b7a8ea5faac65d": { + "balance": "0xec8a3a71c22540000" + }, + "33380c6fff5acd2651309629db9a71bf3f20c5ba": { + "balance": "0x368c8623a8b4d100000" + }, + "333ad1596401e05aea2d36ca47318ef4cd2cb3df": { + "balance": "0x9dc05cce28c2b80000" + }, + "334340ee4b9cdc81f850a75116d50ee9b69825bf": { + "balance": "0x6c6b935b8bbd400000" + }, + "33481e856ebed48ea708a27426ef28e867f57cd1": { + "balance": "0xad78ebc5ac6200000" + }, + "33565ba9da2c03e778ce12294f081dfe81064d24": { + "balance": "0x3635c9adc5dea000000" + }, + "33581cee233088c0860d944e0cf1ceabb8261c2e": { + "balance": "0xb98bc829a6f90000" + }, + "335858f749f169cabcfe52b796e3c11ec47ea3c2": { + "balance": "0xad78ebc5ac6200000" + }, + "335e22025b7a77c3a074c78b8e3dfe071341946e": { + "balance": "0x227ca730ab3f6ac0000" + }, + "33629bd52f0e107bc071176c64df108f64777d49": { + "balance": "0x1cfdd7468216e8000" + }, + "337b3bdf86d713dbd07b5dbfcc022b7a7b1946ae": { + "balance": "0xd7c198710e66b00000" + }, + "337cfe1157a5c6912010dd561533791769c2b6a6": { + "balance": "0x3635c9adc5dea00000" + }, + "33b336f5ba5edb7b1ccc7eb1a0d984c1231d0edc": { + "balance": "0x6c6b935b8bbd400000" + }, + "33c407133b84b3ca4c3ded1f4658900c38101624": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "33d172ab075c51db1cd40a8ca8dbff0d93b843bb": { + "balance": "0x136780510d12de38000" + }, + "33e9b71823952e1f66958c278fc28b1196a6c5a4": { + "balance": "0x56bc75e2d63100000" + }, + "33ea6b7855e05b07ab80dab1e14de9b649e99b6c": { + "balance": "0x1cd6fbad57dbd00000" + }, + "33f15223310d44de8b6636685f3a4c3d9c5655a5": { + "balance": "0xd9462c6cb4b5a0000" + }, + "33f4a6471eb1bca6a9f85b3b4872e10755c82be1": { + "balance": "0x6c6b935b8bbd400000" + }, + "33fb577a4d214fe010d32cca7c3eeda63f87ceef": { + "balance": "0x3635c9adc5dea00000" + }, + "33fd718f0b91b5cec88a5dc15eecf0ecefa4ef3d": { + "balance": "0x177224aa844c720000" + }, + "341480cc8cb476f8d01ff30812e7c70e05afaf5d": { + "balance": "0x6c6b935b8bbd400000" + }, + "34272d5e7574315dcae9abbd317bac90289d4765": { + "balance": "0x62a992e53a0af00000" + }, + "3430a16381f869f6ea5423915855e800883525a9": { + "balance": "0x3ca5c66d9bc44300000" + }, + "34318625818ec13f11835ae97353ce377d6f590a": { + "balance": "0x52663ccab1e1c00000" + }, + "34393c5d91b9de597203e75bac4309b5fa3d28c3": { + "balance": "0xa844a7424d9c80000" + }, + "3439998b247cb4bf8bc80a6d2b3527f1dfe9a6d2": { + "balance": "0x796e3ea3f8ab00000" + }, + "34437d1465640b136cb5841c3f934f9ba0b7097d": { + "balance": "0x960db77681e940000" + }, + "344a8db086faed4efc37131b3a22b0782dad7095": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "34664d220fa7f37958024a3332d684bcc6d4c8bd": { + "balance": "0x21e19e0c9bab2400000" + }, + "3466f67e39636c01f43b3a21a0e8529325c08624": { + "balance": "0x2db1167650acd80000" + }, + "3485361ee6bf06ef6508ccd23d94641f814d3e2f": { + "balance": "0x6c6b935b8bbd400000" + }, + "3485f621256433b98a4200dad857efe55937ec98": { + "balance": "0x6c6b935b8bbd400000" + }, + "34958a46d30e30b273ecc6e5d358a212e5307e8c": { + "balance": "0x6c6b935b8bbd400000" + }, + "3497dd66fd118071a78c2cb36e40b6651cc82598": { + "balance": "0x5f1016b5076d00000" + }, + "349a816b17ab3d27bbc0ae0051f6a070be1ff29d": { + "balance": "0x21e19e0c9bab2400000" + }, + "349d2c918fd09e2807318e66ce432909176bd50b": { + "balance": "0x3cb71f51fc55800000" + }, + "34a0431fff5ead927f3c69649616dc6e97945f6f": { + "balance": "0x15af1d78b58c400000" + }, + "34a85d6d243fb1dfb7d1d2d44f536e947a4cee9e": { + "balance": "0x43c33c1937564800000" + }, + "34a901a69f036bcf9f7843c0ba01b426e8c3dc2b": { + "balance": "0xd8d726b7177a800000" + }, + "34b454416e9fb4274e6addf853428a0198d62ee1": { + "balance": "0x161042779f1ffc0000" + }, + "34c8e5f1330fcb4b14ca75cb2580a4b93d204e36": { + "balance": "0x6c6b935b8bbd400000" + }, + "34e2849bea583ab0cc37975190f322b395055582": { + "balance": "0x1a5c5e857fdf2b20000" + }, + "34fa7792bad8bbd7ff64056214a33eb6600c1ea8": { + "balance": "0x2b5e3af16b1880000" + }, + "34ff26eb60a8d1a95a489fae136ee91d4e58084c": { + "balance": "0x2086ac351052600000" + }, + "34ff582952ff24458f7b13d51f0b4f987022c1fe": { + "balance": "0x9806de3da6e9780000" + }, + "35106ba94e8563d4b3cb3c5c692c10e604b7ced8": { + "balance": "0x6c6b935b8bbd400000" + }, + "35145f620397c69cb8e00962961f0f4886643989": { + "balance": "0x14542ba12a337c00000" + }, + "35147430c3106500e79fa2f502462e94703c23b1": { + "balance": "0x6c6acc67d7b1d40000" + }, + "351787843505f8e4eff46566cce6a59f4d1c5fe7": { + "balance": "0x1f5718987664b480000" + }, + "351f16e5e0735af56751b0e225b2421171394090": { + "balance": "0x2d4ca05e2b43ca80000" + }, + "3524a000234ebaaf0789a134a2a417383ce5282a": { + "balance": "0x1317955947d8e2c0000" + }, + "3526eece1a6bdc3ee7b400fe935b48463f31bed7": { + "balance": "0x477879b6d14300000" + }, + "352a785f4a921632504ce5d015f83c49aa838d6d": { + "balance": "0xe9e7e0fb35b7780000" + }, + "352d29a26e8a41818181746467f582e6e84012e0": { + "balance": "0x14542ba12a337c00000" + }, + "352e77c861696ef96ad54934f894aa8ea35151dd": { + "balance": "0x3635c9adc5dea00000" + }, + "352f25babf4a690673e35195efa8f79d05848aad": { + "balance": "0xe253c39be6e7dc00000" + }, + "3536453322c1466cb905af5c335ca8db74bff1e6": { + "balance": "0x183b5f03b1479c0000" + }, + "353dbec42f92b50f975129b93c4c997375f09073": { + "balance": "0x6c5db2a4d815dc0000" + }, + "3540c7bd7a8442d5bee21a2180a1c4edff1649e0": { + "balance": "0x432eac4c6f05b98000" + }, + "3549bd40bbbc2b30095cac8be2c07a0588e0aed6": { + "balance": "0x1158e460913d00000" + }, + "3552a496eba67f12be6eedab360cd13661dc7480": { + "balance": "0x1043561a8829300000" + }, + "3554947b7b947b0040da52ca180925c6d3b88ffe": { + "balance": "0x39fbae8d042dd0000" + }, + "355c0c39f5d5700b41d375b3f17851dcd52401f9": { + "balance": "0xd7b3b7ba5abf4c0000" + }, + "355ccfe0e77d557b971be1a558bc02df9eee0594": { + "balance": "0x5f5cb1afc865280000" + }, + "3571cf7ad304ecaee595792f4bbfa484418549d6": { + "balance": "0x13bcd0d892d9e160000" + }, + "3575c770668a9d179f1ef768c293f80166e2aa3d": { + "balance": "0x19b21248a3ef280000" + }, + "357a02c0a9dfe287de447fb67a70ec5b62366647": { + "balance": "0x1731790534df20000" + }, + "35855ec641ab9e081ed0c2a6dcd81354d0244a87": { + "balance": "0x4127abe993a7aa8000" + }, + "3588895ac9fbafec012092dc05c0c302d90740fa": { + "balance": "0xa2a15d09519be00000" + }, + "3599493ce65772cf93e98af1195ec0955dc98002": { + "balance": "0x5151590c67b3280000" + }, + "35a08081799173e001cc5bd46a02406dc95d1787": { + "balance": "0x21e19e0c9bab2400000" + }, + "35a549e8fd6c368d6dcca6d2e7d18e4db95f5284": { + "balance": "0x1b1a089237073d0000" + }, + "35a6885083c899dabbf530ed6c12f4dd3a204cf5": { + "balance": "0xad78ebc5ac6200000" + }, + "35aaa0465d1c260c420fa30e2629869fb6559207": { + "balance": "0x263781e0e087c80000" + }, + "35ac1d3ed7464fa3db14e7729213ceaa378c095e": { + "balance": "0x52663ccab1e1c00000" + }, + "35af040a0cc2337a76af288154c7561e1a233349": { + "balance": "0x3635c9adc5dea00000" + }, + "35b03ea4245736f57b85d2eb79628f036ddcd705": { + "balance": "0xd8d726b7177a800000" + }, + "35bd246865fab490ac087ac1f1d4f2c10d0cda03": { + "balance": "0x15af1d78b58c400000" + }, + "35bf6688522f35467a7f75302314c02ba176800e": { + "balance": "0x3af418202d954e00000" + }, + "35c8adc11125432b3b77acd64625fe58ebee9d66": { + "balance": "0x6c6b935b8bbd400000" + }, + "35d2970f49dcc81ea9ee707e9c8a0ab2a8bb7463": { + "balance": "0x4e1003b28d92800000" + }, + "35e096120deaa5c1ecb1645e2ccb8b4edbd9299a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "35ea2163a38cdf9a123f82a5ec00258dae0bc767": { + "balance": "0xd8d726b7177a800000" + }, + "35f1da127b83376f1b88c82a3359f67a5e67dd50": { + "balance": "0x678a932062e4180000" + }, + "35f2949cf78bc219bb4f01907cf3b4b3d3865482": { + "balance": "0xfb5c86c92e4340000" + }, + "35f5860149e4bbc04b8ac5b272be55ad1aca58e0": { + "balance": "0xad78ebc5ac6200000" + }, + "3602458da86f6d6a9d9eb03daf97fe5619d442fa": { + "balance": "0x6c6b935b8bbd400000" + }, + "3605372d93a9010988018f9f315d032ed1880fa1": { + "balance": "0x1b1bcf51896a7d0000" + }, + "3616d448985f5d32aefa8b93a993e094bd854986": { + "balance": "0xb227f63be813c0000" + }, + "3616fb46c81578c9c8eb4d3bf880451a88379d7d": { + "balance": "0xad78ebc5ac6200000" + }, + "361c75931696bc3d427d93e76c77fd13b241f6f4": { + "balance": "0x1dc5d8fc266dd60000" + }, + "361d9ed80b5bd27cf9f1226f26753258ee5f9b3f": { + "balance": "0xbf6914ba7d72c20000" + }, + "361f3ba9ed956b770f257d3672fe1ff9f7b0240c": { + "balance": "0x2086ac351052600000" + }, + "36227cdfa0fd3b9d7e6a744685f5be9aa366a7f0": { + "balance": "0xac2730ee9c6c18000" + }, + "362fbcb10662370a068fc2652602a2577937cce6": { + "balance": "0xad78ebc5ac6200000" + }, + "3630c5e565ceaa8a0f0ffe32875eae2a6ce63c19": { + "balance": "0x937722b3774d00000" + }, + "36339f84a5c2b44ce53dfdb6d4f97df78212a7df": { + "balance": "0x116f18b81715a00000" + }, + "36343aeca07b6ed58a0e62fa4ecb498a124fc971": { + "balance": "0x1043561a8829300000" + }, + "366175403481e0ab15bb514615cbb989ebc68f82": { + "balance": "0x6c6b935b8bbd400000" + }, + "36726f3b885a24f92996da81625ec8ad16d8cbe6": { + "balance": "0x53af75d18148578000" + }, + "3673954399f6dfbe671818259bb278e2e92ee315": { + "balance": "0x2a5a058fc295ed000000" + }, + "36758e049cd98bcea12277a676f9297362890023": { + "balance": "0xd8d726b7177a800000" + }, + "367f59cc82795329384e41e1283115e791f26a01": { + "balance": "0x6c6b935b8bbd400000" + }, + "36810ff9d213a271eda2b8aa798be654fa4bbe06": { + "balance": "0x6c6b935b8bbd400000" + }, + "368c5414b56b8455171fbf076220c1cba4b5ca31": { + "balance": "0x1e3ef911e83d720000" + }, + "3690246ba3c80679e22eac4412a1aefce6d7cd82": { + "balance": "0x43c33c1937564800000" + }, + "36928b55bc861509d51c8cf1d546bfec6e3e90af": { + "balance": "0x6acb3df27e1f880000" + }, + "369822f5578b40dd1f4471706b22cd971352da6b": { + "balance": "0x12c1b6eed03d280000" + }, + "369ef761195f3a373e24ece6cd22520fe0b9e86e": { + "balance": "0x1cffafc94db2088000" + }, + "36a08fd6fd1ac17ce15ed57eefb12a2be28188bf": { + "balance": "0x487a9a304539440000" + }, + "36a0e61e1be47fa87e30d32888ee0330901ca991": { + "balance": "0x1158e460913d00000" + }, + "36b2c85e3aeeebb70d63c4a4730ce2e8e88a3624": { + "balance": "0x21e19e0c9bab2400000" + }, + "36bf43ff35df90908824336c9b31ce33067e2f50": { + "balance": "0x49721510c1c1e9480000" + }, + "36bfe1fa3b7b70c172eb042f6819a8972595413e": { + "balance": "0x3635c9adc5dea00000" + }, + "36c510bf8d6e569bf2f37d47265dbcb502ff2bce": { + "balance": "0x65a4da25d3016c00000" + }, + "36d85dc3683156e63bf880a9fab7788cf8143a27": { + "balance": "0x43c33c1937564800000" + }, + "36df8f883c1273ec8a171f7a33cfd649b1fe6075": { + "balance": "0xc52484ac416890000" + }, + "36e156610cd8ff64e780d89d0054385ca76755aa": { + "balance": "0x2f6f10780d22cc00000" + }, + "36fec62c2c425e219b18448ad757009d8c54026f": { + "balance": "0x15af1d78b58c400000" + }, + "3700e3027424d939dbde5d42fb78f6c4dbec1a8f": { + "balance": "0x22b1c8c1227a00000" + }, + "3702e704cc21617439ad4ea27a5714f2fda1e932": { + "balance": "0x3635c9adc5dea00000" + }, + "3703350c4d6fe337342cddc65bf1e2386bf3f9b2": { + "balance": "0x6d8121a194d1100000" + }, + "3708e59de6b4055088782902e0579c7201a8bf50": { + "balance": "0x2a5a058fc295ed000000" + }, + "3712367e5e55a96d5a19168f6eb2bc7e9971f869": { + "balance": "0x3635c9adc5dea00000" + }, + "37195a635dcc62f56a718049d47e8f9f96832891": { + "balance": "0x6acb3df27e1f880000" + }, + "3727341f26c12001e378405ee38b2d8464ec7140": { + "balance": "0x6c6b935b8bbd400000" + }, + "372e453a6b629f27678cc8aeb5e57ce85ec0aef9": { + "balance": "0xad78ebc5ac6200000" + }, + "3734cb187491ede713ae5b3b2d12284af46b8101": { + "balance": "0xa2a15d09519be00000" + }, + "3737216ee91f177732fb58fa4097267207e2cf55": { + "balance": "0x52663ccab1e1c00000" + }, + "373c547e0cb5ce632e1c5ad66155720c01c40995": { + "balance": "0xfe54dcdce6c55a0000" + }, + "376cd7577383e902951b60a2017ba7ea29e33576": { + "balance": "0x6c6b935b8bbd400000" + }, + "378ea1dc8edc19bae82638029ea8752ce98bcfcd": { + "balance": "0x6c6b935b8bbd400000" + }, + "378f37243f3ff0bef5e1dc85eb4308d9340c29f9": { + "balance": "0x6c6e59e67c78540000" + }, + "37959c20b7e9931d72f5a8ae869dafddad3b6d5c": { + "balance": "0xad78ebc5ac6200000" + }, + "379a7f755a81a17edb7daaa28afc665dfa6be63a": { + "balance": "0x15af1d78b58c40000" + }, + "379c7166849bc24a02d6535e2def13daeef8aa8d": { + "balance": "0x56bc75e2d63100000" + }, + "37a05aceb9395c8635a39a7c5d266ae610d10bf2": { + "balance": "0x65a4da25d3016c00000" + }, + "37a10451f36166cf643dd2de6c1cbba8a011cfa3": { + "balance": "0x14998f32ac78700000" + }, + "37a7a6ff4ea3d60ec307ca516a48d3053bb79cbb": { + "balance": "0x6c6b935b8bbd400000" + }, + "37ab66083a4fa23848b886f9e66d79cdc150cc70": { + "balance": "0x12be22ffb5ec00380000" + }, + "37ac29bda93f497bc4aeaab935452c431510341e": { + "balance": "0x35659ef93f0fc40000" + }, + "37b8beac7b1ca38829d61ab552c766f48a10c32f": { + "balance": "0x15af1d78b58c400000" + }, + "37bbc47212d82fcb5ee08f5225ecc2041ad2da7d": { + "balance": "0xb1cf24ddd0b1400000" + }, + "37cb868d2c3f95b257611eb34a4188d58b749802": { + "balance": "0x6c6b935b8bbd400000" + }, + "37d980a12ee3bf23cc5cdb63b4ae45691f74c837": { + "balance": "0x6c6b935b8bbd400000" + }, + "37e169a93808d8035698f815c7235613c1e659f2": { + "balance": "0x3635c9adc5dea00000" + }, + "37eada93c475ded2f7e15e7787d400470fa52062": { + "balance": "0xad78ebc5ac6200000" + }, + "37fac1e6bc122e936dfb84de0c4bef6e0d60c2d7": { + "balance": "0x6c6b935b8bbd400000" + }, + "3807eff43aa97c76910a19752dd715ee0182d94e": { + "balance": "0xd90156f6fc2fb0000" + }, + "3815b0743f94fc8cc8654fd9d597ed7d8b77c57e": { + "balance": "0x2809d429d896750000" + }, + "381db4c8465df446a4ce15bf81d47e2f17c980bf": { + "balance": "0x6c6b935b8bbd4000000" + }, + "38202c5cd7078d4f887673ab07109ad8ada89720": { + "balance": "0x3635c9adc5dea00000" + }, + "3821862493242c0aeb84b90de05d250c1e50c074": { + "balance": "0x11776c58e946dc0000" + }, + "382591e7217b435e8e884cdbf415fe377a6fe29e": { + "balance": "0x1b2df9d219f57980000" + }, + "382ba76db41b75606dd48a48f0137e9174e031b6": { + "balance": "0x1158e460913d00000" + }, + "3831757eae7557cb8a37a4b10644b63e4d3b3c75": { + "balance": "0xad78ebc5ac6200000" + }, + "383304dd7a5720b29c1a10f60342219f48032f80": { + "balance": "0x12f939c99edab800000" + }, + "383a7c899ee18bc214969870bc7482f6d8f3570e": { + "balance": "0x21e19e0c9bab2400000" + }, + "38430e931d93be01b4c3ef0dc535f1e0a9610063": { + "balance": "0x21e19e0c9bab2400000" + }, + "38439aaa24e3636f3a18e020ea1da7e145160d86": { + "balance": "0x8cf23f909c0fa00000" + }, + "38458e0685573cb4d28f53098829904570179266": { + "balance": "0x22b1c8c1227a00000" + }, + "3847667038f33b01c1cc795d8daf5475eff5a0d4": { + "balance": "0x277b9bf4246c410000" + }, + "38643babea6011316cc797d9b093c897a17bdae7": { + "balance": "0x1220bb7445daa00000" + }, + "38695fc7e1367ceb163ebb053751f9f68ddb07a0": { + "balance": "0x6c6b935b8bbd400000" + }, + "3872f48dc5e3f817bc6b2ad2d030fc5e0471193d": { + "balance": "0xd8d726b7177a800000" + }, + "387eeafd6b4009deaf8bd5b85a72983a8dcc3487": { + "balance": "0xd8d726b7177a800000" + }, + "3881defae1c07b3ce04c78abe26b0cdc8d73f010": { + "balance": "0xad78ebc5ac6200000" + }, + "3883becc08b9be68ad3b0836aac3b620dc0017ef": { + "balance": "0x6c6b935b8bbd400000" + }, + "3885fee67107dc3a3c741ee290c98918c9b99397": { + "balance": "0x1158e460913d00000" + }, + "3887192c7f705006b630091276b39ac680448d6b": { + "balance": "0x340aad21b3b700000" + }, + "38898bbb4553e00bbfd0cf268b2fc464d154add5": { + "balance": "0x1158e460913d000000" + }, + "388bdcdae794fc44082e667501344118ea96cd96": { + "balance": "0x5a87e7d7f5f6580000" + }, + "388c85a9b9207d8146033fe38143f6d34b595c47": { + "balance": "0xad78ebc5ac6200000" + }, + "3896ad743579d38e2302454d1fb6e2ab69e01bfd": { + "balance": "0x65ea3db75546600000" + }, + "38a3dccf2fcfe0c91a2624bd0cbf88ee4a076c33": { + "balance": "0x6c6b935b8bbd400000" + }, + "38a744efa6d5c2137defef8ef9187b649eee1c78": { + "balance": "0xd8d726b7177a800000" + }, + "38ac664ee8e0795e4275cb852bcba6a479ad9c8d": { + "balance": "0x1158e460913d00000" + }, + "38b2197106123387a0d4de368431a8bacdda30e2": { + "balance": "0x1158e460913d00000" + }, + "38b3965c21fa893931079beacfffaf153678b6eb": { + "balance": "0x93c6a0a51e2670000" + }, + "38b403fb1fb7c14559a2d6f6564a5552bca39aff": { + "balance": "0x6c6b935b8bbd400000" + }, + "38b50146e71916a5448de12a4d742135dcf39833": { + "balance": "0x6d190c475169a200000" + }, + "38bf2a1f7a69de0e2546adb808b36335645da9ff": { + "balance": "0x6c700439d9b5600000" + }, + "38c10b90c859cbb7815692f99dae520ab5febf5e": { + "balance": "0x2c9e4966fa5cf240000" + }, + "38c7851f5ffd4cee98df30f3b25597af8a6ca263": { + "balance": "0x8ead3a2f7d7e180000" + }, + "38d2e9154964b41c8d50a7487d391e7ee2c3d3c2": { + "balance": "0xbdbc41e0348b300000" + }, + "38da1ba2de9e2c954b092dd9d81204fd016ba016": { + "balance": "0x2268ed01f34b3300000" + }, + "38df0c4abe7ded5fe068eadf154ac691774324a4": { + "balance": "0x61093d7c2c6d380000" + }, + "38e2af73393ea98a1d993a74df5cd754b98d529a": { + "balance": "0x61093d7c2c6d380000" + }, + "38e46de4453c38e941e7930f43304f94bb7b2be8": { + "balance": "0x6cb7e74867d5e60000" + }, + "38e7dba8fd4f1f850dbc2649d8e84f0952e3eb3c": { + "balance": "0x2b5e3af16b1880000" + }, + "38e8a31af2d265e31a9fff2d8f46286d1245a467": { + "balance": "0x1158e460913d00000" + }, + "38ea6f5b5a7b88417551b4123dc127dfe9342da6": { + "balance": "0x15af1d78b58c400000" + }, + "38eec6e217f4d41aa920e424b9525197041cd4c6": { + "balance": "0xf00d25eb922e670000" + }, + "38f387e1a4ed4a73106ef2b462e474e2e3143ad0": { + "balance": "0x14542ba12a337c00000" + }, + "391161b0e43c302066e8a68d2ce7e199ecdb1d57": { + "balance": "0xd8d726b7177a800000" + }, + "3915eab5ab2e5977d075dec47d96b68b4b5cf515": { + "balance": "0xd07018185120f400000" + }, + "391a77405c09a72b5e8436237aaaf95d68da1709": { + "balance": "0x2a9264af3d1b90000" + }, + "391f20176d12360d724d51470a90703675594a4d": { + "balance": "0x56bc75e2d631000000" + }, + "392433d2ce83d3fb4a7602cca3faca4ec140a4b0": { + "balance": "0x2c3c465ca58ec0000" + }, + "393f783b5cdb86221bf0294fb714959c7b45899c": { + "balance": "0x14061b9d77a5e980000" + }, + "393ff4255e5c658f2e7f10ecbd292572671bc2d2": { + "balance": "0x6c6b935b8bbd400000" + }, + "394132600f4155e07f4d45bc3eb8d9fb72dcd784": { + "balance": "0x9f6e92edea07d40000" + }, + "3951e48e3c869e6b72a143b6a45068cdb9d466d0": { + "balance": "0x1158e460913d00000" + }, + "3954bdfe0bf587c695a305d9244c3d5bdddac9bb": { + "balance": "0x410278327f985608000" + }, + "395d6d255520a8db29abc47d83a5db8a1a7df087": { + "balance": "0x56bc75e2d63100000" + }, + "39636b25811b176abfcfeeca64bc87452f1fdff4": { + "balance": "0x15af1d78b58c400000" + }, + "3969b4f71bb8751ede43c016363a7a614f76118e": { + "balance": "0x6c6b935b8bbd400000" + }, + "39782ffe06ac78822a3c3a8afe305e50a56188ce": { + "balance": "0x21e19e0c9bab2400000" + }, + "397a6ef8763a18f00fac217e055c0d3094101011": { + "balance": "0x6c6b935b8bbd400000" + }, + "397cdb8c80c67950b18d654229610e93bfa6ee1a": { + "balance": "0x3f95c8e08215210000" + }, + "39824f8bced176fd3ea22ec6a493d0ccc33fc147": { + "balance": "0xd8d726b7177a800000" + }, + "39936c2719450b9420cc2522cf91db01f227c1c1": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "3995e096b08a5a726800fcd17d9c64c64e088d2b": { + "balance": "0xad78ebc5ac6200000" + }, + "399aa6f5d078cb0970882bc9992006f8fbdf3471": { + "balance": "0x3635c9adc5dea00000" + }, + "39aa05e56d7d32385421cf9336e90d3d15a9f859": { + "balance": "0x168d28e3f00280000" + }, + "39aaf0854db6eb39bc7b2e43846a76171c0445de": { + "balance": "0x6449e84e47a8a80000" + }, + "39b1c471ae94e12164452e811fbbe2b3cd7275ac": { + "balance": "0x6c6b935b8bbd400000" + }, + "39b299327490d72f9a9edff11b83afd0e9d3c450": { + "balance": "0xad78ebc5ac6200000" + }, + "39bac68d947859f59e9226089c96d62e9fbe3cde": { + "balance": "0x22b1c8c1227a00000" + }, + "39bfd978689bec048fc776aa15247f5e1d7c39a2": { + "balance": "0x43c33c1937564800000" + }, + "39c773367c8825d3596c686f42bf0d14319e3f84": { + "balance": "0x73f75d1a085ba0000" + }, + "39d4a931402c0c79c457186f24df8729cf957031": { + "balance": "0xd8d726b7177a800000" + }, + "39d6caca22bccd6a72f87ee7d6b59e0bde21d719": { + "balance": "0x6c8754c8f30c080000" + }, + "39e0db4d60568c800b8c5500026c2594f5768960": { + "balance": "0x3635c9adc5dea00000" + }, + "39ee4fe00fbced647068d4f57c01cb22a80bccd1": { + "balance": "0x14542ba12a337c00000" + }, + "39f198331e4b21c1b760a3155f4ab2fe00a74619": { + "balance": "0x6c6b935b8bbd400000" + }, + "39f44663d92561091b82a70dcf593d754005973a": { + "balance": "0xad78b2edc21598000" + }, + "3a035594c747476d42d1ee966c36224cdd224993": { + "balance": "0x134af74569f9c50000" + }, + "3a04572847d31e81f7765ca5bfc9d557159f3683": { + "balance": "0x7362d0dabeafd8000" + }, + "3a06e3bb1edcfd0c44c3074de0bb606b049894a2": { + "balance": "0x21e19e0c9bab2400000" + }, + "3a10888b7e149cae272c01302c327d0af01a0b24": { + "balance": "0xebec21ee1da40000" + }, + "3a3108c1e680a33b336c21131334409d97e5adec": { + "balance": "0x1158e460913d00000" + }, + "3a368efe4ad786e26395ec9fc6ad698cae29fe01": { + "balance": "0x2245899675f9f40000" + }, + "3a3dd104cd7eb04f21932fd433ea7affd39369f5": { + "balance": "0x13614f23e242260000" + }, + "3a4297da3c555e46c073669d0478fce75f2f790e": { + "balance": "0x6ac5c62d9486070000" + }, + "3a476bd2c9e664c63ab266aa4c6e4a4825f516c3": { + "balance": "0xad78ebc5ac6200000" + }, + "3a48e0a7098b06a905802b87545731118e89f439": { + "balance": "0x6c6b935b8bbd400000" + }, + "3a4da78dce05aeb87de9aead9185726da1926798": { + "balance": "0xad78ebc5ac6200000" + }, + "3a59a08246a8206f8d58f70bb1f0d35c5bcc71bd": { + "balance": "0xa076407d3f7440000" + }, + "3a72d635aadeee4382349db98a1813a4cfeb3df1": { + "balance": "0x2a5a058fc295ed000000" + }, + "3a7db224acae17de7798797d82cdf8253017dfa8": { + "balance": "0x10f0cf064dd59200000" + }, + "3a805fa0f7387f73055b7858ca8519edd93d634f": { + "balance": "0x6449e84e47a8a80000" + }, + "3a84e950ed410e51b7e8801049ab2634b285fea1": { + "balance": "0x3f52fdaa822d2c80000" + }, + "3a86ee94862b743dd34f410969d94e2c5652d4ad": { + "balance": "0xaede69ad30e810000" + }, + "3a9132b7093d3ec42e1e4fb8cb31ecdd43ae773c": { + "balance": "0x6c6b935b8bbd400000" + }, + "3a9960266df6492063538a99f487c950a3a5ec9e": { + "balance": "0x5150ae84a8cdf000000" + }, + "3a9b111029ce1f20c9109c7a74eeeef34f4f2eb2": { + "balance": "0xd8d726b7177a800000" + }, + "3a9e5441d44b243be55b75027a1ceb9eacf50df2": { + "balance": "0x3635c9adc5dea00000" + }, + "3aa07a34a1afc8967d3d1383b96b62cf96d5fa90": { + "balance": "0x43c33c1937564800000" + }, + "3aa42c21b9b31c3e27ccd17e099af679cdf56907": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "3aa948ea02397755effb2f9dc9392df1058f7e33": { + "balance": "0x2e141ea081ca080000" + }, + "3aadf98b61e5c896e7d100a3391d3250225d61df": { + "balance": "0xcaf67003701680000" + }, + "3aae4872fd9093cbcad1406f1e8078bab50359e2": { + "balance": "0x222c8eb3ff6640000" + }, + "3abb8adfc604f48d5984811d7f1d52fef6758270": { + "balance": "0xf29719b66f110c0000" + }, + "3ac2f0ff1612e4a1c346d53382abf6d8a25baa53": { + "balance": "0x6c6b935b8bbd400000" + }, + "3ac9dc7a436ae98fd01c7a9621aa8e9d0b8b531d": { + "balance": "0x61093d7c2c6d380000" + }, + "3ad06149b21c55ff867cc3fb9740d2bcc7101231": { + "balance": "0x29b76432b94451200000" + }, + "3ad70243d88bf0400f57c8c1fd57811848af162a": { + "balance": "0x2e9ee5c38653f00000" + }, + "3ad915d550b723415620f5a9b5b88a85f382f035": { + "balance": "0x3635c9adc5dea00000" + }, + "3ae160e3cd60ae31b9d6742d68e14e76bd96c517": { + "balance": "0x1a055690d9db80000" + }, + "3ae62bd271a760637fad79c31c94ff62b4cd12f7": { + "balance": "0x6c6b935b8bbd400000" + }, + "3aea4e82d2400248f99871a41ca257060d3a221b": { + "balance": "0x3635c9adc5dea00000" + }, + "3af65b3e28895a4a001153391d1e69c31fb9db39": { + "balance": "0xd5967be4fc3f100000" + }, + "3b07db5a357f5af2484cbc9d77d73b1fd0519fc7": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "3b0accaf4b607cfe61d17334c214b75cdefdbd89": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b13631a1b89cb566548899a1d60915cdcc4205b": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b159099075207c6807663b1f0f7eda54ac8cce3": { + "balance": "0x6ac4e65b69f92d8000" + }, + "3b1937d5e793b89b63fb8eb5f1b1c9ca6ba0fa8e": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b22da2a0271c8efe102532773636a69b1c17e09": { + "balance": "0x1b36a6444a3e180000" + }, + "3b22dea3c25f1b59c7bd27bb91d3a3eaecef3984": { + "balance": "0x56bc75e2d63100000" + }, + "3b2367f8494b5fe18d683c055d89999c9f3d1b34": { + "balance": "0x21e19e0c9bab2400000" + }, + "3b2c45990e21474451cf4f59f01955b331c7d7c9": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b4100e30a73b0c734b18ffa8426d19b19312f1a": { + "balance": "0xbb5d1aa700afd900000" + }, + "3b42a66d979f582834747a8b60428e9b4eeccd23": { + "balance": "0x21a1c790fadc580000" + }, + "3b4768fd71e2db2cbe7fa050483c27b4eb931df3": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b566a8afad19682dc2ce8679a3ce444a5b0fd4f": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b5c251d7fd7893ba209fe541cecd0ce253a990d": { + "balance": "0x65a4da25d3016c00000" + }, + "3b5e8b3c77f792decb7a8985df916efb490aac23": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b6e814f770748a7c3997806347605480a3fd509": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b7b4f53c45655f3dc5f017edc23b16f9bc536fa": { + "balance": "0x56bc75e2d63100000" + }, + "3b7b8e27de33d3ce7961b98d19a52fe79f6c25be": { + "balance": "0x152d02c7e14af6800000" + }, + "3b7c77dbe95dc2602ce3269a9545d04965fefdbd": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b8098533f7d9bdcd307dbb23e1777ca18418936": { + "balance": "0x6c6b935b8bbd400000" + }, + "3b93b16136f11eaf10996c95990d3b2739ccea5f": { + "balance": "0x21e19e0c9bab2400000" + }, + "3bab4b01a7c84ba13feea9b0bb191b77a3aadca3": { + "balance": "0xad78ebc5ac6200000" + }, + "3bb53598cc20e2055dc553b049404ac9b7dd1e83": { + "balance": "0x21571df77c00be0000" + }, + "3bbc13d04accc0707aebdcaef087d0b87e0b5ee3": { + "balance": "0xbed1d0263d9f000000" + }, + "3bc6e3ee7a56ce8f14a37532590f63716b9966e8": { + "balance": "0x6c6b935b8bbd400000" + }, + "3bc85d6c735b9cda4bba5f48b24b13e70630307b": { + "balance": "0x6acb3df27e1f880000" + }, + "3bd624b548cb659736907ed8aa3c0c705e24b575": { + "balance": "0x6c6b935b8bbd400000" + }, + "3bd9a06d1bd36c4edd27fc0d1f5b088ddae3c72a": { + "balance": "0x1b1a7a420ba00d0000" + }, + "3bddbc8134f77d55597fc97c26d26698090604eb": { + "balance": "0xbe202d6a0eda0000" + }, + "3bf86ed8a3153ec933786a02ac090301855e576b": { + "balance": "0x5f4a8c8375d155400000" + }, + "3bfbd3847c17a61cf3f17b52f8eba1b960b3f39f": { + "balance": "0xa2a15d09519be00000" + }, + "3c03bbc023e1e93fa3a3a6e428cf0cd8f95e1ec6": { + "balance": "0x52663ccab1e1c00000" + }, + "3c0c3defac9cea7acc319a96c30b8e1fedab4574": { + "balance": "0x692ae8897081d00000" + }, + "3c15b3511df6f0342e7348cc89af39a168b7730f": { + "balance": "0x3635c9adc5dea00000" + }, + "3c1f91f301f4b565bca24751aa1f761322709ddd": { + "balance": "0x61093d7c2c6d380000" + }, + "3c286cfb30146e5fd790c2c8541552578de334d8": { + "balance": "0x2291b11aa306e8c0000" + }, + "3c322e611fdb820d47c6f8fc64b6fad74ca95f5e": { + "balance": "0xd258ece1b13150000" + }, + "3c5a241459c6abbf630239c98a30d20b8b3ac561": { + "balance": "0x88b23acffd9900000" + }, + "3c79c863c3d372b3ff0c6f452734a7f97042d706": { + "balance": "0x98a7d9b8314c00000" + }, + "3c83c1701db0388b68210d00f5717cd9bd322c6a": { + "balance": "0x65a4da25d3016c00000" + }, + "3c860e2e663f46db53427b29fe3ea5e5bf62bbcc": { + "balance": "0x556f64c1fe7fa0000" + }, + "3c869c09696523ced824a070414605bb76231ff2": { + "balance": "0x3635c9adc5dea00000" + }, + "3c925619c9b33144463f0537d896358706c520b0": { + "balance": "0x6c6b935b8bbd400000" + }, + "3c98594bf68b57351e8814ae9e6dfd2d254aa06f": { + "balance": "0x1043561a8829300000" + }, + "3cadeb3d3eed3f62311d52553e70df4afce56f23": { + "balance": "0xd8d726b7177a800000" + }, + "3caedb5319fe806543c56e5021d372f71be9062e": { + "balance": "0x878678326eac9000000" + }, + "3cafaf5e62505615068af8eb22a13ad8a9e55070": { + "balance": "0x6c660645aa47180000" + }, + "3cb179cb4801a99b95c3b0c324a2bdc101a65360": { + "balance": "0x168d28e3f00280000" + }, + "3cb561ce86424b359891e364ec925ffeff277df7": { + "balance": "0xad78ebc5ac6200000" + }, + "3ccb71aa6880cb0b84012d90e60740ec06acd78f": { + "balance": "0x6c6b935b8bbd400000" + }, + "3ccef88679573947e94997798a1e327e08603a65": { + "balance": "0x2bc916d69f3b020000" + }, + "3cd1d9731bd548c1dd6fcea61beb75d91754f7d3": { + "balance": "0x1161d01b215cae48000" + }, + "3cd3a6e93579c56d494171fc533e7a90e6f59464": { + "balance": "0x6c6b935b8bbd400000" + }, + "3cd6b7593cbee77830a8b19d0801958fcd4bc57a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "3cd7f7c7c2353780cde081eeec45822b25f2860c": { + "balance": "0xad78ebc5ac6200000" + }, + "3ce1dc97fcd7b7c4d3a18a49d6f2a5c1b1a906d7": { + "balance": "0xad78ebc5ac6200000" + }, + "3cea302a472a940379dd398a24eafdbadf88ad79": { + "balance": "0xa2a15d09519be00000" + }, + "3ceca96bb1cdc214029cbc5e181d398ab94d3d41": { + "balance": "0x10f0cf064dd592000000" + }, + "3cf484524fbdfadae26dc185e32b2b630fd2e726": { + "balance": "0x185452cb2a91c30000" + }, + "3cf9a1d465e78b7039e3694478e2627b36fcd141": { + "balance": "0x4a60532ad51bf00000" + }, + "3cfbf066565970639e130df2a7d16b0e14d6091c": { + "balance": "0x5c283d410394100000" + }, + "3d09688d93ad07f3abe68c722723cd680990435e": { + "balance": "0x65a4ce99f769e6e0000" + }, + "3d31587b5fd5869845788725a663290a49d3678c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "3d3fad49c9e5d2759c8e8e5a7a4d60a0dd135692": { + "balance": "0x1158e460913d00000" + }, + "3d574fcf00fae1d98cc8bf9ddfa1b3953b9741bc": { + "balance": "0x6acb3df27e1f880000" + }, + "3d5a8b2b80be8b35d8ecf789b5ed7a0775c5076c": { + "balance": "0x1158e460913d00000" + }, + "3d66cd4bd64d5c8c1b5eea281e106d1c5aad2373": { + "balance": "0x69c4f3a8a110a60000" + }, + "3d6ae053fcbc318d6fd0fbc353b8bf542e680d27": { + "balance": "0xc673ce3c40160000" + }, + "3d6ff82c9377059fb30d9215723f60c775c891fe": { + "balance": "0xd8e5ce617f2d50000" + }, + "3d79a853d71be0621b44e29759656ca075fdf409": { + "balance": "0x6c6b935b8bbd400000" + }, + "3d7ea5bf03528100ed8af8aed2653e921b6e6725": { + "balance": "0x3635c9adc5dea00000" + }, + "3d813ff2b6ed57b937dabf2b381d148a411fa085": { + "balance": "0x56bc75e2d63100000" + }, + "3d881433f04a7d0d27f84944e08a512da3555287": { + "balance": "0x410d586a20a4c00000" + }, + "3d89e505cb46e211a53f32f167a877bec87f4b0a": { + "balance": "0x15b3557f1937f8000" + }, + "3d8d0723721e73a6c0d860aa0557abd14c1ee362": { + "balance": "0x10f0cf064dd59200000" + }, + "3d8f39881b9edfe91227c33fa4cdd91e678544b0": { + "balance": "0x4ab07ba43ada98000" + }, + "3d9d6be57ff83e065985664f12564483f2e600b2": { + "balance": "0x6eace43f23bd800000" + }, + "3da39ce3ef4a7a3966b32ee7ea4ebc2335a8f11f": { + "balance": "0x6c6b935b8bbd400000" + }, + "3daa01ceb70eaf9591fa521ba4a27ea9fb8ede4a": { + "balance": "0x5a63d2c9bc76540000" + }, + "3db5fe6a68bd3612ac15a99a61e555928eeceaf3": { + "balance": "0x55a6e79ccd1d300000" + }, + "3db9ed7f024c7e26372feacf2b050803445e3810": { + "balance": "0x45b148b4996a300000" + }, + "3dbf0dbfd77890800533f09dea8301b9f025d2a6": { + "balance": "0x3635c9adc5dea00000" + }, + "3dcef19c868b15d34eda426ec7e04b18b6017002": { + "balance": "0x6c68ccd09b022c0000" + }, + "3dd12e556a603736feba4a6fa8bd4ac45d662a04": { + "balance": "0x23757b9183e078280000" + }, + "3dde8b15b3ccbaa5780112c3d674f313bba68026": { + "balance": "0x601d515a3e4f940000" + }, + "3ddedbe48923fbf9e536bf9ffb0747c9cdd39eef": { + "balance": "0x368c8623a8b4d100000" + }, + "3deae43327913f62808faa1b6276a2bd6368ead9": { + "balance": "0x6c6b935b8bbd400000" + }, + "3df762049eda8ac6927d904c7af42f94e5519601": { + "balance": "0x6c6b935b8bbd400000" + }, + "3e040d40cb80ba0125f3b15fdefcc83f3005da1b": { + "balance": "0x384524cc70b7780000" + }, + "3e0b8ed86ed669e12723af7572fbacfe829b1e16": { + "balance": "0x514de7f9b812dc0000" + }, + "3e0cbe6a6dcb61f110c45ba2aa361d7fcad3da73": { + "balance": "0x1b2df9d219f57980000" + }, + "3e194b4ecef8bb711ea2ff24fec4e87bd032f7d1": { + "balance": "0x8b9dc1bc1a036a8000" + }, + "3e1b2230afbbd310b4926a4c776d5ae7819c661d": { + "balance": "0x65a4da25d3016c00000" + }, + "3e1c53300e4c168912163c7e99b95da268ad280a": { + "balance": "0x3662325cd18fe00000" + }, + "3e1c962063e0d5295941f210dca3ab531eec8809": { + "balance": "0xa2a15d09519be00000" + }, + "3e2ca0d234baf607ad466a1b85f4a6488ef00ae7": { + "balance": "0x4da21a3483d568000" + }, + "3e2f26235e137a7324e4dc154b5df5af46ea1a49": { + "balance": "0x137aad8032db90000" + }, + "3e3161f1ea2fbf126e79da1801da9512b37988c9": { + "balance": "0xa6dd90cae5114480000" + }, + "3e36c17253c11cf38974ed0db1b759160da63783": { + "balance": "0x17b7883c06916600000" + }, + "3e3cd3bec06591d6346f254b621eb41c89008d31": { + "balance": "0x35dfbeda9f37340000" + }, + "3e45bd55db9060eced923bb9cb733cb3573fb531": { + "balance": "0x58e7926ee858a00000" + }, + "3e4d13c55a84e46ed7e9cb90fd355e8ad991e38f": { + "balance": "0x3635c9adc5dea00000" + }, + "3e4e9265223c9738324cf20bd06006d0073edb8c": { + "balance": "0x73f75d1a085ba0000" + }, + "3e4fbd661015f6461ed6735cefef01f31445de3a": { + "balance": "0x36e342998b8b0200000" + }, + "3e53ff2107a8debe3328493a92a586a7e1f49758": { + "balance": "0x4e69c2a71a405ab0000" + }, + "3e5a39fdda70df1126ab0dc49a7378311a537a1f": { + "balance": "0x821ab0d44149800000" + }, + "3e5abd09ce5af7ba8487c359e0f2a93a986b0b18": { + "balance": "0x21e19e0c9bab2400000" + }, + "3e5cb8928c417825c03a3bfcc52183e5c91e42d7": { + "balance": "0xe731d9c52c962f0000" + }, + "3e5e93fb4c9c9d1246f8f247358e22c3c5d17b6a": { + "balance": "0x821ab0d4414980000" + }, + "3e618350fa01657ab0ef3ebac8e37012f8fc2b6f": { + "balance": "0x9806de3da6e9780000" + }, + "3e63ce3b24ca2865b4c5a687b7aea3597ef6e548": { + "balance": "0x6c6b935b8bbd400000" + }, + "3e66b84769566ab67945d5fa81373556bcc3a1fa": { + "balance": "0x83d6c7aab63600000" + }, + "3e76a62db187aa74f63817533b306cead0e8cebe": { + "balance": "0x69b5afac750bb800000" + }, + "3e7a966b5dc357ffb07e9fe067c45791fd8e3049": { + "balance": "0x3342d60dff1960000" + }, + "3e81772175237eb4cbe0fe2dcafdadffeb6a1999": { + "balance": "0x1dd0c885f9a0d800000" + }, + "3e8349b67f5745449f659367d9ad4712db5b895a": { + "balance": "0x62a992e53a0af00000" + }, + "3e83544f0082552572c782bee5d218f1ef064a9d": { + "balance": "0x56cd55fc64dfe0000" + }, + "3e84b35c5b2265507061d30b6f12da033fe6f8b9": { + "balance": "0x61093d7c2c6d380000" + }, + "3e8641d43c42003f0a33c929f711079deb2b9e46": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "3e8745ba322f5fd6cb50124ec46688c7a69a7fae": { + "balance": "0x10afc1ade3b4ed40000" + }, + "3e914e3018ac00449341c49da71d04dfeeed6221": { + "balance": "0xd8d726b7177a800000" + }, + "3e9410d3b9a87ed5e451a6b91bb8923fe90fb2b5": { + "balance": "0xad78ebc5ac6200000" + }, + "3e94df5313fa520570ef232bc3311d5f622ff183": { + "balance": "0x6c6b935b8bbd400000" + }, + "3e9b34a57f3375ae59c0a75e19c4b641228d9700": { + "balance": "0xf8699329677e0000" + }, + "3eada8c92f56067e1bb73ce378da56dc2cdfd365": { + "balance": "0x77cde93aeb0d480000" + }, + "3eaf0879b5b6db159b589f84578b6a74f6c10357": { + "balance": "0x18938b671fa65a28000" + }, + "3eaf316b87615d88f7adc77c58e712ed4d77966b": { + "balance": "0x56dbc4cee24648000" + }, + "3eb8b33b21d23cda86d8288884ab470e164691b5": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "3eb9ef06d0c259040319947e8c7a6812aa0253d8": { + "balance": "0x90d972f32323c0000" + }, + "3ecc8e1668dde995dc570fe414f44211c534a615": { + "balance": "0x6c6b935b8bbd400000" + }, + "3ecdb532e397579662b2a46141e78f8235936a5f": { + "balance": "0x39fbae8d042dd0000" + }, + "3eee6f1e96360b7689b3069adaf9af8eb60ce481": { + "balance": "0x3635c9adc5dea00000" + }, + "3f08d9ad894f813e8e2148c160d24b353a8e74b0": { + "balance": "0xcb49b44ba602d800000" + }, + "3f0c83aac5717962734e5ceaeaecd39b28ad06be": { + "balance": "0x6c6b935b8bbd400000" + }, + "3f10800282d1b7ddc78fa92d8230074e1bf6aeae": { + "balance": "0x10afc1ade3b4ed40000" + }, + "3f1233714f204de9de4ee96d073b368d8197989f": { + "balance": "0x217c41074e6bb0000" + }, + "3f173aa6edf469d185e59bd26ae4236b92b4d8e1": { + "balance": "0x1158e460913d000000" + }, + "3f1bc420c53c002c9e90037c44fe6a8ef4ddc962": { + "balance": "0x960db77681e940000" + }, + "3f236108eec72289bac3a65cd283f95e041d144c": { + "balance": "0x3634bf39ab98788000" + }, + "3f2da093bb16eb064f8bfa9e30b929d15f8e1c4c": { + "balance": "0x6c6b935b8bbd400000" + }, + "3f2dd55db7eab0ebee65b33ed8202c1e992e958b": { + "balance": "0x2c73c937742c500000" + }, + "3f2f381491797cc5c0d48296c14fd0cd00cdfa2d": { + "balance": "0x2b95bdcc39b6100000" + }, + "3f30d3bc9f602232bc724288ca46cd0b0788f715": { + "balance": "0xd8d726b7177a800000" + }, + "3f3c8e61e5604cef0605d436dd22accd862217fc": { + "balance": "0x487a9a304539440000" + }, + "3f3f46b75cabe37bfacc8760281f4341ca7f463d": { + "balance": "0x20ac448235fae88000" + }, + "3f472963197883bbda5a9b7dfcb22db11440ad31": { + "balance": "0x1a19643cb1eff08000" + }, + "3f4cd1399f8a34eddb9a17a471fc922b5870aafc": { + "balance": "0xad78ebc5ac6200000" + }, + "3f551ba93cd54693c183fb9ad60d65e1609673c9": { + "balance": "0x6c6b935b8bbd400000" + }, + "3f627a769e6a950eb87017a7cd9ca20871136831": { + "balance": "0x2eb8eb1a172dcb80000" + }, + "3f6dd3650ee428dcb7759553b017a96a94286ac9": { + "balance": "0x487a9a304539440000" + }, + "3f747237806fed3f828a6852eb0867f79027af89": { + "balance": "0x5150ae84a8cdf00000" + }, + "3f75ae61cc1d8042653b5baec4443e051c5e7abd": { + "balance": "0x52d542804f1ce0000" + }, + "3fb7d197b3ba4fe045efc23d50a14585f558d9b2": { + "balance": "0x1158e460913d00000" + }, + "3fbc1e4518d73400c6d046359439fb68ea1a49f4": { + "balance": "0x3790bb8551376400000" + }, + "3fbed6e7e0ca9c84fbe9ebcf9d4ef9bb49428165": { + "balance": "0x6c6b935b8bbd400000" + }, + "3fd0bb47798cf44cdfbe4d333de637df4a00e45c": { + "balance": "0x56c5579f722140000" + }, + "3fe40fbd919aad2818df01ee4df46c46842ac539": { + "balance": "0x14542ba12a337c00000" + }, + "3fe801e61335c5140dc7eda2ef5204460a501230": { + "balance": "0x6c6b935b8bbd400000" + }, + "3ff836b6f57b901b440c30e4dbd065cf37d3d48c": { + "balance": "0xad78ebc5ac6200000" + }, + "3ffcb870d4023d255d5167d8a507cefc366b68ba": { + "balance": "0x23343c4354d2ac0000" + }, + "401354a297952fa972ad383ca07a0a2811d74a71": { + "balance": "0xc249fdd327780000" + }, + "4030a925706b2c101c8c5cb9bd05fbb4f6759b18": { + "balance": "0xd8d726b7177a800000" + }, + "403145cb4ae7489fcc90cd985c6dc782b3cc4e44": { + "balance": "0x1453ff387b27cac0000" + }, + "403220600a36f73f24e190d1edb2d61be3f41354": { + "balance": "0x107ad8f556c6c00000" + }, + "4039bd50a2bde15ffe37191f410390962a2b8886": { + "balance": "0xad78ebc5ac6200000" + }, + "403c64896a75cad816a9105e18d8aa5bf80f238e": { + "balance": "0x35659ef93f0fc40000" + }, + "403d53cf620f0922b417848dee96c190b5bc8271": { + "balance": "0x215f835bc769da80000" + }, + "404100db4c5d0eec557823b58343758bcc2c8083": { + "balance": "0x1158e460913d00000" + }, + "4041374b0feef4792e4b33691fb86897a4ff560c": { + "balance": "0x13c9647e25a9940000" + }, + "40467d80e74c35407b7db51789234615fea66818": { + "balance": "0x150894e849b3900000" + }, + "40585200683a403901372912a89834aadcb55fdb": { + "balance": "0x6c6b935b8bbd400000" + }, + "4058808816fdaa3a5fc98ed47cfae6c18315422e": { + "balance": "0xad4c8316a0b0c0000" + }, + "405f596b94b947344c033ce2dcbff12e25b79784": { + "balance": "0x6c6b935b8bbd400000" + }, + "40630024bd2c58d248edd8465617b2bf1647da0e": { + "balance": "0x3635c9adc5dea00000" + }, + "40652360d6716dc55cf9aab21f3482f816cc2cbd": { + "balance": "0x21e19e0c9bab2400000" + }, + "407295ebd94b48269c2d569c9b9af9aa05e83e5e": { + "balance": "0x21e19e0c9bab2400000" + }, + "4073fa49b87117cb908cf1ab512da754a932d477": { + "balance": "0x6acb3df27e1f880000" + }, + "408a69a40715e1b313e1354e600800a1e6dc02a5": { + "balance": "0x1e7b891cc92540000" + }, + "409bd75085821c1de70cdc3b11ffc3d923c74010": { + "balance": "0xd8d726b7177a800000" + }, + "409d5a962edeeebea178018c0f38b9cdb213f289": { + "balance": "0x1158e460913d00000" + }, + "40a331195b977325c2aa28fa2f42cb25ec3c253c": { + "balance": "0x6c6b935b8bbd400000" + }, + "40a7f72867a7dc86770b162b7557a434ed50cce9": { + "balance": "0x3635c9adc5dea00000" + }, + "40ab0a3e83d0c8ac9366910520eab1772bac3b1a": { + "balance": "0x34f10c2dc05e7c0000" + }, + "40ab66fe213ea56c3afb12c75be33f8e32fd085d": { + "balance": "0xd8d726b7177a800000" + }, + "40ad74bc0bce2a45e52f36c3debb1b3ada1b7619": { + "balance": "0x170162de109c6580000" + }, + "40cf890591eae4a18f812a2954cb295f633327e6": { + "balance": "0x29bf736fc591a0000" + }, + "40cf90ef5b768c5da585002ccbe6617650d8e837": { + "balance": "0x36330322d5238c0000" + }, + "40d45d9d7625d15156c932b771ca7b0527130958": { + "balance": "0x152d02c7e14af6800000" + }, + "40db1ba585ce34531edec5494849391381e6ccd3": { + "balance": "0x61093d7c2c6d380000" + }, + "40df495ecf3f8b4cef2a6c189957248fe884bc2b": { + "balance": "0x28a857425466f800000" + }, + "40e0dbf3efef9084ea1cd7e503f40b3b4a8443f6": { + "balance": "0xd8d726b7177a800000" + }, + "40e2440ae142c880366a12c6d4102f4b8434b62a": { + "balance": "0x3635c9adc5dea00000" + }, + "40e3c283f7e24de0410c121bee60a5607f3e29a6": { + "balance": "0x3635c9adc5dea00000" + }, + "40ea5044b204b23076b1a5803bf1d30c0f88871a": { + "balance": "0x2f6f10780d22cc00000" + }, + "40eddb448d690ed72e05c225d34fc8350fa1e4c5": { + "balance": "0x17b7883c06916600000" + }, + "40f4f4c06c732cd35b119b893b127e7d9d0771e4": { + "balance": "0x21e19e0c9bab2400000" + }, + "41010fc8baf8437d17a04369809a168a17ca56fb": { + "balance": "0x56bc75e2d63100000" + }, + "4103299671d46763978fa4aa19ee34b1fc952784": { + "balance": "0xad78ebc5ac6200000" + }, + "41033c1b6d05e1ca89b0948fc64453fbe87ab25e": { + "balance": "0x487a9a304539440000" + }, + "41098a81452317c19e3eef0bd123bbe178e9e9ca": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "411610b178d5617dfab934d293f512a93e5c10e1": { + "balance": "0x93739534d28680000" + }, + "411c831cc6f44f1965ec5757ab4e5b3ca4cffd1f": { + "balance": "0x170a0f5040e5040000" + }, + "412a68f6c645559cc977fc4964047a201d1bb0e2": { + "balance": "0xa968163f0a57b400000" + }, + "413f4b02669ccff6806bc826fcb7deca3b0ea9bc": { + "balance": "0x1158e460913d00000" + }, + "414599092e879ae25372a84d735af5c4e510cd6d": { + "balance": "0x15af1d78b58c400000" + }, + "41485612d03446ec4c05e5244e563f1cbae0f197": { + "balance": "0x34957444b840e80000" + }, + "415d096ab06293183f3c033d25f6cf7178ac3bc7": { + "balance": "0x22b1c8c1227a00000" + }, + "4166fc08ca85f766fde831460e9dc93c0e21aa6c": { + "balance": "0x3635c9adc5dea00000" + }, + "416784af609630b070d49a8bcd12235c6428a408": { + "balance": "0x43c33c1937564800000" + }, + "4167cd48e733418e8f99ffd134121c4a4ab278c4": { + "balance": "0xc55325ca7415e00000" + }, + "416c86b72083d1f8907d84efd2d2d783dffa3efb": { + "balance": "0x6c6acc67d7b1d40000" + }, + "4173419d5c9f6329551dc4d3d0ceac1b701b869e": { + "balance": "0x4c53ecdc18a600000" + }, + "4174fa1bc12a3b7183cbabb77a0b59557ba5f1db": { + "balance": "0x6c6b935b8bbd400000" + }, + "41786a10d447f484d33244ccb7facd8b427b5b8c": { + "balance": "0x3635c9adc5dea00000" + }, + "417a3cd19496530a6d4204c3b5a17ce0f207b1a5": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "417e4e2688b1fd66d821529e46ed4f42f8b3db3d": { + "balance": "0x6c6b935b8bbd400000" + }, + "419a71a36c11d105e0f2aef5a3e598078e85c80b": { + "balance": "0x10f0cf064dd59200000" + }, + "419bde7316cc1ed295c885ace342c79bf7ee33ea": { + "balance": "0x14542ba12a337c00000" + }, + "41a2f2e6ecb86394ec0e338c0fc97e9c5583ded2": { + "balance": "0x6cee06ddbe15ec0000" + }, + "41a8c2830081b102df6e0131657c07ab635b54ce": { + "balance": "0x6c6acc67d7b1d40000" + }, + "41a8e236a30e6d63c1ff644d132aa25c89537e01": { + "balance": "0x1158e460913d00000" + }, + "41a9a404fc9f5bfee48ec265b12523338e29a8bf": { + "balance": "0x150894e849b3900000" + }, + "41ad369f758fef38a19aa3149379832c818ef2a0": { + "balance": "0x36369ed7747d260000" + }, + "41b2d34fde0b1029262b4172c81c1590405b03ae": { + "balance": "0x3635c9adc5dea00000" + }, + "41b2dbd79dda9b864f6a7030275419c39d3efd3b": { + "balance": "0xad78ebc5ac62000000" + }, + "41c3c2367534d13ba2b33f185cdbe6ac43c2fa31": { + "balance": "0xd8d726b7177a800000" + }, + "41cb9896445f70a10a14215296daf614e32cf4d5": { + "balance": "0x678a932062e4180000" + }, + "41ce79950935cff55bf78e4ccec2fe631785db95": { + "balance": "0x6c6b935b8bbd400000" + }, + "41d3b731a326e76858baa5f4bd89b57b36932343": { + "balance": "0x155bd9307f9fe80000" + }, + "41e4a20275e39bdcefeb655c0322744b765140c2": { + "balance": "0x21e19e0c9bab2400000" + }, + "41ed2d8e7081482c919fc23d8f0091b3c82c4685": { + "balance": "0x463a1e765bd78a0000" + }, + "41f27e744bd29de2b0598f02a0bb9f98e681eaa4": { + "balance": "0x1a4aba225c207400000" + }, + "41f489a1ec747bc29c3e5f9d8db97877d4d1b4e9": { + "balance": "0x73f75d1a085ba0000" + }, + "420fb86e7d2b51401fc5e8c72015decb4ef8fc2e": { + "balance": "0x3635c9adc5dea00000" + }, + "421684baa9c0b4b5f55338e6f6e7c8e146d41cb7": { + "balance": "0x5150ae84a8cdf00000" + }, + "42399659aca6a5a863ea2245c933fe9a35b7880e": { + "balance": "0x6ece32c26c82700000" + }, + "423bca47abc00c7057e3ad34fca63e375fbd8b4a": { + "balance": "0x3cfc82e37e9a7400000" + }, + "423c3107f4bace414e499c64390a51f74615ca5e": { + "balance": "0x6c6b935b8bbd400000" + }, + "423cc4594cf4abb6368de59fd2b1230734612143": { + "balance": "0x6c6b935b8bbd400000" + }, + "4244f1331158b9ce26bbe0b9236b9203ca351434": { + "balance": "0x21e19e0c9bab2400000" + }, + "425177eb74ad0a9d9a5752228147ee6d6356a6e6": { + "balance": "0xb98bc829a6f90000" + }, + "425725c0f08f0811f5f006eec91c5c5c126b12ae": { + "balance": "0x821ab0d4414980000" + }, + "4258fd662fc4ce3295f0d4ed8f7bb1449600a0a9": { + "balance": "0x16c452ed6088ad80000" + }, + "425c1816868f7777cc2ba6c6d28c9e1e796c52b3": { + "balance": "0x21e19e0c9bab2400000" + }, + "425c338a1325e3a1578efa299e57d986eb474f81": { + "balance": "0x6c6b935b8bbd400000" + }, + "426259b0a756701a8b663528522156c0288f0f24": { + "balance": "0x218ae196b8d4f300000" + }, + "426d15f407a01135b13a6b72f8f2520b3531e302": { + "balance": "0x1158e460913d00000" + }, + "426f78f70db259ac8534145b2934f4ef1098b5d8": { + "balance": "0x138400eca364a00000" + }, + "42732d8ef49ffda04b19780fd3c18469fb374106": { + "balance": "0x170b00e5e4a9be0000" + }, + "427417bd16b1b3d22dbb902d8f9657016f24a61c": { + "balance": "0x6c6b935b8bbd400000" + }, + "42746aeea14f27beff0c0da64253f1e7971890a0": { + "balance": "0x54069233bf7f780000" + }, + "427b462ab84e5091f48a46eb0cdc92ddcb26e078": { + "balance": "0x6c6b935b8bbd400000" + }, + "427e4751c3babe78cff8830886febc10f9908d74": { + "balance": "0x6acb3df27e1f880000" + }, + "427ec668ac9404e895cc861511d1620a4912be98": { + "balance": "0x878678326eac9000000" + }, + "4280a58f8bb10b9440de94f42b4f592120820191": { + "balance": "0x6c6b935b8bbd400000" + }, + "428a1ee0ed331d7952ccbe1c7974b2852bd1938a": { + "balance": "0x77b74a4e8de5650000" + }, + "429c06b487e8546abdfc958a25a3f0fba53f6f00": { + "balance": "0xbb644af542198000" + }, + "42a98bf16027ce589c4ed2c95831e2724205064e": { + "balance": "0x21e19e0c9bab2400000" + }, + "42c6edc515d35557808d13cd44dcc4400b2504e4": { + "balance": "0xaba14c59ba7320000" + }, + "42cecfd2921079c2d7df3f08b07aa3beee5e219a": { + "balance": "0x3635c9adc5dea00000" + }, + "42d1a6399b3016a8597f8b640927b8afbce4b215": { + "balance": "0xa18bcec34888100000" + }, + "42d34940edd2e7005d46e2188e4cfece8311d74d": { + "balance": "0x890b0c2e14fb80000" + }, + "42d3a5a901f2f6bd9356f112a70180e5a1550b60": { + "balance": "0x3224f42723d4540000" + }, + "42d6b263d9e9f4116c411424fc9955783c763030": { + "balance": "0x6c6b935b8bbd400000" + }, + "42db0b902559e04087dd5c441bc7611934184b89": { + "balance": "0x6d33b17d253a620000" + }, + "42ddd014dc52bfbcc555325a40b516f4866a1dd3": { + "balance": "0x6c6b935b8bbd400000" + }, + "4319263f75402c0b5325f263be4a5080651087f0": { + "balance": "0x354b0f14631bab0000" + }, + "431f2c19e316b044a4b3e61a0c6ff8c104a1a12f": { + "balance": "0x3635c9adc5dea00000" + }, + "43227d65334e691cf231b4a4e1d339b95d598afb": { + "balance": "0x21e19e0c9bab2400000" + }, + "432809a2390f07c665921ff37d547d12f1c9966a": { + "balance": "0x65a4da25d3016c00000" + }, + "4329fc0931cbeb033880fe4c9398ca45b0e2d11a": { + "balance": "0x6c7120716d33680000" + }, + "432d884bd69db1acc0d89c64ade4cb4fc3a88b7a": { + "balance": "0x869a8c10808eec0000" + }, + "4331ab3747d35720a9d8ca25165cd285acd4bda8": { + "balance": "0x6c6b935b8bbd400000" + }, + "433a3b68e56b0df1862b90586bbd39c840ff1936": { + "balance": "0x6c6b935b8bbd400000" + }, + "433e3ba1c51b810fc467d5ba4dea42f7a9885e69": { + "balance": "0x878678326eac9000000" + }, + "433eb94a339086ed12d9bde9cd1d458603c97dd6": { + "balance": "0x152d02c7e14af6800000" + }, + "4349225a62f70aea480a029915a01e5379e64fa5": { + "balance": "0x8cd67e2334c0d80000" + }, + "4354221e62dc09e6406436163a185ef06d114a81": { + "balance": "0x6c6b935b8bbd400000" + }, + "435443b81dfdb9bd8c6787bc2518e2d47e57c15f": { + "balance": "0x1438d9397881ef20000" + }, + "4361d4846fafb377b6c0ee49a596a78ddf3516a3": { + "balance": "0xc2127af858da700000" + }, + "4364309a9fa07095600f79edc65120cdcd23dc64": { + "balance": "0x21e19e0c9bab2400000" + }, + "4367ae4b0ce964f4a54afd4b5c368496db169e9a": { + "balance": "0x6c6b935b8bbd400000" + }, + "43748928e8c3ec4436a1d092fbe43ac749be1251": { + "balance": "0x15af1d78b58c400000" + }, + "43767bf7fd2af95b72e9312da9443cb1688e4343": { + "balance": "0x1043561a8829300000" + }, + "437983388ab59a4ffc215f8e8269461029c3f1c1": { + "balance": "0x43c33c1937564800000" + }, + "43898c49a34d509bfed4f76041ee91caf3aa6aa5": { + "balance": "0x1043561a8829300000" + }, + "438c2f54ff8e629bab36b1442b760b12a88f02ae": { + "balance": "0x6c6b935b8bbd400000" + }, + "4398628ea6632d393e929cbd928464c568aa4a0c": { + "balance": "0x4be4e7267b6ae00000" + }, + "439d2f2f5110a4d58b1757935015408740fec7f8": { + "balance": "0xcfa5c5150f4c888000" + }, + "439dee3f7679ff1030733f9340c096686b49390b": { + "balance": "0x6c6b935b8bbd400000" + }, + "43b079baf0727999e66bf743d5bcbf776c3b0922": { + "balance": "0x6c6b935b8bbd400000" + }, + "43bc2d4ddcd6583be2c7bc094b28fb72e62ba83b": { + "balance": "0x6c6b935b8bbd400000" + }, + "43c7ebc5b3e7af16f47dc5617ab10e0f39b4afbb": { + "balance": "0x678a932062e4180000" + }, + "43cb9652818c6f4d6796b0e89409306c79db6349": { + "balance": "0x6c6b935b8bbd400000" + }, + "43cc08d0732aa58adef7619bed46558ad7774173": { + "balance": "0xf0e7dcb0122a8f0000" + }, + "43d5a71ce8b8f8ae02b2eaf8eaf2ca2840b93fb6": { + "balance": "0x14542ba12a337c00000" + }, + "43db7ff95a086d28ebbfb82fb8fb5f230a5ebccd": { + "balance": "0xdf6eb0b2d3ca0000" + }, + "43e7ec846358d7d0f937ad1c350ba069d7bf72bf": { + "balance": "0x670ae629214680000" + }, + "43f16f1e75c3c06a9478e8c597a40a3cb0bf04cc": { + "balance": "0x9df7dfa8f760480000" + }, + "43f470ed659e2991c375957e5ddec5bd1d382231": { + "balance": "0x56bc75e2d63100000" + }, + "43f7e86e381ec51ec4906d1476cba97a3db584e4": { + "balance": "0x3635c9adc5dea00000" + }, + "43ff38743ed0cd43308c066509cc8e7e72c862aa": { + "balance": "0x692ae8897081d00000" + }, + "43ff8853e98ed8406b95000ada848362d6a0392a": { + "balance": "0x4ae0b1c4d2e84d00000" + }, + "44098866a69b68c0b6bc168229b9603587058967": { + "balance": "0xa31062beeed700000" + }, + "4419ac618d5dea7cdc6077206fb07dbdd71c1702": { + "balance": "0xd8d726b7177a800000" + }, + "441a52001661fac718b2d7b351b7c6fb521a7afd": { + "balance": "0x15af1d78b58c400000" + }, + "441aca82631324acbfa2468bda325bbd78477bbf": { + "balance": "0x14542ba12a337c00000" + }, + "441f37e8a029fd02482f289c49b5d06d00e408a4": { + "balance": "0x1211ecb56d13488000" + }, + "4420aa35465be617ad2498f370de0a3cc4d230af": { + "balance": "0x6c6b935b8bbd400000" + }, + "44232ff66ddad1fd841266380036afd7cf7d7f42": { + "balance": "0xad78ebc5ac6200000" + }, + "44250d476e062484e9080a3967bf3a4a732ad73f": { + "balance": "0x1158e460913d00000" + }, + "4429a29fee198450672c0c1d073162250bec6474": { + "balance": "0x362aaf8202f2500000" + }, + "44355253b27748e3f34fe9cae1fb718c8f249529": { + "balance": "0xad78ebc5ac6200000" + }, + "4438e880cb2766b0c1ceaec9d2418fceb952a044": { + "balance": "0x73fa073903f080000" + }, + "444caf79b71338ee9aa7c733b02acaa7dc025948": { + "balance": "0x22b1c8c1227a00000" + }, + "445cb8de5e3df520b499efc980f52bff40f55c76": { + "balance": "0x6c6b935b8bbd400000" + }, + "446a8039cecf9dce4879cbcaf3493bf545a88610": { + "balance": "0x17b7883c06916600000" + }, + "4474299d0ee090dc90789a1486489c3d0d645e6d": { + "balance": "0x3635c9adc5dea00000" + }, + "448bf410ad9bbc2fecc4508d87a7fc2e4b8561ad": { + "balance": "0xad6eedd17cf3b8000" + }, + "44901e0d0e08ac3d5e95b8ec9d5e0ff5f12e0393": { + "balance": "0x16a1f9f5fd7d960000" + }, + "4493123c021ece3b33b1a452c9268de14007f9d3": { + "balance": "0x16a6502f15a1e540000" + }, + "449ac4fbe383e36738855e364a57f471b2bfa131": { + "balance": "0x29b76432b94451200000" + }, + "44a01fb04ac0db2cce5dbe281e1c46e28b39d878": { + "balance": "0x6c6acc67d7b1d40000" + }, + "44a63d18424587b9b307bfc3c364ae10cd04c713": { + "balance": "0x1158e460913d00000" + }, + "44a8989e32308121f72466978db395d1f76c3a4b": { + "balance": "0x18850299f42b06a0000" + }, + "44c1110b18870ec81178d93d215838c551d48e64": { + "balance": "0xad6f98593bd8f0000" + }, + "44c14765127cde11fab46c5d2cf4d4b2890023fd": { + "balance": "0x6c6b935b8bbd400000" + }, + "44c54eaa8ac940f9e80f1e74e82fc14f1676856a": { + "balance": "0x1ab2cf7c9f87e200000" + }, + "44cd77535a893fa7c4d5eb3a240e79d099a72d2d": { + "balance": "0x2c73c937742c500000" + }, + "44dfba50b829becc5f4f14d1b04aab3320a295e5": { + "balance": "0x3635c9adc5dea00000" + }, + "44e2fdc679e6bee01e93ef4a3ab1bcce012abc7c": { + "balance": "0x163d194900c5458000" + }, + "44f62f2aaabc29ad3a6b04e1ff6f9ce452d1c140": { + "balance": "0x39992648a23c8a00000" + }, + "44fff37be01a3888d3b8b8e18880a7ddefeeead3": { + "balance": "0xe0c5bfc7dae9a8000" + }, + "4506fe19fa4b006baa3984529d8516db2b2b50ab": { + "balance": "0x6c6b935b8bbd400000" + }, + "451b3699475bed5d7905f8905aa3456f1ed788fc": { + "balance": "0x8ac7230489e8000000" + }, + "451b7070259bdba27100e36e23428a53dfe304e9": { + "balance": "0xb98bc829a6f90000" + }, + "45272b8f62e9f9fa8ce04420e1aea3eba9686eac": { + "balance": "0xd8d726b7177a800000" + }, + "452b64db8ef7d6df87c788639c2290be8482d575": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "453e359a3397944c5a275ab1a2f70a5e5a3f6989": { + "balance": "0xd02ab486cedc00000" + }, + "4549b15979255f7e65e99b0d5604db98dfcac8bf": { + "balance": "0xd8d726b7177a800000" + }, + "454b61b344c0ef965179238155f277c3829d0b38": { + "balance": "0x6c6b935b8bbd400000" + }, + "454f0141d721d33cbdc41018bd01119aa4784818": { + "balance": "0x14542ba12a337c00000" + }, + "45533390e340fe0de3b3cf5fb9fc8ea552e29e62": { + "balance": "0x4f2591f896a6500000" + }, + "455396a4bbd9bae8af9fb7c4d64d471db9c24505": { + "balance": "0x8ba52e6fc45e40000" + }, + "455b9296921a74d1fc41617f43b8303e6f3ed76c": { + "balance": "0xe3aeb5737240a00000" + }, + "455cb8ee39ffbc752331e5aefc588ef0ee593454": { + "balance": "0x3635463a780def8000" + }, + "456ae0aca48ebcfae166060250525f63965e760f": { + "balance": "0x1043561a8829300000" + }, + "456f8d746682b224679349064d1b368c7c05b176": { + "balance": "0xc893d09c8f51500000" + }, + "457029c469c4548d168cec3e65872e4428d42b67": { + "balance": "0x6c6b935b8bbd400000" + }, + "4571de672b9904bad8743692c21c4fdcea4c2e01": { + "balance": "0xd8d726b7177a800000" + }, + "45781bbe7714a1c8f73b1c747921df4f84278b70": { + "balance": "0x6c6b935b8bbd400000" + }, + "457bcef37dd3d60b2dd019e3fe61d46b3f1e7252": { + "balance": "0x1158e460913d00000" + }, + "458e3cc99e947844a18e6a42918fef7e7f5f5eb3": { + "balance": "0x7b53f79e888dac00000" + }, + "459393d63a063ef3721e16bd9fde45ee9dbd77fb": { + "balance": "0x6abad6a3c153050000" + }, + "45a570dcc2090c86a6b3ea29a60863dde41f13b5": { + "balance": "0xc9a95ee2986520000" + }, + "45a820a0672f17dc74a08112bc643fd1167736c3": { + "balance": "0xad6c43b2815ed8000" + }, + "45b47105fe42c4712dce6e2a21c05bffd5ea47a9": { + "balance": "0x6c6b935b8bbd400000" + }, + "45bb829652d8bfb58b8527f0ecb621c29e212ec3": { + "balance": "0x6c6b935b8bbd400000" + }, + "45c0d19f0b8e054f9e893836d5ecae7901af2812": { + "balance": "0x10f0cf064dd59200000" + }, + "45c4ecb4ee891ea984a7c5cefd8dfb00310b2850": { + "balance": "0x6b56051582a9700000" + }, + "45ca8d956608f9e00a2f9974028640888465668f": { + "balance": "0x6c6b935b8bbd400000" + }, + "45ca9862003b4e40a3171fb5cafa9028cac8de19": { + "balance": "0x2eb8eb1a172dcb80000" + }, + "45d1c9eedf7cab41a779057b79395f5428d80528": { + "balance": "0x6c6b935b8bbd400000" + }, + "45d4b54d37a8cf599821235f062fa9d170ede8a4": { + "balance": "0x1190673b5fda900000" + }, + "45db03bccfd6a5f4d0266b82a22a368792c77d83": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "45e3a93e72144ada860cbc56ff85145ada38c6da": { + "balance": "0x57473d05dabae80000" + }, + "45e68db8dbbaba5fc2cb337c62bcd0d61b059189": { + "balance": "0x6c6b935b8bbd400000" + }, + "45e68db94c7d0ab7ac41857a71d67147870f4e71": { + "balance": "0x54b40b1f852bda000000" + }, + "45f4fc60f08eaca10598f0336329801e3c92cb46": { + "balance": "0xad78ebc5ac6200000" + }, + "460d5355b2ceeb6e62107d81e51270b26bf45620": { + "balance": "0x6cb7e74867d5e60000" + }, + "46224f32f4ece5c8867090d4409d55e50b18432d": { + "balance": "0x14542ba12a337c00000" + }, + "4627c606842671abde8295ee5dd94c7f549534f4": { + "balance": "0xf895fbd8732f40000" + }, + "462b678b51b584f3ed7ada070b5cd99c0bf7b87f": { + "balance": "0x56bc75e2d63100000" + }, + "464d9c89cce484df000277198ed8075fa63572d1": { + "balance": "0x1158e460913d00000" + }, + "46504e6a215ac83bccf956befc82ab5a679371c8": { + "balance": "0x1c212805c2b4a50000" + }, + "4651dc420e08c3293b27d2497890eb50223ae2f4": { + "balance": "0x43c33c1937564800000" + }, + "46531e8b1bde097fdf849d6d119885608a008df7": { + "balance": "0xad78ebc5ac6200000" + }, + "466292f0e80d43a78774277590a9eb45961214f4": { + "balance": "0x34957444b840e80000" + }, + "4662a1765ee921842ddc88898d1dc8627597bd7e": { + "balance": "0x21e19e0c9bab2400000" + }, + "4665e47396c7db97eb2a03d90863d5d4ba319a94": { + "balance": "0x2086ac351052600000" + }, + "466fda6b9b58c5532750306a10a2a8c768103b07": { + "balance": "0xad6eedd17cf3b8000" + }, + "467124ae7f452f26b3d574f6088894fa5d1cfb3b": { + "balance": "0x925e06eec972b00000" + }, + "46722a36a01e841d03f780935e917d85d5a67abd": { + "balance": "0xcec76f0e71520000" + }, + "46779a5656ff00d73eac3ad0c38b6c853094fb40": { + "balance": "0xc8253c96c6af00000" + }, + "4677b04e0343a32131fd6abb39b1b6156bba3d5b": { + "balance": "0xad78ebc5ac6200000" + }, + "467d5988249a68614716659840ed0ae6f6f457bc": { + "balance": "0x1501a48cefdfde0000" + }, + "467e0ed54f3b76ae0636176e07420815a021736e": { + "balance": "0x6c6b935b8bbd400000" + }, + "467ea10445827ef1e502daf76b928a209e0d4032": { + "balance": "0x6c6b935b8bbd400000" + }, + "467fbf41441600757fe15830c8cd5f4ffbbbd560": { + "balance": "0x21e19e0c9bab2400000" + }, + "469358709332c82b887e20bcddd0220f8edba7d0": { + "balance": "0x3a9d5baa4abf1d00000" + }, + "4697baaf9ccb603fd30430689d435445e9c98bf5": { + "balance": "0xad201a6794ff80000" + }, + "46a30b8a808931217445c3f5a93e882c0345b426": { + "balance": "0xd8db5ebd7b2638000" + }, + "46a430a2d4a894a0d8aa3feac615361415c3f81f": { + "balance": "0x6c6b935b8bbd400000" + }, + "46aa501870677e7f0a504876b4e8801a0ad01c46": { + "balance": "0x2b5e3af16b18800000" + }, + "46bfc5b207eb2013e2e60f775fecd71810c5990c": { + "balance": "0x54069233bf7f780000" + }, + "46c1aa2244b9c8a957ca8fac431b0595a3b86824": { + "balance": "0xd8d726b7177a800000" + }, + "46d80631284203f6288ecd4e5758bb9d41d05dbe": { + "balance": "0x6c6b935b8bbd400000" + }, + "470ac5d1f3efe28f3802af925b571e63868b397d": { + "balance": "0x6c6b935b8bbd400000" + }, + "471010da492f4018833b088d9872901e06129174": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "4712540265cbeec3847022c59f1b318d43400a9e": { + "balance": "0xbdbc41e0348b300000" + }, + "4714cfa4f46bd6bd70737d75878197e08f88e631": { + "balance": "0x27f3edfb34e6e400000" + }, + "472048cc609aeb242165eaaa8705850cf3125de0": { + "balance": "0x3635c9adc5dea00000" + }, + "47219229e8cd56659a65c2a943e2dd9a8f4bfd89": { + "balance": "0x52663ccab1e1c00000" + }, + "4737d042dc6ae73ec73ae2517acea2fdd96487c5": { + "balance": "0x3635c9adc5dea00000" + }, + "474158a1a9dc693c133f65e47b5c3ae2f773a86f": { + "balance": "0xada55474b81340000" + }, + "4745ab181a36aa8cbf2289d0c45165bc7ebe2381": { + "balance": "0x222c8eb3ff6640000" + }, + "475066f9ad26655196d5535327bbeb9b7929cb04": { + "balance": "0xa4cc799563c3800000" + }, + "4752218e54de423f86c0501933917aea08c8fed5": { + "balance": "0x43c33c1937564800000" + }, + "475a6193572d4a4e59d7be09cb960ddd8c530e2f": { + "balance": "0x242cf78cdf07ff8000" + }, + "47648bed01f3cd3249084e635d14daa9e7ec3c8a": { + "balance": "0xa844a7424d9c80000" + }, + "47688410ff25d654d72eb2bc06e4ad24f833b094": { + "balance": "0x8b28d61f3d3ac0000" + }, + "476b5599089a3fb6f29c6c72e49b2e4740ea808d": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "47730f5f8ebf89ac72ef80e46c12195038ecdc49": { + "balance": "0xab4dcf399a3a600000" + }, + "477b24eee8839e4fd19d1250bd0b6645794a61ca": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "4781a10a4df5eebc82f4cfe107ba1d8a7640bd66": { + "balance": "0x61093d7c2c6d380000" + }, + "47885ababedf4d928e1c3c71d7ca40d563ed595f": { + "balance": "0x62a992e53a0af00000" + }, + "478dc09a1311377c093f9cc8ae74111f65f82f39": { + "balance": "0xd8d726b7177a800000" + }, + "478e524ef2a381d70c82588a93ca7a5fa9d51cbf": { + "balance": "0x35fa97226f8899700000" + }, + "479298a9de147e63a1c7d6d2fce089c7e64083bd": { + "balance": "0x21e19dd3c3c0d798000" + }, + "479abf2da4d58716fd973a0d13a75f530150260a": { + "balance": "0x1158e460913d00000" + }, + "47a281dff64167197855bf6e705eb9f2cef632ea": { + "balance": "0x3636c9796436740000" + }, + "47beb20f759100542aa93d41118b3211d664920e": { + "balance": "0x6c6b935b8bbd400000" + }, + "47c247f53b9fbeb17bba0703a00c009fdb0f6eae": { + "balance": "0x43c33c1937564800000" + }, + "47c7e5efb48b3aed4b7c6e824b435f357df4c723": { + "balance": "0xfc936392801c0000" + }, + "47cf9cdaf92fc999cc5efbb7203c61e4f1cdd4c3": { + "balance": "0x71f8a93d01e540000" + }, + "47d20e6ae4cad3f829eac07e5ac97b66fdd56cf5": { + "balance": "0x3635c9adc5dea00000" + }, + "47d792a756779aedf1343e8883a6619c6c281184": { + "balance": "0x6c6b935b8bbd400000" + }, + "47e25df8822538a8596b28c637896b4d143c351d": { + "balance": "0x110be9eb24b881500000" + }, + "47f4696bd462b20da09fb83ed2039818d77625b3": { + "balance": "0x813ca56906d340000" + }, + "47fef58584465248a0810d60463ee93e5a6ee8d3": { + "balance": "0xf58cd3e1269160000" + }, + "47ff6feb43212060bb1503d7a397fc08f4e70352": { + "balance": "0x6c6b935b8bbd400000" + }, + "47fff42c678551d141eb75a6ee398117df3e4a8d": { + "balance": "0x56beae51fd2d10000" + }, + "48010ef3b8e95e3f308f30a8cb7f4eb4bf60d965": { + "balance": "0x6c6b935b8bbd400000" + }, + "480af52076009ca73781b70e43b95916a62203ab": { + "balance": "0x321972f4083d878000" + }, + "480f31b989311e4124c6a7465f5a44094d36f9d0": { + "balance": "0x3790bb855137640000" + }, + "481115296ab7db52492ff7b647d63329fb5cbc6b": { + "balance": "0x368c8623a8b4d100000" + }, + "481e3a91bfdc2f1c8428a0119d03a41601417e1c": { + "balance": "0x3635c9adc5dea00000" + }, + "4828e4cbe34e1510afb72c2beeac8a4513eaebd9": { + "balance": "0xd5967be4fc3f100000" + }, + "482982ac1f1c6d1721feecd9b9c96cd949805055": { + "balance": "0x21e19e0c9bab2400000" + }, + "48302c311ef8e5dc664158dd583c81194d6e0d58": { + "balance": "0xb6676ce0bccb5c0000" + }, + "483ba99034e900e3aedf61499d3b2bce39beb7aa": { + "balance": "0x35659ef93f0fc40000" + }, + "48548b4ba62bcb2f0d34a88dc69a680e539cf046": { + "balance": "0x56cf1cbbb74320000" + }, + "4863849739265a63b0a2bf236a5913e6f959ce15": { + "balance": "0x52663ccab1e1c00000" + }, + "48659d8f8c9a2fd44f68daa55d23a608fbe500dc": { + "balance": "0x6c6b935b8bbd400000" + }, + "48669eb5a801d8b75fb6aa58c3451b7058c243bf": { + "balance": "0x68d42c138dab9f00000" + }, + "486a6c8583a84484e3df43a123837f8c7e2317d0": { + "balance": "0x1187c571ab80450000" + }, + "487adf7d70a6740f8d51cbdd68bb3f91c4a5ce68": { + "balance": "0x39fbae8d042dd0000" + }, + "487e108502b0b189ef9c8c6da4d0db6261eec6c0": { + "balance": "0x678a932062e4180000" + }, + "4888fb25cd50dbb9e048f41ca47d78b78a27c7d9": { + "balance": "0x3a9d5baa4abf1d00000" + }, + "489334c2b695c8ee0794bd864217fb9fd8f8b135": { + "balance": "0xfc936392801c0000" + }, + "48a30de1c919d3fd3180e97d5f2b2a9dbd964d2d": { + "balance": "0x2629f66e0c5300000" + }, + "48bf14d7b1fc84ebf3c96be12f7bce01aa69b03e": { + "balance": "0x68155a43676e00000" + }, + "48c2ee91a50756d8ce9abeeb7589d22c6fee5dfb": { + "balance": "0xae8e7a0bb575d00000" + }, + "48c5c6970b9161bb1c7b7adfed9cdede8a1ba864": { + "balance": "0xd8d726b7177a800000" + }, + "48d2434b7a7dbbff08223b6387b05da2e5093126": { + "balance": "0x3cfc82e37e9a7400000" + }, + "48d4f2468f963fd79a006198bb67895d2d5aa4d3": { + "balance": "0x4be4e7267b6ae00000" + }, + "48e0cbd67f18acdb7a6291e1254db32e0972737f": { + "balance": "0x56be03ca3e47d8000" + }, + "48f60a35484fe7792bcc8a7b6393d0dda1f6b717": { + "balance": "0xc328093e61ee400000" + }, + "48f883e567b436a27bb5a3124dbc84dec775a800": { + "balance": "0x29d76e869dcd800000" + }, + "490145afa8b54522bb21f352f06da5a788fa8f1d": { + "balance": "0x1f46c62901a03fb0000" + }, + "4909b31998ead414b8fb0e846bd5cbde393935be": { + "balance": "0xd8d726b7177a800000" + }, + "4912d902931676ff39fc34fe3c3cc8fb2182fa7a": { + "balance": "0x1158e460913d00000" + }, + "49136fe6e28b7453fcb16b6bbbe9aaacba8337fd": { + "balance": "0x6c6b935b8bbd400000" + }, + "491561db8b6fafb9007e62d050c282e92c4b6bc8": { + "balance": "0x65a4da25d3016c00000" + }, + "49185dd7c23632f46c759473ebae966008cd3598": { + "balance": "0xdc55fdb17647b0000" + }, + "492cb5f861b187f9df21cd4485bed90b50ffe22d": { + "balance": "0x1b19e50b44977c0000" + }, + "492de46aaf8f1d708d59d79af1d03ad2cb60902f": { + "balance": "0x6c6b935b8bbd400000" + }, + "492e70f04d18408cb41e25603730506b35a2876b": { + "balance": "0x222c8eb3ff6640000" + }, + "493a67fe23decc63b10dda75f3287695a81bd5ab": { + "balance": "0x2fb474098f67c00000" + }, + "493d48bda015a9bfcf1603936eab68024ce551e0": { + "balance": "0x138a388a43c000000" + }, + "494256e99b0f9cd6e5ebca3899863252900165c8": { + "balance": "0x2f6f10780d22cc00000" + }, + "494dec4d5ee88a2771a815f1ee7264942fb58b28": { + "balance": "0x6c6b935b8bbd400000" + }, + "495b641b1cdea362c3b4cbbd0f5cc50b1e176b9c": { + "balance": "0x3635c9adc5dea00000" + }, + "4968a2cedb457555a139295aea28776e54003c87": { + "balance": "0x2231aefc9a6628f0000" + }, + "496d365534530a5fc1577c0a5241cb88c4da7072": { + "balance": "0x61093d7c2c6d380000" + }, + "496e319592b341eaccd778dda7c8196d54cac775": { + "balance": "0x1f5718987664b480000" + }, + "496f5843f6d24cd98d255e4c23d1e1f023227545": { + "balance": "0x5f179fd4a6ee098000" + }, + "4970d3acf72b5b1f32a7003cf102c64ee0547941": { + "balance": "0x1da56a4b0835bf800000" + }, + "4977a7939d0939689455ce2639d0ee5a4cd910ed": { + "balance": "0x62a992e53a0af00000" + }, + "4979194ec9e97db9bee8343b7c77d9d7f3f1dc9f": { + "balance": "0x1158e460913d00000" + }, + "49793463e1681083d6abd6e725d5bba745dccde8": { + "balance": "0x1d98e94c4e471f0000" + }, + "4981c5ff66cc4e9680251fc4cd2ff907cb327865": { + "balance": "0x28a857425466f80000" + }, + "49897fe932bbb3154c95d3bce6d93b6d732904dd": { + "balance": "0xd8d726b7177a800000" + }, + "4989e1ab5e7cd00746b3938ef0f0d064a2025ba5": { + "balance": "0x6c6b935b8bbd400000" + }, + "498abdeb14c26b7b7234d70fceaef361a76dff72": { + "balance": "0xa2a15d09519be00000" + }, + "49a645e0667dfd7b32d075cc2467dd8c680907c4": { + "balance": "0x70601958fcb9c0000" + }, + "49b74e169265f01a89ec4c9072c5a4cd72e4e835": { + "balance": "0x368c8623a8b4d100000" + }, + "49bdbc7ba5abebb6389e91a3285220d3451bd253": { + "balance": "0x3635c9adc5dea00000" + }, + "49c941e0e5018726b7290fc473b471d41dae80d1": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "49c9771fca19d5b9d245c891f8158fe49f47a062": { + "balance": "0x21e19e0c9bab2400000" + }, + "49cf1e54be363106b920729d2d0ba46f0867989a": { + "balance": "0xe873f44133cb00000" + }, + "49d2c28ee9bc545eaaf7fd14c27c4073b4bb5f1a": { + "balance": "0x4fe9b806b40daf0000" + }, + "49ddee902e1d0c99d1b11af3cc8a96f78e4dcf1a": { + "balance": "0xacea5e4c18c530000" + }, + "49f028395b5a86c9e07f7778630e4c2e3d373a77": { + "balance": "0x6a74a5038db918000" + }, + "4a192035e2619b24b0709d56590e9183ccf2c1d9": { + "balance": "0x21e19e0c9bab2400000" + }, + "4a4053b31d0ee5dbafb1d06bd7ac7ff3222c47d6": { + "balance": "0x4be4e7267b6ae00000" + }, + "4a430170152de5172633dd8262d107a0afd96a0f": { + "balance": "0xab4dcf399a3a600000" + }, + "4a47fc3e177f567a1e3893e000e36bba23520ab8": { + "balance": "0x6c6b935b8bbd400000" + }, + "4a52bad20357228faa1e996bed790c93674ba7d0": { + "balance": "0x487a9a304539440000" + }, + "4a53dcdb56ce4cdce9f82ec0eb13d67352e7c88b": { + "balance": "0xe3aeb5737240a00000" + }, + "4a5fae3b0372c230c125d6d470140337ab915656": { + "balance": "0x56bc75e2d631000000" + }, + "4a719061f5285495b37b9d7ef8a51b07d6e6acac": { + "balance": "0xad4c8316a0b0c0000" + }, + "4a73389298031b8816cca946421c199e18b343d6": { + "balance": "0x223868b879146f0000" + }, + "4a735d224792376d331367c093d31c8794341582": { + "balance": "0x66ffcbfd5e5a300000" + }, + "4a7494cce44855cc80582842be958a0d1c0072ee": { + "balance": "0x821ab0d44149800000" + }, + "4a75c3d4fa6fccbd5dd5a703c15379a1e783e9b7": { + "balance": "0x62a992e53a0af00000" + }, + "4a81abe4984c7c6bef63d69820e55743c61f201c": { + "balance": "0x36401004e9aa3470000" + }, + "4a82694fa29d9e213202a1a209285df6e745c209": { + "balance": "0xd8d726b7177a800000" + }, + "4a835c25824c47ecbfc79439bf3f5c3481aa75cd": { + "balance": "0x4be4e7267b6ae00000" + }, + "4a918032439159bb315b6725b6830dc83697739f": { + "balance": "0x12a32ef678334c0000" + }, + "4a97e8fcf4635ea7fc5e96ee51752ec388716b60": { + "balance": "0x1d9945ab2b03480000" + }, + "4a9a26fd0a8ba10f977da4f77c31908dab4a8016": { + "balance": "0x61093d7c2c6d380000" + }, + "4aa148c2c33401e66a2b586e6577c4b292d3f240": { + "balance": "0xbb860b285f7740000" + }, + "4aa693b122f314482a47b11cc77c68a497876162": { + "balance": "0x6acb3df27e1f880000" + }, + "4ab2d34f04834fbf7479649cab923d2c4725c553": { + "balance": "0xbed1d0263d9f000000" + }, + "4ac07673e42f64c1a25ec2fa2d86e5aa2b34e039": { + "balance": "0x6c6b935b8bbd400000" + }, + "4ac5acad000b8877214cb1ae00eac9a37d59a0fd": { + "balance": "0xd8d726b7177a800000" + }, + "4ac9905a4cb6ab1cfd62546ee5917300b87c4fde": { + "balance": "0x3708baed3d68900000" + }, + "4acfa9d94eda6625c9dfa5f9f4f5d107c4031fdf": { + "balance": "0x222c8eb3ff6640000" + }, + "4ad047fae67ef162fe68fedbc27d3b65caf10c36": { + "balance": "0x6acb3df27e1f880000" + }, + "4ad95d188d6464709add2555fb4d97fe1ebf311f": { + "balance": "0x12c1b6eed03d280000" + }, + "4adbf4aae0e3ef44f7dd4d8985cfaf096ec48e98": { + "balance": "0x821ab0d4414980000" + }, + "4ae2a04d3909ef454e544ccfd614bfefa71089ae": { + "balance": "0x1801159df1eef80000" + }, + "4ae93082e45187c26160e66792f57fad3551c73a": { + "balance": "0x4961520daff82280000" + }, + "4af0db077bb9ba5e443e21e148e59f379105c592": { + "balance": "0x2086ac351052600000" + }, + "4b0619d9d8aa313a9531ac7dbe04ca0d6a5ad1b6": { + "balance": "0x6c6b935b8bbd400000" + }, + "4b0bd8acfcbc53a6010b40d4d08ddd2d9d69622d": { + "balance": "0x243d4d18229ca20000" + }, + "4b19eb0c354bc1393960eb06063b83926f0d67b2": { + "balance": "0x19274b259f6540000" + }, + "4b29437c97b4a844be71cca3b648d4ca0fdd9ba4": { + "balance": "0x824719834cfac0000" + }, + "4b31bf41abc75c9ae2cd8f7f35163b6e2b745054": { + "balance": "0x14b550a013c7380000" + }, + "4b3a7cc3a7d7b00ed5282221a60259f25bf6538a": { + "balance": "0x3635c9adc5dea00000" + }, + "4b3aab335ebbfaa870cc4d605e7d2e74c668369f": { + "balance": "0xcb49b44ba602d800000" + }, + "4b3c7388cc76da3d62d40067dabccd7ef0433d23": { + "balance": "0x56cd55fc64dfe0000" + }, + "4b3dfbdb454be5279a3b8addfd0ed1cd37a9420d": { + "balance": "0x6c6b935b8bbd400000" + }, + "4b470f7ba030bc7cfcf338d4bf0432a91e2ea5ff": { + "balance": "0x6c6b935b8bbd400000" + }, + "4b53ae59c784b6b5c43616b9a0809558e684e10c": { + "balance": "0x410d586a20a4c00000" + }, + "4b58101f44f7e389e12d471d1635b71614fdd605": { + "balance": "0x8ac7230489e800000" + }, + "4b5cdb1e428c91dd7cb54a6aed4571da054bfe52": { + "balance": "0x4c53ecdc18a600000" + }, + "4b60a3e253bf38c8d5662010bb93a473c965c3e5": { + "balance": "0x50c5e761a444080000" + }, + "4b74f5e58e2edf76daf70151964a0b8f1de0663c": { + "balance": "0x1190ae4944ba120000" + }, + "4b762166dd1118e84369f804c75f9cd657bf730c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "4b792e29683eb586e394bb33526c6001b397999e": { + "balance": "0x2086ac351052600000" + }, + "4b904e934bd0cc8b20705f879e905b93ea0ccc30": { + "balance": "0x6c6b935b8bbd400000" + }, + "4b9206ba6b549a1a7f969e1d5dba867539d1fa67": { + "balance": "0x1ab2cf7c9f87e200000" + }, + "4b984ef26c576e815a2eaed2f5177f07dbb1c476": { + "balance": "0x54915956c409600000" + }, + "4b9e068fc4680976e61504912985fd5ce94bab0d": { + "balance": "0x243d4d18229ca20000" + }, + "4ba0d9e89601772b496847a2bb4340186787d265": { + "balance": "0x3635c9adc5dea00000" + }, + "4ba53ab549e2016dfa223c9ed5a38fad91288d07": { + "balance": "0x4be4e7267b6ae00000" + }, + "4ba8e0117fc0b6a3e56b24a3a58fe6cef442ff98": { + "balance": "0x131beb925ffd3200000" + }, + "4bac846af4169f1d95431b341d8800b22180af1a": { + "balance": "0x1158e460913d00000" + }, + "4bb6d86b8314c22d8d37ea516d0019f156aae12d": { + "balance": "0x3635c9adc5dea00000" + }, + "4bb9655cfb2a36ea7c637a7b859b4a3154e26ebe": { + "balance": "0x3635c9adc5dea000000" + }, + "4bbcbf38b3c90163a84b1cd2a93b58b2a3348d87": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "4bd6dd0cff23400e1730ba7b894504577d14e74a": { + "balance": "0x2ba0ccddd0df73b00000" + }, + "4be8628a8154874e048d80c142181022b180bcc1": { + "balance": "0x340aad21b3b700000" + }, + "4be90d412129d5a4d0424361d6649d4e47a62316": { + "balance": "0x3708baed3d68900000" + }, + "4bea288eea42c4955eb9faad2a9faf4783cbddac": { + "balance": "0x618be1663c4af490000" + }, + "4bf4479799ef82eea20943374f56a1bf54001e5e": { + "balance": "0xd5967be4fc3f100000" + }, + "4bf8bf1d35a231315764fc8001809a949294fc49": { + "balance": "0x39fbae8d042dd0000" + }, + "4bf8e26f4c2790da6533a2ac9abac3c69a199433": { + "balance": "0xad78ebc5ac6200000" + }, + "4c0aca508b3caf5ee028bc707dd1e800b838f453": { + "balance": "0xfc936392801c0000" + }, + "4c0b1515dfced7a13e13ee12c0f523ae504f032b": { + "balance": "0xa968163f0a57b400000" + }, + "4c13980c32dcf3920b78a4a7903312907c1b123f": { + "balance": "0x3410015faae0c0000" + }, + "4c1579af3312e4f88ae93c68e9449c2e9a68d9c4": { + "balance": "0x6c6b935b8bbd400000" + }, + "4c23b370fc992bb67cec06e26715b62f0b3a4ac3": { + "balance": "0x21e19e0c9bab2400000" + }, + "4c24b78baf2bafc7fcc69016426be973e20a50b2": { + "balance": "0xa2a15d09519be00000" + }, + "4c2f1afef7c5868c44832fc77cb03b55f89e6d6e": { + "balance": "0x43c33c1937564800000" + }, + "4c377bb03ab52c4cb79befa1dd114982924c4ae9": { + "balance": "0x631603ccd38dd70000" + }, + "4c3e95cc3957d252ce0bf0c87d5b4f2234672e70": { + "balance": "0x878678326eac900000" + }, + "4c423c76930d07f93c47a5cc4f615745c45a9d72": { + "balance": "0x56bc75e2d63100000" + }, + "4c45d4c9a725d11112bfcbca00bf31186ccaadb7": { + "balance": "0x15af1d78b58c400000" + }, + "4c4e6f13fb5e3f70c3760262a03e317982691d10": { + "balance": "0x56bc75e2d63100000" + }, + "4c5afe40f18ffc48d3a1aec41fc29de179f4d297": { + "balance": "0x6c6b935b8bbd400000" + }, + "4c5b3dc0e2b9360f91289b1fe13ce12c0fbda3e1": { + "balance": "0x6c6b935b8bbd400000" + }, + "4c666b86f1c5ee8ca41285f5bde4f79052081406": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "4c696be99f3a690440c3436a59a7d7e937d6ba0d": { + "balance": "0xbb9125542263900000" + }, + "4c6a248fc97d705def495ca20759169ef0d36471": { + "balance": "0x29331e6558f0e00000" + }, + "4c6a9dc2cab10abb2e7c137006f08fecb5b779e1": { + "balance": "0x1b0d04202f47ec0000" + }, + "4c6b93a3bec16349540cbfcae96c9621d6645010": { + "balance": "0x6c6b935b8bbd400000" + }, + "4c759813ad1386bed27ffae9e4815e3630cca312": { + "balance": "0x6c6b935b8bbd400000" + }, + "4c760cd9e195ee4f2d6bce2500ff96da7c43ee91": { + "balance": "0xcb49b44ba602d800000" + }, + "4c767b65fd91161f4fbdcc6a69e2f6ad711bb918": { + "balance": "0x270801d946c9400000" + }, + "4c7e2e2b77ad0cd6f44acb2861f0fb8b28750ef9": { + "balance": "0x1158e460913d00000" + }, + "4c85ed362f24f6b9f04cdfccd022ae535147cbb9": { + "balance": "0x5150ae84a8cdf00000" + }, + "4c935bb250778b3c4c7f7e07fc251fa630314aab": { + "balance": "0x5150ae84a8cdf00000" + }, + "4c997992036c5b433ac33d25a8ea1dc3d4e4e6d8": { + "balance": "0x1953b3d4ab1680000" + }, + "4c99dae96481e807c1f99f8b7fbde29b7547c5bf": { + "balance": "0x821ab0d4414980000" + }, + "4c9a862ad115d6c8274ed0b944bdd6a5500510a7": { + "balance": "0x56bc75e2d63100000" + }, + "4ca783b556e5bf53aa13c8116613d65782c9b642": { + "balance": "0x5561840b4ad83c00000" + }, + "4ca7b717d9bc8793b04e051a8d23e1640f5ba5e3": { + "balance": "0x43b514549ecf620000" + }, + "4ca8db4a5efefc80f4cd9bbcccb03265931332b6": { + "balance": "0xad78ebc5ac6200000" + }, + "4cac91fb83a147d2f76c3267984b910a79933348": { + "balance": "0x75792a8abdef7c0000" + }, + "4cadf573ce4ceec78b8e1b21b0ed78eb113b2c0e": { + "balance": "0x6c6b935b8bbd400000" + }, + "4cb5c6cd713ca447b848ae2f56b761ca14d7ad57": { + "balance": "0xe7eeba3410b740000" + }, + "4cc22c9bc9ad05d875a397dbe847ed221c920c67": { + "balance": "0x6c6b935b8bbd400000" + }, + "4cd0b0a6436362595ceade052ebc9b929fb6c6c0": { + "balance": "0x6c6b935b8bbd400000" + }, + "4cda41dd533991290794e22ae324143e309b3d3d": { + "balance": "0x821ab0d44149800000" + }, + "4cee901b4ac8b156c5e2f8a6f1bef572a7dceb7e": { + "balance": "0x3635c9adc5dea00000" + }, + "4cefbe2398e47d52e78db4334c8b697675f193ae": { + "balance": "0xd96fce90cfabcc0000" + }, + "4cf5537b85842f89cfee359eae500fc449d2118f": { + "balance": "0x3635c9adc5dea00000" + }, + "4d08471d68007aff2ae279bc5e3fe4156fbbe3de": { + "balance": "0x878678326eac9000000" + }, + "4d200110124008d56f76981256420c946a6ff45c": { + "balance": "0xad6eedd17cf3b8000" + }, + "4d24b7ac47d2f27de90974ba3de5ead203544bcd": { + "balance": "0x56bc75e2d63100000" + }, + "4d29fc523a2c1629532121da9998e9b5ab9d1b45": { + "balance": "0xdb44e049bb2c0000" + }, + "4d38d90f83f4515c03cc78326a154d358bd882b7": { + "balance": "0xa076407d3f7440000" + }, + "4d4cf5807429615e30cdface1e5aae4dad3055e6": { + "balance": "0x2086ac351052600000" + }, + "4d57e716876c0c95ef5eaebd35c8f41b069b6bfe": { + "balance": "0x6c6b935b8bbd400000" + }, + "4d67f2ab8599fef5fc413999aa01fd7fce70b43d": { + "balance": "0x21e19e0c9bab2400000" + }, + "4d6e8fe109ccd2158e4db114132fe75fecc8be5b": { + "balance": "0x15b3557f1937f8000" + }, + "4d71a6eb3d7f327e1834278e280b039eddd31c2f": { + "balance": "0x14542ba12a337c00000" + }, + "4d7cfaa84cb33106800a8c802fb8aa463896c599": { + "balance": "0x61093d7c2c6d380000" + }, + "4d801093c19ca9b8f342e33cc9c77bbd4c8312cf": { + "balance": "0x12b3e7fb95cda48000" + }, + "4d828894752f6f25175daf2177094487954b6f9f": { + "balance": "0x4f212bc2c49c838000" + }, + "4d82d7700c123bb919419bbaf046799c6b0e2c66": { + "balance": "0x43c33c1937564800000" + }, + "4d836d9d3b0e2cbd4de050596faa490cffb60d5d": { + "balance": "0x1043561a8829300000" + }, + "4d8697af0fbf2ca36e8768f4af22133570685a60": { + "balance": "0x1158e460913d00000" + }, + "4d9279962029a8bd45639737e98b511eff074c21": { + "balance": "0x487a9a304539440000" + }, + "4d93696fa24859f5d2939aebfa54b4b51ae1dccc": { + "balance": "0x10910d4cdc9f60000" + }, + "4d9c77d0750c5e6fbc247f2fd79274686cb353d6": { + "balance": "0x1158e460913d00000" + }, + "4da5edc688b0cb62e1403d1700d9dcb99ffe3fd3": { + "balance": "0x6c6b935b8bbd400000" + }, + "4da8030769844bc34186b85cd4c7348849ff49e9": { + "balance": "0x21e19e0c9bab2400000" + }, + "4db1c43a0f834d7d0478b8960767ec1ac44c9aeb": { + "balance": "0x2f5181305627370000" + }, + "4db21284bcd4f787a7556500d6d7d8f36623cf35": { + "balance": "0x6928374f77a3630000" + }, + "4dc3da13b2b4afd44f5d0d3189f444d4ddf91b1b": { + "balance": "0x6c6b935b8bbd400000" + }, + "4dc4bf5e7589c47b28378d7503cf96488061dbbd": { + "balance": "0x5f68e8131ecf800000" + }, + "4dc9d5bb4b19cecd94f19ec25d200ea72f25d7ed": { + "balance": "0x6c6b935b8bbd400000" + }, + "4dcd11815818ae29b85d01367349a8a7fb12d06b": { + "balance": "0x1ac4286100191f00000" + }, + "4dcf62a3de3f061db91498fd61060f1f6398ff73": { + "balance": "0x6c6acc67d7b1d40000" + }, + "4dd131c74a068a37c90aded4f309c2409f6478d3": { + "balance": "0x15af39e4aab2740000" + }, + "4ddda7586b2237b053a7f3289cf460dc57d37a09": { + "balance": "0x21e19e0c9bab2400000" + }, + "4de3fe34a6fbf634c051997f47cc7f48791f5824": { + "balance": "0x6c5db2a4d815dc0000" + }, + "4df140ba796585dd5489315bca4bba680adbb818": { + "balance": "0x90f534608a72880000" + }, + "4e020779b5ddd3df228a00cb48c2fc979da6ae38": { + "balance": "0x6c6b935b8bbd400000" + }, + "4e0bd32473c4c51bf25654def69f797c6b29a232": { + "balance": "0x56c95de8e8ca1d0000" + }, + "4e2225a1bb59bc88a2316674d333b9b0afca6655": { + "balance": "0x8670e9ec6598c0000" + }, + "4e2310191ead8d3bc6489873a5f0c2ec6b87e1be": { + "balance": "0x3635c9adc5dea00000" + }, + "4e232d53b3e6be8f895361d31c34d4762b12c82e": { + "balance": "0x5f68e8131ecf800000" + }, + "4e2bfa4a466f82671b800eee426ad00c071ba170": { + "balance": "0xd8d726b7177a800000" + }, + "4e3edad4864dab64cae4c5417a76774053dc6432": { + "balance": "0x2008fb478cbfa98000" + }, + "4e4318f5e13e824a54edfe30a7ed4f26cd3da504": { + "balance": "0x6c6b935b8bbd400000" + }, + "4e5b77f9066159e615933f2dda7477fa4e47d648": { + "balance": "0xad78ebc5ac6200000" + }, + "4e6600806289454acda330a2a3556010dfacade6": { + "balance": "0x14542ba12a337c00000" + }, + "4e73cf2379f124860f73d6d91bf59acc5cfc845b": { + "balance": "0x22ca3587cf4eb0000" + }, + "4e7aa67e12183ef9d7468ea28ad239c2eef71b76": { + "balance": "0x10afc1ade3b4ed40000" + }, + "4e7b54474d01fefd388dfcd53b9f662624418a05": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "4e892e8081bf36e488fddb3b2630f3f1e8da30d2": { + "balance": "0x28aba30752451fc0000" + }, + "4e8a6d63489ccc10a57f885f96eb04ecbb546024": { + "balance": "0x3eae3130ecc96900000" + }, + "4e8e47ae3b1ef50c9d54a38e14208c1abd3603c2": { + "balance": "0x7928db1276660c0000" + }, + "4e90ccb13258acaa9f4febc0a34292f95991e230": { + "balance": "0xdb44e049bb2c0000" + }, + "4ea56e1112641c038d0565a9c296c463afefc17e": { + "balance": "0x9ddc1e3b901180000" + }, + "4ea70f04313fae65c3ff224a055c3d2dab28dddf": { + "balance": "0x43c30fb0884a96c0000" + }, + "4eb1454b573805c8aca37edec7149a41f61202f4": { + "balance": "0x1043561a8829300000" + }, + "4eb87ba8788eba0df87e5b9bd50a8e45368091c1": { + "balance": "0x1158e460913d00000" + }, + "4ebc5629f9a6a66b2cf3363ac4895c0348e8bf87": { + "balance": "0x3637096c4bcc690000" + }, + "4ec768295eeabafc42958415e22be216cde77618": { + "balance": "0x33b1dbc39c5480000" + }, + "4ecc19948dd9cd87b4c7201ab48e758f28e7cc76": { + "balance": "0x1b1dab61d3aa640000" + }, + "4ed14d81b60b23fb25054d8925dfa573dcae6168": { + "balance": "0x126e72a69a50d00000" + }, + "4ee13c0d41200b46d19dee5c4bcec71d82bb8e38": { + "balance": "0x1abee13ccbeefaf8000" + }, + "4eead40aad8c73ef08fc84bc0a92c9092f6a36bf": { + "balance": "0x1731790534df20000" + }, + "4eebe80cb6f3ae5904f6f4b28d907f907189fcab": { + "balance": "0x6c6acc67d7b1d40000" + }, + "4eebf1205d0cc20cee6c7f8ff3115f56d48fba26": { + "balance": "0x10d3aa536e2940000" + }, + "4ef1c214633ad9c0703b4e2374a2e33e3e429291": { + "balance": "0x487a9a304539440000" + }, + "4efcd9c79fb4334ca6247b0a33bd9cc33208e272": { + "balance": "0x487a9a304539440000" + }, + "4f06246b8d4bd29661f43e93762201d286935ab1": { + "balance": "0x105394ffc4636110000" + }, + "4f152b2fb8659d43776ebb1e81673aa84169be96": { + "balance": "0x6c6b935b8bbd400000" + }, + "4f177f9d56953ded71a5611f393322c30279895c": { + "balance": "0xd55ef90a2da180000" + }, + "4f1a2da54a4c6da19d142412e56e815741db2325": { + "balance": "0x56bc75e2d63100000" + }, + "4f23b6b817ffa5c664acdad79bb7b726d30af0f9": { + "balance": "0x5f68e8131ecf800000" + }, + "4f26690c992b7a312ab12e1385d94acd58288e7b": { + "balance": "0x2f6f10780d22cc00000" + }, + "4f2b47e2775a1fa7178dad92985a5bbe493ba6d6": { + "balance": "0xad78ebc5ac6200000" + }, + "4f3a4854911145ea01c644044bdb2e5a960a982f": { + "balance": "0xd8d726b7177a800000" + }, + "4f3f2c673069ac97c2023607152981f5cd6063a0": { + "balance": "0x2086ac351052600000" + }, + "4f4a9be10cd5d3fb5de48c17be296f895690645b": { + "balance": "0x878678326eac9000000" + }, + "4f52ad6170d25b2a2e850eadbb52413ff2303e7f": { + "balance": "0xa4cc799563c3800000" + }, + "4f5801b1eb30b712d8a0575a9a71ff965d4f34eb": { + "balance": "0x1043561a8829300000" + }, + "4f5df5b94357de948604c51b7893cddf6076baad": { + "balance": "0xcbd47b6eaa8cc00000" + }, + "4f64a85e8e9a40498c0c75fceb0337fb49083e5e": { + "balance": "0x3635c9adc5dea00000" + }, + "4f67396d2553f998785f704e07a639197dd1948d": { + "balance": "0x104472521ba7380000" + }, + "4f6d4737d7a940382487264886697cf7637f8015": { + "balance": "0x5a87e7d7f5f6580000" + }, + "4f7330096f79ed264ee0127f5d30d2f73c52b3d8": { + "balance": "0x1b1a7a420ba00d0000" + }, + "4f767bc8794aef9a0a38fea5c81f14694ff21a13": { + "balance": "0x1bc433f23f83140000" + }, + "4f85bc1fc5cbc9c001e8f1372e07505370d8c71f": { + "balance": "0x32f51edbaaa3300000" + }, + "4f88dfd01091a45a9e2676021e64286cd36b8d34": { + "balance": "0x3635c9adc5dea00000" + }, + "4f8972838f70c903c9b6c6c46162e99d6216d451": { + "balance": "0xf9e89a0f2c56c80000" + }, + "4f8ae80238e60008557075ab6afe0a7f2e74d729": { + "balance": "0x56bc75e2d63100000" + }, + "4f8e8d274fb22a3fd36a47fe72980471544b3434": { + "balance": "0xad78ebc5ac6200000" + }, + "4f9ce2af9b8c5e42c6808a3870ec576f313545d1": { + "balance": "0x21e19e0c9bab2400000" + }, + "4fa3f32ef4086448b344d5f0a9890d1ce4d617c3": { + "balance": "0x5150ae84a8cdf00000" + }, + "4fa554ab955c249217386a4d3263bbf72895434e": { + "balance": "0x1154e53217ddb0000" + }, + "4fa983bb5e3073a8edb557effeb4f9fb1d60ef86": { + "balance": "0x56b9af57e575ec0000" + }, + "4faf90b76ecfb9631bf9022176032d8b2c207009": { + "balance": "0x36363b5d9a77700000" + }, + "4fc46c396e674869ad9481638f0013630c87caac": { + "balance": "0x3635c9adc5dea00000" + }, + "4fcc19ea9f4c57dcbce893193cfb166aa914edc5": { + "balance": "0x17b8baa7f19546a0000" + }, + "4fce8429ba49caa0369d1e494db57e89eab2ad39": { + "balance": "0x2a5a058fc295ed000000" + }, + "4fdac1aa517007e0089430b3316a1badd12c01c7": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "4fe56ab3bae1b0a44433458333c4b05a248f8241": { + "balance": "0x762d93d1dd6f900000" + }, + "4feb846be43041fd6b34202897943e3f21cb7f04": { + "balance": "0x482fe260cbca90000" + }, + "4fee50c5f988206b09a573469fb1d0b42ebb6dce": { + "balance": "0x6cee06ddbe15ec0000" + }, + "4ff676e27f681a982d8fd9d20e648b3dce05e945": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "4ff67fb87f6efba9279930cfbd1b7a343c79fade": { + "balance": "0x15af1d78b58c400000" + }, + "5006fe4c22173980f00c74342b39cd231c653129": { + "balance": "0x6c6b935b8bbd400000" + }, + "500c16352e901d48ba8d04e2c767121772790b02": { + "balance": "0x1a3a6824973098000" + }, + "500c902958f6421594d1b6ded712490d52ed6c44": { + "balance": "0x6acb3df27e1f880000" + }, + "500e34cde5bd9e2b71bb92d7cf55eee188d5fa0c": { + "balance": "0x121ea68c114e5100000" + }, + "5032e4bcf7932b49fdba377b6f1499636513cfc3": { + "balance": "0x56bc75e2d63100000" + }, + "50378af7ef54043f892ab7ce97d647793511b108": { + "balance": "0x11164759ffb320000" + }, + "503bdbd8bc421c32a443032deb2e3e4cd5ba8b4e": { + "balance": "0x6c6b935b8bbd400000" + }, + "504666ce8931175e11a5ed11c1dcaa06e57f4e66": { + "balance": "0x27f3edfb34e6e400000" + }, + "50584d9206a46ce15c301117ee28f15c30e60e75": { + "balance": "0xb9f65d00f63c0000" + }, + "505a33a18634dd4800693c67f48a1d693d4833f8": { + "balance": "0x18921b79941dcd00000" + }, + "505e4f7c275588c533a20ebd2ac13b409bbdea3c": { + "balance": "0xf43fc2c04ee00000" + }, + "5062e5134c612f12694dbd0e131d4ce197d1b6a4": { + "balance": "0x3635c9adc5dea00000" + }, + "506411fd79003480f6f2b6aac26b7ba792f094b2": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5067f4549afbfe884c59cbc12b96934923d45db0": { + "balance": "0x3635c9adc5dea00000" + }, + "50763add868fd7361178342fc055eaa2b95f6846": { + "balance": "0x39f9046e0898f0000" + }, + "508cf19119db70aa86454253da764a2cb1b2be1a": { + "balance": "0x3635c9adc5dea00000" + }, + "509982f56237ee458951047e0a2230f804e2e895": { + "balance": "0x3b4ad496106b7f00000" + }, + "509a20bc48e72be1cdaf9569c711e8648d957334": { + "balance": "0x6c6b935b8bbd400000" + }, + "509c8668036d143fb8ae70b11995631f3dfcad87": { + "balance": "0x3635c9adc5dea00000" + }, + "50ad187ab21167c2b6e78be0153f44504a07945e": { + "balance": "0x56cd55fc64dfe0000" + }, + "50b9fef0a1329b02d16506255f5a2db71ec92d1f": { + "balance": "0x47da821564085c0000" + }, + "50bb67c8b8d8bd0f63c4760904f2d333f400aace": { + "balance": "0x6c6b935b8bbd400000" + }, + "50bef2756248f9a7a380f91b051ba3be28a649ed": { + "balance": "0x6c69f73e29134e0000" + }, + "50ca86b5eb1d01874df8e5f34945d49c6c1ab848": { + "balance": "0x3635c9adc5dea00000" + }, + "50cd97e9378b5cf18f173963236c9951ef7438a5": { + "balance": "0x4be4e7267b6ae00000" + }, + "50dcbc27bcad984093a212a9b4178eabe9017561": { + "balance": "0x7e362790b5ca40000" + }, + "50e13023bd9ca96ad4c53fdfd410cb6b1f420bdf": { + "balance": "0xad78ebc5ac6200000" + }, + "50e1c8ec98415bef442618708799437b86e6c205": { + "balance": "0x14542ba12a337c00000" + }, + "50f8fa4bb9e2677c990a4ee8ce70dd1523251e4f": { + "balance": "0x1693d23164f6b0000" + }, + "50fb36c27107ee2ca9a3236e2746cca19ace6b49": { + "balance": "0x6c6b935b8bbd400000" + }, + "50fef296955588caae74c62ec32a23a454e09ab8": { + "balance": "0x411dffabc507380000" + }, + "5102a4a42077e11c58df4773e3ac944623a66d9f": { + "balance": "0x6c7015fd52ed408000" + }, + "51039377eed0c573f986c5e8a95fb99a59e9330f": { + "balance": "0x6acb3df27e1f880000" + }, + "5103bc09933e9921fd53dc536f11f05d0d47107d": { + "balance": "0xd8d726b7177a800000" + }, + "5104ecc0e330dd1f81b58ac9dbb1a9fbf88a3c85": { + "balance": "0x152d02c7e14af6800000" + }, + "510d8159cc945768c7450790ba073ec0d9f89e30": { + "balance": "0x8ac7230489e8000000" + }, + "510eda5601499a0d5e1a006bfffd833672f2e267": { + "balance": "0x6c6b935b8bbd400000" + }, + "51126446ab3d8032557e8eba65597d75fadc815c": { + "balance": "0x1174a5cdf88bc80000" + }, + "5118557d600d05c2fcbf3806ffbd93d02025d730": { + "balance": "0x267d3ab6423f5800000" + }, + "511e0efb04ac4e3ff2e6550e498295bfcd56ffd5": { + "balance": "0x243d4d18229ca20000" + }, + "512116817ba9aaf843d1507c65a5ea640a7b9eec": { + "balance": "0x2b5e3af16b1880000" + }, + "5126460d692c71c9af6f05574d93998368a23799": { + "balance": "0x2d1a51c7e00500000" + }, + "51277fe7c81eebd252a03df69a6b9f326e272207": { + "balance": "0x3402e79cab44c8000" + }, + "51296f5044270d17707646129c86aad1645eadc1": { + "balance": "0x487c72b310d4648000" + }, + "512b91bbfaa9e581ef683fc90d9db22a8f49f48b": { + "balance": "0x41a522386d9b95c00000" + }, + "5135fb8757600cf474546252f74dc0746d06262c": { + "balance": "0x6c6b935b8bbd400000" + }, + "514632efbd642c04de6ca342315d40dd90a2dba6": { + "balance": "0x90f534608a72880000" + }, + "514b7512c9ae5ea63cbf11715b63f21e18d296c1": { + "balance": "0x6c6acc67d7b1d40000" + }, + "5153a0c3c8912881bf1c3501bf64b45649e48222": { + "balance": "0xd8d726b7177a800000" + }, + "515651d6db4faf9ecd103a921bbbbe6ae970fdd4": { + "balance": "0x43c33c1937564800000" + }, + "515f30bc90cdf4577ee47d65d785fbe2e837c6bc": { + "balance": "0x2271b5e018ba0580000" + }, + "5160ed612e1b48e73f3fc15bc4321b8f23b8a24b": { + "balance": "0x1e826b422865d80000" + }, + "5161fd49e847f67455f1c8bb7abb36e985260d03": { + "balance": "0x410d586a20a4c00000" + }, + "516954025fca2608f47da81c215eedfd844a09ff": { + "balance": "0x14b550a013c7380000" + }, + "5169c60aee4ceed1849ab36d664cff97061e8ea8": { + "balance": "0xa2a15d09519be00000" + }, + "517c75430de401c341032686112790f46d4d369e": { + "balance": "0x150894e849b3900000" + }, + "517cd7608e5d0d83a26b717f3603dac2277dc3a4": { + "balance": "0x6c6b935b8bbd400000" + }, + "51865db148881951f51251710e82b9be0d7eadb2": { + "balance": "0x6c6b935b8bbd400000" + }, + "51891b2ccdd2f5a44b2a8bc49a5d9bca6477251c": { + "balance": "0x10ce1d3d8cb3180000" + }, + "518cef27b10582b6d14f69483ddaa0dd3c87bb5c": { + "balance": "0x2086ac351052600000" + }, + "51a6d627f66a8923d88d6094c4715380d3057cb6": { + "balance": "0x3e73d27a35941e0000" + }, + "51a8c2163602a32ee24cf4aa97fd9ea414516941": { + "balance": "0x368f7e6b8672c0000" + }, + "51b4758e9e1450e7af4268c3c7b1e7bd6f5c7550": { + "balance": "0x3635c9adc5dea00000" + }, + "51ca8bd4dc644fac47af675563d5804a0da21eeb": { + "balance": "0x2ab7b260ff3fd00000" + }, + "51d24bc3736f88dd63b7222026886630b6eb878d": { + "balance": "0x6c6b935b8bbd400000" + }, + "51d78b178d707e396e8710965c4f41b1a1d9179d": { + "balance": "0x5fee222041e340000" + }, + "51e32f14f4ca5e287cdac057a7795ea9e0439953": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "51e43fe0d25c782860af81ea89dd793c13f0cbb1": { + "balance": "0x340aad21b3b700000" + }, + "51e7b55c2f9820eed73884361b5066a59b6f45c6": { + "balance": "0x6c6b935b8bbd400000" + }, + "51ea1c0934e3d04022ed9c95a087a150ef705e81": { + "balance": "0x1547081e7224d200000" + }, + "51ee0cca3bcb10cd3e983722ced8493d926c0866": { + "balance": "0x36356633ebd8ea0000" + }, + "51f4663ab44ff79345f427a0f6f8a6c8a53ff234": { + "balance": "0x43c33c1937564800000" + }, + "51f55ef47e6456a418ab32b9221ed27dba6608ee": { + "balance": "0xe3aeb5737240a00000" + }, + "51f9c432a4e59ac86282d6adab4c2eb8919160eb": { + "balance": "0x703b5b89c3a6e7400000" + }, + "520f66a0e2657ff0ac4195f2f064cf2fa4b24250": { + "balance": "0x22b1c8c1227a00000" + }, + "52102354a6aca95d8a2e86d5debda6de69346076": { + "balance": "0x6c6b935b8bbd400000" + }, + "5213f459e078ad3ab95a0920239fcf1633dc04ca": { + "balance": "0x8cf2187c2afb188000" + }, + "5215183b8f80a9bc03d26ce91207832a0d39e620": { + "balance": "0x3635c9adc5dea00000" + }, + "52214378b54004056a7cc08c891327798ac6b248": { + "balance": "0x337fe5feaf2d1800000" + }, + "522323aad71dbc96d85af90f084b99c3f09decb7": { + "balance": "0x14542ba12a337c00000" + }, + "523e140dc811b186dee5d6c88bf68e90b8e096fd": { + "balance": "0x6c6b935b8bbd400000" + }, + "523f6d64690fdacd942853591bb0ff20d3656d95": { + "balance": "0x62a992e53a0af00000" + }, + "524fb210522c5e23bb67dfbf8c26aa616da49955": { + "balance": "0x363562a66d34238000" + }, + "5255dc69155a45b970c604d30047e2f530690e7f": { + "balance": "0x1158e460913d00000" + }, + "5260dc51ee07bddaababb9ee744b393c7f4793a6": { + "balance": "0x1d8665fa5fa4c0000" + }, + "5267f4d41292f370863c90d793296903843625c7": { + "balance": "0x4be4e7267b6ae00000" + }, + "526bb533b76e20c8ee1ebf123f1e9ff4148e40be": { + "balance": "0xaadec983fcff40000" + }, + "526cb09ce3ada3672eec1deb46205be89a4b563e": { + "balance": "0x85ca615bf9c0100000" + }, + "52738c90d860e04cb12f498d96fdb5bf36fc340e": { + "balance": "0x1a055690d9db80000" + }, + "527a8ca1268633a6c939c5de1b929aee92aeac8d": { + "balance": "0x30ca024f987b900000" + }, + "528101ce46b720a2214dcdae6618a53177ffa377": { + "balance": "0x1b9612b9dc01ae0000" + }, + "5281733473e00d87f11e9955e589b59f4ac28e7a": { + "balance": "0x8bd62ff4eec559200000" + }, + "5298ab182a19359ffcecafd7d1b5fa212dede6dd": { + "balance": "0x1158e460913d00000" + }, + "529aa002c6962a3a8545027fd8b05f22b5bf9564": { + "balance": "0x5a87e7d7f5f6580000" + }, + "529e824fa072582b4032683ac7eecc1c04b4cac1": { + "balance": "0x6c6b935b8bbd400000" + }, + "52a5e4de4393eeccf0581ac11b52c683c76ea15d": { + "balance": "0x43c30fb0884a96c0000" + }, + "52b4257cf41b6e28878d50d57b99914ffa89873a": { + "balance": "0xd50dc9aa2c41770000" + }, + "52b8a9592634f7300b7c5c59a3345b835f01b95c": { + "balance": "0x6c6b935b8bbd400000" + }, + "52bdd9af5978850bc24110718b3723759b437e59": { + "balance": "0x5dc892aa1131c80000" + }, + "52cd20403ba7eda6bc307a3d63b5911b817c1263": { + "balance": "0x1158e460913d00000" + }, + "52d380511df19d5ec2807bbcb676581b67fd37a3": { + "balance": "0xb9f65d00f63c0000" + }, + "52e1731350f983cc2c4189842fde0613fad50ce1": { + "balance": "0x277017338a30ae00000" + }, + "52e46783329a769301b175009d346768f4c87ee4": { + "balance": "0x6c6b935b8bbd400000" + }, + "52f058d46147e9006d29bf2c09304ad1cddd6e15": { + "balance": "0x5150ae84a8cdf00000" + }, + "52f15423323c24f19ae2ab673717229d3f747d9b": { + "balance": "0x37a034cbe8e3f38000" + }, + "52f8b509fee1a874ab6f9d87367fbeaf15ac137f": { + "balance": "0x3635c9adc5dea00000" + }, + "52fb46ac5d00c3518b2c3a1c177d442f8165555f": { + "balance": "0x5150ae84a8cdf00000" + }, + "530077c9f7b907ff9cec0c77a41a70e9029add4a": { + "balance": "0x6c6b935b8bbd400000" + }, + "530319db0a8f93e5bb7d4dbf4816314fbed8361b": { + "balance": "0x6c6b935b8bbd400000" + }, + "53047dc8ac9083d90672e8b3473c100ccd278323": { + "balance": "0x22b1c8c1227a00000" + }, + "530b61e42f39426d2408d40852b9e34ab5ebebc5": { + "balance": "0xe7eeba3410b740000" + }, + "530ffac3bc3412e2ec0ea47b7981c770f5bb2f35": { + "balance": "0x73f75d1a085ba0000" + }, + "5317ecb023052ca7f5652be2fa854cfe4563df4d": { + "balance": "0x1b1ab319f5ec750000" + }, + "53194d8afa3e883502767edbc30586af33b114d3": { + "balance": "0x6c6b935b8bbd400000" + }, + "532a7da0a5ad7407468d3be8e07e69c7dd64e861": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "532d32b00f305bcc24dcef56817d622f34fb2c24": { + "balance": "0x6194049f30f7200000" + }, + "533444584082eba654e1ad30e149735c6f7ba922": { + "balance": "0x5dc892aa1131c80000" + }, + "5338ef70eac9dd9af5a0503b5efad1039e67e725": { + "balance": "0x90f534608a72880000" + }, + "53396f4a26c2b4604496306c5442e7fcba272e36": { + "balance": "0x43f2f08d40e5afc0000" + }, + "533a73a4a2228eee05c4ffd718bbf3f9c1b129a7": { + "balance": "0x14542ba12a337c00000" + }, + "533c06928f19d0a956cc28866bf6c8d8f4191a94": { + "balance": "0xfd8c14338e6300000" + }, + "534065361cb854fac42bfb5c9fcde0604ac919da": { + "balance": "0x6c6b935b8bbd400000" + }, + "53437fecf34ab9d435f4deb8ca181519e2592035": { + "balance": "0xa31062beeed700000" + }, + "535201a0a1d73422801f55ded4dfaee4fbaa6e3b": { + "balance": "0x226211f7915428000" + }, + "53608105ce4b9e11f86bf497ffca3b78967b5f96": { + "balance": "0x43c33c1937564800000" + }, + "536e4d8029b73f5579dca33e70b24eba89e11d7e": { + "balance": "0x6acb3df27e1f880000" + }, + "53700d53254d430f22781a4a76a463933b5d6b08": { + "balance": "0x6acb3df27e1f880000" + }, + "537f9d4d31ef70839d84b0d9cdb72b9afedbdf35": { + "balance": "0xed2b525841adfc00000" + }, + "5381448503c0c702542b1de7cc5fb5f6ab1cf6a5": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "53942e7949d6788bb780a7e8a0792781b1614b84": { + "balance": "0x35deb46684f10c80000" + }, + "5395a4455d95d178b4532aa4725b193ffe512961": { + "balance": "0x3635c9adc5dea00000" + }, + "53989ed330563fd57dfec9bd343c3760b0799390": { + "balance": "0x150894e849b39000000" + }, + "53a244672895480f4a2b1cdf7da5e5a242ec4dbc": { + "balance": "0x3635c9adc5dea00000" + }, + "53a714f99fa00fef758e23a2e746326dad247ca7": { + "balance": "0x50c5e761a444080000" + }, + "53af32c22fef99803f178cf90b802fb571c61cb9": { + "balance": "0xd255d112e103a00000" + }, + "53c0bb7fc88ea422d2ef7e540e2d8f28b1bb8183": { + "balance": "0x1158e460913d00000" + }, + "53c5fe0119e1e848640cee30adea96940f2a5d8b": { + "balance": "0x49ada5fa8c10c880000" + }, + "53c9eca40973f63bb5927be0bc6a8a8be1951f74": { + "balance": "0x6c6b935b8bbd400000" + }, + "53ce88e66c5af2f29bbd8f592a56a3d15f206c32": { + "balance": "0x7a28c31cc36040000" + }, + "53cec6c88092f756efe56f7db11228a2db45b122": { + "balance": "0xd8d726b7177a800000" + }, + "53e35b12231f19c3fd774c88fec8cbeedf1408b2": { + "balance": "0x1bc16d674ec8000000" + }, + "53e4d9696dcb3f4d7b3f70dcaa4eecb71782ff5c": { + "balance": "0xad78ebc5ac6200000" + }, + "53faf165be031ec18330d9fce5bd1281a1af08db": { + "balance": "0x796e3ea3f8ab00000" + }, + "540a1819bd7c35861e791804e5fbb3bc97c9abb1": { + "balance": "0x4ed7dac64230200000" + }, + "540c072802014ef0d561345aec481e8e11cb3570": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "540cf23dd95c4d558a279d778d2b3735b3164191": { + "balance": "0x21e19e0c9bab2400000" + }, + "541060fc58c750c40512f83369c0a63340c122b6": { + "balance": "0x6acb3df27e1f880000" + }, + "5413c97ffa4a6e2a7bba8961dc9fce8530a787d7": { + "balance": "0x3635c9adc5dea00000" + }, + "541db20a80cf3b17f1621f1b3ff79b882f50def3": { + "balance": "0x3635c9adc5dea00000" + }, + "542e8096bafb88162606002e8c8a3ed19814aeac": { + "balance": "0x6c6b935b8bbd400000" + }, + "54310b3aa88703a725dfa57de6e646935164802c": { + "balance": "0x678a932062e4180000" + }, + "5431b1d18751b98fc9e2888ac7759f1535a2db47": { + "balance": "0x6c6b935b8bbd400000" + }, + "5431ca427e6165a644bae326bd09750a178c650d": { + "balance": "0x6c6b935b8bbd400000" + }, + "5435c6c1793317d32ce13bba4c4ffeb973b78adc": { + "balance": "0xd8e6b1c1285ef0000" + }, + "543629c95cdef428ad37d453ca9538a9f90900ac": { + "balance": "0x92896529baddc880000" + }, + "54391b4d176d476cea164e5fb535c69700cb2535": { + "balance": "0x56cd55fc64dfe0000" + }, + "543a8c0efb8bcd15c543e2a6a4f807597631adef": { + "balance": "0x13f80e7e14f2d440000" + }, + "543f8c674e2462d8d5daa0e80195a8708e11a29e": { + "balance": "0x37758833b3a7a0000" + }, + "544b5b351d1bc82e9297439948cf4861dac9ae11": { + "balance": "0x4a89f54ef0121c00000" + }, + "544dda421dc1eb73bb24e3e56a248013b87c0f44": { + "balance": "0x6acb3df27e1f880000" + }, + "54575c3114751e3c631971da6a2a02fd3ffbfcc8": { + "balance": "0x692ae8897081d00000" + }, + "545bb070e781172eb1608af7fc2895d6cb87197e": { + "balance": "0x79a5c17ec748900000" + }, + "5475d7f174bdb1f789017c7c1705989646079d49": { + "balance": "0x1fd933494aa5fe00000" + }, + "548558d08cfcb101181dac1eb6094b4e1a896fa6": { + "balance": "0x6c6acc67d7b1d40000" + }, + "54939ff08921b467cf2946751d856378296c63ed": { + "balance": "0x3635c9adc5dea00000" + }, + "549b47649cfad993e4064d2636a4baa0623305cc": { + "balance": "0x209d922f5259c50000" + }, + "549d51af29f724c967f59423b85b2681e7b15136": { + "balance": "0xcbd47b6eaa8cc00000" + }, + "54a1370116fe22099e015d07cd2669dd291cc9d1": { + "balance": "0x1158e460913d00000" + }, + "54a62bf9233e146ffec3876e45f20ee8414adeba": { + "balance": "0x21e19e0c9bab2400000" + }, + "54b4429b182f0377be7e626939c5db6440f75d7a": { + "balance": "0x6acb3df27e1f880000" + }, + "54bcb8e7f73cda3d73f4d38b2d0847e600ba0df8": { + "balance": "0x3a70415882df180000" + }, + "54c93e03a9b2e8e4c3672835a9ee76f9615bc14e": { + "balance": "0x10d3aa536e2940000" + }, + "54ce88275956def5f9458e3b95decacd484021a0": { + "balance": "0x6c6b935b8bbd400000" + }, + "54db5e06b4815d31cb56a8719ba33af2d73e7252": { + "balance": "0x24521e2a3017b80000" + }, + "54e01283cc8b384538dd646770b357c960d6cacd": { + "balance": "0x10f0cf064dd59200000" + }, + "54ec7300b81ac84333ed1b033cd5d7a33972e234": { + "balance": "0xad78ebc5ac6200000" + }, + "54febcce20fe7a9098a755bd90988602a48c089e": { + "balance": "0x22b1c8c1227a000000" + }, + "550aadae1221b07afea39fba2ed62e05e5b7b5f9": { + "balance": "0x1158e460913d00000" + }, + "550c306f81ef5d9580c06cb1ab201b95c748a691": { + "balance": "0x2417d4c470bf140000" + }, + "551999ddd205563327b9b530785acff9bc73a4ba": { + "balance": "0x14542ba12a337c00000" + }, + "551e7784778ef8e048e495df49f2614f84a4f1dc": { + "balance": "0x2086ac351052600000" + }, + "5529830a61c1f13c197e550beddfd6bd195c9d02": { + "balance": "0x21e19e0c9bab2400000" + }, + "552987f0651b915b2e1e5328c121960d4bdd6af4": { + "balance": "0x61093d7c2c6d380000" + }, + "553b6b1c57050e88cf0c31067b8d4cd1ff80cb09": { + "balance": "0x15af1d78b58c400000" + }, + "553f37d92466550e9fd775ae74362df030179132": { + "balance": "0x6c6b935b8bbd400000" + }, + "554336ee4ea155f9f24f87bca9ca72e253e12cd2": { + "balance": "0x56bc75e2d63100000" + }, + "5543dd6d169eec8a213bbf7a8af9ffd15d4ff759": { + "balance": "0xfc936392801c0000" + }, + "5547fdb4ae11953e01292b7807fa9223d0e4606a": { + "balance": "0x55d117dcb1d260000" + }, + "5552f4b3ed3e1da79a2f78bb13e8ae5a68a9df3b": { + "balance": "0x3635c9adc5dea00000" + }, + "555ca9f05cc134ab54ae9bea1c3ff87aa85198ca": { + "balance": "0x56bc75e2d63100000" + }, + "555d8d3ce1798aca902754f164b8be2a02329c6c": { + "balance": "0x21e19e0c9bab2400000" + }, + "555df19390c16d01298772bae8bc3a1152199cbd": { + "balance": "0xad78ebc5ac6200000" + }, + "555ebe84daa42ba256ea789105cec4b693f12f18": { + "balance": "0x56bc75e2d63100000" + }, + "557f5e65e0da33998219ad4e99570545b2a9d511": { + "balance": "0x2559cbb985842400000" + }, + "558360206883dd1b6d4a59639e5629d0f0c675d0": { + "balance": "0x6c6b935b8bbd400000" + }, + "5584423050e3c2051f0bbd8f44bd6dbc27ecb62c": { + "balance": "0xa2a15d09519be00000" + }, + "55852943492970f8d629a15366cdda06a94f4513": { + "balance": "0x6c6b935b8bbd400000" + }, + "55866486ec168f79dbe0e1abb18864d98991ae2c": { + "balance": "0xdf6eb0b2d3ca0000" + }, + "558c54649a8a6e94722bd6d21d14714f71780534": { + "balance": "0x6c6b935b8bbd400000" + }, + "559194304f14b1b93afe444f0624e053c23a0009": { + "balance": "0x15af1d78b58c400000" + }, + "5593c9d4b664730fd93ca60151c25c2eaed93c3b": { + "balance": "0xad78ebc5ac6200000" + }, + "559706c332d20779c45f8a6d046a699159b74921": { + "balance": "0x149b442e85a3cf8000" + }, + "5598b3a79a48f32b1f5fc915b87b645d805d1afe": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "55a3df57b7aaec16a162fd5316f35bec082821cf": { + "balance": "0x6acb3df27e1f880000" + }, + "55a4cac0cb8b582d9fef38c5c9fff9bd53093d1f": { + "balance": "0x6acb3df27e1f880000" + }, + "55a61b109480b5b2c4fcfdef92d90584160c0d35": { + "balance": "0x26c564d2b53f60000" + }, + "55aa5d313ebb084da0e7801091e29e92c5dec3aa": { + "balance": "0x6c6b935b8bbd400000" + }, + "55ab99b0e0e55d7bb874b7cfe834de631c97ec23": { + "balance": "0x37e98ce36899e40000" + }, + "55af092f94ba6a79918b0cf939eab3f01b3f51c7": { + "balance": "0x820d5e39576120000" + }, + "55c564664166a1edf3913e0169f1cd451fdb5d0c": { + "balance": "0x8217ea49508e6c0000" + }, + "55ca6abe79ea2497f46fdbb830346010fe469cbe": { + "balance": "0x1369fb96128ac480000" + }, + "55caff4bba04d220c9a5d2018672ec85e31ef83e": { + "balance": "0x6c6b935b8bbd400000" + }, + "55d057bcc04bd0f4af9642513aa5090bb3ff93fe": { + "balance": "0x3bfe452c8edd4c0000" + }, + "55d42eb495bf46a634997b5f2ea362814918e2b0": { + "balance": "0x5c0d265b5b2a80000" + }, + "55da9dcdca61cbfe1f133c7bcefc867b9c8122f9": { + "balance": "0x2fb474098f67c00000" + }, + "55e220876262c218af4f56784798c7e55da09e91": { + "balance": "0x73d99c15645d30000" + }, + "55fd08d18064bd202c0ec3d2cce0ce0b9d169c4d": { + "balance": "0x6acb3df27e1f880000" + }, + "5600730a55f6b20ebd24811faa3de96d1662abab": { + "balance": "0x65ea3db75546600000" + }, + "5603241eb8f08f721e348c9d9ad92f48e390aa24": { + "balance": "0xad78ebc5ac6200000" + }, + "560536794a9e2b0049d10233c41adc5f418a264a": { + "balance": "0x3635c9adc5dea00000" + }, + "5607590059a9fec1881149a44b36949aef85d560": { + "balance": "0x6c6b935b8bbd400000" + }, + "560becdf52b71f3d8827d927610f1a980f33716f": { + "balance": "0x17474d705f56d08000" + }, + "560da37e956d862f81a75fd580a7135c1b246352": { + "balance": "0x21e19e0c9bab2400000" + }, + "560fc08d079f047ed8d7df75551aa53501f57013": { + "balance": "0x19bff2ff57968c00000" + }, + "561be9299b3e6b3e63b79b09169d1a948ae6db01": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "562020e3ed792d2f1835fe5f55417d5111460c6a": { + "balance": "0x43c33c1937564800000" + }, + "5620f46d1451c2353d6243a5d4b427130be2d407": { + "balance": "0x340aad21b3b700000" + }, + "562105e82b099735de49f62692cc87cd38a8edcd": { + "balance": "0x14542ba12a337c00000" + }, + "562a8dcbbeeef7b360685d27303bd69e094accf6": { + "balance": "0x21e19e0c9bab2400000" + }, + "562bced38ab2ab6c080f3b0541b8456e70824b3f": { + "balance": "0x22ca3587cf4eb00000" + }, + "562be95aba17c5371fe2ba828799b1f55d2177d6": { + "balance": "0x816d37e87b9d1e00000" + }, + "562f16d79abfcec3943e34b20f05f97bdfcda605": { + "balance": "0xd8d726b7177a800000" + }, + "56373daab46316fd7e1576c61e6affcb6559ddd7": { + "balance": "0xbac715d146c9e0000" + }, + "56397638bb3cebf1f62062794b5eb942f916171d": { + "balance": "0x6c6b935b8bbd400000" + }, + "563a03ab9c56b600f6d25b660c21e16335517a75": { + "balance": "0x3635c9adc5dea00000" + }, + "563cb8803c1d32a25b27b64114852bd04d9c20cd": { + "balance": "0xb149ead0ad9d80000" + }, + "56586391040c57eec6f5affd8cd4abde10b50acc": { + "balance": "0xd8d726b7177a800000" + }, + "566c10d638e8b88b47d6e6a414497afdd00600d4": { + "balance": "0x56b394263a40c0000" + }, + "566c28e34c3808d9766fe8421ebf4f2b1c4f7d77": { + "balance": "0x6acb3df27e1f880000" + }, + "568df31856699bb5acfc1fe1d680df9960ca4359": { + "balance": "0x4acf5552f3b2498000" + }, + "5691dd2f6745f20e22d2e1d1b955aa2903d65656": { + "balance": "0x6ac5c62d9486070000" + }, + "56a1d60d40f57f308eebf087dee3b37f1e7c2cba": { + "balance": "0x3edcaec82d06f80000" + }, + "56ac20d63bd803595cec036da7ed1dc66e0a9e07": { + "balance": "0x3772a53ccdc658000" + }, + "56b6c23dd2ec90b4728f3bb2e764c3c50c85f144": { + "balance": "0x3635c9adc5dea00000" + }, + "56df05bad46c3f00ae476ecf017bb8c877383ff1": { + "balance": "0xab15daaef70400000" + }, + "56ee197f4bbf9f1b0662e41c2bbd9aa1f799e846": { + "balance": "0x3635c9adc5dea00000" + }, + "56f493a3d108aaa2d18d98922f8efe1662cfb73d": { + "balance": "0x6d8121a194d1100000" + }, + "56fc1a7bad4047237ce116146296238e078f93ad": { + "balance": "0x9a63f08ea63880000" + }, + "56febf9e1003af15b1bd4907ec089a4a1b91d268": { + "balance": "0xad78ebc5ac6200000" + }, + "5717cc9301511d4a81b9f583148beed3d3cc8309": { + "balance": "0x8cf23f909c0fa00000" + }, + "5717f2d8f18ffcc0e5fe247d3a4219037c3a649c": { + "balance": "0xd8bb6549b02bb80000" + }, + "571950ea2c90c1427d939d61b4f2de4cf1cfbfb0": { + "balance": "0x1158e460913d00000" + }, + "5719f49b720da68856f4b9e708f25645bdbc4b41": { + "balance": "0x22b1c8c1227a000000" + }, + "572ac1aba0de23ae41a7cae1dc0842d8abfc103b": { + "balance": "0x678a932062e4180000" + }, + "572dd8cd3fe399d1d0ec281231b7cefc20b9e4bb": { + "balance": "0x233c8fe42703e800000" + }, + "574921838cc77d6c98b17d903a3ae0ee0da95bd0": { + "balance": "0xb5328178ad0f2a00000" + }, + "574ad9355390e4889ef42acd138b2a27e78c00ae": { + "balance": "0x5467b732a913340000" + }, + "574de1b3f38d915846ae3718564a5ada20c2f3ed": { + "balance": "0xd8d726b7177a800000" + }, + "575c00c2818210c28555a0ff29010289d3f82309": { + "balance": "0x21e19e0c9bab2400000" + }, + "5773b6026721a1dd04b7828cd62b591bfb34534c": { + "balance": "0x5b7ac4553de7ae00000" + }, + "5777441c83e03f0be8dd340bde636850847c620b": { + "balance": "0x21e19e0c9bab2400000" + }, + "5778ffdc9b94c5a59e224eb965b6de90f222d170": { + "balance": "0x122d7ff36603fc0000" + }, + "577aeee8d4bc08fc97ab156ed57fb970925366be": { + "balance": "0x120df1147258bf0000" + }, + "577b2d073c590c50306f5b1195a4b2ba9ecda625": { + "balance": "0x1440bdd49515f00000" + }, + "577bfe64e3a1e3800e94db1c6c184d8dc8aafc66": { + "balance": "0x5134ed17417f280000" + }, + "57825aeb09076caa477887fbc9ae37e8b27cc962": { + "balance": "0x56bc75e2d63100000" + }, + "57883010b4ac857fedac03eab2551723a8447ffb": { + "balance": "0x3635c9adc5dea00000" + }, + "5789d01db12c816ac268e9af19dc0dd6d99f15df": { + "balance": "0xad78ebc5ac6200000" + }, + "5792814f59a33a1843faa01baa089eb02ffb5cf1": { + "balance": "0x1b1ab319f5ec750000" + }, + "5793abe6f1533311fd51536891783b3f9625ef1c": { + "balance": "0x2cd8a656f23fda0000" + }, + "5797b60fd2894ab3c2f4aede86daf2e788d745ad": { + "balance": "0x14542ba12a337c00000" + }, + "57a852fdb9b1405bf53ccf9508f83299d3206c52": { + "balance": "0x6c6b935b8bbd400000" + }, + "57b23d6a1adc06c652a779c6a7fb6b95b9fead66": { + "balance": "0xad78ebc5ac6200000" + }, + "57bc20e2d62b3d19663cdb4c309d5b4f2fc2db8f": { + "balance": "0x56bc75e2d63100000" + }, + "57bddf078834009c89d88e6282759dc45335b470": { + "balance": "0x74717cfb6883100000" + }, + "57beea716cbd81700a73d67f9ff039529c2d9025": { + "balance": "0xad78ebc5ac6200000" + }, + "57d032a43d164e71aa2ef3ffd8491b0a4ef1ea5b": { + "balance": "0x6c6b935b8bbd400000" + }, + "57d3df804f2beee6ef53ab94cb3ee9cf524a18d3": { + "balance": "0x1556616b9606670000" + }, + "57d5fd0e3d3049330ffcdcd020456917657ba2da": { + "balance": "0x6bf20195f554d40000" + }, + "57dd9471cbfa262709f5f486bcb774c5f527b8f8": { + "balance": "0xaadec983fcff40000" + }, + "57df23bebdc65eb75feb9cb2fad1c073692b2baf": { + "balance": "0xd8d726b7177a800000" + }, + "5800cd8130839e94495d2d8415a8ea2c90e0c5cb": { + "balance": "0xad78ebc5ac6200000" + }, + "5803e68b34da121aef08b602badbafb4d12481ca": { + "balance": "0x3cfc82e37e9a7400000" + }, + "5816c2687777b6d7d2a2432d59a41fa059e3a406": { + "balance": "0x1c4fe43adb0a5e900000" + }, + "581a3af297efa4436a29af0072929abf9826f58b": { + "balance": "0x6c6b935b8bbd400000" + }, + "581b9fd6eae372f3501f42eb9619eec820b78a84": { + "balance": "0x42be2c00ca53b8d8000" + }, + "581bdf1bb276dbdd86aedcdb397a01efc0e00c5b": { + "balance": "0x3635c9adc5dea00000" + }, + "581f34b523e5b41c09c87c298e299cbc0e29d066": { + "balance": "0x3d5833aafd39758000" + }, + "5824a7e22838277134308c5f4b50dab65e43bb31": { + "balance": "0x14542ba12a337c00000" + }, + "582b70669c97aab7d68148d8d4e90411e2810d56": { + "balance": "0x36356633ebd8ea0000" + }, + "582e7cc46f1d7b4e6e9d95868bfd370573178f4c": { + "balance": "0x6c6b935b8bbd400000" + }, + "583e83ba55e67e13e0e76f8392d873cd21fbf798": { + "balance": "0x1158e460913d00000" + }, + "5869fb867d71f1387f863b698d09fdfb87c49b5c": { + "balance": "0xc6bbf858b316080000" + }, + "587d6849b168f6c3332b7abae7eb6c42c37f48bf": { + "balance": "0x2fb474098f67c00000" + }, + "5887dc6a33dfed5ac1edefe35ef91a216231ac96": { + "balance": "0xd8d726b7177a80000" + }, + "588ed990a2aff44a94105d58c305257735c868ac": { + "balance": "0x368c8623a8b4d100000" + }, + "58ae2ddc5f4c8ada97e06c0086171767c423f5d7": { + "balance": "0x57473d05dabae80000" + }, + "58aed6674affd9f64233272a578dd9386b99c263": { + "balance": "0xb8507a820728200000" + }, + "58b808a65b51e6338969afb95ec70735e451d526": { + "balance": "0x8784bc1b9837a380000" + }, + "58b8ae8f63ef35ed0762f0b6233d4ac14e64b64d": { + "balance": "0x6c6b935b8bbd400000" + }, + "58ba1569650e5bbbb21d35d3e175c0d6b0c651a9": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "58c555bc293cdb16c6362ed97ae9550b92ea180e": { + "balance": "0x1158e460913d00000" + }, + "58c650ced40bb65641b8e8a924a039def46854df": { + "balance": "0x100bd33fb98ba0000" + }, + "58c90754d2f20a1cb1dd330625e04b45fa619d5c": { + "balance": "0x6c6b935b8bbd400000" + }, + "58e2f11223fc8237f69d99c6289c148c0604f742": { + "balance": "0x5150ae84a8cdf000000" + }, + "58e554af3d87629620da61d538c7f5b4b54c4afe": { + "balance": "0x46509d694534728000" + }, + "58e5c9e344c806650dacfc904d33edba5107b0de": { + "balance": "0x10910d4cdc9f60000" + }, + "58e661d0ba73d6cf24099a5562b808f7b3673b68": { + "balance": "0x6c6b935b8bbd400000" + }, + "58f05b262560503ca761c61890a4035f4c737280": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "58fb947364e7695765361ebb1e801ffb8b95e6d0": { + "balance": "0xad78ebc5ac6200000" + }, + "590181d445007bd0875aaf061c8d51153900836a": { + "balance": "0x6c6b935b8bbd400000" + }, + "5902e44af769a87246a21e079c08bf36b06efeb3": { + "balance": "0x3635c9adc5dea00000" + }, + "590acbda37290c0d3ec84fc2000d7697f9a4b15d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "590ccb5911cf78f6f622f535c474375f4a12cfcf": { + "balance": "0x43c33c1937564800000" + }, + "5910106debd291a1cd80b0fbbb8d8d9e93a7cc1e": { + "balance": "0x6c6b935b8bbd400000" + }, + "59161749fedcf1c721f2202d13ade2abcf460b3d": { + "balance": "0x6c6b935b8bbd400000" + }, + "591bef3171d1c5957717a4e98d17eb142c214e56": { + "balance": "0x43c33c1937564800000" + }, + "59203cc37599b648312a7cc9e06dacb589a9ae6a": { + "balance": "0x80f7971b6400e8000" + }, + "59268171b833e0aa13c54b52ccc0422e4fa03aeb": { + "balance": "0xa2a15d09519be00000" + }, + "592777261e3bd852c48eca95b3a44c5b7f2d422c": { + "balance": "0x43c33c1937564800000" + }, + "593044670faeff00a55b5ae051eb7be870b11694": { + "balance": "0x73f75d1a085ba0000" + }, + "593b45a1864ac5c7e8f0caaeba0d873cd5d113b2": { + "balance": "0x14542ba12a337c00000" + }, + "593c48935beaff0fde19b04d309cd530a28e52ce": { + "balance": "0xd8d726b7177a800000" + }, + "59473cd300fffae240f5785626c65dfec792b9af": { + "balance": "0x1158e460913d00000" + }, + "5948bc3650ed519bf891a572679fd992f8780c57": { + "balance": "0xaadec983fcff40000" + }, + "594a76f06935388dde5e234696a0668bc20d2ddc": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "59569a21d28fba4bda37753405a081f2063da150": { + "balance": "0xd8d726b7177a800000" + }, + "5956b28ec7890b76fc061a1feb52d82ae81fb635": { + "balance": "0x6c6b935b8bbd400000" + }, + "595e23d788a2d4bb85a15df7136d264a635511b3": { + "balance": "0xd5967be4fc3f100000" + }, + "597038ff91a0900cbbab488af483c790e6ec00a0": { + "balance": "0x21e19e0c9bab2400000" + }, + "5970fb1b144dd751e4ce2eca7caa20e363dc4da3": { + "balance": "0x21e19e0c9bab2400000" + }, + "5975b9528f23af1f0e2ec08ac8ebaa786a2cb8e0": { + "balance": "0x12bf50503ae3038000" + }, + "5975d78d974ee5bb9e4d4ca2ae77c84b9c3b4b82": { + "balance": "0x4a4491bd6dcd280000" + }, + "5985c59a449dfc5da787d8244e746c6d70caa55f": { + "balance": "0x56bc75e2d63100000" + }, + "598aaabae9ed833d7bc222e91fcaa0647b77580b": { + "balance": "0x6194049f30f7200000" + }, + "5992624c54cdec60a5ae938033af8be0c50cbb0a": { + "balance": "0xc454e0f8870f2b0000" + }, + "599728a78618d1a17b9e34e0fed8e857d5c40622": { + "balance": "0x2f6f10780d22cc00000" + }, + "5997ffefb3c1d9d10f1ae2ac8ac3c8e2d2292783": { + "balance": "0x3635c9adc5dea00000" + }, + "59a087b9351ca42f58f36e021927a22988284f38": { + "balance": "0x100bd33fb98ba0000" + }, + "59a12df2e3ef857aceff9306b309f6a500f70134": { + "balance": "0x3635c9adc5dea00000" + }, + "59b96deb8784885d8d3b4a166143cc435d2555a1": { + "balance": "0x487a9a304539440000" + }, + "59b9e733cba4be00429b4bd9dfa64732053a7d55": { + "balance": "0x1158e460913d00000" + }, + "59c5d06b170ee4d26eb0a0eb46cb7d90c1c91019": { + "balance": "0x21e19e0c9bab2400000" + }, + "59c7f785c93160e5807ed34e5e534bc6188647a7": { + "balance": "0x22b1c8c1227a000000" + }, + "59d139e2e40c7b97239d23dfaca33858f602d22b": { + "balance": "0x6c6b935b8bbd400000" + }, + "59f6247b0d582aaa25e5114765e4bf3c774f43c2": { + "balance": "0x2b5e3af16b1880000" + }, + "59fe00696dbd87b7976b29d1156c8842a2e17914": { + "balance": "0x6c6b935b8bbd400000" + }, + "5a0d609aae2332b137ab3b2f26615a808f37e433": { + "balance": "0x21e19e0c9bab24000000" + }, + "5a192b964afd80773e5f5eda6a56f14e25e0c6f3": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5a1a336962d6e0c63031cc83c6a5c6a6f4478ecb": { + "balance": "0x3635c9adc5dea00000" + }, + "5a1d2d2d1d520304b6208849570437eb3091bb9f": { + "balance": "0x6acb3df27e1f880000" + }, + "5a267331facb262daaecd9dd63a9700c5f5259df": { + "balance": "0x56bc75e2d63100000" + }, + "5a285755391e914e58025faa48cc685f4fd4f5b8": { + "balance": "0x581767ba6189c400000" + }, + "5a2916b8d2e8cc12e207ab464d433e2370d823d9": { + "balance": "0x6c6b935b8bbd400000" + }, + "5a2b1c853aeb28c45539af76a00ac2d8a8242896": { + "balance": "0x15af1d78b58c40000" + }, + "5a2daab25c31a61a92a4c82c9925a1d2ef58585e": { + "balance": "0xc380da9c7950c0000" + }, + "5a30feac37ac9f72d7b4af0f2bc73952c74fd5c3": { + "balance": "0x6c6b935b8bbd400000" + }, + "5a5468fa5ca226c7532ecf06e1bc1c45225d7ec9": { + "balance": "0x678a932062e4180000" + }, + "5a565285374a49eedd504c957d510874d00455bc": { + "balance": "0x56bc75e2d63100000" + }, + "5a5ee8e9bb0e8ab2fecb4b33d29478be50bbd44b": { + "balance": "0x2a1129d09367200000" + }, + "5a5f8508da0ebebb90be9033bd4d9e274105ae00": { + "balance": "0x16a6502f15a1e540000" + }, + "5a6071bcebfcba4ab57f4db96fc7a68bece2ba5b": { + "balance": "0x6c6b935b8bbd400000" + }, + "5a60c924162873fc7ea4da7f972e350167376031": { + "balance": "0x487f277a885798000" + }, + "5a6686b0f17e07edfc59b759c77d5bef164d3879": { + "balance": "0x50c5e761a444080000" + }, + "5a70106f20d63f875265e48e0d35f00e17d02bc9": { + "balance": "0x1158e460913d00000" + }, + "5a74ba62e7c81a3474e27d894fed33dd24ad95fe": { + "balance": "0xfc936392801c0000" + }, + "5a7735007d70b06844da9901cdfadb11a2582c2f": { + "balance": "0x14542ba12a337c00000" + }, + "5a82f96cd4b7e2d93d10f3185dc8f43d4b75aa69": { + "balance": "0x6c633fbab98c040000" + }, + "5a87f034e6f68f4e74ffe60c64819436036cf7d7": { + "balance": "0x1158e460913d00000" + }, + "5a891155f50e42074374c739baadf7df2651153a": { + "balance": "0x102da6fd0f73a3c0000" + }, + "5a9c8b69fc614d69564999b00dcb42db67f97e90": { + "balance": "0xb9e615abad3a778000" + }, + "5aaf1c31254a6e005fba7f5ab0ec79d7fc2b630e": { + "balance": "0x14061b9d77a5e980000" + }, + "5ab1a5615348001c7c775dc75748669b8be4de14": { + "balance": "0x256a72fb29e69c0000" + }, + "5abfec25f74cd88437631a7731906932776356f9": { + "balance": "0x9d83cc0dfa11177ff8000" + }, + "5ac2908b0f398c0df5bac2cb13ca7314fba8fa3d": { + "balance": "0xad4c8316a0b0c0000" + }, + "5ac99ad7816ae9020ff8adf79fa9869b7cea6601": { + "balance": "0x472698b413b43200000" + }, + "5ad12c5ed4fa827e2150cfa0d68c0aa37b1769b8": { + "balance": "0x2b5e3af16b18800000" + }, + "5ad5e420755613886f35aa56ac403eebdfe4b0d0": { + "balance": "0x10f0cf064dd592000000" + }, + "5ade77fd81c25c0af713b10702768c1eb2f975e7": { + "balance": "0x1158e460913d00000" + }, + "5ae64e853ba0a51282cb8db52e41615e7c9f733f": { + "balance": "0x6c6b935b8bbd400000" + }, + "5aed0e6cfe95f9d680c76472a81a2b680a7f93e2": { + "balance": "0xaadec983fcff40000" + }, + "5aef16a226dd68071f2483e1da42598319f69b2c": { + "balance": "0x6c6b935b8bbd400000" + }, + "5af46a25ac09cb73616b53b14fb42ff0a51cddb2": { + "balance": "0xd8d726b7177a800000" + }, + "5af7c072b2c5acd71c76addcce535cf7f8f93585": { + "balance": "0x1158e460913d00000" + }, + "5afda9405c8e9736514574da928de67456010918": { + "balance": "0x145b8b0239a46920000" + }, + "5b06d1e6930c1054692b79e3dbe6ecce53966420": { + "balance": "0xb227f63be813c0000" + }, + "5b25cae86dcafa2a60e7723631fc5fa49c1ad87d": { + "balance": "0x870c58510e85200000" + }, + "5b287c7e734299e727626f93fb1187a60d5057fe": { + "balance": "0x57cd934a914cb0000" + }, + "5b290c01967c812e4dc4c90b174c1b4015bae71e": { + "balance": "0x820eb348d52b90000" + }, + "5b2b64e9c058e382a8b299224eecaa16e09c8d92": { + "balance": "0x8ba52e6fc45e40000" + }, + "5b2e2f1618552eab0db98add55637c2951f1fb19": { + "balance": "0x28a857425466f800000" + }, + "5b30608c678e1ac464a8994c3b33e5cdf3497112": { + "balance": "0x15af1d78b58c400000" + }, + "5b333696e04cca1692e71986579c920d6b2916f9": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5b430d779696a3653fc60e74fbcbacf6b9c2baf1": { + "balance": "0x2f6f10780d22cc00000" + }, + "5b437365ae3a9a2ff97c68e6f90a7620188c7d19": { + "balance": "0x6c8754c8f30c080000" + }, + "5b49afcd75447838f6e7ceda8d21777d4fc1c3c0": { + "balance": "0xd8d726b7177a800000" + }, + "5b4c0c60f10ed2894bdb42d9dd1d210587810a0d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5b4ea16db6809b0352d4b6e81c3913f76a51bb32": { + "balance": "0x15af1d78b58c400000" + }, + "5b5be0d8c67276baabd8edb30d48ea75640b8b29": { + "balance": "0x2cb1f55fb7be100000" + }, + "5b5d517029321562111b43086d0b043591109a70": { + "balance": "0x8cf23f909c0fa00000" + }, + "5b5d8c8eed6c85ac215661de026676823faa0a0c": { + "balance": "0x43c33c1937564800000" + }, + "5b6d55f6712967405c659129f4b1de09acf2cb7b": { + "balance": "0xe7eeba3410b740000" + }, + "5b70c49cc98b3df3fbe2b1597f5c1b6347a388b7": { + "balance": "0x34957444b840e80000" + }, + "5b736eb18353629bde9676dadd165034ce5ecc68": { + "balance": "0x6acb3df27e1f880000" + }, + "5b759fa110a31c88469f54d44ba303d57dd3e10f": { + "balance": "0x5b46dd2f0ea3b80000" + }, + "5b7784caea01799ca30227827667ce207c5cbc76": { + "balance": "0x6c6b935b8bbd400000" + }, + "5b78eca27fbdea6f26befba8972b295e7814364b": { + "balance": "0x6c6b935b8bbd400000" + }, + "5b800bfd1b3ed4a57d875aed26d42f1a7708d72a": { + "balance": "0x15a82d1d5bb88e00000" + }, + "5b85e60e2af0544f2f01c64e2032900ebd38a3c7": { + "balance": "0x6c6b935b8bbd400000" + }, + "5ba2c6c35dfaec296826591904d544464aeabd5e": { + "balance": "0x1158e460913d00000" + }, + "5baf6d749620803e8348af3710e5c4fbf20fc894": { + "balance": "0x10f4002615dfe900000" + }, + "5bc1f95507b1018642e45cd9c0e22733b9b1a326": { + "balance": "0x56bc75e2d63100000" + }, + "5bd23547477f6d09d7b2a005c5ee650c510c56d7": { + "balance": "0x21e19e0c9bab2400000" + }, + "5bd24aac3612b20c609eb46779bf95698407c57c": { + "balance": "0x6acb3df27e1f880000" + }, + "5bd6862d517d4de4559d4eec0a06cad05e2f946e": { + "balance": "0xad78ebc5ac6200000" + }, + "5be045512a026e3f1cebfd5a7ec0cfc36f2dc16b": { + "balance": "0x68155a43676e00000" + }, + "5bf9f2226e5aeacf1d80ae0a59c6e38038bc8db5": { + "balance": "0x14542ba12a337c00000" + }, + "5bfafe97b1dd1d712be86d41df79895345875a87": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5c0f2e51378f6b0d7bab617331580b6e39ad3ca5": { + "balance": "0x2086ac3510526000000" + }, + "5c29f9e9a523c1f8669448b55c48cbd47c25e610": { + "balance": "0x3446a0dad04cb00000" + }, + "5c308bac4857d33baea074f3956d3621d9fa28e1": { + "balance": "0x10f08eda8e555098000" + }, + "5c312a56c784b122099b764d059c21ece95e84ca": { + "balance": "0x52663ccab1e1c0000" + }, + "5c31996dcac015f9be985b611f468730ef244d90": { + "balance": "0xad78ebc5ac6200000" + }, + "5c323457e187761a8276e359b7b7af3f3b6e3df6": { + "balance": "0x21e19e0c9bab2400000" + }, + "5c3c1c645b917543113b3e6c1c054da1fe742b9a": { + "balance": "0x2b5e3af16b18800000" + }, + "5c3d19441d196cb443662020fcad7fbb79b29e78": { + "balance": "0xc673ce3c40160000" + }, + "5c3f567faff7bad1b5120022e8cbcaa82b4917b3": { + "balance": "0x6c6b935b8bbd400000" + }, + "5c4368918ace6409c79eca80cdaae4391d2b624e": { + "balance": "0xd8d726b7177a800000" + }, + "5c464197791c8a3da3c925436f277ab13bf2faa2": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "5c4881165cb42bb82e97396c8ef44adbf173fb99": { + "balance": "0x5fee222041e340000" + }, + "5c4892907a0720df6fd3413e63ff767d6b398023": { + "balance": "0x2cb009fd3b5790f8000" + }, + "5c4f24e994ed8f850ea7818f471c8fac3bcf0452": { + "balance": "0x5d80688d9e31c00000" + }, + "5c5419565c3aad4e714e0739328e3521c98f05cc": { + "balance": "0x1c9f78d2893e400000" + }, + "5c6136e218de0a61a137b2b3962d2a6112b809d7": { + "balance": "0xff3dbb65ff4868000" + }, + "5c61ab79b408dd3229f662593705d72f1e147bb8": { + "balance": "0x4d0243d3498cd840000" + }, + "5c6d041da7af4487b9dc48e8e1f60766d0a56dbc": { + "balance": "0x4f070a003e9c740000" + }, + "5c6f36af90ab1a656c6ec8c7d521512762bba3e1": { + "balance": "0x6c68ccd09b022c0000" + }, + "5c7b9ec7a2438d1e3c7698b545b9c3fd77b7cd55": { + "balance": "0x3635c9adc5dea00000" + }, + "5c936f3b9d22c403db5e730ff177d74eef42dbbf": { + "balance": "0x410d586a20a4c0000" + }, + "5cb731160d2e8965670bde925d9de5510935347d": { + "balance": "0x22b1c8c1227a00000" + }, + "5cb953a0e42f5030812226217fffc3ce230457e4": { + "balance": "0x56bc75e2d63100000" + }, + "5cbd8daf27ddf704cdd0d909a789ba36ed4f37b2": { + "balance": "0xb9f65d00f63c0000" + }, + "5cc4cba621f220637742057f6055b80dffd77e13": { + "balance": "0x878477b7d253b660000" + }, + "5cc7d3066d45d27621f78bb4b339473e442a860f": { + "balance": "0x21e1899f0377aea0000" + }, + "5cccf1508bfd35c20530aa642500c10dee65eaed": { + "balance": "0x2e141ea081ca080000" + }, + "5cce72d068c7c3f55b1d2819545e77317cae8240": { + "balance": "0x692ae8897081d00000" + }, + "5cd0e475b54421bdfc0c12ea8e082bd7a5af0a6a": { + "balance": "0x332ca1b67940c0000" + }, + "5cd588a14ec648ccf64729f9167aa7bf8be6eb3d": { + "balance": "0x3635c9adc5dea00000" + }, + "5cd8af60de65f24dc3ce5730ba92653022dc5963": { + "balance": "0x61093d7c2c6d380000" + }, + "5cdc4708f14f40dcc15a795f7dc8cb0b7faa9e6e": { + "balance": "0x1d1c5f3eda20c40000" + }, + "5ce0b6862cce9162e87e0849e387cb5df4f9118c": { + "balance": "0x5a87e7d7f5f6580000" + }, + "5ce2e7ceaaa18af0f8aafa7fbad74cc89e3cd436": { + "balance": "0x43c33c1937564800000" + }, + "5ce44068b8f4a3fe799e6a8311dbfdeda29dee0e": { + "balance": "0x6c6b935b8bbd400000" + }, + "5cebe30b2a95f4aefda665651dc0cf7ef5758199": { + "balance": "0xfc936392801c0000" + }, + "5cf18fa7c8a7c0a2b3d5efd1990f64ddc569242c": { + "balance": "0x3635c9adc5dea00000" + }, + "5cf44e10540d65716423b1bcb542d21ff83a94cd": { + "balance": "0x21e19e0c9bab2400000" + }, + "5cf8c03eb3e872e50f7cfd0c2f8d3b3f2cb5183a": { + "balance": "0xad78ebc5ac6200000" + }, + "5cfa8d568575658ca4c1a593ac4c5d0e44c60745": { + "balance": "0xfc66fae3746ac0000" + }, + "5cfa9877f719c79d9e494a08d1e41cf103fc87c9": { + "balance": "0xad78ebc5ac6200000" + }, + "5d1dc3387b47b8451e55106c0cc67d6dc72b7f0b": { + "balance": "0x6c6b935b8bbd400000" + }, + "5d231a70c1dfeb360abd97f616e2d10d39f3cab5": { + "balance": "0x15af1d78b58c400000" + }, + "5d24bdbc1c47f0eb83d128cae48ac33c4817e91f": { + "balance": "0x3635c9adc5dea00000" + }, + "5d2819e8d57821922ee445650ccaec7d40544a8d": { + "balance": "0xad78ebc5ac6200000" + }, + "5d2f7f0b04ba4be161e19cb6f112ce7a5e7d7fe4": { + "balance": "0x1e87f85809dc00000" + }, + "5d32f6f86e787ff78e63d78b0ef95fe6071852b8": { + "balance": "0x15be6174e1912e0000" + }, + "5d39ef9ea6bdfff15d11fe91f561a6f9e31f5da5": { + "balance": "0x6c6b935b8bbd400000" + }, + "5d3f3b1f7130b0bb21a0fd32396239179a25657f": { + "balance": "0xd3ab8ea5e8fd9e80000" + }, + "5d5751819b4f3d26ed0c1ac571552735271dbefa": { + "balance": "0x3635c9adc5dea00000" + }, + "5d5c2c1099bbeefb267e74b58880b444d94449e0": { + "balance": "0xdbf0bd181e2e70000" + }, + "5d5cdbe25b2a044b7b9be383bcaa5807b06d3c6b": { + "balance": "0x6c6b935b8bbd400000" + }, + "5d5d6e821c6eef96810c83c491468560ef70bfb5": { + "balance": "0x6c6b935b8bbd400000" + }, + "5d68324bcb776d3ffd0bf9fea91d9f037fd6ab0f": { + "balance": "0x6c6b935b8bbd400000" + }, + "5d6ae8cbd6b3393c22d16254100d0238e808147c": { + "balance": "0x2707e56d51a30c0000" + }, + "5d6c5c720d66a6abca8397142e63d26818eaab54": { + "balance": "0x22b1c8c1227a00000" + }, + "5d6ccf806738091042ad97a6e095fe8c36aa79c5": { + "balance": "0xa31062beeed700000" + }, + "5d71799c8df3bccb7ee446df50b8312bc4eb71c5": { + "balance": "0xad78ebc5ac6200000" + }, + "5d822d9b3ef4b502627407da272f67814a6becd4": { + "balance": "0x1158e460913d00000" + }, + "5d83b21bd2712360436b67a597ee3378db3e7ae4": { + "balance": "0x6c6b935b8bbd400000" + }, + "5d872b122e994ef27c71d7deb457bf65429eca6c": { + "balance": "0x1b1aded81d394108000" + }, + "5d8d31faa864e22159cd6f5175ccecc53fa54d72": { + "balance": "0x5b696b70dd567100000" + }, + "5d958a9bd189c2985f86c58a8c69a7a78806e8da": { + "balance": "0x228f16f861578600000" + }, + "5da2a9a4c2c0a4a924cbe0a53ab9d0c627a1cfa0": { + "balance": "0x27bf38c6544df50000" + }, + "5da4ca88935c27f55c311048840e589e04a8a049": { + "balance": "0x4563918244f400000" + }, + "5da54785c9bd30575c89deb59d2041d20a39e17b": { + "balance": "0x6aa209f0b91d658000" + }, + "5db69fe93e6fb6fbd450966b97238b110ad8279a": { + "balance": "0x878678326eac9000000" + }, + "5db7bba1f9573f24115d8c8c62e9ce8895068e9f": { + "balance": "0x2b5aad72c65200000" + }, + "5db84400570069a9573cab04b4e6b69535e202b8": { + "balance": "0x20dd68aaf3289100000" + }, + "5dc36de5359450a1ec09cb0c44cf2bb42b3ae435": { + "balance": "0x3c946d893b33060000" + }, + "5dc6f45fef26b06e3302313f884daf48e2746fb9": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5dcdb6b87a503c6d8a3c65c2cf9a9aa883479a1e": { + "balance": "0x1f2bba5d84f99c00000" + }, + "5dd112f368c0e6ceff77a9df02a5481651a02fb7": { + "balance": "0x93472c85c6d540000" + }, + "5dd53ae897526b167d39f1744ef7c3da5b37a293": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "5dded049a6e1f329dc4b971e722c9c1f2ade83f0": { + "balance": "0x3635c9adc5dea00000" + }, + "5de598aba344378cab4431555b4f79992dc290c6": { + "balance": "0x487a9a304539440000" + }, + "5de9e7d5d1b667d095dd34099c85b0421a0bc681": { + "balance": "0x1158e460913d00000" + }, + "5df3277ca85936c7a0d2c0795605ad25095e7159": { + "balance": "0x6c6b935b8bbd400000" + }, + "5dff811dad819ece3ba602c383fb5dc64c0a3a48": { + "balance": "0xa1544be879ea80000" + }, + "5e031b0a724471d476f3bcd2eb078338bf67fbef": { + "balance": "0xfc936392801c0000" + }, + "5e0785532c7723e4c0af9357d5274b73bdddddde": { + "balance": "0x54b41ea9bdb61dc0000" + }, + "5e11ecf69d551d7f4f84df128046b3a13240a328": { + "balance": "0x1158e460913d00000" + }, + "5e1fbd4e58e2312b3c78d7aaaafa10bf9c3189e3": { + "balance": "0x878678326eac9000000" + }, + "5e32c72191b8392c55f510d8e3326e3a60501d62": { + "balance": "0x9513ea9de0243800000" + }, + "5e51b8a3bb09d303ea7c86051582fd600fb3dc1a": { + "balance": "0x1158e460913d00000" + }, + "5e58e255fc19870a04305ff2a04631f2ff294bb1": { + "balance": "0xf43fc2c04ee00000" + }, + "5e5a441974a83d74c687ebdc633fb1a49e7b1ad7": { + "balance": "0xa2a15d09519be00000" + }, + "5e65458be964ae449f71773704979766f8898761": { + "balance": "0x1ca7cc735b6f7c0000" + }, + "5e67df8969101adabd91accd6bb1991274af8df2": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5e6e9747e162f8b45c656e0f6cae7a84bac80e4e": { + "balance": "0x6c6b935b8bbd400000" + }, + "5e731b55ced452bb3f3fe871ddc3ed7ee6510a8f": { + "balance": "0xa2a15d09519be00000" + }, + "5e74ed80e9655788e1bb269752319667fe754e5a": { + "balance": "0x30927f74c9de00000" + }, + "5e772e27f28800c50dda973bb33e10762e6eea20": { + "balance": "0x61093d7c2c6d380000" + }, + "5e7b8c54dc57b0402062719dee7ef5e37ea35d62": { + "balance": "0x9bf9810fd05c840000" + }, + "5e7f70378775589fc66a81d3f653e954f55560eb": { + "balance": "0x83f289181d84c80000" + }, + "5e806e845730f8073e6cc9018ee90f5c05f909a3": { + "balance": "0x201e96dacceaf200000" + }, + "5e8e4df18cf0af770978a8df8dac90931510a679": { + "balance": "0x6c6b935b8bbd400000" + }, + "5e90c85877198756b0366c0e17b28e52b446505a": { + "balance": "0x144a4a18efeb680000" + }, + "5e95fe5ffcf998f9f9ac0e9a81dab83ead77003d": { + "balance": "0x1d42c20d32797f0000" + }, + "5ead29037a12896478b1296ab714e9cb95428c81": { + "balance": "0x3e043072d406e0000" + }, + "5eb371c407406c427b3b7de271ad3c1e04269579": { + "balance": "0xa2a15d09519be00000" + }, + "5ecdbaeab9106ffe5d7b519696609a05baeb85ad": { + "balance": "0x1158e460913d00000" + }, + "5ed0d6338559ef44dc7a61edeb893fa5d83fa1b5": { + "balance": "0xbed1d0263d9f00000" + }, + "5ed3bbc05240e0d399eb6ddfe60f62de4d9509af": { + "balance": "0x2914c02475f9d6d30000" + }, + "5ed3f1ebe2ae6756b5d8dc19cad02c419aa5778b": { + "balance": "0x0" + }, + "5ed56115bd6505a88273df5c56839470d24a2db7": { + "balance": "0x38e6591ee56668000" + }, + "5ef8c96186b37984cbfe04c598406e3b0ac3171f": { + "balance": "0x1fd933494aa5fe00000" + }, + "5efbdfe5389999633c26605a5bfc2c1bb5959393": { + "balance": "0x3c057c95cd9080000" + }, + "5f13154631466dcb1353c890932a7c97e0878e90": { + "balance": "0x14542ba12a337c00000" + }, + "5f167aa242bc4c189adecb3ac4a7c452cf192fcf": { + "balance": "0x6c6b4c4da6ddbe0000" + }, + "5f1c8a04c90d735b8a152909aeae636fb0ce1665": { + "balance": "0x17b7827618c5a370000" + }, + "5f23ba1f37a96c45bc490259538a54c28ba3b0d5": { + "balance": "0x410d586a20a4c00000" + }, + "5f26cf34599bc36ea67b9e7a9f9b4330c9d542a3": { + "balance": "0x3635c9adc5dea00000" + }, + "5f29c9de765dde25852af07d33f2ce468fd20982": { + "balance": "0x6c6b935b8bbd400000" + }, + "5f2f07d2d697e8c567fcfdfe020f49f360be2139": { + "balance": "0x6c6b935b8bbd400000" + }, + "5f321b3daaa296cadf29439f9dab062a4bffedd6": { + "balance": "0x47025903ea7ae0000" + }, + "5f333a3b2310765a0d1832b9be4c0a03704c1c09": { + "balance": "0x3635c9adc5dea00000" + }, + "5f344b01c7191a32d0762ac188f0ec2dd460911d": { + "balance": "0x3635c9adc5dea00000" + }, + "5f363e0ab747e02d1b3b66abb69ea53c7baf523a": { + "balance": "0x277017338a30ae00000" + }, + "5f375b86600c40cca8b2676b7a1a1d1644c5f52c": { + "balance": "0x44618d74c623f0000" + }, + "5f3e1e6739b0c62200e00a003691d9efb238d89f": { + "balance": "0xa2a15d09519be00000" + }, + "5f483ffb8f680aedf2a38f7833afdcde59b61e4b": { + "balance": "0x6c6b935b8bbd400000" + }, + "5f4ace4c1cc13391e01f00b198e1f20b5f91cbf5": { + "balance": "0x10f0fa8b9d3811a0000" + }, + "5f521282e9b278dc8c034c72af53ee29e5443d78": { + "balance": "0x161732d2f8f3ae00000" + }, + "5f68a24c7eb4117667737b33393fb3c2148a53b6": { + "balance": "0x2cede918d453c0000" + }, + "5f708eaf39d823946c51b3a3e9b7b3c003e26341": { + "balance": "0x62a992e53a0af00000" + }, + "5f742e487e3ab81af2f94afdbe1b9b8f5ccc81bc": { + "balance": "0x75c445d41163e60000" + }, + "5f74ed0e24ff80d9b2c4a44baa9975428cd6b935": { + "balance": "0xa18bcec34888100000" + }, + "5f76f0a306269c78306b3d650dc3e9c37084db61": { + "balance": "0x821ab0d44149800000" + }, + "5f77a107ab1226b3f95f10ee83aefc6c5dff3edc": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "5f7b3bbac16dab831a4a0fc53b0c549dc36c31ca": { + "balance": "0x692ae8897081d00000" + }, + "5f93ff832774db5114c55bb4bf44ccf3b58f903f": { + "balance": "0x28a9c91a263458290000" + }, + "5f9616c47b4a67f406b95a14fe6fc268396f1721": { + "balance": "0xad78ebc5ac6200000" + }, + "5f981039fcf50225e2adf762752112d1cc26b6e3": { + "balance": "0x1b1a416a2153a50000" + }, + "5f99dc8e49e61d57daef606acdd91b4d7007326a": { + "balance": "0xa2a15d09519be00000" + }, + "5fa61f152de6123516c751242979285f796ac791": { + "balance": "0xb0f11972963b00000" + }, + "5fa7bfe043886127d4011d8356a47e947963aca8": { + "balance": "0x62a992e53a0af00000" + }, + "5fa8a54e68176c4fe2c01cf671c515bfbdd528a8": { + "balance": "0x45e155fa0110fa400000" + }, + "5fad960f6b2c84569c9f4d47bf1985fcb2c65da6": { + "balance": "0x36356633ebd8ea0000" + }, + "5fc6c11426b4a1eae7e51dd512ad1090c6f1a85b": { + "balance": "0x93fe5c57d710680000" + }, + "5fcd84546896dd081db1a320bd4d8c1dd1528c4c": { + "balance": "0x1158e460913d00000" + }, + "5fcda847aaf8d7fa8bca08029ca2849166aa15a3": { + "balance": "0x21cab81259a3bf0000" + }, + "5fd1c3e31778276cb42ea740f5eae9c641dbc701": { + "balance": "0xa844a7424d9c80000" + }, + "5fd3d6777ec2620ae83a05528ed425072d3ca8fd": { + "balance": "0x6c6b935b8bbd400000" + }, + "5fd973af366aa5157c54659bcfb27cbfa5ac15d6": { + "balance": "0xd8d726b7177a800000" + }, + "5fe77703808f823e6c399352108bdb2c527cb87c": { + "balance": "0x6a4076cf7995a00000" + }, + "5fec49c665e64ee89dd441ee74056e1f01e92870": { + "balance": "0x1569b9e733474c00000" + }, + "5ff326cd60fd136b245e29e9087a6ad3a6527f0d": { + "balance": "0x65ea3db75546600000" + }, + "5ff93de6ee054cad459b2d5eb0f6870389dfcb74": { + "balance": "0xbed1d0263d9f00000" + }, + "6006e36d929bf45d8f16231b126a011ae283d925": { + "balance": "0x98a7d9b8314c00000" + }, + "6021e85a8814fce1e82a41abd1d3b2dad2faefe0": { + "balance": "0x6c6b935b8bbd400000" + }, + "6038740ae28d66ba93b0be08482b3205a0f7a07b": { + "balance": "0x11216185c29f700000" + }, + "603f2fab7afb6e017b94766069a4b43b38964923": { + "balance": "0x59d2db2414da990000" + }, + "6042276df2983fe2bc4759dc1943e18fdbc34f77": { + "balance": "0x6acb3df27e1f880000" + }, + "6042c644bae2b96f25f94d31f678c90dc96690db": { + "balance": "0x6c6b935b8bbd400000" + }, + "604cdf18628dbfa8329194d478dd5201eecc4be7": { + "balance": "0x13f306a2409fc0000" + }, + "604e9477ebf4727c745bcabbedcb6ccf29994022": { + "balance": "0x36369ed7747d260000" + }, + "60676d1fa21fca052297e24bf96389c5b12a70d7": { + "balance": "0xd177c5a7a68d60000" + }, + "60676e92d18b000509c61de540e6c5ddb676d509": { + "balance": "0x410d586a20a4c00000" + }, + "606f177121f7855c21a5062330c8762264a97b31": { + "balance": "0xd8d726b7177a800000" + }, + "60864236930d04d8402b5dcbeb807f3caf611ea2": { + "balance": "0xd8d726b7177a800000" + }, + "60ab71cd26ea6d6e59a7a0f627ee079c885ebbf6": { + "balance": "0x1731790534df20000" + }, + "60af0ee118443c9b37d2fead77f5e521debe1573": { + "balance": "0x678a932062e4180000" + }, + "60b358cb3dbefa37f47df2d7365840da8e3bc98c": { + "balance": "0x1158e460913d00000" + }, + "60b8d6b73b79534fb08bb8cbcefac7f393c57bfe": { + "balance": "0x5f68e8131ecf800000" + }, + "60be6f953f2a4d25b6256ffd2423ac1438252e4e": { + "balance": "0x821ab0d4414980000" + }, + "60c3714fdddb634659e4a2b1ea42c4728cc7b8ba": { + "balance": "0xb98bc829a6f90000" + }, + "60cc3d445ebdf76a7d7ae571c6971dff68cc8585": { + "balance": "0x3635c9adc5dea00000" + }, + "60d5667140d12614b21c8e5e8a33082e32dfcf23": { + "balance": "0x43c33c1937564800000" + }, + "60de22a1507432a47b01cc68c52a0bf8a2e0d098": { + "balance": "0x10910d4cdc9f60000" + }, + "60e0bdd0a259bb9cb09d3f37e5cd8b9daceabf8a": { + "balance": "0x4a4491bd6dcd280000" + }, + "60e3cc43bcdb026aad759c7066f555bbf2ac66f5": { + "balance": "0x6c6b935b8bbd400000" + }, + "61042b80fd6095d1b87be2f00f109fabafd157a6": { + "balance": "0x56bc75e2d63100000" + }, + "6107d71dd6d0eefb11d4c916404cb98c753e117d": { + "balance": "0x6c6b935b8bbd400000" + }, + "610fd6ee4eebab10a8c55d0b4bd2e7d6ef817156": { + "balance": "0x1159561065d5d0000" + }, + "6114b0eae5576903f80bfb98842d24ed92237f1e": { + "balance": "0x56bc75e2d63100000" + }, + "6121af398a5b2da69f65c6381aec88ce9cc6441f": { + "balance": "0x22b1c8c1227a000000" + }, + "612667f172135b950b2cd1de10afdece6857b873": { + "balance": "0x3635c9adc5dea00000" + }, + "612ced8dc0dc9e899ee46f7962333315f3f55e44": { + "balance": "0x125e35f9cd3d9b0000" + }, + "6134d942f037f2cc3d424a230c603d67abd3edf7": { + "balance": "0x6c6b935b8bbd400000" + }, + "613ac53be565d46536b820715b9b8d3ae68a4b95": { + "balance": "0xcbd47b6eaa8cc00000" + }, + "613fab44b16bbe554d44afd178ab1d02f37aeaa5": { + "balance": "0x6c6b935b8bbd400000" + }, + "614e8bef3dd2c59b59a4145674401018351884ea": { + "balance": "0x1158e460913d00000" + }, + "61518464fdd8b73c1bb6ac6db600654938dbf17a": { + "balance": "0xad78ebc5ac6200000" + }, + "61547d376e5369bcf978fc162c3c56ae453547e8": { + "balance": "0xad78ebc5ac6200000" + }, + "6158e107c5eb54cb7604e0cd8dc1e07500d91c3c": { + "balance": "0x2b5e3af16b1880000" + }, + "615a6f36777f40d6617eb5819896186983fd3731": { + "balance": "0x14061b9d77a5e980000" + }, + "615f82365c5101f071e7d2cb6af14f7aad2c16c6": { + "balance": "0x1158e460913d00000" + }, + "6170dd0687bd55ca88b87adef51cfdc55c4dd458": { + "balance": "0x6cb32f5c34fe440000" + }, + "61733947fab820dbd351efd67855ea0e881373a0": { + "balance": "0x1158e460913d00000" + }, + "6179979907fe7f037e4c38029d60bcbab832b3d6": { + "balance": "0x57473d05dabae80000" + }, + "617f20894fa70e94a86a49cd74e03238f64d3cd9": { + "balance": "0x10f0dbae61009528000" + }, + "617ff2cc803e31c9082233b825d025be3f7b1056": { + "balance": "0x6acb3df27e1f880000" + }, + "6191ddc9b64a8e0890b4323709d7a07c48b92a64": { + "balance": "0x2a034919dfbfbc0000" + }, + "6196c3d3c0908d254366b7bca55745222d9d4db1": { + "balance": "0xd8d726b7177a800000" + }, + "619f171445d42b02e2e07004ad8afe694fa53d6a": { + "balance": "0x1158e460913d00000" + }, + "61adf5929a5e2981684ea243baa01f7d1f5e148a": { + "balance": "0x5fabf6c984f230000" + }, + "61b1b8c012cd4c78f698e470f90256e6a30f48dd": { + "balance": "0xad78ebc5ac6200000" + }, + "61b3df2e9e9fd968131f1e88f0a0eb5bd765464d": { + "balance": "0xd8d726b7177a800000" + }, + "61b902c5a673885826820d1fe14549e4865fbdc2": { + "balance": "0x1224efed2ae1918000" + }, + "61b905de663fc17386523b3a28e2f7d037a655cd": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "61ba87c77e9b596de7ba0e326fddfeec2163ef66": { + "balance": "0xad78ebc5ac6200000" + }, + "61bf84d5ab026f58c873f86ff0dfca82b55733ae": { + "balance": "0x6c6b935b8bbd400000" + }, + "61c4ee7c864c4d6b5e37ea1331c203739e826b2f": { + "balance": "0x1a1353b382a918000" + }, + "61c830f1654718f075ccaba316faacb85b7d120b": { + "balance": "0x15af1d78b58c400000" + }, + "61c8f1fa43bf846999ecf47b2b324dfb6b63fe3a": { + "balance": "0x2b5e3af16b18800000" + }, + "61c9dce8b2981cb40e98b0402bc3eb28348f03ac": { + "balance": "0xaacacd9b9e22b0000" + }, + "61cea71fa464d62a07063f920b0cc917539733d8": { + "balance": "0x5a87e7d7f5f6580000" + }, + "61d101a033ee0e2ebb3100ede766df1ad0244954": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "61ed5596c697207f3d55b2a51aa7d50f07fa09e8": { + "balance": "0x6c6b935b8bbd400000" + }, + "61ff8e67b34d9ee6f78eb36ffea1b9f7c15787af": { + "balance": "0x58e7926ee858a00000" + }, + "6205c2d5647470848a3840f3887e9b015d34755c": { + "balance": "0x6194049f30f7200000" + }, + "6228ade95e8bb17d1ae23bfb0518414d497e0eb8": { + "balance": "0x15af1d78b58c400000" + }, + "6229dcc203b1edccfdf06e87910c452a1f4d7a72": { + "balance": "0x6e1d41a8f9ec3500000" + }, + "622be4b45495fcd93143efc412d699d6cdc23dc5": { + "balance": "0xf015f25736420000" + }, + "62331df2a3cbee3520e911dea9f73e905f892505": { + "balance": "0x6c6b935b8bbd400000" + }, + "625644c95a873ef8c06cdb9e9f6d8d7680043d62": { + "balance": "0x6194049f30f7200000" + }, + "6265b2e7730f36b776b52d0c9d02ada55d8e3cb6": { + "balance": "0x3635c9adc5dea00000" + }, + "62680a15f8ccb8bdc02f7360c25ad8cfb57b8ccd": { + "balance": "0x3635c9adc5dea00000" + }, + "6294eae6e420a3d5600a39c4141f838ff8e7cc48": { + "balance": "0xa030dcebbd2f4c0000" + }, + "62971bf2634cee0be3c9890f51a56099dbb9519b": { + "balance": "0x238fd42c5cf0400000" + }, + "629be7ab126a5398edd6da9f18447e78c692a4fd": { + "balance": "0x6c6b935b8bbd400000" + }, + "62b4a9226e61683c72c183254690daf511b4117a": { + "balance": "0xe18398e7601900000" + }, + "62b9081e7710345e38e02e16449ace1b85bcfc4e": { + "balance": "0x3154c9729d05780000" + }, + "62c37c52b97f4b040b1aa391d6dec152893c4707": { + "balance": "0x3635c9adc5dea00000" + }, + "62c9b271ffd5b770a5eee4edc9787b5cdc709714": { + "balance": "0x6c6b935b8bbd400000" + }, + "62d5cc7117e18500ac2f9e3c26c86b0a94b0de15": { + "balance": "0x5b12aefafa8040000" + }, + "62dc72729024375fc37cbb9c7c2393d10233330f": { + "balance": "0x6c6b935b8bbd400000" + }, + "62e6b2f5eb94fa7a43831fc87e254a3fe3bf8f89": { + "balance": "0xd8d726b7177a80000" + }, + "62f2e5ccecd52cc4b95e0597df27cc079715608c": { + "balance": "0x7c0860e5a80dc0000" + }, + "62fb8bd1f0e66b90533e071e6cbe6111fef0bc63": { + "balance": "0x3ba1910bf341b000000" + }, + "630a913a9031c9492abd4c41dbb15054cfec4416": { + "balance": "0x13458db67af35e00000" + }, + "630c5273126d517ce67101811cab16b8534cf9a8": { + "balance": "0x1feccc62573bbd38000" + }, + "631030a5b27b07288a45696f189e1114f12a81c0": { + "balance": "0x1b1a7a420ba00d0000" + }, + "6310b020fd98044957995092090f17f04e52cdfd": { + "balance": "0x55a6e79ccd1d300000" + }, + "632b9149d70178a7333634275e82d5953f27967b": { + "balance": "0x25f273933db5700000" + }, + "632cecb10cfcf38ec986b43b8770adece9200221": { + "balance": "0x1158e460913d00000" + }, + "6331028cbb5a21485bc51b565142993bdb2582a9": { + "balance": "0x1cfdd7468216e80000" + }, + "63334fcf1745840e4b094a3bb40bb76f9604c04c": { + "balance": "0xd7a5d703a717e80000" + }, + "63340a57716bfa63eb6cd133721202575bf796f0": { + "balance": "0xb61e0a20c12718000" + }, + "634efc24371107b4cbf03f79a93dfd93e431d5fd": { + "balance": "0x423582e08edc5c8000" + }, + "635c00fdf035bca15fa3610df3384e0fb79068b1": { + "balance": "0x1e7e4171bf4d3a00000" + }, + "63612e7862c27b587cfb6daf9912cb051f030a9f": { + "balance": "0x25b19d4bfe8ed0000" + }, + "63666755bd41b5986997783c13043008242b3cb5": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "637be71b3aa815ff453d5642f73074450b64c82a": { + "balance": "0x6c6b935b8bbd400000" + }, + "637d67d87f586f0a5a479e20ee13ea310a10b647": { + "balance": "0xa3a5926afa1e7300000" + }, + "637f5869d6e4695f0eb9e27311c4878aff333380": { + "balance": "0x6ac04e68aaec860000" + }, + "63977cad7d0dcdc52b9ac9f2ffa136e8642882b8": { + "balance": "0x410d586a20a4c0000" + }, + "63a61dc30a8e3b30a763c4213c801cbf98738178": { + "balance": "0x3635c9adc5dea00000" + }, + "63ac545c991243fa18aec41d4f6f598e555015dc": { + "balance": "0x2086ac351052600000" + }, + "63b9754d75d12d384039ec69063c0be210d5e0e3": { + "balance": "0x920b860cc8ecfd8000" + }, + "63bb664f9117037628594da7e3c5089fd618b5b5": { + "balance": "0x1158e460913d00000" + }, + "63c2a3d235e5eeabd0d4a6afdb89d94627396495": { + "balance": "0x434ef05b9d84820000" + }, + "63c8dfde0b8e01dadc2e748c824cc0369df090b3": { + "balance": "0xd255d112e103a00000" + }, + "63d55ad99b9137fd1b20cc2b4f03d42cbaddf334": { + "balance": "0x15af1d78b58c400000" + }, + "63d80048877596e0c28489e650cd4ac180096a49": { + "balance": "0xf2dc7d47f15600000" + }, + "63e414603e80d4e5a0f5c18774204642258208e4": { + "balance": "0x10f0cf064dd59200000" + }, + "63e88e2e539ffb450386b4e46789b223f5476c45": { + "balance": "0x155170a778e25d00000" + }, + "63ef2fbc3daf5edaf4a295629ccf31bcdf4038e5": { + "balance": "0x4f2591f896a6500000" + }, + "63f0e5a752f79f67124eed633ad3fd2705a397d4": { + "balance": "0xd5967be4fc3f100000" + }, + "63f5b53d79bf2e411489526530223845fac6f601": { + "balance": "0x65a4da25d3016c00000" + }, + "63fc93001305adfbc9b85d29d9291a05f8f1410b": { + "balance": "0x3635c9adc5dea00000" + }, + "63fe6bcc4b8a9850abbe75803730c932251f145b": { + "balance": "0xfc936392801c0000" + }, + "6403d062549690c8e8b63eae41d6c109476e2588": { + "balance": "0x6c6b935b8bbd400000" + }, + "64042ba68b12d4c151651ca2813b7352bd56f08e": { + "balance": "0x2086ac351052600000" + }, + "6405dd13e93abcff377e700e3c1a0086eca27d29": { + "balance": "0xfc936392801c0000" + }, + "640aba6de984d94517377803705eaea7095f4a11": { + "balance": "0x21e19e0c9bab2400000" + }, + "640bf87415e0cf407301e5599a68366da09bbac8": { + "balance": "0x1abc9f416098158000" + }, + "6420f8bcc8164a6152a99d6b99693005ccf7e053": { + "balance": "0x36356633ebd8ea0000" + }, + "64241a7844290e0ab855f1d4aa75b55345032224": { + "balance": "0x56bc75e2d631000000" + }, + "64264aedd52dcae918a012fbcd0c030ee6f71821": { + "balance": "0x3635c9adc5dea00000" + }, + "64370e87202645125a35b207af1231fb6072f9a7": { + "balance": "0xad78ebc5ac6200000" + }, + "643d9aeed4b180947ed2b9207cce4c3ddc55e1f7": { + "balance": "0xad78ebc5ac6200000" + }, + "6443b8ae639de91cf73c5ae763eeeed3ddbb9253": { + "balance": "0x6c6b935b8bbd400000" + }, + "64457fa33b0832506c4f7d1180dce48f46f3e0ff": { + "balance": "0x6c6b935b8bbd400000" + }, + "64464a6805b462412a901d2db8174b06c22deea6": { + "balance": "0x19c846a029c7c80000" + }, + "644ba6c61082e989109f5c11d4b40e991660d403": { + "balance": "0xd8d726b7177a800000" + }, + "64628c6fb8ec743adbd87ce5e018d531d9210437": { + "balance": "0x1731790534df20000" + }, + "6463f715d594a1a4ace4bb9c3b288a74decf294d": { + "balance": "0x6acb3df27e1f880000" + }, + "646628a53c2c4193da88359ce718dadd92b7a48d": { + "balance": "0xad8006c2f5ef00000" + }, + "64672da3ab052821a0243d1ce4b6e0a36517b8eb": { + "balance": "0xad78ebc5ac6200000" + }, + "646afba71d849e80c0ed59cac519b278e7f7abe4": { + "balance": "0x3635c9adc5dea00000" + }, + "646e043d0597a664948fbb0dc15475a3a4f3a6ed": { + "balance": "0x1158e460913d00000" + }, + "6470a4f92ec6b0fccd01234fa59023e9ff1f3aac": { + "balance": "0xa2a15d09519be00000" + }, + "647b85044df2cf0b4ed4882e88819fe22ae5f793": { + "balance": "0x36363b5d9a77700000" + }, + "6485470e61db110aebdbafd536769e3c599cc908": { + "balance": "0x2086ac351052600000" + }, + "648f5bd2a2ae8902db37847d1cb0db9390b06248": { + "balance": "0x1a535ecf0760a048000" + }, + "649a2b9879cd8fb736e6703b0c7747849796f10f": { + "balance": "0x18ee22da01ad34f0000" + }, + "649a85b93653075fa6562c409a565d087ba3e1ba": { + "balance": "0x6c6b935b8bbd400000" + }, + "64adcceec53dd9d9dd15c8cc1a9e736de4241d2c": { + "balance": "0x30927f74c9de00000" + }, + "64cf0935bf19d2cebbecd8780d27d2e2b2c34166": { + "balance": "0x6acb3df27e1f880000" + }, + "64d80c3b8ba68282290b75e65d8978a15a87782c": { + "balance": "0x6acb3df27e1f880000" + }, + "64dba2d6615b8bd7571836dc75bc79d314f5ecee": { + "balance": "0x21e19e0c9bab2400000" + }, + "64e0217a5b38aa40583625967fa9883690388b6f": { + "balance": "0xad78ebc5ac6200000" + }, + "64e02abb016cc23a2934f6bcddb681905021d563": { + "balance": "0x3635c9adc5dea00000" + }, + "64e03ef070a54703b7184e48276c5c0077ef4b34": { + "balance": "0x1158e460913d000000" + }, + "64e2de21200b1899c3a0c0653b5040136d0dc842": { + "balance": "0x43c33c1937564800000" + }, + "64ec8a5b743f3479e707dae9ee20ddaa4f40f1d9": { + "balance": "0xad78ebc5ac6200000" + }, + "6503860b191008c15583bfc88158099301762828": { + "balance": "0x3635c9adc5dea00000" + }, + "65053191319e067a25e6361d47f37f6318f83419": { + "balance": "0x155bd9307f9fe80000" + }, + "65093b239bbfba23c7775ca7da5a8648a9f54cf7": { + "balance": "0x15af1d78b58c400000" + }, + "6509eeb1347e842ffb413e37155e2cbc738273fd": { + "balance": "0x6c6b935b8bbd400000" + }, + "650b425555e4e4c51718146836a2c1ee77a5b421": { + "balance": "0x43c33c1937564800000" + }, + "650cf67db060cce17568d5f2a423687c49647609": { + "balance": "0x56bc75e2d63100000" + }, + "6510df42a599bcb0a519cca961b488759a6f6777": { + "balance": "0x6c6b935b8bbd400000" + }, + "653675b842d7d8b461f722b4117cb81dac8e639d": { + "balance": "0x1ae361fc1451c0000" + }, + "654b7e808799a83d7287c67706f2abf49a496404": { + "balance": "0x6acb3df27e1f880000" + }, + "654f524847b3a6acc0d3d5f1f362b603edf65f96": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "655934da8e744eaa3de34dbbc0894c4eda0b61f2": { + "balance": "0xad78ebc5ac6200000" + }, + "655d5cd7489629e2413c2105b5a172d933c27af8": { + "balance": "0xdb03186cd840a60000" + }, + "656018584130db83ab0591a8128d9381666a8d0e": { + "balance": "0x3779f912019fc0000" + }, + "6560941328ff587cbc56c38c78238a7bb5f442f6": { + "balance": "0x2861906b59c47a0000" + }, + "656579daedd29370d9b737ee3f5cd9d84bc2b342": { + "balance": "0x4d853c8f8908980000" + }, + "657473774f63ac3d6279fd0743d5790c4f161503": { + "balance": "0xad78ebc5ac6200000" + }, + "6580b1bc94390f04b397bd73e95d96ef11eaf3a8": { + "balance": "0x1158e460913d00000" + }, + "65849be1af20100eb8a3ba5a5be4d3ae8db5a70e": { + "balance": "0x15af1d78b58c400000" + }, + "659c0a72c767a3a65ced0e1ca885a4c51fd9b779": { + "balance": "0x6c6b935b8bbd400000" + }, + "65a52141f56bef98991724c6e7053381da8b5925": { + "balance": "0x3429c335d57fe0000" + }, + "65a9dad42e1632ba3e4e49623fab62a17e4d3611": { + "balance": "0x50c4cb2a10c600000" + }, + "65af8d8b5b1d1eedfa77bcbc96c1b133f83306df": { + "balance": "0x55005f0c614480000" + }, + "65af9087e05167715497c9a5a749189489004def": { + "balance": "0x2d43f3ebfafb2c0000" + }, + "65b42faecc1edfb14283ca979af545f63b30e60c": { + "balance": "0xfc936392801c0000" + }, + "65d33eb39cda6453b19e61c1fe4db93170ef9d34": { + "balance": "0xb98bc829a6f90000" + }, + "65d8dd4e251cbc021f05b010f2d5dc520c3872e0": { + "balance": "0x2d43579a36a90e0000" + }, + "65ea26eabbe2f64ccccfe06829c25d4637520225": { + "balance": "0x25f273933db5700000" + }, + "65ea67ad3fb56ad5fb94387dd38eb383001d7c68": { + "balance": "0x56bc75e2d63100000" + }, + "65ebaed27edb9dcc1957aee5f452ac2105a65c0e": { + "balance": "0x937dfadae25e29b8000" + }, + "65ee20b06d9ad589a7e7ce04b9f5f795f402aece": { + "balance": "0x6c6b935b8bbd400000" + }, + "65f534346d2ffb787fa9cf185d745ba42986bd6e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "65f5870f26bce089677dfc23b5001ee492483428": { + "balance": "0x112b1f155aa32a30000" + }, + "65fd02d704a12a4dace9471b0645f962a89671c8": { + "balance": "0x18d1ce6e427cd8000" + }, + "65ff874fafce4da318d6c93d57e2c38a0d73e820": { + "balance": "0x3638021cecdab00000" + }, + "660557bb43f4be3a1b8b85e7df7b3c5bcd548057": { + "balance": "0x14542ba12a337c00000" + }, + "66082c75a8de31a53913bbd44de3a0374f7faa41": { + "balance": "0x4f2591f896a6500000" + }, + "6611ce59a98b072ae959dc49ad511daaaaa19d6b": { + "balance": "0xad78ebc5ac6200000" + }, + "66201bd227ae6dc6bdfed5fbde811fecfe5e9dd9": { + "balance": "0x203e9e8492788c0000" + }, + "662334814724935b7931ddca6100e00d467727cd": { + "balance": "0x2288269d0783d40000" + }, + "66274fea82cd30b6c29b23350e4f4f3d310a5899": { + "balance": "0x70370550ab82980000" + }, + "662cfa038fab37a01745a364e1b98127c503746d": { + "balance": "0xd5967be4fc3f100000" + }, + "6635b46f711d2da6f0e16370cd8ee43efb2c2d52": { + "balance": "0x6c6b935b8bbd400000" + }, + "663604b0503046e624cd26a8b6fb4742dce02a6f": { + "balance": "0x38b9b797ef68c0000" + }, + "6636d7ac637a48f61d38b14cfd4865d36d142805": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "6640ccf053555c130ae2b656647ea6e31637b9ab": { + "balance": "0x6acb3df27e1f880000" + }, + "66424bd8785b8cb461102a900283c35dfa07ef6a": { + "balance": "0x22e2db26666fc8000" + }, + "664cd67dccc9ac8228b45c55db8d76550b659cdc": { + "balance": "0x155bd9307f9fe80000" + }, + "664e43119870af107a448db1278b044838ffcdaf": { + "balance": "0x15af1d78b58c400000" + }, + "6651736fb59b91fee9c93aa0bd6ea2f7b2506180": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "665b000f0b772750cc3c217a5ef429a92bf1ccbb": { + "balance": "0xd8d726b7177a800000" + }, + "66662006015c1f8e3ccfcaebc8ee6807ee196303": { + "balance": "0x1b1b3a1ac261ec0000" + }, + "666746fb93d1935c5a3c684e725010c4fad0b1d8": { + "balance": "0x1158e460913d00000" + }, + "666b4f37d55d63b7d056b615bb74c96b3b01991a": { + "balance": "0xd8d726b7177a800000" + }, + "66719c0682b2ac7f9e27abebec7edf8decf0ae0d": { + "balance": "0x1158e460913d00000" + }, + "6671b182c9f741a0cd3c356c73c23126d4f9e6f4": { + "balance": "0xad78ebc5ac6200000" + }, + "6679aeecd87a57a73f3356811d2cf49d0c4d96dc": { + "balance": "0x2086ac351052600000" + }, + "667b61c03bb937a9f5d0fc5a09f1ea3363c77035": { + "balance": "0xe664992288f2280000" + }, + "6685fd2e2544702c360b8bb9ee78f130dad16da5": { + "balance": "0x6c6b935b8bbd400000" + }, + "668b6ba8ab08eace39c502ef672bd5ccb6a67a20": { + "balance": "0x697d95d4201333c0000" + }, + "66925de3e43f4b41bf9dadde27d5488ef569ea0d": { + "balance": "0x222c8eb3ff6640000" + }, + "66b0c100c49149935d14c0dc202cce907cea1a3d": { + "balance": "0x6acb3df27e1f880000" + }, + "66b1a63da4dcd9f81fe54f5e3fcb4055ef7ec54f": { + "balance": "0xaeb272adf9cfa0000" + }, + "66b39837cb3cac8a802afe3f12a258bbca62dacd": { + "balance": "0x15af1d78b58c400000" + }, + "66c8331efe7198e98b2d32b938688e3241d0e24f": { + "balance": "0x2098051970e39d00000" + }, + "66cc8ab23c00d1b82acd7d73f38c99e0d05a4fa6": { + "balance": "0x56bc75e2d63100000" + }, + "66dcc5fb4ee7fee046e141819aa968799d644491": { + "balance": "0x487a9a304539440000" + }, + "66e09427c1e63deed7e12b8c55a6a19320ef4b6a": { + "balance": "0x93739534d28680000" + }, + "66ec16ee9caab411c55a6629e318de6ee216491d": { + "balance": "0x2ee449550898e40000" + }, + "66f50406eb1b11a946cab45927cca37470e5a208": { + "balance": "0x6c6b935b8bbd400000" + }, + "66fdc9fee351fa1538eb0d87d819fcf09e7c106a": { + "balance": "0x14627b5d93781b20000" + }, + "67048f3a12a4dd1f626c64264cb1d7971de2ca38": { + "balance": "0x9c2007651b2500000" + }, + "6704f169e0d0b36b57bbc39f3c45437b5ee3d28d": { + "balance": "0x155bd9307f9fe80000" + }, + "671015b97670b10d5e583f3d62a61c1c79c5143f": { + "balance": "0x15af1d78b58c400000" + }, + "6710c2c03c65992b2e774be52d3ab4a6ba217ef7": { + "balance": "0x274d656ac90e3400000" + }, + "671110d96aaff11523cc546bf9940eedffb2faf7": { + "balance": "0xd8d726b7177a800000" + }, + "6715c14035fb57bb3d667f7b707498c41074b855": { + "balance": "0x25f273933db5700000" + }, + "671bbca099ff899bab07ea1cf86965c3054c8960": { + "balance": "0x2b5e3af16b1880000" + }, + "6727daf5b9d68efcab489fedec96d7f7325dd423": { + "balance": "0x6c6b935b8bbd400000" + }, + "672cbca8440a8577097b19aff593a2ad9d28a756": { + "balance": "0x4563918244f400000" + }, + "672ec42faa8cd69aaa71b32cc7b404881d52ff91": { + "balance": "0x21e19e0c9bab2400000" + }, + "672fa0a019088db3166f6119438d07a99f8ba224": { + "balance": "0x2d4ca05e2b43ca80000" + }, + "673144f0ec142e770f4834fee0ee311832f3087b": { + "balance": "0x1b1b6bd7af64c70000" + }, + "67350b5331926f5e28f3c1e986f96443809c8b8c": { + "balance": "0x1314fb370629800000" + }, + "673706b1b0e4dc7a949a7a796258a5b83bb5aa83": { + "balance": "0x368c8623a8b4d100000" + }, + "6742a2cfce8d79a2c4a51b77747498912245cd6a": { + "balance": "0xdfd5b80b7e4680000" + }, + "674adb21df4c98c7a347ac4c3c24266757dd7039": { + "balance": "0x6c6b935b8bbd400000" + }, + "67518e5d02b205180f0463a32004471f753c523e": { + "balance": "0x6b918aac494b168000" + }, + "675d5caa609bf70a18aca580465d8fb7310d1bbb": { + "balance": "0x43c33c1937564800000" + }, + "67632046dcb25a54936928a96f423f3320cbed92": { + "balance": "0x6c6b935b8bbd400000" + }, + "6765df25280e8e4f38d4b1cf446fc5d7eb659e34": { + "balance": "0x56bc75e2d63100000" + }, + "6776e133d9dc354c12a951087b639650f539a433": { + "balance": "0x68155a43676e00000" + }, + "6785513cf732e47e87670770b5419be10cd1fc74": { + "balance": "0x6c6b935b8bbd400000" + }, + "679437eacf437878dc293d48a39c87b7421a216c": { + "balance": "0x37f81821db2680000" + }, + "679b9a109930517e8999099ccf2a914c4c8dd934": { + "balance": "0x340aad21b3b700000" + }, + "67a80e0190721f94390d6802729dd12c31a895ad": { + "balance": "0x6c6b1375bc91560000" + }, + "67b8a6e90fdf0a1cac441793301e8750a9fa7957": { + "balance": "0x30849ebe16369c0000" + }, + "67bc85e87dc34c4e80aafa066ba8d29dbb8e438e": { + "balance": "0x15d1cf4176aeba0000" + }, + "67c926093e9b8927933810d98222d62e2b8206bb": { + "balance": "0x678a932062e4180000" + }, + "67cfda6e70bf7657d39059b59790e5145afdbe61": { + "balance": "0x23050d095866580000" + }, + "67d682a282ef73fb8d6e9071e2614f47ab1d0f5e": { + "balance": "0x3635c9adc5dea00000" + }, + "67d6a8aa1bf8d6eaf7384e993dfdf10f0af68a61": { + "balance": "0xabcbb5718974b8000" + }, + "67da922effa472a6b124e84ea8f86b24e0f515aa": { + "balance": "0x1158e460913d00000" + }, + "67df242d240dd4b8071d72f8fcf35bb3809d71e8": { + "balance": "0xd8d726b7177a800000" + }, + "67ee406ea4a7ae6a3a381eb4edd2f09f174b4928": { + "balance": "0x3829635f0968b00000" + }, + "67f2bb78b8d3e11f7c458a10b5c8e0a1d374467d": { + "balance": "0x61093d7c2c6d380000" + }, + "67fc527dce1785f0fb8bc7e518b1c669f7ecdfb5": { + "balance": "0xd02ab486cedc00000" + }, + "68027d19558ed7339a08aee8de3559be063ec2ea": { + "balance": "0x6c6b935b8bbd400000" + }, + "680640838bd07a447b168d6d923b90cf6c43cdca": { + "balance": "0x5dc892aa1131c80000" + }, + "6807ddc88db489b033e6b2f9a81553571ab3c805": { + "balance": "0x19f8e7559924c0000" + }, + "680d5911ed8dd9eec45c060c223f89a7f620bbd5": { + "balance": "0x43c33c1937564800000" + }, + "6811b54cd19663b11b94da1de2448285cd9f68d9": { + "balance": "0x3ba1910bf341b00000" + }, + "68190ca885da4231874c1cfb42b1580a21737f38": { + "balance": "0xcf152640c5c8300000" + }, + "682897bc4f8e89029120fcffb787c01a93e64184": { + "balance": "0x21e19e0c9bab2400000" + }, + "68295e8ea5afd9093fc0a465d157922b5d2ae234": { + "balance": "0x1154e53217ddb0000" + }, + "682e96276f518d31d7e56e30dfb009c1218201bd": { + "balance": "0x1158e460913d00000" + }, + "6835c8e8b74a2ca2ae3f4a8d0f6b954a3e2a8392": { + "balance": "0x3429c335d57fe0000" + }, + "683633010a88686bea5a98ea53e87997cbf73e69": { + "balance": "0x56b394263a40c0000" + }, + "683dba36f7e94f40ea6aea0d79b8f521de55076e": { + "balance": "0x796e3ea3f8ab00000" + }, + "68419c6dd2d3ce6fcbb3c73e2fa079f06051bde6": { + "balance": "0x6acb3df27e1f880000" + }, + "68473b7a7d965904bedba556dfbc17136cd5d434": { + "balance": "0x56bc75e2d63100000" + }, + "6847825bdee8240e28042c83cad642f286a3bddc": { + "balance": "0x5150ae84a8cdf00000" + }, + "684a44c069339d08e19a75668bdba303be855332": { + "balance": "0xed2b525841adfc00000" + }, + "68531f4dda808f5320767a03113428ca0ce2f389": { + "balance": "0x10d3aa536e2940000" + }, + "687927e3048bb5162ae7c15cf76bd124f9497b9e": { + "balance": "0x6c6b935b8bbd400000" + }, + "68809af5d532a11c1a4d6e32aac75c4c52b08ead": { + "balance": "0x21e19e0c9bab2400000" + }, + "6886ada7bbb0617bda842191c68c922ea3a8ac82": { + "balance": "0x3ee23bde0e7d200000" + }, + "68883e152e5660fee59626e7e3b4f05110e6222f": { + "balance": "0xb94633be975a62a0000" + }, + "688a569e965524eb1d0ac3d3733eab909fb3d61e": { + "balance": "0x478eae0e571ba00000" + }, + "688eb3853bbcc50ecfee0fa87f0ab693cabdef02": { + "balance": "0x6b10a18400647c00000" + }, + "68a7425fe09eb28cf86eb1793e41b211e57bd68d": { + "balance": "0x243d4d18229ca20000" + }, + "68a86c402388fddc59028fec7021e98cbf830eac": { + "balance": "0x10910d4cdc9f60000" + }, + "68acdaa9fb17d3c309911a77b05f5391fa034ee9": { + "balance": "0x1e52e336cde22180000" + }, + "68addf019d6b9cab70acb13f0b3117999f062e12": { + "balance": "0x2b51212e6b7c88000" + }, + "68b31836a30a016ada157b638ac15da73f18cfde": { + "balance": "0x168d28e3f00280000" + }, + "68b6854788a7c6496cdbf5f84b9ec5ef392b78bb": { + "balance": "0x42bf06b78ed3b500000" + }, + "68c08490c89bf0d6b6f320b1aca95c8312c00608": { + "balance": "0xd8d726b7177a800000" + }, + "68c7d1711b011a33f16f1f55b5c902cce970bdd7": { + "balance": "0x83d6c7aab63600000" + }, + "68c8791dc342c373769ea61fb7b510f251d32088": { + "balance": "0x3635c9adc5dea00000" + }, + "68df947c495bebaeb8e889b3f953d533874bf106": { + "balance": "0x1d9945ab2b03480000" + }, + "68e8022740f4af29eb48db32bcecddfd148d3de3": { + "balance": "0x3635c9adc5dea00000" + }, + "68ec79d5be7155716c40941c79d78d17de9ef803": { + "balance": "0x1b233877b5208c0000" + }, + "68eec1e288ac31b6eaba7e1fbd4f04ad579a6b5d": { + "balance": "0x6c6b935b8bbd400000" + }, + "68f525921dc11c329b754fbf3e529fc723c834cd": { + "balance": "0x57473d05dabae80000" + }, + "68f719ae342bd7fef18a05cbb02f705ad38ed5b2": { + "balance": "0x38ebad5cdc90280000" + }, + "68f7573cd457e14c03fea43e302d30347c10705c": { + "balance": "0x10f0cf064dd59200000" + }, + "68f8f45155e98c5029a4ebc5b527a92e9fa83120": { + "balance": "0xf07b44b40793208000" + }, + "68fe1357218d095849cd579842c4aa02ff888d93": { + "balance": "0x6c6b935b8bbd400000" + }, + "690228e4bb12a8d4b5e0a797b0c5cf2a7509131e": { + "balance": "0x65ea3db75546600000" + }, + "690594d306613cd3e2fd24bca9994ad98a3d73f8": { + "balance": "0x6c6b935b8bbd400000" + }, + "69073269729e6414b26ec8dc0fd935c73b579f1e": { + "balance": "0x65a4da25d3016c00000" + }, + "6919dd5e5dfb1afa404703b9faea8cee35d00d70": { + "balance": "0x14061b9d77a5e980000" + }, + "693492a5c51396a482881669ccf6d8d779f00951": { + "balance": "0x12bf50503ae3038000" + }, + "693d83be09459ef8390b2e30d7f7c28de4b4284e": { + "balance": "0x6c6b935b8bbd400000" + }, + "69517083e303d4fbb6c2114514215d69bc46a299": { + "balance": "0x56bc75e2d63100000" + }, + "695550656cbf90b75d92ad9122d90d23ca68ca4d": { + "balance": "0x3635c9adc5dea00000" + }, + "6958f83bb2fdfb27ce0409cd03f9c5edbf4cbedd": { + "balance": "0x43c33c1937564800000" + }, + "695b0f5242753701b264a67071a2dc880836b8db": { + "balance": "0xe398811bec680000" + }, + "695b4cce085856d9e1f9ff3e79942023359e5fbc": { + "balance": "0x10f0cf064dd59200000" + }, + "6966063aa5de1db5c671f3dd699d5abe213ee902": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "6974c8a414ceaefd3c2e4dfdbef430568d9a960b": { + "balance": "0x121ea68c114e510000" + }, + "6978696d5150a9a263513f8f74c696f8b1397cab": { + "balance": "0x167f482d3c5b1c00000" + }, + "69797bfb12c9bed682b91fbc593591d5e4023728": { + "balance": "0x21e19e0c9bab2400000" + }, + "697f55536bf85ada51841f0287623a9f0ed09a17": { + "balance": "0x21e19e0c9bab2400000" + }, + "6982fe8a867e93eb4a0bd051589399f2ec9a5292": { + "balance": "0x6c6b935b8bbd400000" + }, + "698a8a6f01f9ab682f637c7969be885f6c5302bf": { + "balance": "0x10d3aa536e2940000" + }, + "698ab9a2f33381e07c0c47433d0d21d6f336b127": { + "balance": "0x1158e460913d00000" + }, + "6994fb3231d7e41d491a9d68d1fa4cae2cc15960": { + "balance": "0xd8d726b7177a800000" + }, + "699c9ee47195511f35f862ca4c22fd35ae8ffbf4": { + "balance": "0x4563918244f400000" + }, + "699fc6d68a4775573c1dcdaec830fefd50397c4e": { + "balance": "0x340aad21b3b700000" + }, + "69af28b0746cac0da17084b9398c5e36bb3a0df2": { + "balance": "0x3677036edf0af60000" + }, + "69b80ed90f84834afa3ff82eb964703b560977d6": { + "balance": "0x1731790534df20000" + }, + "69b81d5981141ec7a7141060dfcf8f3599ffc63e": { + "balance": "0x10f0cf064dd59200000" + }, + "69bcfc1d43b4ba19de7b274bdffb35139412d3d7": { + "balance": "0x35659ef93f0fc40000" + }, + "69bd25ade1a3346c59c4e930db2a9d715ef0a27a": { + "balance": "0xd8d726b7177a800000" + }, + "69c08d744754de709ce96e15ae0d1d395b3a2263": { + "balance": "0x3635c9adc5dea00000" + }, + "69c2d835f13ee90580408e6a3283c8cca6a434a2": { + "balance": "0x238fd42c5cf0400000" + }, + "69c94e07c4a9be3384d95dfa3cb9290051873b7b": { + "balance": "0x3cb71f51fc5580000" + }, + "69cb3e2153998d86e5ee20c1fcd1a6baeeb2863f": { + "balance": "0xd8d726b7177a800000" + }, + "69d39d510889e552a396135bfcdb06e37e387633": { + "balance": "0xd8d726b7177a800000" + }, + "69d98f38a3ba3dbc01fa5c2c1427d862832f2f70": { + "balance": "0x152d02c7e14af6800000" + }, + "69e2e2e704307ccc5b5ca3f164fece2ea7b2e512": { + "balance": "0x17b7883c06916600000" + }, + "69ff429074cb9b6c63bc914284bce5f0c8fbf7d0": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "69ff8901b541763f817c5f2998f02dcfc1df2997": { + "balance": "0x22b1c8c1227a00000" + }, + "6a023af57d584d845e698736f130db9db40dfa9a": { + "balance": "0x55b201c8900980000" + }, + "6a04f5d53fc0f515be942b8f12a9cb7ab0f39778": { + "balance": "0xa9aab3459be1940000" + }, + "6a05b21c4f17f9d73f5fb2b0cb89ff5356a6cc7e": { + "balance": "0x5150ae84a8cdf00000" + }, + "6a0f056066c2d56628850273d7ecb7f8e6e9129e": { + "balance": "0x10f0d293cc7a5880000" + }, + "6a13d5e32c1fd26d7e91ff6e053160a89b2c8aad": { + "balance": "0x2e62f20a69be40000" + }, + "6a2e86469a5bf37cee82e88b4c3863895d28fcaf": { + "balance": "0x1c229266385bbc0000" + }, + "6a3694424c7cc6b8bcd9bccaba540cc1f5df18d7": { + "balance": "0x6c6b935b8bbd400000" + }, + "6a42ca971c6578d5ade295c3e7f4ad331dd3424e": { + "balance": "0x14542ba12a337c00000" + }, + "6a44af96b3f032ae641beb67f4b6c83342d37c5d": { + "balance": "0x19274b259f6540000" + }, + "6a4c8907b600248057b1e46354b19bdc859c991a": { + "balance": "0x1158e460913d00000" + }, + "6a514e6242f6b68c137e97fea1e78eb555a7e5f7": { + "balance": "0x1158e460913d00000" + }, + "6a53d41ae4a752b21abed5374649953a513de5e5": { + "balance": "0x6c6b935b8bbd400000" + }, + "6a6159074ab573e0ee581f0f3df2d6a594629b74": { + "balance": "0x10ce1d3d8cb3180000" + }, + "6a6337833f8f6a6bf10ca7ec21aa810ed444f4cb": { + "balance": "0x37bd24345ce8a40000" + }, + "6a6353b971589f18f2955cba28abe8acce6a5761": { + "balance": "0xa2a15d09519be00000" + }, + "6a63fc89abc7f36e282d80787b7b04afd6553e71": { + "balance": "0x8ac7230489e800000" + }, + "6a679e378fdce6bfd97fe62f043c6f6405d79e99": { + "balance": "0xd8d726b7177a800000" + }, + "6a686bf220b593deb9b7324615fb9144ded3f39d": { + "balance": "0x4f2591f896a6500000" + }, + "6a6b18a45a76467e2e5d5a2ef911c3e12929857b": { + "balance": "0x115d3a99a9614f400000" + }, + "6a74844d8e9cb5581c45079a2e94462a6cee8821": { + "balance": "0x3ab53a552dd4c90000" + }, + "6a7b2e0d88867ff15d207c222bebf94fa6ce8397": { + "balance": "0xcb49b44ba602d800000" + }, + "6a7c252042e7468a3ff773d6450bba85efa26391": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "6a8a4317c45faa0554ccdb482548183e295a24b9": { + "balance": "0x3635c9adc5dea00000" + }, + "6a8cea2de84a8df997fd3f84e3083d93de57cda9": { + "balance": "0x56be03ca3e47d8000" + }, + "6a9758743b603eea3aa0524b42889723c4153948": { + "balance": "0x22385a827e815500000" + }, + "6aa5732f3b86fb8c81efbe6b5b47b563730b06c8": { + "balance": "0x3635c9adc5dea00000" + }, + "6ab323ae5056ed0a453072c5abe2e42fcf5d7139": { + "balance": "0x2fb474098f67c00000" + }, + "6ab5b4c41cddb829690c2fda7f20c85e629dd5d5": { + "balance": "0x64d4af714c32900000" + }, + "6ac40f532dfee5118117d2ad352da77d4f6da2c8": { + "balance": "0x15af1d78b58c400000" + }, + "6ac4d4be2db0d99da3faaaf7525af282051d6a90": { + "balance": "0x458ca58a962b28000" + }, + "6acddca3cd2b4990e25cd65c24149d0912099e79": { + "balance": "0xa2a1e07c9f6c908000" + }, + "6ad90be252d9cd464d998125fab693060ba8e429": { + "balance": "0xd8d726b7177a800000" + }, + "6add932193cd38494aa3f03aeccc4b7ab7fabca2": { + "balance": "0x4db73254763000000" + }, + "6ae57f27917c562a132a4d1bf7ec0ac785832926": { + "balance": "0x14542ba12a337c00000" + }, + "6aeb9f74742ea491813dbbf0d6fcde1a131d4db3": { + "balance": "0x17e554308aa0300000" + }, + "6af235d2bbe050e6291615b71ca5829658810142": { + "balance": "0xa2a15d09519be00000" + }, + "6af6c7ee99df271ba15bf384c0b764adcb4da182": { + "balance": "0x36356633ebd8ea0000" + }, + "6af8e55969682c715f48ad4fc0fbb67eb59795a3": { + "balance": "0x6c6b935b8bbd400000" + }, + "6af940f63ec9b8d876272aca96fef65cdacecdea": { + "balance": "0xa2a15d09519be00000" + }, + "6af9f0dfeeaebb5f64bf91ab771669bf05295553": { + "balance": "0x15af1d78b58c400000" + }, + "6aff1466c2623675e3cb0e75e423d37a25e442eb": { + "balance": "0x5dc892aa1131c80000" + }, + "6b0da25af267d7836c226bcae8d872d2ce52c941": { + "balance": "0x14542ba12a337c00000" + }, + "6b10f8f8b3e3b60de90aa12d155f9ff5ffb22c50": { + "balance": "0x6c6b935b8bbd400000" + }, + "6b17598a8ef54f797ae515ccb6517d1859bf8011": { + "balance": "0x56bc75e2d63100000" + }, + "6b20c080606a79c73bd8e75b11717a4e8db3f1c3": { + "balance": "0x103f735803f0140000" + }, + "6b2284440221ce16a8382de5ff0229472269deec": { + "balance": "0x3635c9adc5dea00000" + }, + "6b30f1823910b86d3acb5a6afc9defb6f3a30bf8": { + "balance": "0xe3aeb5737240a00000" + }, + "6b38de841fad7f53fe02da115bd86aaf662466bd": { + "balance": "0x5dc892aa1131c80000" + }, + "6b4b99cb3fa9f7b74ce3a48317b1cd13090a1a7a": { + "balance": "0x31b327e695de20000" + }, + "6b5ae7bf78ec75e90cb503c778ccd3b24b4f1aaf": { + "balance": "0x2b5e3af16b18800000" + }, + "6b63a2dfb2bcd0caec0022b88be30c1451ea56aa": { + "balance": "0x2bdb6bf91f7f4c8000" + }, + "6b6577f3909a4d6de0f411522d4570386400345c": { + "balance": "0x65ea3db75546600000" + }, + "6b72a8f061cfe6996ad447d3c72c28c0c08ab3a7": { + "balance": "0xe78c6ac79912620000" + }, + "6b760d4877e6a627c1c967bee451a8507ddddbab": { + "balance": "0x3154c9729d05780000" + }, + "6b83bae7b565244558555bcf4ba8da2011891c17": { + "balance": "0x6c6b935b8bbd400000" + }, + "6b925dd5d8ed6132ab6d0860b82c44e1a51f1fee": { + "balance": "0x503b203e9fba200000" + }, + "6b94615db750656ac38c7e1cf29a9d13677f4e15": { + "balance": "0x28a857425466f800000" + }, + "6b951a43274eeafc8a0903b0af2ec92bf1efc839": { + "balance": "0x56bc75e2d63100000" + }, + "6b992521ec852370848ad697cc2df64e63cc06ff": { + "balance": "0x3635c9adc5dea00000" + }, + "6ba8f7e25fc2d871618e24e40184199137f9f6aa": { + "balance": "0x15af64869a6bc20000" + }, + "6ba9b21b35106be159d1c1c2657ac56cd29ffd44": { + "balance": "0xf2dc7d47f156000000" + }, + "6baf7a2a02ae78801e8904ad7ac05108fc56cff6": { + "balance": "0x3635c9adc5dea00000" + }, + "6bb2aca23fa1626d18efd6777fb97db02d8e0ae4": { + "balance": "0x878678326eac9000000" + }, + "6bb4a661a33a71d424d49bb5df28622ed4dffcf4": { + "balance": "0x222c8eb3ff66400000" + }, + "6bb50813146a9add42ee22038c9f1f7469d47f47": { + "balance": "0xada55474b81340000" + }, + "6bbc3f358a668dd1a11f0380f3f73108426abd4a": { + "balance": "0xd8d726b7177a800000" + }, + "6bbd1e719390e6b91043f8b6b9df898ea8001b34": { + "balance": "0x6c6c4fa6c3da588000" + }, + "6bc85acd5928722ef5095331ee88f484b8cf8357": { + "balance": "0x9c2007651b2500000" + }, + "6bd3e59f239fafe4776bb9bddd6bee83ba5d9d9f": { + "balance": "0x3635c9adc5dea00000" + }, + "6bd457ade051795df3f2465c3839aed3c5dee978": { + "balance": "0x3634bf39ab98788000" + }, + "6be16313643ebc91ff9bb1a2e116b854ea933a45": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "6be7595ea0f068489a2701ec4649158ddc43e178": { + "balance": "0x6c6b935b8bbd400000" + }, + "6be9030ee6e2fbc491aca3de4022d301772b7b7d": { + "balance": "0x1731790534df20000" + }, + "6bec311ad05008b4af353c958c40bd06739a3ff3": { + "balance": "0x377f62a0f0a62700000" + }, + "6bf7b3c065f2c1e7c6eb092ba0d15066f393d1b8": { + "balance": "0x15af1d78b58c400000" + }, + "6bf86f1e2f2b8032a95c4d7738a109d3d0ed8104": { + "balance": "0x62a992e53a0af00000" + }, + "6c05e34e5ef2f42ed09deff1026cd66bcb6960bb": { + "balance": "0x6c6b935b8bbd400000" + }, + "6c08a6dc0173c7342955d1d3f2c065d62f83aec7": { + "balance": "0x1158e460913d00000" + }, + "6c0ae9f043c834d44271f13406593dfe094f389f": { + "balance": "0x52442ae133b62a8000" + }, + "6c0cc917cbee7d7c099763f14e64df7d34e2bf09": { + "balance": "0xd8d726b7177a80000" + }, + "6c0e712f405c59725fe829e9774bf4df7f4dd965": { + "balance": "0xc2868889ca68a440000" + }, + "6c101205b323d77544d6dc52af37aca3cec6f7f1": { + "balance": "0x21e19e0c9bab2400000" + }, + "6c15ec3520bf8ebbc820bd0ff19778375494cf9d": { + "balance": "0x6cb7e74867d5e60000" + }, + "6c1ddd33c81966dc8621776071a4129482f2c65f": { + "balance": "0x878678326eac9000000" + }, + "6c25327f8dcbb2f45e561e86e35d8850e53ab059": { + "balance": "0x3bcdf9bafef2f00000" + }, + "6c2e9be6d4ab450fd12531f33f028c614674f197": { + "balance": "0xc2127af858da700000" + }, + "6c359e58a13d4578a9338e335c67e7639f5fb4d7": { + "balance": "0xbd15b94fc8b280000" + }, + "6c3d18704126aa99ee3342ce60f5d4c85f1867cd": { + "balance": "0x2b5e3af16b1880000" + }, + "6c474bc66a54780066aa4f512eefa773abf919c7": { + "balance": "0x5188315f776b80000" + }, + "6c4e426e8dc005dfa3516cb8a680b02eea95ae8e": { + "balance": "0x487a9a304539440000" + }, + "6c52cf0895bb35e656161e4dc46ae0e96dd3e62c": { + "balance": "0xd8d8583fa2d52f0000" + }, + "6c5422fb4b14e6d98b6091fdec71f1f08640419d": { + "balance": "0x15af1d78b58c400000" + }, + "6c5c3a54cda7c2f118edba434ed81e6ebb11dd7a": { + "balance": "0xad78ebc5ac6200000" + }, + "6c63f84556d290bfcd99e434ee9997bfd779577a": { + "balance": "0x6c6b935b8bbd400000" + }, + "6c63fc85029a2654d79b2bea4de349e4524577c5": { + "balance": "0x23c757072b8dd00000" + }, + "6c6564e5c9c24eaaa744c9c7c968c9e2c9f1fbae": { + "balance": "0x499b42a21139640000" + }, + "6c67d6db1d03516c128b8ff234bf3d49b26d2941": { + "balance": "0x152d02c7e14af6800000" + }, + "6c67e0d7b62e2a08506945a5dfe38263339f1f22": { + "balance": "0x6acb3df27e1f880000" + }, + "6c6aa0d30b64721990b9504a863fa0bfb5e57da7": { + "balance": "0x925e06eec972b00000" + }, + "6c714a58fff6e97d14b8a5e305eb244065688bbd": { + "balance": "0xd8d726b7177a800000" + }, + "6c800d4b49ba07250460f993b8cbe00b266a2553": { + "balance": "0x1ab2cf7c9f87e20000" + }, + "6c808cabb8ff5fbb6312d9c8e84af8cf12ef0875": { + "balance": "0xd8d8583fa2d52f0000" + }, + "6c822029218ac8e98a260c1e064029348839875b": { + "balance": "0x10f97b787e1e3080000" + }, + "6c84cba77c6db4f7f90ef13d5ee21e8cfc7f8314": { + "balance": "0x6c6b935b8bbd400000" + }, + "6c8687e3417710bb8a93559021a1469e6a86bc77": { + "balance": "0x25b2da278d96b7b8000" + }, + "6c882c27732cef5c7c13a686f0a2ea77555ac289": { + "balance": "0x152d02c7e14af6800000" + }, + "6ca5de00817de0cedce5fd000128dede12648b3c": { + "balance": "0x1158e460913d00000" + }, + "6ca6a132ce1cd288bee30ec7cfeffb85c1f50a54": { + "balance": "0x6c6b935b8bbd400000" + }, + "6cb11ecb32d3ce829601310636f5a10cf7cf9b5f": { + "balance": "0x43fe8949c3801f50000" + }, + "6cc1c878fa6cde8a9a0b8311247e741e4642fe6d": { + "balance": "0x35659ef93f0fc40000" + }, + "6ccb03acf7f53ce87aadcc21a9932de915f89804": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "6cd212aee04e013f3d2abad2a023606bfb5c6ac7": { + "balance": "0x6c6acc67d7b1d40000" + }, + "6cd228dc712169307fe27ceb7477b48cfc8272e5": { + "balance": "0x434ea94db8a500000" + }, + "6ce1b0f6adc47051e8ab38b39edb4186b03babcc": { + "balance": "0x41799794cd24cc0000" + }, + "6ceae3733d8fa43d6cd80c1a96e8eb93109c83b7": { + "balance": "0x102794ad20da680000" + }, + "6d0569e5558fc7df2766f2ba15dc8aeffc5beb75": { + "balance": "0xd8e6001e6c302b0000" + }, + "6d120f0caae44fd94bcafe55e2e279ef96ba5c7a": { + "balance": "0xd8d726b7177a800000" + }, + "6d1456fff0104ee844a3314737843338d24cd66c": { + "balance": "0x7b06ce87fdd680000" + }, + "6d20ef9704670a500bb269b5832e859802049f01": { + "balance": "0x70c1cc73b00c80000" + }, + "6d2f976734b9d0070d1883cf7acab8b3e4920fc1": { + "balance": "0x21e19e0c9bab2400000" + }, + "6d39a9e98f81f769d73aad2cead276ac1387babe": { + "balance": "0x155bd9307f9fe80000" + }, + "6d3b7836a2b9d899721a4d237b522385dce8dfcd": { + "balance": "0x3636c25e66ece70000" + }, + "6d3f2ba856ccbb0237fa7661156b14b013f21240": { + "balance": "0x3635c9adc5dea00000" + }, + "6d4008b4a888a826f248ee6a0b0dfde9f93210b9": { + "balance": "0x127fcb8afae20d00000" + }, + "6d40ca27826d97731b3e86effcd7b92a4161fe89": { + "balance": "0x6c6b935b8bbd400000" + }, + "6d44974a31d187eda16ddd47b9c7ec5002d61fbe": { + "balance": "0x32f51edbaaa3300000" + }, + "6d4b5c05d06a20957e1748ab6df206f343f92f01": { + "balance": "0x21f360699bf825f8000" + }, + "6d4cbf3d8284833ae99344303e08b4d614bfda3b": { + "balance": "0x28a857425466f800000" + }, + "6d59b21cd0e2748804d9abe064eac2bef0c95f27": { + "balance": "0x6c6b935b8bbd400000" + }, + "6d63d38ee8b90e0e6ed8f192eda051b2d6a58bfd": { + "balance": "0x1a055690d9db80000" + }, + "6d6634b5b8a40195d949027af4828802092ceeb6": { + "balance": "0xa2a15d09519be00000" + }, + "6d7d1c949511f88303808c60c5ea0640fcc02683": { + "balance": "0x21e19e0c9bab2400000" + }, + "6d846dc12657e91af25008519c3e857f51707dd6": { + "balance": "0xf8d30bc92342f80000" + }, + "6d9193996b194617211106d1635eb26cc4b66c6c": { + "balance": "0x15aa1e7e9dd51c0000" + }, + "6d9997509882027ea947231424bedede2965d0ba": { + "balance": "0x6c81c7b31195e00000" + }, + "6da0ed8f1d69339f059f2a0e02471cb44fb8c3bb": { + "balance": "0x32bc38bb63a8160000" + }, + "6db72bfd43fef465ca5632b45aab7261404e13bf": { + "balance": "0x6c6b935b8bbd400000" + }, + "6dbe8abfa1742806263981371bf3d35590806b6e": { + "balance": "0x43c33c1937564800000" + }, + "6dc3f92baa1d21dab7382b893261a0356fa7c187": { + "balance": "0x5dc892aa1131c80000" + }, + "6dc7053a718616cfc78bee6382ee51add0c70330": { + "balance": "0x6c6b935b8bbd400000" + }, + "6dcc7e64fcafcbc2dc6c0e5e662cb347bffcd702": { + "balance": "0x43c33c1937564800000" + }, + "6dda5f788a6c688ddf921fa3852eb6d6c6c62966": { + "balance": "0x22b1c8c1227a00000" + }, + "6ddb6092779d5842ead378e21e8120fd4c6bc132": { + "balance": "0x6c6b935b8bbd400000" + }, + "6ddfef639155daab0a5cb4953aa8c5afaa880453": { + "balance": "0x62a992e53a0af00000" + }, + "6de02f2dd67efdb7393402fa9eaacbcf589d2e56": { + "balance": "0x40138b917edfb80000" + }, + "6de4b581385cf7fc9fe8c77d131fe2ee7724c76a": { + "balance": "0x7d2997733dcce40000" + }, + "6de4d15219182faf3aa2c5d4d2595ff23091a727": { + "balance": "0x55a6e79ccd1d300000" + }, + "6dedf62e743f4d2c2a4b87a787f5424a7aeb393c": { + "balance": "0x9c2007651b2500000" + }, + "6df24f6685a62f791ba337bf3ff67e91f3d4bc3a": { + "balance": "0x756b49d40a48180000" + }, + "6df5c84f7b909aab3e61fe0ecb1b3bf260222ad2": { + "balance": "0xd8d726b7177a800000" + }, + "6dff90e6dc359d2590882b1483edbcf887c0e423": { + "balance": "0x3635c9adc5dea00000" + }, + "6e01e4ad569c95d007ada30d5e2db12888492294": { + "balance": "0xd8d726b7177a800000" + }, + "6e073b66d1b8c66744d88096a8dd99ec7e0228da": { + "balance": "0xd8d726b7177a800000" + }, + "6e0ee70612c976287d499ddfa6c0dcc12c06deea": { + "balance": "0x70bd5b95621460000" + }, + "6e12b51e225b4a4372e59ad7a2a1a13ea3d3a137": { + "balance": "0x30046c8cc775f040000" + }, + "6e1a046caf5b4a57f4fd4bc173622126b4e2fd86": { + "balance": "0x61093d7c2c6d380000" + }, + "6e1ea4b183e252c9bb7767a006d4b43696cb8ae9": { + "balance": "0xff3783c85eed08000" + }, + "6e255b700ae7138a4bacf22888a9e2c00a285eec": { + "balance": "0xd8d726b7177a800000" + }, + "6e270ad529f1f0b8d9cb6d2427ec1b7e2dc64a74": { + "balance": "0xad78ebc5ac6200000" + }, + "6e2eab85dc89fe29dc0aa1853247dab43a523d56": { + "balance": "0x4563918244f400000" + }, + "6e3a51db743d334d2fe88224b5fe7c008e80e624": { + "balance": "0x5bf0ba6634f680000" + }, + "6e4c2ab7db026939dbd3bc68384af660a61816b2": { + "balance": "0x90d972f32323c0000" + }, + "6e4d2e39c8836629e5b487b1918a669aebdd9536": { + "balance": "0x3635c9adc5dea00000" + }, + "6e5c2d9b1c546a86eefd5d0a5120c9e4e730190e": { + "balance": "0xad201a6794ff80000" + }, + "6e60aee1a78f8eda8b424c73e353354ae67c3042": { + "balance": "0xbd35a48d9919e60000" + }, + "6e64e6129f224e378c0e6e736a7e7a06c211e9ec": { + "balance": "0x3635c9adc5dea00000" + }, + "6e6d5bbbb9053b89d744a27316c2a7b8c09b547d": { + "balance": "0x3152710a023e6d8000" + }, + "6e72b2a1186a8e2916543b1cb36a68870ea5d197": { + "balance": "0xa1544be879ea80000" + }, + "6e761eaa0f345f777b5441b73a0fa5b56b85f22d": { + "balance": "0x6c6b935b8bbd400000" + }, + "6e79edd4845b076e4cd88d188b6e432dd93f35aa": { + "balance": "0x33c5499031720c0000" + }, + "6e8212b722afd408a7a73ed3e2395ee6454a0330": { + "balance": "0x89e917994f71c0000" + }, + "6e84876dbb95c40b6656e42ba9aea08a993b54dc": { + "balance": "0x3bbc60e3b6cbbe0000" + }, + "6e84c2fd18d8095714a96817189ca21cca62bab1": { + "balance": "0x127b6c702621cd8000" + }, + "6e866d032d405abdd65cf651411d803796c22311": { + "balance": "0x6c6b935b8bbd400000" + }, + "6e899e59a9b41ab7ea41df7517860f2acb59f4fd": { + "balance": "0x43c33c1937564800000" + }, + "6e89c51ea6de13e06cdc748b67c4410fe9bcab03": { + "balance": "0xd8d726b7177a800000" + }, + "6e8a26689f7a2fdefd009cbaaa5310253450daba": { + "balance": "0x6f213717bad8d30000" + }, + "6e96faeda3054302c45f58f161324c99a3eebb62": { + "balance": "0x1158e460913d00000" + }, + "6eb0a5a9ae96d22cf01d8fd6483b9f38f08c2c8b": { + "balance": "0xd8d726b7177a800000" + }, + "6eb3819617404058268f0c3cff3596bfe9148c1c": { + "balance": "0x5a87e7d7f5f6580000" + }, + "6eb5578a6bb7c32153195b0d8020a6914852c059": { + "balance": "0x8bc2abf40221f4800000" + }, + "6ebb5e6957aa821ef659b6018a393a504cae4450": { + "balance": "0x6c6b935b8bbd400000" + }, + "6ebcf9957f5fc5e985add475223b04b8c14a7aed": { + "balance": "0x5dc892aa1131c80000" + }, + "6ec3659571b11f889dd439bcd4d67510a25be57e": { + "balance": "0x6aaf7c8516d0c0000" + }, + "6ec89b39f9f5276a553e8da30e6ec17aa47eefc7": { + "balance": "0x18424f5f0b1b4e0000" + }, + "6ec96d13bdb24dc7a557293f029e02dd74b97a55": { + "balance": "0xd8d726b7177a800000" + }, + "6ecaefa6fc3ee534626db02c6f85a0c395571e77": { + "balance": "0x2086ac351052600000" + }, + "6ed2a12b02f8c688c7b5d3a6ea14d63687dab3b6": { + "balance": "0x6c6b935b8bbd400000" + }, + "6ed884459f809dfa1016e770edaf3e9fef46fa30": { + "balance": "0xb852d6782093f10000" + }, + "6edf7f5283725c953ee64317f66188af1184b033": { + "balance": "0x1b464311d45a6880000" + }, + "6ee8aad7e0a065d8852d7c3b9a6e5fdc4bf50c00": { + "balance": "0x1158e460913d00000" + }, + "6eefdc850e87b715c72791773c0316c3559b58a4": { + "balance": "0xd8d726b7177a800000" + }, + "6ef9e8c9b6217d56769af97dbb1c8e1b8be799d2": { + "balance": "0x9ddc1e3b901180000" + }, + "6efba8fb2ac5b6730729a972ec224426a287c3ad": { + "balance": "0xf5985fbcbe1680000" + }, + "6efd90b535e00bbd889fda7e9c3184f879a151db": { + "balance": "0x22385a827e815500000" + }, + "6f051666cb4f7bd2b1907221b829b555d7a3db74": { + "balance": "0x5f68e8131ecf800000" + }, + "6f0edd23bcd85f6015f9289c28841fe04c83efeb": { + "balance": "0x10910d4cdc9f60000" + }, + "6f137a71a6f197df2cbbf010dcbd3c444ef5c925": { + "balance": "0x6c6b935b8bbd400000" + }, + "6f176065e88e3c6fe626267d18a088aaa4db80bc": { + "balance": "0xbed1d0263d9f000000" + }, + "6f18ec767e320508195f1374500e3f2e125689ff": { + "balance": "0x3635c9adc5dea00000" + }, + "6f1f4907b8f61f0c51568d692806b382f50324f5": { + "balance": "0x6c6b935b8bbd400000" + }, + "6f24c9af2b763480515d1b0951bb77a540f1e3f9": { + "balance": "0x6acb3df27e1f880000" + }, + "6f2576da4de283bbe8e3ee69ddd66e5e711db3f5": { + "balance": "0x44591d67fecc800000" + }, + "6f29bb375be5ed34ed999bb830ee2957dde76d16": { + "balance": "0x6c6b935b8bbd400000" + }, + "6f2a31900e240395b19f159c1d00dfe4d898ebdf": { + "balance": "0x6c660645aa47180000" + }, + "6f2a42e6e033d01061131929f7a6ee1538021e52": { + "balance": "0x6c6b935b8bbd400000" + }, + "6f39cc37caaa2ddc9b610f6131e0619fae772a3c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "6f44ca09f0c6a8294cbd519cdc594ad42c67579f": { + "balance": "0x2b5e3af16b1880000" + }, + "6f50929777824c291a49c46dc854f379a6bea080": { + "balance": "0x138400eca364a00000" + }, + "6f6cf20649a9e973177ac67dbadee4ebe5c7bdda": { + "balance": "0x11363297d01a8600000" + }, + "6f791d359bc3536a315d6382b88311af8ed6da47": { + "balance": "0x4fcc1a89027f00000" + }, + "6f794dbdf623daa6e0d00774ad6962737c921ea4": { + "balance": "0x6c6b935b8bbd400000" + }, + "6f7ac681d45e418fce8b3a1db5bc3be6f06c9849": { + "balance": "0x6c6b935b8bbd400000" + }, + "6f81f3abb1f933b1df396b8e9cc723a89b7c9806": { + "balance": "0xf2dc7d47f15600000" + }, + "6f8f0d15cc96fb7fe94f1065bc6940f8d12957b2": { + "balance": "0x3635c9adc5dea00000" + }, + "6f92d6e4548c78996509ee684b2ee29ba3c532b4": { + "balance": "0x3635c9adc5dea00000" + }, + "6fa60df818a5446418b1bbd62826e0b9825e1318": { + "balance": "0x2cb92cc8f6714400000" + }, + "6fa6388d402b30afe59934c3b9e13d1186476018": { + "balance": "0x24521e2a3017b80000" + }, + "6fa72015fa78696efd9a86174f7f1f21019286b1": { + "balance": "0x487a9a304539440000" + }, + "6fc25e7e00ca4f60a9fe6f28d1fde3542e2d1079": { + "balance": "0x2aef353bcddd600000" + }, + "6fc53662371dca587b59850de78606e2359df383": { + "balance": "0x9c2007651b2500000" + }, + "6fcc2c732bdd934af6ccd16846fb26ef89b2aa9b": { + "balance": "0x21e2b1d42261d490000" + }, + "6fd4e0f3f32bee6d3767fdbc9d353a6d3aab7899": { + "balance": "0x25b064a875ea940000" + }, + "6fd947d5a73b175008ae6ee8228163da289b167d": { + "balance": "0x65a4da25d3016c00000" + }, + "6fd98e563d12ce0fd60f4f1f850ae396a9823c02": { + "balance": "0x445be3f2ef87940000" + }, + "6fddbd9bca66e28765c2162c8433548c1052ed11": { + "balance": "0x1184429b82a818800000" + }, + "6ff5d361b52ad0b68b1588607ec304ae5665fc98": { + "balance": "0x692ae8897081d00000" + }, + "6ff6cc90d649de4e96cffee1077a5b302a848dcb": { + "balance": "0x18ce79c78802c0000" + }, + "6ffe5cf82cc9ea5e36cad7c2974ce7249f3749e6": { + "balance": "0x692ae8897081d00000" + }, + "7005a772282b1f62afda63f89b5dc6ab64c84cb9": { + "balance": "0x3cfc82e37e9a7400000" + }, + "700711e311bb947355f755b579250ca7fd765a3e": { + "balance": "0x61093d7c2c6d380000" + }, + "7010be2df57bd0ab9ae8196cd50ab0c521aba9f9": { + "balance": "0x6acb3df27e1f880000" + }, + "7023c70956e04a92d70025aad297b539af355869": { + "balance": "0x6c6b935b8bbd400000" + }, + "7025965d2b88da197d4459be3dc9386344cc1f31": { + "balance": "0x6cb7e74867d5e60000" + }, + "702802f36d00250fab53adbcd696f0176f638a49": { + "balance": "0x6c6b935b8bbd400000" + }, + "704819d2e44d6ed1da25bfce84c49fcca25613e5": { + "balance": "0x15af1d78b58c400000" + }, + "704a6eb41ba34f13addde7d2db7df04915c7a221": { + "balance": "0x62a992e53a0af00000" + }, + "704ab1150d5e10f5e3499508f0bf70650f028d4b": { + "balance": "0xd8d726b7177a800000" + }, + "704ae21d762d6e1dde28c235d13104597236db1a": { + "balance": "0x6c6b935b8bbd400000" + }, + "704d243c2978e46c2c86adbecd246e3b295ff633": { + "balance": "0x6d121bebf795f00000" + }, + "704d5de4846d39b53cd21d1c49f096db5c19ba29": { + "balance": "0x83d6c7aab63600000" + }, + "705ddd38355482b8c7d3b515bda1500dd7d7a817": { + "balance": "0x15af1d78b58c400000" + }, + "70616e2892fa269705b2046b8fe3e72fa55816d3": { + "balance": "0x43c33c1937564800000" + }, + "70670fbb05d33014444b8d1e8e7700258b8caa6d": { + "balance": "0x6c6b935b8bbd400000" + }, + "7081fa6baad6cfb7f51b2cca16fb8970991a64ba": { + "balance": "0xcaec005f6c0f68000" + }, + "7085ae7e7e4d932197b5c7858c00a3674626b7a5": { + "balance": "0x14542ba12a337c00000" + }, + "7086b4bde3e35d4aeb24b825f1a215f99d85f745": { + "balance": "0x6c68ccd09b022c0000" + }, + "708a2af425ceb01e87ffc1be54c0f532b20eacd6": { + "balance": "0x745d483b1f5a18000" + }, + "708ea707bae4357f1ebea959c3a250acd6aa21b3": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "708fa11fe33d85ad1befcbae3818acb71f6a7d7e": { + "balance": "0xfc936392801c0000" + }, + "7091303116d5f2389b23238b4d656a8596d984d3": { + "balance": "0x3b4e7e80aa58330000" + }, + "7099d12f6ec656899b049a7657065d62996892c8": { + "balance": "0x15af1d78b58c400000" + }, + "709fe9d2c1f1ce42207c9585044a60899f35942f": { + "balance": "0x6c6b935b8bbd400000" + }, + "70a03549aa6168e97e88a508330a5a0bea74711a": { + "balance": "0x487a9a304539440000" + }, + "70a4067d448cc25dc8e70e651cea7cf84e92109e": { + "balance": "0x98a7d9b8314c00000" + }, + "70ab34bc17b66f9c3b63f151274f2a727c539263": { + "balance": "0x6c6b935b8bbd400000" + }, + "70c213488a020c3cfb39014ef5ba6404724bcaa3": { + "balance": "0x692ae8897081d00000" + }, + "70d25ed2c8ada59c088cf70dd22bf2db93acc18a": { + "balance": "0x39474545e4adbc0000" + }, + "70e5e9da735ff077249dcb9aaf3db2a48d9498c0": { + "balance": "0x3635c9adc5dea00000" + }, + "70fee08b00c6c2c04a3c625c1ff77caf1c32df01": { + "balance": "0xad78ebc5ac6200000" + }, + "7101bd799e411cde14bdfac25b067ac890eab8e8": { + "balance": "0x4e9b8aae48de470000" + }, + "7109dd011d15f3122d9d3a27588c10d77744508b": { + "balance": "0x6c6b935b8bbd400000" + }, + "710b0274d712c77e08a5707d6f3e70c0ce3d92cf": { + "balance": "0x15af1d78b58c4000000" + }, + "710be8fd5e2918468be2aabea80d828435d79612": { + "balance": "0xf43fc2c04ee00000" + }, + "71135d8f05963c905a4a07922909235a896a52ea": { + "balance": "0xa2a15d09519be00000" + }, + "711ecf77d71b3d0ea95ce4758afecdb9c131079d": { + "balance": "0x29331e6558f0e00000" + }, + "71213fca313404204ecba87197741aa9dfe96338": { + "balance": "0x340aad21b3b700000" + }, + "712b76510214dc620f6c3a1dd29aa22bf6d214fb": { + "balance": "0x14542ba12a337c00000" + }, + "712ff7370a13ed360973fedc9ff5d2c93a505e9e": { + "balance": "0xd5967be4fc3f100000" + }, + "7133843a78d939c69d4486e10ebc7b602a349ff7": { + "balance": "0x11d5cacce21f840000" + }, + "7148aef33261d8031fac3f7182ff35928daf54d9": { + "balance": "0xde42ee1544dd900000" + }, + "7163758cbb6c4c525e0414a40a049dcccce919bb": { + "balance": "0xad78ebc5ac6200000" + }, + "7168b3bb8c167321d9bdb023a6e9fd11afc9afd9": { + "balance": "0x61093d7c2c6d380000" + }, + "7169724ee72271c534cad6420fb04ee644cb86fe": { + "balance": "0x163c2b40dba5520000" + }, + "716ad3c33a9b9a0a18967357969b94ee7d2abc10": { + "balance": "0x1a2117fe412a480000" + }, + "716ba01ead2a91270635f95f25bfaf2dd610ca23": { + "balance": "0x979e7012056aa780000" + }, + "716d50cca01e938500e6421cc070c3507c67d387": { + "balance": "0x6c6b935b8bbd400000" + }, + "71762c63678c18d1c6378ce068e666381315147e": { + "balance": "0x6c6b935b8bbd400000" + }, + "71784c105117c1f68935797fe159abc74e43d16a": { + "balance": "0x6c81c7b31195e00000" + }, + "7179726f5c71ae1b6d16a68428174e6b34b23646": { + "balance": "0x18ea250097cbaf60000" + }, + "717cf9beab3638308ded7e195e0c86132d163fed": { + "balance": "0x3326ee6f865f4220000" + }, + "7180b83ee5574317f21c8072b191d895d46153c3": { + "balance": "0x18efc84ad0c7b00000" + }, + "71946b7117fc915ed107385f42d99ddac63249c2": { + "balance": "0x6c6b935b8bbd400000" + }, + "719e891fbcc0a33e19c12dc0f02039ca05b801df": { + "balance": "0x14f5538463a1b540000" + }, + "71c7230a1d35bdd6819ed4b9a88e94a0eb0786dd": { + "balance": "0xeca08b353d24140000" + }, + "71d2cc6d02578c65f73c575e76ce8fbcfadcf356": { + "balance": "0x3ecc078688a480000" + }, + "71d9494e50c5dd59c599dba3810ba1755e6537f0": { + "balance": "0xd8d726b7177a800000" + }, + "71e38ff545f30fe14ca863d4f5297fd48c73a5ce": { + "balance": "0xc2127af858da700000" + }, + "71ea5b11ad8d29b1a4cb67bf58ca6c9f9c338c16": { + "balance": "0x56bc75e2d631000000" + }, + "71ec3aec3f8f9221f9149fede06903a0f9a232f2": { + "balance": "0xad78ebc5ac6200000" + }, + "71f2cdd1b046e2da2fbb5a26723422b8325e25a3": { + "balance": "0x56b394263a40c0000" + }, + "71fa22cc6d33206b7d701a163a0dab31ae4d31d6": { + "balance": "0x57473d05dabae80000" + }, + "7201d1c06920cd397ae8ad869bcda6e47ffb1b5a": { + "balance": "0x1158e460913d00000" + }, + "72072a0ef1cff3d567cdd260e708ddc11cbc9a31": { + "balance": "0x56bc75e2d63100000" + }, + "72094f3951ffc9771dced23ada080bcaf9c7cca7": { + "balance": "0x14542ba12a337c00000" + }, + "720994dbe56a3a95929774e20e1fe525cf3704e4": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "720e6b22bf430966fa32b6acb9a506eebf662c61": { + "balance": "0x83d6c7aab63600000" + }, + "721158be5762b119cc9b2035e88ee4ee78f29b82": { + "balance": "0x21e19e0c9bab2400000" + }, + "721f9d17e5a0e74205947aeb9bc6a7938961038f": { + "balance": "0x2d041d705a2c60000" + }, + "7222fec7711781d26eaa4e8485f7aa3fac442483": { + "balance": "0x18b84570022a200000" + }, + "72393d37b451effb9e1ff3b8552712e2a970d8c2": { + "balance": "0x35659ef93f0fc40000" + }, + "723d8baa2551d2addc43c21b45e8af4ca2bfb2c2": { + "balance": "0x5f68e8131ecf800000" + }, + "72402300e81d146c2e644e2bbda1da163ca3fb56": { + "balance": "0x17b7883c06916600000" + }, + "72480bede81ad96423f2228b5c61be44fb523100": { + "balance": "0x15af1d78b58c4000000" + }, + "724ce858857ec5481c86bd906e83a04882e5821d": { + "balance": "0xa2a15d09519be00000" + }, + "726a14c90e3f84144c765cffacba3e0df11b48be": { + "balance": "0x21e19e0c9bab2400000" + }, + "7283cd4675da58c496556151dafd80c7f995d318": { + "balance": "0x29331e6558f0e00000" + }, + "7286e89cd9de8f7a8a00c86ffdb53992dd9251d1": { + "balance": "0x692ae8897081d00000" + }, + "728f9ab080157db3073156dbca1a169ef3179407": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "7294c918b1aefb4d25927ef9d799e71f93a28e85": { + "balance": "0xaadec983fcff40000" + }, + "7294ec9da310bc6b4bbdf543b0ef45abfc3e1b4d": { + "balance": "0x4a89f54ef0121c00000" + }, + "729aad4627744e53f5d66309aa74448b3acdf46f": { + "balance": "0x6c6b935b8bbd400000" + }, + "72a2fc8675feb972fa41b50dffdbbae7fa2adfb7": { + "balance": "0x9ab4fc67b528c80000" + }, + "72a8260826294726a75bf39cd9aa9e07a3ea14cd": { + "balance": "0x6c6b935b8bbd400000" + }, + "72b05962fb2ad589d65ad16a22559eba1458f387": { + "balance": "0x73f75d1a085ba0000" + }, + "72b5633fe477fe542e742facfd690c137854f216": { + "balance": "0x5a87e7d7f5f6580000" + }, + "72b7a03dda14ca9c661a1d469fd33736f673c8e8": { + "balance": "0x6c6b935b8bbd400000" + }, + "72b904440e90e720d6ac1c2ad79c321dcc1c1a86": { + "balance": "0x54069233bf7f780000" + }, + "72b90a4dc097239492c5b9777dcd1e52ba2be2c2": { + "balance": "0x14542ba12a337c00000" + }, + "72bb27cb99f3e2c2cf90a98f707d30e4a201a071": { + "balance": "0x58e7926ee858a00000" + }, + "72c083beadbdc227c5fb43881597e32e83c26056": { + "balance": "0x43c33c1937564800000" + }, + "72cd048a110574482983492dfb1bd27942a696ba": { + "balance": "0x6c6b935b8bbd400000" + }, + "72d03d4dfab3500cf89b86866f15d4528e14a195": { + "balance": "0xf34b82fd8e91200000" + }, + "72dabb5b6eed9e99be915888f6568056381608f8": { + "balance": "0xb4c96c52cb4fe8000" + }, + "72fb49c29d23a18950c4b2dc0ddf410f532d6f53": { + "balance": "0x6c6b935b8bbd400000" + }, + "72feaf124579523954645b7fafff0378d1c8242e": { + "balance": "0x3635c9adc5dea00000" + }, + "7301dc4cf26d7186f2a11bf8b08bf229463f64a3": { + "balance": "0x6c6b935b8bbd400000" + }, + "730447f97ce9b25f22ba1afb36df27f9586beb9b": { + "balance": "0x2c73c937742c500000" + }, + "7306de0e288b56cfdf987ef0d3cc29660793f6dd": { + "balance": "0x1b8abfb62ec8f60000" + }, + "730d8763c6a4fd824ab8b859161ef7e3a96a1200": { + "balance": "0x43c33c1937564800000" + }, + "73128173489528012e76b41a5e28c68ba4e3a9d4": { + "balance": "0x3635c9adc5dea00000" + }, + "7313461208455455465445a459b06c3773b0eb30": { + "balance": "0x6c6b935b8bbd400000" + }, + "732fead60f7bfdd6a9dec48125e3735db1b6654f": { + "balance": "0x1158e460913d00000" + }, + "734223d27ff23e5906caed22595701bb34830ca1": { + "balance": "0x6c6b935b8bbd400000" + }, + "73473e72115110d0c3f11708f86e77be2bb0983c": { + "balance": "0x1158e460913d00000" + }, + "7352586d021ad0cf77e0e928404a59f374ff4582": { + "balance": "0xb8507a820728200000" + }, + "73550beb732ba9ddafda7ae406e18f7feb0f8bb2": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "735b97f2fc1bd24b12076efaf3d1288073d20c8c": { + "balance": "0x1158e460913d00000" + }, + "735e328666ed5637142b3306b77ccc5460e72c3d": { + "balance": "0x6ab8f37879c9910000" + }, + "7363cd90fbab5bb8c49ac20fc62c398fe6fb744c": { + "balance": "0x6c6b935b8bbd400000" + }, + "736b44503dd2f6dd5469ff4c5b2db8ea4fec65d0": { + "balance": "0x1104ee759f21e30000" + }, + "736bf1402c83800f893e583192582a134eb532e9": { + "balance": "0x21e19d293c01f260000" + }, + "738ca94db7ce8be1c3056cd6988eb376359f3353": { + "balance": "0x5665b96cf35acf00000" + }, + "73914b22fc2f131584247d82be4fecbf978ad4ba": { + "balance": "0x6c6b935b8bbd400000" + }, + "73932709a97f02c98e51b091312865122385ae8e": { + "balance": "0x4d853c8f8908980000" + }, + "7393cbe7f9ba2165e5a7553500b6e75da3c33abf": { + "balance": "0x56bc75e2d63100000" + }, + "73b4d499de3f38bf35aaf769a6e318bc6d123692": { + "balance": "0x6c6b935b8bbd400000" + }, + "73bedd6fda7ba3272185087b6351fc133d484e37": { + "balance": "0x11226bf9dce59780000" + }, + "73bfe7710f31cab949b7a2604fbf5239cee79015": { + "balance": "0x6c6b935b8bbd400000" + }, + "73cf80ae9688e1580e68e782cd0811f7aa494d2c": { + "balance": "0x1a4aba225c207400000" + }, + "73d7269ff06c9ffd33754ce588f74a966abbbbba": { + "balance": "0x165c96647b38a200000" + }, + "73d8fee3cb864dce22bb26ca9c2f086d5e95e63b": { + "balance": "0x3635c9adc5dea00000" + }, + "73df3c3e7955f4f2d859831be38000b1076b3884": { + "balance": "0x6acb3df27e1f880000" + }, + "73e4a2b60cf48e8baf2b777e175a5b1e4d0c2d8f": { + "balance": "0x56bc75e2d63100000" + }, + "740af1eefd3365d78ba7b12cb1a673e06a077246": { + "balance": "0x42bf06b78ed3b500000" + }, + "740bfd52e01667a3419b029a1b8e45576a86a2db": { + "balance": "0x38ebad5cdc902800000" + }, + "740f641614779dcfa88ed1d425d60db42a060ca6": { + "balance": "0x3622c6760810570000" + }, + "7412c9bc30b4df439f023100e63924066afd53af": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "741693c30376508513082020cc2b63e9fa92131b": { + "balance": "0x410d586a20a4c00000" + }, + "7421ce5be381738ddc83f02621974ff0686c79b8": { + "balance": "0x58788cb94b1d800000" + }, + "74316adf25378c10f576d5b41a6f47fa98fce33d": { + "balance": "0x1238131e5c7ad50000" + }, + "743651b55ef8429df50cf81938c2508de5c8870f": { + "balance": "0x6c6b935b8bbd400000" + }, + "743de50026ca67c94df54f066260e1d14acc11ac": { + "balance": "0x6c6b935b8bbd400000" + }, + "7445202f0c74297a004eb3726aa6a82dd7c02fa1": { + "balance": "0x6c6b935b8bbd400000" + }, + "744b03bba8582ae5498e2dc22d19949467ab53fc": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "744c0c77ba7f236920d1e434de5da33e48ebf02c": { + "balance": "0x6acb3df27e1f880000" + }, + "7450ff7f99eaa9116275deac68e428df5bbcd8b9": { + "balance": "0x6c6b935b8bbd400000" + }, + "7456c5b2c5436e3e571008933f1805ccfe34e9ec": { + "balance": "0x3635c9adc5dea00000" + }, + "745ad3abc6eeeb2471689b539e789ce2b8268306": { + "balance": "0x3d4194bea011928000" + }, + "745aecbaf9bb39b74a67ea1ce623de368481baa6": { + "balance": "0x21e19e0c9bab2400000" + }, + "745ccf2d819edbbddea8117b5c49ed3c2a066e93": { + "balance": "0xd8d726b7177a800000" + }, + "7462c89caa9d8d7891b2545def216f7464d5bb21": { + "balance": "0x5eaed54a28b310000" + }, + "74648caac748dd135cd91ea14c28e1bd4d7ff6ae": { + "balance": "0xa80d24677efef00000" + }, + "7471f72eeb300624eb282eab4d03723c649b1b58": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "747abc9649056d3926044d28c3ad09ed17b67d70": { + "balance": "0x10f0dbae61009528000" + }, + "747ff7943b71dc4dcdb1668078f83dd7cc4520c2": { + "balance": "0x340aad21b3b700000" + }, + "7480de62254f2ba82b578219c07ba5be430dc3cb": { + "balance": "0x17da3a04c7b3e000000" + }, + "7484d26becc1eea8c6315ec3ee0a450117dc86a0": { + "balance": "0x28a857425466f800000" + }, + "74863acec75d03d53e860e64002f2c165e538377": { + "balance": "0x3635c9adc5dea00000" + }, + "7489cc8abe75cda4ef0d01cef2605e47eda67ab1": { + "balance": "0x73f75d1a085ba0000" + }, + "748c285ef1233fe4d31c8fb1378333721c12e27a": { + "balance": "0x6c6b935b8bbd400000" + }, + "749087ac0f5a97c6fad021538bf1d6cda18e0daa": { + "balance": "0x3635c9adc5dea00000" + }, + "7495ae78c0d90261e2140ef2063104731a60d1ed": { + "balance": "0x1db50718925210000" + }, + "749a4a768b5f237248938a12c623847bd4e688dc": { + "balance": "0x3e733628714200000" + }, + "749ad6f2b5706bbe2f689a44c4b640b58e96b992": { + "balance": "0x56bc75e2d63100000" + }, + "74a17f064b344e84db6365da9591ff1628257643": { + "balance": "0x1158e460913d00000" + }, + "74aeec915de01cc69b2cb5a6356feea14658c6c5": { + "balance": "0xc9a95ee2986520000" + }, + "74afe54902d615782576f8baac13ac970c050f6e": { + "balance": "0x9a1aaa3a9fba70000" + }, + "74b7e0228baed65957aebb4d916d333aae164f0e": { + "balance": "0x6c6b935b8bbd400000" + }, + "74bc4a5e2045f4ff8db184cf3a9b0c065ad807d2": { + "balance": "0x6c6b935b8bbd400000" + }, + "74bce9ec38362d6c94ccac26d5c0e13a8b3b1d40": { + "balance": "0x363526410442f50000" + }, + "74bf7a5ab59293149b5c60cf364263e5ebf1aa0d": { + "balance": "0x6470c3e771e3c0000" + }, + "74c73c90528a157336f1e7ea20620ae53fd24728": { + "balance": "0x1e63a2e538f16e30000" + }, + "74d1a4d0c7524e018d4e06ed3b648092b5b6af2c": { + "balance": "0x2b5e3af16b1880000" + }, + "74d366b07b2f56477d7c7077ac6fe497e0eb6559": { + "balance": "0x10f0cf064dd59200000" + }, + "74d37a51747bf8b771bfbf43943933d100d21483": { + "balance": "0x3635c9adc5dea00000" + }, + "74d671d99cbea1ab57906375b63ff42b50451d17": { + "balance": "0x3635c9adc5dea00000" + }, + "74ebf4425646e6cf81b109ce7bf4a2a63d84815f": { + "balance": "0x22b1c8c1227a00000" + }, + "74ed33acf43f35b98c9230b9e6642ecb5330839e": { + "balance": "0x24f6dffb498d280000" + }, + "74ef2869cbe608856045d8c2041118579f2236ea": { + "balance": "0x33cd64591956e0000" + }, + "74fc5a99c0c5460503a13b0509459da19ce7cd90": { + "balance": "0xad78ebc5ac6200000" + }, + "750bbb8c06bbbf240843cc75782ee02f08a97453": { + "balance": "0x2d43f3ebfafb2c0000" + }, + "7514adbdc63f483f304d8e94b67ff3309f180b82": { + "balance": "0x21c4a06e2d13598000" + }, + "7517f16c28d132bb40e3ba36c6aef131c462da17": { + "balance": "0xfc936392801c0000" + }, + "751a2ca34e7187c163d28e3618db28b13c196d26": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "751abcb6cc033059911815c96fd191360ab0442d": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "7526e482529f0a14eec98871dddd0e721b0cd9a2": { + "balance": "0x1158e460913d00000" + }, + "7529f3797bb6a20f7ea6492419c84c867641d81c": { + "balance": "0x6c6b935b8bbd400000" + }, + "752a5ee232612cd3005fb26e5b597de19f776be6": { + "balance": "0x127fcb8afae20d00000" + }, + "752c9febf42f66c4787bfa7eb17cf5333bba5070": { + "balance": "0x6a99f2b54fdd580000" + }, + "7539333046deb1ef3c4daf50619993f444e1de68": { + "balance": "0x40138b917edfb80000" + }, + "7553aa23b68aa5f57e135fe39fdc235eaca8c98c": { + "balance": "0x3635c9adc5dea00000" + }, + "755a60bf522fbd8fff9723446b7e343a7068567e": { + "balance": "0x43c33c1937564800000" + }, + "755f587e5efff773a220726a13d0f2130d9f896b": { + "balance": "0x3635c9adc5dea00000" + }, + "75621865b6591365606ed378308c2d1def4f222c": { + "balance": "0xa80d24677efef00000" + }, + "75636cdb109050e43d5d6ec47e359e218e857eca": { + "balance": "0x4d8b2276c8962280000" + }, + "7566496162ba584377be040a4f87777a707acaeb": { + "balance": "0xd8d726b7177a800000" + }, + "756b84eb85fcc1f4fcdcc2b08db6a86e135fbc25": { + "balance": "0xae8e7a0bb575d00000" + }, + "756f45e3fa69347a9a973a725e3c98bc4db0b5a0": { + "balance": "0xad78ebc5ac6200000" + }, + "757b65876dbf29bf911d4f0692a2c9beb1139808": { + "balance": "0xdf93a59337d6dd8000" + }, + "757fa55446c460968bb74b5ebca96c4ef2c709c5": { + "balance": "0x3708baed3d68900000" + }, + "75804aac64b4199083982902994d9c5ed8828f11": { + "balance": "0x1e3d07b0a620e40000" + }, + "7592c69d067b51b6cc639d1164d5578c60d2d244": { + "balance": "0x1158e460913d00000" + }, + "75abe5270f3a78ce007cf37f8fbc045d489b7bb1": { + "balance": "0x6c6acc67d7b1d40000" + }, + "75ac547017134c04ae1e11d60e63ec04d18db4ef": { + "balance": "0x14542ba12a337c00000" + }, + "75b0e9c942a4f0f6f86d3f95ff998022fa67963b": { + "balance": "0x50c5e761a444080000" + }, + "75b95696e8ec4510d56868a7c1a735c68b244890": { + "balance": "0x15af1d78b58c4000000" + }, + "75be8ff65e5788aec6b2a52d5fa7b1e7a03ba675": { + "balance": "0x3abcdc5343d740000" + }, + "75c11d024d12ae486c1095b7a7b9c4af3e8edeb9": { + "balance": "0x1158e460913d00000" + }, + "75c1ad23d23f24b384d0c3149177e86697610d21": { + "balance": "0x15c5bcd6c288bbd0000" + }, + "75c2ffa1bef54919d2097f7a142d2e14f9b04a58": { + "balance": "0x90f358504032a10000" + }, + "75d67ce14e8d29e8c2ffe381917b930b1aff1a87": { + "balance": "0xa2a15d09519be00000" + }, + "75de7e9352e90b13a59a5878ffecc7831cac4d82": { + "balance": "0x9489237adb9a500000" + }, + "75f7539d309e9039989efe2e8b2dbd865a0df088": { + "balance": "0x855b5ba65c84f00000" + }, + "7608f437b31f18bc0b64d381ae86fd978ed7b31f": { + "balance": "0x2b5e3af16b1880000" + }, + "760ff3354e0fde938d0fb5b82cef5ba15c3d2916": { + "balance": "0x21e19e0c9bab2400000" + }, + "761a6e362c97fbbd7c5977acba2da74687365f49": { + "balance": "0x9f74ae1f953d00000" + }, + "761e6caec189c230a162ec006530193e67cf9d19": { + "balance": "0x6c6b935b8bbd400000" + }, + "761f8a3a2af0a8bdbe1da009321fb29764eb62a1": { + "balance": "0x21e19e0c9bab2400000" + }, + "762998e1d75227fced7a70be109a4c0b4ed86414": { + "balance": "0x1158e460913d00000" + }, + "762d6f30dab99135e4eca51d5243d6c8621102d5": { + "balance": "0xf498941e664280000" + }, + "76331e30796ce664b2700e0d4153700edc869777": { + "balance": "0x6c6b935b8bbd400000" + }, + "763886e333c56feff85be3951ab0b889ce262e95": { + "balance": "0x6c6b935b8bbd400000" + }, + "763a7cbab70d7a64d0a7e52980f681472593490c": { + "balance": "0x2086ac351052600000" + }, + "763eece0b08ac89e32bfa4bece769514d8cb5b85": { + "balance": "0xd8d726b7177a800000" + }, + "7640a37f8052981515bce078da93afa4789b5734": { + "balance": "0x6c6b935b8bbd400000" + }, + "7641f7d26a86cddb2be13081810e01c9c83c4b20": { + "balance": "0xb98bc829a6f90000" + }, + "764692cccb33405dd0ab0c3379b49caf8e6221ba": { + "balance": "0x1158e460913d00000" + }, + "764d5212263aff4a2a14f031f04ec749dc883e45": { + "balance": "0x6449e84e47a8a80000" + }, + "764fc46d428b6dbc228a0f5f55c9508c772eab9f": { + "balance": "0x581767ba6189c400000" + }, + "76506eb4a780c951c74a06b03d3b8362f0999d71": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "765be2e12f629e6349b97d21b62a17b7c830edab": { + "balance": "0x14542ba12a337c00000" + }, + "76628150e2995b5b279fc83e0dd5f102a671dd1c": { + "balance": "0x878678326eac9000000" + }, + "766b3759e8794e926dac473d913a8fb61ad0c2c9": { + "balance": "0x4b06dbbb40f4a0000" + }, + "7670b02f2c3cf8fd4f4730f3381a71ea431c33c7": { + "balance": "0xe7eeba3410b740000" + }, + "767a03655af360841e810d83f5e61fb40f4cd113": { + "balance": "0x35659ef93f0fc40000" + }, + "767ac690791c2e23451089fe6c7083fe55deb62b": { + "balance": "0x2c73c937742c500000" + }, + "767fd7797d5169a05f7364321c19843a8c348e1e": { + "balance": "0x104e70464b1580000" + }, + "76846f0de03b5a76971ead298cdd08843a4bc6c6": { + "balance": "0xd71b0fe0a28e0000" + }, + "768498934e37e905f1d0e77b44b574bcf3ec4ae8": { + "balance": "0x43c33c1937564800000" + }, + "768ce0daa029b7ded022e5fc574d11cde3ecb517": { + "balance": "0x1174a5cdf88bc80000" + }, + "7693bdeb6fc82b5bca721355223175d47a084b4d": { + "balance": "0x4a89f54ef0121c00000" + }, + "76aaf8c1ac012f8752d4c09bb46607b6651d5ca8": { + "balance": "0x1158e460913d00000" + }, + "76ab87dd5a05ad839a4e2fc8c85aa6ba05641730": { + "balance": "0x6c6b935b8bbd400000" + }, + "76afc225f4fa307de484552bbe1d9d3f15074c4a": { + "balance": "0xa290b5c7ad39680000" + }, + "76becae4a31d36f3cb577f2a43594fb1abc1bb96": { + "balance": "0x543a9ce0e1332f00000" + }, + "76c27535bcb59ce1fa2d8c919cabeb4a6bba01d1": { + "balance": "0x6c6b935b8bbd400000" + }, + "76ca22bcb8799e5327c4aa2a7d0949a1fcce5f29": { + "balance": "0x52a03f228c5ae20000" + }, + "76cac488111a4fd595f568ae3a858770fc915d5f": { + "balance": "0xad78ebc5ac6200000" + }, + "76cb9c8b69f4387675c48253e234cb7e0d74a426": { + "balance": "0x190f4482eb91dae0000" + }, + "76f83ac3da30f7092628c7339f208bfc142cb1ee": { + "balance": "0x9a18ffe7427d640000" + }, + "76f9ad3d9bbd04ae055c1477c0c35e7592cb2a20": { + "balance": "0x8833f11e3458f200000" + }, + "76ffc157ad6bf8d56d9a1a7fddbc0fea010aabf4": { + "balance": "0x3635c9adc5dea00000" + }, + "77028e409cc43a3bd33d21a9fc53ec606e94910e": { + "balance": "0xd255d112e103a00000" + }, + "770c2fb2c4a81753ac0182ea460ec09c90a516f8": { + "balance": "0x1158e460913d00000" + }, + "770d98d31b4353fceee8560c4ccf803e88c0c4e0": { + "balance": "0x2086ac351052600000" + }, + "7713ab8037411c09ba687f6f9364f0d3239fac28": { + "balance": "0x21e19e0c9bab2400000" + }, + "771507aeee6a255dc2cd9df55154062d0897b297": { + "balance": "0x121ea68c114e510000" + }, + "7719888795ad745924c75760ddb1827dffd8cda8": { + "balance": "0x6c6b4c4da6ddbe0000" + }, + "7727af101f0aaba4d23a1cafe17c6eb5dab1c6dc": { + "balance": "0x6c6b935b8bbd400000" + }, + "772c297f0ad194482ee8c3f036bdeb01c201d5cc": { + "balance": "0xad78ebc5ac6200000" + }, + "77306ffe2e4a8f3ca826c1a249f7212da43aeffd": { + "balance": "0x43c33c1937564800000" + }, + "773141127d8cf318aebf88365add3d5527d85b6a": { + "balance": "0x3636d7af5ec98e0000" + }, + "7746b6c6699c8f34ca2768a820f1ffa4c207fe05": { + "balance": "0xd8d8583fa2d52f0000" + }, + "7751f363a0a7fd0533190809ddaf9340d8d11291": { + "balance": "0x1158e460913d00000" + }, + "7757a4b9cc3d0247ccaaeb9909a0e56e1dd6dcc2": { + "balance": "0x1158e460913d00000" + }, + "775c10c93e0db7205b2643458233c64fc33fd75b": { + "balance": "0x6c6b935b8bbd400000" + }, + "77617ebc4bebc5f5ddeb1b7a70cdeb6ae2ffa024": { + "balance": "0x6acb3df27e1f880000" + }, + "776943ffb2ef5cdd35b83c28bc046bd4f4677098": { + "balance": "0xa2a15d09519be00000" + }, + "77701e2c493da47c1b58f421b5495dee45bea39b": { + "balance": "0x148f649cf6142a58000" + }, + "77798f201257b9c35204957057b54674aefa51df": { + "balance": "0x813ca56906d340000" + }, + "778c43d11afe3b586ff374192d96a7f23d2b9b7f": { + "balance": "0x8bb4fcfa3b7d6b8000" + }, + "778c79f4de1953ebce98fe8006d53a81fb514012": { + "balance": "0x36330322d5238c0000" + }, + "779274bf1803a336e4d3b00ddd93f2d4f5f4a62e": { + "balance": "0x3635c9adc5dea00000" + }, + "77a17122fa31b98f1711d32a99f03ec326f33d08": { + "balance": "0x5c283d410394100000" + }, + "77a34907f305a54c85db09c363fde3c47e6ae21f": { + "balance": "0x35659ef93f0fc40000" + }, + "77a769fafdecf4a638762d5ba3969df63120a41d": { + "balance": "0x6c6b935b8bbd400000" + }, + "77be6b64d7c733a436adec5e14bf9ad7402b1b46": { + "balance": "0x3635c9adc5dea00000" + }, + "77bfe93ccda750847e41a1affee6b2da96e7214e": { + "balance": "0x1043561a8829300000" + }, + "77c4a697e603d42b12056cbba761e7f51d0443f5": { + "balance": "0x24dce54d34a1a00000" + }, + "77cc02f623a9cf98530997ea67d95c3b491859ae": { + "balance": "0x497303c36ea0c20000" + }, + "77d43fa7b481dbf3db530cfbf5fdced0e6571831": { + "balance": "0x6c6b935b8bbd400000" + }, + "77da5e6c72fb36bce1d9798f7bcdf1d18f459c2e": { + "balance": "0x13695bb6cf93e0000" + }, + "77f4e3bdf056883cc87280dbe640a18a0d02a207": { + "balance": "0xa81993a2bfb5b0000" + }, + "77f609ca8720a023262c55c46f2d26fb3930ac69": { + "balance": "0xf015f25736420000" + }, + "77f81b1b26fc84d6de97ef8b9fbd72a33130cc4a": { + "balance": "0x3635c9adc5dea00000" + }, + "7819b0458e314e2b53bfe00c38495fd4b9fdf8d6": { + "balance": "0x1158e460913d00000" + }, + "781b1501647a2e06c0ed43ff197fccec35e1700b": { + "balance": "0xa2a15d09519be00000" + }, + "782f52f0a676c77716d574c81ec4684f9a020a97": { + "balance": "0x2e14e206b730ad8000" + }, + "78355df0a230f83d032c703154414de3eedab557": { + "balance": "0x6c6b935b8bbd400000" + }, + "7836f7ef6bc7bd0ff3acaf449c84dd6b1e2c939f": { + "balance": "0xe08de7a92cd97c0000" + }, + "7837fcb876da00d1eb3b88feb3df3fa4042fac82": { + "balance": "0x5f68e8131ecf800000" + }, + "783eec8aa5dac77b2e6623ed5198a431abbaee07": { + "balance": "0x17da3a04c7b3e00000" + }, + "785c8ea774d73044a734fa790a1b1e743e77ed7c": { + "balance": "0xcf152640c5c830000" + }, + "7860a3de38df382ae4a4dce18c0c07b98bce3dfa": { + "balance": "0x3635c9adc5dea00000" + }, + "78634371e17304cbf339b1452a4ce438dc764cce": { + "balance": "0x21e19e0c9bab2400000" + }, + "7864dc999fe4f8e003c0f43decc39aae1522dc0f": { + "balance": "0x51e102bd8ece00000" + }, + "78746a958dced4c764f876508c414a68342cecb9": { + "balance": "0x2be374fe8e2c40000" + }, + "787d313fd36b053eeeaedbce74b9fb0678333289": { + "balance": "0x5c058b7842719600000" + }, + "78859c5b548b700d9284cee4b6633c2f52e529c2": { + "balance": "0xa030dcebbd2f4c0000" + }, + "788e809741a3b14a22a4b1d937c82cfea489eebe": { + "balance": "0x17b7883c06916600000" + }, + "78a1e254409fb1b55a7cb4dd8eba3b30c8bad9ef": { + "balance": "0x56bc75e2d63100000" + }, + "78a5e89900bd3f81dd71ba869d25fec65261df15": { + "balance": "0xafd812fee03d5700000" + }, + "78b978a9d7e91ee529ea4fc4b76feaf8762f698c": { + "balance": "0x6c6b935b8bbd4000000" + }, + "78ce3e3d474a8a047b92c41542242d0a08c70f99": { + "balance": "0x21e19e0c9bab2400000" + }, + "78cf8336b328db3d87813a472b9e89b75e0cf3bc": { + "balance": "0x3635c9adc5dea00000" + }, + "78d4f8c71c1e68a69a98f52fcb45da8af56ea1a0": { + "balance": "0x6c6b935b8bbd400000" + }, + "78df2681d6d602e22142d54116dea15d454957aa": { + "balance": "0x102794ad20da680000" + }, + "78e08bc533413c26e291b3143ffa7cc9afb97b78": { + "balance": "0xad78ebc5ac6200000" + }, + "78e83f80b3678c7a0a4e3e8c84dccde064426277": { + "balance": "0x61093d7c2c6d380000" + }, + "78f5c74785c5668a838072048bf8b453594ddaab": { + "balance": "0x15af1d78b58c400000" + }, + "790f91bd5d1c5cc4739ae91300db89e1c1303c93": { + "balance": "0x6c6b935b8bbd400000" + }, + "7917e5bd82a9790fd650d043cdd930f7799633db": { + "balance": "0xd8d4602c26bf6c0000" + }, + "7919e7627f9b7d54ea3b14bb4dd4649f4f39dee0": { + "balance": "0x5a87e7d7f5f6580000" + }, + "791f6040b4e3e50dcf3553f182cd97a90630b75d": { + "balance": "0xd8d726b7177a800000" + }, + "7930c2d9cbfa87f510f8f98777ff8a8448ca5629": { + "balance": "0xad6eedd17cf3b8000" + }, + "794529d09d017271359730027075b87ad83dae6e": { + "balance": "0x10ce1d3d8cb3180000" + }, + "794b51c39e53d9e762b0613b829a44b472f4fff3": { + "balance": "0x2435e0647841cc8000" + }, + "79551cede376f747e3716c8d79400d766d2e0195": { + "balance": "0x9cb37afa4ff78680000" + }, + "795ebc2626fc39b0c86294e0e837dcf523553090": { + "balance": "0x3635c9adc5dea00000" + }, + "796ebbf49b3e36d67694ad79f8ff36767ac6fab0": { + "balance": "0x34bc4fdde27c00000" + }, + "796f87ba617a2930b1670be92ed1281fb0b346e1": { + "balance": "0x6f5e86fb528280000" + }, + "797427e3dbf0feae7a2506f12df1dc40326e8505": { + "balance": "0x3635c9adc5dea00000" + }, + "797510e386f56393ced8f477378a444c484f7dad": { + "balance": "0x3635c9adc5dea00000" + }, + "797bb7f157d9feaa17f76da4f704b74dc1038341": { + "balance": "0xb50fcfafebecb00000" + }, + "7988901331e387f713faceb9005cb9b65136eb14": { + "balance": "0x6acb3df27e1f880000" + }, + "7989d09f3826c3e5af8c752a8115723a84d80970": { + "balance": "0x1686f8614cf0ad0000" + }, + "7995bd8ce2e0c67bf1c7a531d477bca1b2b97561": { + "balance": "0x14248d617829ece0000" + }, + "79aeb34566b974c35a5881dec020927da7df5d25": { + "balance": "0x6c6b935b8bbd400000" + }, + "79b120eb8806732321288f675a27a9225f1cd2eb": { + "balance": "0x85a0bf37dec9e40000" + }, + "79b48d2d6137c3854d611c01ea42427a0f597bb7": { + "balance": "0xa5aa85009e39c0000" + }, + "79b8aad879dd30567e8778d2d231c8f37ab8734e": { + "balance": "0x6c6b935b8bbd400000" + }, + "79bf2f7b6e328aaf26e0bb093fa22da29ef2f471": { + "balance": "0x61093d7c2c6d380000" + }, + "79c130c762b8765b19d2abc9a083ab8f3aad7940": { + "balance": "0xd5967be4fc3f100000" + }, + "79c1be19711f73bee4e6316ae7549459aacea2e0": { + "balance": "0x15af1d78b58c400000" + }, + "79c6002f8452ca157f1317e80a2faf24475559b7": { + "balance": "0x1158e460913d00000" + }, + "79cac6494f11ef2798748cb53285bd8e22f97cda": { + "balance": "0x6c6b935b8bbd400000" + }, + "79cfa9780ae6d87b2c31883f09276986c89a6735": { + "balance": "0x3635c9adc5dea00000" + }, + "79dba256472db4e058f2e4cdc3ea4e8a42773833": { + "balance": "0x4f2591f896a6500000" + }, + "79ed10cf1f6db48206b50919b9b697081fbdaaf3": { + "balance": "0x6c6b935b8bbd400000" + }, + "79f08e01ce0988e63c7f8f2908fade43c7f9f5c9": { + "balance": "0xfc936392801c0000" + }, + "79fd6d48315066c204f9651869c1096c14fc9781": { + "balance": "0x6c6b935b8bbd400000" + }, + "79ffb4ac13812a0b78c4a37b8275223e176bfda5": { + "balance": "0xf015f25736420000" + }, + "7a0589b143a8e5e107c9ac66a9f9f8597ab3e7ab": { + "balance": "0x51e932d76e8f7b0000" + }, + "7a0a78a9cc393f91c3d9e39a6b8c069f075e6bf5": { + "balance": "0x487a9a304539440000" + }, + "7a1370a742ec2687e761a19ac5a794329ee67404": { + "balance": "0xa2a1326761e2920000" + }, + "7a2dfc770e24368131b7847795f203f3d50d5b56": { + "balance": "0x269fec7f0361d200000" + }, + "7a33834e8583733e2d52aead589bd1affb1dd256": { + "balance": "0x3635c9adc5dea00000" + }, + "7a36aba5c31ea0ca7e277baa32ec46ce93cf7506": { + "balance": "0x43c33c1937564800000" + }, + "7a381122bada791a7ab1f6037dac80432753baad": { + "balance": "0x21e19e0c9bab2400000" + }, + "7a48d877b63a8f8f9383e9d01e53e80c528e955f": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "7a4f9b850690c7c94600dbee0ca4b0a411e9c221": { + "balance": "0x678a932062e4180000" + }, + "7a63869fc767a4c6b1cd0e0649f3634cb121d24b": { + "balance": "0x433874f632cc60000" + }, + "7a67dd043a504fc2f2fc7194e9becf484cecb1fb": { + "balance": "0xd8d726b7177a80000" + }, + "7a6b26f438d9a352449155b8876cbd17c9d99b64": { + "balance": "0x14542ba12a337c00000" + }, + "7a6d781c77c4ba1fcadf687341c1e31799e93d27": { + "balance": "0xeda838c4929080000" + }, + "7a7068e1c3375c0e599db1fbe6b2ea23b8f407d2": { + "balance": "0x6c6b935b8bbd400000" + }, + "7a74cee4fa0f6370a7894f116cd00c1147b83e59": { + "balance": "0x2b5e3af16b18800000" + }, + "7a79e30ff057f70a3d0191f7f53f761537af7dff": { + "balance": "0x15af1d78b58c400000" + }, + "7a7a4f807357a4bbe68e1aa806393210c411ccb3": { + "balance": "0x65a4da25d3016c00000" + }, + "7a8563867901206f3f2bf0fa3e1c8109cabccd85": { + "balance": "0x76d41c62494840000" + }, + "7a8797690ab77b5470bf7c0c1bba612508e1ac7d": { + "balance": "0x1e09296c3378de40000" + }, + "7a8c89c014509d56d7b68130668ff6a3ecec7370": { + "balance": "0x1043561a8829300000" + }, + "7a94b19992ceb8ce63bc92ee4b5aded10c4d9725": { + "balance": "0x38d1a8064bb64c80000" + }, + "7aa79ac04316cc8d08f20065baa6d4142897d54e": { + "balance": "0x4be4e7267b6ae00000" + }, + "7aad4dbcd3acf997df93586956f72b64d8ad94ee": { + "balance": "0xd8d726b7177a800000" + }, + "7ab256b204800af20137fabcc916a23258752501": { + "balance": "0x43c33c1937564800000" + }, + "7aba56f63a48bc0817d6b97039039a7ad62fae2e": { + "balance": "0x2086ac351052600000" + }, + "7abb10f5bd9bc33b8ec1a82d64b55b6b18777541": { + "balance": "0x43c33c1937564800000" + }, + "7ac48d40c664cc9a6d89f1c5f5c80a1c70e744e6": { + "balance": "0xa31062beeed7000000" + }, + "7ac58f6ffc4f8107ae6e30378e4e9f99c57fbb24": { + "balance": "0x22b1c8c1227a00000" + }, + "7ad3f307616f19dcb143e6444dab9c3c33611f52": { + "balance": "0x2b5e3af16b1880000" + }, + "7ad82caea1a8b4ed05319b9c9870173c814e06ee": { + "balance": "0x2164b7a04ac8a00000" + }, + "7ade5d66b944bb860c0efdc86276d58f4653f711": { + "balance": "0x6c6b935b8bbd400000" + }, + "7adfedb06d91f3cc7390450b85550270883c7bb7": { + "balance": "0x1178fa40515db40000" + }, + "7ae1c19e53c71cee4c73fae2d7fc73bf9ab5e392": { + "balance": "0x3635c9adc5dea00000" + }, + "7ae659eb3bc46852fa86fac4e21c768d50388945": { + "balance": "0xf810c1cb501b80000" + }, + "7aea25d42b2612286e99c53697c6bc4100e2dbbf": { + "balance": "0x6c6b935b8bbd400000" + }, + "7aef7b551f0b9c46e755c0f38e5b3a73fe1199f5": { + "balance": "0x50c5e761a444080000" + }, + "7b0b31ff6e24745ead8ed9bb85fc0bf2fe1d55d4": { + "balance": "0x2b5e3af16b18800000" + }, + "7b0fea1176d52159333a143c294943da36bbddb4": { + "balance": "0x1fc7da64ea14c100000" + }, + "7b11673cc019626b290cbdce26046f7e6d141e21": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "7b122162c913e7146cad0b7ed37affc92a0bf27f": { + "balance": "0x51af096b2301d18000" + }, + "7b1bf53a9cbe83a7dea434579fe72aac8d2a0cd0": { + "balance": "0xad4c8316a0b0c0000" + }, + "7b1daf14891b8a1e1bd429d8b36b9a4aa1d9afbf": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "7b1fe1ab4dfd0088cdd7f60163ef59ec2aee06f5": { + "balance": "0x6c6b935b8bbd400000" + }, + "7b25bb9ca8e702217e9333225250e53c36804d48": { + "balance": "0x65ea3db75546600000" + }, + "7b27d0d1f3dd3c140294d0488b783ebf4015277d": { + "balance": "0x15af1d78b58c400000" + }, + "7b4007c45e5a573fdbb6f8bd746bf94ad04a3c26": { + "balance": "0x33821f5135d259a0000" + }, + "7b43c7eea8d62355b0a8a81da081c6446b33e9e0": { + "balance": "0xd8d726b7177a800000" + }, + "7b4d2a38269069c18557770d591d24c5121f5e83": { + "balance": "0x25f273933db5700000" + }, + "7b6175ec9befc738249535ddde34688cd36edf25": { + "balance": "0x21e19e0c9bab2400000" + }, + "7b66126879844dfa34fe65c9f288117fefb449ad": { + "balance": "0x14542ba12a337c00000" + }, + "7b6a84718dd86e63338429ac811d7c8a860f21f1": { + "balance": "0x61093d7c2c6d380000" + }, + "7b712c7af11676006a66d2fc5c1ab4c479ce6037": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "7b73242d75ca9ad558d650290df17692d54cd8b8": { + "balance": "0x6c6e59e67c78540000" + }, + "7b761feb7fcfa7ded1f0eb058f4a600bf3a708cb": { + "balance": "0xf95dd2ec27cce00000" + }, + "7b827cae7ff4740918f2e030ab26cb98c4f46cf5": { + "balance": "0x194684c0b39de100000" + }, + "7b893286427e72db219a21fc4dcd5fbf59283c31": { + "balance": "0x21e19e0c9bab2400000" + }, + "7b9226d46fe751940bc416a798b69ccf0dfab667": { + "balance": "0xe3aeb5737240a00000" + }, + "7b98e23cb96beee80a168069ebba8f20edd55ccf": { + "balance": "0xba0c91587c14a0000" + }, + "7bb0fdf5a663b5fba28d9c902af0c811e252f298": { + "balance": "0xad78ebc5ac6200000" + }, + "7bb9571f394b0b1a8eba5664e9d8b5e840677bea": { + "balance": "0x11164759ffb320000" + }, + "7bb984c6dbb9e279966afafda59c01d02627c804": { + "balance": "0x1b464311d45a6880000" + }, + "7bbbec5e70bdead8bb32b42805988e9648c0aa97": { + "balance": "0x3636d7af5ec98e0000" + }, + "7bca1da6c80a66baa5db5ac98541c4be276b447d": { + "balance": "0x24cf049680fa3c0000" + }, + "7bddb2ee98de19ee4c91f661ee8e67a91d054b97": { + "balance": "0x3635c9adc5dea00000" + }, + "7be2f7680c802da6154c92c0194ae732517a7169": { + "balance": "0xfc936392801c0000" + }, + "7be7f2456971883b9a8dbe4c91dec08ac34e8862": { + "balance": "0xa2a15d09519be00000" + }, + "7be8ccb4f11b66ca6e1d57c0b5396221a31ba53a": { + "balance": "0x1158e460913d00000" + }, + "7beb81fb2f5e91526b2ac9795e76c69bcff04bc0": { + "balance": "0xeb22e794f0a8d600000" + }, + "7c0883054c2d02bc7a852b1f86c42777d0d5c856": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "7c0f5e072043c9ee740242197e78cc4b98cdf960": { + "balance": "0xad78ebc5ac6200000" + }, + "7c1df24a4f7fb2c7b472e0bb006cb27dcd164156": { + "balance": "0x3635c9adc5dea00000" + }, + "7c29d47d57a733f56b9b217063b513dc3b315923": { + "balance": "0xd8d726b7177a800000" + }, + "7c2b9603884a4f2e464eceb97d17938d828bc02c": { + "balance": "0xa2a15d09519be00000" + }, + "7c382c0296612e4e97e440e02d3871273b55f53b": { + "balance": "0xab640391201300000" + }, + "7c3eb713c4c9e0381cd8154c7c9a7db8645cde17": { + "balance": "0xad78ebc5ac6200000" + }, + "7c4401ae98f12ef6de39ae24cf9fc51f80eba16b": { + "balance": "0xad78ebc5ac6200000" + }, + "7c45f0f8442a56dbd39dbf159995415c52ed479b": { + "balance": "0x6c6b935b8bbd400000" + }, + "7c532db9e0c06c26fd40acc56ac55c1ee92d3c3a": { + "balance": "0x3f870857a3e0e3800000" + }, + "7c60a05f7a4a5f8cf2784391362e755a8341ef59": { + "balance": "0x6694f0182a37ae0000" + }, + "7c60e51f0be228e4d56fdd2992c814da7740c6bc": { + "balance": "0xad78ebc5ac6200000" + }, + "7c6924d07c3ef5891966fe0a7856c87bef9d2034": { + "balance": "0x6c6b935b8bbd400000" + }, + "7c8bb65a6fbb49bd413396a9d7e31053bbb37aa9": { + "balance": "0x14542ba12a337c00000" + }, + "7c9a110cb11f2598b2b20e2ca400325e41e9db33": { + "balance": "0x581767ba6189c400000" + }, + "7cbca88fca6a0060b960985c9aa1b02534dc2208": { + "balance": "0x19127a1391ea2a0000" + }, + "7cbeb99932e97e6e02058cfc62d0b26bc7cca52b": { + "balance": "0x6c6b935b8bbd400000" + }, + "7cc24a6a958c20c7d1249660f7586226950b0d9a": { + "balance": "0x6acb3df27e1f880000" + }, + "7cd20eccb518b60cab095b720f571570caaa447e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "7cd5d81eab37e11e6276a3a1091251607e0d7e38": { + "balance": "0x3684d5ef981f40000" + }, + "7cdf74213945953db39ad0e8a9781add792e4d1d": { + "balance": "0x6c6b935b8bbd400000" + }, + "7ce4686446f1949ebed67215eb0d5a1dd72c11b8": { + "balance": "0x7839d321b81ab80000" + }, + "7cef4d43aa417f9ef8b787f8b99d53f1fea1ee88": { + "balance": "0x678a932062e4180000" + }, + "7d0350e40b338dda736661872be33f1f9752d755": { + "balance": "0x2b4f5a6f191948000" + }, + "7d04d2edc058a1afc761d9c99ae4fc5c85d4c8a6": { + "balance": "0x42a9c4675c9467d00000" + }, + "7d0b255efb57e10f7008aa22d40e9752dfcf0378": { + "balance": "0x19f8e7559924c0000" + }, + "7d13d6705884ab2157dd8dcc7046caf58ee94be4": { + "balance": "0x1d0da07cbb3ee9c00000" + }, + "7d273e637ef1eac481119413b91c989dc5eac122": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "7d2a52a7cf0c8436a8e007976b6c26b7229d1e15": { + "balance": "0x17bf06b32a241c0000" + }, + "7d34803569e00bd6b59fff081dfa5c0ab4197a62": { + "balance": "0x5cd87cb7b9fb860000" + }, + "7d34ff59ae840a7413c6ba4c5bb2ba2c75eab018": { + "balance": "0xa2a15d09519be00000" + }, + "7d392852f3abd92ff4bb5bb26cb60874f2be6795": { + "balance": "0x3636c25e66ece70000" + }, + "7d445267c59ab8d2a2d9e709990e09682580c49f": { + "balance": "0x3635c9adc5dea00000" + }, + "7d551397f79a2988b064afd0efebee802c7721bc": { + "balance": "0x857e0d6f1da76a00000" + }, + "7d5aa33fc14b51841a06906edb2bb49c2a117269": { + "balance": "0x104400a2470e680000" + }, + "7d5d2f73949dadda0856b206989df0078d51a1e5": { + "balance": "0x23c757072b8dd000000" + }, + "7d6e990daa7105de2526339833f77b5c0b85d84f": { + "balance": "0x43c33c1937564800000" + }, + "7d73863038ccca22f96affda10496e51e1e6cd48": { + "balance": "0x1158e460913d00000" + }, + "7d7dd5ee614dbb6fbfbcd26305247a058c41faa1": { + "balance": "0x6c6b935b8bbd400000" + }, + "7d7e7c61779adb7706c94d32409a2bb4e994bf60": { + "balance": "0x2ef20d9fc71a140000" + }, + "7d82e523cc2dc591da3954e8b6bb2caf6461e69c": { + "balance": "0x7d8dc2efffb1a90000" + }, + "7d858493f07415e0912d05793c972113eae8ae88": { + "balance": "0x628dd177d2bc280000" + }, + "7d901b28bf7f88ef73d8f73cca97564913ea8a24": { + "balance": "0x33c5499031720c0000" + }, + "7d980f4b566bb045517e4c14c87750de9346744b": { + "balance": "0x487a9a304539440000" + }, + "7d9c59631e2ba2e8e82891f3979922aaa3b567a1": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "7d9d221a3df89ddd7b5f61c1468c6787d6b333e6": { + "balance": "0x77b227cd83be80000" + }, + "7da7613445a21299aa74f0ad71431ec43fbb1be9": { + "balance": "0x3afb087b876900000" + }, + "7db4c7d5b797e9296e6382f203693db409449d62": { + "balance": "0x15af1d78b58c400000" + }, + "7db9eacc52e429dc83b461c5f4d86010e5383a28": { + "balance": "0x3635c9adc5dea00000" + }, + "7dd46da677e161825e12e80dc446f58276e1127c": { + "balance": "0x2c73c937742c500000" + }, + "7dd8d7a1a34fa1f8e73ccb005fc2a03a15b8229c": { + "balance": "0xad78ebc5ac6200000" + }, + "7ddd57165c87a2707f025dcfc2508c09834759bc": { + "balance": "0x4be4e7267b6ae00000" + }, + "7de442c82386154d2e993cbd1280bb7ca6b12ada": { + "balance": "0xd8f2e8247ec9480000" + }, + "7de7fe419cc61f91f408d234cc80d5ca3d054d99": { + "balance": "0x1158e460913d00000" + }, + "7dece6998ae1900dd3770cf4b93812bad84f0322": { + "balance": "0x56bc75e2d63100000" + }, + "7dfc342dffcf45dfee74f84c0995397bd1a63172": { + "balance": "0xd8d726b7177a80000" + }, + "7dfd2962b575bcbeee97f49142d63c30ab009f66": { + "balance": "0xd8d726b7177a800000" + }, + "7e1e29721d6cb91057f6c4042d8a0bbc644afe73": { + "balance": "0x8a9aba557e36c0000" + }, + "7e236666b2d06e63ea4e2ab84357e2dfc977e50e": { + "balance": "0x36356633ebd8ea0000" + }, + "7e24d9e22ce1da3ce19f219ccee523376873f367": { + "balance": "0x13fd9079caa60ff0000" + }, + "7e24fbdad290175eb2df6d180a19b9a9f41370be": { + "balance": "0x3635c9adc5dea00000" + }, + "7e268f131ddf687cc325c412f78ba961205e9112": { + "balance": "0x36364ee7d301b3c0000" + }, + "7e29290038493559194e946d4e460b96fc38a156": { + "balance": "0x10c13c527763880000" + }, + "7e2ba86da52e785d8625334f3397ba1c4bf2e8d1": { + "balance": "0xaadec983fcff40000" + }, + "7e3f63e13129a221ba1ab06326342cd98b5126ae": { + "balance": "0x56a02659a523340000" + }, + "7e47637e97c14622882be057bea229386f4052e5": { + "balance": "0x17da3a04c7b3e00000" + }, + "7e4e9409704121d1d77997026ff06ea9b19a8b90": { + "balance": "0x8d16549ed58fa40000" + }, + "7e59dc60be8b2fc19abd0a5782c52c28400bce97": { + "balance": "0x3635c9adc5dea00000" + }, + "7e5b19ae1be94ff4dee635492a1b012d14db0213": { + "balance": "0x56bc75e2d63100000" + }, + "7e5d9993104e4cb545e179a2a3f971f744f98482": { + "balance": "0x6c6b935b8bbd400000" + }, + "7e71171f2949fa0c3ac254254b1f0440e5e6a038": { + "balance": "0x22b1c8c1227a00000" + }, + "7e7c1e9a61a08a83984835c70ec31d34d3eaa87f": { + "balance": "0xa5aa85009e39c0000" + }, + "7e7f18a02eccaa5d61ab8fbf030343c434a25ef7": { + "balance": "0x39fbae8d042dd0000" + }, + "7e81f6449a03374191f3b7cb05d938b72e090dff": { + "balance": "0x56bc75e2d63100000" + }, + "7e8649e690fc8c1bfda1b5e186581f649b50fe33": { + "balance": "0x556f64c1fe7fa0000" + }, + "7e87863ec43a481df04d017762edcb5caa629b5a": { + "balance": "0x222c8eb3ff6640000" + }, + "7e8f96cc29f57b0975120cb593b7dd833d606b53": { + "balance": "0xaadec983fcff40000" + }, + "7e972a8a7c2a44c93b21436c38d21b9252c345fe": { + "balance": "0x61093d7c2c6d380000" + }, + "7e99dfbe989d3ba529d19751b7f4317f8953a3e2": { + "balance": "0x15af1d78b58c400000" + }, + "7ea0f96ee0a573a330b56897761f3d4c0130a8e3": { + "balance": "0x487a9a304539440000" + }, + "7ea791ebab0445a00efdfc4e4a8e9a7e7565136d": { + "balance": "0xfc936392801c0000" + }, + "7eaba035e2af3793fd74674b102540cf190addb9": { + "balance": "0x45026c835b60440000" + }, + "7eb4b0185c92b6439a08e7322168cb353c8a774a": { + "balance": "0x227196ca04983ca0000" + }, + "7ebd95e9c470f7283583dc6e9d2c4dce0bea8f84": { + "balance": "0x2f6f10780d22cc00000" + }, + "7ed0a5a847bef9a9da7cba1d6411f5c316312619": { + "balance": "0x228eb37e8751d0000" + }, + "7edafba8984baf631a820b6b92bbc2c53655f6bd": { + "balance": "0x6c6b935b8bbd400000" + }, + "7edb02c61a227287611ad950696369cc4e647a68": { + "balance": "0xeda838c4929080000" + }, + "7ee5ca805dce23af89c2d444e7e40766c54c7404": { + "balance": "0xd0bd412edbd820000" + }, + "7ee604c7a9dc2909ce321de6b9b24f5767577555": { + "balance": "0x12bf9c7985cf62d8000" + }, + "7ef16fd8d15b378a0fba306b8d03dd98fc92619f": { + "balance": "0x25f273933db5700000" + }, + "7ef98b52bee953bef992f305fda027f8911c5851": { + "balance": "0x1be722206996bc8000" + }, + "7efc90766a00bc52372cac97fabd8a3c831f8ecd": { + "balance": "0x890b0c2e14fb80000" + }, + "7efec0c6253caf397f71287c1c07f6c9582b5b86": { + "balance": "0x1a2cbcb84f30d58000" + }, + "7f01dc7c3747ca608f983dfc8c9b39e755a3b914": { + "balance": "0xb386cad5f7a5a0000" + }, + "7f0662b410298c99f311d3a1454a1eedba2fea76": { + "balance": "0xad78ebc5ac6200000" + }, + "7f06c89d59807fa60bc60136fcf814cbaf2543bd": { + "balance": "0x21e19e0c9bab2400000" + }, + "7f0b90a1fdd48f27b268feb38382e55ddb50ef0f": { + "balance": "0x32f51edbaaa3300000" + }, + "7f0ec3db804692d4d1ea3245365aab0590075bc4": { + "balance": "0xd8d726b7177a800000" + }, + "7f0f04fcf37a53a4e24ede6e93104e78be1d3c9e": { + "balance": "0x6c6b935b8bbd400000" + }, + "7f13d760498d7193ca6859bc95c901386423d76c": { + "balance": "0x10f0cf064dd59200000" + }, + "7f150afb1a77c2b45928c268c1e9bdb4641d47d8": { + "balance": "0x6c6b935b8bbd400000" + }, + "7f1619988f3715e94ff1d253262dc5581db3de1c": { + "balance": "0x30ca024f987b900000" + }, + "7f1c81ee1697fc144b7c0be5493b5615ae7fddca": { + "balance": "0x1b1dab61d3aa640000" + }, + "7f2382ffd8f83956467937f9ba72374623f11b38": { + "balance": "0x2086ac351052600000" + }, + "7f3709391f3fbeba3592d175c740e87a09541d02": { + "balance": "0x1a055690d9db800000" + }, + "7f389c12f3c6164f6446566c77669503c2792527": { + "balance": "0x556f64c1fe7fa0000" + }, + "7f3a1e45f67e92c880e573b43379d71ee089db54": { + "balance": "0x152d02c7e14af6800000" + }, + "7f3d7203c8a447f7bf36d88ae9b6062a5eee78ae": { + "balance": "0x14542ba12a337c00000" + }, + "7f46bb25460dd7dae4211ca7f15ad312fc7dc75c": { + "balance": "0x16a6502f15a1e540000" + }, + "7f49e7a4269882bd8722d4a6f566347629624079": { + "balance": "0x6c6b935b8bbd400000" + }, + "7f49f20726471ac1c7a83ef106e9775ceb662566": { + "balance": "0x14061b9d77a5e980000" + }, + "7f4b5e278578c046cceaf65730a0e068329ed5b6": { + "balance": "0x65ea3db75546600000" + }, + "7f4f593b618c330ba2c3d5f41eceeb92e27e426c": { + "balance": "0x966edc756b7cfc0000" + }, + "7f541491d2ac00d2612f94aa7f0bcb014651fbd4": { + "balance": "0x14620c57dddae00000" + }, + "7f5ae05ae0f8cbe5dfe721f044d7a7bef4c27997": { + "balance": "0x340aad21b3b700000" + }, + "7f603aec1759ea5f07c7f8d41a1428fbbaf9e762": { + "balance": "0x1158e460913d00000" + }, + "7f616c6f008adfa082f34da7d0650460368075fb": { + "balance": "0x3635c9adc5dea00000" + }, + "7f61fa6cf5f898b440dac5abd8600d6d691fdef9": { + "balance": "0xf2dc7d47f15600000" + }, + "7f655c6789eddf455cb4b88099720639389eebac": { + "balance": "0x14542ba12a337c00000" + }, + "7f6b28c88421e4857e459281d78461692489d3fb": { + "balance": "0x6c6b935b8bbd400000" + }, + "7f6efb6f4318876d2ee624e27595f44446f68e93": { + "balance": "0x54069233bf7f780000" + }, + "7f7192c0df1c7db6d9ed65d71184d8e4155a17ba": { + "balance": "0x453728d33942c0000" + }, + "7f7a3a21b3f5a65d81e0fcb7d52dd00a1aa36dba": { + "balance": "0x56bc75e2d63100000" + }, + "7f8dbce180ed9c563635aad2d97b4cbc428906d9": { + "balance": "0x90f534608a72880000" + }, + "7f993ddb7e02c282b898f6155f680ef5b9aff907": { + "balance": "0x43c33c1937564800000" + }, + "7f9f9b56e4289dfb58e70fd5f12a97b56d35c6a5": { + "balance": "0x6acb3df27e1f880000" + }, + "7fa37ed67887751a471f0eb306be44e0dbcd6089": { + "balance": "0x3976747fe11a100000" + }, + "7faa30c31519b584e97250ed2a3cf3385ed5fd50": { + "balance": "0x6c6b935b8bbd400000" + }, + "7fcf5ba6666f966c5448c17bf1cb0bbcd8019b06": { + "balance": "0x56bc3d0aebe498000" + }, + "7fd679e5fb0da2a5d116194dcb508318edc580f3": { + "balance": "0x1639e49bba162800000" + }, + "7fdba031c78f9c096d62d05a369eeab0bccc55e5": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "7fdbc3a844e40d96b2f3a635322e6065f4ca0e84": { + "balance": "0x6c6b935b8bbd400000" + }, + "7fdfc88d78bf1b285ac64f1adb35dc11fcb03951": { + "balance": "0x7c06fda02fb0360000" + }, + "7fea1962e35d62059768c749bedd96cab930d378": { + "balance": "0x6c6b935b8bbd400000" + }, + "7fef8c38779fb307ec6f044bebe47f3cfae796f1": { + "balance": "0x92340f86cf09e8000" + }, + "7ff0c63f70241bece19b737e5341b12b109031d8": { + "balance": "0x12c1b6eed03d280000" + }, + "7ffabfbc390cbe43ce89188f0868b27dcb0f0cad": { + "balance": "0x1595182224b26480000" + }, + "7ffd02ed370c7060b2ae53c078c8012190dfbb75": { + "balance": "0x21e19e0c9bab2400000" + }, + "80022a1207e910911fc92849b069ab0cdad043d3": { + "balance": "0xb98bc829a6f90000" + }, + "8009a7cbd192b3aed4adb983d5284552c16c7451": { + "balance": "0xd8d726b7177a800000" + }, + "800e7d631c6e573a90332f17f71f5fd19b528cb9": { + "balance": "0x83d6c7aab63600000" + }, + "80156d10efa8b230c99410630d37e269d4093cea": { + "balance": "0x6c6b935b8bbd400000" + }, + "801732a481c380e57ed62d6c29de998af3fa3b13": { + "balance": "0x56bc75e2d63100000" + }, + "801d65c518b11d0e3f4f470221417013c8e53ec5": { + "balance": "0xd8d726b7177a800000" + }, + "8026435aac728d497b19b3e7e57c28c563954f2b": { + "balance": "0x5dc892aa1131c80000" + }, + "802dc3c4ff2d7d925ee2859f4a06d7ba60f1308c": { + "balance": "0x550940c8fd34c0000" + }, + "8030b111c6983f0485ddaca76224c6180634789f": { + "balance": "0x4563918244f400000" + }, + "8035bcffaefdeeea35830c497d14289d362023de": { + "balance": "0x1043561a8829300000" + }, + "8035fe4e6b6af27ae492a578515e9d39fa6fa65b": { + "balance": "0xd8d726b7177a800000" + }, + "8043ed22f997e5a2a4c16e364486ae64975692c4": { + "balance": "0x3d4904ffc9112e8000" + }, + "8043fdd0bc4c973d1663d55fc135508ec5d4f4fa": { + "balance": "0x1158e460913d00000" + }, + "804ca94972634f633a51f3560b1d06c0b293b3b1": { + "balance": "0xad78ebc5ac6200000" + }, + "80522ddf944ec52e27d724ed4c93e1f7be6083d6": { + "balance": "0xad78ebc5ac6200000" + }, + "80591a42179f34e64d9df75dcd463b28686f5574": { + "balance": "0x43c33c1937564800000" + }, + "805ce51297a0793b812067f017b3e7b2df9bb1f9": { + "balance": "0x56bc75e2d63100000" + }, + "805d846fb0bc02a7337226d685be9ee773b9198a": { + "balance": "0x43c30fb0884a96c0000" + }, + "8063379a7bf2cb923a84c5093e68dac7f75481c5": { + "balance": "0x1176102e6e32df0000" + }, + "806854588ecce541495f81c28a290373df0274b2": { + "balance": "0x1f8cdf5c6e8d580000" + }, + "806f44bdeb688037015e84ff218049e382332a33": { + "balance": "0x6c5db2a4d815dc0000" + }, + "80744618de396a543197ee4894abd06398dd7c27": { + "balance": "0x6c6b935b8bbd400000" + }, + "8077c3e4c445586e094ce102937fa05b737b568c": { + "balance": "0x56bc75e2d63100000" + }, + "80907f593148b57c46c177e23d25abc4aae18361": { + "balance": "0x56bc75e2d63100000" + }, + "80977316944e5942e79b0e3abad38da746086519": { + "balance": "0x21a754a6dc5280000" + }, + "80a0f6cc186cf6201400736e065a391f52a9df4a": { + "balance": "0x21e19e0c9bab2400000" + }, + "80abec5aa36e5c9d098f1b942881bd5acac6963d": { + "balance": "0x6c6b935b8bbd400000" + }, + "80b23d380b825c46e0393899a85556462da0e18c": { + "balance": "0x6c6b935b8bbd400000" + }, + "80b42de170dbd723f454e88f7716452d92985092": { + "balance": "0x104623c0762dd10000" + }, + "80b79f338390d1ba1b3737a29a0257e5d91e0731": { + "balance": "0x1158e460913d00000" + }, + "80bf995ed8ba92701d10fec49f9e7d014dbee026": { + "balance": "0x1f0437ca1a7e128000" + }, + "80c04efd310f440483c73f744b5b9e64599ce3ec": { + "balance": "0x410d586a20a4c00000" + }, + "80c3a9f695b16db1597286d1b3a8b7696c39fa27": { + "balance": "0x56bc75e2d63100000" + }, + "80c53ee7e3357f94ce0d7868009c208b4a130125": { + "balance": "0x6c6b935b8bbd400000" + }, + "80cc21bd99f39005c58fe4a448909220218f66cb": { + "balance": "0x3636c9796436740000" + }, + "80d5c40c59c7f54ea3a55fcfd175471ea35099b3": { + "balance": "0x3635c9adc5dea00000" + }, + "80da2fdda29a9e27f9e115975e69ae9cfbf3f27e": { + "balance": "0xad78ebc5ac6200000" + }, + "80e7b3205230a566a1f061d922819bb4d4d2a0e1": { + "balance": "0x2f6f10780d22cc00000" + }, + "80ea1acc136eca4b68c842a95adf6b7fee7eb8a2": { + "balance": "0xd8d726b7177a800000" + }, + "80f07ac09e7b2c3c0a3d1e9413a544c73a41becb": { + "balance": "0x1158e460913d00000" + }, + "810db25675f45ea4c7f3177f37ce29e22d67999c": { + "balance": "0xad78ebc5ac6200000" + }, + "81139bfdcca656c430203f72958c543b6580d40c": { + "balance": "0x6c6b935b8bbd400000" + }, + "811461a2b0ca90badac06a9ea16e787b33b196cc": { + "balance": "0x8e3f50b173c100000" + }, + "81164deb10814ae08391f32c08667b6248c27d7a": { + "balance": "0x155bd9307f9fe80000" + }, + "81186931184137d1192ac88cd3e1e5d0fdb86a74": { + "balance": "0x9d3595ab2438d00000" + }, + "812a55c43caedc597218379000ce510d548836fd": { + "balance": "0xfc936392801c0000" + }, + "812ea7a3b2c86eed32ff4f2c73514cc63bacfbce": { + "balance": "0x3635c9adc5dea00000" + }, + "8134dd1c9df0d6c8a5812426bb55c761ca831f08": { + "balance": "0x6a2160bb57ccc0000" + }, + "814135da8f9811075783bf1ab67062af8d3e9f40": { + "balance": "0x1158e460913d00000" + }, + "81498ca07b0f2f17e8bbc7e61a7f4ae7be66b78b": { + "balance": "0x581fbb5b33bb00000" + }, + "81556db27349ab8b27004944ed50a46e941a0f5f": { + "balance": "0xd8bb6549b02bb80000" + }, + "8155fa6c51eb31d808412d748aa086105018122f": { + "balance": "0x65ea3db75546600000" + }, + "8156360bbd370961ceca6b6691d75006ad204cf2": { + "balance": "0x878678326eac9000000" + }, + "8161d940c3760100b9080529f8a60325030f6edc": { + "balance": "0x1043561a8829300000" + }, + "8164e78314ae16b28926cc553d2ccb16f356270d": { + "balance": "0x1ca134e95fb32c80000" + }, + "8165cab0eafb5a328fc41ac64dae715b2eef2c65": { + "balance": "0x3635c9adc5dea00000" + }, + "8168edce7f2961cf295b9fcd5a45c06cdeda6ef5": { + "balance": "0xad78ebc5ac6200000" + }, + "816d9772cf11399116cc1e72c26c6774c9edd739": { + "balance": "0xad78ebc5ac6200000" + }, + "8173c835646a672e0152be10ffe84162dd256e4c": { + "balance": "0x1aabdf2145b4300000" + }, + "817493cd9bc623702a24a56f9f82e3fd48f3cd31": { + "balance": "0x9e4b23f12d4ca00000" + }, + "8179c80970182cc5b7d82a4df06ea94db63a25f3": { + "balance": "0x276f259de66bf40000" + }, + "817ac33bd8f847567372951f4a10d7a91ce3f430": { + "balance": "0xad7c406c66dc18000" + }, + "818ffe271fc3973565c303f213f6d2da89897ebd": { + "balance": "0x136e05342fee1b98000" + }, + "8197948121732e63d9c148194ecad46e30b749c8": { + "balance": "0xd8d726b7177a800000" + }, + "819af9a1c27332b1c369bbda1b3de1c6e933d640": { + "balance": "0x1109e654b98f7a0000" + }, + "819cdaa5303678ef7cec59d48c82163acc60b952": { + "balance": "0x31351545f79816c0000" + }, + "819eb4990b5aba5547093da12b6b3c1093df6d46": { + "balance": "0x3635c9adc5dea00000" + }, + "81a88196fac5f23c3e12a69dec4b880eb7d97310": { + "balance": "0x6c6b935b8bbd400000" + }, + "81bccbff8f44347eb7fca95b27ce7c952492aaad": { + "balance": "0x840c12165dd780000" + }, + "81bd75abd865e0c3f04a0b4fdbcb74d34082fbb7": { + "balance": "0xd8d726b7177a800000" + }, + "81c18c2a238ddc4cba230a072dd7dc101e620273": { + "balance": "0x487a9a304539440000" + }, + "81c9e1aee2d3365d53bcfdcd96c7c538b0fd7eec": { + "balance": "0x62a992e53a0af00000" + }, + "81cfad760913d3c322fcc77b49c2ae3907e74f6e": { + "balance": "0xaadec983fcff40000" + }, + "81d619ff5726f2405f12904c72eb1e24a0aaee4f": { + "balance": "0x43c33c1937564800000" + }, + "81efe296ae76c860d1c5fbd33d47e8ce9996d157": { + "balance": "0x3635c9adc5dea00000" + }, + "81f8de2c283d5fd4afbda85dedf9760eabbbb572": { + "balance": "0xa2a15d09519be00000" + }, + "820c19291196505b65059d9914b7090be1db87de": { + "balance": "0x796e3ea3f8ab00000" + }, + "821cb5cd05c7ef909fe1be60733d8963d760dc41": { + "balance": "0xd8d726b7177a800000" + }, + "821d798af19989c3ae5b84a7a7283cd7fda1fabe": { + "balance": "0x43c33c1937564800000" + }, + "821eb90994a2fbf94bdc3233910296f76f9bf6e7": { + "balance": "0x21e19e0c9bab2400000" + }, + "82249fe70f61c6b16f19a324840fdc020231bb02": { + "balance": "0x20336b08a93635b0000" + }, + "8228ebc087480fd64547ca281f5eace3041453b9": { + "balance": "0x6acb3df27e1f880000" + }, + "8229ceb9f0d70839498d44e6abed93c5ca059f5d": { + "balance": "0x1a1c1b3c989a20100000" + }, + "822edff636563a6106e52e9a2598f7e6d0ef2782": { + "balance": "0x1f4f9693d42d38000" + }, + "823219a25976bb2aa4af8bad41ac3526b493361f": { + "balance": "0x6c6b935b8bbd400000" + }, + "8232d1f9742edf8dd927da353b2ae7b4cbce7592": { + "balance": "0x243d4d18229ca20000" + }, + "8234f463d18485501f8f85ace4972c9b632dbccc": { + "balance": "0x6c6b935b8bbd400000" + }, + "823768746737ce6da312d53e54534e106f967cf3": { + "balance": "0x1158e460913d00000" + }, + "823ba7647238d113bce9964a43d0a098118bfe4d": { + "balance": "0xad78ebc5ac6200000" + }, + "824074312806da4748434266ee002140e3819ac2": { + "balance": "0x51b1d3839261ac0000" + }, + "82438fd2b32a9bdd674b49d8cc5fa2eff9781847": { + "balance": "0x1158e460913d00000" + }, + "82485728d0e281563758c75ab27ed9e882a0002d": { + "balance": "0x7f808e9291e6c0000" + }, + "824b3c3c443e19295d7ef6faa7f374a4798486a8": { + "balance": "0x1158e460913d00000" + }, + "8251358ca4e060ddb559ca58bc0bddbeb4070203": { + "balance": "0x6c6b935b8bbd400000" + }, + "825135b1a7fc1605614c8aa4d0ac6dbad08f480e": { + "balance": "0x4d853c8f8908980000" + }, + "825309a7d45d1812f51e6e8df5a7b96f6c908887": { + "balance": "0x8034f7d9b166d40000" + }, + "825a7f4e10949cb6f8964268f1fa5f57e712b4c4": { + "balance": "0x1158e460913d00000" + }, + "8261fa230c901d43ff579f4780d399f31e6076bc": { + "balance": "0x6c6b935b8bbd400000" + }, + "8262169b615870134eb4ac6c5f471c6bf2f789fc": { + "balance": "0x19127a1391ea2a0000" + }, + "8263ece5d709e0d7ae71cca868ed37cd2fef807b": { + "balance": "0x35ab028ac154b80000" + }, + "826ce5790532e0548c6102a30d3eac836bd6388f": { + "balance": "0x3cfc82e37e9a7400000" + }, + "826eb7cd7319b82dd07a1f3b409071d96e39677f": { + "balance": "0x3635c9adc5dea00000" + }, + "827531a6c5817ae35f82b00b9754fcf74c55e232": { + "balance": "0xc328093e61ee400000" + }, + "8275cd684c3679d5887d03664e338345dc3cdde1": { + "balance": "0xdb44e049bb2c0000" + }, + "8284923b62e68bbf7c2b9f3414d13ef6c812a904": { + "balance": "0xd255d112e103a00000" + }, + "828ba651cb930ed9787156299a3de44cd08b7212": { + "balance": "0x487a9a304539440000" + }, + "82a15cef1d6c8260eaf159ea3f0180d8677dce1c": { + "balance": "0x6c6b935b8bbd400000" + }, + "82a8b96b6c9e13ebec1e9f18ac02a60ea88a48ff": { + "balance": "0x6c6b8c408e73b30000" + }, + "82a8cbbfdff02b2e38ae4bbfca15f1f0e83b1aea": { + "balance": "0x49b991c27ef6d8000" + }, + "82e4461eb9d849f0041c1404219e4272c4900ab4": { + "balance": "0x6c6b935b8bbd400000" + }, + "82e577b515cb2b0860aafe1ce09a59e09fe7d040": { + "balance": "0x2086ac351052600000" + }, + "82ea01e3bf2e83836e71704e22a2719377efd9c3": { + "balance": "0xa4cc799563c3800000" + }, + "82f2e991fd324c5f5d17768e9f61335db6319d6c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "82f39b2758ae42277b86d69f75e628d958ebcab0": { + "balance": "0x878678326eac9000000" + }, + "82f854c9c2f087dffa985ac8201e626ca5467686": { + "balance": "0x152d02c7e14af6800000" + }, + "82ff716fdf033ec7e942c909d9831867b8b6e2ef": { + "balance": "0x61093d7c2c6d380000" + }, + "8308ed0af7f8a3c1751fafc877b5a42af7d35882": { + "balance": "0x3635c9adc5dea00000" + }, + "831c44b3084047184b2ad218680640903750c45d": { + "balance": "0x6acb3df27e1f880000" + }, + "83210583c16a4e1e1dac84ebd37e3d0f7c57eba4": { + "balance": "0x6c6b935b8bbd400000" + }, + "832c54176bdf43d2c9bcd7b808b89556b89cbf31": { + "balance": "0xad78ebc5ac6200000" + }, + "833316985d47742bfed410604a91953c05fb12b0": { + "balance": "0x6c6b935b8bbd400000" + }, + "8334764b7b397a4e578f50364d60ce44899bff94": { + "balance": "0x503b203e9fba20000" + }, + "833b6a8ec8da408186ac8a7d2a6dd61523e7ce84": { + "balance": "0x3635c9adc5dea000000" + }, + "833d3fae542ad5f8b50ce19bde2bec579180c88c": { + "balance": "0x12c1b6eed03d280000" + }, + "833db42c14163c7be4cab86ac593e06266d699d5": { + "balance": "0x24e40d2b6943ef900000" + }, + "83563bc364ed81a0c6da3b56ff49bbf267827a9c": { + "balance": "0x3ab91d17b20de500000" + }, + "837a645dc95c49549f899c4e8bcf875324b2f57c": { + "balance": "0x208c394af1c8880000" + }, + "838bd565f99fde48053f7917fe333cf84ad548ab": { + "balance": "0xad78ebc5ac6200000" + }, + "83908aa7478a6d1c9b9b0281148f8f9f242b9fdc": { + "balance": "0x6c6b935b8bbd400000" + }, + "8392e53776713578015bff4940cf43849d7dcba1": { + "balance": "0x84df0355d56170000" + }, + "8397a1bc47acd647418159b99cea57e1e6532d6e": { + "balance": "0x1f10fa827b550b40000" + }, + "8398e07ebcb4f75ff2116de77c1c2a99f303a4cf": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "83a3148833d9644984f7c475a7850716efb480ff": { + "balance": "0xb8507a820728200000" + }, + "83a402438e0519773d5448326bfb61f8b20cf52d": { + "balance": "0x52663ccab1e1c00000" + }, + "83a93b5ba41bf88720e415790cdc0b67b4af34c4": { + "balance": "0xad78ebc5ac6200000" + }, + "83c23d8a502124ee150f08d71dc6727410a0f901": { + "balance": "0x7331f3bfe661b180000" + }, + "83c897a84b695eebe46679f7da19d776621c2694": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "83d532d38d6dee3f60adc68b936133c7a2a1b0dd": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "83dbf8a12853b40ac61996f8bf1dc8fdbaddd329": { + "balance": "0x34957444b840e80000" + }, + "83dbfd8eda01d0de8e158b16d0935fc2380a5dc7": { + "balance": "0x2086ac351052600000" + }, + "83e48055327c28b5936fd9f4447e73bdb2dd3376": { + "balance": "0x90f534608a72880000" + }, + "83fe5a1b328bae440711beaf6aad6026eda6d220": { + "balance": "0x43c33c1937564800000" + }, + "84008a72f8036f3feba542e35078c057f32a8825": { + "balance": "0x56bc75e2d63100000" + }, + "840ec83ea93621f034e7bb3762bb8e29ded4c479": { + "balance": "0x878678326eac900000" + }, + "841145b44840c946e21dbc190264b8e0d5029369": { + "balance": "0x3f870857a3e0e3800000" + }, + "84232107932b12e03186583525ce023a703ef8d9": { + "balance": "0x6c6b935b8bbd400000" + }, + "84244fc95a6957ed7c1504e49f30b8c35eca4b79": { + "balance": "0x6c6b935b8bbd400000" + }, + "8431277d7bdd10457dc017408c8dbbbd414a8df3": { + "balance": "0x222c8eb3ff6640000" + }, + "84375afbf59b3a1d61a1be32d075e0e15a4fbca5": { + "balance": "0xad78ebc5ac6200000" + }, + "843bd3502f45f8bc4da370b323bdac3fcf5f19a6": { + "balance": "0x50039d63d11c900000" + }, + "84503334630d77f74147f68b2e086613c8f1ade9": { + "balance": "0x56bc75e2d631000000" + }, + "845203750f7148a9aa262921e86d43bf641974fd": { + "balance": "0x56bc75e2d63100000" + }, + "8461ecc4a6a45eb1a5b947fb86b88069b91fcd6f": { + "balance": "0x6c6b935b8bbd400000" + }, + "84675e9177726d45eaa46b3992a340ba7f710c95": { + "balance": "0x3635c9adc5dea00000" + }, + "84686c7bad762c54b667d59f90943cd14d117a26": { + "balance": "0x1158e460913d00000" + }, + "8489f6ad1d9a94a297789156899db64154f1dbb5": { + "balance": "0x137407c03c8c268000" + }, + "848c994a79003fe7b7c26cc63212e1fc2f9c19eb": { + "balance": "0x6c6b935b8bbd400000" + }, + "848fbd29d67cf4a013cb02a4b176ef244e9ee68d": { + "balance": "0x1172a636bbdc20000" + }, + "84949dba559a63bfc845ded06e9f2d9b7f11ef24": { + "balance": "0x6c6b935b8bbd400000" + }, + "849ab80790b28ff1ffd6ba394efc7463105c36f7": { + "balance": "0x1e02be4ae6c840000" + }, + "849b116f596301c5d8bb62e0e97a8248126e39f3": { + "balance": "0x1043561a8829300000" + }, + "84a74ceecff65cb93b2f949d773ef1ad7fb4a245": { + "balance": "0x50a9b444685c70000" + }, + "84aac7fa197ff85c30e03b7a5382b957f41f3afb": { + "balance": "0x88b23acffd9900000" + }, + "84af1b157342d54368260d17876230a534b54b0e": { + "balance": "0x35659ef93f0fc40000" + }, + "84b0ee6bb837d3a4c4c5011c3a228c0edab4634a": { + "balance": "0x1158e460913d00000" + }, + "84b4b74e6623ba9d1583e0cfbe49643f16384149": { + "balance": "0x1158e460913d00000" + }, + "84b6b6adbe2f5b3e2d682c66af1bc4905340c3ed": { + "balance": "0x2192f8d22215008000" + }, + "84b91e2e2902d05e2b591b41083bd7beb2d52c74": { + "balance": "0x215e5128b4504648000" + }, + "84bcbf22c09607ac84341d2edbc03bfb1739d744": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "84bfcef0491a0ae0694b37ceac024584f2aa0467": { + "balance": "0x6c6acc67d7b1d40000" + }, + "84cb7da0502df45cf561817bbd2362f451be02da": { + "balance": "0x487a9a304539440000" + }, + "84cc7878da605fdb019fab9b4ccfc157709cdda5": { + "balance": "0x48798513af04c90000" + }, + "84db1459bb00812ea67ecb3dc189b72187d9c501": { + "balance": "0x811b8fbda85ab8000" + }, + "84e9949680bece6841b9a7e5250d08acd87d16cd": { + "balance": "0xad78ebc5ac6200000" + }, + "84e9cf8166c36abfa49053b7a1ad4036202681ef": { + "balance": "0x6c6b935b8bbd400000" + }, + "84ec06f24700fe42414cb9897c154c88de2f6132": { + "balance": "0x487a9a304539440000" + }, + "84f522f0520eba52dd18ad21fa4b829f2b89cb97": { + "balance": "0x10c5106d5134f130000" + }, + "850b9db18ff84bf0c7da49ea3781d92090ad7e64": { + "balance": "0x8cf23f909c0fa00000" + }, + "8510ee934f0cbc900e1007eb38a21e2a5101b8b2": { + "balance": "0x5bf0ba6634f680000" + }, + "8516fcaf77c893970fcd1a958ba9a00e49044019": { + "balance": "0xaa3eb1691bce58000" + }, + "851aa91c82f42fad5dd8e8bb5ea69c8f3a5977d1": { + "balance": "0x80e561f2578798000" + }, + "851c0d62be4635d4777e8035e37e4ba8517c6132": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "851dc38adb4593729a76f33a8616dab6f5f59a77": { + "balance": "0x56bc75e2d63100000" + }, + "8532490897bbb4ce8b7f6b837e4cba848fbe9976": { + "balance": "0x56bc75e2d63100000" + }, + "853e6abaf44469c72f151d4e223819aced4e3728": { + "balance": "0x6c6b935b8bbd400000" + }, + "854691ce714f325ced55ce5928ce9ba12facd1b8": { + "balance": "0xed70b5e9c3f2f00000" + }, + "854c0c469c246b83b5d1b3eca443b39af5ee128a": { + "balance": "0x56bc75e2d631000000" + }, + "855d9aef2c39c6230d09c99ef6494989abe68785": { + "balance": "0x8ba52e6fc45e40000" + }, + "8563c49361b625e768771c96151dbfbd1c906976": { + "balance": "0x6c6b935b8bbd400000" + }, + "8566610901aace38b83244f3a9c831306a67b9dc": { + "balance": "0xb08213bcf8ffe00000" + }, + "856aa23c82d7215bec8d57f60ad75ef14fa35f44": { + "balance": "0x43c33c1937564800000" + }, + "856e5ab3f64c9ab56b009393b01664fc0324050e": { + "balance": "0x61093d7c2c6d380000" + }, + "856eb204241a87830fb229031343dc30854f581a": { + "balance": "0x3635c9adc5dea00000" + }, + "85732c065cbd64119941aed430ac59670b6c51c4": { + "balance": "0x27a57362ab0a0e8000" + }, + "8578e10212ca14ff0732a8241e37467db85632a9": { + "balance": "0x14542ba12a337c00000" + }, + "8579dadf1a395a3471e20b6f763d9a0ff19a3f6f": { + "balance": "0xd8d726b7177a800000" + }, + "857f100b1a5930225efc7e9020d78327b41c02cb": { + "balance": "0x6c6b935b8bbd400000" + }, + "85946d56a4d371a93368539690b60ec825107454": { + "balance": "0x5dc892aa1131c80000" + }, + "8599cbd5a6a9dcd4b966be387d69775da5e33c6f": { + "balance": "0xc51f1b1d52622900000" + }, + "859c600cf13d1d0273d5d1da3cd789e495899f27": { + "balance": "0x90f534608a72880000" + }, + "85a2f6ea94d05e8c1d9ae2f4910338a358e98ded": { + "balance": "0x6c6b935b8bbd400000" + }, + "85b16f0b8b34dff3804f69e2168a4f7b24d1042b": { + "balance": "0x112f423c7646d40000" + }, + "85b2998d0c73302cb2ba13f489313301e053be15": { + "balance": "0x21e19e0c9bab2400000" + }, + "85bb51bc3bfe9a1b2a2f6b1cda95bca8b38c8d5e": { + "balance": "0x11712da04ba1ef0000" + }, + "85c8f3cc7a354feac99a5e7bfe7cdfa351cfe355": { + "balance": "0x15af1d78b58c400000" + }, + "85ca1e727e9d1a87991cc2c41840ebb9edf21d1b": { + "balance": "0xb98bc829a6f90000" + }, + "85ca8bc6da2803d0725f5e1a456c89f9bc774e2f": { + "balance": "0x2086ac351052600000" + }, + "85d0d88754ac84b8b21ba93dd2bfec72626faba8": { + "balance": "0x3635c9adc5dea00000" + }, + "85eb256b51c819d60ea61a82d12c9358d59c1cae": { + "balance": "0x18efc84ad0c7b00000" + }, + "85f0e7c1e3aff805a627a2aaf2cff6b4c0dbe9cb": { + "balance": "0x1158e460913d00000" + }, + "86026cad3fe4ea1ce7fca260d3d45eb09ea6a364": { + "balance": "0xad78ebc5ac6200000" + }, + "860f5ffc10de767ded807f71e861d647dfd219b1": { + "balance": "0x21e19e0c9bab2400000" + }, + "86153063a1ae7f02f1a88136d4d69c7c5e3e4327": { + "balance": "0x3635c9adc5dea00000" + }, + "86245f596691093ece3f3d3ca2263eace81941d9": { + "balance": "0xa31062beeed700000" + }, + "862569211e8c6327b5415e3a67e5738b15baaf6e": { + "balance": "0x796e3ea3f8ab00000" + }, + "86297d730fe0f7a9ee24e08fb1087b31adb306a7": { + "balance": "0x6c6b935b8bbd400000" + }, + "8644cc281be332ccced36da483fb2a0746d9ba2e": { + "balance": "0x15af1d78b58c400000" + }, + "86499a1228ff2d7ee307759364506f8e8c8307a5": { + "balance": "0x6acb3df27e1f880000" + }, + "864bec5069f855a4fd5892a6c4491db07c88ff7c": { + "balance": "0x3635c9adc5dea00000" + }, + "86570ab259c9b1c32c9729202f77f590c07dd612": { + "balance": "0xad78ebc5ac6200000" + }, + "8663a241a0a89e70e182c845e2105c8ad7264bcf": { + "balance": "0x323b13d8398f3238000" + }, + "8667fa1155fed732cfb8dca5a0d765ce0d0705ed": { + "balance": "0x46ec965c393b10000" + }, + "8668af868a1e98885f937f2615ded6751804eb2d": { + "balance": "0x1158e460913d00000" + }, + "86740a46648e845a5d96461b18091ff57be8a16f": { + "balance": "0x14c0973485bf39400000" + }, + "867eba56748a5904350d2ca2a5ce9ca00b670a9b": { + "balance": "0x43c33c1937564800000" + }, + "86806474c358047d9406e6a07f40945bc8328e67": { + "balance": "0x1752eb0f7013d100000" + }, + "86883d54cd3915e549095530f9ab1805e8c5432d": { + "balance": "0xd8d726b7177a800000" + }, + "868c23be873466d4c74c220a19b245d1787e807f": { + "balance": "0x4a13bbbd92c88e8000" + }, + "86924fb211aad23cf5ce600e0aae806396444087": { + "balance": "0x21e19e0c9bab2400000" + }, + "8693e9b8be94425eef7969bc69f9d42f7cad671e": { + "balance": "0x3637096c4bcc690000" + }, + "869f1aa30e4455beb1822091de5cadec79a8f946": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "86a1eadeeb30461345d9ef6bd05216fa247c0d0c": { + "balance": "0x6c6b935b8bbd400000" + }, + "86a5f8259ed5b09e188ce346ee92d34aa5dd93fa": { + "balance": "0xad78ebc5ac6200000" + }, + "86b7bd563ceab686f96244f9ddc02ad7b0b14bc2": { + "balance": "0x21e19e0c9bab2400000" + }, + "86c28b5678af37d727ec05e4447790f15f71f2ea": { + "balance": "0xad78ebc5ac6200000" + }, + "86c4ce06d9ac185bb148d96f7b7abe73f441006d": { + "balance": "0x21e19e0c9bab2400000" + }, + "86c8d0d982b539f48f9830f9891f9d607a942659": { + "balance": "0x2ced37761824fb00000" + }, + "86c934e38e53be3b33f274d0539cfca159a4d0d1": { + "balance": "0x34957444b840e80000" + }, + "86ca0145957e6b0dfe36875fbe7a0dec55e17a28": { + "balance": "0x21e19e0c9bab2400000" + }, + "86caafacf32aa0317c032ac36babed974791dc03": { + "balance": "0x878678326eac9000000" + }, + "86cdb7e51ac44772be3690f61d0e59766e8bfc18": { + "balance": "0xd8d726b7177a800000" + }, + "86df73bd377f2c09de63c45d67f283eaefa0f4ab": { + "balance": "0x3635c9adc5dea00000" + }, + "86e3fe86e93da486b14266eadf056cbfa4d91443": { + "balance": "0x6c6b935b8bbd400000" + }, + "86e8670e27598ea09c3899ab7711d3b9fe901c17": { + "balance": "0xad78ebc5ac6200000" + }, + "86ef6426211949cc37f4c75e7850369d0cf5f479": { + "balance": "0x2d65f32ea045af60000" + }, + "86f05d19063e9369c6004eb3f123943a7cff4eab": { + "balance": "0x6c6acc67d7b1d40000" + }, + "86f23e9c0aafc78b9c404dcd60339a925bffa266": { + "balance": "0x15af1d78b58c400000" + }, + "86f4f40ad984fbb80933ae626e0e42f9333fdd41": { + "balance": "0x3635c9adc5dea00000" + }, + "86f95c5b11a293940e35c0b898d8b75f08aab06d": { + "balance": "0x644e3e875fccf740000" + }, + "86fff220e59305c09f483860d6f94e96fbe32f57": { + "balance": "0x2535b6ab4c0420000" + }, + "870796abc0db84af82da52a0ed68734de7e636f5": { + "balance": "0x1043561a8829300000" + }, + "870f15e5df8b0eabd02569537a8ef93b56785c42": { + "balance": "0x150894e849b3900000" + }, + "87183160d172d2e084d327b86bcb7c1d8e6784ef": { + "balance": "0xd8d8583fa2d52f0000" + }, + "871b8a8b51dea1989a5921f13ec1a955a515ad47": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "8725e8c753b3acbfdca55f3c62dfe1a59454968a": { + "balance": "0x3637096c4bcc690000" + }, + "8737dae671823a8d5917e0157ace9c43468d946b": { + "balance": "0x6c6acc67d7b1d40000" + }, + "873b7f786d3c99ff012c4a7cae2677270240b9c5": { + "balance": "0x5dc892aa1131c80000" + }, + "873c6f70efb6b1d0f2bbc57eebcd70617c6ce662": { + "balance": "0x36f0d5275d09570000" + }, + "873e49135c3391991060290aa7f6ccb8f85a78db": { + "balance": "0x1158e460913d00000" + }, + "875061ee12e820041a01942cb0e65bb427b00060": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "87584a3f613bd4fac74c1e780b86d6caeb890cb2": { + "balance": "0x5c283d410394100000" + }, + "8764d02722000996ecd475b433298e9f540b05bf": { + "balance": "0xad78ebc5ac6200000" + }, + "876c3f218b4776df3ca9dbfb270de152d94ed252": { + "balance": "0x56bc75e2d63100000" + }, + "8775a610c502b9f1e6ad4cdadb8ce29bff75f6e4": { + "balance": "0x2086ac351052600000" + }, + "87764e3677eef604cbc59aed24abdc566b09fc25": { + "balance": "0xa2a15d09519be00000" + }, + "8787d12677a5ec291e57e31ffbfad105c3324b87": { + "balance": "0x2a24eb53208f3128000" + }, + "8794bf47d54540ece5c72237a1ffb511ddb74762": { + "balance": "0x6c6b935b8bbd400000" + }, + "87a53ea39f59a35bada8352521645594a1a714cb": { + "balance": "0x678a932062e4180000" + }, + "87a7c508ef71582dd9a54372f89cb01f252fb180": { + "balance": "0xad78ebc5ac6200000" + }, + "87af25d3f6f8eea15313d5fe4557e810c524c083": { + "balance": "0x42bf06b78ed3b500000" + }, + "87b10f9c280098179a2b76e9ce90be61fc844d0d": { + "balance": "0x487a9a304539440000" + }, + "87bf7cd5d8a929e1c785f9e5449106ac232463c9": { + "balance": "0x437b11fcc45640000" + }, + "87c498170934b8233d1ad1e769317d5c475f2f40": { + "balance": "0x3708baed3d68900000" + }, + "87cf36ad03c9eae9053abb5242de9117bb0f2a0b": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "87d7ac0653ccc67aa9c3469eef4352193f7dbb86": { + "balance": "0x2a5a058fc295ed000000" + }, + "87e3062b2321e9dfb0875ce3849c9b2e3522d50a": { + "balance": "0x21e19e0c9bab2400000" + }, + "87e6034ecf23f8b5639d5f0ea70a22538a920423": { + "balance": "0x11c7ea162e78200000" + }, + "87ef6d8b6a7cbf9b5c8c97f67ee2adc2a73b3f77": { + "balance": "0xadd1bd23c3c480000" + }, + "87fb26c31e48644d693134205cae43b21f18614b": { + "balance": "0x4a4491bd6dcd280000" + }, + "87fc4635263944ce14a46c75fa4a821f39ce7f72": { + "balance": "0x1158e460913d00000" + }, + "87fcbe7c4193ffcb08143779c9bec83fe7fda9fc": { + "balance": "0x56f985d38644b8000" + }, + "88015d7203c5e0224aeda286ed12f1a51b789333": { + "balance": "0x10f08eda8e555098000" + }, + "88106c27d20b74b4b98ca62b232bd5c97411171f": { + "balance": "0xaadec983fcff40000" + }, + "881230047c211d2d5b00d8de4c5139de5e3227c7": { + "balance": "0x21e19e0c9bab2400000" + }, + "882aa798bf41df179f85520130f15ccdf59b5e58": { + "balance": "0x6c6b935b8bbd400000" + }, + "882bd3a2e9d74110b24961c53777f22f1f46dc5d": { + "balance": "0x2d4ca05e2b43ca80000" + }, + "882c8f81872c79fed521cb5f950d8b032322ea69": { + "balance": "0x878678326eac9000000" + }, + "882f75708386653c80171d0663bfe30b017ed0ad": { + "balance": "0x6c6b935b8bbd400000" + }, + "88344909644c7ad4930fd873ca1c0da2d434c07f": { + "balance": "0x727739fcb004d0000" + }, + "8834b2453471f324fb26be5b25166b5b5726025d": { + "balance": "0x1f0ff8f01daad40000" + }, + "883a78aeabaa50d8ddd8570bcd34265f14b19363": { + "balance": "0xd25522fda379a18000" + }, + "8845e9f90e96336bac3c616be9d88402683e004c": { + "balance": "0x6c6b935b8bbd400000" + }, + "8846928d683289a2d11df8db7a9474988ef01348": { + "balance": "0x21e19e0c9bab2400000" + }, + "884980eb4565c1048317a8f47fdbb461965be481": { + "balance": "0xd8d6119a8146050000" + }, + "884a7a39d0916e05f1c242df55607f37df8c5fda": { + "balance": "0x4f4843c157c8ca00000" + }, + "885493bda36a0432976546c1ddce71c3f4570021": { + "balance": "0xbbf510ddfcb260000" + }, + "88609e0a465b6e99fce907166d57e9da0814f5c8": { + "balance": "0x43c33c1937564800000" + }, + "886d0a9e17c9c095af2ea2358b89ec705212ee94": { + "balance": "0x18493fba64ef00000" + }, + "88797e58675ed5cc4c19980783dbd0c956085153": { + "balance": "0x6c6b935b8bbd400000" + }, + "887cac41cd706f3345f2d34ac34e01752a6e5909": { + "balance": "0x20465cee9da1370000" + }, + "88888a57bd9687cbf950aeeacf9740dcc4d1ef59": { + "balance": "0x62a992e53a0af00000" + }, + "8889448316ccf14ed86df8e2f478dc63c4338340": { + "balance": "0xd2f13f7789f00000" + }, + "888c16144933197cac26504dd76e06fd6600c789": { + "balance": "0x56bc75e2d63100000" + }, + "888e94917083d152202b53163939869d271175b4": { + "balance": "0xd8d726b7177a800000" + }, + "889087f66ff284f8b5efbd29493b706733ab1447": { + "balance": "0x215f835bc769da80000" + }, + "8895eb726226edc3f78cc6a515077b3296fdb95e": { + "balance": "0xd5967be4fc3f100000" + }, + "88975a5f1ef2528c300b83c0c607b8e87dd69315": { + "balance": "0x486cb9799191e0000" + }, + "889da40fb1b60f9ea9bd7a453e584cf7b1b4d9f7": { + "balance": "0x22b1c8c1227a00000" + }, + "889da662eb4a0a2a069d2bc24b05b4ee2e92c41b": { + "balance": "0x5a2c8c5456c9f28000" + }, + "88a122a2382c523931fb51a0ccad3beb5b7259c3": { + "balance": "0x6c6b935b8bbd400000" + }, + "88a2154430c0e41147d3c1fee3b3b006f851edbd": { + "balance": "0x36356633ebd8ea0000" + }, + "88b217ccb786a254cf4dc57f5d9ac3c455a30483": { + "balance": "0x3224f42723d4540000" + }, + "88bc43012edb0ea9f062ac437843250a39b78fbb": { + "balance": "0x43c33c1937564800000" + }, + "88c2516a7cdb09a6276d7297d30f5a4db1e84b86": { + "balance": "0xd8d726b7177a800000" + }, + "88c361640d6b69373b081ce0c433bd590287d5ec": { + "balance": "0xa968163f0a57b400000" + }, + "88d541c840ce43cefbaf6d19af6b9859b573c145": { + "balance": "0x93739534d28680000" + }, + "88de13b09931877c910d593165c364c8a1641bd3": { + "balance": "0xa2a15d09519be00000" + }, + "88dec5bd3f4eba2d18b8aacefa7b721548c319ba": { + "balance": "0x4a4491bd6dcd280000" + }, + "88e6f9b247f988f6c0fc14c56f1de53ec69d43cc": { + "balance": "0x56bc75e2d63100000" + }, + "88ee7f0efc8f778c6b687ec32be9e7d6f020b674": { + "balance": "0x6c6b935b8bbd400000" + }, + "88f1045f19f2d3191816b1df18bb6e1435ad1b38": { + "balance": "0xd02ab486cedc00000" + }, + "89009e3c6488bd5e570d1da34eabe28ed024de1b": { + "balance": "0x43c33c1937564800000" + }, + "89054430dcdc28ac15fa635ef87c105e602bf70c": { + "balance": "0x5dacd13ca9e300000" + }, + "8908760cd39b9c1e8184e6a752ee888e3f0b7045": { + "balance": "0x14542ba12a337c00000" + }, + "890fe11f3c24db8732d6c2e772e2297c7e65f139": { + "balance": "0xd5627137da8b5900000" + }, + "8914a680a5aec5226d4baaec2e5552b44dd7c874": { + "balance": "0x56cd55fc64dfe0000" + }, + "891cb8238c88e93a1bcf61db49bd82b47a7f4f84": { + "balance": "0x914878a8c05ee00000" + }, + "8925da4549e15155e57a628522cea9dddf627d81": { + "balance": "0x3636c25e66ece70000" + }, + "893017ff1adad499aa065401b4236ce6e92b625a": { + "balance": "0x6c6acc67d7b1d40000" + }, + "8933491760c8f0b4df8caac78ed835caee21046d": { + "balance": "0x43c33c1937564800000" + }, + "893608751d68d046e85802926673cdf2f57f7cb8": { + "balance": "0x11164759ffb320000" + }, + "8938d1b4daee55a54d738cf17e4477f6794e46f7": { + "balance": "0xfc936392801c0000" + }, + "893a6c2eb8b40ab096b4f67e74a897b840746e86": { + "balance": "0x5dc892aa1131c80000" + }, + "893cdddf5377f3c751bf2e541120045a47cba101": { + "balance": "0x56bc75e2d63100000" + }, + "895613236f3584216ad75c5d3e07e3fa6863a778": { + "balance": "0x6c6b935b8bbd400000" + }, + "8957727e72cf629020f4e05edf799aa7458062d0": { + "balance": "0x77432217e683600000" + }, + "895d694e880b13ccd0848a86c5ce411f88476bbf": { + "balance": "0xad6eedd17cf3b8000" + }, + "895ec5545644e0b78330fffab8ddeac9e833156c": { + "balance": "0x2086ac351052600000" + }, + "896009526a2c7b0c09a6f63a80bdf29d9c87de9c": { + "balance": "0xbbb86b8223edeb0000" + }, + "8967d7b9bdb7b4aed22e65a15dc803cb7a213f10": { + "balance": "0x15af1d78b58c400000" + }, + "896e335ca47af57962fa0f4dbf3e45e688cba584": { + "balance": "0x4a2fc0ab6052120000" + }, + "8973aefd5efaee96095d9e288f6a046c97374b43": { + "balance": "0x7a4c4a0f332140000" + }, + "898c72dd736558ef9e4be9fdc34fef54d7fc7e08": { + "balance": "0x3635c9adc5dea00000" + }, + "899b3c249f0c4b81df75d212004d3d6d952fd223": { + "balance": "0x6c6b935b8bbd400000" + }, + "89ab13ee266d779c35e8bb04cd8a90cc2103a95b": { + "balance": "0xcb49b44ba602d800000" + }, + "89c433d601fad714da6369308fd26c1dc9942bbf": { + "balance": "0x6c6b935b8bbd400000" + }, + "89d75b8e0831e46f80bc174188184e006fde0eae": { + "balance": "0x3635c9adc5dea00000" + }, + "89e3b59a15864737d493c1d23cc53dbf8dcb1362": { + "balance": "0xd8d726b7177a800000" + }, + "89fc8e4d386b0d0bb4a707edf3bd560df1ad8f4e": { + "balance": "0xa030dcebbd2f4c0000" + }, + "89fee30d1728d96cecc1dab3da2e771afbcfaa41": { + "balance": "0x6c6acc67d7b1d40000" + }, + "8a1cc5ac111c49bfcfd848f37dd768aa65c88802": { + "balance": "0x21e19e0c9bab2400000" + }, + "8a20e5b5cee7cd1f5515bace3bf4f77ffde5cc07": { + "balance": "0x4563918244f400000" + }, + "8a217db38bc35f215fd92906be42436fe7e6ed19": { + "balance": "0x14542ba12a337c00000" + }, + "8a243a0a9fea49b839547745ff2d11af3f4b0522": { + "balance": "0x35659ef93f0fc40000" + }, + "8a247d186510809f71cffc4559471c3910858121": { + "balance": "0x61093d7c2c6d380000" + }, + "8a3470282d5e2a2aefd7a75094c822c4f5aeef8a": { + "balance": "0xd28bc606478a58000" + }, + "8a36869ad478997cbf6d8924d20a3c8018e9855b": { + "balance": "0x1158e460913d00000" + }, + "8a4314fb61cd938fc33e15e816b113f2ac89a7fb": { + "balance": "0x17764e7aed65100000" + }, + "8a4f4a7f52a355ba105fca2072d3065fc8f7944b": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "8a5831282ce14a657a730dc18826f7f9b99db968": { + "balance": "0xeabe8a5b41c1360000" + }, + "8a5fb75793d043f1bcd43885e037bd30a528c927": { + "balance": "0x13536e6d2e9ac20000" + }, + "8a66abbc2d30ce21a833b0db8e561d5105e0a72c": { + "balance": "0x25f1de5c76acdf0000" + }, + "8a746c5d67064711bfca685b95a4fe291a27028e": { + "balance": "0x22b1c8c1227a00000" + }, + "8a780ab87a9145fe10ed60fa476a740af4cab1d2": { + "balance": "0x121b2e5e6464780000" + }, + "8a7a06be199a3a58019d846ac9cbd4d95dd757de": { + "balance": "0xa2a423944256f40000" + }, + "8a810114b2025db9fbb50099a6e0cb9e2efa6bdc": { + "balance": "0x678a932062e4180000" + }, + "8a86e4a51c013b1fb4c76bcf30667c78d52eedef": { + "balance": "0x6c6b935b8bbd400000" + }, + "8a9eca9c5aba8e139f8003edf1163afb70aa3aa9": { + "balance": "0x23c757072b8dd00000" + }, + "8ab839aeaf2ad37cb78bacbbb633bcc5c099dc46": { + "balance": "0x6c6b935b8bbd400000" + }, + "8ac89bd9b8301e6b0677fa25fcf0f58f0cc7b611": { + "balance": "0x1158e460913d00000" + }, + "8adc53ef8c18ed3051785d88e996f3e4b20ecd51": { + "balance": "0x8e4d316827686400000" + }, + "8ae6f80b70e1f23c91fbd5a966b0e499d95df832": { + "balance": "0xaadec983fcff40000" + }, + "8ae9ef8c8a8adfa6ab798ab2cdc405082a1bbb70": { + "balance": "0x6c6b935b8bbd400000" + }, + "8af626a5f327d7506589eeb7010ff9c9446020d2": { + "balance": "0x4be4e7267b6ae00000" + }, + "8b01da34d470c1d115acf4d8113c4dd8a8c338e4": { + "balance": "0x5572dcefab697900000" + }, + "8b07d050754dc9ba230db01c310afdb5395aa1b3": { + "balance": "0x666b06e62a6200000" + }, + "8b20ad3b94656dbdc0dd21a393d8a7d9e02138cb": { + "balance": "0xa2a15d09519be00000" + }, + "8b27392206b958cd375d7ef8af2cf8ef0598c0bc": { + "balance": "0x3635c9adc5dea00000" + }, + "8b30c04098d7a7e6420c357ea7bfa49bac9a8a18": { + "balance": "0x1b1b113f91fb0140000" + }, + "8b338411f26ccf37658cc75521d77629099e467d": { + "balance": "0x6c6b935b8bbd400000" + }, + "8b36224c7356e751f0c066c35e3b44860364bfc2": { + "balance": "0x3627bac7a3d9278000" + }, + "8b3696f3c60de32432a2e4c395ef0303b7e81e75": { + "balance": "0x65a4da25d3016c00000" + }, + "8b393fb0813ee101db1e14ecc7d322c72b8c0473": { + "balance": "0x18b26a313e8ae90000" + }, + "8b48e19d39dd35b66e6e1bb6b9c657cb2cf59d04": { + "balance": "0x3c755ac9c024a018000" + }, + "8b505e2871f7deb7a63895208e8227dcaa1bff05": { + "balance": "0xcf68efc308d79bc0000" + }, + "8b57b2bc83cc8d4de331204e893f2f3b1db1079a": { + "balance": "0x22b1c8c1227a00000" + }, + "8b5c914b128bf1695c088923fa467e7911f351fa": { + "balance": "0x556f64c1fe7fa0000" + }, + "8b5f29cc2faa262cdef30ef554f50eb488146eac": { + "balance": "0x13b68705c9720810000" + }, + "8b7056f6abf3b118d026e944d5c073433ca451d7": { + "balance": "0x3635c6204739d98000" + }, + "8b714522fa2839620470edcf0c4401b713663df1": { + "balance": "0xad78ebc5ac6200000" + }, + "8b74a7cb1bb8c58fce267466a30358adaf527f61": { + "balance": "0x2e257784e25b4500000" + }, + "8b7e9f6f05f7e36476a16e3e7100c9031cf404af": { + "balance": "0x3635c9adc5dea00000" + }, + "8b81156e698639943c01a75272ad3d35851ab282": { + "balance": "0x12b3165f65d3e50000" + }, + "8b9577920053b1a00189304d888010d9ef2cb4bf": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "8b9841862e77fbbe919470935583a93cf027e450": { + "balance": "0x6c6c5334427f1f0000" + }, + "8b997dbc078ad02961355da0a159f2927ed43d64": { + "balance": "0xaadec983fcff40000" + }, + "8b9fda7d981fe9d64287f85c94d83f9074849fcc": { + "balance": "0x2f6f10780d22cc00000" + }, + "8bb0212f3295e029cab1d961b04133a1809e7b91": { + "balance": "0x6c6b935b8bbd400000" + }, + "8bbeacfc29cfe93402db3c41d99ab759662e73ec": { + "balance": "0x6c6b935b8bbd400000" + }, + "8bc1ff8714828bf286ff7e8a7709106548ed1b18": { + "balance": "0x21e19e0c9bab2400000" + }, + "8bd0b65a50ef5cef84fec420be7b89ed1470ceb9": { + "balance": "0x28a77936e92c81c0000" + }, + "8bd6b1c6d74d010d1008dba6ef835d4430b35c32": { + "balance": "0x2b5e3af16b1880000" + }, + "8bd8d4c4e943f6c8073921dc17e3e8d7a0761627": { + "balance": "0x9f04219d8d34950000" + }, + "8bdfda6c215720eda2136f91052321af4e936c1f": { + "balance": "0x3635e619bb04d40000" + }, + "8bea40379347a5c891d59a6363315640f5a7e07a": { + "balance": "0x6c6b76ef96970c0000" + }, + "8bf02bd748690e1fd1c76d270833048b66b25fd3": { + "balance": "0x27fade568eba9600000" + }, + "8bf297f8f453523ed66a1acb7676856337b93bf0": { + "balance": "0xd8d726b7177a800000" + }, + "8bf373d076814cbc57e1c6d16a82c5be13c73d37": { + "balance": "0xad78ebc5ac6200000" + }, + "8c1023fde1574db8bb54f1739670157ca47da652": { + "balance": "0x179cf9ac3a1b1770000" + }, + "8c1fbe5f0aea359c5aa1fa08c8895412ca8e05a6": { + "balance": "0x3635c9adc5dea00000" + }, + "8c22426055b76f11f0a2de1a7f819a619685fe60": { + "balance": "0x6b56051582a9700000" + }, + "8c2b7d8b608d28b77f5caa9cd645242a823e4cd9": { + "balance": "0x62a992e53a0af00000" + }, + "8c2fbeee8eacc5c5d77c16abd462ee9c8145f34b": { + "balance": "0x692ae8897081d00000" + }, + "8c3a9ee71f729f236cba3867b4d79d8ceee25dbc": { + "balance": "0x56bc75e2d63100000" + }, + "8c50aa2a9212bcde56418ae261f0b35e7a9dbb82": { + "balance": "0x15af1d78b58c400000" + }, + "8c54c7f8b9896e75d7d5f5c760258699957142ad": { + "balance": "0x22b1c8c1227a00000" + }, + "8c5d16ed65e3ed7e8b96ca972bc86173e3500b03": { + "balance": "0x6c6b935b8bbd400000" + }, + "8c6aa882ee322ca848578c06cb0fa911d3608305": { + "balance": "0x2086ac351052600000" + }, + "8c6ae7a05a1de57582ae2768204276c0ff47ed03": { + "balance": "0x2c0bb3dd30c4e2000000" + }, + "8c6f9f4e5b7ae276bf58497bd7bf2a7d25245f64": { + "balance": "0x93fe5c57d710680000" + }, + "8c75956e8fed50f5a7dd7cfd27da200f6746aea6": { + "balance": "0x3635c9adc5dea00000" + }, + "8c7cb4e48b25031aa1c4f92925d631a8c3edc761": { + "balance": "0x3635c9adc5dea00000" + }, + "8c7fa5cae82fedb69ab189d3ff27ae209293fb93": { + "balance": "0x15af880d8cdb830000" + }, + "8c81410ea8354cc5c65c41be8bd5de733c0b111d": { + "balance": "0x205b4dfa1ee74780000" + }, + "8c83d424a3cf24d51f01923dd54a18d6b6fede7b": { + "balance": "0xd8d726b7177a800000" + }, + "8c900a8236b08c2b65405d39d75f20062a7561fd": { + "balance": "0x58e7926ee858a00000" + }, + "8c93c3c6db9d37717de165c3a1b4fe51952c08de": { + "balance": "0x15af1d78b58c400000" + }, + "8c999591fd72ef7111efca7a9e97a2356b3b000a": { + "balance": "0xdd64e2aa0a67500000" + }, + "8ca6989746b06e32e2487461b1ce996a273acfd7": { + "balance": "0x1158e460913d00000" + }, + "8cb3aa3fcd212854d7578fcc30fdede6742a312a": { + "balance": "0x1043561a8829300000" + }, + "8cc0d7c016fa7aa950114aa1db094882eda274ea": { + "balance": "0x8a9aba557e36c0000" + }, + "8cc652dd13e7fe14dabbb36d5d320db9ffee8a54": { + "balance": "0x61093d7c2c6d380000" + }, + "8ccabf25077f3aa41545344d53be1b2b9c339000": { + "balance": "0x5be866c562c5440000" + }, + "8ccf3aa21ab742576ad8c422f71bb188591dea8a": { + "balance": "0x3635c9adc5dea00000" + }, + "8cd0cd22e620eda79c0461e896c93c44837e2968": { + "balance": "0x6c6b935b8bbd400000" + }, + "8cde8b732e6023878eb23ed16229124b5f7afbec": { + "balance": "0x73f75d1a085ba0000" + }, + "8ce22f9fa372449a420610b47ae0c8d565481232": { + "balance": "0x6c6b935b8bbd400000" + }, + "8ce4949d8a16542d423c17984e6739fa72ceb177": { + "balance": "0x54b405926f4a63d8000" + }, + "8ce5e3b5f591d5eca38abf228f2e3c35134bdac0": { + "balance": "0x7dc35b84897c380000" + }, + "8cee38d6595788a56e3fb94634b3ffe1fbdb26d6": { + "balance": "0x43c33c1937564800000" + }, + "8ceea15eec3bdad8023f98ecf25b2b8fef27db29": { + "balance": "0x6c6b935b8bbd400000" + }, + "8cf3546fd1cda33d58845fc8fcfecabca7c5642a": { + "balance": "0x1f1e39932cb3278000" + }, + "8cf6da0204dbc4860b46ad973fc111008d9e0c46": { + "balance": "0xad78ebc5ac6200000" + }, + "8cfedef198db0a9143f09129b3fd64dcbb9b4956": { + "balance": "0x6c6b935b8bbd400000" + }, + "8d04a5ebfb5db409db0617c9fa5631c192861f4a": { + "balance": "0x34957444b840e80000" + }, + "8d06e464245cad614939e0af0845e6d730e20374": { + "balance": "0xadc8a28f3d87d8000" + }, + "8d07d42d831c2d7c838aa1872b3ad5d277176823": { + "balance": "0x12ee1f9ddbee680000" + }, + "8d0b9ea53fd263415eac11391f7ce9123c447062": { + "balance": "0x6c6b935b8bbd400000" + }, + "8d1794da509cb297053661a14aa892333231e3c1": { + "balance": "0xad201a6794ff80000" + }, + "8d1abd897dacd4312e18080c88fb9647eab44052": { + "balance": "0xbb59a27953c600000" + }, + "8d2303341e1e1eb5e8189bde03f73a60a2a54861": { + "balance": "0x56bc75e2d63100000" + }, + "8d238e036596987643d73173c37b0ad06055b96c": { + "balance": "0x7148bf0a2af0660000" + }, + "8d2e31b08803b2c5f13d398ecad88528209f6057": { + "balance": "0x21db8bbcad11e840000" + }, + "8d378f0edc0bb0f0686d6a20be6a7692c4fa24b8": { + "balance": "0x56bc75e2d63100000" + }, + "8d4b603c5dd4570c34669515fdcc665890840c77": { + "balance": "0xfc936392801c0000" + }, + "8d51a4cc62011322c696fd725b9fb8f53feaaa07": { + "balance": "0x3635c9adc5dea00000" + }, + "8d544c32c07fd0842c761d53a897d6c950bb7599": { + "balance": "0xad78ebc5ac6200000" + }, + "8d5ef172bf77315ea64e85d0061986c794c6f519": { + "balance": "0xd5967be4fc3f100000" + }, + "8d616b1eee77eef6f176e0698db3c0c141b2fc8f": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "8d6170ff66978e773bb621bf72b1ba7be3a7f87e": { + "balance": "0xad78ebc5ac6200000" + }, + "8d620bde17228f6cbba74df6be87264d985cc179": { + "balance": "0x56bc75e2d63100000" + }, + "8d629c20608135491b5013f1002586a0383130e5": { + "balance": "0x4a4491bd6dcd280000" + }, + "8d6657f59711b1f803c6ebef682f915b62f92dc9": { + "balance": "0x6c6b935b8bbd400000" + }, + "8d667637e29eca05b6bfbef1f96d460eefbf9984": { + "balance": "0xd8d726b7177a800000" + }, + "8d6df209484d7b94702b03a53e56b9fb0660f6f0": { + "balance": "0x6c6b935b8bbd400000" + }, + "8d795c5f4a5689ad62da961671f028065286d554": { + "balance": "0x6f05b59d3b20000000" + }, + "8d7f3e61299c2db9b9c0487cf627519ed00a9123": { + "balance": "0x5e74a8505e80a00000" + }, + "8d89170b92b2be2c08d57c48a7b190a2f146720f": { + "balance": "0x42bf06b78ed3b500000" + }, + "8d93dac785f88f1a84bf927d53652b45a154ccdd": { + "balance": "0x890b0c2e14fb80000" + }, + "8d9952d0bb4ebfa0efd01a3aa9e8e87f0525742e": { + "balance": "0xbb9125542263900000" + }, + "8d9a0c70d2262042df1017d6c303132024772712": { + "balance": "0x6c6b935b8bbd400000" + }, + "8d9ed7f4553058c26f7836a3802d3064eb1b363d": { + "balance": "0x4e1003b28d9280000" + }, + "8da1178f55d97772bb1d24111a404a4f8715b95d": { + "balance": "0x2f9ac3f6de00808000" + }, + "8da1d359ba6cb4bcc57d7a437720d55db2f01c72": { + "balance": "0x4563918244f400000" + }, + "8dab948ae81da301d972e3f617a912e5a753712e": { + "balance": "0x15af1d78b58c400000" + }, + "8daddf52efbd74da95b969a5476f4fbbb563bfd2": { + "balance": "0x2d43f3ebfafb2c0000" + }, + "8db185fe1b70a94a6a080e7e23a8bedc4acbf34b": { + "balance": "0x4be4e7267b6ae00000" + }, + "8db58e406e202df9bc703c480bd8ed248d52a032": { + "balance": "0x6c6b935b8bbd400000" + }, + "8dbc3e6cb433e194f40f82b40faadb1f8b856116": { + "balance": "0x678a932062e4180000" + }, + "8dc1d5111d09af25fdfcac455c7cec283e6d6775": { + "balance": "0x6c6b935b8bbd400000" + }, + "8dd484ff8a307364eb66c525a571aac701c5c318": { + "balance": "0xd8d726b7177a800000" + }, + "8dd6a9bae57f518549ada677466fea8ab04fd9b4": { + "balance": "0xd8d726b7177a800000" + }, + "8dde3cb8118568ef4503fe998ccdf536bf19a098": { + "balance": "0xd8d726b7177a800000" + }, + "8dde60eb08a099d7daa356daaab2470d7b025a6b": { + "balance": "0xaadec983fcff40000" + }, + "8df339214b6ad1b24663ce716034749d6ef838d9": { + "balance": "0x2544faa778090e00000" + }, + "8df53d96191471e059de51c718b983e4a51d2afd": { + "balance": "0x6c6b935b8bbd4000000" + }, + "8dfbafbc0e5b5c86cd1ad697feea04f43188de96": { + "balance": "0x15252b7f5fa0de0000" + }, + "8e073bad25e42218615f4a0e6b2ea8f8de2230c0": { + "balance": "0x823d629d026bfa0000" + }, + "8e0fee38685a94aabcd7ce857b6b1409824f75b8": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "8e23facd12c765c36ab81a6dd34d8aa9e68918ae": { + "balance": "0x911e4868dba9b0000" + }, + "8e2f9034c9254719c38e50c9aa64305ed696df1e": { + "balance": "0x1004e2e45fb7ee00000" + }, + "8e3240b0810e1cf407a500804740cf8d616432a4": { + "balance": "0x22f6655ef0b388000" + }, + "8e486a0442d171c8605be348fee57eb5085eff0d": { + "balance": "0xd8d726b7177a800000" + }, + "8e6156336be2cdbe32140df08a2ba55fd0a58463": { + "balance": "0x4099e1d6357180000" + }, + "8e670815fb67aeaea57b86534edc00cdf564fee5": { + "balance": "0xb2e4b323d9c5100000" + }, + "8e6d7485cbe990acc1ad0ee9e8ccf39c0c93440e": { + "balance": "0x33c5499031720c0000" + }, + "8e74e0d1b77ebc823aca03f119854cb12027f6d7": { + "balance": "0x16b352da5e0ed3000000" + }, + "8e78f351457d016f4ad2755ec7424e5c21ba6d51": { + "balance": "0x7ea28327577080000" + }, + "8e7936d592008fdc7aa04edeeb755ab513dbb89d": { + "balance": "0x1158e460913d00000" + }, + "8e7fd23848f4db07906a7d10c04b21803bb08227": { + "balance": "0x3635c9adc5dea00000" + }, + "8e92aba38e72a098170b92959246537a2e5556c0": { + "balance": "0xe7eeba3410b740000" + }, + "8e98766524b0cf2747c50dd43b9567594d9731de": { + "balance": "0x6c44b7c26182280000" + }, + "8e9b35ad4a0a86f758446fffde34269d940ceacd": { + "balance": "0xd8d726b7177a800000" + }, + "8e9c08f738661f9676236eff82ba6261dd3f4822": { + "balance": "0x56bc75e2d63100000" + }, + "8e9c429266df057efa78dd1d5f77fc40742ad466": { + "balance": "0x10442ed1b56c7c8000" + }, + "8ea656e71ec651bfa17c5a5759d86031cc359977": { + "balance": "0x56bc75e2d63100000" + }, + "8eae29435598ba8f1c93428cdb3e2b4d31078e00": { + "balance": "0x6c6b935b8bbd400000" + }, + "8eb1fbe4e5d3019cd7d30dae9c0d5b4c76fb6331": { + "balance": "0x6c6b935b8bbd400000" + }, + "8eb51774af206b966b8909c45aa6722748802c0c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "8eb8c71982a00fb84275293253f8044544b66b49": { + "balance": "0x15af1d78b58c400000" + }, + "8ecbcfacbfafe9f00c3922a24e2cf0026756ca20": { + "balance": "0x131beb925ffd3200000" + }, + "8eceb2e124536c5b5ffc640ed14ff15ed9a8cb71": { + "balance": "0x6c6b935b8bbd400000" + }, + "8ed0af11ff2870da0681004afe18b013f7bd3882": { + "balance": "0xd8d726b7177a800000" + }, + "8ed143701f2f72280fd04a7b4164281979ea87c9": { + "balance": "0xc249fdd327780000" + }, + "8ed1528b447ed4297902f639c514d0944a88f8c8": { + "balance": "0xac6e77ab663a80000" + }, + "8ed4284c0f47449c15b8d9b3245de8beb6ce80bf": { + "balance": "0x2b5e3af16b18800000" + }, + "8ede7e3dc50749c6c50e2e28168478c34db81946": { + "balance": "0x43c30fb0884a96c0000" + }, + "8ee584337ddbc80f9e3498df55f0a21eacb57fb1": { + "balance": "0x1158e460913d00000" + }, + "8eebec1a62c08b05a7d1d59180af9ff0d18e3f36": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "8ef4d8a2c23c5279187b64e96f741404085385f3": { + "balance": "0x103dc1e9a9697b0000" + }, + "8ef711e43a13918f1303e81d0ea78c9eefd67eb2": { + "balance": "0xd8d726b7177a800000" + }, + "8efec058cc546157766a632775404a334aaada87": { + "balance": "0x6c5db2a4d815dc0000" + }, + "8f02bda6c36922a6be6a509be51906d393f7b99b": { + "balance": "0x37490dc12ebe7f8000" + }, + "8f0538ed71da1155e0f3bde5667ceb84318a1a87": { + "balance": "0x692ae8897081d00000" + }, + "8f067c7c1bbd57780b7b9eeb9ec0032f90d0dcf9": { + "balance": "0x43c33c1937564800000" + }, + "8f0ab894bd3f4e697dbcfb859d497a9ba195994a": { + "balance": "0x85d638b65472aa20000" + }, + "8f0af37566d152802f1ae8f928b25af9b139b448": { + "balance": "0xad78ebc5ac6200000" + }, + "8f1952eed1c548d9ee9b97d0169a07933be69f63": { + "balance": "0x3635c9adc5dea00000" + }, + "8f1fcc3c51e252b693bc5b0ec3f63529fe69281e": { + "balance": "0x14542ba12a337c00000" + }, + "8f226096c184ebb40105e08dac4d22e1c2d54d30": { + "balance": "0x109e437bd1618c0000" + }, + "8f29a14a845ad458f2d108b568d813166bcdf477": { + "balance": "0x21e19e0c9bab2400000" + }, + "8f31c7005197ec997a87e69bec48649ab94bb2a5": { + "balance": "0xd8d726b7177a800000" + }, + "8f41b1fbf54298f5d0bc2d122f4eb95da4e5cd3d": { + "balance": "0x1333832f5e335c0000" + }, + "8f47328ee03201c9d35ed2b5412b25decc859362": { + "balance": "0x6c6b935b8bbd400000" + }, + "8f473d0ab876ddaa15608621d7013e6ff714b675": { + "balance": "0x19801c83b6c7c00000" + }, + "8f4d1d41693e462cf982fd81d0aa701d3a5374c9": { + "balance": "0xd8d726b7177a800000" + }, + "8f4d1e7e4561284a34fef9673c0d34e12af4aa03": { + "balance": "0x6c6b935b8bbd400000" + }, + "8f4fb1aea7cd0f570ea5e61b40a4f4510b6264e4": { + "balance": "0xd8d726b7177a800000" + }, + "8f561b41b209f248c8a99f858788376250609cf3": { + "balance": "0x5c283d410394100000" + }, + "8f58d8348fc1dc4e0dd8343b6543c857045ee940": { + "balance": "0x2e3038df47303280000" + }, + "8f60895fbebbb5017fcbff3cdda397292bf25ba6": { + "balance": "0x174406ff9f6fd28000" + }, + "8f64b9c1246d857831643107d355b5c75fef5d4f": { + "balance": "0x6c6acc67d7b1d40000" + }, + "8f660f8b2e4c7cc2b4ac9c47ed28508d5f8f8650": { + "balance": "0x43c33c1937564800000" + }, + "8f69eafd0233cadb4059ab779c46edf2a0506e48": { + "balance": "0x60f06620a849450000" + }, + "8f717ec1552f4c440084fba1154a81dc003ebdc0": { + "balance": "0x21e19e0c9bab2400000" + }, + "8f8acb107607388479f64baaabea8ff007ada97d": { + "balance": "0x5c6f3080ad423f40000" + }, + "8f8cd26e82e7c6defd02dfad07979021cbf7150c": { + "balance": "0xa2a15d09519be00000" + }, + "8f8f37d0ad8f335d2a7101b41156b688a81a9cbe": { + "balance": "0x3cb71f51fc5580000" + }, + "8f92844f282a92999ee5b4a8d773d06b694dbd9f": { + "balance": "0x692ae8897081d00000" + }, + "8fac748f784a0fed68dba43319b42a75b4649c6e": { + "balance": "0x3154c9729d05780000" + }, + "8fd9a5c33a7d9edce0997bdf77ab306424a11ea9": { + "balance": "0x6c6b935b8bbd400000" + }, + "8feffadb387a1547fb284da9b8147f3e7c6dc6da": { + "balance": "0x2d627be45305080000" + }, + "8ff46045687723dc33e4d099a06904f1ebb584dc": { + "balance": "0x6c6b935b8bbd400000" + }, + "8ffa062122ac307418821adb9311075a3703bfa3": { + "balance": "0x3635c9adc5dea00000" + }, + "8ffe322997b8e404422d19c54aadb18f5bc8e9b7": { + "balance": "0xd5967be4fc3f100000" + }, + "900194c4b1074305d19de405b0ac78280ecaf967": { + "balance": "0x3635c9adc5dea00000" + }, + "9003d270891ba2df643da8341583193545e3e000": { + "balance": "0xd8d726b7177a800000" + }, + "90057af9aa66307ec9f033b29724d3b2f41eb6f9": { + "balance": "0x19d1d6aadb2c52e80000" + }, + "900f0b8e35b668f81ef252b13855aa5007d012e7": { + "balance": "0x170a0f5040e5040000" + }, + "9018cc1f48d2308e252ab6089fb99a7c1d569410": { + "balance": "0xad78ebc5ac6200000" + }, + "901d99b699e5c6911519cb2076b4c76330c54d22": { + "balance": "0x6c6b935b8bbd400000" + }, + "902d74a157f7d2b9a3378b1f56703730e03a1719": { + "balance": "0xd8d726b7177a800000" + }, + "903413878aea3bc1086309a3fe768b65559e8cab": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "904966cc2213b5b8cb5bd6089ef9cddbef7edfcc": { + "balance": "0x6c6b935b8bbd400000" + }, + "904caa429c619d940f8e6741826a0db692b19728": { + "balance": "0x3635c9adc5dea00000" + }, + "9052f2e4a3e3c12dd1c71bf78a4ec3043dc88b7e": { + "balance": "0xe7eeba3410b740000" + }, + "905526568ac123afc0e84aa715124febe83dc87c": { + "balance": "0xf8699329677e0000" + }, + "9092918707c621fdbd1d90fb80eb787fd26f7350": { + "balance": "0x855b5ba65c84f00000" + }, + "909b5e763a39dcc795223d73a1dbb7d94ca75ac8": { + "balance": "0x6c6b935b8bbd400000" + }, + "90acced7e48c08c6b934646dfa0adf29dc94074f": { + "balance": "0x30b4b157bbd490000" + }, + "90b1f370f9c1eb0be0fb8e2b8ad96a416371dd8a": { + "balance": "0x30ca024f987b900000" + }, + "90b62f131a5f29b45571513ee7a74a8f0b232202": { + "balance": "0x890b0c2e14fb80000" + }, + "90bd62a050845261fa4a9f7cf241ea630b05efb8": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "90c41eba008e20cbe927f346603fc88698125969": { + "balance": "0x246ddf97976680000" + }, + "90d2809ae1d1ffd8f63eda01de49dd552df3d1bc": { + "balance": "0xd8bb6549b02bb80000" + }, + "90dc09f717fc2a5b69fd60ba08ebf40bf4e8246c": { + "balance": "0xd8d8583fa2d52f0000" + }, + "90e300ac71451e401f887f6e7728851647a80e07": { + "balance": "0x15af1d78b58c400000" + }, + "90e35aabb2deef408bb9b5acef714457dfde6272": { + "balance": "0x56cd55fc64dfe0000" + }, + "90e7070f4d033fe6910c9efe5a278e1fc6234def": { + "balance": "0x571380819b3040000" + }, + "90e93e4dc17121487952333614002be42356498e": { + "balance": "0x678a932062e4180000" + }, + "90e9a9a82edaa814c284d232b6e9ba90701d4952": { + "balance": "0x56be03ca3e47d8000" + }, + "90f774c9147dde90853ddc43f08f16d455178b8c": { + "balance": "0xd8d726b7177a800000" + }, + "90fc537b210658660a83baa9ac4a8402f65746a8": { + "balance": "0x65ea3db75546600000" + }, + "91050a5cffadedb4bb6eaafbc9e5013428e96c80": { + "balance": "0x5c283d410394100000" + }, + "91051764af6b808e4212c77e30a5572eaa317070": { + "balance": "0x3635c9adc5dea00000" + }, + "910b7d577a7e39aa23acf62ad7f1ef342934b968": { + "balance": "0x21e19e0c9bab2400000" + }, + "910e996543344c6815fb97cda7af4b8698765a5b": { + "balance": "0x59af69829cf640000" + }, + "911feea61fe0ed50c5b9e5a0d66071399d28bdc6": { + "balance": "0x340aad21b3b700000" + }, + "911ff233e1a211c0172c92b46cf997030582c83a": { + "balance": "0x6acb3df27e1f880000" + }, + "9120e71173e1ba19ba8f9f4fdbdcaa34e1d6bb78": { + "balance": "0x6c6b935b8bbd400000" + }, + "91211712719f2b084d3b3875a85069f466363141": { + "balance": "0x3635c9adc5dea00000" + }, + "912304118b80473d9e9fe3ee458fbe610ffda2bb": { + "balance": "0xad78ebc5ac6200000" + }, + "91546b79ecf69f936b5a561508b0d7e50cc5992f": { + "balance": "0xe7eeba3410b740000" + }, + "9156d18029350e470408f15f1aa3be9f040a67c6": { + "balance": "0x3635c9adc5dea00000" + }, + "91620f3eb304e813d28b0297556d65dc4e5de5aa": { + "balance": "0xcf152640c5c8300000" + }, + "916bf7e3c545921d3206d900c24f14127cbd5e70": { + "balance": "0x3d0ddbc7df2bb100000" + }, + "916cf17d71412805f4afc3444a0b8dd1d9339d16": { + "balance": "0xc673ce3c40160000" + }, + "917b8f9f3a8d09e9202c52c29e724196b897d35e": { + "balance": "0x8ba52e6fc45e40000" + }, + "918967918cd897dd0005e36dc6c883ef438fc8c7": { + "balance": "0x796e3ea3f8ab00000" + }, + "91898eab8c05c0222883cd4db23b7795e1a24ad7": { + "balance": "0x6c6b935b8bbd400000" + }, + "9191f94698210516cf6321a142070e20597674ed": { + "balance": "0xee9d5be6fc110000" + }, + "91a4149a2c7b1b3a67ea28aff34725e0bf8d7524": { + "balance": "0x692ae8897081d00000" + }, + "91a787bc5196f34857fe0c372f4df376aaa76613": { + "balance": "0x6c6b935b8bbd400000" + }, + "91a8baaed012ea2e63803b593d0d0c2aab4c5b0a": { + "balance": "0x5150ae84a8cdf00000" + }, + "91ac5cfe67c54aa7ebfba448666c461a3b1fe2e1": { + "balance": "0x15c93492bf9dfc0000" + }, + "91bb3f79022bf3c453f4ff256e269b15cf2c9cbd": { + "balance": "0x52585c13fe3a5c0000" + }, + "91c75e3cb4aa89f34619a164e2a47898f5674d9c": { + "balance": "0x6c6b935b8bbd400000" + }, + "91c80caa081b38351d2a0e0e00f80a34e56474c1": { + "balance": "0x3635c9adc5dea00000" + }, + "91cc46aa379f856a6640dccd5a648a7902f849d9": { + "balance": "0xad78ebc5ac6200000" + }, + "91d2a9ee1a6db20f5317cca7fbe2313895db8ef8": { + "balance": "0x1ccc3a52f306e280000" + }, + "91d66ea6288faa4b3d606c2aa45c7b6b8a252739": { + "balance": "0x6c6b935b8bbd400000" + }, + "91dbb6aaad149585be47375c5d6de5ff09191518": { + "balance": "0x43c33c1937564800000" + }, + "91e8810652e8e6161525d63bb7751dc20f676076": { + "balance": "0x274d656ac90e340000" + }, + "91f516146cda20281719978060c6be4149067c88": { + "balance": "0x6c6b935b8bbd400000" + }, + "91f624b24a1fa5a056fe571229e7379db14b9a1e": { + "balance": "0x28a8517c669b3570000" + }, + "91fe8a4c6164df8fa606995d6ba7adcaf1c893ce": { + "balance": "0x39992648a23c8a00000" + }, + "921f5261f4f612760706892625c75e7bce96b708": { + "balance": "0x6c6b935b8bbd400000" + }, + "9221c9ce01232665741096ac07235903ad1fe2fc": { + "balance": "0x6db63335522628000" + }, + "9225983860a1cb4623c72480ac16272b0c95e5f5": { + "balance": "0x6c6b935b8bbd400000" + }, + "9225d46a5a80943924a39e5b84b96da0ac450581": { + "balance": "0x878678326eac9000000" + }, + "922a20c79a1d3a26dd3829677bf1d45c8f672bb6": { + "balance": "0xd8d726b7177a800000" + }, + "92438e5203b6346ff886d7c36288aacccc78ceca": { + "balance": "0x3635c9adc5dea00000" + }, + "9243d7762d77287b12638688b9854e88a769b271": { + "balance": "0x3635c9adc5dea00000" + }, + "924bce7a853c970bb5ec7bb759baeb9c7410857b": { + "balance": "0xbe202d6a0eda0000" + }, + "924efa6db595b79313277e88319625076b580a10": { + "balance": "0x6c6b935b8bbd400000" + }, + "92558226b384626cad48e09d966bf1395ee7ea5d": { + "balance": "0x121ea68c114e510000" + }, + "926082cb7eed4b1993ad245a477267e1c33cd568": { + "balance": "0x144a74badfa4b60000" + }, + "926209b7fda54e8ddb9d9e4d3d19ebdc8e88c29f": { + "balance": "0x6c6b935b8bbd400000" + }, + "9268d62646563611dc3b832a30aa2394c64613e3": { + "balance": "0x6c6b935b8bbd400000" + }, + "92698e345378c62d8eda184d94366a144b0c105b": { + "balance": "0x4be4e7267b6ae00000" + }, + "92793ac5b37268774a7130de2bbd330405661773": { + "balance": "0x22ca3587cf4eb0000" + }, + "9279b2228cec8f7b4dda3f320e9a0466c2f585ca": { + "balance": "0x10f0cf064dd59200000" + }, + "927cb7dc187036b5427bc7e200c5ec450c1d27d4": { + "balance": "0xbb59a27953c600000" + }, + "927cc2bfda0e088d02eff70b38b08aa53cc30941": { + "balance": "0x646f60a1f986360000" + }, + "9284f96ddb47b5186ee558aa31324df5361c0f73": { + "balance": "0x3635c9adc5dea000000" + }, + "929d368eb46a2d1fbdc8ffa0607ede4ba88f59ad": { + "balance": "0x6c6b935b8bbd400000" + }, + "92a7c5a64362e9f842a23deca21035857f889800": { + "balance": "0x6c6acc67d7b1d40000" + }, + "92a898d46f19719c38126a8a3c27867ae2cee596": { + "balance": "0x6c6b935b8bbd400000" + }, + "92a971a739799f8cb48ea8475d72b2d2474172e6": { + "balance": "0xd5967be4fc3f100000" + }, + "92aae59768eddff83cfe60bb512e730a05a161d7": { + "balance": "0x5c9778410c76d18000" + }, + "92ad1b3d75fba67d54663da9fc848a8ade10fa67": { + "balance": "0x6c6b935b8bbd400000" + }, + "92ae5b7c7eb492ff1ffa16dd42ad9cad40b7f8dc": { + "balance": "0x2ee449550898e40000" + }, + "92c0f573eccf62c54810ee6ba8d1f113542b301b": { + "balance": "0xb7726f16ccb1e00000" + }, + "92c13fe0d6ce87fd50e03def9fa6400509bd7073": { + "balance": "0x22b1c8c1227a00000" + }, + "92c94c2820dfcf7156e6f13088ece7958b3676fd": { + "balance": "0x52d542804f1ce0000" + }, + "92cfd60188efdfb2f8c2e7b1698abb9526c1511f": { + "balance": "0x6c6b935b8bbd400000" + }, + "92d8ad9a4d61683b80d4a6672e84c20d62421e80": { + "balance": "0x1158e460913d00000" + }, + "92dca5e102b3b81b60f1a504634947c374a88ccb": { + "balance": "0x6c6b935b8bbd400000" + }, + "92e435340e9d253c00256389f52b067d55974e76": { + "balance": "0xe873f44133cb00000" + }, + "92e4392816e5f2ef5fb65837cec2c2325cc64922": { + "balance": "0x21e19e0c9bab2400000" + }, + "92e6581e1da1f9b846e09347333dc818e2d2ac66": { + "balance": "0xc55325ca7415e00000" + }, + "931df34d1225bcd4224e63680d5c4c09bce735a6": { + "balance": "0x3afb087b876900000" + }, + "931fe712f64207a2fd5022728843548bfb8cbb05": { + "balance": "0x6c6b935b8bbd400000" + }, + "93235f340d2863e18d2f4c52996516138d220267": { + "balance": "0x4002e44fda7d40000" + }, + "93258255b37c7f58f4b10673a932dd3afd90f4f2": { + "balance": "0x3635c9adc5dea00000" + }, + "9328d55ccb3fce531f199382339f0e576ee840a3": { + "balance": "0xd8d726b7177a800000" + }, + "9329ffdc268babde8874b366406c81445b9b2d35": { + "balance": "0x16e62f8c730ca18000" + }, + "932b9c04d40d2ac83083d94298169dae81ab2ed0": { + "balance": "0x6c6b935b8bbd400000" + }, + "933436c8472655f64c3afaaf7c4c621c83a62b38": { + "balance": "0x3635c9adc5dea00000" + }, + "933bf33f8299702b3a902642c33e0bfaea5c1ca3": { + "balance": "0xd2f13f7789f00000" + }, + "9340345ca6a3eabdb77363f2586043f29438ce0b": { + "balance": "0x1cc805da0dfff10000" + }, + "9340b5f678e45ee05eb708bb7abb6ec8f08f1b6b": { + "balance": "0x14542ba12a337c00000" + }, + "934af21b7ebfa467e2ced65aa34edd3a0ec71332": { + "balance": "0x7801f3e80cc0ff00000" + }, + "935069444a6a984de2084e46692ab99f671fc727": { + "balance": "0x1e7e4171bf4d3a00000" + }, + "93507e9e8119cbceda8ab087e7ecb071383d6981": { + "balance": "0x2f6f10780d22cc00000" + }, + "93678a3c57151aeb68efdc43ef4d36cb59a009f3": { + "balance": "0x1a12a92bc3c3e0000" + }, + "936dcf000194e3bff50ac5b4243a3ba014d661d8": { + "balance": "0x21e19e0c9bab2400000" + }, + "936f3813f5f6a13b8e4ffec83fe7f826186a71cd": { + "balance": "0x1c30731cec03200000" + }, + "9374869d4a9911ee1eaf558bc4c2b63ec63acfdd": { + "balance": "0x3635c9adc5dea00000" + }, + "937563d8a80fd5a537b0e66d20a02525d5d88660": { + "balance": "0x878678326eac900000" + }, + "9376dce2af2ec8dcda741b7e7345664681d93668": { + "balance": "0x3635c9adc5dea00000" + }, + "93868ddb2a794d02ebda2fa4807c76e3609858dc": { + "balance": "0x6dee15fc7c24a78000" + }, + "939c4313d2280edf5e071bced846063f0a975d54": { + "balance": "0x1969368974c05b000000" + }, + "93a6b3ab423010f981a7489d4aad25e2625c5741": { + "balance": "0x44680fe6a1ede4e8000" + }, + "93aa8f92ebfff991fc055e906e651ac768d32bc8": { + "balance": "0x32f51edbaaa3300000" + }, + "93b4bf3fdff6de3f4e56ba6d7799dc4b93a6548f": { + "balance": "0x10910d4cdc9f60000" + }, + "93bc7d9a4abd44c8bbb8fe8ba804c61ad8d6576c": { + "balance": "0xd8d6119a8146050000" + }, + "93c2e64e5de5589ed25006e843196ee9b1cf0b3e": { + "balance": "0x5a87e7d7f5f6580000" + }, + "93c88e2d88621e30f58a9586bed4098999eb67dd": { + "balance": "0x69b5afac750bb800000" + }, + "93e0f37ecdfb0086e3e862a97034447b1e4dec1a": { + "balance": "0x1a055690d9db80000" + }, + "93e303411afaf6c107a44101c9ac5b36e9d6538b": { + "balance": "0xdf9ddfecd0365400000" + }, + "93f18cd2526040761488c513174d1e7963768b2c": { + "balance": "0x82ffac9ad593720000" + }, + "940f715140509ffabf974546fab39022a41952d2": { + "balance": "0x4be4e7267b6ae00000" + }, + "942c6b8c955bc0d88812678a236725b32739d947": { + "balance": "0x54069233bf7f780000" + }, + "943d37864a4a537d35c8d99723cd6406ce2562e6": { + "balance": "0x6c6b935b8bbd400000" + }, + "94439ca9cc169a79d4a09cae5e67764a6f871a21": { + "balance": "0xd02ab486cedc00000" + }, + "94449c01b32a7fa55af8104f42cdd844aa8cbc40": { + "balance": "0x38111a1f4f03c100000" + }, + "9445ba5c30e98961b8602461d0385d40fbd80311": { + "balance": "0x21e19e0c9bab2400000" + }, + "944f07b96f90c5f0d7c0c580533149f3f585a078": { + "balance": "0x402f4cfee62e80000" + }, + "9454b3a8bff9709fd0e190877e6cb6c89974dbd6": { + "balance": "0x90f534608a72880000" + }, + "945d96ea573e8df7262bbfa572229b4b16016b0f": { + "balance": "0xb589ef914c1420000" + }, + "945e18769d7ee727c7013f92de24d117967ff317": { + "balance": "0x6c6b935b8bbd400000" + }, + "94612781033b57b146ee74e753c672017f5385e4": { + "balance": "0xc328093e61ee400000" + }, + "94644ad116a41ce2ca7fbec609bdef738a2ac7c7": { + "balance": "0x10f0cf064dd59200000" + }, + "9470cc36594586821821c5c996b6edc83b6d5a32": { + "balance": "0x14d1120d7b1600000" + }, + "9475c510ec9a26979247744c3d8c3b0e0b5f44d3": { + "balance": "0x21e19e0c9bab2400000" + }, + "947e11e5ea290d6fc3b38048979e0cd44ec7c17f": { + "balance": "0x6c6b935b8bbd400000" + }, + "9483d98f14a33fdc118d403955c29935edfc5f70": { + "balance": "0x18ea3b34ef51880000" + }, + "949131f28943925cfc97d41e0cea0b262973a730": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "949f84f0b1d7c4a7cf49ee7f8b2c4a134de32878": { + "balance": "0x252248deb6e6940000" + }, + "949f8c107bc7f0aceaa0f17052aadbd2f9732b2e": { + "balance": "0x6c6b935b8bbd400000" + }, + "94a7cda8f481f9d89d42c303ae1632b3b709db1d": { + "balance": "0x1043561a8829300000" + }, + "94a9a71691317c2064271b51c9353fbded3501a8": { + "balance": "0xb50fcfafebecb00000" + }, + "94ad4bad824bd0eb9ea49c58cebcc0ff5e08346b": { + "balance": "0x692ae8897081d00000" + }, + "94bbc67d13f89ebca594be94bc5170920c30d9f3": { + "balance": "0x458ffa3150a540000" + }, + "94be3ae54f62d663b0d4cc9e1ea8fe9556ea9ebf": { + "balance": "0x143132ca843180000" + }, + "94c055e858357aaa30cf2041fa9059ce164a1f91": { + "balance": "0x43c25e0dcc1bd1c0000" + }, + "94c742fd7a8b7906b3bfe4f8904fc0be5c768033": { + "balance": "0x43c33c1937564800000" + }, + "94ca56de777fd453177f5e0694c478e66aff8a84": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "94d81074db5ae197d2bb1373ab80a87d121c4bd3": { + "balance": "0x1fd933494aa5fe00000" + }, + "94db807873860aac3d5aea1e885e52bff2869954": { + "balance": "0xae8e7a0bb575d00000" + }, + "94e1f5cb9b8abace03a1a6428256553b690c2355": { + "balance": "0x1158e460913d00000" + }, + "94ef8be45077c7d4c5652740de946a62624f713f": { + "balance": "0x56cf5593a18f88000" + }, + "94f13f9f0836a3ee2437a84922d2984dc0f7d53b": { + "balance": "0xa2a0329bc38abe0000" + }, + "94f8f057db7e60e675ad940f155885d1a477348e": { + "balance": "0x15be6174e1912e0000" + }, + "94fcceadfe5c109c5eaeaf462d43873142c88e22": { + "balance": "0x1043561a88293000000" + }, + "95034e1621865137cd4739b346dc17da3a27c34e": { + "balance": "0x55a6e79ccd1d300000" + }, + "950c68a40988154d2393fff8da7ccda99614f72c": { + "balance": "0xf94146fd8dcde58000" + }, + "950fe9c6cad50c18f11a9ed9c45740a6180612d0": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "952183cfd38e352e579d36decec5b18450f7fba0": { + "balance": "0x6c6b935b8bbd400000" + }, + "95278b08dee7c0f2c8c0f722f9fcbbb9a5241fda": { + "balance": "0x829309f64f0db00000" + }, + "952c57d2fb195107d4cd5ca300774119dfad2f78": { + "balance": "0x6c6b935b8bbd400000" + }, + "953572f0ea6df9b197cae40e4b8ecc056c4371c5": { + "balance": "0x3635c9adc5dea00000" + }, + "953ef652e7b769f53d6e786a58952fa93ee6abe7": { + "balance": "0x9b0a791f1211300000" + }, + "95447046313b2f3a5e19b948fd3b8bedc82c717c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "955db3b74360b9a268677e73cea821668af6face": { + "balance": "0x65a4da25d3016c00000" + }, + "9560e8ac6718a6a1cdcff189d603c9063e413da6": { + "balance": "0xd8d726b7177a800000" + }, + "9567a0de811de6ff095b7ee64e7f1b83c2615b80": { + "balance": "0xe7eeba3410b740000" + }, + "95681cdae69b2049ce101e325c759892cac3f811": { + "balance": "0x9ae92a9bc94c400000" + }, + "9568b7de755628af359a84543de23504e15e41e6": { + "balance": "0x878678326eac9000000" + }, + "9569c63a9284a805626db3a32e9d236393476151": { + "balance": "0x6acb3df27e1f880000" + }, + "95809e8da3fbe4b7f281f0b8b1715f420f7d7d63": { + "balance": "0x6c6b935b8bbd400000" + }, + "959f57fded6ae37913d900b81e5f48a79322c627": { + "balance": "0xddb26104749118000" + }, + "959ff17f1d51b473b44010052755a7fa8c75bd54": { + "balance": "0x6acb3df27e1f880000" + }, + "95a577dc2eb3ae6cb9dfc77af697d7efdfe89a01": { + "balance": "0x75f610f70ed200000" + }, + "95cb6d8a6379f94aba8b885669562c4d448e56a7": { + "balance": "0x6c6b935b8bbd400000" + }, + "95d550427b5a514c751d73a0f6d29fb65d22ed10": { + "balance": "0x1043561a8829300000" + }, + "95d98d0c1069908f067a52acac2b8b534da37afd": { + "balance": "0x6f59b630a929708000" + }, + "95df4e3445d7662624c48eba74cf9e0a53e9f732": { + "balance": "0xbdbc41e0348b3000000" + }, + "95e6a54b2d5f67a24a4875af75107ca7ea9fd2fa": { + "balance": "0x487a9a304539440000" + }, + "95e6f93dac228bc7585a25735ac2d076cc3a4017": { + "balance": "0x14542ba12a337c00000" + }, + "95e7616424cd0961a71727247437f0069272280e": { + "balance": "0x15af1d78b58c400000" + }, + "95e80a82c20cbe3d2060242cb92d735810d034a2": { + "balance": "0x1c32e463fd4b98000" + }, + "95f62d0243ede61dad9a3165f53905270d54e242": { + "balance": "0x57473d05dabae80000" + }, + "95fb5afb14c1ef9ab7d179c5c300503fd66a5ee2": { + "balance": "0x1daf7a02b0dbe8000" + }, + "9610592202c282ab9bd8a884518b3e0bd4758137": { + "balance": "0xe873f44133cb00000" + }, + "961c59adc74505d1864d1ecfcb8afa0412593c93": { + "balance": "0x878678326eac9000000" + }, + "962c0dec8a3d464bf39b1215eafd26480ae490cd": { + "balance": "0x6c82e3eaa513e80000" + }, + "962cd22a8edf1e4f4e55b4b15ddbfb5d9d541971": { + "balance": "0x6c6b935b8bbd400000" + }, + "96334bfe04fffa590213eab36514f338b864b736": { + "balance": "0x15af1d78b58c400000" + }, + "9637dc12723d9c78588542eab082664f3f038d9d": { + "balance": "0x3635c9adc5dea00000" + }, + "964eab4b276b4cd8983e15ca72b106900fe41fce": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "9662ee021926682b31c5f200ce457abea76c6ce9": { + "balance": "0x24590e8589eb6a0000" + }, + "966c04781cb5e67dde3235d7f8620e1ab663a9a5": { + "balance": "0x100d2050da6351600000" + }, + "967076a877b18ec15a415bb116f06ef32645dba3": { + "balance": "0x6c6b935b8bbd400000" + }, + "967bfaf76243cdb9403c67d2ceefdee90a3feb73": { + "balance": "0x349d87f2a2dc2f0000" + }, + "967d4142af770515dd7062af93498dbfdff29f20": { + "balance": "0x11854d0f9cee40000" + }, + "968b14648f018333687cd213fa640aec04ce6323": { + "balance": "0x3635c9adc5dea00000" + }, + "968dea60df3e09ae3c8d3505e9c080454be0e819": { + "balance": "0x14542ba12a337c00000" + }, + "96924191b7df655b3319dc6d6137f481a73a0ff3": { + "balance": "0xd9ecb4fd208e500000" + }, + "9696052138338c722f1140815cf7749d0d3b3a74": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "96a55f00dff405dc4de5e58c57f6f6f0cac55d2f": { + "balance": "0x6a6616379c87b58000" + }, + "96aa573fed2f233410dbae5180145b23c31a02f0": { + "balance": "0x5dc892aa1131c80000" + }, + "96ad579bbfa8db8ebec9d286a72e4661eed8e356": { + "balance": "0x3a0ba42bec61830000" + }, + "96b434fe0657e42acc8212b6865139dede15979c": { + "balance": "0xd8d726b7177a800000" + }, + "96b906ea729f4655afe3e57d35277c967dfa1577": { + "balance": "0x3635c9adc5dea00000" + }, + "96d62dfd46087f62409d93dd606188e70e381257": { + "balance": "0x6c6b935b8bbd400000" + }, + "96d9cca8f55eea0040ec6eb348a1774b95d93ef4": { + "balance": "0xd8d726b7177a800000" + }, + "96e7c0c9d5bf10821bf140c558a145b7cac21397": { + "balance": "0x393ef1a5127c800000" + }, + "96ea6ac89a2bac95347b51dba63d8bd5ebdedce1": { + "balance": "0x6c6b935b8bbd400000" + }, + "96eafbf2fb6f4db9a436a74c45b5654452e23819": { + "balance": "0x1158e460913d00000" + }, + "96eb523e832f500a017de13ec27f5d366c560eff": { + "balance": "0x10acceba43ee280000" + }, + "96f0462ae6f8b96088f7e9c68c74b9d8ad34b347": { + "balance": "0x61093d7c2c6d380000" + }, + "96f820500b70f4a3e3239d619cff8f222075b135": { + "balance": "0xad78ebc5ac6200000" + }, + "96fe59c3dbb3aa7cc8cb62480c65e56e6204a7e2": { + "balance": "0x43c33c1937564800000" + }, + "96ff6f509968f36cb42cba48db32f21f5676abf8": { + "balance": "0x6acb3df27e1f880000" + }, + "970938522afb5e8f994873c9fbdc26e3b37e314c": { + "balance": "0x3635c9adc5dea00000" + }, + "970abd53a54fca4a6429207c182d4d57bb39d4a0": { + "balance": "0x6c6b935b8bbd400000" + }, + "970d8b8a0016d143054f149fb3b8e550dc0797c7": { + "balance": "0x3635c9adc5dea00000" + }, + "972c2f96aa00cf8a2f205abcf8937c0c75f5d8d9": { + "balance": "0xad78ebc5ac6200000" + }, + "973f4e361fe5decd989d4c8f7d7cc97990385daf": { + "balance": "0x150f8543a387420000" + }, + "974d0541ab4a47ec7f75369c0069b64a1b817710": { + "balance": "0x15af1d78b58c400000" + }, + "974d2f17895f2902049deaaecf09c3046507402d": { + "balance": "0xcc19c29437ab8000" + }, + "9752d14f5e1093f071711c1adbc4e3eb1e5c57f3": { + "balance": "0x6c6b935b8bbd400000" + }, + "9756e176c9ef693ee1eec6b9f8b151d313beb099": { + "balance": "0x410d586a20a4c00000" + }, + "975f3764e97bbccf767cbd3b795ba86d8ba9840e": { + "balance": "0x12c1b6eed03d280000" + }, + "976a18536af41874426308871bcd1512a775c9f8": { + "balance": "0x21e19e0c9bab2400000" + }, + "976e3ceaf3f1af51f8c29aff5d7fa21f0386d8ee": { + "balance": "0xd02ab486cedc00000" + }, + "9777cc61cf756be3b3c20cd4491c69d275e7a120": { + "balance": "0x21e19e0c9bab2400000" + }, + "97810bafc37e84306332aacb35e92ad911d23d24": { + "balance": "0x3635c9adc5dea00000" + }, + "978c430ce4359b06bc2cdf5c2985fc950e50d5c8": { + "balance": "0x1a055690d9db800000" + }, + "9795f64319fc17dd0f8261f9d206fb66b64cd0c9": { + "balance": "0xad78ebc5ac6200000" + }, + "9799ca21dbcf69bfa1b3f72bac51b9e3ca587cf9": { + "balance": "0x5c283d410394100000" + }, + "979cbf21dfec8ace3f1c196d82df962534df394f": { + "balance": "0x9991d478dd4d160000" + }, + "979d681c617da16f21bcaca101ed16ed015ab696": { + "balance": "0x65ea3db75546600000" + }, + "979f30158b574b999aab348107b9eed85b1ff8c1": { + "balance": "0x34957444b840e80000" + }, + "97a86f01ce3f7cfd4441330e1c9b19e1b10606ef": { + "balance": "0x6c6b935b8bbd400000" + }, + "97b91efe7350c2d57e7e406bab18f3617bcde14a": { + "balance": "0x21e1999bbd5d2be0000" + }, + "97d0d9725e3b70e675843173938ed371b62c7fac": { + "balance": "0x93739534d28680000" + }, + "97d9e46a7604d7b5a4ea4ee61a42b3d2350fc3ed": { + "balance": "0x6c6b935b8bbd400000" + }, + "97dc26ec670a31e0221d2a75bc5dc9f90c1f6fd4": { + "balance": "0x2b5e3af16b1880000" + }, + "97de21e421c37fe4b8025f9a51b7b390b5df7804": { + "balance": "0x10f0cf064dd592000000" + }, + "97e28973b860c567402800fbb63ce39a048a3d79": { + "balance": "0x542253a126ce40000" + }, + "97e5cc6127c4f885be02f44b42d1c8b0ac91e493": { + "balance": "0xad78ebc5ac6200000" + }, + "97f1fe4c8083e596212a187728dd5cf80a31bec5": { + "balance": "0x1158e460913d00000" + }, + "97f7760657c1e202759086963eb4211c5f8139b9": { + "balance": "0xa8a097fcb3d17680000" + }, + "97f99b6ba31346cd98a9fe4c308f87c5a58c5151": { + "balance": "0x14542ba12a337c00000" + }, + "980a84b686fc31bdc83c221058546a71b11f838a": { + "balance": "0x2a415548af86818000" + }, + "9810e34a94db6ed156d0389a0e2b80f4fd6b0a8a": { + "balance": "0x6c6b935b8bbd400000" + }, + "981ddf0404e4d22dda556a0726f00b2d98ab9569": { + "balance": "0x36356633ebd8ea0000" + }, + "981f712775c0dad97518ffedcb47b9ad1d6c2762": { + "balance": "0x16a6502f15a1e540000" + }, + "9834682180b982d166badb9d9d1d9bbf016d87ee": { + "balance": "0x6c6b935b8bbd400000" + }, + "9836b4d30473641ab56aeee19242761d72725178": { + "balance": "0x6c6b935b8bbd400000" + }, + "98397342ec5f3d4cb877e54ef5d6f1d366731bd4": { + "balance": "0x14061b9d77a5e980000" + }, + "9846648836a307a057184fd51f628a5f8c12427c": { + "balance": "0x40b69bf43dce8f00000" + }, + "984a7985e3cc7eb5c93691f6f8cc7b8f245d01b2": { + "balance": "0x14542ba12a337c00000" + }, + "985d70d207892bed398590024e2421b1cc119359": { + "balance": "0x43c33c1937564800000" + }, + "986df47e76e4d7a789cdee913cc9831650936c9d": { + "balance": "0x10f0cf064dd59200000" + }, + "9874803fe1f3a0365e7922b14270eaeb032cc1b5": { + "balance": "0x3cf5928824c6c20000" + }, + "9875623495a46cdbf259530ff838a1799ec38991": { + "balance": "0x6c6b935b8bbd400000" + }, + "987618c85656207c7bac1507c0ffefa2fb64b092": { + "balance": "0x37dfe433189e38000" + }, + "987c9bcd6e3f3990a52be3eda4710c27518f4f72": { + "balance": "0x15af1d78b58c400000" + }, + "9882967cee68d2a839fad8ab4a7c3dddf6c0adc8": { + "balance": "0x4878be1ffaf95d0000" + }, + "98855c7dfbee335344904a12c40c731795b13a54": { + "balance": "0x39fbae8d042dd00000" + }, + "989c0ccff654da03aeb11af701054561d6297e1d": { + "balance": "0xd8d726b7177a800000" + }, + "98a0e54c6d9dc8be96276cebf4fec460f6235d85": { + "balance": "0x6ac882100952c78000" + }, + "98b769cc305cecfb629a00c907069d7ef9bc3a12": { + "balance": "0x168d28e3f00280000" + }, + "98ba4e9ca72fddc20c69b4396f76f8183f7a2a4e": { + "balance": "0x2b5e3af16b188000000" + }, + "98be696d51e390ff1c501b8a0f6331b628ddc5ad": { + "balance": "0x6c6b935b8bbd400000" + }, + "98bed3a72eccfbafb923489293e429e703c7e25b": { + "balance": "0x6c6b935b8bbd400000" + }, + "98bf4af3810b842387db70c14d46099626003d10": { + "balance": "0xd8d726b7177a800000" + }, + "98c10ebf2c4f97cba5a1ab3f2aafe1cac423f8cb": { + "balance": "0x1043561a8829300000" + }, + "98c19dba810ba611e68f2f83ee16f6e7744f0c1f": { + "balance": "0xad78ebc5ac6200000" + }, + "98c5494a03ac91a768dffc0ea1dde0acbf889019": { + "balance": "0x2a5a058fc295ed000000" + }, + "98d204f9085f8c8e7de23e589b64c6eff692cc63": { + "balance": "0x6c6b935b8bbd400000" + }, + "98d3731992d1d40e1211c7f735f2189afa0702e0": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "98e2b6d606fd2d6991c9d6d4077fdf3fdd4585da": { + "balance": "0x30df1a6f8ad6280000" + }, + "98e3e90b28fccaee828779b8d40a5568c4116e21": { + "balance": "0x22b1c8c1227a00000" + }, + "98e6f547db88e75f1f9c8ac2c5cf1627ba580b3e": { + "balance": "0x3635c9adc5dea00000" + }, + "98f4af3af0aede5fafdc42a081ecc1f89e3ccf20": { + "balance": "0x1fd933494aa5fe00000" + }, + "98f6b8e6213dbc9a5581f4cce6655f95252bdb07": { + "balance": "0x115872b0bca4300000" + }, + "9909650dd5b1397b8b8b0eb69499b291b0ad1213": { + "balance": "0xad78ebc5ac6200000" + }, + "991173601947c2084a62d639527e961512579af9": { + "balance": "0x2086ac351052600000" + }, + "99129d5b3c0cde47ea0def4dfc070d1f4a599527": { + "balance": "0x6c6b935b8bbd400000" + }, + "9917d68d4af341d651e7f0075c6de6d7144e7409": { + "balance": "0x132d4476c08e6f00000" + }, + "991ac7ca7097115f26205eee0ef7d41eb4e311ae": { + "balance": "0x1158e460913d00000" + }, + "992365d764c5ce354039ddfc912e023a75b8e168": { + "balance": "0xfc936392801c0000" + }, + "992646ac1acaabf5ddaba8f9429aa6a94e7496a7": { + "balance": "0x3637507a30abeb0000" + }, + "99268327c373332e06c3f6164287d455b9d5fa4b": { + "balance": "0x6c6b935b8bbd400000" + }, + "9928ff715afc3a2b60f8eb4cc4ba4ee8dab6e59d": { + "balance": "0x17da3a04c7b3e00000" + }, + "9932ef1c85b75a9b2a80057d508734c51085becc": { + "balance": "0x2b83fa5301d590000" + }, + "993f146178605e66d517be782ef0b3c61a4e1925": { + "balance": "0x17c1f0535d7a5830000" + }, + "99413704b1a32e70f3bc0d69dd881c38566b54cb": { + "balance": "0x5cc6b694631f7120000" + }, + "994152fc95d5c1ca8b88113abbad4d710e40def6": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "9944fee9d34a4a880023c78932c00b59d5c82a82": { + "balance": "0x28a8a56b3690070000" + }, + "994cc2b5227ec3cf048512467c41b7b7b748909f": { + "balance": "0x6c6b935b8bbd400000" + }, + "9971df60f0ae66dce9e8c84e17149f09f9c52f64": { + "balance": "0xad78ebc5ac6200000" + }, + "9976947eff5f6ae5da08dd541192f378b428ff94": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "997d6592a31589acc31b9901fbeb3cc3d65b3215": { + "balance": "0x6c6b935b8bbd400000" + }, + "9982a5890ffb5406d3aca8d2bfc1dd70aaa80ae0": { + "balance": "0x6c6b935b8bbd400000" + }, + "99878f9d6e0a7ed9aec78297b73879a80195afe0": { + "balance": "0xd7c198710e66b00000" + }, + "998c1f93bcdb6ff23c10d0dc924728b73be2ff9f": { + "balance": "0x365bf3a433eaf30000" + }, + "9991614c5baa47dd6c96874645f97add2c3d8380": { + "balance": "0x6acb3df27e1f880000" + }, + "99924a9816bb7ddf3fec1844828e9ad7d06bf4e6": { + "balance": "0x5f68e8131ecf800000" + }, + "99997668f7c1a4ff9e31f9977ae3224bcb887a85": { + "balance": "0xfc936392801c00000" + }, + "999c49c174ca13bc836c1e0a92bff48b271543ca": { + "balance": "0xb1cf24ddd0b1400000" + }, + "99a4de19ded79008cfdcd45d014d2e584b8914a8": { + "balance": "0x5150ae84a8cdf00000" + }, + "99a96bf2242ea1b39ece6fcc0d18aed00c0179f3": { + "balance": "0x1043561a8829300000" + }, + "99b018932bcad355b6792b255db6702dec8ce5dd": { + "balance": "0xd8d8583fa2d52f0000" + }, + "99b743d1d9eff90d9a1934b4db21d519d89b4a38": { + "balance": "0x56bc75e2d63100000" + }, + "99b8c824869de9ed24f3bff6854cb6dd45cc3f9f": { + "balance": "0x65ea3db75546600000" + }, + "99c0174cf84e0783c220b4eb6ae18fe703854ad3": { + "balance": "0x7079a2573d0c780000" + }, + "99c1d9f40c6ab7f8a92fce2fdce47a54a586c53f": { + "balance": "0x35659ef93f0fc40000" + }, + "99c236141daec837ece04fdaee1d90cf8bbdc104": { + "balance": "0x766516acac0d200000" + }, + "99c31fe748583787cdd3e525b281b218961739e3": { + "balance": "0x3708baed3d68900000" + }, + "99c475bf02e8b9214ada5fad02fdfd15ba365c0c": { + "balance": "0x2009c5c8bf6fdc0000" + }, + "99c883258546cc7e4e971f522e389918da5ea63a": { + "balance": "0xd8d726b7177a800000" + }, + "99c9f93e45fe3c1418c353e4c5ac3894eef8121e": { + "balance": "0x585baf145050b0000" + }, + "99d1579cd42682b7644e1d4f7128441eeffe339d": { + "balance": "0x43c33c1937564800000" + }, + "99d1b585965f406a42a49a1ca70f769e765a3f98": { + "balance": "0x3894f0e6f9b9f700000" + }, + "99dfd0504c06c743e46534fd7b55f1f9c7ec3329": { + "balance": "0x6c6b935b8bbd400000" + }, + "99f4147ccc6bcb80cc842e69f6d00e30fa4133d9": { + "balance": "0x15af1d78b58c400000" + }, + "99f77f998b20e0bcdcd9fc838641526cf25918ef": { + "balance": "0x61093d7c2c6d380000" + }, + "99fad50038d0d9d4c3fbb4bce05606ecadcd5121": { + "balance": "0x6c6b935b8bbd400000" + }, + "99fe0d201228a753145655d428eb9fd94985d36d": { + "balance": "0x6920bff3515a3a0000" + }, + "9a079c92a629ca15c8cafa2eb28d5bc17af82811": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "9a0d3cee3d9892ea3b3700a27ff84140d9025493": { + "balance": "0x340aad21b3b700000" + }, + "9a24ce8d485cc4c86e49deb39022f92c7430e67e": { + "balance": "0x46791fc84e07d00000" + }, + "9a2ce43b5d89d6936b8e8c354791b8afff962425": { + "balance": "0x6c6b935b8bbd400000" + }, + "9a390162535e398877e416787d6239e0754e937c": { + "balance": "0x3635c9adc5dea00000" + }, + "9a3da65023a13020d22145cfc18bab10bd19ce4e": { + "balance": "0x18bf6ea3464a3a0000" + }, + "9a3e2b1bf346dd070b027357feac44a4b2c97db8": { + "balance": "0x21e19e0c9bab2400000" + }, + "9a4ca8b82117894e43db72b9fa78f0b9b93ace09": { + "balance": "0x2b5e3af16b1880000" + }, + "9a522e52c195bfb7cf5ffaaedb91a3ba7468161d": { + "balance": "0x3635c9adc5dea00000" + }, + "9a5af31c7e06339ac8b4628d7c4db0ce0f45c8a4": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "9a633fcd112cceeb765fe0418170732a9705e79c": { + "balance": "0xfc936392801c0000" + }, + "9a63d185a79129fdab19b58bb631ea36a420544e": { + "balance": "0x246ddf97976680000" + }, + "9a6708ddb8903c289f83fe889c1edcd61f854423": { + "balance": "0x3635c9adc5dea00000" + }, + "9a6ff5f6a7af7b7ae0ed9c20ecec5023d281b786": { + "balance": "0x8a12b9bd6a67ec0000" + }, + "9a82826d3c29481dcc2bd2950047e8b60486c338": { + "balance": "0x43c33c1937564800000" + }, + "9a8eca4189ff4aa8ff7ed4b6b7039f0902219b15": { + "balance": "0x1158e460913d00000" + }, + "9a953b5bcc709379fcb559d7b916afdaa50cadcc": { + "balance": "0x56bc75e2d63100000" + }, + "9a990b8aeb588d7ee7ec2ed8c2e64f7382a9fee2": { + "balance": "0x1d127db69fd8b0000" + }, + "9a9d1dc0baa77d6e20c3d849c78862dd1c054c87": { + "balance": "0x2fb474098f67c00000" + }, + "9aa48c66e4fb4ad099934e32022e827427f277ba": { + "balance": "0x21e19e0c9bab2400000" + }, + "9aa8308f42910e5ade09c1a5e282d6d91710bdbf": { + "balance": "0xad78ebc5ac6200000" + }, + "9aaafa0067647ed999066b7a4ca5b4b3f3feaa6f": { + "balance": "0x3635c9adc5dea00000" + }, + "9ab988b505cfee1dbe9cd18e9b5473b9a2d4f536": { + "balance": "0x1158e460913d000000" + }, + "9ab98d6dbb1eaae16d45a04568541ad3d8fe06cc": { + "balance": "0xec50464fe23f38000" + }, + "9aba2b5e27ff78baaab5cdc988b7be855cebbdce": { + "balance": "0x21e0c0013070adc0000" + }, + "9ac4da51d27822d1e208c96ea64a1e5b55299723": { + "balance": "0x56c5579f722140000" + }, + "9ac85397792a69d78f286b86432a07aeceb60e64": { + "balance": "0xc673ce3c40160000" + }, + "9ac907ee85e6f3e223459992e256a43fa08fa8b2": { + "balance": "0x21e19e0c9bab2400000" + }, + "9ad47fdcf9cd942d28effd5b84115b31a658a13e": { + "balance": "0xb259ec00d53b280000" + }, + "9adbd3bc7b0afc05d1d2eda49ff863939c48db46": { + "balance": "0xad6eedd17cf3b8000" + }, + "9adf458bff3599eee1a26398853c575bc38c6313": { + "balance": "0xf2dc7d47f15600000" + }, + "9ae13bd882f2576575921a94974cbea861ba0d35": { + "balance": "0xab4dcf399a3a600000" + }, + "9ae9476bfecd3591964dd325cf8c2a24faed82c1": { + "balance": "0xd8d726b7177a800000" + }, + "9af100cc3dae83a33402051ce4496b16615483f6": { + "balance": "0x6c6b935b8bbd400000" + }, + "9af11399511c213181bfda3a8b264c05fc81b3ce": { + "balance": "0x2f6f10780d22cc00000" + }, + "9af5c9894c33e42c2c518e3ac670ea9505d1b53e": { + "balance": "0xfc936392801c0000" + }, + "9af9dbe47422d177f945bdead7e6d82930356230": { + "balance": "0xd5967be4fc3f100000" + }, + "9afa536b4c66bc38d875c4b30099d9261fdb38eb": { + "balance": "0xb2a8f842a77bc8000" + }, + "9b06ad841dffbe4ccf46f1039fc386f3c321446e": { + "balance": "0x6c6b935b8bbd400000" + }, + "9b1168de8ab64b47552f3389800a9cc08b4666cf": { + "balance": "0x5dc892aa1131c80000" + }, + "9b1811c3051f46e664ae4bc9c824d18592c4574a": { + "balance": "0xad6eedd17cf3b8000" + }, + "9b18478655a4851cc906e660feac61f7f4c8bffc": { + "balance": "0xe2478d38907d840000" + }, + "9b22a80d5c7b3374a05b446081f97d0a34079e7f": { + "balance": "0xa2a15d09519be00000" + }, + "9b2be7f56754f505e3441a10f7f0e20fd3ddf849": { + "balance": "0x126e72a69a50d00000" + }, + "9b32cf4f5115f4b34a00a64c617de06387354323": { + "balance": "0x5b81ed888207c8000" + }, + "9b43dcb95fde318075a567f1e6b57617055ef9e8": { + "balance": "0xd5967be4fc3f100000" + }, + "9b444fd337e5d75293adcfff70e1ea01db023222": { + "balance": "0x56bc75e2d63100000" + }, + "9b4824ff9fb2abda554dee4fb8cf549165570631": { + "balance": "0x1158e460913d00000" + }, + "9b4c2715780ca4e99e60ebf219f1590c8cad500a": { + "balance": "0x56bc75e2d631000000" + }, + "9b59eb213b1e7565e45047e04ea0374f10762d16": { + "balance": "0x6c6b935b8bbd400000" + }, + "9b5c39f7e0ac168c8ed0ed340477117d1b682ee9": { + "balance": "0x55005f0c614480000" + }, + "9b5ec18e8313887df461d2902e81e67a8f113bb1": { + "balance": "0x56bc75e2d63100000" + }, + "9b64d3cd8d2b73f66841b5c46bb695b88a9ab75d": { + "balance": "0x1203a4f760c168000" + }, + "9b658fb361e046d4fcaa8aef6d02a99111223625": { + "balance": "0x6c6b935b8bbd400000" + }, + "9b6641b13e172fc072ca4b8327a3bc28a15b66a9": { + "balance": "0x68155a43676e00000" + }, + "9b68f67416a63bf4451a31164c92f672a68759e9": { + "balance": "0xcb49b44ba602d800000" + }, + "9b773669e87d76018c090f8255e54409b9dca8b2": { + "balance": "0x1158e460913d00000" + }, + "9b77ebced7e215f0920e8c2b870024f6ecb2ff31": { + "balance": "0x3635c9adc5dea00000" + }, + "9b7c8810cc7cc89e804e6d3e38121850472877fe": { + "balance": "0x6c6b935b8bbd400000" + }, + "9ba53dc8c95e9a472feba2c4e32c1dc4dd7bab46": { + "balance": "0x487a9a304539440000" + }, + "9bacd3d40f3b82ac91a264d9d88d908eac8664b9": { + "balance": "0x43c33c1937564800000" + }, + "9bb760d5c289a3e1db18db095345ca413b9a43c2": { + "balance": "0xaadec983fcff40000" + }, + "9bb76204186af2f63be79168601687fc9bad661f": { + "balance": "0x1043561a8829300000" + }, + "9bb9b02a26bfe1ccc3f0c6219e261c397fc5ca78": { + "balance": "0x487a9a304539440000" + }, + "9bc573bcda23b8b26f9073d90c230e8e71e0270b": { + "balance": "0x362f75a4305d0c0000" + }, + "9bd7c38a4210304a4d653edeff1b3ce45fce7843": { + "balance": "0xf498941e664280000" + }, + "9bd88068e13075f3a8cac464a5f949d6d818c0f6": { + "balance": "0x14542ba12a337c00000" + }, + "9bd905f1719fc7acd0159d4dc1f8db2f21472338": { + "balance": "0x3635c9adc5dea00000" + }, + "9bdbdc9b973431d13c89a3f9757e9b3b6275bfc7": { + "balance": "0x1b1a7dcf8a44d38000" + }, + "9be3c329b62a28b8b0886cbd8b99f8bc930ce3e6": { + "balance": "0x409e52b48369a0000" + }, + "9bf58efbea0784eb068adecfa0bb215084c73a35": { + "balance": "0x13a6b2b564871a00000" + }, + "9bf672d979b36652fc5282547a6a6bc212ae4368": { + "balance": "0x238fd42c5cf0400000" + }, + "9bf703b41c3624e15f4054962390bcba3052f0fd": { + "balance": "0x1483e01533c2e3c0000" + }, + "9bf71f7fb537ac54f4e514947fa7ff6728f16d2f": { + "balance": "0x1cf84a30a0a0c0000" + }, + "9bf9b3b2f23cf461eb591f28340bc719931c8364": { + "balance": "0x3635c9adc5dea00000" + }, + "9bfc659c9c601ea42a6b21b8f17084ec87d70212": { + "balance": "0x21e19e0c9bab2400000" + }, + "9bfff50db36a785555f07652a153b0c42b1b8b76": { + "balance": "0x6c6b935b8bbd400000" + }, + "9c05e9d0f0758e795303717e31da213ca157e686": { + "balance": "0x3635c9adc5dea00000" + }, + "9c1b771f09af882af0643083de2aa79dc097c40e": { + "balance": "0x8670e9ec6598c00000" + }, + "9c28a2c4086091cb5da226a657ce3248e8ea7b6f": { + "balance": "0xf2dc7d47f15600000" + }, + "9c2fd54089af665df5971d73b804616039647375": { + "balance": "0x3635c9adc5dea00000" + }, + "9c344098ba615a398f11d009905b177c44a7b602": { + "balance": "0x3635c9adc5dea00000" + }, + "9c3d0692ceeef80aa4965ceed262ffc7f069f2dc": { + "balance": "0xad78ebc5ac6200000" + }, + "9c405cf697956138065e11c5f7559e67245bd1a5": { + "balance": "0xad78ebc5ac6200000" + }, + "9c45202a25f6ad0011f115a5a72204f2f2198866": { + "balance": "0x10fcf3a62b080980000" + }, + "9c49deff47085fc09704caa2dca8c287a9a137da": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "9c4bbcd5f1644a6f075824ddfe85c571d6abf69c": { + "balance": "0x6194049f30f7200000" + }, + "9c526a140683edf1431cfaa128a935e2b614d88b": { + "balance": "0x6046f37e5945c0000" + }, + "9c54e4ed479a856829c6bb42da9f0b692a75f728": { + "balance": "0x197a8f6dd5519800000" + }, + "9c581a60b61028d934167929b22d70b313c34fd0": { + "balance": "0xa968163f0a57b400000" + }, + "9c5cc111092c122116f1a85f4ee31408741a7d2f": { + "balance": "0x1ab2cf7c9f87e20000" + }, + "9c6bc9a46b03ae5404f043dfcf21883e4110cc33": { + "balance": "0xad78ebc5ac6200000" + }, + "9c78963fbc263c09bd72e4f8def74a9475f7055c": { + "balance": "0x2eb8eb1a172dcb80000" + }, + "9c78fbb4df769ce2c156920cfedfda033a0e254a": { + "balance": "0x6acb3df27e1f880000" + }, + "9c7b6dc5190fe2912963fcd579683ec7395116b0": { + "balance": "0x2a1129d09367200000" + }, + "9c80bc18e9f8d4968b185da8c79fa6e11ffc3e23": { + "balance": "0xd02ab486cedc00000" + }, + "9c98fdf1fdcd8ba8f4c5b04c3ae8587efdf0f6e6": { + "balance": "0x14542ba12a337c00000" + }, + "9c99a1da91d5920bc14e0cb914fdf62b94cb8358": { + "balance": "0x43c33c1937564800000" + }, + "9c99b62606281b5cefabf36156c8fe62839ef5f3": { + "balance": "0xd8d726b7177a800000" + }, + "9c9a07a8e57c3172a919ef64789474490f0d9f51": { + "balance": "0x21e19e0c9bab2400000" + }, + "9c9de44724a4054da0eaa605abcc802668778bea": { + "balance": "0xad7d5ca3fa5a20000" + }, + "9c9f3b8a811b21f3ff3fe20fe970051ce66a824f": { + "balance": "0x3ec2debc07d4be0000" + }, + "9c9f89a3910f6a2ae8a91047a17ab788bddec170": { + "balance": "0x21e19e0c9bab2400000" + }, + "9ca0429f874f8dcee2e9c062a9020a842a587ab9": { + "balance": "0x6c6b935b8bbd400000" + }, + "9ca42ee7a0b898f6a5cc60b5a5d7b1bfa3c33231": { + "balance": "0x6c6b935b8bbd400000" + }, + "9cb28ac1a20a106f7f373692c5ce4c73f13732a1": { + "balance": "0x3635c9adc5dea00000" + }, + "9ccddcb2cfc2b25b08729a0a98d9e6f0202ea2c1": { + "balance": "0x56bc75e2d63100000" + }, + "9ce27f245e02d1c312c1d500788c9def7690453b": { + "balance": "0xad78ebc5ac6200000" + }, + "9ce5363b13e8238aa4dd15acd0b2e8afe0873247": { + "balance": "0xad78ebc5ac6200000" + }, + "9cf2928beef09a40f9bfc953be06a251116182fb": { + "balance": "0x14542ba12a337c00000" + }, + "9d069197d1de50045a186f5ec744ac40e8af91c6": { + "balance": "0x6c6b935b8bbd400000" + }, + "9d0e7d92fb305853d798263bf15e97c72bf9d7e0": { + "balance": "0x3635c9adc5dea00000" + }, + "9d0f347e826b7dceaad279060a35c0061ecf334b": { + "balance": "0xd8d726b7177a800000" + }, + "9d207517422cc0d60de7c237097a4d4fce20940c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "9d250ae4f110d71cafc7b0adb52e8d9acb6679b8": { + "balance": "0x2156d6e997213c00000" + }, + "9d2bfc36106f038250c01801685785b16c86c60d": { + "balance": "0x5077d75df1b675800000" + }, + "9d30cb237bc096f17036fc80dd21ca68992ca2d9": { + "balance": "0x66ee7318fdc8f300000" + }, + "9d32962ea99700d93228e9dbdad2cc37bb99f07e": { + "balance": "0xb4632bedd4ded40000" + }, + "9d34dac25bd15828faefaaf28f710753b39e89dc": { + "balance": "0x3b1c56fed02df00000" + }, + "9d369165fb70b81a3a765f188fd60cbe5e7b0968": { + "balance": "0x6c6b935b8bbd400000" + }, + "9d40e012f60425a340d82d03a1c757bfabc706fb": { + "balance": "0x9346f3addc88d8000" + }, + "9d4174aa6af28476e229dadb46180808c67505c1": { + "balance": "0x421afda42ed6970000" + }, + "9d4213339a01551861764c87a93ce8f85f87959a": { + "balance": "0xad78ebc5ac6200000" + }, + "9d460c1b379ddb19a8c85b4c6747050ddf17a875": { + "balance": "0xb50fcfafebecb00000" + }, + "9d47ba5b4c8505ad8da42934280b61a0e1e8b971": { + "balance": "0x56bc75e2d63100000" + }, + "9d4d321177256ebd9afbda304135d517c3dc5693": { + "balance": "0x2164b7a04ac8a00000" + }, + "9d4ff989b7bed9ab109d10c8c7e55f02d76734ad": { + "balance": "0x3635c9adc5dea00000" + }, + "9d511543b3d9dc60d47f09d49d01b6c498d82078": { + "balance": "0x26197b9516fc3940000" + }, + "9d6ecfa03af2c6e144b7c4692a86951e902e9e1f": { + "balance": "0xa2a5aa60ad243f0000" + }, + "9d7655e9f3e5ba5d6e87e412aebe9ee0d49247ee": { + "balance": "0x8e09311c1d80fa0000" + }, + "9d7831e834c20b1baa697af1d8e0c621c5afff9a": { + "balance": "0x4b06dbbb40f4a0000" + }, + "9d78a975b7db5e4d8e28845cfbe7e31401be0dd9": { + "balance": "0x48a43c54602f700000" + }, + "9d799e943e306ba2e5b99c8a6858cbb52c0cf735": { + "balance": "0x1043561a8829300000" + }, + "9d7fda7070bf3ee9bbd9a41f55cad4854ae6c22c": { + "balance": "0x255cba3c46fcf120000" + }, + "9d81aea69aed6ad07089d61445348c17f34bfc5b": { + "balance": "0x1043561a8829300000" + }, + "9d911f3682f32fe0792e9fb6ff3cfc47f589fca5": { + "balance": "0xd8d726b7177a800000" + }, + "9d913b5d339c95d87745562563fea98b23c60cc4": { + "balance": "0x941302c7f4d230000" + }, + "9d93fab6e22845f8f45a07496f11de71530debc7": { + "balance": "0x6c4fd1ee246e780000" + }, + "9d99b189bbd9a48fc2e16e8fcda33bb99a317bbb": { + "balance": "0x3d16e10b6d8bb20000" + }, + "9d9c4efe9f433989e23be94049215329fa55b4cb": { + "balance": "0xde3b28903c6b58000" + }, + "9d9e57fde30e5068c03e49848edce343b7028358": { + "balance": "0x5dc892aa1131c80000" + }, + "9da3302240af0511c6fd1857e6ddb7394f77ab6b": { + "balance": "0xa80d24677efef00000" + }, + "9da4ec407077f4b9707b2d9d2ede5ea5282bf1df": { + "balance": "0xd8d726b7177a800000" + }, + "9da609fa3a7e6cf2cc0e70cdabe78dc4e382e11e": { + "balance": "0x410d586a20a4c00000" + }, + "9da61ccd62bf860656e0325d7157e2f160d93bb5": { + "balance": "0x10f0ca956f8799e0000" + }, + "9da6e075989c7419094cc9f6d2e49393bb199688": { + "balance": "0x259bb71d5adf3f00000" + }, + "9da8e22ca10e67fea44e525e4751eeac36a31194": { + "balance": "0xe18398e7601900000" + }, + "9db2e15ca681f4c66048f6f9b7941ed08b1ff506": { + "balance": "0xd8d726b7177a800000" + }, + "9dc10fa38f9fb06810e11f60173ec3d2fd6a751e": { + "balance": "0x6acb3df27e1f880000" + }, + "9dd2196624a1ddf14a9d375e5f07152baf22afa2": { + "balance": "0x41b05e2463a5438000" + }, + "9dd46b1c6d3f05e29e9c6f037eed9a595af4a9aa": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "9ddd355e634ee9927e4b7f6c97e7bf3a2f1e687a": { + "balance": "0x2b5e3af16b1880000" + }, + "9de20ae76aa08263b205d5142461961e2408d266": { + "balance": "0xda933d8d8c6700000" + }, + "9de20bc37e7f48a80ffd7ad84ffbf1a1abe1738c": { + "balance": "0xad78ebc5ac6200000" + }, + "9de7386dde401ce4c67b71b6553f8aa34ea5a17d": { + "balance": "0x340aad21b3b700000" + }, + "9deb39027af877992b89f2ec4a1f822ecdf12693": { + "balance": "0x6c6b935b8bbd400000" + }, + "9defe56a0ff1a1947dba0923f7dd258d8f12fa45": { + "balance": "0x5b12aefafa804000000" + }, + "9df057cd03a4e27e8e032f857985fd7f01adc8d7": { + "balance": "0x6c6b935b8bbd400000" + }, + "9df32a501c0b781c0281022f42a1293ffd7b892a": { + "balance": "0x1e7e4171bf4d3a00000" + }, + "9e01765aff08bc220550aca5ea2e1ce8e5b09923": { + "balance": "0x3635c9adc5dea00000" + }, + "9e20e5fd361eabcf63891f5b87b09268b8eb3793": { + "balance": "0x56bc75e2d63100000" + }, + "9e232c08c14dc1a6ed0b8a3b2868977ba5c17d10": { + "balance": "0x1158e460913d00000" + }, + "9e23c5e4b782b00a5fadf1aead87dacf5b0367a1": { + "balance": "0x1158e460913d00000" + }, + "9e35399071a4a101e9194daa3f09f04a0b5f9870": { + "balance": "0xd8d726b7177a800000" + }, + "9e3eb509278fe0dcd8e0bbe78a194e06b6803943": { + "balance": "0x32f51edbaaa3300000" + }, + "9e427272516b3e67d4fcbf82f59390d04c8e28e5": { + "balance": "0xd8d726b7177a800000" + }, + "9e4cec353ac3e381835e3c0991f8faa5b7d0a8e6": { + "balance": "0x21e18b9e9ab45e48000" + }, + "9e5811b40be1e2a1e1d28c3b0774acde0a09603d": { + "balance": "0xa2a15d09519be00000" + }, + "9e5a311d9f69898a7c6a9d6360680438e67a7b2f": { + "balance": "0x50c5e761a444080000" + }, + "9e7c2050a227bbfd60937e268cea3e68fea8d1fe": { + "balance": "0x56bc75e2d63100000" + }, + "9e7f65a90e8508867bccc914256a1ea574cf07e3": { + "balance": "0x433874f632cc600000" + }, + "9e8144e08e89647811fe6b72d445d6a5f80ad244": { + "balance": "0x21e19e0c9bab2400000" + }, + "9e8f64ddcde9b8b451bafaa235a9bf511a25ac91": { + "balance": "0x90f534608a72880000" + }, + "9e951f6dc5e352afb8d04299d2478a451259bf56": { + "balance": "0x3e7419881a73a0000" + }, + "9e960dcd03d5ba99cb115d17ff4c09248ad4d0be": { + "balance": "0xad78ebc5ac6200000" + }, + "9eaf6a328a4076024efa6b67b48b21eedcc0f0b8": { + "balance": "0x890b0c2e14fb80000" + }, + "9eb1ff71798f28d6e989fa1ea0588e27ba86cb7d": { + "balance": "0x7a1fe160277000000" + }, + "9eb281c32719c40fdb3e216db0f37fbc73a026b7": { + "balance": "0x1158e460913d00000" + }, + "9eb3a7cb5e6726427a3a361cfa8d6164dbd0ba16": { + "balance": "0x2b95bdcc39b6100000" + }, + "9eb7834e171d41e069a77947fca87622f0ba4e48": { + "balance": "0x56bc75e2d63100000" + }, + "9ec03e02e587b7769def538413e97f7e55be71d8": { + "balance": "0x42bf06b78ed3b500000" + }, + "9ecbabb0b22782b3754429e1757aaba04b81189f": { + "balance": "0x2ca7bb061f5e998000" + }, + "9ece1400800936c7c6485fcdd3626017d09afbf6": { + "balance": "0x10ce1d3d8cb3180000" + }, + "9ed4e63f526542d44fddd34d59cd25388ffd6bda": { + "balance": "0xd29b34a46348940000" + }, + "9ed80eda7f55054db9fb5282451688f26bb374c1": { + "balance": "0x1043561a8829300000" + }, + "9edc90f4be210865214ab5b35e5a8dd77415279d": { + "balance": "0xd8d726b7177a800000" + }, + "9edeac4c026b93054dc5b1d6610c6f3960f2ad73": { + "balance": "0x410d586a20a4c00000" + }, + "9ee93f339e6726ec65eea44f8a4bfe10da3d3282": { + "balance": "0x6c6b935b8bbd400000" + }, + "9ee9760cc273d4706aa08375c3e46fa230aff3d5": { + "balance": "0x1e52e336cde22180000" + }, + "9eeb07bd2b7890195e7d46bdf2071b6617514ddb": { + "balance": "0x6c6b935b8bbd400000" + }, + "9eef442d291a447d74c5d253c49ef324eac1d8f0": { + "balance": "0xb96608c8103bf00000" + }, + "9ef1896b007c32a15114fb89d73dbd47f9122b69": { + "balance": "0xd8d726b7177a800000" + }, + "9f017706b830fb9c30efb0a09f506b9157457534": { + "balance": "0x6c6b935b8bbd400000" + }, + "9f10f2a0463b65ae30b070b3df18cf46f51e89bd": { + "balance": "0x678a932062e4180000" + }, + "9f19fac8a32437d80ac6837a0bb7841729f4972e": { + "balance": "0x233df3299f61720000" + }, + "9f1aa8fcfc89a1a5328cbd6344b71f278a2ca4a0": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "9f21302ca5096bea7402b91b0fd506254f999a3d": { + "balance": "0x4397451a003dd80000" + }, + "9f271d285500d73846b18f733e25dd8b4f5d4a8b": { + "balance": "0x2723c346ae18080000" + }, + "9f3497f5ef5fe63095836c004eb9ce02e9013b4b": { + "balance": "0x2256861bf9cf080000" + }, + "9f3a74fd5e7edcc1162993171381cbb632b7cff0": { + "balance": "0x21e19e0c9bab2400000" + }, + "9f46e7c1e9078cae86305ac7060b01467d6685ee": { + "balance": "0x243d4d18229ca20000" + }, + "9f496cb2069563144d0811677ba0e4713a0a4143": { + "balance": "0x3cd2e0bf63a4480000" + }, + "9f4a7195ac7c151ca258cafda0cab083e049c602": { + "balance": "0x53538c32185cee0000" + }, + "9f4ac9c9e7e24cb2444a0454fa5b9ad9d92d3853": { + "balance": "0x2d43f3ebfafb2c0000" + }, + "9f5f44026b576a4adb41e95961561d41039ca391": { + "balance": "0xd8d726b7177a80000" + }, + "9f607b3f12469f446121cebf3475356b71b4328c": { + "balance": "0xd8d726b7177a800000" + }, + "9f61beb46f5e853d0a8521c7446e68e34c7d0973": { + "balance": "0x1e5b8fa8fe2ac00000" + }, + "9f64a8e8dacf4ade30d10f4d59b0a3d5abfdbf74": { + "balance": "0x36369ed7747d260000" + }, + "9f662e95274121f177566e636d23964cf1fd686f": { + "balance": "0x6c6b935b8bbd400000" + }, + "9f6a322a6d469981426ae844865d7ee0bb15c7b3": { + "balance": "0x2b5ee57929fdb8000" + }, + "9f7986924aeb02687cd64189189fb167ded2dd5c": { + "balance": "0x35659ef93f0fc40000" + }, + "9f7a0392f857732e3004a375e6b1068d49d83031": { + "balance": "0x6c6b935b8bbd400000" + }, + "9f8245c3ab7d173164861cd3991b94f1ba40a93a": { + "balance": "0x9b0a791f1211300000" + }, + "9f83a293c324d4106c18faa8888f64d299054ca0": { + "balance": "0xad78ebc5ac6200000" + }, + "9f86a066edb61fcb5856de93b75c8c791864b97b": { + "balance": "0x6c6b935b8bbd400000" + }, + "9f98eb34d46979b0a6de8b05aa533a89b825dcf1": { + "balance": "0x4b06dbbb40f4a0000" + }, + "9f9fe0c95f10fee87af1af207236c8f3614ef02f": { + "balance": "0x14542ba12a337c00000" + }, + "9faea13c733412dc4b490402bfef27a0397a9bc3": { + "balance": "0x10ce1d3d8cb3180000" + }, + "9fbe066de57236dc830725d32a02aef9246c6c5e": { + "balance": "0x6c6b935b8bbd400000" + }, + "9fd1052a60506bd1a9ef003afd9d033c267d8e99": { + "balance": "0x3635c9adc5dea00000" + }, + "9fd64373f2fbcd9c0faca60547cad62e26d9851f": { + "balance": "0x3635c9adc5dea00000" + }, + "9fe501aa57ead79278937cd6308c5cfa7a5629fe": { + "balance": "0x2b5ee57929fdb8000" + }, + "9ffc5fe06f33f5a480b75aa94eb8556d997a16c0": { + "balance": "0x1158e460913d00000" + }, + "9ffcf5ef46d933a519d1d16c6ba3189b27496224": { + "balance": "0x3635c9adc5dea00000" + }, + "9ffedcc36b7cc312ad2a9ede431a514fccb49ba3": { + "balance": "0x244f579f3f5ca40000" + }, + "a006268446643ec5e81e7acb3f17f1c351ee2ed9": { + "balance": "0xd8d726b7177a800000" + }, + "a008019863c1a77c1499eb39bbd7bf2dd7a31cb9": { + "balance": "0x76d41c62494840000" + }, + "a009bf076f1ba3fa57d2a7217218bed5565a7a7a": { + "balance": "0x3635c9adc5dea00000" + }, + "a01e9476df84431825c836e8803a97e22fa5a0cd": { + "balance": "0x14542ba12a337c00000" + }, + "a01f12d70f44aa7b113b285c22dcdb45873454a7": { + "balance": "0xfc936392801c0000" + }, + "a01fd1906a908506dedae1e208128872b56ee792": { + "balance": "0xa2a15d09519be00000" + }, + "a0228240f99e1de9cb32d82c0f2fa9a3d44b0bf3": { + "balance": "0x56bc75e2d631000000" + }, + "a02bde6461686e19ac650c970d0672e76dcb4fc2": { + "balance": "0x1e09296c3378de40000" + }, + "a02c1e34064f0475f7fa831ccb25014c3aa31ca2": { + "balance": "0x340aad21b3b700000" + }, + "a02dc6aa328b880de99eac546823fccf774047fb": { + "balance": "0x6acb3df27e1f880000" + }, + "a02e3f8f5959a7aab7418612129b701ca1b80010": { + "balance": "0x1158e460913d00000" + }, + "a0347f0a98776390165c166d32963bf74dcd0a2f": { + "balance": "0x3635c9adc5dea00000" + }, + "a035a3652478f82dbd6d115faa8ca946ec9e681d": { + "balance": "0x5f4e42dd4afec0000" + }, + "a03a3dc7c533d1744295be955d61af3f52b51af5": { + "balance": "0x22b1c8c1227a00000" + }, + "a0459ef3693aacd1647cd5d8929839204cef53be": { + "balance": "0x3635c9adc5dea00000" + }, + "a04f2ae02add14c12faf65cb259022d0830a8e26": { + "balance": "0x152d02c7e14af6800000" + }, + "a06cd1f396396c0a64464651d7c205efaf387ca3": { + "balance": "0x6c6acc67d7b1d40000" + }, + "a072691c8dd7cd4237ff72a75c1a9506d0ce5b9e": { + "balance": "0x140ec80fa7ee880000" + }, + "a072cebe62a9e9f61cc3fbf88a9efbfe3e9a8d70": { + "balance": "0x15af1d78b58c400000" + }, + "a07682000b1bcf3002f85c80c0fa2949bd1e82fd": { + "balance": "0xd8d726b7177a800000" + }, + "a07aa16d74aee8a9a3288d52db1551d593883297": { + "balance": "0x2086ac351052600000" + }, + "a08d215b5b6aac4861a281ac7e400b78fef04cbf": { + "balance": "0x1158e460913d00000" + }, + "a0951970dfd0832fb83bda12c23545e79041756c": { + "balance": "0x2086ac351052600000" + }, + "a09f4d5eaa65a2f4cb750a49923401dae59090af": { + "balance": "0x796e3ea3f8ab00000" + }, + "a0a0e65204541fca9b2fb282cd95138fae16f809": { + "balance": "0x21e19e0c9bab2400000" + }, + "a0aa5f0201f04d3bbeb898132f7c11679466d901": { + "balance": "0x1fbed5215bb4c0000" + }, + "a0aadbd9509722705f6d2358a5c79f37970f00f6": { + "balance": "0xad78ebc5ac6200000" + }, + "a0b771951ce1deee363ae2b771b73e07c4b5e800": { + "balance": "0x4be4e7267b6ae00000" + }, + "a0de5c601e696635c698b7ae9ca4539fc7b941ec": { + "balance": "0x12c3cbd704c9770000" + }, + "a0e8ba661b48154cf843d4c2a5c0f792d528ee29": { + "balance": "0x15af1d78b58c400000" + }, + "a0fc7e53c5ebd27a2abdac45261f84ab3b51aefb": { + "balance": "0xa313daec9bc0d90000" + }, + "a0ff5b4cf016027e8323497d4428d3e5a83b8795": { + "balance": "0x16598d3c83ec0420000" + }, + "a106465bbd19e1b6bce50d1b1157dc59095a3630": { + "balance": "0x6c6b935b8bbd400000" + }, + "a106e6923edd53ca8ed650968a9108d6ccfd9670": { + "balance": "0x202fe1505afec898000" + }, + "a109e18bb0a39c9ef82fa19597fc5ed8e9eb6d58": { + "balance": "0x58e7926ee858a00000" + }, + "a11a03c4bb26d21eff677d5d555c80b25453ee7a": { + "balance": "0x3cb2759bc410f8000" + }, + "a11effab6cf0f5972cffe4d56596e98968144a8f": { + "balance": "0x5a87e7d7f5f6580000" + }, + "a1204dad5f560728a35c0d8fc79481057bf77386": { + "balance": "0x3635c9adc5dea00000" + }, + "a12623e629df93096704b16084be2cd89d562da4": { + "balance": "0x1ccc9324511e4500000" + }, + "a12a6c2d985daf0e4f5f207ae851aaf729b332cd": { + "balance": "0x152d02c7e14af6800000" + }, + "a1336dfb96b6bcbe4b3edf3205be5723c90fad52": { + "balance": "0x10f0cf064dd59200000" + }, + "a13b9d82a99b3c9bba5ae72ef2199edc7d3bb36c": { + "balance": "0x6c6acc67d7b1d40000" + }, + "a13cfe826d6d1841dcae443be8c387518136b5e8": { + "balance": "0x1da56a4b0835bf800000" + }, + "a1432ed2c6b7777a88e8d46d388e70477f208ca5": { + "balance": "0x1b1a7e413a196c50000" + }, + "a144f6b60f72d64a21e330dadb62d8990ade2b09": { + "balance": "0x3635c9adc5dea00000" + }, + "a15025f595acdbf3110f77c5bf24477e6548f9e8": { + "balance": "0x6c6b935b8bbd400000" + }, + "a158148a2e0f3e92dc2ce38febc20107e3253c96": { + "balance": "0x6c6b935b8bbd400000" + }, + "a16160851d2b9c349b92e46f829abfb210943595": { + "balance": "0x61093d7c2c6d380000" + }, + "a166f911c644ac3213d29e0e1ae010f794d5ad26": { + "balance": "0x6c6b935b8bbd400000" + }, + "a16d9e3d63986159a800b46837f45e8bb980ee0b": { + "balance": "0x6e1175da7ad1200000" + }, + "a17070c2e9c5a940a4ec0e4954c4d7d643be8f49": { + "balance": "0x6c6b17033b361c8000" + }, + "a17c9e4323069518189d5207a0728dcb92306a3f": { + "balance": "0x3635c9adc5dea00000" + }, + "a18360e985f2062e8f8efe02ad2cbc91ad9a5aad": { + "balance": "0xa2a15d09519be00000" + }, + "a1911405cf6e999ed011f0ddcd2a4ff7c28f2526": { + "balance": "0x22b1c8c1227a00000" + }, + "a192698007cc11aa603d221d5feea076bcf7c30d": { + "balance": "0x6c6b935b8bbd400000" + }, + "a192f06ab052d5fd7f94eea8318e827815fe677a": { + "balance": "0x71f8a93d01e540000" + }, + "a1998144968a5c70a6415554cefec2824690c4a5": { + "balance": "0x1158e460913d00000" + }, + "a1a1f0fa6d20b50a794f02ef52085c9d036aa6ca": { + "balance": "0x3635c9adc5dea00000" + }, + "a1ae8d4540d4db6fdde7146f415b431eb55c7983": { + "balance": "0xaadec983fcff40000" + }, + "a1b47c4d0ed6018842e6cfc8630ac3a3142e5e6b": { + "balance": "0x1158e460913d00000" + }, + "a1c4f45a82e1c478d845082eb18875c4ea6539ab": { + "balance": "0x2a5a058fc295ed000000" + }, + "a1dcd0e5b05a977c9623e5ae2f59b9ada2f33e31": { + "balance": "0x56bc75e2d63100000" + }, + "a1e4380a3b1f749673e270229993ee55f35663b4": { + "balance": "0x6c6b935b8bbd400000" + }, + "a1f193a0592f1feb9fdfc90aa813784eb80471c9": { + "balance": "0x4be4e7267b6ae00000" + }, + "a1f2854050f872658ed82e52b0ad7bbc1cb921f6": { + "balance": "0x6d0317e2b326f70000" + }, + "a1f5b840140d5a9acef402ac3cc3886a68cad248": { + "balance": "0x6c6b935b8bbd400000" + }, + "a1f765c44fe45f790677944844be4f2d42165fbd": { + "balance": "0xc7e9cfde768ec70000" + }, + "a1f7dde1d738d8cd679ea1ee965bee224be7d04d": { + "balance": "0x3d184450e5e93c0000" + }, + "a1f8d8bcf90e777f19b3a649759ad95027abdfc3": { + "balance": "0xad78ebc5ac6200000" + }, + "a202547242806f6e70e74058d6e5292defc8c8d4": { + "balance": "0x6c8754c8f30c080000" + }, + "a20d071b1b003063497d7990e1249dabf36c35f7": { + "balance": "0x3635c9adc5dea00000" + }, + "a20d8ff60caae31d02e0b665fa435d76f77c9442": { + "balance": "0x1a8a909dfcef400000" + }, + "a211da03cc0e31ecce5309998718515528a090df": { + "balance": "0xad78ebc5ac6200000" + }, + "a21442ab05340ade68c915f3c3399b9955f3f7eb": { + "balance": "0x2a034919dfbfbc0000" + }, + "a2222259dd9c3e3ded127084f808e92a1887302c": { + "balance": "0x8c8339dafed480000" + }, + "a22ade0ddb5c6ef8d0cd8de94d82b11082cb2e91": { + "balance": "0x374b57f3cef2700000" + }, + "a24c3ab62181e9a15b78c4621e4c7c588127be26": { + "balance": "0x8cde43a83d3310000" + }, + "a257ad594bd88328a7d90fc0a907df95eecae316": { + "balance": "0x1c3786ff3846930000" + }, + "a25b086437fd2192d0a0f64f6ed044f38ef3da32": { + "balance": "0x12290f15180bdc0000" + }, + "a276b058cb98d88beedb67e543506c9a0d9470d8": { + "balance": "0x90aafc76e02fbe0000" + }, + "a282e969cac9f7a0e1c0cd90f5d0c438ac570da3": { + "balance": "0x2207eb89fc27380000" + }, + "a291e9c7990d552dd1ae16cebc3fca342cbaf1d1": { + "balance": "0x43c33c1937564800000" + }, + "a29319e81069e5d60df00f3de5adee3505ecd5fb": { + "balance": "0x6c6b935b8bbd400000" + }, + "a2968fc1c64bac0b7ae0d68ba949874d6db253f4": { + "balance": "0x43c33c1937564800000" + }, + "a29d5bda74e003474872bd5894b88533ff64c2b5": { + "balance": "0x21e19e0c9bab2400000" + }, + "a29d661a6376f66d0b74e2fe9d8f26c0247ec84c": { + "balance": "0xdf3304079c13d20000" + }, + "a2a435de44a01bd0ecb29e44e47644e46a0cdffb": { + "balance": "0x1b1d445a7affe78000" + }, + "a2ace4c993bb1e5383f8ac74e179066e814f0591": { + "balance": "0x56bc75e2d63100000" + }, + "a2b701f9f5cdd09e4ba62baebae3a88257105885": { + "balance": "0x3635c9adc5dea00000" + }, + "a2c5854ff1599f98892c5725d262be1da98aadac": { + "balance": "0x1109ff333010e78000" + }, + "a2c7eaffdc2c9d937345206c909a52dfb14c478f": { + "balance": "0x7c0860e5a80dc0000" + }, + "a2d2aa626b09d6d4e4b13f7ffc5a88bd7ad36742": { + "balance": "0xfb8078507553830000" + }, + "a2d38de1c73906f6a7ca6efeb97cf6f69cc421be": { + "balance": "0x3635c9adc5dea00000" + }, + "a2dc65ee256b59a5bd7929774f904b358df3ada1": { + "balance": "0x483bce28beb09f80000" + }, + "a2e0683a805de6a05edb2ffbb5e96f0570b637c3": { + "balance": "0x1158e460913d00000" + }, + "a2e1b8aa900e9c139b3fa122354f6156d92a18b1": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "a2e2b5941e0c01944bfe1d5fb4e8a34b922ccfb1": { + "balance": "0xad78ebc5ac6200000" + }, + "a2e460a989cb15565f9ecca7d121a18e4eb405b6": { + "balance": "0x6c6b935b8bbd400000" + }, + "a2ecce2c49f72a0995a0bda57aacf1e9f001e22a": { + "balance": "0xd8d726b7177a800000" + }, + "a2f472fe4f22b77db489219ea4023d11582a9329": { + "balance": "0x878678326eac9000000" + }, + "a2f798e077b07d86124e1407df32890dbb4b6379": { + "balance": "0xad78ebc5ac6200000" + }, + "a2f86bc061884e9eef05640edd51a2f7c0596c69": { + "balance": "0x6c6c44fe47ec050000" + }, + "a2fa17c0fb506ce494008b9557841c3f641b8cae": { + "balance": "0x1158e460913d00000" + }, + "a304588f0d850cd8d38f76e9e83c1bf63e333ede": { + "balance": "0x2285601216c8c0000" + }, + "a3058c51737a4e96c55f2ef6bd7bb358167ec2a7": { + "balance": "0x20db3ae4481ad48000" + }, + "a309df54cabce70c95ec3033149cd6678a6fd4cf": { + "balance": "0xc1f12c75101580000" + }, + "a30a45520e5206d9004070e6af3e7bb2e8dd5313": { + "balance": "0x15af1d78b58c400000" + }, + "a30e0acb534c9b3084e8501da090b4eb16a2c0cd": { + "balance": "0x6c6b935b8bbd400000" + }, + "a3203095edb7028e6871ce0a84f548459f83300a": { + "balance": "0xd8d726b7177a800000" + }, + "a321091d3018064279db399d2b2a88a6f440ae24": { + "balance": "0xad78ebc5ac62000000" + }, + "a3232d068d50064903c9ebc563b515acc8b7b097": { + "balance": "0x6c8754c8f30c080000" + }, + "a3241d890a92baf52908dc4aa049726be426ebd3": { + "balance": "0x43c2da661ca2f540000" + }, + "a3294626ec2984c43b43da4d5d8e4669b11d4b59": { + "balance": "0x36a4cf636319c00000" + }, + "a32cf7dde20c3dd5679ff5e325845c70c5962662": { + "balance": "0x1158e460913d00000" + }, + "a339a3d8ca280e27d2415b26d1fc793228b66043": { + "balance": "0x36f28695b78ff00000" + }, + "a33cb450f95bb46e25afb50fe05feee6fb8cc8ea": { + "balance": "0x2a1129d09367200000" + }, + "a33f70da7275ef057104dfa7db64f472e9f5d553": { + "balance": "0x45946b0f9e9d60000" + }, + "a34076f84bd917f20f8342c98ba79e6fb08ecd31": { + "balance": "0xe3aeb5737240a00000" + }, + "a3430e1f647f321ed34739562323c7d623410b56": { + "balance": "0x3634fb9f1489a70000" + }, + "a34f9d568bf7afd94c2a5b8a5ff55c66c4087999": { + "balance": "0x847d503b220eb00000" + }, + "a35606d51220ee7f2146d411582ee4ee4a45596e": { + "balance": "0xd8aabe080bc9400000" + }, + "a356551bb77d4f45a6d7e09f0a089e79cca249cb": { + "balance": "0x126e72a69a50d00000" + }, + "a35c19132cac1935576abfed6c0495fb07881ba0": { + "balance": "0x6c6b935b8bbd400000" + }, + "a365918bfe3f2627b9f3a86775d8756e0fd8a94b": { + "balance": "0x15af1d78b58c400000" + }, + "a36e0d94b95364a82671b608cb2d373245612909": { + "balance": "0x821d221b5291f8000" + }, + "a375b4bc24a24e1f797593cc302b2f331063fa5c": { + "balance": "0xad78ebc5ac6200000" + }, + "a37622ac9bbdc4d82b75015d745b9f8de65a28ec": { + "balance": "0x9dc05cce28c2b80000" + }, + "a379a5070c503d2fac89b8b3afa080fd45ed4bec": { + "balance": "0x42bf06b78ed3b500000" + }, + "a3802d8a659e89a2c47e905430b2a827978950a7": { + "balance": "0x3635c9adc5dea00000" + }, + "a38306cb70baa8e49186bd68aa70a83d242f2907": { + "balance": "0x6c6b935b8bbd400000" + }, + "a38476691d34942eea6b2f76889223047db4617a": { + "balance": "0x6c6b935b8bbd400000" + }, + "a387ce4e961a7847f560075c64e1596b5641d21c": { + "balance": "0x243d4d18229ca20000" + }, + "a387ecde0ee4c8079499fd8e03473bd88ad7522a": { + "balance": "0x6acb3df27e1f880000" + }, + "a3883a24f7f166205f1a6a9949076c26a76e7178": { + "balance": "0x62a992e53a0af00000" + }, + "a38b5bd81a9db9d2b21d5ec7c60552cd02ed561b": { + "balance": "0x14542ba12a337c00000" + }, + "a390ca122b8501ee3e5e07a8ca4b419f7e4dae15": { + "balance": "0x56bc75e2d63100000" + }, + "a3932a31d6ff75fb3b1271ace7caa7d5e1ff1051": { + "balance": "0x43c33c1937564800000" + }, + "a394ad4fd9e6530e6f5c53faecbede81cb172da1": { + "balance": "0x12f939c99edab800000" + }, + "a3979a92760a135adf69d72f75e167755f1cb8c3": { + "balance": "0x56bc75e2d63100000" + }, + "a39bfee4aec9bd75bd22c6b672898ca9a1e95d32": { + "balance": "0x21e19e0c9bab2400000" + }, + "a3a262afd2936819230892fde84f2d5a594ab283": { + "balance": "0x65ea3db75546600000" + }, + "a3a2e319e7d3a1448b5aa2468953160c2dbcba71": { + "balance": "0x6c6b935b8bbd400000" + }, + "a3a57b0716132804d60aac281197ff2b3d237b01": { + "balance": "0x4be4e7267b6ae00000" + }, + "a3a93ef9dbea2636263d06d8492f6a41de907c22": { + "balance": "0x340aad21b3b700000" + }, + "a3ae1879007d801cb5f352716a4dd8ba2721de3d": { + "balance": "0x2a5a058fc295ed000000" + }, + "a3ba0d3a3617b1e31b4e422ce269e873828d5d69": { + "balance": "0x2e141ea081ca080000" + }, + "a3bc979b7080092fa1f92f6e0fb347e28d995045": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "a3bff1dfa9971668360c0d82828432e27bf54e67": { + "balance": "0xad78ebc5ac6200000" + }, + "a3c14ace28b192cbb062145fcbbd5869c67271f6": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "a3c33afc8cb4704e23153de2049d35ae71332472": { + "balance": "0x2b58addb89a2580000" + }, + "a3d0b03cffbb269f796ac29d80bfb07dc7c6ad06": { + "balance": "0x6c6b935b8bbd400000" + }, + "a3d583a7b65b23f60b7905f3e4aa62aac87f4227": { + "balance": "0x38befa126d5a9f8000" + }, + "a3db364a332d884ba93b2617ae4d85a1489bea47": { + "balance": "0x5c283d410394100000" + }, + "a3e051fb744aa3410c3b88f899f5d57f168df12d": { + "balance": "0xa030dcebbd2f4c0000" + }, + "a3e3a6ea509573e21bd0239ece0523a7b7d89b2f": { + "balance": "0x6acb3df27e1f880000" + }, + "a3f4ad14e0bb44e2ce2c14359c75b8e732d37054": { + "balance": "0xad78ebc5ac6200000" + }, + "a3facc50195c0b4933c85897fecc5bbd995c34b8": { + "balance": "0x1158e460913d00000" + }, + "a4035ab1e5180821f0f380f1131b7387c8d981cd": { + "balance": "0x1158e460913d00000" + }, + "a40aa2bbce0c72b4d0dfffcc42715b2b54b01bfa": { + "balance": "0x3635c9adc5dea00000" + }, + "a419a984142363267575566089340eea0ea20819": { + "balance": "0x6c6acc67d7b1d40000" + }, + "a421dbb89b3a07419084ad10c3c15dfe9b32d0c2": { + "balance": "0x43c33c1937564800000" + }, + "a422e4bf0bf74147cc895bed8f16d3cef3426154": { + "balance": "0x12ef3f62ee11368000" + }, + "a4259f8345f7e3a8b72b0fec2cf75e321fda4dc2": { + "balance": "0x678a932062e4180000" + }, + "a42908e7fe53980a9abf4044e957a54b70e99cbe": { + "balance": "0x6c6b935b8bbd400000" + }, + "a429fa88731fdd350e8ecd6ea54296b6484fe695": { + "balance": "0x6ac5c62d9486070000" + }, + "a430995ddb185b9865dbe62539ad90d22e4b73c2": { + "balance": "0x21e19e0c9bab2400000" + }, + "a436c75453ccca4a1f1b62e5c4a30d86dde4be68": { + "balance": "0x6c6b935b8bbd400000" + }, + "a437fe6ec103ca8d158f63b334224eccac5b3ea3": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "a43b6da6cb7aac571dff27f09d39f846f53769b1": { + "balance": "0x14998f32ac78700000" + }, + "a43b81f99356c0af141a03010d77bd042c71c1ee": { + "balance": "0x6c6b935b8bbd400000" + }, + "a43e1947a9242b355561c30a829dfeeca2815af8": { + "balance": "0xd23d99969fd6918000" + }, + "a4489a50ead5d5445a7bee4d2d5536c2a76c41f8": { + "balance": "0xad78ebc5ac6200000" + }, + "a44fe800d96fcad73b7170d0f610cb8c0682d6ce": { + "balance": "0xd8d726b7177a800000" + }, + "a45432a6f2ac9d56577b938a37fabac8cc7c461c": { + "balance": "0x3635c9adc5dea00000" + }, + "a466d770d898d8c9d405e4a0e551efafcde53cf9": { + "balance": "0x1ab2cf7c9f87e20000" + }, + "a4670731175893bbcff4fa85ce97d94fc51c4ba8": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "a46b4387fb4dcce011e76e4d73547d4481e09be5": { + "balance": "0x487a9a304539440000" + }, + "a46cd237b63eea438c8e3b6585f679e4860832ac": { + "balance": "0x3635c9adc5dea00000" + }, + "a47779d8bc1c7bce0f011ccb39ef68b854f8de8f": { + "balance": "0x6c6b935b8bbd400000" + }, + "a4826b6c3882fad0ed5c8fbb25cc40cc4f33759f": { + "balance": "0x701b43e34433d00000" + }, + "a4875928458ec2005dbb578c5cd33580f0cf1452": { + "balance": "0x3635c9adc5dea00000" + }, + "a49f523aa51364cbc7d995163d34eb590ded2f08": { + "balance": "0x9027421b2a9fbc0000" + }, + "a4a49f0bc8688cc9e6dc04e1e08d521026e65574": { + "balance": "0xad78ebc5ac6200000" + }, + "a4a7d306f510cd58359428c0d2f7c3609d5674d7": { + "balance": "0xb58cb61c3ccf340000" + }, + "a4a83a0738799b971bf2de708c2ebf911ca79eb2": { + "balance": "0x2086ac351052600000" + }, + "a4b09de6e713dc69546e76ef0acf40b94f0241e6": { + "balance": "0x117dc0627ec8700000" + }, + "a4d2b429f1ad5349e31704969edc5f25ee8aca10": { + "balance": "0x21e19e0c9bab2400000" + }, + "a4d6c82eddae5947fbe9cdfbd548ae33d91a7191": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "a4da34450d22ec0ffcede0004b02f7872ee0b73a": { + "balance": "0x50f616673f0830000" + }, + "a4dd59ab5e517d398e49fa537f899fed4c15e95d": { + "balance": "0x43c33c1937564800000" + }, + "a4e623451e7e94e7e89ba5ed95c8a83a62ffc4ea": { + "balance": "0x1158e460913d00000" + }, + "a4ed11b072d89fb136759fc69b428c48aa5d4ced": { + "balance": "0xe3f1527a03ca80000" + }, + "a4fb14409a67b45688a8593e5cc2cf596ced6f11": { + "balance": "0x61093d7c2c6d380000" + }, + "a514d00edd7108a6be839a638db2415418174196": { + "balance": "0x65a4da25d3016c00000" + }, + "a522de7eb6ae1250522a513133a93bd42849475c": { + "balance": "0x43c33c1937564800000" + }, + "a524a8cccc49518d170a328270a2f88133fbaf5d": { + "balance": "0xff7022dac108a0000" + }, + "a539b4a401b584dfe0f344b1b422c65543167e2e": { + "balance": "0xad78ebc5ac6200000" + }, + "a53ead54f7850af21438cbe07af686279a315b86": { + "balance": "0x21e19e0c9bab2400000" + }, + "a543a066fb32a8668aa0736a0c9cd40d78098727": { + "balance": "0x3635c9adc5dea00000" + }, + "a567770b6ae320bdde50f904d663e746a61dace6": { + "balance": "0x6c6b935b8bbd400000" + }, + "a568db4d57e4d67462d733c69a9e0fe26e218327": { + "balance": "0x3b6bff9266c0ae0000" + }, + "a5698035391e67a49013c0002079593114feb353": { + "balance": "0xd02ab486cedc00000" + }, + "a570223ae3caa851418a9843a1ac55db4824f4fd": { + "balance": "0xad78ebc5ac6200000" + }, + "a57360f002e0d64d2d74457d8ca4857ee00bcddf": { + "balance": "0x1233e232f618aa0000" + }, + "a575f2891dcfcda83c5cf01474af11ee01b72dc2": { + "balance": "0x56cd55fc64dfe0000" + }, + "a5783bf33432ff82ac498985d7d460ae67ec3673": { + "balance": "0x62a992e53a0af00000" + }, + "a5874d754635a762b381a5c4c792483af8f23d1d": { + "balance": "0x2b5e3af16b1880000" + }, + "a5a4227f6cf98825c0d5baff5315752ccc1a1391": { + "balance": "0x21e19e0c9bab2400000" + }, + "a5ab4bd3588f46cb272e56e93deed386ba8b753d": { + "balance": "0x4842f04105872c8000" + }, + "a5bad86509fbe0e0e3c0e93f6d381f1af6e9d481": { + "balance": "0x14542ba12a337c00000" + }, + "a5c336083b04f9471b8c6ed73679b74d66c363ec": { + "balance": "0xa3650a4c9d20e20000" + }, + "a5cd123992194b34c4781314303b03c54948f4b9": { + "balance": "0x6cfcc3d91da5630000" + }, + "a5d5b8b62d002def92413710d13b6ff8d4fc7dd3": { + "balance": "0x15af1d78b58c400000" + }, + "a5d96e697d46358d119af7819dc7087f6ae47fef": { + "balance": "0x317bee8af3315a78000" + }, + "a5de5e434fdcdd688f1c31b6fb512cb196724701": { + "balance": "0x2b5e3af16b18800000" + }, + "a5e0fc3c3affed3db6710947d1d6fb017f3e276d": { + "balance": "0x6c6b935b8bbd400000" + }, + "a5e93b49ea7c509de7c44d6cfeddef5910deaaf2": { + "balance": "0x6c6b935b8bbd400000" + }, + "a5e9cd4b74255d22b7d9b27ae8dd43ed6ed0252b": { + "balance": "0x298db2f54411d98000" + }, + "a5f0077b351f6c505cd515dfa6d2fa7f5c4cd287": { + "balance": "0x878678326eac9000000" + }, + "a5f075fd401335577b6683c281e6d101432dc6e0": { + "balance": "0x914878a8c05ee00000" + }, + "a5fe2ce97f0e8c3856be0de5f4dcb2ce5d389a16": { + "balance": "0x13db0b8b6863e0000" + }, + "a5ff62222d80c013cec1a0e8850ed4d354dac16d": { + "balance": "0xb41075c168b180000" + }, + "a609c26dd350c235e44b2b9c1dddccd0a9d9f837": { + "balance": "0x3635c9adc5dea00000" + }, + "a60c1209754f5d87b181da4f0817a81859ef9fd8": { + "balance": "0x2b5e3af16b1880000" + }, + "a6101c961e8e1c15798ffcd0e3201d7786ec373a": { + "balance": "0x14542ba12a337c00000" + }, + "a613456996408af1c2e93e177788ab55895e2b32": { + "balance": "0x15919ff477c88b80000" + }, + "a61887818f914a20e31077290b83715a6b2d6ef9": { + "balance": "0x65ea3db75546600000" + }, + "a61a54df784a44d71b771b87317509211381f200": { + "balance": "0x3635c9adc5dea00000" + }, + "a61cdbadf04b1e54c883de6005fcdf16beb8eb2f": { + "balance": "0x6c6b935b8bbd400000" + }, + "a639acd96b31ba53b0d08763229e1f06fd105e9d": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "a642501004c90ea9c9ed1998ba140a4cd62c6f5f": { + "balance": "0xd94fb8b10f8b18000" + }, + "a644ed922cc237a3e5c4979a995477f36e50bc62": { + "balance": "0x1fa73d845d7e960000" + }, + "a646a95c6d6f59f104c6541d7760757ab392b08c": { + "balance": "0xe3aeb5737240a00000" + }, + "a6484cc684c4c91db53eb68a4da45a6a6bda3067": { + "balance": "0x14542ba12a337c00000" + }, + "a64e5ffb704c2c9139d77ef61d8cdfa31d7a88e9": { + "balance": "0x7c0860e5a80dc0000" + }, + "a65426cff378ed23253513b19f496de45fa7e18f": { + "balance": "0x18650127cc3dc800000" + }, + "a66a4963b27f1ee1932b172be5964e0d3ae54b51": { + "balance": "0x960db77681e940000" + }, + "a67f38819565423aa85f3e3ab61bc763cbab89dd": { + "balance": "0x7377b022c6be080000" + }, + "a68c313445c22d919ee46cc2d0cdff043a755825": { + "balance": "0x41374fd21b0d88000" + }, + "a68e0c30cba3bc5a883e540320f999c7cd558e5c": { + "balance": "0x6192333762a58c8000" + }, + "a690f1a4b20ab7ba34628620de9ca040c43c1963": { + "balance": "0xd8d726b7177a800000" + }, + "a69d7cd17d4842fe03f62a90b2fbf8f6af7bb380": { + "balance": "0x56bc75e2d63100000" + }, + "a6a08252c8595177cc2e60fc27593e2379c81fb1": { + "balance": "0x11651ac3e7a758000" + }, + "a6a0de421ae54f6d17281308f5646d2f39f7775d": { + "balance": "0x6c6b935b8bbd400000" + }, + "a6b2d573297360102c07a18fc21df2e7499ff4eb": { + "balance": "0xd96fce90cfabcc0000" + }, + "a6c910ce4d494a919ccdaaa1fc3b82aa74ba06cf": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "a6e3baa38e104a1e27a4d82869afb1c0ae6eff8d": { + "balance": "0x11140eead8b710000" + }, + "a6eebbe464d39187bf80ca9c13d72027ec5ba8be": { + "balance": "0xa2a15d09519be00000" + }, + "a6f62b8a3d7f11220701ab9ffffcb327959a2785": { + "balance": "0x1b6e291f18dba80000" + }, + "a6f93307f8bce03195fece872043e8a03f7bd11a": { + "balance": "0x9c734bad5111580000" + }, + "a701df79f594901afe1444485e6b20c3bda2b9b3": { + "balance": "0x3635c9adc5dea00000" + }, + "a7024cfd742c1ec13c01fea18d3042e65f1d5dee": { + "balance": "0x263119a28abd0b08000" + }, + "a718aaad59bf395cba2b23e09b02fe0c89816247": { + "balance": "0x36303c97e468780000" + }, + "a7247c53d059eb7c9310f628d7fc6c6a0a773f08": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "a7253763cf4a75df92ca1e766dc4ee8a2745147b": { + "balance": "0x2463770e90a8f500000" + }, + "a72ee666c4b35e82a506808b443cebd5c632c7dd": { + "balance": "0x2b5e3af16b18800000" + }, + "a74444f90fbb54e56f3ac9b6cfccaa4819e4614a": { + "balance": "0x1158e460913d00000" + }, + "a747439ad0d393b5a03861d77296326de8bb9db9": { + "balance": "0x3635c9adc5dea00000" + }, + "a7607b42573bb6f6b4d4f23c7e2a26b3a0f6b6f0": { + "balance": "0x57473d05dabae80000" + }, + "a76929890a7b47fb859196016c6fdd8289ceb755": { + "balance": "0x10f0cf064dd59200000" + }, + "a76b743f981b693072a131b22ba510965c2fefd7": { + "balance": "0xfc936392801c0000" + }, + "a76d3f156251b72c0ccf4b47a3393cbd6f49a9c5": { + "balance": "0x487a9a304539440000" + }, + "a77428bcb2a0db76fc8ef1e20e461a0a32c5ac15": { + "balance": "0x15be6174e1912e0000" + }, + "a7758cecb60e8f614cce96137ef72b4fbd07774a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "a7775e4af6a23afa201fb78b915e51a515b7a728": { + "balance": "0x68155a43676e00000" + }, + "a77f3ee19e9388bbbb2215c62397b96560132360": { + "balance": "0xad78ebc5ac6200000" + }, + "a7859fc07f756ea7dcebbccd42f05817582d973f": { + "balance": "0x21e19e0c9bab2400000" + }, + "a7966c489f4c748a7ae980aa27a574251767caf9": { + "balance": "0xa2a15d09519be00000" + }, + "a7a3bb6139b0ada00c1f7f1f9f56d994ba4d1fa8": { + "balance": "0x6c6b935b8bbd400000" + }, + "a7a3f153cdc38821c20c5d8c8241b294a3f82b24": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "a7a517d7ad35820b09d497fa7e5540cde9495853": { + "balance": "0x6c6b935b8bbd400000" + }, + "a7c9d388ebd873e66b1713448397d0f37f8bd3a8": { + "balance": "0x10f0cf064dd59200000" + }, + "a7dcbba9b9bf6762c145416c506a71e3b497209c": { + "balance": "0x6c6acc67d7b1d40000" + }, + "a7e74f0bdb278ff0a805a648618ec52b166ff1be": { + "balance": "0x56bc75e2d63100000" + }, + "a7e83772bc200f9006aa2a260dbaa8483dc52b30": { + "balance": "0xb42d5366637e50000" + }, + "a7ef35ce87eda6c28df248785815053ec97a5045": { + "balance": "0x10f0ce949e00f930000" + }, + "a7f9220c8047826bd5d5183f4e676a6d77bfed36": { + "balance": "0x85068976be81c0000" + }, + "a807104f2703d679f8deafc442befe849e42950b": { + "balance": "0x6c6b935b8bbd400000" + }, + "a80cb1738bac08d4f9c08b4deff515545fa8584f": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "a819d2ece122e028c8e8a04a064d02b9029b08b9": { + "balance": "0x3635c9adc5dea00000" + }, + "a825fd5abb7926a67cf36ba246a24bd27be6f6ed": { + "balance": "0xf43fc2c04ee00000" + }, + "a8285539869d88f8a961533755717d7eb65576ae": { + "balance": "0xad78ebc5ac6200000" + }, + "a83382b6e15267974a8550b98f7176c1a353f9be": { + "balance": "0xbffdaf2fc1b1a40000" + }, + "a8446c4781a737ac4328b1e15b8a0b3fbb0fd668": { + "balance": "0x48794d1f246192a0000" + }, + "a8455b411765d6901e311e726403091e42c56683": { + "balance": "0xb73aec3bfe14500000" + }, + "a86613e6c4a4c9c55f5c10bcda32175dcbb4af60": { + "balance": "0x243d6c2e36be6ae0000" + }, + "a86db07d9f812f4796622d40e03d135874a88a74": { + "balance": "0x1158e460913d00000" + }, + "a87f7abd6fa31194289678efb63cf584ee5e2a61": { + "balance": "0xd8d726b7177a800000" + }, + "a880e2a8bf88a1a82648b4013c49c4594c433cc8": { + "balance": "0x1004e2e45fb7ee00000" + }, + "a88577a073fbaf33c4cd202e00ea70ef711b4006": { + "balance": "0x6c6b935b8bbd400000" + }, + "a8914c95b560ec13f140577338c32bcbb77d3a7a": { + "balance": "0x9c2007651b2500000" + }, + "a89ac93b23370472daac337e9afdf642543f3e57": { + "balance": "0x21e19e0c9bab2400000" + }, + "a89df34859edd7c820db887740d8ff9e15157c7b": { + "balance": "0x6c6b935b8bbd400000" + }, + "a8a43c009100616cb4ae4e033f1fc5d7e0b6f152": { + "balance": "0xd588d078b43f4d8000" + }, + "a8a708e84f82db86a35502193b4c6ee9a76ebe8f": { + "balance": "0x3708baed3d68900000" + }, + "a8a7b68adab4e3eadff19ffa58e34a3fcec0d96a": { + "balance": "0x14542ba12a337c00000" + }, + "a8a8dbdd1a85d1beee2569e91ccc4d09ae7f6ea1": { + "balance": "0x13a6b2b564871a00000" + }, + "a8aca748f9d312ec747f8b6578142694c7e9f399": { + "balance": "0x6c6b935b8bbd400000" + }, + "a8b65ba3171a3f77a6350b9daf1f8d55b4d201eb": { + "balance": "0x2862f3b0d222040000" + }, + "a8beb91c2b99c8964aa95b6b4a184b1269fc3483": { + "balance": "0x15af1d78b58c400000" + }, + "a8c0b02faf02cb5519dda884de7bbc8c88a2da81": { + "balance": "0xe7c2518505060000" + }, + "a8c1d6aa41fe3d65f67bd01de2a866ed1ed9ae52": { + "balance": "0x1a055690d9db80000" + }, + "a8cafac32280d021020bf6f2a9782883d7aabe12": { + "balance": "0x56bc75e2d63100000" + }, + "a8db0b9b201453333c757f6ad9bcb555c02da93b": { + "balance": "0x7742b7830f341d0000" + }, + "a8e42a4e33d7526cca19d9a36dcd6e8040d0ea73": { + "balance": "0x3a8c02c5ea2de00000" + }, + "a8e7201ff619faffc332e6ad37ed41e301bf014a": { + "balance": "0x2086ac351052600000" + }, + "a8ee1df5d44b128469e913569ef6ac81eeda4fc8": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "a8ef9ad274436042903e413c3b0c62f5f52ed584": { + "balance": "0x21e19e0c9bab2400000" + }, + "a8f37f0ab3a1d448a9e3ce40965f97a646083a34": { + "balance": "0x11e0e4f8a50bd40000" + }, + "a8f89dd5cc6e64d7b1eeace00702022cd7d2f03d": { + "balance": "0x25f273933db5700000" + }, + "a90476e2efdfee4f387b0f32a50678b0efb573b5": { + "balance": "0x21e19e0c9bab2400000" + }, + "a9145046fa3628cf5fd4c613927be531e6db1fdd": { + "balance": "0x6124fee993bc00000" + }, + "a914cdb571bfd93d64da66a4e108ea134e50d000": { + "balance": "0x4d8738994713798000" + }, + "a91a5a7b341f99c535144e20be9c6b3bb4c28e4d": { + "balance": "0x126753aa224a70b0000" + }, + "a9252551a624ae513719dabe5207fbefb2fd7749": { + "balance": "0x22b1c8c1227a00000" + }, + "a927d48bb6cb814bc609cbcaa9151f5d459a27e1": { + "balance": "0xeb935090064180000" + }, + "a929c8bd71db0c308dac06080a1747f21b1465aa": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "a94bbb8214cf8da0c2f668a2ac73e86248528d4b": { + "balance": "0x340aad21b3b7000000" + }, + "a951b244ff50cfae591d5e1a148df6a938ef2a1a": { + "balance": "0x5e001584dfcf580000" + }, + "a960b1cadd3b5c1a8e6cb3abcaf52ee7c3d9fa88": { + "balance": "0x528bc3545e52680000" + }, + "a961171f5342b173dd70e7bfe5b5ca238b13bcdd": { + "balance": "0xb82794a9244f0c8000" + }, + "a975b077fcb4cc8efcbf838459b6fa243a4159d6": { + "balance": "0x22b1c8c1227a00000" + }, + "a97beb3a48c45f1528284cb6a95f7de453358ec6": { + "balance": "0x690836c0af5f5600000" + }, + "a97e072144499fe5ebbd354acc7e7efb58985d08": { + "balance": "0x90f534608a72880000" + }, + "a986762f7a4f294f2e0b173279ad2c81a2223458": { + "balance": "0x1158e460913d00000" + }, + "a98f109835f5eacd0543647c34a6b269e3802fac": { + "balance": "0x15af1d78b58c400000" + }, + "a997dfc7986a27050848fa1c64d7a7d6e07acca2": { + "balance": "0x7c0860e5a80dc0000" + }, + "a99991cebd98d9c838c25f7a7416d9e244ca250d": { + "balance": "0x3635c9adc5dea00000" + }, + "a9a1cdc33bfd376f1c0d76fb6c84b6b4ac274d68": { + "balance": "0x10f0cf064dd59200000" + }, + "a9a8eca11a23d64689a2aa3e417dbb3d336bb59a": { + "balance": "0xe3453cd3b67ba8000" + }, + "a9acf600081bb55bb6bfbab1815ffc4e17e85a95": { + "balance": "0xad78ebc5ac6200000" + }, + "a9ad1926bc66bdb331588ea8193788534d982c98": { + "balance": "0x65a4da25d3016c00000" + }, + "a9af21acbe482f8131896a228036ba51b19453c3": { + "balance": "0x2b5e021980cc18000" + }, + "a9b2d2e0494eab18e07d37bbb856d80e80f84cd3": { + "balance": "0x21e19e0c9bab2400000" + }, + "a9ba6f413b82fcddf3affbbdd09287dcf50415ca": { + "balance": "0xd8d726b7177a800000" + }, + "a9be88ad1e518b0bbb024ab1d8f0e73f790e0c76": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "a9bfc410dddb20711e45c07387eab30a054e19ac": { + "balance": "0x3e99601edf4e530000" + }, + "a9d4a2bcbe5b9e0869d70f0fe2e1d6aacd45edc5": { + "balance": "0xac6e77ab663a80000" + }, + "a9d64b4f3bb7850722b58b478ba691375e224e42": { + "balance": "0x14542ba12a337c00000" + }, + "a9d6f871ca781a759a20ac3adb972cf12829a208": { + "balance": "0x3224f42723d4540000" + }, + "a9dc0424c6969d798358b393b1933a1f51bee00a": { + "balance": "0x43c33c1937564800000" + }, + "a9e194661aac704ee9dea043974e9692ded84a5d": { + "balance": "0x1a26a51422a0700000" + }, + "a9e28337e6357193d9e2cb236b01be44b81427df": { + "balance": "0x77432217e683600000" + }, + "a9e6e25e656b762558619f147a21985b8874edfe": { + "balance": "0x6c6b935b8bbd400000" + }, + "a9e9dbce7a2cb03694799897bed7c54d155fdaa8": { + "balance": "0xab5ae8fc99d658000" + }, + "a9ed377b7d6ec25971c1a597a3b0f3bead57c98f": { + "balance": "0x15af1d78b58c400000" + }, + "aa0200f1d17e9c54da0647bb96395d57a78538d8": { + "balance": "0x393ef1a5127c800000" + }, + "aa0ca3737337178a0caac3099c584b056c56301c": { + "balance": "0x2fb474098f67c00000" + }, + "aa136b47962bb8b4fb540db4ccf5fdd042ffb8cf": { + "balance": "0x1b1b6bd7af64c70000" + }, + "aa14422d6f0ae5a758194ed15780c838d67f1ee1": { + "balance": "0x60932056c449de80000" + }, + "aa16269aac9c0d803068d82fc79151dadd334b66": { + "balance": "0xd8d726b7177a800000" + }, + "aa167026d39ab7a85635944ed9edb2bfeba11850": { + "balance": "0x1c1d5e21b4fcf680000" + }, + "aa1b3768c16d821f580e76c8e4c8e86d7dc78853": { + "balance": "0x15af1d78b58c400000" + }, + "aa1df92e51dff70b1973e0e924c66287b494a178": { + "balance": "0x1cf84a30a0a0c00000" + }, + "aa2c670096d3f939305325427eb955a8a60db3c5": { + "balance": "0x6c95590699232d0000" + }, + "aa3135cb54f102cbefe09e96103a1a796718ff54": { + "balance": "0x32222d9c331940000" + }, + "aa321fdbd449180db8ddd34f0fe906ec18ee0914": { + "balance": "0x252248deb6e6940000" + }, + "aa3925dc220bb4ae2177b2883078b6dc346ca1b2": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "aa3f29601a1331745e05c42830a15e71938a6237": { + "balance": "0x5c283d410394100000" + }, + "aa47a4ffc979363232c99b99fada0f2734b0aeee": { + "balance": "0x1b8489df4dbff940000" + }, + "aa493d3f4fb866491cf8f800efb7e2324ed7cfe5": { + "balance": "0x5c283d410394100000" + }, + "aa56a65dc4abb72f11bae32b6fbb07444791d5c9": { + "balance": "0x2894e975bf496c0000" + }, + "aa5afcfd8309c2df9d15be5e6a504e7d706624c5": { + "balance": "0x13cf422e305a1378000" + }, + "aa8eb0823b07b0e6d20aadda0e95cf3835be192e": { + "balance": "0x1bc16d674ec800000" + }, + "aa91237e740d25a92f7fa146faa18ce56dc6e1f3": { + "balance": "0x3224f42723d4540000" + }, + "aa960e10c52391c54e15387cc67af827b5316dcc": { + "balance": "0x6c6b935b8bbd400000" + }, + "aa9bd4589535db27fa2bc903ca17d679dd654806": { + "balance": "0x6c6b935b8bbd400000" + }, + "aaa8defe11e3613f11067fb983625a08995a8dfc": { + "balance": "0xad78ebc5ac6200000" + }, + "aaaae68b321402c8ebc13468f341c63c0cf03fce": { + "balance": "0x52663ccab1e1c00000" + }, + "aaad1baade5af04e2b17439e935987bf8c2bb4b9": { + "balance": "0x6c6b935b8bbd400000" + }, + "aab00abf5828d7ebf26b47ceaccdb8ba03325166": { + "balance": "0x21e19e0c9bab2400000" + }, + "aabdb35c1514984a039213793f3345a168e81ff1": { + "balance": "0x10cac896d239000000" + }, + "aaca60d9d700e78596bbbbb1f1e2f70f4627f9d8": { + "balance": "0x3635bb77cb4b860000" + }, + "aaced8a9563b1bc311dbdffc1ae7f57519c4440c": { + "balance": "0x6c6b935b8bbd400000" + }, + "aad2b7f8106695078e6c138ec81a7486aaca1eb2": { + "balance": "0xad78ebc5ac6200000" + }, + "aae61e43cb0d0c96b30699f77e00d711d0a3979b": { + "balance": "0x3635c9adc5dea00000" + }, + "aae732eda65988c3a00c7f472f351c463b1c968e": { + "balance": "0x6c6b935b8bbd400000" + }, + "aaf023fef290a49bb78bb7abc95d669c50d528b0": { + "balance": "0xad78ebc5ac6200000" + }, + "aaf5b207b88b0de4ac40d747cee06e172df6e745": { + "balance": "0x6a7b71d7f51d0900000" + }, + "aaf9ee4b886c6d1e95496fd274235bf4ecfcb07d": { + "balance": "0x4be4e7267b6ae00000" + }, + "aafb7b013aa1f8541c7e327bf650adbd194c208f": { + "balance": "0x499e092d01f4780000" + }, + "ab098633eeee0ccefdf632f9575456f6dd80fc86": { + "balance": "0x2a5a058fc295ed000000" + }, + "ab0ced762e1661fae1a92afb1408889413794825": { + "balance": "0x678a932062e4180000" + }, + "ab14d221e33d544629198cd096ed63dfa28d9f47": { + "balance": "0x14542ba12a337c00000" + }, + "ab209fdca979d0a647010af9a8b52fc7d20d8cd1": { + "balance": "0x1eee2532c7c2d040000" + }, + "ab27ba78c8e5e3daef31ad05aef0ff0325721e08": { + "balance": "0x195ece006e02d00000" + }, + "ab2871e507c7be3965498e8fb462025a1a1c4264": { + "balance": "0x2a034919dfbfbc0000" + }, + "ab3861226ffec1289187fb84a08ec3ed043264e8": { + "balance": "0x3635c9adc5dea00000" + }, + "ab3d86bc82927e0cd421d146e07f919327cdf6f9": { + "balance": "0x678a932062e4180000" + }, + "ab3e62e77a8b225e411592b1af300752fe412463": { + "balance": "0x215f835bc769da80000" + }, + "ab3e78294ba886a0cfd5d3487fb3a3078d338d6e": { + "balance": "0x6acb3df27e1f880000" + }, + "ab4004c0403f7eabb0ea586f212156c4203d67f1": { + "balance": "0x6c6acc67d7b1d40000" + }, + "ab416fe30d58afe5d9454c7fce7f830bcc750356": { + "balance": "0x6353701c605db8000" + }, + "ab4572fbb1d72b575d69ec6ad17333873e8552fc": { + "balance": "0x6c6ac54cda68470000" + }, + "ab5a79016176320973e8cd38f6375530022531c0": { + "balance": "0x3635c9adc5dea00000" + }, + "ab5dfc1ea21adc42cf8c3f6e361e243fd0da61e5": { + "balance": "0x1043561a8829300000" + }, + "ab6b65eab8dfc917ec0251b9db0ecfa0fa032849": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ab7091932e4bc39dbb552380ca934fd7166d1e6e": { + "balance": "0xb50fcfafebecb00000" + }, + "ab7416ff32254951cbbc624ec7fb45fc7ecaa872": { + "balance": "0x126e72a69a50d00000" + }, + "ab7c42c5e52d641a07ad75099c62928b7f86622f": { + "balance": "0x12361aa21d14ba0000" + }, + "ab7d54c7c6570efca5b4b8ce70f52a5773e5d53b": { + "balance": "0xf283abe9d9f380000" + }, + "ab7e0b83ed9a424c6d1e6a6f87a4dbf06409c7d6": { + "balance": "0x821ab0d44149800000" + }, + "ab84a0f147ad265400002b85029a41fc9ce57f85": { + "balance": "0x3635c9adc5dea00000" + }, + "ab93b26ece0a0aa21365afed1fa9aea31cd54468": { + "balance": "0x572b7b98736c200000" + }, + "ab948a4ae3795cbca13126e19253bdc21d3a8514": { + "balance": "0xad78ebc5ac6200000" + }, + "ab9ad36e5c74ce2e96399f57839431d0e79f96ab": { + "balance": "0x8e3f50b173c100000" + }, + "abb2e6a72a40ba6ed908cdbcec3c5612583132fe": { + "balance": "0x4f2591f896a6500000" + }, + "abc068b4979b0ea64a62d3b7aa897d73810dc533": { + "balance": "0x6acb3df27e1f880000" + }, + "abc45f84db7382dde54c5f7d8938c42f4f3a3bc4": { + "balance": "0xad78ebc5ac6200000" + }, + "abc4caeb474d4627cb6eb456ecba0ecd08ed8ae1": { + "balance": "0xd5967be4fc3f100000" + }, + "abc74706964960dfe0dca3dca79e9216056f1cf4": { + "balance": "0x878678326eac9000000" + }, + "abc9a99e8a2148a55a6d82bd51b98eb5391fdbaf": { + "balance": "0x14542ba12a337c00000" + }, + "abcdbc8f1dd13af578d4a4774a62182bedf9f9be": { + "balance": "0x1fcc27bc459d20000" + }, + "abd154903513b8da4f019f68284b0656a1d0169b": { + "balance": "0x3635c9adc5dea00000" + }, + "abd21eff954fc6a7de26912a7cbb303a6607804e": { + "balance": "0x523c9aa696eb940000" + }, + "abd4d6c1666358c0406fdf3af248f78ece830104": { + "balance": "0x727de34a24f9000000" + }, + "abd9605b3e91acfd777830d16463478ae0fc7720": { + "balance": "0x73f75d1a085ba0000" + }, + "abdc9f1bcf4d19ee96591030e772c334302f7d83": { + "balance": "0x87e5e11a81cb5f80000" + }, + "abde147b2af789eaa586547e66c4fa2664d328a4": { + "balance": "0xd6b6081f34c128000" + }, + "abe07ced6ac5ddf991eff6c3da226a741bd243fe": { + "balance": "0x21e19e0c9bab2400000" + }, + "abf12fa19e82f76c718f01bdca0003674523ef30": { + "balance": "0x6c6b935b8bbd400000" + }, + "abf728cf9312f22128024e7046c251f5dc5901ed": { + "balance": "0x641e8a13563d8f80000" + }, + "abf8ffe0708a99b528cc1ed4e9ce4b0d0630be8c": { + "balance": "0x7ab5c2aeeee6380000" + }, + "abfcf5f25091ce57875fc674dcf104e2a73dd2f2": { + "balance": "0x11164759ffb320000" + }, + "abfe936425dcc7b74b955082bbaaf2a11d78bc05": { + "balance": "0x4be4e7267b6ae00000" + }, + "ac024f594f9558f04943618eb0e6b2ee501dc272": { + "balance": "0x6c6b935b8bbd400000" + }, + "ac122a03cd058c122e5fe17b872f4877f9df9572": { + "balance": "0x6ac5c62d9486070000" + }, + "ac142eda1157b9a9a64390df7e6ae694fac98905": { + "balance": "0xad78ebc5ac6200000" + }, + "ac1dfc984b71a19929a81d81f04a7cbb14073703": { + "balance": "0x2086ac351052600000" + }, + "ac21c1e5a3d7e0b50681679dd6c792dbca87decb": { + "balance": "0x152d02c7e14af6800000" + }, + "ac2889b5966f0c7f9edb42895cb69d1c04f923a2": { + "balance": "0x10f0cf064dd59200000" + }, + "ac28b5edea05b76f8c5f97084541277c96696a4c": { + "balance": "0x3635c9adc5dea00000" + }, + "ac2c8e09d06493a63858437bd20be01962450365": { + "balance": "0x678a932062e4180000" + }, + "ac2e766dac3f648f637ac6713fddb068e4a4f04d": { + "balance": "0xaadec983fcff40000" + }, + "ac3900298dd14d7cc96d4abb428da1bae213ffed": { + "balance": "0x53ca12974851c010000" + }, + "ac3da526cfce88297302f34c49ca520dc271f9b2": { + "balance": "0x2b5e3af16b18800000" + }, + "ac4460a76e6db2b9fcd152d9c7718d9ac6ed8c6f": { + "balance": "0xad78ebc5ac6200000" + }, + "ac4acfc36ed6094a27e118ecc911cd473e8fb91f": { + "balance": "0x61913e14403c0c0000" + }, + "ac4cc256ae74d624ace80db078b2207f57198f6b": { + "balance": "0x6c7974123f64a40000" + }, + "ac4ee9d502e7d2d2e99e59d8ca7d5f00c94b4dd6": { + "balance": "0x3635c9adc5dea00000" + }, + "ac52b77e15664814f39e4f271be641308d91d6cc": { + "balance": "0xbed1d0263d9f00000" + }, + "ac5999a89d2dd286d5a80c6dee7e86aad40f9e12": { + "balance": "0xd255d112e103a00000" + }, + "ac5f627231480d0d95302e6d89fc32cb1d4fe7e3": { + "balance": "0xad78ebc5ac6200000" + }, + "ac608e2bac9dd20728d2947effbbbf900a9ce94b": { + "balance": "0x1454b0db37568fc0000" + }, + "ac6d02e9a46b379fac4ac9b1d7b5d47bc850ce16": { + "balance": "0x5f68e8131ecf800000" + }, + "ac6f68e837cf1961cb14ab47446da168a16dde89": { + "balance": "0x487a9a304539440000" + }, + "ac77bdf00fd5985b5db12bbef800380abc2a0677": { + "balance": "0x3635c9adc5dea00000" + }, + "ac7e03702723cb16ee27e22dd0b815dc2d5cae9f": { + "balance": "0x3635c9adc5dea000000" + }, + "ac8b509aefea1dbfaf2bb33500d6570b6fd96d51": { + "balance": "0x62a992e53a0af00000" + }, + "ac8e87ddda5e78fcbcb9fa7fc3ce038f9f7d2e34": { + "balance": "0x6c6b935b8bbd400000" + }, + "ac9fff68c61b011efbecf038ed72db97bb9e7281": { + "balance": "0x205b4dfa1ee74780000" + }, + "aca1e6bc64cc3180f620e94dc5b1bcfd8158e45d": { + "balance": "0x6c6b935b8bbd400000" + }, + "aca2a838330b17302da731d30db48a04f0f207c1": { + "balance": "0x487a9a304539440000" + }, + "acaaddcbf286cb0e215dda55598f7ff0f4ada5c6": { + "balance": "0x3635c9adc5dea00000" + }, + "acb94338554bc488cc88ae2d9d94080d6bdf8410": { + "balance": "0x3635c9adc5dea00000" + }, + "acbc2d19e06c3babbb5b6f052b6bf7fc37e07229": { + "balance": "0xad78ebc5ac6200000" + }, + "acbd185589f7a68a67aa4b1bd65077f8c64e4e21": { + "balance": "0xad78ebc5ac6200000" + }, + "acc062702c59615d3444ef6214b8862b009a02ed": { + "balance": "0x514fcb24ff9c500000" + }, + "acc0909fda2ea6b7b7a88db7a0aac868091ddbf6": { + "balance": "0x133765f1e26c78000" + }, + "acc1c78786ab4d2b3b277135b5ba123e0400486b": { + "balance": "0x44591d67fecc80000" + }, + "acc46a2a555c74ded4a2bd094e821b97843b40c0": { + "balance": "0x692ae8897081d00000" + }, + "acc59f3b30ceffc56461cc5b8df48902240e0e7b": { + "balance": "0x6c6b935b8bbd400000" + }, + "acce01e0a70610dc70bb91e9926fa9957f372fba": { + "balance": "0x1d1c5f3eda20c40000" + }, + "acd8dd91f714764c45677c63d852e56eb9eece2e": { + "balance": "0x6c6b935b8bbd400000" + }, + "ace2abb63b0604409fbde3e716d2876d44e8e5dd": { + "balance": "0x83d6c7aab63600000" + }, + "acec91ef6941cf630ba9a3e787a012f4a2d91dd4": { + "balance": "0x10f0cf064dd592000000" + }, + "ad0a4ae478e9636e88c604f242cf5439c6d45639": { + "balance": "0xbed1d0263d9f000000" + }, + "ad1799aad7602b4540cd832f9db5f11150f1687a": { + "balance": "0x6c6b935b8bbd400000" + }, + "ad1d68a038fd2586067ef6d135d9628e79c2c924": { + "balance": "0xfe09a5279e2abc0000" + }, + "ad2a5c00f923aaf21ab9f3fb066efa0a03de2fb2": { + "balance": "0x3635bb77cb4b860000" + }, + "ad3565d52b688added08168b2d3872d17d0a26ae": { + "balance": "0x56bc75e2d63100000" + }, + "ad377cd25eb53e83ae091a0a1d2b4516f484afde": { + "balance": "0x692ae8897081d00000" + }, + "ad414d29cb7ee973fec54e22a388491786cf5402": { + "balance": "0x2f6f10780d22cc00000" + }, + "ad44357e017e244f476931c7b8189efee80a5d0a": { + "balance": "0x1043561a8829300000" + }, + "ad57aa9d00d10c439b35efcc0becac2e3955c313": { + "balance": "0xad78ebc5ac6200000" + }, + "ad59a78eb9a74a7fbdaefafa82eada8475f07f95": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ad5a8d3c6478b69f657db3837a2575ef8e1df931": { + "balance": "0x20156e104c1b30000" + }, + "ad660dec825522a9f62fcec3c5b731980dc286ea": { + "balance": "0xa2a15d09519be00000" + }, + "ad6628352ed3390bafa86d923e56014cfcb360f4": { + "balance": "0x6c6b935b8bbd400000" + }, + "ad728121873f0456d0518b80ab6580a203706595": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ad732c976593eec4783b4e2ecd793979780bfedb": { + "balance": "0x6c6b935b8bbd400000" + }, + "ad7dd053859edff1cb6f9d2acbed6dd5e332426f": { + "balance": "0x6acb3df27e1f880000" + }, + "ad80d865b85c34d2e6494b2e7aefea6b9af184db": { + "balance": "0xd8d726b7177a800000" + }, + "ad8bfef8c68a4816b3916f35cb7bfcd7d3040976": { + "balance": "0x878678326eac9000000" + }, + "ad8e48a377695de014363a523a28b1a40c78f208": { + "balance": "0x3635c9adc5dea00000" + }, + "ad910a23d6850613654af786337ad2a70868ac6d": { + "balance": "0x6c68ccd09b022c0000" + }, + "ad927e03d1599a78ca2bf0cad2a183dceb71eac0": { + "balance": "0x6acb3df27e1f880000" + }, + "ad92ca066edb7c711dfc5b166192d1edf8e77185": { + "balance": "0x79f905c6fd34e800000" + }, + "ad94235fc3b3f47a2413af31e884914908ef0c45": { + "balance": "0x1b1b0142d815840000" + }, + "ad9e97a0482f353a05c0f792b977b6c7e811fa5f": { + "balance": "0xad78ebc5ac6200000" + }, + "ad9f4c890a3b511cee51dfe6cfd7f1093b76412c": { + "balance": "0x1b767cbfeb0ce40000" + }, + "adaa0e548c035affed64ca678a963fabe9a26bfd": { + "balance": "0x3cb71f51fc5580000" + }, + "adb948b1b6fefe207de65e9bbc2de98e605d0b57": { + "balance": "0x6c6b935b8bbd400000" + }, + "adc19ec835afe3e58d87dc93a8a9213c90451326": { + "balance": "0x6adbe5342282000000" + }, + "adc8228ef928e18b2a807d00fb3c6c79cd1d9e96": { + "balance": "0x13c69df334ee80000" + }, + "addb26317227f45c87a2cb90dc4cfd02fb23caf8": { + "balance": "0x3635c9adc5dea00000" + }, + "ade6f8163bf7c7bb4abe8e9893bd0cc112fe8872": { + "balance": "0x11c25d004d01f80000" + }, + "adeb204aa0c38e179e81a94ed8b3e7d53047c26b": { + "balance": "0x20f5b1eaad8d800000" + }, + "adeb52b604e5f77faaac88275b8d6b49e9f9f97f": { + "balance": "0x71426b00956ed20000" + }, + "adf1acfe99bc8c14b304c8d905ba27657b8a7bc4": { + "balance": "0x43c33c1937564800000" + }, + "adf85203c8376a5fde9815384a350c3879c4cb93": { + "balance": "0x3e31fc675815aa0000" + }, + "adff0d1d0b97471e76d789d2e49c8a74f9bd54ff": { + "balance": "0x65ea3db75546600000" + }, + "ae062c448618643075de7a0030342dced63dbad7": { + "balance": "0x2cc6cd8cc282b30000" + }, + "ae10e27a014f0d306baf266d4897c89aeee2e974": { + "balance": "0x43c33c1937564800000" + }, + "ae126b382cf257fad7f0bc7d16297e54cc7267da": { + "balance": "0x1043561a8829300000" + }, + "ae13a08511110f32e53be4127845c843a1a57c7b": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ae179a460db66326743d24e67523a57b246daf7f": { + "balance": "0x10007ae7ce5bbe40000" + }, + "ae222865799079aaf4f0674a0cdaab02a6d570ff": { + "balance": "0x6c6b935b8bbd400000" + }, + "ae239acffd4ebe2e1ba5b4170572dc79cc6533ec": { + "balance": "0x28a857425466f800000" + }, + "ae2f9c19ac76136594432393b0471d08902164d3": { + "balance": "0x25df05c6a897e40000" + }, + "ae34861d342253194ffc6652dfde51ab44cad3fe": { + "balance": "0x194608686316bd8000" + }, + "ae36f7452121913e800e0fcd1a65a5471c23846f": { + "balance": "0x8e3f50b173c100000" + }, + "ae3f98a443efe00f3e711d525d9894dc9a61157b": { + "balance": "0x1004e2e45fb7ee0000" + }, + "ae47e2609cfafe369d66d415d939de05081a9872": { + "balance": "0x5baecf025f9b6500000" + }, + "ae4f122e35c0b1d1e4069291457c83c07f965fa3": { + "balance": "0x3635c9adc5dea00000" + }, + "ae5055814cb8be0c117bb8b1c8d2b63b4698b728": { + "balance": "0x1bc932ec573a38000" + }, + "ae538c73c5b38d8d584d7ebdadefb15cabe48357": { + "balance": "0x3627e8f712373c0000" + }, + "ae57cc129a96a89981dac60d2ffb877d5dc5e432": { + "balance": "0x3c3a2394b396550000" + }, + "ae5aa1e6c2b60f6fd3efe721bb4a719cbe3d6f5d": { + "balance": "0x2b24c6b55a5e620000" + }, + "ae5c9bdad3c5c8a1220444aea5c229c1839f1d64": { + "balance": "0x19e2a4c818b9060000" + }, + "ae5ce3355a7ba9b332760c0950c2bc45a85fa9a0": { + "balance": "0x15af1d78b58c400000" + }, + "ae5d221afcd3d29355f508eadfca408ce33ca903": { + "balance": "0x152d02c7e14af6800000" + }, + "ae635bf73831119d2d29c0d04ff8f8d8d0a57a46": { + "balance": "0x487a9a304539440000" + }, + "ae648155a658370f929be384f7e001047e49dd46": { + "balance": "0x2df24ae32be20440000" + }, + "ae6f0c73fdd77c489727512174d9b50296611c4c": { + "balance": "0x14542ba12a337c00000" + }, + "ae70e69d2c4a0af818807b1a2705f79fd0b5dbc4": { + "balance": "0x35659ef93f0fc40000" + }, + "ae7739124ed153052503fc101410d1ffd8cd13b7": { + "balance": "0x3634fb9f1489a70000" + }, + "ae78bb849139a6ba38ae92a09a69601cc4cb62d1": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ae842210f44d14c4a4db91fc9d3b3b50014f7bf7": { + "balance": "0xd8d726b7177a800000" + }, + "ae842e81858ecfedf6506c686dc204ac15bf8b24": { + "balance": "0x22b1c8c1227a00000" + }, + "ae8954f8d6166de507cf61297d0fc7ca6b9e7128": { + "balance": "0x1043561a8829300000" + }, + "ae9ecd6bdd952ef497c0050ae0ab8a82a91898ce": { + "balance": "0x1a055690d9db80000" + }, + "ae9f5c3fbbe0c9bcbf1af8ff74ea280b3a5d8b08": { + "balance": "0x5dc892aa1131c80000" + }, + "aead88d689416b1c91f2364421375b7d3c70fb2e": { + "balance": "0x6c6b935b8bbd400000" + }, + "aeadfcd0978edad74a32bd01a0a51d37f246e661": { + "balance": "0xe18398e7601900000" + }, + "aeb916ebf49d0f86c13f7331cef19e129937512d": { + "balance": "0x2085655b8d1b0a0000" + }, + "aebd4f205de799b64b3564b256d42a711d37ef99": { + "balance": "0x3fcf8b4574f84e0000" + }, + "aec27ce2133e82d052520afb5c576d9f7eb93ed2": { + "balance": "0xdd04120ba09cfe60000" + }, + "aec27ff5d7f9ddda91183f46f9d52543b6cd2b2f": { + "balance": "0x18650127cc3dc80000" + }, + "aee49d68adedb081fd43705a5f78c778fb90de48": { + "balance": "0x1158e460913d00000" + }, + "aef5b12258a18dec07d5ec2e316574919d79d6d6": { + "balance": "0x6c6b935b8bbd400000" + }, + "aefcfe88c826ccf131d54eb4ea9eb80e61e1ee25": { + "balance": "0x126e72a69a50d00000" + }, + "af06f5fa6d1214ec43967d1bd4dde74ab814a938": { + "balance": "0x4c53ecdc18a600000" + }, + "af1148ef6c8e103d7530efc91679c9ac27000993": { + "balance": "0xad78ebc5ac6200000" + }, + "af203e229d7e6d419df4378ea98715515f631485": { + "balance": "0x6acb3df27e1f880000" + }, + "af2058c7282cf67c8c3cf930133c89617ce75d29": { + "balance": "0x177224aa844c7200000" + }, + "af26f7c6bf453e2078f08953e4b28004a2c1e209": { + "balance": "0x56bc75e2d63100000" + }, + "af3087e62e04bf900d5a54dc3e946274da92423b": { + "balance": "0x1158e460913d00000" + }, + "af3614dcb68a36e45a4e911e62796247222d595b": { + "balance": "0x7a81065f1103bc0000" + }, + "af3615c789d0b1152ad4db25fe5dcf222804cf62": { + "balance": "0x3635c9adc5dea00000" + }, + "af3cb5965933e7dad883693b9c3e15beb68a4873": { + "balance": "0x6c6b935b8bbd400000" + }, + "af4493e8521ca89d95f5267c1ab63f9f45411e1b": { + "balance": "0xad78ebc5ac6200000" + }, + "af4cf41785161f571d0ca69c94f8021f41294eca": { + "balance": "0x215f835bc769da80000" + }, + "af529bdb459cc185bee5a1c58bf7e8cce25c150d": { + "balance": "0xaadec983fcff40000" + }, + "af67fd3e127fd9dc36eb3fcd6a80c7be4f7532b2": { + "balance": "0x5a87e7d7f5f6580000" + }, + "af771039345a343001bc0f8a5923b126b60d509c": { + "balance": "0x35659ef93f0fc40000" + }, + "af7f79cb415a1fb8dbbd094607ee8d41fb7c5a3b": { + "balance": "0x21e19e0c9bab2400000" + }, + "af87d2371ef378957fbd05ba2f1d66931b01e2b8": { + "balance": "0x25f273933db5700000" + }, + "af880fc7567d5595cacce15c3fc14c8742c26c9e": { + "balance": "0x73f75d1a085ba0000" + }, + "af8e1dcb314c950d3687434d309858e1a8739cd4": { + "balance": "0xe7eeba3410b740000" + }, + "af992dd669c0883e5515d3f3112a13f617a4c367": { + "balance": "0x6c6b935b8bbd400000" + }, + "afa1d5ad38fed44759c05b8993c1aa0dace19f40": { + "balance": "0x4563918244f400000" + }, + "afa539586e4719174a3b46b9b3e663a7d1b5b987": { + "balance": "0x10f0cf064dd59200000" + }, + "afa6946effd5ff53154f82010253df47ae280ccc": { + "balance": "0x6acb3df27e1f880000" + }, + "afc8ebe8988bd4105acc4c018e546a1e8f9c7888": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "afcc7dbb8356d842d43ae7e23c8422b022a30803": { + "balance": "0x66ffcbfd5e5a3000000" + }, + "afd019ff36a09155346b69974815a1c912c90aa4": { + "balance": "0x6c6b935b8bbd400000" + }, + "afdac5c1cb56e245bf70330066a817eaafac4cd1": { + "balance": "0x1158e460913d00000" + }, + "afdd1b786162b8317e20f0e979f4b2ce486d765d": { + "balance": "0x1158e460913d00000" + }, + "aff1045adf27a1aa329461b24de1bae9948a698b": { + "balance": "0x1cf84a30a0a0c0000" + }, + "aff107960b7ec34ed690b665024d60838c190f70": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "aff11ccf699304d5f5862af86083451c26e79ae5": { + "balance": "0x6c5db2a4d815dc0000" + }, + "aff161740a6d909fe99c59a9b77945c91cc91448": { + "balance": "0x340aad21b3b700000" + }, + "affc99d5ebb4a84fe7788d97dce274b038240438": { + "balance": "0x10f0cf064dd59200000" + }, + "affea0473722cb7f0e0e86b9e11883bf428d8d54": { + "balance": "0x692ae8897081d00000" + }, + "b00996b0566ecb3e7243b8227988dcb352c21899": { + "balance": "0x28a857425466f800000" + }, + "b01e389b28a31d8e4995bdd7d7c81beeab1e4119": { + "balance": "0x3635c9adc5dea00000" + }, + "b02d062873334545cea29218e4057760590f7423": { + "balance": "0xacb6a1c7d93a880000" + }, + "b02fa29387ec12e37f6922ac4ce98c5b09e0b00f": { + "balance": "0x6c6b935b8bbd400000" + }, + "b036916bdacf94b69e5a8a65602975eb026104dd": { + "balance": "0x1158e460913d00000" + }, + "b041310fe9eed6864cedd4bee58df88eb4ed3cac": { + "balance": "0x21e19e0c9bab2400000" + }, + "b055af4cadfcfdb425cf65ba6431078f07ecd5ab": { + "balance": "0x56bc75e2d63100000" + }, + "b0571153db1c4ed7acaefe13ecdfdb72e7e4f06a": { + "balance": "0x110cff796ac195200000" + }, + "b06eab09a610c6a53d56a946b2c43487ac1d5b2d": { + "balance": "0x3635c9adc5dea00000" + }, + "b07249e055044a9155359a402937bbd954fe48b6": { + "balance": "0x56bc75e2d63100000" + }, + "b07618328a901307a1b7a0d058fcd5786e9e72fe": { + "balance": "0x667495d4a4330ce0000" + }, + "b079bb4d9866143a6da72ae7ac0022062981315c": { + "balance": "0x29331e6558f0e00000" + }, + "b07bcc085ab3f729f24400416837b69936ba8873": { + "balance": "0x6c6d84bccdd9ce0000" + }, + "b07bcf1cc5d4462e5124c965ecf0d70dc27aca75": { + "balance": "0x56bc75e2d631000000" + }, + "b07cb9c12405b711807543c4934465f87f98bd2d": { + "balance": "0x6c6b935b8bbd400000" + }, + "b07fdeaff91d4460fe6cd0e8a1b0bd8d22a62e87": { + "balance": "0x11d2529f3535ab00000" + }, + "b09fe6d4349b99bc37938054022d54fca366f7af": { + "balance": "0x2a5a058fc295ed000000" + }, + "b0aa00950c0e81fa3210173e729aaf163a27cd71": { + "balance": "0x878678326eac9000000" + }, + "b0ac4eff6680ee14169cdadbffdb30804f6d25f5": { + "balance": "0x6c6b935b8bbd400000" + }, + "b0b36af9aeeedf97b6b02280f114f13984ea3260": { + "balance": "0x35659ef93f0fc40000" + }, + "b0b779b94bfa3c2e1f587bcc9c7e21789222378f": { + "balance": "0x54069233bf7f780000" + }, + "b0baeb30e313776c4c6d247402ba4167afcda1cc": { + "balance": "0x6acb3df27e1f880000" + }, + "b0bb29a861ea1d424d45acd4bfc492fb8ed809b7": { + "balance": "0x4563918244f400000" + }, + "b0c1b177a220e41f7c74d07cde8569c21c75c2f9": { + "balance": "0x12f939c99edab800000" + }, + "b0c7ce4c0dc3c2bbb99cc1857b8a455f611711ce": { + "balance": "0xd8d726b7177a800000" + }, + "b0cef8e8fb8984a6019f01c679f272bbe68f5c77": { + "balance": "0x83d6c7aab63600000" + }, + "b0d32bd7e4e695b7b01aa3d0416f80557dba9903": { + "balance": "0x3739ff0f6e613300000" + }, + "b0d3c9872b85056ea0c0e6d1ecf7a77e3ce6ab85": { + "balance": "0x10f08eda8e555098000" + }, + "b0e469c886593815b3495638595daef0665fae62": { + "balance": "0x692ae8897081d00000" + }, + "b0e760bb07c081777345e0578e8bc898226d4e3b": { + "balance": "0x6c6b935b8bbd400000" + }, + "b1043004ec1941a8cf4f2b00b15700ddac6ff17e": { + "balance": "0x3635c9adc5dea00000" + }, + "b105dd3d987cffd813e9c8500a80a1ad257d56c6": { + "balance": "0x6c6acc67d7b1d40000" + }, + "b10fd2a647102f881f74c9fbc37da632949f2375": { + "balance": "0x22b1c8c1227a00000" + }, + "b115ee3ab7641e1aa6d000e41bfc1ec7210c2f32": { + "balance": "0x2c0bb3dd30c4e200000" + }, + "b1178ad47383c31c8134a1941cbcd474d06244e2": { + "balance": "0x3635c9adc5dea00000" + }, + "b1179589e19db9d41557bbec1cb24ccc2dec1c7f": { + "balance": "0x152d02c7e14af6800000" + }, + "b119e79aa9b916526581cbf521ef474ae84dcff4": { + "balance": "0x4fba1001e5befe0000" + }, + "b11fa7fb270abcdf5a2eab95aa30c4b53636efbf": { + "balance": "0x2b5e3af16b18800000" + }, + "b124bcb6ffa430fcae2e86b45f27e3f21e81ee08": { + "balance": "0x6c6b935b8bbd400000" + }, + "b129a5cb7105fe810bd895dc7206a991a4545488": { + "balance": "0x1a055690d9db80000" + }, + "b12ed07b8a38ad5506363fc07a0b6d799936bdaf": { + "balance": "0x21e19e0c9bab2400000" + }, + "b134c004391ab4992878337a51ec242f42285742": { + "balance": "0x6c6b935b8bbd400000" + }, + "b13f93af30e8d7667381b2b95bc1a699d5e3e129": { + "balance": "0x16c4abbebea0100000" + }, + "b1459285863ea2db3759e546ceb3fb3761f5909c": { + "balance": "0x3cd72a894087e08000" + }, + "b146a0b925553cf06fcaf54a1b4dfea621290757": { + "balance": "0x6c6e59e67c78540000" + }, + "b14a7aaa8f49f2fb9a8102d6bbe4c48ae7c06fb2": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "b14bbeff70720975dc6191b2a44ff49f2672873c": { + "balance": "0x7c0860e5a80dc0000" + }, + "b14cc8de33d6338236539a489020ce4655a32bc6": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "b14ddb0386fb606398b8cc47565afae00ff1d66a": { + "balance": "0xa12aff083e66f00000" + }, + "b153f828dd076d4a7c1c2574bb2dee1a44a318a8": { + "balance": "0x15af1d78b58c400000" + }, + "b1540e94cff3465cc3d187e7c8e3bdaf984659e2": { + "balance": "0xa215e44390e3330000" + }, + "b158db43fa62d30e65f3d09bf781c7b67372ebaa": { + "balance": "0x6c5db2a4d815dc0000" + }, + "b161725fdcedd17952d57b23ef285b7e4b1169e8": { + "balance": "0x2b6dfed3664958000" + }, + "b16479ba8e7df8f63e1b95d149cd8529d735c2da": { + "balance": "0x2de33a6aac32548000" + }, + "b166e37d2e501ae73c84142b5ffb5aa655dd5a99": { + "balance": "0x6c5db2a4d815dc0000" + }, + "b183ebee4fcb42c220e47774f59d6c54d5e32ab1": { + "balance": "0x56f7a9c33c04d10000" + }, + "b188078444027e386798a8ae68698919d5cc230d": { + "balance": "0xe7eeba3410b740000" + }, + "b1896a37e5d8825a2d01765ae5de629977de8352": { + "balance": "0xad78ebc5ac6200000" + }, + "b18e67a5050a1dc9fb190919a33da838ef445014": { + "balance": "0x1158e460913d00000" + }, + "b1a2b43a7433dd150bb82227ed519cd6b142d382": { + "balance": "0x946d620d744b880000" + }, + "b1c0d08b36e184f9952a4037e3e53a667d070a4e": { + "balance": "0x3635c9adc5dea00000" + }, + "b1c328fb98f2f19ab6646f0a7c8c566fda5a8540": { + "balance": "0x878678326eac900000" + }, + "b1c751786939bba0d671a677a158c6abe7265e46": { + "balance": "0x21e19e0c9bab2400000" + }, + "b1cd4bdfd104489a026ec99d597307a04279f173": { + "balance": "0x43c33c1937564800000" + }, + "b1cf94f8091505055f010ab4bac696e0ca0f67a1": { + "balance": "0x55a6e79ccd1d300000" + }, + "b1d6b01b94d854fe8b374aa65e895cf22aa2560e": { + "balance": "0x32f51edbaaa3300000" + }, + "b1dba5250ba9625755246e067967f2ad2f0791de": { + "balance": "0x10f0cf064dd592000000" + }, + "b1e2dd95e39ae9775c55aeb13f12c2fa233053ba": { + "balance": "0x6c6b935b8bbd400000" + }, + "b1e6e810c24ab0488de9e01e574837829f7c77d0": { + "balance": "0x15af1d78b58c400000" + }, + "b1e9c5f1d21e61757a6b2ee75913fc5a1a4101c3": { + "balance": "0x6c6b935b8bbd400000" + }, + "b203d29e6c56b92699c4b92d1f6f84648dc4cfbc": { + "balance": "0x15af1d78b58c400000" + }, + "b216dc59e27c3d7279f5cd5bb2becfb2606e14d9": { + "balance": "0x15af1d78b58c400000" + }, + "b21b7979bf7c5ca01fa82dd640b41c39e6c6bc75": { + "balance": "0x6c6acc67d7b1d40000" + }, + "b223bf1fbf80485ca2b5567d98db7bc3534dd669": { + "balance": "0xd8d726b7177a800000" + }, + "b22d5055d9623135961e6abd273c90deea16a3e7": { + "balance": "0x4be4e7267b6ae00000" + }, + "b22dadd7e1e05232a93237baed98e0df92b1869e": { + "balance": "0x6c6b935b8bbd400000" + }, + "b234035f7544463ce1e22bc553064684c513cd51": { + "balance": "0xd89fa3dc48dcf0000" + }, + "b247cf9c72ec482af3eaa759658f793d670a570c": { + "balance": "0x31708ae00454400000" + }, + "b2676841ee9f2d31c172e82303b0fe9bbf9f1e09": { + "balance": "0xad78ebc5ac6200000" + }, + "b279c7d355c2880392aad1aa21ee867c3b3507df": { + "balance": "0x445be3f2ef87940000" + }, + "b27c1a24204c1e118d75149dd109311e07c073ab": { + "balance": "0xa80d24677efef00000" + }, + "b28181a458a440f1c6bb1de8400281a3148f4c35": { + "balance": "0x14620c57dddae00000" + }, + "b28245037cb192f75785cb86cbfe7c930da258b0": { + "balance": "0x3635c9adc5dea000000" + }, + "b287f7f8d8c3872c1b586bcd7d0aedbf7e732732": { + "balance": "0x1158e460913d00000" + }, + "b28bb39f3466517cd46f979cf59653ee7d8f152e": { + "balance": "0x18650127cc3dc80000" + }, + "b28dbfc6499894f73a71faa00abe0f4bc9d19f2a": { + "balance": "0x56bc75e2d63100000" + }, + "b2968f7d35f208871631c6687b3f3daeabc6616c": { + "balance": "0x875c47f289f760000" + }, + "b29f5b7c1930d9f97a115e067066f0b54db44b3b": { + "balance": "0x3635c9adc5dea00000" + }, + "b2a144b1ea67b9510f2267f9da39d3f93de26642": { + "balance": "0x6c6b935b8bbd400000" + }, + "b2a2c2111612fb8bbb8e7dd9378d67f1a384f050": { + "balance": "0x1158e460913d00000" + }, + "b2a498f03bd7178bd8a789a00f5237af79a3e3f8": { + "balance": "0x41bad155e6512200000" + }, + "b2aa2f1f8e93e79713d92cea9ffce9a40af9c82d": { + "balance": "0x6c6b935b8bbd400000" + }, + "b2b516fdd19e7f3864b6d2cf1b252a4156f1b03b": { + "balance": "0x2e983c76115fc0000" + }, + "b2b7cdb4ff4b61d5b7ce0b2270bbb5269743ec04": { + "balance": "0x6c6b935b8bbd400000" + }, + "b2bdbedf95908476d7148a370cc693743628057f": { + "balance": "0xd8d726b7177a800000" + }, + "b2bfaa58b5196c5cb7f89de15f479d1838de713d": { + "balance": "0x1236efcbcbb340000" + }, + "b2c53efa33fe4a3a1a80205c73ec3b1dbcad0602": { + "balance": "0x6801dab35918938000" + }, + "b2d0360515f17daba90fcbac8205d569b915d6ac": { + "balance": "0x14542ba12a337c00000" + }, + "b2d1e99af91231858e7065dd1918330dc4c747d5": { + "balance": "0x3894f0e6f9b9f700000" + }, + "b2d9ab9664bcf6df203c346fc692fd9cbab9205e": { + "balance": "0x17be78976065180000" + }, + "b2ddb786d3794e270187d0451ad6c8b79e0e8745": { + "balance": "0x15af1d78b58c400000" + }, + "b2e085fddd1468ba07415b274e734e11237fb2a9": { + "balance": "0x56bc75e2d63100000" + }, + "b2e9d76bf50fc36bf7d3944b63e9ca889b699968": { + "balance": "0x9032ea62b74b100000" + }, + "b2f9c972c1e9737755b3ff1b3088738396395b26": { + "balance": "0x43c33c1937564800000" + }, + "b2fc84a3e50a50af02f94da0383ed59f71ff01d7": { + "balance": "0x65a4da25d3016c00000" + }, + "b3050beff9de33c80e1fa15225e28f2c413ae313": { + "balance": "0x25f273933db5700000" + }, + "b31196714a48dff726ea9433cd2912f1a414b3b3": { + "balance": "0x914878a8c05ee00000" + }, + "b3145b74506d1a8d047cdcdc55392a7b5350799a": { + "balance": "0x1b6229741c0d3d5d8000" + }, + "b320834836d1dbfda9e7a3184d1ad1fd4320ccc0": { + "balance": "0x3635c9adc5dea00000" + }, + "b323dcbf2eddc5382ee4bbbb201ca3931be8b438": { + "balance": "0x6c6b935b8bbd400000" + }, + "b32400fd13c5500917cb037b29fe22e7d5228f2d": { + "balance": "0x878678326eac9000000" + }, + "b325674c01e3f7290d5226339fbeac67d221279f": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "b32825d5f3db249ef4e85cc4f33153958976e8bc": { + "balance": "0x1b2df9d219f5798000" + }, + "b32af3d3e8d075344926546f2e32887bf93b16bd": { + "balance": "0xad78ebc5ac6200000" + }, + "b32f1c2689a5ce79f1bc970b31584f1bcf2283e7": { + "balance": "0x1158e460913d00000" + }, + "b33c0323fbf9c26c1d8ac44ef74391d0804696da": { + "balance": "0x1158e460913d00000" + }, + "b34f04b8db65bba9c26efc4ce6efc50481f3d65d": { + "balance": "0x43c33c1937564800000" + }, + "b3557d39b5411b84445f5f54f38f62d2714d0087": { + "balance": "0x2086ac351052600000" + }, + "b358e97c70b605b1d7d729dfb640b43c5eafd1e7": { + "balance": "0x43c33c1937564800000" + }, + "b35e8a1c0dac7e0e66dbac736a592abd44012561": { + "balance": "0xcfce55aa12b30000" + }, + "b3667894b7863c068ad344873fcff4b5671e0689": { + "balance": "0x43c33c1937564800000" + }, + "b3717731dad65132da792d876030e46ac227bb8a": { + "balance": "0x3635c9adc5dea00000" + }, + "b3731b046c8ac695a127fd79d0a5d5fa6ae6d12e": { + "balance": "0x6c4fd1ee246e780000" + }, + "b37c2b9f50637bece0ca959208aefee6463ba720": { + "balance": "0x15af1d78b58c400000" + }, + "b388b5dfecd2c5e4b596577c642556dbfe277855": { + "balance": "0x1158e460913d00000" + }, + "b38c4e537b5df930d65a74d043831d6b485bbde4": { + "balance": "0x15af1d78b58c400000" + }, + "b39139576194a0866195151f33f2140ad1cc86cf": { + "balance": "0x152d02c7e14af6800000" + }, + "b39f4c00b2630cab7db7295ef43d47d501e17fd7": { + "balance": "0xd8d726b7177a800000" + }, + "b3a64b1176724f5409e1414a3523661baee74b4a": { + "balance": "0x16368ff4ff9c10000" + }, + "b3a6bd41f9d9c3201e050b87198fbda399342210": { + "balance": "0xc461e1dd1029b58000" + }, + "b3a8c2cb7d358e5739941d945ba9045a023a8bbb": { + "balance": "0x3635c9adc5dea00000" + }, + "b3ae54fba09d3ee1d6bdd1e957923919024c35fa": { + "balance": "0x38d2cee65b22a8000" + }, + "b3b7f493b44a2c8d80ec78b1cdc75a652b73b06c": { + "balance": "0x6c6b935b8bbd400000" + }, + "b3c228731d186d2ded5b5fbe004c666c8e469b86": { + "balance": "0x19274b259f6540000" + }, + "b3c260609b9df4095e6c5dff398eeb5e2df49985": { + "balance": "0xdc55fdb17647b0000" + }, + "b3c65b845aba6cd816fbaae983e0e46c82aa8622": { + "balance": "0x3635c9adc5dea00000" + }, + "b3c94811e7175b148b281c1a845bfc9bb6fbc115": { + "balance": "0xad78ebc5ac6200000" + }, + "b3e20eb4de18bd060221689894bee5aeb25351ee": { + "balance": "0x3fc80cce516598000" + }, + "b3e3c439069880156600c2892e448d4136c92d9b": { + "balance": "0x2e141ea081ca080000" + }, + "b3f82a87e59a39d0d2808f0751eb72c2329cdcc5": { + "balance": "0x10f0cf064dd59200000" + }, + "b3fc1d6881abfcb8becc0bb021b8b73b7233dd91": { + "balance": "0x2b5e3af16b1880000" + }, + "b40594c4f3664ef849cca6227b8a25aa690925ee": { + "balance": "0xd8d726b7177a800000" + }, + "b41eaf5d51a5ba1ba39bb418dbb54fab750efb1f": { + "balance": "0x3635c9adc5dea00000" + }, + "b424d68d9d0d00cec1938c854e15ffb880ba0170": { + "balance": "0xad78ebc5ac6200000" + }, + "b4256273962bf631d014555cc1da0dcc31616b49": { + "balance": "0x6c6b935b8bbd400000" + }, + "b43067fe70d9b55973ba58dc64dd7f311e554259": { + "balance": "0xad78ebc5ac6200000" + }, + "b43657a50eecbc3077e005d8f8d94f377876bad4": { + "balance": "0x1ec1b3a1ff75a0000" + }, + "b43c27f7a0a122084b98f483922541c8836cee2c": { + "balance": "0x26c29e47c4844c0000" + }, + "b4413576869c08f9512ad311fe925988a52d3414": { + "balance": "0x21e19e0c9bab2400000" + }, + "b44605552471a6eee4daab71ff3bb41326d473e0": { + "balance": "0x2d7e3d51ba53d00000" + }, + "b447571dacbb3ecbb6d1cf0b0c8f3838e52324e2": { + "balance": "0x1a318667fb4058000" + }, + "b44783c8e57b480793cbd69a45d90c7b4f0c48ac": { + "balance": "0x1158e460913d00000" + }, + "b44815a0f28e569d0e921a4ade8fb2642526497a": { + "balance": "0x302379bf2ca2e0000" + }, + "b4496ddb27799a222457d73979116728e8a1845b": { + "balance": "0x8d819ea65fa62f8000" + }, + "b4524c95a7860e21840296a616244019421c4aba": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "b45cca0d36826662683cf7d0b2fdac687f02d0c4": { + "balance": "0x3635c9adc5dea00000" + }, + "b46440c797a556e04c7d9104660491f96bb076bf": { + "balance": "0xcec76f0e71520000" + }, + "b46ace865e2c50ea4698d216ab455dff5a11cd72": { + "balance": "0x3635c9adc5dea00000" + }, + "b46d1182e5aacaff0d26b2fcf72f3c9ffbcdd97d": { + "balance": "0xaa2a603cdd7f2c0000" + }, + "b48921c9687d5510744584936e8886bdbf2df69b": { + "balance": "0x3635c9adc5dea00000" + }, + "b498bb0f520005b6216a4425b75aa9adc52d622b": { + "balance": "0xd8d726b7177a800000" + }, + "b4b11d109f608fa8edd3fea9f8c315649aeb3d11": { + "balance": "0x10f0cf064dd59200000" + }, + "b4b14bf45455d0ab0803358b7524a72be1a2045b": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "b4b185d943ee2b58631e33dff5af6854c17993ac": { + "balance": "0x3635c9adc5dea00000" + }, + "b4bf24cb83686bc469869fefb044b909716993e2": { + "balance": "0x6c6b935b8bbd400000" + }, + "b4c20040ccd9a1a3283da4d4a2f365820843d7e2": { + "balance": "0x3635c9adc5dea00000" + }, + "b4c8170f7b2ab536d1d9a25bdd203ae1288dc3d5": { + "balance": "0xad78ebc5ac6200000" + }, + "b4d82f2e69943f7de0f5f7743879406fac2e9cec": { + "balance": "0x22b1c8c1227a00000" + }, + "b4dd460cd016725a64b22ea4f8e06e06674e033e": { + "balance": "0x1231bb8748547a80000" + }, + "b4dd5499daeb2507fb2de12297731d4c72b16bb0": { + "balance": "0x1158e460913d00000" + }, + "b5046cb3dc1dedbd364514a2848e44c1de4ed147": { + "balance": "0x37b7d9bb820405e0000" + }, + "b508f987b2de34ae4cf193de85bff61389621f88": { + "balance": "0x14542ba12a337c00000" + }, + "b50955aa6e341571986608bdc891c2139f540cdf": { + "balance": "0x6acb3df27e1f880000" + }, + "b50c149a1906fad2786ffb135aab501737e9e56f": { + "balance": "0x150894e849b3900000" + }, + "b50c9f5789ae44e2dce017c714caf00c830084c2": { + "balance": "0x155bd9307f9fe80000" + }, + "b514882c979bb642a80dd38754d5b8c8296d9a07": { + "balance": "0x33c5499031720c0000" + }, + "b51ddcb4dd4e8ae6be336dd9654971d9fec86b41": { + "balance": "0x16d464f83de2948000" + }, + "b51e558eb5512fbcfa81f8d0bd938c79ebb5242b": { + "balance": "0x26c29e47c4844c0000" + }, + "b523fff9749871b35388438837f7e6e0dea9cb6b": { + "balance": "0x6c6b935b8bbd400000" + }, + "b52dfb45de5d74e3df208332bc571c809b8dcf32": { + "balance": "0x14542ba12a337c00000" + }, + "b535f8db879fc67fec58824a5cbe6e5498aba692": { + "balance": "0x678a932062e4180000" + }, + "b537d36a70eeb8d3e5c80de815225c1158cb92c4": { + "balance": "0x5150ae84a8cdf00000" + }, + "b53bcb174c2518348b818aece020364596466ba3": { + "balance": "0x6c6b935b8bbd400000" + }, + "b5493ef173724445cf345c035d279ba759f28d51": { + "balance": "0x1158e460913d00000" + }, + "b553d25d6b5421e81c2ad05e0b8ba751f8f010e3": { + "balance": "0x6c6b935b8bbd400000" + }, + "b55474ba58f0f2f40e6cbabed4ea176e011fcad6": { + "balance": "0x6acb3df27e1f880000" + }, + "b555d00f9190cc3677aef314acd73fdc39399259": { + "balance": "0x6c6b935b8bbd400000" + }, + "b557ab9439ef50d237b553f02508364a466a5c03": { + "balance": "0xad78ebc5ac6200000" + }, + "b56a780028039c81caf37b6775c620e786954764": { + "balance": "0x6c6b935b8bbd400000" + }, + "b56ad2aec6c8c3f19e1515bbb7dd91285256b639": { + "balance": "0x3635c9adc5dea00000" + }, + "b57413060af3f14eb479065f1e9d19b3757ae8cc": { + "balance": "0x22b1c8c1227a00000" + }, + "b57549bfbc9bdd18f736b22650e48a73601fa65c": { + "balance": "0x182d7e4cfda0380000" + }, + "b577b6befa054e9c040461855094b002d7f57bd7": { + "balance": "0x1823f3cf621d23400000" + }, + "b57b04fa23d1203fae061eac4542cb60f3a57637": { + "balance": "0xa5aa85009e39c0000" + }, + "b5870ce342d43343333673038b4764a46e925f3e": { + "balance": "0x3635c9adc5dea00000" + }, + "b587b44a2ca79e4bc1dd8bfdd43a207150f2e7e0": { + "balance": "0x222c8eb3ff66400000" + }, + "b589676d15a04448344230d4ff27c95edf122c49": { + "balance": "0x3635c9adc5dea00000" + }, + "b58b52865ea55d8036f2fab26098b352ca837e18": { + "balance": "0xfc936392801c0000" + }, + "b5906b0ae9a28158e8ac550e39da086ee3157623": { + "balance": "0xad78ebc5ac6200000" + }, + "b5a4679685fa14196c2e9230c8c4e33bffbc10e2": { + "balance": "0x4be4e7267b6ae00000" + }, + "b5a589dd9f4071dbb6fba89b3f5d5dae7d96c163": { + "balance": "0x6c6b935b8bbd400000" + }, + "b5a606f4ddcbb9471ec67f658caf2b00ee73025e": { + "balance": "0xea756ea92afc740000" + }, + "b5ad5157dda921e6bafacd9086ae73ae1f611d3f": { + "balance": "0x6c6b935b8bbd400000" + }, + "b5add1e7809f7d03069bfe883b0a932210be8712": { + "balance": "0x3635c9adc5dea00000" + }, + "b5ba29917c78a1d9e5c5c713666c1e411d7f693a": { + "balance": "0xa80d24677efef00000" + }, + "b5c816a8283ca4df68a1a73d63bd80260488df08": { + "balance": "0xad78ebc5ac6200000" + }, + "b5cac5ed03477d390bb267d4ebd46101fbc2c3da": { + "balance": "0xaadec983fcff40000" + }, + "b5cdbc4115406f52e5aa85d0fea170d2979cc7ba": { + "balance": "0x487a9a304539440000" + }, + "b5d9934d7b292bcf603b2880741eb760288383a0": { + "balance": "0xe7c2518505060000" + }, + "b5dd50a15da34968890a53b4f13fe1af081baaaa": { + "balance": "0xd8d726b7177a800000" + }, + "b5fa8184e43ed3e0b8ab91216461b3528d84fd09": { + "balance": "0x914878a8c05ee00000" + }, + "b5fb7ea2ddc1598b667a9d57dd39e85a38f35d56": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "b600429752f399c80d0734744bae0a022eca67c6": { + "balance": "0x1158e460913d00000" + }, + "b600feab4aa96c537504d96057223141692c193a": { + "balance": "0x15af1d78b58c400000" + }, + "b6047cdf932db3e4045f4976122341537ed5961e": { + "balance": "0x1158e460913d00000" + }, + "b615e940143eb57f875893bc98a61b3d618c1e8c": { + "balance": "0x1158e460913d00000" + }, + "b61c34fcacda701a5aa8702459deb0e4ae838df8": { + "balance": "0x7695a92c20d6fe00000" + }, + "b63064bd3355e6e07e2d377024125a33776c4afa": { + "balance": "0x8375a2abcca24400000" + }, + "b635a4bc71fb28fdd5d2c322983a56c284426e69": { + "balance": "0x93739534d28680000" + }, + "b646df98b49442746b61525c81a3b04ba3106250": { + "balance": "0x6acb3df27e1f880000" + }, + "b65941d44c50d24666670d364766e991c02e11c2": { + "balance": "0x2086ac351052600000" + }, + "b65bd780c7434115162027565223f44e5498ff8c": { + "balance": "0x43c30fb0884a96c0000" + }, + "b66411e3a02dedb726fa79107dc90bc1cae64d48": { + "balance": "0x6c6b935b8bbd400000" + }, + "b66675142e3111a1c2ea1eb2419cfa42aaf7a234": { + "balance": "0x3635c9adc5dea00000" + }, + "b66f92124b5e63035859e390628869dbdea9485e": { + "balance": "0x215f835bc769da80000" + }, + "b672734afcc224e2e609fc51d4f059732744c948": { + "balance": "0x1004e2e45fb7ee0000" + }, + "b6771b0bf3427f9ae7a93e7c2e61ee63941fdb08": { + "balance": "0x3fb26692954bfc00000" + }, + "b67a80f170197d96cdcc4ab6cba627b4afa6e12c": { + "balance": "0x821ab0d44149800000" + }, + "b68899e7610d4c93a23535bcc448945ba1666f1c": { + "balance": "0xad78ebc5ac6200000" + }, + "b6a82933c9eadabd981e5d6d60a6818ff806e36b": { + "balance": "0x15af1d78b58c400000" + }, + "b6aacb8cb30bab2ae4a2424626e6e12b02d04605": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "b6b34a263f10c3d2eceb0acc559a7b2ab85ce565": { + "balance": "0xd8d726b7177a800000" + }, + "b6bfe1c3ef94e1846fb9e3acfe9b50c3e9069233": { + "balance": "0x6c6acc67d7b1d40000" + }, + "b6cd7432d5161be79768ad45de3e447a07982063": { + "balance": "0xd8d726b7177a800000" + }, + "b6ce4dc560fc73dc69fb7a62e388db7e72ea764f": { + "balance": "0x345df169e9a3580000" + }, + "b6decf82969819ba02de29b9b593f21b64eeda0f": { + "balance": "0x281d901f4fdd100000" + }, + "b6e6c3222b6b6f9be2875d2a89f127fb64100fe2": { + "balance": "0x1b21d5323cc30200000" + }, + "b6e8afd93dfa9af27f39b4df06076710bee3dfab": { + "balance": "0x15af1d78b58c40000" + }, + "b6f78da4f4d041b3bc14bc5ba519a5ba0c32f128": { + "balance": "0x247dd32c3fe195048000" + }, + "b6fb39786250081426a342c70d47ee521e5bc563": { + "balance": "0x32d26d12e980b600000" + }, + "b70dba9391682b4a364e77fe99256301a6c0bf1f": { + "balance": "0xad78ebc5ac6200000" + }, + "b71623f35107cf7431a83fb3d204b29ee0b1a7f4": { + "balance": "0x11164759ffb320000" + }, + "b71a13ba8e95167b80331b52d69e37054fe7a826": { + "balance": "0xad78ebc5ac6200000" + }, + "b71b62f4b448c02b1201cb5e394ae627b0a560ee": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "b72220ade364d0369f2d2da783ca474d7b9b34ce": { + "balance": "0x1b1ab319f5ec750000" + }, + "b7230d1d1ff2aca366963914a79df9f7c5ea2c98": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "b7240af2af90b33c08ae9764103e35dce3638428": { + "balance": "0x1cadd2fe9686e638000" + }, + "b727a9fc82e1cffc5c175fa1485a9befa2cdbdd1": { + "balance": "0x3627e8f712373c0000" + }, + "b72c2a011c0df50fbb6e28b20ae1aad217886790": { + "balance": "0xd8d726b7177a800000" + }, + "b7382d37db0398ac72410cf9813de9f8e1ec8dad": { + "balance": "0x3636c25e66ece70000" + }, + "b73b4ff99eb88fd89b0b6d57a9bc338e886fa06a": { + "balance": "0x1bc16d674ec800000" + }, + "b73d6a77559c86cf6574242903394bacf96e3570": { + "balance": "0x4f1a77ccd3ba00000" + }, + "b74372dbfa181dc9242f39bf1d3731dffe2bdacf": { + "balance": "0x6c6b935b8bbd400000" + }, + "b7479dab5022c4d5dbaaf8de171b4e951dd1a457": { + "balance": "0x4563918244f400000" + }, + "b749b54e04d5b19bdcedfb84da7701ab478c27ae": { + "balance": "0x914878a8c05ee00000" + }, + "b74ed2666001c16333cf7af59e4a3d4860363b9c": { + "balance": "0xa7ebd5e4363a00000" + }, + "b75149e185f6e3927057739073a1822ae1cf0df2": { + "balance": "0xd8d8583fa2d52f0000" + }, + "b753a75f9ed10b21643a0a3dc0517ac96b1a4068": { + "balance": "0x15c8185b2c1ff40000" + }, + "b756ad52f3bf74a7d24c67471e0887436936504c": { + "balance": "0x43c33c1937564800000" + }, + "b7576e9d314df41ec5506494293afb1bd5d3f65d": { + "balance": "0x1158e460913d00000" + }, + "b758896f1baa864f17ebed16d953886fee68aae6": { + "balance": "0x3635c9adc5dea00000" + }, + "b768b5234eba3a9968b34d6ddb481c8419b3655d": { + "balance": "0xcfce55aa12b30000" + }, + "b782bfd1e2de70f467646f9bc09ea5b1fcf450af": { + "balance": "0xe7eeba3410b740000" + }, + "b7a2c103728b7305b5ae6e961c94ee99c9fe8e2b": { + "balance": "0xa968163f0a57b400000" + }, + "b7a31a7c38f3db09322eae11d2272141ea229902": { + "balance": "0x6c6b935b8bbd400000" + }, + "b7a6791c16eb4e2162f14b6537a02b3d63bfc602": { + "balance": "0x2a526391ac93760000" + }, + "b7a7f77c348f92a9f1100c6bd829a8ac6d7fcf91": { + "balance": "0x62a992e53a0af00000" + }, + "b7c077946674ba9341fb4c747a5d50f5d2da6415": { + "balance": "0x3635c9adc5dea00000" + }, + "b7c0d0cc0b4d342d4062bac624ccc3c70cc6da3f": { + "balance": "0xd8d726b7177a800000" + }, + "b7c9f12b038e73436d17e1c12ffe1aeccdb3f58c": { + "balance": "0x1d460162f516f00000" + }, + "b7cc6b1acc32d8b295df68ed9d5e60b8f64cb67b": { + "balance": "0x1043561a8829300000" + }, + "b7ce684b09abda53389a875369f71958aeac3bdd": { + "balance": "0x6c6b935b8bbd400000" + }, + "b7d12e84a2e4c4a6345af1dd1da9f2504a2a996e": { + "balance": "0xad78ebc5ac6200000" + }, + "b7d252ee9402b0eef144295f0e69f0db586c0871": { + "balance": "0x23c757072b8dd00000" + }, + "b7d581fe0af1ec383f3b3c416783f385146a7612": { + "balance": "0x43c33c1937564800000" + }, + "b7f67314cb832e32e63b15a40ce0d7ffbdb26985": { + "balance": "0x398279264a818d0000" + }, + "b8040536958d5998ce4bec0cfc9c2204989848e9": { + "balance": "0x52ea70d498fd50a0000" + }, + "b8310a16cc6abc465007694b930f978ece1930bd": { + "balance": "0x281d901f4fdd100000" + }, + "b834acf3015322c58382eeb2b79638906e88b6de": { + "balance": "0x5150ae84a8cdf000000" + }, + "b84b53d0bb125656cddc52eb852ab71d7259f3d5": { + "balance": "0x3635c9adc5dea000000" + }, + "b84c8b9fd33ece00af9199f3cf5fe0cce28cd14a": { + "balance": "0xcf152640c5c8300000" + }, + "b85218f342f8012eda9f274e63ce2152b2dcfdab": { + "balance": "0xa80d24677efef00000" + }, + "b8555010776e3c5cb311a5adeefe9e92bb9a64b9": { + "balance": "0xd8d726b7177a800000" + }, + "b85f26dd0e72d9c29ebaf697a8af77472c2b58b5": { + "balance": "0x28519acc7190c700000" + }, + "b85ff03e7b5fc422981fae5e9941dacbdaba7584": { + "balance": "0x487a9a304539440000" + }, + "b86607021b62d340cf2652f3f95fd2dc67698bdf": { + "balance": "0x10f0cf064dd59200000" + }, + "b87de1bcd29269d521b8761cc39cfb4319d2ead5": { + "balance": "0x3635c9adc5dea00000" + }, + "b87f5376c2de0b6cc3c179c06087aa473d6b4674": { + "balance": "0x487a9a304539440000" + }, + "b884add88d83dc564ab8e0e02cbdb63919aea844": { + "balance": "0x6c6b935b8bbd400000" + }, + "b88a37c27f78a617d5c091b7d5b73a3761e65f2a": { + "balance": "0x6c6b935b8bbd400000" + }, + "b8947822d5ace7a6ad8326e95496221e0be6b73d": { + "balance": "0x1158e460913d00000" + }, + "b89c036ed7c492879921be41e10ca1698198a74c": { + "balance": "0x62a992e53a0af00000" + }, + "b89f4632df5909e58b2a9964f74feb9a3b01e0c5": { + "balance": "0x48875bcc6e7cbeb8000" + }, + "b8a79c84945e47a9c3438683d6b5842cff7684b1": { + "balance": "0x6c6b935b8bbd400000" + }, + "b8a979352759ba09e35aa5935df175bff678a108": { + "balance": "0x1158e460913d00000" + }, + "b8ab39805bd821184f6cbd3d2473347b12bf175c": { + "balance": "0x6685ac1bfe32c0000" + }, + "b8ac117d9f0dba80901445823c4c9d4fa3fedc6e": { + "balance": "0x3564c4427a8fc7d8000" + }, + "b8bc9bca7f71b4ed12e620438d620f53c114342f": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "b8bedd576a4b4c2027da735a5bc3f533252a1808": { + "balance": "0x6c6b935b8bbd400000" + }, + "b8c2703d8c3f2f44c584bc10e7c0a6b64c1c097e": { + "balance": "0x12cddb8ead6f9f80000" + }, + "b8cc0f060aad92d4eb8b36b3b95ce9e90eb383d7": { + "balance": "0x1fc3842bd1f071c00000" + }, + "b8d2ddc66f308c0158ae3ccb7b869f7d199d7b32": { + "balance": "0x2dcbf4840eca000000" + }, + "b8d389e624a3a7aebce4d3e5dbdf6cdc29932aed": { + "balance": "0xad78ebc5ac6200000" + }, + "b8d531a964bcea13829620c0ced72422dadb4cca": { + "balance": "0x93715cc5ab8a70000" + }, + "b8d5c324a8209d7c8049d0d4aede02ba80ab578b": { + "balance": "0x393928629fff75e8000" + }, + "b8f20005b61352ffa7699a1b52f01f5ab39167f1": { + "balance": "0x21e19e0c9bab2400000" + }, + "b8f30758faa808dbc919aa7b425ec922b93b8129": { + "balance": "0x3636d7af5ec98e0000" + }, + "b9013c51bd078a098fae05bf2ace0849c6be17a5": { + "balance": "0x4563918244f400000" + }, + "b9144b677c2dc614ceefdf50985f1183208ea64c": { + "balance": "0x6c6b935b8bbd400000" + }, + "b916b1a01cdc4e56e7657715ea37e2a0f087d106": { + "balance": "0x826e3181e027068000" + }, + "b91d9e916cd40d193db60e79202778a0087716fc": { + "balance": "0x15f1ba7f4716200000" + }, + "b9231eb26e5f9e4b4d288f03906704fab96c87d6": { + "balance": "0x42bf06b78ed3b500000" + }, + "b92427ad7578b4bfe20a9f63a7c5506d5ca12dc8": { + "balance": "0x6c6b935b8bbd400000" + }, + "b927abd2d28aaaa24db31778d27419df8e1b04bb": { + "balance": "0x17e11c2a26f478000" + }, + "b94d47b3c052a5e50e4261ae06a20f45d8eee297": { + "balance": "0x6c6b935b8bbd400000" + }, + "b95396daaa490df2569324fcc6623be052f132ca": { + "balance": "0x6c6b935b8bbd400000" + }, + "b959dce02e91d9db02b1bd8b7d17a9c41a97af09": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "b95c9b10aa981cf4a67a71cc52c504dee8cf58bd": { + "balance": "0xd8d726b7177a800000" + }, + "b95cfda8465ba9c2661b249fc3ab661bdfa35ff0": { + "balance": "0x114a4e79a2c2108000" + }, + "b96841cabbc7dbd69ef0cf8f81dff3c8a5e21570": { + "balance": "0x28a857425466f800000" + }, + "b97a6733cd5fe99864b3b33460d1672434d5cafd": { + "balance": "0x6c65bbaa46c2cf8000" + }, + "b981ad5e6b7793a23fc6c1e8692eb2965d18d0da": { + "balance": "0x21e18d2c821c7520000" + }, + "b98ca31785ef06be49a1e47e864f60d076ca472e": { + "balance": "0xd8d726b7177a800000" + }, + "b9920fd0e2c735c256463caa240fb7ac86a93dfa": { + "balance": "0x5f68e8131ecf800000" + }, + "b992a967308c02b98af91ee760fd3b6b4824ab0e": { + "balance": "0x6c6b935b8bbd400000" + }, + "b9a985501ee950829b17fae1c9cf348c3156542c": { + "balance": "0xff17517ca9a620000" + }, + "b9b0a3219a3288d9b35b091b14650b8fe23dce2b": { + "balance": "0x2f6f10780d22cc00000" + }, + "b9cf71b226583e3a921103a5316f855a65779d1b": { + "balance": "0x5150ae84a8cdf000000" + }, + "b9e90c1192b3d5d3e3ab0700f1bf655f5dd4347a": { + "balance": "0x1b19e50b44977c0000" + }, + "b9fd3833e88e7cf1fa9879bdf55af4b99cd5ce3f": { + "balance": "0x3635c9adc5dea00000" + }, + "ba0249e01d945bef93ee5ec61925e03c5ca509fd": { + "balance": "0xd8d726b7177a800000" + }, + "ba0f39023bdb29eb1862a9f9059cab5d306e662f": { + "balance": "0x6c6b935b8bbd400000" + }, + "ba10f2764290f875434372f79dbf713801caac01": { + "balance": "0x33c5499031720c0000" + }, + "ba1531fb9e791896bcf3a80558a359f6e7c144bd": { + "balance": "0xd5967be4fc3f100000" + }, + "ba176dbe3249e345cd4fa967c0ed13b24c47e586": { + "balance": "0x15aef9f1c31c7f0000" + }, + "ba1f0e03cb9aa021f4dcebfa94e5c889c9c7bc9e": { + "balance": "0x6d190c475169a200000" + }, + "ba1fcaf223937ef89e85675503bdb7ca6a928b78": { + "balance": "0x22b1c8c1227a000000" + }, + "ba24fc436753a739db2c8d40e6d4d04c528e86fa": { + "balance": "0x2c0bb3dd30c4e200000" + }, + "ba42f9aace4c184504abf5425762aca26f71fbdc": { + "balance": "0x207077dd8a79c0000" + }, + "ba469aa5c386b19295d4a1b5473b540353390c85": { + "balance": "0x6c6b935b8bbd400000" + }, + "ba6440aeb3737b8ef0f1af9b0c15f4c214ffc7cf": { + "balance": "0x3635c9adc5dea00000" + }, + "ba6d31b9a261d640b5dea51ef2162c3109f1eba8": { + "balance": "0x10f0cf064dd59200000" + }, + "ba70e8b4759c0c3c82cc00ac4e9a94dd5bafb2b8": { + "balance": "0x3043fa33c412d70000" + }, + "ba8a63f3f40de4a88388bc50212fea8e064fbb86": { + "balance": "0x6c6b935b8bbd400000" + }, + "ba8e46d69d2e2343d86c60d82cf42c2041a0c1c2": { + "balance": "0x56bc75e2d63100000" + }, + "baa4b64c2b15b79f5f204246fd70bcbd86e4a92a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "bac8922c4acc7d2cb6fd59a14eb45cf3e702214b": { + "balance": "0x2b5e3af16b18800000" + }, + "bad235d5085dc7b068a67c412677b03e1836884c": { + "balance": "0x6c6b935b8bbd400000" + }, + "bad4425e171c3e72975eb46ac0a015db315a5d8f": { + "balance": "0x6c6b935b8bbd400000" + }, + "badc2aef9f5951a8d78a6b35c3d0b3a4e6e2e739": { + "balance": "0x14542ba12a337c00000" + }, + "bade43599e02f84f4c3014571c976b13a36c65ab": { + "balance": "0xd8d726b7177a800000" + }, + "bae9b82f7299631408659dd74e891cb8f3860fe5": { + "balance": "0x6acb3df27e1f880000" + }, + "bb0366a7cfbd3445a70db7fe5ae34885754fd468": { + "balance": "0x14def2c42ebd6400000" + }, + "bb076aac92208069ea318a31ff8eeb14b7e996e3": { + "balance": "0x813ca56906d340000" + }, + "bb0857f1c911b24b86c8a70681473fe6aaa1cce2": { + "balance": "0x56bc75e2d63100000" + }, + "bb19bf91cbad74cceb5f811db27e411bc2ea0656": { + "balance": "0xf43fc2c04ee00000" + }, + "bb27c6a7f91075475ab229619040f804c8ec7a6a": { + "balance": "0x21e19e0c9bab2400000" + }, + "bb371c72c9f0316cea2bd9c6fbb4079e775429ef": { + "balance": "0x5f68e8131ecf800000" + }, + "bb3b010b18e6e2be1135871026b7ba15ea0fde24": { + "balance": "0x2207c80309b77700000" + }, + "bb3b9005f46fd2ca3b30162599928c77d9f6b601": { + "balance": "0x1b1ae7f2b1bf7db0000" + }, + "bb3fc0a29c034d710812dcc775c8cab9d28d6975": { + "balance": "0x39d4e844d1cf5f0000" + }, + "bb48eaf516ce2dec3e41feb4c679e4957641164f": { + "balance": "0xcf152640c5c8300000" + }, + "bb4b4a4b548070ff41432c9e08a0ca6fa7bc9f76": { + "balance": "0x2e141ea081ca080000" + }, + "bb56a404723cff20d0685488b05a02cdc35aacaa": { + "balance": "0x1158e460913d00000" + }, + "bb618e25221ad9a740b299ed1406bc3934b0b16d": { + "balance": "0x3635c9adc5dea00000" + }, + "bb61a04bffd57c10470d45c39103f64650347616": { + "balance": "0x3635c9adc5dea00000" + }, + "bb6823a1bd819f13515538264a2de052b4442208": { + "balance": "0x16368ff4ff9c10000" + }, + "bb6c284aac8a69b75cddb00f28e145583b56bece": { + "balance": "0x6c6b935b8bbd400000" + }, + "bb75cb5051a0b0944b4673ca752a97037f7c8c15": { + "balance": "0xad78ebc5ac6200000" + }, + "bb993b96ee925ada7d99d786573d3f89180ce3aa": { + "balance": "0x6c6b935b8bbd400000" + }, + "bba3c68004248e489573abb2743677066b24c8a7": { + "balance": "0x6c6b935b8bbd400000" + }, + "bba4fac3c42039d828e742cde0efffe774941b39": { + "balance": "0x6c6ad382d4fb610000" + }, + "bba8ab22d2fedbcfc63f684c08afdf1c175090b5": { + "balance": "0x55f29f37e4e3b8000" + }, + "bba976f1a1215f7512871892d45f7048acd356c8": { + "balance": "0x6c6b935b8bbd400000" + }, + "bbab000b0408ed015a37c04747bc461ab14e151b": { + "balance": "0x14542ba12a337c00000" + }, + "bbabf6643beb4bd01c120bd0598a0987d82967d1": { + "balance": "0xb5328178ad0f2a0000" + }, + "bbb4ee1d82f2e156442cc93338a2fc286fa28864": { + "balance": "0x4a4491bd6dcd280000" + }, + "bbb5a0f4802c8648009e8a6998af352cde87544f": { + "balance": "0x52d542804f1ce0000" + }, + "bbb643d2187b364afc10a6fd368d7d55f50d1a3c": { + "balance": "0x3635c9adc5dea00000" + }, + "bbb8ffe43f98de8eae184623ae5264e424d0b8d7": { + "balance": "0x5d53ffde928080000" + }, + "bbbd6ecbb5752891b4ceb3cce73a8f477059376f": { + "balance": "0x1f399b1438a100000" + }, + "bbbf39b1b67995a42241504f9703d2a14a515696": { + "balance": "0x55a6e79ccd1d300000" + }, + "bbc8eaff637e94fcc58d913c7770c88f9b479277": { + "balance": "0xad78ebc5ac6200000" + }, + "bbc9d8112e5beb02dd29a2257b1fe69b3536a945": { + "balance": "0x6c6b935b8bbd400000" + }, + "bbca65b3266ea2fb73a03f921635f912c7bede00": { + "balance": "0x6acb3df27e1f880000" + }, + "bbf84292d954acd9e4072fb860b1504106e077ae": { + "balance": "0x5150ae84a8cdf00000" + }, + "bbf85aaaa683738f073baef44ac9dc34c4c779ea": { + "balance": "0x6c6b935b8bbd400000" + }, + "bbf8616d97724af3def165d0e28cda89b800009a": { + "balance": "0x62ef12e2b17618000" + }, + "bbfe0a830cace87b7293993a7e9496ce64f8e394": { + "balance": "0x14542ba12a337c00000" + }, + "bc0ca4f217e052753614d6b019948824d0d8688b": { + "balance": "0x15af1d78b58c400000" + }, + "bc0e8745c3a549445c2be900f52300804ab56289": { + "balance": "0x7029bf5dd4c53b28000" + }, + "bc0f98598f88056a26339620923b8f1eb074a9fd": { + "balance": "0xad78ebc5ac6200000" + }, + "bc1609d685b76b48ec909aa099219022f89b2ccd": { + "balance": "0x40138b917edfb80000" + }, + "bc171e53d17ac9b61241ae436deec7af452e7496": { + "balance": "0x121ea68c114e5100000" + }, + "bc1b021a78fde42d9b5226d6ec26e06aa3670090": { + "balance": "0x4563918244f400000" + }, + "bc1e80c181616342ebb3fb3992072f1b28b802c6": { + "balance": "0xd8d726b7177a800000" + }, + "bc237148d30c13836ffa2cad520ee4d2e5c4eeff": { + "balance": "0x6acb3df27e1f880000" + }, + "bc46d537cf2edd403565bde733b2e34b215001bd": { + "balance": "0x43c33c1937564800000" + }, + "bc4e471560c99c8a2a4b1b1ad0c36aa6502b7c4b": { + "balance": "0x28a857425466f800000" + }, + "bc62b3096a91e7dc11a1592a293dd2542150d751": { + "balance": "0x3635c9adc5dea00000" + }, + "bc69a0d2a31c3dbf7a9122116901b2bdfe9802a0": { + "balance": "0xa2a15d09519be00000" + }, + "bc6b58364bf7f1951c309e0cba0595201cd73f9a": { + "balance": "0x62401a457e45f80000" + }, + "bc73f7b1ca3b773b34249ada2e2c8a9274cc17c2": { + "balance": "0x6c6b935b8bbd400000" + }, + "bc7afc8477412274fc265df13c054473427d43c6": { + "balance": "0x70c95920ce3250000" + }, + "bc967fe4418c18b99858966d870678dca2b88879": { + "balance": "0x1d9cbdd8d7ed2100000" + }, + "bc999e385c5aebcac8d6f3f0d60d5aa725336d0d": { + "balance": "0x6c6b935b8bbd400000" + }, + "bc9c95dfab97a574cea2aa803b5caa197cef0cff": { + "balance": "0x16c4abbebea0100000" + }, + "bc9e0ec6788f7df4c7fc210aacd220c27e45c910": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "bca3ffd4683fba0ad3bbc90734b611da9cfb457e": { + "balance": "0xad78ebc5ac6200000" + }, + "bcaed0acb6a76f113f7c613555a2c3b0f5bf34a5": { + "balance": "0xa7ebd5e4363a00000" + }, + "bcaf347918efb2d63dde03e39275bbe97d26df50": { + "balance": "0x56bc75e2d63100000" + }, + "bcb422dc4dd2aae94abae95ea45dd1731bb6b0ba": { + "balance": "0x18424f5f0b1b4e0000" + }, + "bcbd31252ec288f91e298cd812c92160e738331a": { + "balance": "0x6b1bc2cac09a590000" + }, + "bcbf6ba166e2340db052ea23d28029b0de6aa380": { + "balance": "0xd255d112e103a00000" + }, + "bcc84597b91e73d5c5b4d69c80ecf146860f779a": { + "balance": "0xed70b5e9c3f2f00000" + }, + "bcc9593b2da6df6a34d71b1aa38dacf876f95b88": { + "balance": "0x1158e460913d00000" + }, + "bcd95ef962462b6edfa10fda87d72242fe3edb5c": { + "balance": "0x121d06e12fff988000" + }, + "bcd99edc2160f210a05e3a1fa0b0434ced00439b": { + "balance": "0x6c6b935b8bbd400000" + }, + "bcdfacb9d9023c3417182e9100e8ea1d373393a3": { + "balance": "0x3342d60dff1960000" + }, + "bce13e22322acfb355cd21fd0df60cf93add26c6": { + "balance": "0xad78ebc5ac6200000" + }, + "bce40475d345b0712dee703d87cd7657fc7f3b62": { + "balance": "0x1a420db02bd7d580000" + }, + "bcedc4267ccb89b31bb764d7211171008d94d44d": { + "balance": "0xad78ebc5ac6200000" + }, + "bcfc98e5c82b6adb180a3fcb120b9a7690c86a3f": { + "balance": "0x6acb3df27e1f880000" + }, + "bd043b67c63e60f841ccca15b129cdfe6590c8e3": { + "balance": "0xad78ebc5ac6200000" + }, + "bd047ff1e69cc6b29ad26497a9a6f27a903fc4dd": { + "balance": "0x2ee449550898e40000" + }, + "bd08e0cddec097db7901ea819a3d1fd9de8951a2": { + "balance": "0x1158e460913d00000" + }, + "bd09126c891c4a83068059fe0e15796c4661a9f4": { + "balance": "0x2b5e3af16b18800000" + }, + "bd0c5cd799ebc48642ef97d74e8e429064fee492": { + "balance": "0x11ac28a8c729580000" + }, + "bd17eed82b9a2592019a1b1b3c0fbad45c408d22": { + "balance": "0xd8d726b7177a80000" + }, + "bd1803370bddb129d239fd16ea8526a6188ae58e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "bd2b70fecc37640f69514fc7f3404946aad86b11": { + "balance": "0x410d586a20a4c00000" + }, + "bd3097a79b3c0d2ebff0e6e86ab0edadbed47096": { + "balance": "0x5a87e7d7f5f6580000" + }, + "bd325d4029e0d8729f6d399c478224ae9e7ae41e": { + "balance": "0xd255d112e103a00000" + }, + "bd432a3916249b4724293af9146e49b8280a7f2a": { + "balance": "0xd8d726b7177a800000" + }, + "bd47f5f76e3b930fd9485209efa0d4763da07568": { + "balance": "0x3635c9adc5dea00000" + }, + "bd4b60faec740a21e3071391f96aa534f7c1f44e": { + "balance": "0x9ddc1e3b901180000" + }, + "bd4bd5b122d8ef7b7c8f0667450320db2116142e": { + "balance": "0x2086ac351052600000" + }, + "bd51ee2ea143d7b1d6b77e7e44bdd7da12f485ac": { + "balance": "0x477e06ccb2b9280000" + }, + "bd59094e074f8d79142ab1489f148e32151f2089": { + "balance": "0x1158e460913d00000" + }, + "bd5a8c94bd8be6470644f70c8f8a33a8a55c6341": { + "balance": "0xad78ebc5ac6200000" + }, + "bd5e473abce8f97a6932f77c2facaf9cc0a00514": { + "balance": "0x3c9258a106a6b70000" + }, + "bd5f46caab2c3d4b289396bbb07f203c4da82530": { + "balance": "0x4563918244f400000" + }, + "bd66ffedb530ea0b2e856dd12ac2296c31fe29e0": { + "balance": "0xad78ebc5ac6200000" + }, + "bd67d2e2f82da8861341bc96a2c0791fddf39e40": { + "balance": "0xad7c07947c8fb0000" + }, + "bd6a474d66345bcdd707594adb63b30c7822af54": { + "balance": "0xd8d726b7177a800000" + }, + "bd723b289a7367b6ece2455ed61edb49670ab9c4": { + "balance": "0x10f0cdea164213f8000" + }, + "bd73c3cbc26a175062ea0320dd84b253bce64358": { + "balance": "0x155bd9307f9fe80000" + }, + "bd7419dc2a090a46e2873d7de6eaaad59e19c479": { + "balance": "0x170bcb671759f080000" + }, + "bd8765f41299c7f479923c4fd18f126d7229047d": { + "balance": "0xd8d726b7177a800000" + }, + "bd93e550403e2a06113ed4c3fba1a8913b19407e": { + "balance": "0x6c6b935b8bbd400000" + }, + "bd9e56e902f4be1fc8768d8038bac63e2acbbf8e": { + "balance": "0x36356633ebd8ea0000" + }, + "bda4be317e7e4bed84c0495eee32d607ec38ca52": { + "balance": "0x7d32277978ef4e8000" + }, + "bdb60b823a1173d45a0792245fb496f1fd3301cf": { + "balance": "0x6c6b935b8bbd400000" + }, + "bdbaf6434d40d6355b1e80e40cc4ab9c68d96116": { + "balance": "0x56bc75e2d63100000" + }, + "bdc02cd4330c93d6fbda4f6db2a85df22f43c233": { + "balance": "0x6c6b935b8bbd400000" + }, + "bdc461462b6322b462bdb33f22799e8108e2417d": { + "balance": "0x243d4d18229ca20000" + }, + "bdc739a699700b2e8e2c4a4c7b058a0e513ddebe": { + "balance": "0x6c6b935b8bbd400000" + }, + "bdc74873af922b9df474853b0fa7ff0bf8c82695": { + "balance": "0xd8c9460063d31c0000" + }, + "bdca2a0ff34588af625fa8e28fc3015ab5a3aa00": { + "balance": "0x7ed73f773552fc0000" + }, + "bdd3254e1b3a6dc6cc2c697d45711aca21d516b2": { + "balance": "0x6c6b935b8bbd400000" + }, + "bddfa34d0ebf1b04af53b99b82494a9e3d8aa100": { + "balance": "0x28a857425466f800000" + }, + "bde4c73f969b89e9ceae66a2b51844480e038e9a": { + "balance": "0x3635c9adc5dea00000" + }, + "bde9786a84e75b48f18e726dd78d70e4af3ed802": { + "balance": "0x1369fb96128ac480000" + }, + "bded11612fb5c6da99d1e30e320bc0995466141e": { + "balance": "0x15af1d78b58c400000" + }, + "bded7e07d0711e684de65ac8b2ab57c55c1a8645": { + "balance": "0x2009c5c8bf6fdc0000" + }, + "bdf693f833c3fe471753184788eb4bfe4adc3f96": { + "balance": "0x6acb3df27e1f880000" + }, + "bdf6e68c0cd7584080e847d72cbb23aad46aeb1d": { + "balance": "0x6acb3df27e1f880000" + }, + "be0a2f385f09dbfce96732e12bb40ac349871ba8": { + "balance": "0x574c115e02b8be0000" + }, + "be0c2a80b9de084b172894a76cf4737a4f529e1a": { + "balance": "0x6c6acc67d7b1d40000" + }, + "be1cd7f4c472070968f3bde268366b21eeea8321": { + "balance": "0xe91a7cd19fa3b00000" + }, + "be2346a27ff9b702044f500deff2e7ffe6824541": { + "balance": "0x1158e460913d00000" + }, + "be2471a67f6047918772d0e36839255ed9d691ae": { + "balance": "0xd8d726b7177a800000" + }, + "be2b2280523768ea8ac35cd9e888d60a719300d4": { + "balance": "0x6c6b935b8bbd400000" + }, + "be2b326e78ed10e550fee8efa8f8070396522f5a": { + "balance": "0x857e0d6f1da76a00000" + }, + "be305a796e33bbf7f9aeae6512959066efda1010": { + "balance": "0x24dce54d34a1a000000" + }, + "be478e8e3dde6bd403bb2d1c657c4310ee192723": { + "balance": "0x1ab2cf7c9f87e20000" + }, + "be4e7d983f2e2a636b1102ec7039efebc842e98d": { + "balance": "0x393ef1a5127c80000" + }, + "be4fd073617022b67f5c13499b827f763639e4e3": { + "balance": "0x6c6b935b8bbd400000" + }, + "be525a33ea916177f17283fca29e8b350b7f530b": { + "balance": "0x8f019aaf46e8780000" + }, + "be53322f43fbb58494d7cce19dda272b2450e827": { + "balance": "0xad7ceaf425c150000" + }, + "be538246dd4e6f0c20bf5ad1373c3b463a131e86": { + "balance": "0xad78ebc5ac6200000" + }, + "be5a60689998639ad75bc105a371743eef0f7940": { + "balance": "0x1b327c73e1257a0000" + }, + "be5cba8d37427986e8ca2600e858bb03c359520f": { + "balance": "0xa030dcebbd2f4c0000" + }, + "be60037e90714a4b917e61f193d834906703b13a": { + "balance": "0x5c283d410394100000" + }, + "be633a3737f68439bac7c90a52142058ee8e8a6f": { + "balance": "0x340aad21b3b7000000" + }, + "be659d85e7c34f8833ea7f488de1fbb5d4149bef": { + "balance": "0x1ebd23ad9d5bb720000" + }, + "be73274d8c5aa44a3cbefc8263c37ba121b20ad3": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "be86d0b0438419ceb1a038319237ba5206d72e46": { + "balance": "0x3634fb9f1489a70000" + }, + "be8d7f18adfe5d6cc775394989e1930c979d007d": { + "balance": "0x3635c9adc5dea00000" + }, + "be9186c34a52514abb9107860f674f97b821bd5b": { + "balance": "0x1ba01ee40603100000" + }, + "be935793f45b70d8045d2654d8dd3ad24b5b6137": { + "balance": "0x2fb474098f67c00000" + }, + "be98a77fd41097b34f59d7589baad021659ff712": { + "balance": "0x30ca024f987b900000" + }, + "be9b8c34b78ee947ff81472eda7af9d204bc8466": { + "balance": "0x821ab0d4414980000" + }, + "bea00df17067a43a82bc1daecafb6c14300e89e6": { + "balance": "0x62a992e53a0af00000" + }, + "bea0afc93aae2108a3fac059623bf86fa582a75e": { + "balance": "0x5c283d410394100000" + }, + "beb3358c50cf9f75ffc76d443c2c7f55075a0589": { + "balance": "0x90f534608a72880000" + }, + "beb4fd315559436045dcb99d49dcec03f40c42dc": { + "balance": "0x6c6b935b8bbd400000" + }, + "bec2e6de39c07c2bae556acfbee2c4728b9982e3": { + "balance": "0x1f0ff8f01daad40000" + }, + "bec6640f4909b58cbf1e806342961d607595096c": { + "balance": "0x6c6acc67d7b1d40000" + }, + "bec8caf7ee49468fee552eff3ac5234eb9b17d42": { + "balance": "0x6c6b935b8bbd400000" + }, + "becef61c1c442bef7ce04b73adb249a8ba047e00": { + "balance": "0x363b56c3a754c80000" + }, + "bed4649df646e2819229032d8868556fe1e053d3": { + "balance": "0xfc936392801c0000" + }, + "bed4c8f006a27c1e5f7ce205de75f516bfb9f764": { + "balance": "0x3635c9adc5dea000000" + }, + "bee8d0b008421954f92d000d390fb8f8e658eaee": { + "balance": "0x3635c9adc5dea00000" + }, + "beecd6af900c8b064afcc6073f2d85d59af11956": { + "balance": "0x6c6b935b8bbd400000" + }, + "beef94213879e02622142bea61290978939a60d7": { + "balance": "0x136857b32ad86048000" + }, + "bef07d97c3481f9d6aee1c98f9d91a180a32442b": { + "balance": "0x152d02c7e14af6800000" + }, + "befb448c0c5f683fb67ee570baf0db5686599751": { + "balance": "0x6acb3df27e1f880000" + }, + "bf05070c2c34219311c4548b2614a438810ded6d": { + "balance": "0x6c6b935b8bbd400000" + }, + "bf05ff5ecf0df2df887759fb8274d93238ac267d": { + "balance": "0x2b5e3af16b18800000" + }, + "bf09d77048e270b662330e9486b38b43cd781495": { + "balance": "0x5c539b7bf4ff28800000" + }, + "bf17f397f8f46f1bae45d187148c06eeb959fa4d": { + "balance": "0x3649c59624bb300000" + }, + "bf183641edb886ce60b8190261e14f42d93cce01": { + "balance": "0x15b3557f1937f8000" + }, + "bf2aea5a1dcf6ed3b5e8323944e983fedfd1acfb": { + "balance": "0x55a6e79ccd1d300000" + }, + "bf4096bc547dbfc4e74809a31c039e7b389d5e17": { + "balance": "0xd5967be4fc3f100000" + }, + "bf49c14898316567d8b709c2e50594b366c6d38c": { + "balance": "0x27bf38c6544df50000" + }, + "bf4c73a7ede7b164fe072114843654e4d8781dde": { + "balance": "0x6c6b935b8bbd400000" + }, + "bf50ce2e264b9fe2b06830617aedf502b2351b45": { + "balance": "0x3635c9adc5dea00000" + }, + "bf59aee281fa43fe97194351a9857e01a3b897b2": { + "balance": "0x2086ac351052600000" + }, + "bf68d28aaf1eeefef646b65e8cc8d190f6c6da9c": { + "balance": "0x6c6b935b8bbd400000" + }, + "bf6925c00751008440a6739a02bf2b6cdaab5e3a": { + "balance": "0x3635c9adc5dea00000" + }, + "bf7701fc6225d5a17815438a8941d21ebc5d059d": { + "balance": "0x65ea3db75546600000" + }, + "bf8b8005d636a49664f74275ef42438acd65ac91": { + "balance": "0xad78ebc5ac6200000" + }, + "bf92418a0c6c31244d220260cb3e867dd7b4ef49": { + "balance": "0x56900d33ca7fc0000" + }, + "bf9acd4445d9c9554689cabbbab18800ff1741c2": { + "balance": "0x3635c9adc5dea00000" + }, + "bf9f271f7a7e12e36dd2fe9facebf385fe6142bd": { + "balance": "0x366f84f7bb7840000" + }, + "bfa8c858df102cb12421008b0a31c4c7190ad560": { + "balance": "0xad78ebc5ac6200000" + }, + "bfaeb91067617dcf8b44172b02af615674835dba": { + "balance": "0x8b59e884813088000" + }, + "bfb0ea02feb61dec9e22a5070959330299c43072": { + "balance": "0x43c33c1937564800000" + }, + "bfbca418d3529cb393081062032a6e1183c6b2dc": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "bfbe05e88c9cbbcc0e92a405fac1d85de248ee24": { + "balance": "0x56bc75e2d63100000" + }, + "bfbfbcb656c2992be8fcde8219fbc54aadd59f29": { + "balance": "0x21e18d2c821c7520000" + }, + "bfc57aa666fae28e9f107a49cb5089a4e22151dd": { + "balance": "0x3635c9adc5dea00000" + }, + "bfcb9730246304700da90b4153e71141622e1c41": { + "balance": "0x3635c9adc5dea00000" + }, + "bfd93c90c29c07bc5fb5fc49aeea55a40e134f35": { + "balance": "0x5ede20f01a459800000" + }, + "bfe3a1fc6e24c8f7b3250560991f93cba2cf8047": { + "balance": "0x10f0cf064dd592000000" + }, + "bfe6bcb0f0c07852643324aa5df5fd6225abc3ca": { + "balance": "0x409e52b48369a0000" + }, + "bff5df769934b8943ca9137d0efef2fe6ebbb34e": { + "balance": "0x56bc75e2d63100000" + }, + "bffb6929241f788693273e7022e60e3eab1fe84f": { + "balance": "0x6c6b935b8bbd400000" + }, + "c0064f1d9474ab915d56906c9fb320a2c7098c9b": { + "balance": "0x13683f7f3c15d80000" + }, + "c007f0bdb6e7009202b7af3ea90902697c721413": { + "balance": "0xa2a0e43e7fb9830000" + }, + "c00ab080b643e1c2bae363e0d195de2efffc1c44": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c02077449a134a7ad1ef7e4d927affeceeadb5ae": { + "balance": "0xfc936392801c0000" + }, + "c02471e3fc2ea0532615a7571d493289c13c36ef": { + "balance": "0x1158e460913d00000" + }, + "c02d6eadeacf1b78b3ca85035c637bb1ce01f490": { + "balance": "0xd8d726b7177a800000" + }, + "c033b1325a0af45472c25527853b1f1c21fa35de": { + "balance": "0x6c6b935b8bbd400000" + }, + "c033be10cb48613bd5ebcb33ed4902f38b583003": { + "balance": "0xa2a15d09519be00000" + }, + "c0345b33f49ce27fe82cf7c84d141c68f590ce76": { + "balance": "0x3635c9adc5dea00000" + }, + "c03de42a109b657a64e92224c08dc1275e80d9b2": { + "balance": "0x1158e460913d00000" + }, + "c04069dfb18b096c7867f8bee77a6dc7477ad062": { + "balance": "0x90f534608a72880000" + }, + "c0413f5a7c2d9a4b8108289ef6ecd271781524f4": { + "balance": "0xa968163f0a57b400000" + }, + "c043f2452dcb9602ef62bd360e033dd23971fe84": { + "balance": "0x6c6b935b8bbd400000" + }, + "c04f4bd4049f044685b883b62959ae631d667e35": { + "balance": "0x13b80b99c5185700000" + }, + "c056d4bd6bf3cbacac65f8f5a0e3980b852740ae": { + "balance": "0x56bc75e2d63100000" + }, + "c05b740620f173f16e52471dc38b9c514a0b1526": { + "balance": "0x796e3ea3f8ab00000" + }, + "c069ef0eb34299abd2e32dabc47944b272334824": { + "balance": "0x68155a43676e00000" + }, + "c06cebbbf7f5149a66f7eb976b3e47d56516da2f": { + "balance": "0x6c6b935b8bbd400000" + }, + "c0725ec2bdc33a1d826071dea29d62d4385a8c25": { + "balance": "0x8a08513463aa6100000" + }, + "c07e3867ada096807a051a6c9c34cc3b3f4ad34a": { + "balance": "0x60f06620a849450000" + }, + "c0895efd056d9a3a81c3da578ada311bfb9356cf": { + "balance": "0xad78ebc5ac6200000" + }, + "c090fe23dcd86b358c32e48d2af91024259f6566": { + "balance": "0xad78ebc5ac6200000" + }, + "c09a66172aea370d9a63da04ff71ffbbfcff7f94": { + "balance": "0x6c6b935b8bbd400000" + }, + "c09e3cfc19f605ff3ec9c9c70e2540d7ee974366": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c0a02ab94ebe56d045b41b629b98462e3a024a93": { + "balance": "0x56bc75e2d63100000" + }, + "c0a39308a80e9e84aaaf16ac01e3b01d74bd6b2d": { + "balance": "0x7664ddd4c1c0b8000" + }, + "c0a6cbad77692a3d88d141ef769a99bb9e3c9951": { + "balance": "0x56bc75e2d63100000" + }, + "c0a7e8435dff14c25577739db55c24d5bf57a3d9": { + "balance": "0xa6dd90cae5114480000" + }, + "c0ae14d724832e2fce2778de7f7b8daf7b12a93e": { + "balance": "0x1158e460913d00000" + }, + "c0afb7d8b79370cfd663c68cc6b9702a37cd9eff": { + "balance": "0x3635c9adc5dea00000" + }, + "c0b0b7a8a6e1acdd05e47f94c09688aa16c7ad8d": { + "balance": "0x37b6d02ac76710000" + }, + "c0b3f244bca7b7de5b48a53edb9cbeab0b6d88c0": { + "balance": "0x13b80b99c5185700000" + }, + "c0c04d0106810e3ec0e54a19f2ab8597e69a573d": { + "balance": "0x2b5e3af16b1880000" + }, + "c0ca3277942e7445874be31ceb902972714f1823": { + "balance": "0xd8d726b7177a80000" + }, + "c0cbad3ccdf654da22cbcf5c786597ca1955c115": { + "balance": "0x6c6b935b8bbd400000" + }, + "c0cbf6032fa39e7c46ff778a94f7d445fe22cf30": { + "balance": "0x10ce1d3d8cb3180000" + }, + "c0e0b903088e0c63f53dd069575452aff52410c3": { + "balance": "0xa2a15d09519be00000" + }, + "c0e457bd56ec36a1246bfa3230fff38e5926ef22": { + "balance": "0x692ae8897081d00000" + }, + "c0ed0d4ad10de03435b153a0fc25de3b93f45204": { + "balance": "0xab4dcf399a3a600000" + }, + "c0f29ed0076611b5e55e130547e68a48e26df5e4": { + "balance": "0xa2a15d09519be00000" + }, + "c1132878235c5ddba5d9f3228b5236e47020dc6f": { + "balance": "0x3635c9adc5dea00000" + }, + "c1170dbaadb3dee6198ea544baec93251860fda5": { + "balance": "0x410d586a20a4c00000" + }, + "c126573d87b0175a5295f1dd07c575cf8cfa15f2": { + "balance": "0x21e19e0c9bab2400000" + }, + "c127aab59065a28644a56ba3f15e2eac13da2995": { + "balance": "0x2086ac351052600000" + }, + "c12b7f40df9a2f7bf983661422ab84c9c1f50858": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "c12cfb7b3df70fceca0ede263500e27873f8ed16": { + "balance": "0x3635c9adc5dea00000" + }, + "c12f881fa112b8199ecbc73ec4185790e614a20f": { + "balance": "0x6c6b935b8bbd400000" + }, + "c1384c6e717ebe4b23014e51f31c9df7e4e25b31": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c1438c99dd51ef1ca8386af0a317e9b041457888": { + "balance": "0xc1daf81d8a3ce0000" + }, + "c1631228efbf2a2e3a4092ee8900c639ed34fbc8": { + "balance": "0x33c5499031720c0000" + }, + "c175be3194e669422d15fee81eb9f2c56c67d9c9": { + "balance": "0xad78ebc5ac6200000" + }, + "c1827686c0169485ec15b3a7c8c01517a2874de1": { + "balance": "0x22b1c8c1227a00000" + }, + "c18ab467feb5a0aadfff91230ff056464d78d800": { + "balance": "0x6c6b935b8bbd400000" + }, + "c1950543554d8a713003f662bb612c10ad4cdf21": { + "balance": "0xfc936392801c0000" + }, + "c1a41a5a27199226e4c7eb198b031b59196f9842": { + "balance": "0xa5aa85009e39c0000" + }, + "c1b2a0fb9cad45cd699192cd27540b88d3384279": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c1b2aa8cb2bf62cdc13a47ecc4657facaa995f98": { + "balance": "0x363793fa96e6a68000" + }, + "c1b500011cfba95d7cd636e95e6cbf6167464b25": { + "balance": "0xad78ebc5ac6200000" + }, + "c1b9a5704d351cfe983f79abeec3dbbbae3bb629": { + "balance": "0x1158e460913d00000" + }, + "c1cbd2e2332a524cf219b10d871ccc20af1fb0fa": { + "balance": "0x3635c9adc5dea00000" + }, + "c1cdc601f89c0428b31302d187e0dc08ad7d1c57": { + "balance": "0x14542ba12a337c00000" + }, + "c1d4af38e9ba799040894849b8a8219375f1ac78": { + "balance": "0x43c33c1937564800000" + }, + "c1e1409ca52c25435134d006c2a6a8542dfb7273": { + "balance": "0x1dd1e4bd8d1ee0000" + }, + "c1eba5684aa1b24cba63150263b7a9131aeec28d": { + "balance": "0x1158e460913d00000" + }, + "c1ec81dd123d4b7c2dd9b4d438a7072c11dc874c": { + "balance": "0x6c6b935b8bbd400000" + }, + "c1f39bd35dd9cec337b96f47c677818160df37b7": { + "balance": "0x1158e460913d00000" + }, + "c1ffad07db96138c4b2a530ec1c7de29b8a0592c": { + "balance": "0xf43fc2c04ee00000" + }, + "c21fa6643a1f14c02996ad7144b75926e87ecb4b": { + "balance": "0x43c33c1937564800000" + }, + "c2340a4ca94c9678b7494c3c852528ede5ee529f": { + "balance": "0x2a36b05a3fd7c8000" + }, + "c239abdfae3e9af5457f52ed2b91fd0ab4d9c700": { + "balance": "0x6c6b935b8bbd400000" + }, + "c23b2f921ce4a37a259ee4ad8b2158d15d664f59": { + "balance": "0x1608995e8bd3f8000" + }, + "c24399b4bf86f7338fbf645e3b22b0e0b7973912": { + "balance": "0x6c6b935b8bbd400000" + }, + "c24ccebc2344cce56417fb684cf81613f0f4b9bd": { + "balance": "0x54069233bf7f780000" + }, + "c25266c7676632f13ef29be455ed948add567792": { + "balance": "0x487a9a304539440000" + }, + "c25cf826550c8eaf10af2234fef904ddb95213be": { + "balance": "0x3635c9adc5dea00000" + }, + "c2663f8145dbfec6c646fc5c49961345de1c9f11": { + "balance": "0x2567ac70392b880000" + }, + "c270456885342b640b4cfc1b520e1a544ee0d571": { + "balance": "0x62a992e53a0af00000" + }, + "c27376f45d21e15ede3b26f2655fcee02ccc0f2a": { + "balance": "0x1158e460913d00000" + }, + "c2779771f0536d79a8708f6931abc44b3035e999": { + "balance": "0x43c4f8300dcb3480000" + }, + "c27f4e08099d8cf39ee11601838ef9fc06d7fc41": { + "balance": "0x61093d7c2c6d380000" + }, + "c282e6993fbe7a912ea047153ffd9274270e285b": { + "balance": "0x7960b331247638000" + }, + "c2836188d9a29253e0cbda6571b058c289a0bb32": { + "balance": "0x6c6b935b8bbd400000" + }, + "c2aa74847e86edfdd3f3db22f8a2152feee5b7f7": { + "balance": "0x6f118886b784a20000" + }, + "c2b2cbe65bc6c2ee7a3c75b2e47c189c062e8d8b": { + "balance": "0x43c33c1937564800000" + }, + "c2bae4a233c2d85724f0dabebda0249d833e37d3": { + "balance": "0x10f0cf064dd59200000" + }, + "c2c13e72d268e7150dc799e7c6cf03c88954ced7": { + "balance": "0x25f273933db5700000" + }, + "c2cb1ada5da9a0423873814793f16144ef36b2f3": { + "balance": "0x48557e3b7017df0000" + }, + "c2d1778ef6ee5fe488c145f3586b6ebbe3fbb445": { + "balance": "0x3e1ff1e03b55a80000" + }, + "c2d9eedbc9019263d9d16cc5ae072d1d3dd9db03": { + "balance": "0x43c33c1937564800000" + }, + "c2e0584a71348cc314b73b2029b6230b92dbb116": { + "balance": "0x6c6b935b8bbd400000" + }, + "c2e2d498f70dcd0859e50b023a710a6d4b2133bd": { + "balance": "0x383911f00cbce10000" + }, + "c2ed5ffdd1add855a2692fe062b5d618742360d4": { + "balance": "0x410d586a20a4c00000" + }, + "c2ee91d3ef58c9d1a589844ea1ae3125d6c5ba69": { + "balance": "0x34957444b840e80000" + }, + "c2fafdd30acb6d6706e9293cb02641f9edbe07b5": { + "balance": "0x5100860b430f480000" + }, + "c2fd0bf7c725ef3e047e5ae1c29fe18f12a7299c": { + "balance": "0x487a9a304539440000" + }, + "c2fe7d75731f636dcd09dbda0671393ba0c82a7d": { + "balance": "0x77432217e683600000" + }, + "c3107a9af3322d5238df0132419131629539577d": { + "balance": "0x1ab4e464d414310000" + }, + "c3110be01dc9734cfc6e1ce07f87d77d1345b7e1": { + "balance": "0x10f0ce949e00f930000" + }, + "c32038ca52aee19745be5c31fcdc54148bb2c4d0": { + "balance": "0x2b5aad72c65200000" + }, + "c325c352801ba883b3226c5feb0df9eae2d6e653": { + "balance": "0xd5967be4fc3f100000" + }, + "c32ec7e42ad16ce3e2555ad4c54306eda0b26758": { + "balance": "0x6c6b935b8bbd400000" + }, + "c332df50b13c013490a5d7c75dbfa366da87b6d6": { + "balance": "0xd8d726b7177a800000" + }, + "c33acdb3ba1aab27507b86b15d67faf91ecf6293": { + "balance": "0x6c6b935b8bbd400000" + }, + "c33ece935a8f4ef938ea7e1bac87cb925d8490ca": { + "balance": "0x7038c16781f78480000" + }, + "c340f9b91c26728c31d121d5d6fc3bb56d3d8624": { + "balance": "0x6c6b935b8bbd400000" + }, + "c346cb1fbce2ab285d8e5401f42dd7234d37e86d": { + "balance": "0x486cb9799191e0000" + }, + "c3483d6e88ac1f4ae73cc4408d6c03abe0e49dca": { + "balance": "0x39992648a23c8a00000" + }, + "c348fc5a461323b57be303cb89361b991913df28": { + "balance": "0x152d02c7e14af6800000" + }, + "c34e3ba1322ed0571183a24f94204ee49c186641": { + "balance": "0x327afefa4a7bc0000" + }, + "c35b95a2a3737cb8f0f596b34524872bd30da234": { + "balance": "0x198be85235e2d500000" + }, + "c3631c7698b6c5111989bf452727b3f9395a6dea": { + "balance": "0x243275896641dbe0000" + }, + "c36c0b63bfd75c2f8efb060883d868cccd6cbdb4": { + "balance": "0xa2a15d09519be00000" + }, + "c3756bcdcc7eec74ed896adfc335275930266e08": { + "balance": "0x14542ba12a337c00000" + }, + "c384ac6ee27c39e2f278c220bdfa5baed626d9d3": { + "balance": "0x2086ac351052600000" + }, + "c3a046e3d2b2bf681488826e32d9c061518cfe8c": { + "balance": "0x8cf23f909c0fa00000" + }, + "c3a9226ae275df2cab312b911040634a9c9c9ef6": { + "balance": "0xd8d726b7177a800000" + }, + "c3b928a76fad6578f04f0555e63952cd21d1520a": { + "balance": "0x6c6b935b8bbd400000" + }, + "c3c2297329a6fd99117e54fc6af379b4d556547e": { + "balance": "0x14542ba12a337c00000" + }, + "c3c3c2510d678020485a63735d1307ec4ca6302b": { + "balance": "0x3635c9adc5dea00000" + }, + "c3cb6b36af443f2c6e258b4a39553a818747811f": { + "balance": "0x57473d05dabae80000" + }, + "c3db5657bb72f10d58f231fddf11980aff678693": { + "balance": "0x14061b9d77a5e980000" + }, + "c3db9fb6f46c480af34465d79753b4e2b74a67ce": { + "balance": "0x43c33c1937564800000" + }, + "c3dd58903886303b928625257ae1a013d71ae216": { + "balance": "0x6c6b935b8bbd400000" + }, + "c3e0471c64ff35fa5232cc3121d1d38d1a0fb7de": { + "balance": "0x6c6b935b8bbd400000" + }, + "c3e20c96df8d4e38f50b265a98a906d61bc51a71": { + "balance": "0x6c6b935b8bbd400000" + }, + "c3e387b03ce95ccfd7fa51dd840183bc43532809": { + "balance": "0x6c6b935b8bbd400000" + }, + "c3f8f67295a5cd049364d05d23502623a3e52e84": { + "balance": "0x14542ba12a337c00000" + }, + "c401c427cccff10decb864202f36f5808322a0a8": { + "balance": "0xb47b51a69cd4020000" + }, + "c4088c025f3e85013f5439fb3440a17301e544fe": { + "balance": "0x7e09db4d9f3f340000" + }, + "c41461a3cfbd32c9865555a4813137c076312360": { + "balance": "0x3635c6204739d98000" + }, + "c420388fbee84ad656dd68cdc1fbaa9392780b34": { + "balance": "0xa2dca63aaf4c58000" + }, + "c42250b0fe42e6b7dcd5c890a6f0c88f5f5fb574": { + "balance": "0x81ee4825359840000" + }, + "c42d6aeb710e3a50bfb44d6c31092969a11aa7f3": { + "balance": "0x82263cafd8cea0000" + }, + "c440c7ca2f964b6972ef664a2261dde892619d9c": { + "balance": "0x43c33c1937564800000" + }, + "c44bdec8c36c5c68baa2ddf1d431693229726c43": { + "balance": "0x152d02c7e14af6800000" + }, + "c44f4ab5bc60397c737eb0683391b633f83c48fa": { + "balance": "0x3635c9adc5dea00000" + }, + "c452e0e4b3d6ae06b836f032ca09db409ddfe0fb": { + "balance": "0x2b5e3af16b18800000" + }, + "c45a1ca1036b95004187cdac44a36e33a94ab5c3": { + "balance": "0xdd00f720301880000" + }, + "c45d47ab0c9aa98a5bd62d16223ea2471b121ca4": { + "balance": "0x202e68f2c2aee40000" + }, + "c4681e73bb0e32f6b726204831ff69baa4877e32": { + "balance": "0x62a992e53a0af00000" + }, + "c46bbdef76d4ca60d316c07f5d1a780e3b165f7e": { + "balance": "0x6c6b935b8bbd400000" + }, + "c47d610b399250f70ecf1389bab6292c91264f23": { + "balance": "0xfa7e7b5df3cd00000" + }, + "c4803bb407c762f90b7596e6fde194931e769590": { + "balance": "0xd8d726b7177a800000" + }, + "c48651c1d9c16bff4c9554886c3f3f26431f6f68": { + "balance": "0x23ab9599c43f080000" + }, + "c489c83ffbb0252ac0dbe3521217630e0f491f14": { + "balance": "0xd8d726b7177a800000" + }, + "c48b693cacefdbd6cb5d7895a42e3196327e261c": { + "balance": "0x3635c9adc5dea00000" + }, + "c493489e56c3bdd829007dc2f956412906f76bfa": { + "balance": "0x2a791488e71540000" + }, + "c496cbb0459a6a01600fc589a55a32b454217f9d": { + "balance": "0xeda838c4929080000" + }, + "c49cfaa967f3afbf55031061fc4cef88f85da584": { + "balance": "0x6c6b935b8bbd400000" + }, + "c4b6e5f09cc1b90df07803ce3d4d13766a9c46f4": { + "balance": "0x14542ba12a337c00000" + }, + "c4bec96308a20f90cab18399c493fd3d065abf45": { + "balance": "0x2f6f10780d22cc00000" + }, + "c4c01afc3e0f045221da1284d7878574442fb9ac": { + "balance": "0x1923c688b73ab040000" + }, + "c4c15318d370c73318cc18bdd466dbaa4c6603bf": { + "balance": "0x11164759ffb320000" + }, + "c4c6cb723dd7afa7eb535615e53f3cef14f18118": { + "balance": "0x6c6b8fce0d18798000" + }, + "c4cc45a2b63c27c0b4429e58cd42da59be739bd6": { + "balance": "0x3635c9adc5dea00000" + }, + "c4cf930e5d116ab8d13b9f9a7ec4ab5003a6abde": { + "balance": "0x1158e460913d000000" + }, + "c4d916574e68c49f7ef9d3d82d1638b2b7ee0985": { + "balance": "0x55a6e79ccd1d300000" + }, + "c4dac5a8a0264fbc1055391c509cc3ee21a6e04c": { + "balance": "0x1606b7fa039ce740000" + }, + "c4dd048bfb840e2bc85cb53fcb75abc443c7e90f": { + "balance": "0xc971dc07c9c7900000" + }, + "c4f2913b265c430fa1ab8adf26c333fc1d9b66f2": { + "balance": "0x1158e460913d00000" + }, + "c4f7b13ac6d4eb4db3d4e6a252af8a07bd5957da": { + "balance": "0xad78ebc5ac6200000" + }, + "c4f7d2e2e22084c44f70feaab6c32105f3da376f": { + "balance": "0x6acb3df27e1f880000" + }, + "c4ff6fbb1f09bd9e102ba033d636ac1c4c0f5304": { + "balance": "0x3635c9adc5dea00000" + }, + "c4ffadaaf2823fbea7bff702021bffc4853eb5c9": { + "balance": "0x24a19c1bd6f128000" + }, + "c500b720734ed22938d78c5e48b2ba9367a575ba": { + "balance": "0x7129e1cdf373ee00000" + }, + "c50fe415a641b0856c4e75bf960515441afa358d": { + "balance": "0x6c6b935b8bbd400000" + }, + "c5134cfbb1df7a20b0ed7057622eeed280947dad": { + "balance": "0xcdff97fabcb4600000" + }, + "c517d0315c878813c717e18cafa1eab2654e01da": { + "balance": "0x21e19e0c9bab2400000" + }, + "c518799a5925576213e21896e0539abb85b05ae3": { + "balance": "0x3635c9adc5dea00000" + }, + "c522e20fbf04ed7f6b05a37b4718d6fce0142e1a": { + "balance": "0xd8d726b7177a800000" + }, + "c524086d46c8112b128b2faf6f7c7d8160a8386c": { + "balance": "0x15af1d78b58c400000" + }, + "c52d1a0c73c2a1be84915185f8b34faa0adf1de3": { + "balance": "0x4be4eab3fa0fa68000" + }, + "c53594c7cfb2a08f284cc9d7a63bbdfc0b319732": { + "balance": "0xa6b2328ff3a62c00000" + }, + "c5374928cdf193705443b14cc20da423473cd9cf": { + "balance": "0x77d10509bb3af8000" + }, + "c538a0ff282aaa5f4b75cfb62c70037ee67d4fb5": { + "balance": "0x6c6b935b8bbd400000" + }, + "c53b50fd3b2b72bc6c430baf194a515585d3986d": { + "balance": "0x1158e460913d00000" + }, + "c53d79f7cb9b70952fd30fce58d54b9f0b59f647": { + "balance": "0x113e2d6744345f80000" + }, + "c549df83c6f65eec0f1dc9a0934a5c5f3a50fd88": { + "balance": "0x9dc05cce28c2b80000" + }, + "c55005a6c37e8ca7e543ce259973a3cace961a4a": { + "balance": "0x6c6b935b8bbd400000" + }, + "c555b93156f09101233c6f7cf6eb3c4f196d3346": { + "balance": "0xa2a15d09519be00000" + }, + "c55a6b4761fd11e8c85f15174d74767cd8bd9a68": { + "balance": "0x73f75d1a085ba0000" + }, + "c56e6b62ba6e40e52aab167d21df025d0055754b": { + "balance": "0x6c6b935b8bbd400000" + }, + "c573e841fa08174a208b060ccb7b4c0d7697127f": { + "balance": "0x243d4d18229ca20000" + }, + "c57612de91110c482e6f505bcd23f3c5047d1d61": { + "balance": "0xc2127af858da700000" + }, + "c5843399d150066bf7979c34ba294620368ad7c0": { + "balance": "0xad78ebc5ac6200000" + }, + "c58b9cc61dedbb98c33f224d271f0e228b583433": { + "balance": "0xd255d112e103a00000" + }, + "c58f62fee9711e6a05dc0910b618420aa127f288": { + "balance": "0xd7c198710e66b00000" + }, + "c593b546b7698710a205ad468b2c13152219a342": { + "balance": "0x54069233bf7f780000" + }, + "c593d6e37d14b566643ac4135f243caa0787c182": { + "balance": "0x28a857425466f800000" + }, + "c5a3b98e4593fea0b38c4f455a5065f051a2f815": { + "balance": "0x44cf468af25bf770000" + }, + "c5a48a8500f9b4e22f0eb16c6f4649687674267d": { + "balance": "0x2c0ec50385043e8000" + }, + "c5a629a3962552cb8eded889636aafbd0c18ce65": { + "balance": "0x21e19e0c9bab2400000" + }, + "c5ae86b0c6c7e3900f1368105c56537faf8d743e": { + "balance": "0xa31062beeed700000" + }, + "c5b009baeaf788a276bd35813ad65b400b849f3b": { + "balance": "0x3635c9adc5dea00000" + }, + "c5b56cd234267c28e89c6f6b2266b086a12f970c": { + "balance": "0xd8d726b7177a800000" + }, + "c5c6a4998a33feb764437a8be929a73ba34a0764": { + "balance": "0xa968163f0a57b400000" + }, + "c5c73d61cce7c8fe4c8fce29f39092cd193e0fff": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "c5c7590b5621ecf8358588de9b6890f2626143f1": { + "balance": "0xa2a15d09519be00000" + }, + "c5cdcee0e85d117dabbf536a3f4069bf443f54e7": { + "balance": "0x6ac5c62d9486070000" + }, + "c5d48ca2db2f85d8c555cb0e9cfe826936783f9e": { + "balance": "0xad78ebc5ac6200000" + }, + "c5de1203d3cc2cea31c82ee2de5916880799eafd": { + "balance": "0x10f0cf064dd59200000" + }, + "c5e488cf2b5677933971f64cb8202dd05752a2c0": { + "balance": "0x3635c9adc5dea00000" + }, + "c5e812f76f15f2e1f2f9bc4823483c8804636f67": { + "balance": "0x3f514193abb840000" + }, + "c5e9939334f1252ed2ba26814487dfd2982b3128": { + "balance": "0x3cb71f51fc5580000" + }, + "c5eb42295e9cadeaf2af12dede8a8d53c579c469": { + "balance": "0xcf152640c5c8300000" + }, + "c5edbbd2ca0357654ad0ea4793f8c5cecd30e254": { + "balance": "0x14542ba12a337c00000" + }, + "c5f64babb7033142f20e46d7aa6201ed86f67103": { + "balance": "0x6c6b935b8bbd400000" + }, + "c5f687717246da8a200d20e5e9bcac60b67f3861": { + "balance": "0x18d993f34aef10000" + }, + "c6045b3c350b4ce9ca0c6b754fb41a69b97e9900": { + "balance": "0x3224f42723d4540000" + }, + "c60b04654e003b4683041f1cbd6bc38fda7cdbd6": { + "balance": "0x6c6b935b8bbd400000" + }, + "c61446b754c24e3b1642d9e51765b4d3e46b34b6": { + "balance": "0x6c6b935b8bbd400000" + }, + "c618521321abaf5b26513a4a9528086f220adc6f": { + "balance": "0x176b344f2a78c0000" + }, + "c6234657a807384126f8968ca1708bb07baa493c": { + "balance": "0x1158e460913d00000" + }, + "c625f8c98d27a09a1bcabd5128b1c2a94856af30": { + "balance": "0xad78ebc5ac6200000" + }, + "c6355ec4768c70a49af69513cd83a5bca7e3b9cd": { + "balance": "0x14542ba12a337c00000" + }, + "c63ac417992e9f9b60386ed953e6d7dff2b090e8": { + "balance": "0xd8d8583fa2d52f0000" + }, + "c63cd7882118b8a91e074d4c8f4ba91851303b9a": { + "balance": "0xe18398e7601900000" + }, + "c652871d192422c6bc235fa063b44a7e1d43e385": { + "balance": "0x8670e9ec6598c0000" + }, + "c667441e7f29799aba616451d53b3f489f9e0f48": { + "balance": "0x2f29ace68addd800000" + }, + "c66ae4cee87fb3353219f77f1d6486c580280332": { + "balance": "0x19a16b06ff8cb0000" + }, + "c674f28c8afd073f8b799691b2f0584df942e844": { + "balance": "0x6c6b935b8bbd400000" + }, + "c697b70477cab42e2b8b266681f4ae7375bb2541": { + "balance": "0x12e5732baba5c980000" + }, + "c69b855539ce1b04714728eec25a37f367951de7": { + "balance": "0x6c6b935b8bbd400000" + }, + "c69be440134d6280980144a9f64d84748a37f349": { + "balance": "0x26c29e47c4844c0000" + }, + "c69d663c8d60908391c8d236191533fdf7775613": { + "balance": "0x1a4aba225c20740000" + }, + "c6a286e065c85f3af74812ed8bd3a8ce5d25e21d": { + "balance": "0xfc936392801c0000" + }, + "c6a30ef5bb3320f40dc5e981230d52ae3ac19322": { + "balance": "0x9ddc1e3b901180000" + }, + "c6ae287ddbe1149ba16ddcca4fe06aa2eaa988a9": { + "balance": "0x15af1d78b58c400000" + }, + "c6c7c191379897dd9c9d9a33839c4a5f62c0890d": { + "balance": "0xd8d854b22430688000" + }, + "c6cd68ec35362c5ad84c82ad4edc232125912d99": { + "balance": "0x5e0549c9632e1d80000" + }, + "c6d8954e8f3fc533d2d230ff025cb4dce14f3426": { + "balance": "0x15af1d78b58c400000" + }, + "c6dbdb9efd5ec1b3786e0671eb2279b253f215ed": { + "balance": "0x3635c9adc5dea00000" + }, + "c6df2075ebd240d44869c2be6bdf82e63d4ef1f5": { + "balance": "0x1158e460913d00000" + }, + "c6e2f5af979a03fd723a1b6efa728318cf9c1800": { + "balance": "0x243d4d18229ca20000" + }, + "c6e324beeb5b36765ecd464260f7f26006c5c62e": { + "balance": "0x6c6b935b8bbd400000" + }, + "c6e4cc0c7283fc1c85bc4813effaaf72b49823c0": { + "balance": "0xf031ec9c87dd30000" + }, + "c6ee35934229693529dc41d9bb71a2496658b88e": { + "balance": "0x42bf06b78ed3b500000" + }, + "c6fb1ee37417d080a0d048923bdabab095d077c6": { + "balance": "0xad78ebc5ac6200000" + }, + "c70527d444c490e9fc3f5cc44e66eb4f306b380f": { + "balance": "0xd8d726b7177a800000" + }, + "c70d856d621ec145303c0a6400cd17bbd6f5eaf7": { + "balance": "0x1158e460913d00000" + }, + "c70fa45576bf9c865f983893002c414926f61029": { + "balance": "0x15b4aa8e9702680000" + }, + "c71145e529c7a714e67903ee6206e4c3042b6727": { + "balance": "0x4d853c8f8908980000" + }, + "c71b2a3d7135d2a85fb5a571dcbe695e13fc43cd": { + "balance": "0x3635c9adc5dea00000" + }, + "c71f1d75873f33dcb2dd4b3987a12d0791a5ce27": { + "balance": "0x3708baed3d68900000" + }, + "c71f92a3a54a7b8c2f5ea44305fccb84eee23148": { + "balance": "0x2b59ca131d2060000" + }, + "c721b2a7aa44c21298e85039d00e2e460e670b9c": { + "balance": "0x7a1fe160277000000" + }, + "c72cb301258e91bc08998a805dd192f25c2f9a35": { + "balance": "0x2009c5c8bf6fdc0000" + }, + "c7368b9709a5c1b51c0adf187a65df14e12b7dba": { + "balance": "0x2026fc77f03e5ae8000" + }, + "c739259e7f85f2659bef5f609ed86b3d596c201e": { + "balance": "0xad78ebc5ac6200000" + }, + "c73e2112282215dc0762f32b7e807dcd1a7aae3e": { + "balance": "0x1760cbc623bb3500000" + }, + "c749668042e71123a648975e08ed6382f83e05e2": { + "balance": "0x2f6f10780d22cc00000" + }, + "c74a3995f807de1db01a2eb9c62e97d0548f696f": { + "balance": "0x3635c9adc5dea00000" + }, + "c7506c1019121ff08a2c8c1591a65eb4bdfb4a3f": { + "balance": "0x2086ac351052600000" + }, + "c75c37ce2da06bbc40081159c6ba0f976e3993b1": { + "balance": "0x3a7923151ecf580000" + }, + "c75d2259306aec7df022768c69899a652185dbc4": { + "balance": "0xd8d726b7177a800000" + }, + "c760971bbc181c6a7cf77441f24247d19ce9b4cf": { + "balance": "0x6c6b935b8bbd400000" + }, + "c76130c73cb9210238025c9df95d0be54ac67fbe": { + "balance": "0x5150ae84a8cdf00000" + }, + "c765e00476810947816af142d46d2ee7bca8cc4f": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c7675e5647b9d8daf4d3dff1e552f6b07154ac38": { + "balance": "0x9c2007651b2500000" + }, + "c77b01a6e911fa988d01a3ab33646beef9c138f3": { + "balance": "0x271b6fa5dbe6cc0000" + }, + "c7837ad0a0bf14186937ace06c5546a36aa54f46": { + "balance": "0xd8d726b7177a800000" + }, + "c79806032bc7d828f19ac6a640c68e3d820fa442": { + "balance": "0x1158e460913d00000" + }, + "c799e34e88ff88be7de28e15e4f2a63d0b33c4cb": { + "balance": "0xad78ebc5ac6200000" + }, + "c79d5062c796dd7761f1f13e558d73a59f82f38b": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "c7a018f0968a51d1f6603c5c49dc545bcb0ff293": { + "balance": "0xd8d726b7177a800000" + }, + "c7aff91929797489555a2ff1d14d5c695a108355": { + "balance": "0x3635c9adc5dea00000" + }, + "c7b1c83e63203f9547263ef6282e7da33b6ed659": { + "balance": "0xfc936392801c0000" + }, + "c7b39b060451000ca1049ba154bcfa00ff8af262": { + "balance": "0x152d02c7e14af6800000" + }, + "c7bf17c4c11f98941f507e77084fffbd2dbd3db5": { + "balance": "0x3635c9adc5dea00000" + }, + "c7bf2ed1ed312940ee6aded1516e268e4a604856": { + "balance": "0x14542ba12a337c00000" + }, + "c7d44fe32c7f8cd5f1a97427b6cd3afc9e45023e": { + "balance": "0x55a6e79ccd1d300000" + }, + "c7d5c7054081e918ec687b5ab36e973d18132935": { + "balance": "0x9ddc1e3b901180000" + }, + "c7de5e8eafb5f62b1a0af2195cf793c7894c9268": { + "balance": "0x3635c9adc5dea00000" + }, + "c7e330cd0c890ac99fe771fcc7e7b009b7413d8a": { + "balance": "0xd8d726b7177a800000" + }, + "c7eac31abce6d5f1dea42202b6a674153db47a29": { + "balance": "0x2009c5c8bf6fdc0000" + }, + "c7ec62b804b1f69b1e3070b5d362c62fb309b070": { + "balance": "0x2c46bf5416066110000" + }, + "c7f72bb758016b374714d4899bce22b4aec70a31": { + "balance": "0x3a26c9478f5e2d0000" + }, + "c80b36d1beafba5fcc644d60ac6e46ed2927e7dc": { + "balance": "0xb98bc829a6f90000" + }, + "c811c2e9aa1ac3462eba5e88fcb5120e9f6e2ca2": { + "balance": "0x4be6d887bd876e0000" + }, + "c817df1b91faf30fe3251571727c9711b45d8f06": { + "balance": "0x6c6acc67d7b1d40000" + }, + "c81fb7d20fd2800192f0aac198d6d6a37d3fcb7d": { + "balance": "0xe1149331c2dde0000" + }, + "c820c711f07705273807aaaa6de44d0e4b48be2e": { + "balance": "0x8670e9ec6598c0000" + }, + "c8231ba5a411a13e222b29bfc1083f763158f226": { + "balance": "0x3637096c4bcc690000" + }, + "c836e24a6fcf29943b3608e662290a215f6529ea": { + "balance": "0xfd45064eaee100000" + }, + "c83ba6dd9549be1d3287a5a654d106c34c6b5da2": { + "balance": "0x17b7883c06916600000" + }, + "c83e9d6a58253beebeb793e6f28b054a58491b74": { + "balance": "0xf46c2b6f5a9140000" + }, + "c841884fa4785fb773b28e9715fae99a5134305d": { + "balance": "0x6c6b935b8bbd400000" + }, + "c84d9bea0a7b9f140220fd8b9097cfbfd5edf564": { + "balance": "0x6ab9ec291ad7d8000" + }, + "c852428d2b586497acd30c56aa13fb5582f84402": { + "balance": "0x3342d60dff19600000" + }, + "c853215b9b9f2d2cd0741e585e987b5fb80c212e": { + "balance": "0x54069233bf7f780000" + }, + "c85325eab2a59b3ed863c86a5f2906a04229ffa9": { + "balance": "0x193d7f7d253de00000" + }, + "c85ef27d820403805fc9ed259fff64acb8d6346a": { + "balance": "0x6c6b935b8bbd400000" + }, + "c8616b4ec09128cdff39d6e4b9ac86eec471d5f2": { + "balance": "0x10d3aa536e2940000" + }, + "c86190904b8d079ec010e462cbffc90834ffaa5c": { + "balance": "0x22385a827e815500000" + }, + "c8710d7e8b5a3bd69a42fe0fa8b87c357fddcdc8": { + "balance": "0xd8d726b7177a800000" + }, + "c87352dba582ee2066b9c002a962e003134f78b1": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c87c77e3c24adecdcd1038a38b56e18dead3b702": { + "balance": "0x1dd0c885f9a0d800000" + }, + "c87d3ae3d88704d9ab0009dcc1a0067131f8ba3c": { + "balance": "0x6ac5c62d9486070000" + }, + "c8814e34523e38e1f927a7dce8466a447a093603": { + "balance": "0x21e19e0c9bab2400000" + }, + "c88255eddcf521c6f81d97f5a42181c9073d4ef1": { + "balance": "0xfc39044d00a2a8000" + }, + "c885a18aabf4541b7b7b7ecd30f6fae6869d9569": { + "balance": "0x6c6b935b8bbd400000" + }, + "c88ca1e6e5f4d558d13780f488f10d4ad3130d34": { + "balance": "0x54069233bf7f780000" + }, + "c88eec54d305c928cc2848c2fee23531acb96d49": { + "balance": "0x6c6ad382d4fb610000" + }, + "c89cf504b9f3f835181fd8424f5ccbc8e1bddf7d": { + "balance": "0x21e19e0c9bab2400000" + }, + "c8a2c4e59e1c7fc54805580438aed3e44afdf00e": { + "balance": "0x2629f66e0c5300000" + }, + "c8aa49e3809f0899f28ab57e6743709d58419033": { + "balance": "0x2fb474098f67c00000" + }, + "c8ab1a3cf46cb8b064df2e222d39607394203277": { + "balance": "0x6c6b935b8bbd400000" + }, + "c8b1850525d946f2ae84f317b15188c536a5dc86": { + "balance": "0x918ddc3a42a3d40000" + }, + "c8d4e1599d03b79809e0130a8dc38408f05e8cd3": { + "balance": "0x9fad06241279160000" + }, + "c8dd27f16bf22450f5771b9fe4ed4ffcb30936f4": { + "balance": "0xaadec983fcff40000" + }, + "c8de7a564c7f4012a6f6d10fd08f47890fbf07d4": { + "balance": "0x1043561a8829300000" + }, + "c8e2adeb545e499d982c0c117363ceb489c5b11f": { + "balance": "0x35659ef93f0fc40000" + }, + "c8e558a3c5697e6fb23a2594c880b7a1b68f9860": { + "balance": "0x21e19e0c9bab2400000" + }, + "c8f2b320e6dfd70906c597bad2f9501312c78259": { + "balance": "0x51934b8b3a57d00000" + }, + "c90300cb1d4077e6a6d7e169a460468cf4a492d7": { + "balance": "0x6c6b935b8bbd400000" + }, + "c90c3765156bca8e4897ab802419153cbe5225a9": { + "balance": "0xad78ebc5ac6200000" + }, + "c910a970556c9716ea53af66ddef93143124913d": { + "balance": "0x55a6e79ccd1d300000" + }, + "c9127b7f6629ee13fc3f60bc2f4467a20745a762": { + "balance": "0x37c9aa4e7ce421d8000" + }, + "c91bb562e42bd46130e2d3ae4652b6a4eb86bc0f": { + "balance": "0x1d460162f516f00000" + }, + "c9308879056dfe138ef8208f79a915c6bc7e70a8": { + "balance": "0x21e19e0c9bab2400000" + }, + "c934becaf71f225f8b4a4bf7b197f4ac9630345c": { + "balance": "0x43c33c1937564800000" + }, + "c93fbde8d46d2bcc0fa9b33bd8ba7f8042125565": { + "balance": "0x4be4e7267b6ae00000" + }, + "c94089553ae4c22ca09fbc98f57075cf2ec59504": { + "balance": "0xd8d726b7177a800000" + }, + "c94110e71afe578aa218e4fc286403b0330ace8d": { + "balance": "0x6c6b935b8bbd400000" + }, + "c946d5acc1346eba0a7279a0ac1d465c996d827e": { + "balance": "0x3783d545fdf0aa40000" + }, + "c94a28fb3230a9ddfa964e770f2ce3c253a7be4f": { + "balance": "0xad78ebc5ac6200000" + }, + "c94a585203da7bbafd93e15884e660d4b1ead854": { + "balance": "0x17b7883c06916600000" + }, + "c94f7c35c027d47df8ef4f9df85a9248a17dd23b": { + "balance": "0x19f8e7559924c0000" + }, + "c951900c341abbb3bafbf7ee2029377071dbc36a": { + "balance": "0x11c25d004d01f80000" + }, + "c953f934c0eb2d0f144bdab00483fd8194865ce7": { + "balance": "0x6c6b935b8bbd400000" + }, + "c96626728aaa4c4fb3d31c26df3af310081710d1": { + "balance": "0xb50fcfafebecb00000" + }, + "c96751656c0a8ef4357b7344322134b983504aca": { + "balance": "0x6c6b935b8bbd400000" + }, + "c98048687f2bfcc9bd90ed18736c57edd352b65d": { + "balance": "0x3635c9adc5dea00000" + }, + "c981d312d287d558871edd973abb76b979e5c35e": { + "balance": "0x6acb3df27e1f880000" + }, + "c982586d63b0d74c201b1af8418372e30c7616be": { + "balance": "0x56bc75e2d63100000" + }, + "c989434f825aaf9c552f685eba7c11db4a5fc73a": { + "balance": "0x1b28c58d9696b40000" + }, + "c989eec307e8839b9d7237cfda08822962abe487": { + "balance": "0x15af1d78b58c400000" + }, + "c992be59c6721caf4e028f9e8f05c25c55515bd4": { + "balance": "0x1158e460913d00000" + }, + "c9957ba94c1b29e5277ec36622704904c63dc023": { + "balance": "0x683efc6782642c0000" + }, + "c99a9cd6c9c1be3534eecd92ecc22f5c38e9515b": { + "balance": "0x105593b3a169d770000" + }, + "c9ac01c3fb0929033f0ccc7e1acfeaaba7945d47": { + "balance": "0x2a36a9e9ca4d2038000" + }, + "c9b698e898d20d4d4f408e4e4d061922aa856307": { + "balance": "0x22b1c8c1227a00000" + }, + "c9b6b686111691ee6aa197c7231a88dc60bd295d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c9c7ac0bdd9342b5ead4360923f68c72a6ba633a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "c9c80dc12e7bab86e949d01e4c3ed35f2b9bba5f": { + "balance": "0x6c6b935b8bbd400000" + }, + "c9d76446d5aadff80b68b91b08cd9bc8f5551ac1": { + "balance": "0x26b4bd9110dce80000" + }, + "c9dcbb056f4db7d9da39936202c5bd8230b3b477": { + "balance": "0x43c33c1937564800000" + }, + "c9e02608066828848aeb28c73672a12925181f4d": { + "balance": "0x1b1b6bd7af64c70000" + }, + "ca0432cb157b5179f02ebba5c9d1b54fec4d88ca": { + "balance": "0x3635c9adc5dea00000" + }, + "ca122cf0f2948896b74843f49afed0ba1618eed7": { + "balance": "0x1e5b8fa8fe2ac00000" + }, + "ca22cda3606da5cad013b8074706d7e9e721a50c": { + "balance": "0x17181c6fa3981940000" + }, + "ca23f62dff0d6460036c62e840aec5577e0befd2": { + "balance": "0x7a1fe160277000000" + }, + "ca25ff34934c1942e22a4e7bd56f14021a1af088": { + "balance": "0xaadec983fcff40000" + }, + "ca373fe3c906b8c6559ee49ccd07f37cd4fb5266": { + "balance": "0x61093d7c2c6d380000" + }, + "ca41ccac30172052d522cd2f2f957d248153409f": { + "balance": "0x6acb3df27e1f880000" + }, + "ca4288014eddc5632f5facb5e38517a8f8bc5d98": { + "balance": "0x126e72a69a50d00000" + }, + "ca428863a5ca30369892d612183ef9fb1a04bcea": { + "balance": "0x52663ccab1e1c00000" + }, + "ca49a5f58adbefae23ee59eea241cf0482622eaa": { + "balance": "0x4d853c8f8908980000" + }, + "ca4ca9e4779d530ecbacd47e6a8058cfde65d98f": { + "balance": "0x2b5e3af16b18800000" + }, + "ca657ec06fe5bc09cf23e52af7f80cc3689e6ede": { + "balance": "0x30ca024f987b900000" + }, + "ca66b2280fa282c5b67631ce552b62ee55ad8474": { + "balance": "0x6ac422f53492880000" + }, + "ca6c818befd251361e02744068be99d8aa60b84a": { + "balance": "0x14542ba12a337c00000" + }, + "ca70f4ddbf069d2143bd6bbc7f696b52789b32e7": { + "balance": "0xa2a15d09519be00000" + }, + "ca747576446a4c8f30b08340fee198de63ec92cf": { + "balance": "0x17c8e1206722a300000" + }, + "ca7ba3ff536c7e5f0e153800bd383db8312998e0": { + "balance": "0x931ac3d6bb2400000" + }, + "ca8276c477b4a07b80107b843594189607b53bec": { + "balance": "0x14542ba12a337c00000" + }, + "ca8409083e01b397cf12928a05b68455ce6201df": { + "balance": "0x56bc75e2d631000000" + }, + "ca98c7988efa08e925ef9c9945520326e9f43b99": { + "balance": "0xd8d726b7177a800000" + }, + "ca9a042a6a806ffc92179500d24429e8ab528117": { + "balance": "0x3ba1910bf341b00000" + }, + "ca9dec02841adf5cc920576a5187edd2bd434a18": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ca9faa17542fafbb388eab21bc4c94e8a7b34788": { + "balance": "0x6c6b8fce0d18798000" + }, + "caaa68ee6cdf0d34454a769b0da148a1faaa1865": { + "balance": "0x1872e1de7fe52c00000" + }, + "caad9dc20d589ce428d8fda3a9d53a607b7988b5": { + "balance": "0xd8d726b7177a800000" + }, + "cab0d32cf3767fa6b3537c84328baa9f50458136": { + "balance": "0x1e5b8fa8fe2ac000000" + }, + "cab9a301e6bd46e940355028eccd40ce4d5a1ac3": { + "balance": "0x15af1d78b58c400000" + }, + "cab9a97ada065c87816e6860a8f1426fe6b3d775": { + "balance": "0x3635c9adc5dea00000" + }, + "cabab6274ed15089737e287be878b757934864e2": { + "balance": "0x43c33c1937564800000" + }, + "cabdaf354f4720a466a764a528d60e3a482a393c": { + "balance": "0x3635c9adc5dea00000" + }, + "cacb675e0996235404efafbb2ecb8152271b55e0": { + "balance": "0x25f273933db5700000" + }, + "cad14f9ebba76680eb836b079c7f7baaf481ed6d": { + "balance": "0xcef3d7bd7d0340000" + }, + "cae3a253bcb2cf4e13ba80c298ab0402da7c2aa0": { + "balance": "0x124bc0ddd92e5600000" + }, + "caef027b1ab504c73f41f2a10979b474f97e309f": { + "balance": "0xad78ebc5ac6200000" + }, + "caf4481d9db78dc4f25f7b4ac8bd3b1ca0106b31": { + "balance": "0x10f0cf064dd59200000" + }, + "cafde855864c2598da3cafc05ad98df2898e8048": { + "balance": "0x300a8ed96ff4a940000" + }, + "cb0dd7cf4e5d8661f6028943a4b9b75c914436a7": { + "balance": "0x1969368974c05b000000" + }, + "cb1bb6f1da5eb10d4899f7e61d06c1b00fdfb52d": { + "balance": "0x384524cc70b7780000" + }, + "cb3d766c983f192bcecac70f4ee03dd9ff714d51": { + "balance": "0x56bc75e2d63100000" + }, + "cb42b44eb5fd60b5837e4f9eb47267523d1a229c": { + "balance": "0x2ee449550898e40000" + }, + "cb47bd30cfa8ec5468aaa6a94642ced9c819c8d4": { + "balance": "0xd8d726b7177a800000" + }, + "cb48fe8265d9af55eb7006bc335645b0a3a183be": { + "balance": "0xa2a15d09519be00000" + }, + "cb4a914d2bb029f32e5fef5c234c4fec2d2dd577": { + "balance": "0x6194049f30f7200000" + }, + "cb4abfc282aed76e5d57affda542c1f382fcacf4": { + "balance": "0x1b90f11c3183faa0000" + }, + "cb4ad0c723da46ab56d526da0c1d25c73daff10a": { + "balance": "0x1ba5abf9e779380000" + }, + "cb4bb1c623ba28dc42bdaaa6e74e1d2aa1256c2a": { + "balance": "0x6c6acc67d7b1d40000" + }, + "cb50587412822304ebcba07dab3a0f09fffee486": { + "balance": "0x4a4491bd6dcd280000" + }, + "cb58990bcd90cfbf6d8f0986f6fa600276b94e2d": { + "balance": "0x3634bf39ab98788000" + }, + "cb68ae5abe02dcf8cbc5aa719c25814651af8b85": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "cb7479109b43b26657f4465f4d18c6f974be5f42": { + "balance": "0x62a992e53a0af00000" + }, + "cb7d2b8089e9312cc9aeaa2773f35308ec6c2a7b": { + "balance": "0x21e19e0c9bab2400000" + }, + "cb86edbc8bbb1f9131022be649565ebdb09e32a1": { + "balance": "0x6c6b935b8bbd400000" + }, + "cb93199b9c90bc4915bd859e3d42866dc8c18749": { + "balance": "0xc90df07def78c0000" + }, + "cb94e76febe208116733e76e805d48d112ec9fca": { + "balance": "0x3635c9adc5dea00000" + }, + "cb9b5103e4ce89af4f64916150bff9eecb9faa5c": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "cba25c7a503cc8e0d04971ca05c762f9b762b48b": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "cba288cd3c1eb4d59ddb06a6421c14c345a47b24": { + "balance": "0xd8d726b7177a800000" + }, + "cbb3189e4bd7f45f178b1c30c76e26314d4a4b0a": { + "balance": "0xffe0b677c65a98000" + }, + "cbb7be17953f2ccc93e1bc99805bf45511434e4c": { + "balance": "0xaae5b9df56d2f200000" + }, + "cbc04b4d8b82caf670996f160c362940d66fcf1a": { + "balance": "0x14542ba12a337c00000" + }, + "cbde9734b8e6aa538c291d6d7facedb0f338f857": { + "balance": "0x6c6b935b8bbd400000" + }, + "cbe1b948864d8474e765145858fca4550f784b92": { + "balance": "0x21e19e0c9bab2400000" + }, + "cbe52fc533d7dd608c92a260b37c3f45deb4eb33": { + "balance": "0x3635c9adc5dea00000" + }, + "cbe810fe0fecc964474a1db97728bc87e973fcbd": { + "balance": "0x21e19e0c9bab2400000" + }, + "cbf16a0fe2745258cd52db2bf21954c975fc6a15": { + "balance": "0x1043561a8829300000" + }, + "cbf37ff854a2f1ce53934494777892d3ec655782": { + "balance": "0x21e19e0c9bab2400000" + }, + "cbfa6af6c283b046e2772c6063b0b21553c40106": { + "balance": "0x6c6b935b8bbd400000" + }, + "cbfa76db04ce38fb205d37b8d377cf1380da0317": { + "balance": "0x4d853c8f8908980000" + }, + "cc034985d3f28c2d39b1a34bced4d3b2b6ca234e": { + "balance": "0x9ddc1e3b901180000" + }, + "cc043c4388d345f884c6855e71142a9f41fd6935": { + "balance": "0x1158e460913d00000" + }, + "cc1d6ead01aada3e8dc7b95dca25df26eefa639d": { + "balance": "0x6c6b935b8bbd400000" + }, + "cc2b5f448f3528d3fe41cc7d1fa9c0dc76f1b776": { + "balance": "0x340aad21b3b700000" + }, + "cc2d04f0a4017189b340ca77198641dcf6456b91": { + "balance": "0xd5967be4fc3f100000" + }, + "cc419fd9912b85135659e77a93bc3df182d45115": { + "balance": "0x21e19e0c9bab2400000" + }, + "cc45fb3a555bad807b388a0357c855205f7c75e8": { + "balance": "0x2ee449550898e40000" + }, + "cc48414d2ac4d42a5962f29eee4497092f431352": { + "balance": "0x8ba52e6fc45e40000" + }, + "cc4a2f2cf86cf3e43375f360a4734691195f1490": { + "balance": "0x4915053bd129098000" + }, + "cc4f0ff2aeb67d54ce3bc8c6510b9ae83e9d328b": { + "balance": "0x15af1d78b58c400000" + }, + "cc4faac00be6628f92ef6b8cb1b1e76aac81fa18": { + "balance": "0xb22a2eab0f0fd0000" + }, + "cc4feb72df98ff35a138e01761d1203f9b7edf0a": { + "balance": "0x17b7883c06916600000" + }, + "cc606f511397a38fc7872bd3b0bd03c71bbd768b": { + "balance": "0x3635c9adc5dea00000" + }, + "cc60f836acdef3548a1fefcca13ec6a937db44a0": { + "balance": "0x4b06dbbb40f4a0000" + }, + "cc6c03bd603e09de54e9c4d5ac6d41cbce715724": { + "balance": "0x556f64c1fe7fa0000" + }, + "cc6c2df00e86eca40f21ffda1a67a1690f477c65": { + "balance": "0xab4dcf399a3a600000" + }, + "cc6d7b12061bc96d104d606d65ffa32b0036eb07": { + "balance": "0x21e19e0c9bab2400000" + }, + "cc73dd356b4979b579b401d4cc7a31a268ddce5a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "cc758d071d25a6320af68c5dc9c4f6955ba94520": { + "balance": "0x14542ba12a337c00000" + }, + "cc7b0481cc32e6faef2386a07022bcb6d2c3b4fc": { + "balance": "0xab4dcf399a3a600000" + }, + "cc943be1222cd1400a2399dd1b459445cf6d54a9": { + "balance": "0x2a740ae6536fc880000" + }, + "cc9519d1f3985f6b255eaded12d5624a972721e1": { + "balance": "0x3635c9adc5dea00000" + }, + "cc9ac715cd6f2610c52b58676456884297018b29": { + "balance": "0xb98bc829a6f90000" + }, + "cca07bb794571d4acf041dad87f0d1ef3185b319": { + "balance": "0x6c6b935b8bbd400000" + }, + "ccabc6048a53464424fcf76eeb9e6e1801fa23d4": { + "balance": "0x2ab7b260ff3fd0000" + }, + "ccae0d3d852a7da3860f0636154c0a6ca31628d4": { + "balance": "0x5c6d12b6bc1a00000" + }, + "ccca24d8c56d6e2c07db086ec07e585be267ac8d": { + "balance": "0xad78ebc5ac6200000" + }, + "ccd521132d986cb96869842622a7dda26c3ed057": { + "balance": "0x6c6b935b8bbd400000" + }, + "ccf43975b76bfe735fec3cb7d4dd24f805ba0962": { + "balance": "0x340aad21b3b700000" + }, + "ccf62a663f1353ba2ef8e6521dc1ecb673ec8ef7": { + "balance": "0x83d6c7aab63600000" + }, + "ccf7110d1bd9a74bfd1d7d7d2d9d55607e7b837d": { + "balance": "0x30ca024f987b900000" + }, + "ccfd725760a68823ff1e062f4cc97e1360e8d997": { + "balance": "0x15ac56edc4d12c0000" + }, + "cd020f8edfcf524798a9b73a640334bbf72f80a5": { + "balance": "0x73f75d1a085ba0000" + }, + "cd06f8c1b5cdbd28e2d96b6346c3e85a0483ba24": { + "balance": "0x3635c9adc5dea00000" + }, + "cd072e6e1833137995196d7bb1725fef8761f655": { + "balance": "0x14542ba12a337c00000" + }, + "cd0a161bc367ae0927a92aac9cf6e5086714efca": { + "balance": "0x6c6b935b8bbd400000" + }, + "cd0af3474e22f069ec3407870dd770443d5b12b0": { + "balance": "0x8e5eb4ee77b2ef0000" + }, + "cd0b0257e783a3d2c2e3ba9d6e79b75ef98024d4": { + "balance": "0x9fad06241279160000" + }, + "cd102cd6db3df14ad6af0f87c72479861bfc3d24": { + "balance": "0x6c6b935b8bbd400000" + }, + "cd1e66ed539dd92fc40bbaa1fa16de8c02c14d45": { + "balance": "0xc77e4256863d80000" + }, + "cd1ed263fbf6f6f7b48aef8f733d329d4382c7c7": { + "balance": "0x100bd33fb98ba0000" + }, + "cd2a36d753e9e0ed012a584d716807587b41d56a": { + "balance": "0xe2ba75b0b1f1c0000" + }, + "cd32a4a8a27f1cc63954aa634f7857057334c7a3": { + "balance": "0x3ad166576c72d40000" + }, + "cd35ff010ec501a721a1b2f07a9ca5877dfcf95a": { + "balance": "0xd96fce90cfabcc0000" + }, + "cd4306d7f6947ac1744d4e13b8ef32cb657e1c00": { + "balance": "0x1b1ab319f5ec750000" + }, + "cd43258b7392a930839a51b2ef8ad23412f75a9f": { + "balance": "0x6c6b935b8bbd400000" + }, + "cd49bf185e70d04507999f92a4de4455312827d0": { + "balance": "0x3635c9adc5dea00000" + }, + "cd5510a242dfb0183de925fba866e312fabc1657": { + "balance": "0x821ab0d44149800000" + }, + "cd566ad7b883f01fd3998a9a58a9dee4724ddca5": { + "balance": "0x330ae1835be300000" + }, + "cd59f3dde77e09940befb6ee58031965cae7a336": { + "balance": "0x21e19e0c9bab2400000" + }, + "cd725d70be97e677e3c8e85c0b26ef31e9955045": { + "balance": "0x487a9a304539440000" + }, + "cd7e47909464d871b9a6dc76a8e9195db3485e7a": { + "balance": "0x215f835bc769da80000" + }, + "cd7ece086b4b619b3b369352ee38b71ddb06439a": { + "balance": "0xad78ebc5ac6200000" + }, + "cd7f09d7ed66d0c38bc5ad4e32b7f2b08dc1b30d": { + "balance": "0x3e3bb34da2a4700000" + }, + "cd9529492b5c29e475acb941402b3d3ba50686b0": { + "balance": "0x6acb3df27e1f880000" + }, + "cd95fa423d6fc120274aacde19f4eeb766f10420": { + "balance": "0xad78ebc5ac6200000" + }, + "cd9b4cef73390c83a8fd71d7b540a7f9cf8b8c92": { + "balance": "0x4e1003b28d9280000" + }, + "cda1741109c0265b3fb2bf8d5ec9c2b8a3346b63": { + "balance": "0x1158e460913d00000" + }, + "cda1b886e3a795c9ba77914e0a2fe5676f0f5ccf": { + "balance": "0x5bf60ea42c2040000" + }, + "cda4530f4b9bc50905b79d17c28fc46f95349bdf": { + "balance": "0x3310e04911f1f80000" + }, + "cdab46a5902080646fbf954204204ae88404822b": { + "balance": "0x1d8a96e5c606eb0000" + }, + "cdb597299030183f6e2d238533f4642aa58754b6": { + "balance": "0x15af1d78b58c400000" + }, + "cdd5d881a7362c9070073bdfbc75e72453ac510e": { + "balance": "0x2da518eae48ee80000" + }, + "cdd60d73efaad873c9bbfb178ca1b7105a81a681": { + "balance": "0x1bc16d674ec800000" + }, + "cdd9efac4d6d60bd71d95585dce5d59705c13564": { + "balance": "0x56bc75e2d63100000" + }, + "cde36d81d128c59da145652193eec2bfd96586ef": { + "balance": "0xd8d726b7177a800000" + }, + "cdea386f9d0fd804d02818f237b7d9fa7646d35e": { + "balance": "0xa349d36d80ec578000" + }, + "cdecf5675433cdb0c2e55a68db5d8bbe78419dd2": { + "balance": "0x1158e460913d00000" + }, + "cdfd8217339725d7ebac11a63655f265eff1cc3d": { + "balance": "0x10f0c696410e3a90000" + }, + "ce079f51887774d8021cb3b575f58f18e9acf984": { + "balance": "0x9c2007651b2500000" + }, + "ce1884ddbbb8e10e4dba6e44feeec2a7e5f92f05": { + "balance": "0xd8d726b7177a800000" + }, + "ce1b0cb46aaecfd79b880cad0f2dda8a8dedd0b1": { + "balance": "0x1158e460913d00000" + }, + "ce26f9a5305f8381094354dbfc92664e84f902b5": { + "balance": "0xc7aaab0591eec0000" + }, + "ce2deab51c0a9ae09cd212c4fa4cc52b53cc0dec": { + "balance": "0x6c6b935b8bbd400000" + }, + "ce2e0da8934699bb1a553e55a0b85c169435bea3": { + "balance": "0x10f0c696410e3a90000" + }, + "ce3a61f0461b00935e85fa1ead82c45e5a64d488": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ce4b065dbcb23047203262fb48c1188364977470": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ce53c8cdd74296aca987b2bc19c2b875a48749d0": { + "balance": "0xa2a15d09519be00000" + }, + "ce5e04f0184369bcfa06aca66ffa91bf59fa0fb9": { + "balance": "0x22b1c8c1227a00000" + }, + "ce5eb63a7bf4fbc2f6e4baa0c68ab1cb4cf98fb4": { + "balance": "0x6c6b935b8bbd400000" + }, + "ce62125adec3370ac52110953a4e760be9451e3b": { + "balance": "0x83d6c7aab63600000" + }, + "ce71086d4c602554b82dcbfce88d20634d53cc4d": { + "balance": "0x92896529baddc880000" + }, + "ce8a6b6d5033b1498b1ffeb41a41550405fa03a2": { + "balance": "0xd8d726b7177a800000" + }, + "ce9786d3712fa200e9f68537eeaa1a06a6f45a4b": { + "balance": "0x61093d7c2c6d380000" + }, + "ce9d21c692cd3c01f2011f505f870036fa8f6cd2": { + "balance": "0x15af1d78b58c400000" + }, + "cea2896623f4910287a2bdc5be83aea3f2e6de08": { + "balance": "0x1fb5a3751e490dc0000" + }, + "cea34a4dd93dd9aefd399002a97d997a1b4b89cd": { + "balance": "0x5150ae84a8cdf00000" + }, + "cea43f7075816b60bbfce68b993af0881270f6c4": { + "balance": "0x6c6b935b8bbd400000" + }, + "cea8743341533cb2f0b9c6efb8fda80d77162825": { + "balance": "0x56bc75e2d63100000" + }, + "ceb089ec8a78337e8ef88de11b49e3dd910f748f": { + "balance": "0x3635c9adc5dea00000" + }, + "ceb33d78e7547a9da2e87d51aec5f3441c87923a": { + "balance": "0x1158e460913d00000" + }, + "ceb389381d48a8ae4ffc483ad0bb5e204cfdb1ec": { + "balance": "0x2827e6e4dd62ba8000" + }, + "cec6fc65853f9cce5f8e844676362e1579015f02": { + "balance": "0x6c6b935b8bbd400000" + }, + "ced3c7be8de7585140952aeb501dc1f876ecafb0": { + "balance": "0xd8d726b7177a800000" + }, + "ced81ec3533ff1bfebf3e3843ee740ad11758d3e": { + "balance": "0x6acb3df27e1f880000" + }, + "cedcb3a1d6843fb6bef643617deaf38f8e98dd5f": { + "balance": "0x19e2a4c818b9060000" + }, + "cee699c0707a7836252b292f047ce8ad289b2f55": { + "balance": "0x119a1e21aa69560000" + }, + "ceed47ca5b899fd1623f21e9bd4db65a10e5b09d": { + "balance": "0x73877404c1eee0000" + }, + "cef77451dfa2c643e00b156d6c6ff84e2373eb66": { + "balance": "0xa31062beeed700000" + }, + "cf1169041c1745e45b172435a2fc99b49ace2b00": { + "balance": "0x1bb88baab2d7c0000" + }, + "cf157612764e0fd696c8cb5fba85df4c0ddc3cb0": { + "balance": "0x65a4da25d3016c00000" + }, + "cf1bdb799b2ea63ce134668bdc198b54840f180b": { + "balance": "0xfc936392801c0000" + }, + "cf2288ef4ebf88e86db13d8a0e0bf52a056582c3": { + "balance": "0x89506fbf9740740000" + }, + "cf264e6925130906c4d7c18591aa41b2a67f6f58": { + "balance": "0x6c6b935b8bbd400000" + }, + "cf26b47bd034bc508e6c4bcfd6c7d30034925761": { + "balance": "0x6194049f30f7200000" + }, + "cf2e2ad635e9861ae95cb9bafcca036b5281f5ce": { + "balance": "0x77432217e6836000000" + }, + "cf2e734042a355d05ffb2e3915b16811f45a695e": { + "balance": "0x6c6b935b8bbd400000" + }, + "cf348f2fe47b7e413c077a7baf3a75fbf8428692": { + "balance": "0x6c6b935b8bbd400000" + }, + "cf3f9128b07203a3e10d7d5755c0c4abc6e2cac2": { + "balance": "0x10f0cf064dd59200000" + }, + "cf3fbfa1fd32d7a6e0e6f8ef4eab57be34025c4c": { + "balance": "0x39a1c0f7594d480000" + }, + "cf4166746e1d3bc1f8d0714b01f17e8a62df1464": { + "balance": "0x3677036edf0af60000" + }, + "cf4f1138f1bd6bf5b6d485cce4c1017fcb85f07d": { + "balance": "0x2fd0bc77c32bff0000" + }, + "cf5a6f9df75579c644f794711215b30d77a0ce40": { + "balance": "0x6c6b935b8bbd400000" + }, + "cf5e0eacd1b39d0655f2f77535ef6608eb950ba0": { + "balance": "0x6c6b935b8bbd400000" + }, + "cf684dfb8304729355b58315e8019b1aa2ad1bac": { + "balance": "0x177224aa844c720000" + }, + "cf694081c76d18c64ca71382be5cd63b3cb476f8": { + "balance": "0x3635c9adc5dea00000" + }, + "cf6e52e6b77480b1867efec6446d9fc3cc3577e8": { + "balance": "0xc0901f6bd98790000" + }, + "cf883a20329667ea226a1e3c765dbb6bab32219f": { + "balance": "0xa4be3564d616660000" + }, + "cf8882359c0fb23387f5674074d8b17ade512f98": { + "balance": "0x14542ba12a337c00000" + }, + "cf89f7460ba3dfe83c5a1d3a019ee1250f242f0f": { + "balance": "0x356813cdcefd028000" + }, + "cf923a5d8fbc3d01aa079d1cfe4b43ce071b1611": { + "balance": "0x6c6b935b8bbd400000" + }, + "cf9be9b9ab86c66b59968e67b8d4dcff46b1814a": { + "balance": "0x23c757072b8dd00000" + }, + "cfa8b37127149bdbfee25c34d878510951ea10eb": { + "balance": "0x6c6b935b8bbd400000" + }, + "cfac2e1bf33205b05533691a02267ee19cd81836": { + "balance": "0x3635c9adc5dea00000" + }, + "cfbb32b7d024350e3321fa20c9a914035372ffc6": { + "balance": "0x15be6174e1912e0000" + }, + "cfc4e6f7f8b011414bfba42f23adfaa78d4ecc5e": { + "balance": "0x6449e84e47a8a80000" + }, + "cfd2728dfb8bdbf3bf73598a6e13eaf43052ea2b": { + "balance": "0x93739534d28680000" + }, + "cfd47493c9f89fe680bda5754dd7c9cfe7cb5bbe": { + "balance": "0x2f473513448fe0000" + }, + "cfde0fc75d6f16c443c3038217372d99f5d907f7": { + "balance": "0x83225e6396b5ec0000" + }, + "cfe2caaf3cec97061d0939748739bffe684ae91f": { + "balance": "0x21e19e0c9bab2400000" + }, + "cfeacaaed57285e0ac7268ce6a4e35ecfdb242d7": { + "balance": "0x3ae4d4240190600000" + }, + "cfecbea07c27002f65fe534bb8842d0925c78402": { + "balance": "0xd8d726b7177a800000" + }, + "cfee05c69d1f29e7714684c88de5a16098e91399": { + "balance": "0x6acb3df27e1f880000" + }, + "cff6a6fe3e9a922a12f21faa038156918c4fcb9c": { + "balance": "0x44591d67fecc80000" + }, + "cff7f89a4d4219a38295251331568210ffc1c134": { + "balance": "0x5f68e8131ecf800000" + }, + "cff8d06b00e3f50c191099ad56ba6ae26571cd88": { + "balance": "0x3635c9adc5dea00000" + }, + "cffc49c1787eebb2b56cabe92404b636147d4558": { + "balance": "0x133e0308f40a3da8000" + }, + "d008513b27604a89ba1763b6f84ce688b346945b": { + "balance": "0x3635c9adc5dea00000" + }, + "d00f067286c0fbd082f9f4a61083ec76deb3cee6": { + "balance": "0x3635c9adc5dea00000" + }, + "d015f6fcb84df7bb410e8c8f04894a881dcac237": { + "balance": "0x384524cc70b7780000" + }, + "d01af9134faf5257174e8b79186f42ee354e642d": { + "balance": "0x3635c9adc5dea00000" + }, + "d02108d2ae3cab10cbcf1657af223e027c8210f6": { + "balance": "0x6c6d84bccdd9ce0000" + }, + "d02afecf8e2ec2b62ac8ad204161fd1fae771d0e": { + "balance": "0x6c6b935b8bbd400000" + }, + "d0319139fbab2e8e2accc1d924d4b11df6696c5a": { + "balance": "0xad78ebc5ac6200000" + }, + "d037d215d11d1df3d54fbd321cd295c5465e273b": { + "balance": "0x4be4e7267b6ae00000" + }, + "d03a2da41e868ed3fef5745b96f5eca462ff6fda": { + "balance": "0xa2a15d09519be00000" + }, + "d03fc165576aaed525e5502c8e140f8b2e869639": { + "balance": "0x17356d8b32501c80000" + }, + "d043a011ec4270ee7ec8b968737515e503f83028": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d04b861b3d9acc563a901689941ab1e1861161a2": { + "balance": "0x1158e460913d00000" + }, + "d05a447c911dbb275bfb2e5a37e5a703a56f9997": { + "balance": "0xad78ebc5ac6200000" + }, + "d05ffb2b74f867204fe531653b0248e21c13544e": { + "balance": "0x3635c9adc5dea00000" + }, + "d062588171cf99bbeb58f126b870f9a3728d61ec": { + "balance": "0xf3f20b8dfa69d00000" + }, + "d0638ea57189a6a699024ad78c71d939c1c2ff8c": { + "balance": "0x8eae566710fc200000" + }, + "d0648a581b3508e135a2935d12c9657045d871ca": { + "balance": "0x1b2df9d219f57980000" + }, + "d071192966eb69c3520fca3aa4dd04297ea04b4e": { + "balance": "0x5f68e8131ecf80000" + }, + "d0718520eae0a4d62d70de1be0ca431c5eea2482": { + "balance": "0x6c6b935b8bbd400000" + }, + "d0775dba2af4c30a3a78365939cd71c2f9de95d2": { + "balance": "0x692ae8897081d00000" + }, + "d07be0f90997caf903c8ac1d53cde904fb190741": { + "balance": "0x36389038b699b40000" + }, + "d07e511864b1cf9969e3560602829e32fc4e71f5": { + "balance": "0x2b5e3af16b1880000" + }, + "d0809498c548047a1e2a2aa6a29cd61a0ee268bd": { + "balance": "0x6c6b935b8bbd400000" + }, + "d082275f745a2cac0276fbdb02d4b2a3ab1711fe": { + "balance": "0x1a055690d9db80000" + }, + "d08fc09a0030fd0928cd321198580182a76aae9f": { + "balance": "0x3635c9adc5dea00000" + }, + "d093e829819fd2e25b973800bb3d5841dd152d05": { + "balance": "0xd8d726b7177a800000" + }, + "d0944aa185a1337061ae20dc9dd96c83b2ba4602": { + "balance": "0xad78ebc5ac6200000" + }, + "d096565b7c7407d06536580355fdd6d239144aa1": { + "balance": "0xd8d726b7177a80000" + }, + "d09cb2e6082d693a13e8d2f68dd1dd8461f55840": { + "balance": "0x3635c9adc5dea00000" + }, + "d0a6c6f9e9c4b383d716b31de78d56414de8fa91": { + "balance": "0x1043561a8829300000" + }, + "d0a7209b80cf60db62f57d0a5d7d521a69606655": { + "balance": "0x8ac7230489e800000" + }, + "d0a8abd80a199b54b08b65f01d209c27fef0115b": { + "balance": "0x161c626dc61a2ef8000" + }, + "d0abcc70c0420e0e172f97d43b87d5e80c336ea9": { + "balance": "0x21e19e0c9bab2400000" + }, + "d0ae735d915e946866e1fea77e5ea466b5cadd16": { + "balance": "0x6c6b935b8bbd400000" + }, + "d0b11d6f2bce945e0c6a5020c3b52753f803f9d1": { + "balance": "0xad78ebc5ac6200000" + }, + "d0c101fd1f01c63f6b1d19bc920d9f932314b136": { + "balance": "0x43c33c1937564800000" + }, + "d0c55abf976fdc3db2afe9be99d499484d576c02": { + "balance": "0x3635c9adc5dea00000" + }, + "d0d0a2ad45f59a9dccc695d85f25ca46ed31a5a3": { + "balance": "0x2d89577d7d40200000" + }, + "d0d62c47ea60fb90a3639209bbfdd4d933991cc6": { + "balance": "0xa844a7424d9c80000" + }, + "d0db456178206f5c4430fe005063903c3d7a49a7": { + "balance": "0x26491e45a753c08000" + }, + "d0e194f34b1db609288509ccd2e73b6131a2538b": { + "balance": "0x36356633ebd8ea0000" + }, + "d0e35e047646e759f4517093d6408642517f084d": { + "balance": "0xd58fa46818eccb8000" + }, + "d0ee4d02cf24382c3090d3e99560de3678735cdf": { + "balance": "0x821ab0d44149800000" + }, + "d0f04f52109aebec9a7b1e9332761e9fe2b97bb5": { + "balance": "0xd8d726b7177a800000" + }, + "d0f9597811b0b992bb7d3757aa25b4c2561d32e2": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d10302faa1929a326904d376bf0b8dc93ad04c4c": { + "balance": "0x61093d7c2c6d380000" + }, + "d1100dd00fe2ddf18163ad964d0b69f1f2e9658a": { + "balance": "0x143120955b2506b0000" + }, + "d116f3dcd5db744bd008887687aa0ec9fd7292aa": { + "balance": "0x3635c9adc5dea00000" + }, + "d119417c46732cf34d1a1afb79c3e7e2cd8eece4": { + "balance": "0x6c6b935b8bbd400000" + }, + "d12d77ae01a92d35117bac705aacd982d02e74c1": { + "balance": "0x3635c9adc5dea00000" + }, + "d135794b149a18e147d16e621a6931f0a40a969a": { + "balance": "0x43c33c1937564800000" + }, + "d1432538e35b7664956ae495a32abdf041a7a21c": { + "balance": "0x42bf06b78ed3b500000" + }, + "d1438267231704fc7280d563adf4763844a80722": { + "balance": "0xad78ebc5ac6200000" + }, + "d1538e9a87e59ca9ec8e5826a5b793f99f96c4c3": { + "balance": "0x3635c9adc5dea00000" + }, + "d1648503b1ccc5b8be03fa1ec4f3ee267e6adf7b": { + "balance": "0x13befbf51eec0900000" + }, + "d1682c2159018dc3d07f08240a8c606daf65f8e1": { + "balance": "0x2a5a058fc295ed000000" + }, + "d171c3f2258aef35e599c7da1aa07300234da9a6": { + "balance": "0x6c6b935b8bbd400000" + }, + "d1778c13fbd968bc083cb7d1024ffe1f49d02caa": { + "balance": "0xd9ecb4fd208e500000" + }, + "d17fbe22d90462ed37280670a2ea0b3086a0d6d6": { + "balance": "0xad6eedd17cf3b8000" + }, + "d1811c55976980f083901d8a0db269222dfb5cfe": { + "balance": "0x54069233bf7f780000" + }, + "d18eb9e1d285dabe93e5d4bae76beefe43b521e8": { + "balance": "0x243d4d18229ca20000" + }, + "d193e583d6070563e7b862b9614a47e99489f3e5": { + "balance": "0x36356633ebd8ea0000" + }, + "d1978f2e34407fab1dc2183d95cfda6260b35982": { + "balance": "0x2ab7b260ff3fd00000" + }, + "d19caf39bb377fdf2cf19bd4fb52591c2631a63c": { + "balance": "0x3635c9adc5dea00000" + }, + "d1a396dcdab2c7494130b3fd307820340dfd8c1f": { + "balance": "0xf92250e2dfd00000" + }, + "d1a71b2d0858e83270085d95a3b1549650035e23": { + "balance": "0x327bb09d06aa8500000" + }, + "d1acb5adc1183973258d6b8524ffa28ffeb23de3": { + "balance": "0xd8d726b7177a800000" + }, + "d1b37f03cb107424e9c4dd575ccd4f4cee57e6cd": { + "balance": "0x6c6b935b8bbd400000" + }, + "d1b5a454ac3405bb4179208c6c84de006bcb9be9": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d1c45954a62b911ad701ff2e90131e8ceb89c95c": { + "balance": "0x4b91a2de457e880000" + }, + "d1c96e70f05ae0e6cd6021b2083750a7717cde56": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d1d5b17ffe2d7bbb79cc7d7930bcb2e518fb1bbf": { + "balance": "0xa2a15d09519be00000" + }, + "d1da0c8fb7c210e0f2ec618f85bdae7d3e734b1c": { + "balance": "0x6acb3df27e1f880000" + }, + "d1dd79fb158160e5b4e8e23f312e6a907fbc4d4e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d1de5aad3a5fd803f1b1aeb6103cb8e14fe723b7": { + "balance": "0x1158e460913d00000" + }, + "d1e1f2b9c16c309874dee7fac32675aff129c398": { + "balance": "0x3f24d8e4a00700000" + }, + "d1e5e234a9f44266a4a6241a84d7a1a55ad5a7fe": { + "balance": "0x43c33c1937564800000" + }, + "d1ea4d72a67b5b3e0f315559f52bd0614d713069": { + "balance": "0x6c6b935b8bbd400000" + }, + "d1ee905957fe7cc70ec8f2868b43fe47b13febff": { + "balance": "0x2629f66e0c5300000" + }, + "d1f1694d22671b5aad6a94995c369fbe6133676f": { + "balance": "0x3635c9adc5dea00000" + }, + "d1f4dc1ddb8abb8848a8b14e25f3b55a8591c266": { + "balance": "0xd8d726b7177a80000" + }, + "d1fed0aee6f5dfd7e25769254c3cfad15adeccaa": { + "balance": "0x2792c8fc4b53280000" + }, + "d2051cb3cb6704f0548cc890ab0a19db3415b42a": { + "balance": "0x121b2e5e6464780000" + }, + "d206aaddb336d45e7972e93cb075471d15897b5d": { + "balance": "0x2086ac351052600000" + }, + "d209482bb549abc4777bea6d7f650062c9c57a1c": { + "balance": "0x11651ac3e7a7580000" + }, + "d20dcb0b78682b94bc3000281448d557a20bfc83": { + "balance": "0x30849ebe16369c0000" + }, + "d2107b353726c3a2b46566eaa7d9f80b5d21dbe3": { + "balance": "0x1158e460913d00000" + }, + "d211b21f1b12b5096181590de07ef81a89537ead": { + "balance": "0x6c6b935b8bbd400000" + }, + "d218efb4db981cdd6a797f4bd48c7c26293ceb40": { + "balance": "0xa1466b31c6431c0000" + }, + "d21a7341eb84fd151054e5e387bb25d36e499c09": { + "balance": "0x2f6f10780d22cc00000" + }, + "d224f880f9479a89d32f09e52be990b288135cef": { + "balance": "0x3a9d5baa4abf1d00000" + }, + "d22f0ca4cd479e661775053bcc49e390f670dd8a": { + "balance": "0x3635c9adc5dea00000" + }, + "d231929735132102471ba59007b6644cc0c1de3e": { + "balance": "0x3637096c4bcc690000" + }, + "d235d15cb5eceebb61299e0e827fa82748911d89": { + "balance": "0xd8d726b7177a800000" + }, + "d23a24d7f9468343c143a41d73b88f7cbe63be5e": { + "balance": "0xad78ebc5ac6200000" + }, + "d23d7affacdc3e9f3dae7afcb4006f58f8a44600": { + "balance": "0xc328093e61ee400000" + }, + "d243184c801e5d79d2063f3578dbae81e7b3a9cb": { + "balance": "0x6bdca2681e1aba0000" + }, + "d24b6644f439c8051dfc64d381b8c86c75c17538": { + "balance": "0x6c6b935b8bbd400000" + }, + "d24bf12d2ddf457decb17874efde2052b65cbb49": { + "balance": "0x2f6f10780d22cc00000" + }, + "d251f903ae18727259eee841a189a1f569a5fd76": { + "balance": "0x21e19e0c9bab2400000" + }, + "d252960b0bf6b2848fdead80136db5f507f8be02": { + "balance": "0x6c6b935b8bbd400000" + }, + "d2581a55ce23ab10d8ad8c44378f59079bd6f658": { + "balance": "0x1dd0c885f9a0d800000" + }, + "d25aecd7eb8bd6345b063b5dbd271c77d3514494": { + "balance": "0x62a992e53a0af00000" + }, + "d27c234ff7accace3d996708f8f9b04970f97d36": { + "balance": "0x487a9a304539440000" + }, + "d28298524df5ec4b24b0ffb9df85170a145a9eb5": { + "balance": "0xf98a3b9b337e20000" + }, + "d283b8edb10a25528a4404de1c65e7410dbcaa67": { + "balance": "0x28a857425466f800000" + }, + "d284a50382f83a616d39b8a9c0f396e0ebbfa95d": { + "balance": "0x3636c25e66ece70000" + }, + "d288e7cb7ba9f620ab0f7452e508633d1c5aa276": { + "balance": "0xd8d726b7177a800000" + }, + "d29dc08efbb3d72e263f78ab7610d0226de76b00": { + "balance": "0x28a857425466f800000" + }, + "d2a030ac8952325f9e1db378a71485a24e1b07b2": { + "balance": "0x6c6b935b8bbd400000" + }, + "d2a479404347c5543aab292ae1bb4a6f158357fa": { + "balance": "0xd8d726b7177a800000" + }, + "d2a5a024230a57ccc666760b89b0e26cafd189c7": { + "balance": "0xa96595a5c6e8a3f8000" + }, + "d2a80327cbe55c4c7bd51ff9dde4ca648f9eb3f8": { + "balance": "0x2b5e3af16b1880000" + }, + "d2a84f75675c62d80c88756c428eee2bcb185421": { + "balance": "0x410d586a20a4c00000" + }, + "d2abd84a181093e5e229136f42d835e8235de109": { + "balance": "0x56be03ca3e47d8000" + }, + "d2ac0d3a58605e1d0f0eb3de25b2cad129ed6058": { + "balance": "0xd8d726b7177a800000" + }, + "d2bf67a7f3c6ce56b7be41675dbbadfe7ea93a33": { + "balance": "0x15af1d78b58c400000" + }, + "d2dbebe89b0357aea98bbe8e496338debb28e805": { + "balance": "0xd8d726b7177a800000" + }, + "d2e21ed56868fab28e0947927adaf29f23ebad6c": { + "balance": "0x6c184f1355d0e80000" + }, + "d2e817738abf1fb486583f80c350318bed860c80": { + "balance": "0xd02cecf5f5d810000" + }, + "d2edd1ddd6d86dc005baeb541d22b640d5c7cae5": { + "balance": "0x1158e460913d00000" + }, + "d2f1998e1cb1580cec4f6c047dcd3dcec54cf73c": { + "balance": "0xad78ebc5ac6200000" + }, + "d2f241255dd7c3f73c07043071ec08ddd9c5cde5": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d2ff672016f63b2f85398f4a6fedbb60a50d3cce": { + "balance": "0x1291246f5b734a0000" + }, + "d30d4c43adcf55b2cb53d68323264134498d89ce": { + "balance": "0x3635c9adc5dea00000" + }, + "d30ee9a12b4d68abace6baca9ad7bf5cd1faf91c": { + "balance": "0x514fcb24ff9c500000" + }, + "d3118ea3c83505a9d893bb67e2de142d537a3ee7": { + "balance": "0x1158e460913d00000" + }, + "d311bcd7aa4e9b4f383ff3d0d6b6e07e21e3705d": { + "balance": "0xad78ebc5ac6200000" + }, + "d315deea1d8c1271f9d1311263ab47c007afb6f5": { + "balance": "0x3c81d4e654b400000" + }, + "d32b2c79c36478c5431901f6d700b04dbe9b8810": { + "balance": "0x15779a9de6eeb00000" + }, + "d32b45564614516c91b07fa9f72dcf787cce4e1c": { + "balance": "0xfc66fae3746ac0000" + }, + "d330728131fe8e3a15487a34573c93457e2afe95": { + "balance": "0xd8d726b7177a800000" + }, + "d331c823825a9e5263d052d8915d4dcde07a5c37": { + "balance": "0x1e931283ccc8500000" + }, + "d333627445f2d787901ef33bb2a8a3675e27ffec": { + "balance": "0x15af1d78b58c400000" + }, + "d33cf82bf14c592640a08608914c237079d5be34": { + "balance": "0x6c6b935b8bbd400000" + }, + "d34d708d7398024533a5a2b2309b19d3c55171bb": { + "balance": "0x15af1d78b58c400000" + }, + "d34e03d36a2bd4d19a5fa16218d1d61e3ffa0b15": { + "balance": "0x1158e460913d000000" + }, + "d35075ca61fe59d123969c36a82d1ab2d918aa38": { + "balance": "0x90f534608a72880000" + }, + "d367009ab658263b62c2333a1c9e4140498e1389": { + "balance": "0x6c6b935b8bbd400000" + }, + "d3679a47df2d99a49b01c98d1c3e0c987ce1e158": { + "balance": "0xf2dc7d47f15600000" + }, + "d38fa2c4cc147ad06ad5a2f75579281f22a7cc1f": { + "balance": "0x43c33c1937564800000" + }, + "d39a5da460392b940b3c69bc03757bf3f2e82489": { + "balance": "0x17c83a97d6b6ca50000" + }, + "d39b7cbc94003fc948f0cde27b100db8ccd6e063": { + "balance": "0x15af1d78b58c400000" + }, + "d3a10ec7a5c9324999dd9e9b6bde7c911e584bda": { + "balance": "0x2086ac351052600000" + }, + "d3a941c961e8ca8b1070f23c6d6d0d2a758a4444": { + "balance": "0xad78ebc5ac6200000" + }, + "d3bb59fa31258be62f8ed232f1a7d47b4a0b41ee": { + "balance": "0x56bc75e2d63100000" + }, + "d3bc730937fa75d8452616ad1ef1fe7fffe0d0e7": { + "balance": "0x484e4ded2eae38000" + }, + "d3c24d4b3a5e0ff8a4622d518edd73f16ab28610": { + "balance": "0x1158e460913d00000" + }, + "d3c6f1e0f50ec3d2a67e6bcd193ec7ae38f1657f": { + "balance": "0x166c5480889db770000" + }, + "d3d6e9fb82542fd29ed9ea3609891e151396b6f7": { + "balance": "0xb6f588aa7bcf5c00000" + }, + "d3dad1b6d08d4581ccae65a8732db6ac69f0c69e": { + "balance": "0x14542ba12a337c00000" + }, + "d3df3b53cb3b4755de54e180451cc44c9e8ae0aa": { + "balance": "0x23c49409b977828000" + }, + "d3f873bd9956135789ab00ebc195b922e94b259d": { + "balance": "0x6c6b935b8bbd400000" + }, + "d402b4f6a099ebe716cb14df4f79c0cd01c6071b": { + "balance": "0x6c6b935b8bbd400000" + }, + "d40d0055fd9a38488aff923fd03d35ec46d711b3": { + "balance": "0x10f08eda8e555098000" + }, + "d40ed66ab3ceff24ca05ecd471efb492c15f5ffa": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d418870bc2e4fa7b8a6121ae0872d55247b62501": { + "balance": "0x55a6e79ccd1d300000" + }, + "d41d7fb49fe701baac257170426cc9b38ca3a9b2": { + "balance": "0x98a7d9b8314c00000" + }, + "d4205592844055b3c7a1f80cefe3b8eb509bcde7": { + "balance": "0x9b3bfd342a9fc8000" + }, + "d42b20bd0311608b66f8a6d15b2a95e6de27c5bf": { + "balance": "0x6c6b935b8bbd400000" + }, + "d4344f7d5cad65d17e5c2d0e7323943d6f62fe92": { + "balance": "0xe7eeba3410b740000" + }, + "d43ee438d83de9a37562bb4e286cb1bd19f4964d": { + "balance": "0x3635c9adc5dea00000" + }, + "d44334b4e23a169a0c16bd21e866bba52d970587": { + "balance": "0x8cf23f909c0fa00000" + }, + "d44d81e18f46e2cfb5c1fcf5041bc8569767d100": { + "balance": "0x7b442e684f65aa40000" + }, + "d44f4ac5fad76bdc1537a3b3af6472319b410d9d": { + "balance": "0x56bc75e2d631000000" + }, + "d44f5edf2bcf2433f211dadd0cc450db1b008e14": { + "balance": "0xe7eeba3410b740000" + }, + "d44f6ac3923b5fd731a4c45944ec4f7ec52a6ae4": { + "balance": "0x21e19e0c9bab2400000" + }, + "d45b3341e8f15c80329320c3977e3b90e7826a7e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d45d5daa138dd1d374c71b9019916811f4b20a4e": { + "balance": "0x1f399b1438a1000000" + }, + "d460a4b908dd2b056759b488850b66a838fc77a8": { + "balance": "0x6acb3df27e1f880000" + }, + "d467cf064c0871989b90d8b2eb14ccc63b360823": { + "balance": "0xad78ebc5ac6200000" + }, + "d46bae61b027e5bb422e83a3f9c93f3c8fc77d27": { + "balance": "0x6c6b935b8bbd400000" + }, + "d46f8223452982a1eea019a8816efc2d6fc00768": { + "balance": "0x76d41c62494840000" + }, + "d475477fa56390d33017518d6711027f05f28dbf": { + "balance": "0x6b111333d4fd4c0000" + }, + "d47c242edffea091bc54d57df5d1fdb93101476c": { + "balance": "0x9df7dfa8f760480000" + }, + "d47d8685faee147c520fd986709175bf2f886bef": { + "balance": "0x6c6b935b8bbd400000" + }, + "d47f50df89a1cff96513bef1b2ae3a2971accf2c": { + "balance": "0x2d89577d7d40200000" + }, + "d482e7f68e41f238fe517829de15477fe0f6dd1d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d4879fd12b1f3a27f7e109761b23ca343c48e3d8": { + "balance": "0x241a9b4f617a280000" + }, + "d48e3f9357e303513841b3f84bda83fc89727587": { + "balance": "0x3635c9adc5dea00000" + }, + "d49a75bb933fca1fca9aa1303a64b6cb44ea30e1": { + "balance": "0x21e19e0c9bab2400000" + }, + "d4b085fb086f3d0d68bf12926b1cc3142cae8770": { + "balance": "0xc893d09c8f51500000" + }, + "d4b2ff3bae1993ffea4d3b180231da439f7502a2": { + "balance": "0x6c6b935b8bbd400000" + }, + "d4b38a5fdb63e01714e9801db47bc990bd509183": { + "balance": "0x14534d95bef905c0000" + }, + "d4b8bdf3df9a51b0b91d16abbea05bb4783c8661": { + "balance": "0x3635c9adc5dea00000" + }, + "d4c4d1a7c3c74984f6857b2f5f07e8face68056d": { + "balance": "0x6c6b935b8bbd400000" + }, + "d4c6ac742e7c857d4a05a04c33d4d05c1467571d": { + "balance": "0xad78ebc5ac6200000" + }, + "d4cb21e590c5a0e06801366aff342c7d7db16424": { + "balance": "0x1ac7a08ead02f80000" + }, + "d4d92c62b280e00f626d8657f1b86166cb1f740f": { + "balance": "0xad7f23634cbd60000" + }, + "d4ebb1929a23871cf77fe049ab9602be08be0a73": { + "balance": "0x678a932062e4180000" + }, + "d4ee4919fb37f2bb970c3fff54aaf1f3dda6c03f": { + "balance": "0x878678326eac9000000" + }, + "d4feed99e8917c5c5458635f3603ecb7e817a7d0": { + "balance": "0x1043c43cde1d398000" + }, + "d4ff46203efa23064b1caf00516e28704a82a4f8": { + "balance": "0x487a9a304539440000" + }, + "d500e4d1c9824ba9f5b635cfa3a8c2c38bbd4ced": { + "balance": "0x15af1d78b58c400000" + }, + "d508d39c70916f6abc4cc7f999f011f077105802": { + "balance": "0x5724d24afe77f0000" + }, + "d50f7fa03e389876d3908b60a537a6706304fb56": { + "balance": "0x56bc75e2d63100000" + }, + "d513a45080ff2febe62cd5854abe29ee4467f996": { + "balance": "0x84e13bc4fc5d80000" + }, + "d5276f0cd5ffd5ffb63f98b5703d5594ede0838b": { + "balance": "0x15af1d78b58c400000" + }, + "d5294b666242303b6df0b1c88d37429bc8c965aa": { + "balance": "0x104d0d00d2b7f60000" + }, + "d52aecc6493938a28ca1c367b701c21598b6a02e": { + "balance": "0x3ba1910bf341b00000" + }, + "d53c567f0c3ff2e08b7d59e2b5c73485437fc58d": { + "balance": "0x2086ac351052600000" + }, + "d541ac187ad7e090522de6da3213e9a7f4439673": { + "balance": "0x6c6b935b8bbd400000" + }, + "d54ba2d85681dc130e5b9b02c4e8c851391fd9b9": { + "balance": "0xd5967be4fc3f100000" + }, + "d55508adbbbe9be81b80f97a6ea89add68da674f": { + "balance": "0x6c6b935b8bbd400000" + }, + "d5550caaf743b037c56fd2558a1c8ed235130750": { + "balance": "0x121e4d49036255b0000" + }, + "d5586da4e59583c8d86cccf71a86197f17996749": { + "balance": "0x6c6b935b8bbd400000" + }, + "d55c1c8dfbe1e02cacbca60fdbdd405b09f0b75f": { + "balance": "0x6c6b935b8bbd400000" + }, + "d561cbbc05515de73ab8cf9eae1357341e7dfdf4": { + "balance": "0x14542ba12a337c00000" + }, + "d56a144d7af0ae8df649abae535a15983aa04d02": { + "balance": "0x10f0cf064dd59200000" + }, + "d572309169b1402ec8131a17a6aac3222f89e6eb": { + "balance": "0x2ec1978c47766a00000" + }, + "d5787668c2c5175b01a8ee1ac3ecc9c8b2aba95a": { + "balance": "0x6c6acc67d7b1d40000" + }, + "d588c3a5df228185d98ee7e60748255cdea68b01": { + "balance": "0xd8d726b7177a800000" + }, + "d58a52e078a805596b0d56ea4ae1335af01c66eb": { + "balance": "0xe7eeba3410b740000" + }, + "d5903e9978ee20a38c3f498d63d57f31a39f6a06": { + "balance": "0x232b36ffc672ab00000" + }, + "d59638d3c5faa7711bf085745f9d5bdc23d498d8": { + "balance": "0x6c6b935b8bbd400000" + }, + "d59d92d2c8701980cc073c375d720af064743c0c": { + "balance": "0x405fdf7e5af85e00000" + }, + "d5a7bec332adde18b3104b5792546aa59b879b52": { + "balance": "0x6c6b935b8bbd400000" + }, + "d5b117ec116eb846418961eb7edb629cd0dd697f": { + "balance": "0xa2a15d09519be00000" + }, + "d5b284040130abf7c1d163712371cc7e28ad66da": { + "balance": "0x6acb3df27e1f880000" + }, + "d5b9d277d8aad20697a51f76e20978996bffe055": { + "balance": "0x7c3fe3c076ab50000" + }, + "d5bd5e8455c130169357c471e3e681b7996a7276": { + "balance": "0x2d9e288f8abb360000" + }, + "d5cba5b26bea5d73fabb1abafacdef85def368cc": { + "balance": "0xad78ebc5ac6200000" + }, + "d5ce55d1b62f59433c2126bcec09bafc9dfaa514": { + "balance": "0xaadec983fcff40000" + }, + "d5e55100fbd1956bbed2ca518d4b1fa376032b0b": { + "balance": "0x56bc75e2d63100000" + }, + "d5e5c135d0c4c3303934711993d0d16ff9e7baa0": { + "balance": "0x6c6b935b8bbd400000" + }, + "d5e656a1b916f9bf45afb07dd8afaf73b4c56f41": { + "balance": "0x542253a126ce40000" + }, + "d5ea472cb9466018110af00c37495b5c2c713112": { + "balance": "0x10eee686c854f440000" + }, + "d5f07552b5c693c20067b378b809cee853b8f136": { + "balance": "0x1b67c6df88c6fa0000" + }, + "d5f7c41e07729dfa6dfc64c4423160a22c609fd3": { + "balance": "0x61093d7c2c6d380000" + }, + "d604abce4330842e3d396ca73ddb5519ed3ec03f": { + "balance": "0x8e31fe1689d8a0000" + }, + "d60651e393783423e5cc1bc5f889e44ef7ea243e": { + "balance": "0x159e76371129c80000" + }, + "d609bf4f146eea6b0dc8e06ddcf4448a1fccc9fa": { + "balance": "0x6c6b935b8bbd400000" + }, + "d609ec0be70d0ad26f6e67c9d4762b52ee51122c": { + "balance": "0x3635c9adc5dea00000" + }, + "d60a52580728520df7546bc1e283291788dbae0c": { + "balance": "0x363489ef3ff0d70000" + }, + "d60b247321a32a5affb96b1e279927cc584de943": { + "balance": "0x7ad020d6ddd7760000" + }, + "d6110276cfe31e42825a577f6b435dbcc10cf764": { + "balance": "0x3635c9adc5dea00000" + }, + "d612597bc31743c78633f633f239b1e9426bd925": { + "balance": "0x1017f7df96be17800000" + }, + "d6234aaf45c6f22e66a225ffb93add629b4ef80f": { + "balance": "0x3635c9adc5dea00000" + }, + "d62edb96fce2969aaf6c545e967cf1c0bc805205": { + "balance": "0x4a565536a5ada8000" + }, + "d6300b3215b11de762ecde4b70b7927d01291582": { + "balance": "0x6c6b935b8bbd400000" + }, + "d6395db5a4bb66e60f4cfbcdf0057bb4d97862e2": { + "balance": "0x3154c9729d05780000" + }, + "d64a2d50f8858537188a24e0f50df1681ab07ed7": { + "balance": "0x8375a2abcca24400000" + }, + "d6580ab5ed4c7dfa506fa6fe64ad5ce129707732": { + "balance": "0xd8d726b7177a800000" + }, + "d6598b1386e93c5ccb9602ff4bbbecdbd3701dc4": { + "balance": "0xc25f4ecb041f00000" + }, + "d6644d40e90bc97fe7dfe7cabd3269fd579ba4b3": { + "balance": "0x89e917994f71c0000" + }, + "d6670c036df754be43dadd8f50feea289d061fd6": { + "balance": "0x144a2903448cef78000" + }, + "d668523a90f0293d65c538d2dd6c57673710196e": { + "balance": "0x2242c30b853ee0000" + }, + "d66ab79294074c8b627d842dab41e17dd70c5de5": { + "balance": "0x3635c9adc5dea00000" + }, + "d66acc0d11b689cea6d9ea5ff4014c224a5dc7c4": { + "balance": "0xfc936392801c0000" + }, + "d66ddf1159cf22fd8c7a4bc8d5807756d433c43e": { + "balance": "0x77432217e683600000" + }, + "d687cec0059087fdc713d4d2d65e77daefedc15f": { + "balance": "0x340aad21b3b700000" + }, + "d688e785c98f00f84b3aa1533355c7a258e87948": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "d6a22e598dabd38ea6e958bd79d48ddd9604f4df": { + "balance": "0x3635c9adc5dea00000" + }, + "d6a7ac4de7b510f0e8de519d973fa4c01ba83400": { + "balance": "0x65ea3db75546600000" + }, + "d6acc220ba2e51dfcf21d443361eea765cbd35d8": { + "balance": "0x1158e460913d00000" + }, + "d6acffd0bfd99c382e7bd56ff0e6144a9e52b08e": { + "balance": "0x8ac7230489e800000" + }, + "d6c0d0bc93a62e257174700e10f024c8b23f1f87": { + "balance": "0x6c6b935b8bbd400000" + }, + "d6cf5c1bcf9da662bcea2255905099f9d6e84dcc": { + "balance": "0x1c49e420157d9c20000" + }, + "d6d03572a45245dbd4368c4f82c95714bd2167e2": { + "balance": "0x3f00c3d66686fc0000" + }, + "d6d6776958ee23143a81adadeb08382009e996c2": { + "balance": "0xa2a15d09519be00000" + }, + "d6d9e30f0842012a7176a917d9d2048ca0738759": { + "balance": "0xd8d726b7177a800000" + }, + "d6e09e98fe1300332104c1ca34fbfac554364ed9": { + "balance": "0x6c6b935b8bbd400000" + }, + "d6e8e97ae9839b9ee507eedb28edfb7477031439": { + "balance": "0x6c6b935b8bbd400000" + }, + "d6eea898d4ae2b718027a19ce9a5eb7300abe3ca": { + "balance": "0x17d4aceee63db8000" + }, + "d6f1e55b1694089ebcb4fe7d7882aa66c8976176": { + "balance": "0x43c23bdbe929db30000" + }, + "d6f4a7d04e8faf20e8c6eb859cf7f78dd23d7a15": { + "balance": "0x724ded1c748140000" + }, + "d6fc0446c6a8d40ae3551db7e701d1fa876e4a49": { + "balance": "0x6c6b935b8bbd400000" + }, + "d703c6a4f11d60194579d58c2766a7ef16c30a29": { + "balance": "0x6c6b935b8bbd400000" + }, + "d7052519756af42590f15391b723a03fa564a951": { + "balance": "0xfa3631480d01fd8000" + }, + "d70a612bd6dda9eab0dddcff4aaf4122d38feae4": { + "balance": "0x1d460162f516f00000" + }, + "d70ad2c4e9eebfa637ef56bd486ad2a1e5bce093": { + "balance": "0xad78ebc5ac6200000" + }, + "d7140c8e5a4307fab0cc27badd9295018bf87970": { + "balance": "0x5f1016b5076d00000" + }, + "d7164aa261c09ad9b2b5068d453ed8eb6aa13083": { + "balance": "0xa2a15d09519be00000" + }, + "d71e43a45177ad51cbe0f72184a5cb503917285a": { + "balance": "0xad78ebc5ac6200000" + }, + "d71fb130f0150c565269e00efb43902b52a455a6": { + "balance": "0xad78ebc5ac6200000" + }, + "d7225738dcf3578438f8e7c8b3837e42e04a262f": { + "balance": "0x182b8cebbb83aa0000" + }, + "d7274d50804d9c77da93fa480156efe57ba501de": { + "balance": "0x692ae8897081d00000" + }, + "d731bb6b5f3c37395e09ceaccd14a918a6060789": { + "balance": "0xd5967be4fc3f100000" + }, + "d73ed2d985b5f21b55b274643bc6da031d8edd8d": { + "balance": "0xa6dd90cae5114480000" + }, + "d744ac7e5310be696a63b003c40bd039370561c6": { + "balance": "0x5a87e7d7f5f6580000" + }, + "d74a6e8d6aab34ce85976814c1327bd6ea0784d2": { + "balance": "0x152d02c7e14af6800000" + }, + "d75a502a5b677287470f65c5aa51b87c10150572": { + "balance": "0x3130b4646385740000" + }, + "d76dbaebc30d4ef67b03e6e6ecc6d84e004d502d": { + "balance": "0x6d76b9188e13850000" + }, + "d771d9e0ca8a08a113775731434eb3270599c40d": { + "balance": "0x1158e460913d00000" + }, + "d7788ef28658aa06cc53e1f3f0de58e5c371be78": { + "balance": "0x16a6502f15a1e540000" + }, + "d77892e2273b235d7689e430e7aeed9cbce8a1f3": { + "balance": "0x6c6b935b8bbd400000" + }, + "d781f7fc09184611568570b4986e2c72872b7ed0": { + "balance": "0x1159561065d5d0000" + }, + "d785a8f18c38b9bc4ffb9b8fa8c7727bd642ee1c": { + "balance": "0x3635c9adc5dea00000" + }, + "d78ecd25adc86bc2051d96f65364866b42a426b7": { + "balance": "0xd23058bf2f26120000" + }, + "d78f84e38944a0e0255faece48ba4950d4bd39d2": { + "balance": "0x10f0cf064dd59200000" + }, + "d79483f6a8444f2549d611afe02c432d15e11051": { + "balance": "0x1158e460913d00000" + }, + "d79835e404fb86bf845fba090d6ba25e0c8866a6": { + "balance": "0x821ab0d44149800000" + }, + "d79aff13ba2da75d46240cac0a2467c656949823": { + "balance": "0x5dc892aa1131c80000" + }, + "d79db5ab43621a7a3da795e58929f3dd25af67d9": { + "balance": "0x6c6acc67d7b1d40000" + }, + "d7a1431ee453d1e49a0550d1256879b4f5d10201": { + "balance": "0x5a87e7d7f5f6580000" + }, + "d7ad09c6d32657685355b5c6ec8e9f57b4ebb982": { + "balance": "0x6acb3df27e1f880000" + }, + "d7b740dff8c457668fdf74f6a266bfc1dcb723f9": { + "balance": "0x1158e460913d00000" + }, + "d7c2803ed7b0e0837351411a8e6637d168bc5b05": { + "balance": "0x641daf5c91bd9358000" + }, + "d7c6265dea11876c903b718e4cd8ab24fe265bde": { + "balance": "0x6c6b935b8bbd400000" + }, + "d7ca7fdcfebe4588eff5421d1522b61328df7bf3": { + "balance": "0xd8e6001e6c302b0000" + }, + "d7cdbd41fff20df727c70b6255c1ba7606055468": { + "balance": "0xad78ebc5ac6200000" + }, + "d7d157e4c0a96437a6d285741dd23ec4361fa36b": { + "balance": "0x6c6b935b8bbd400000" + }, + "d7d2c6fca8ad1f75395210b57de5dfd673933909": { + "balance": "0x126e72a69a50d00000" + }, + "d7d3c75920590438b82c3e9515be2eb6ed7a8b1a": { + "balance": "0xcb49b44ba602d800000" + }, + "d7d7f2caa462a41b3b30a34aeb3ba61010e2626f": { + "balance": "0x6c6b935b8bbd400000" + }, + "d7e74afdbad55e96cebc5a374f2c8b768680f2b0": { + "balance": "0x55de6a779bbac0000" + }, + "d7eb903162271c1afa35fe69e37322c8a4d29b11": { + "balance": "0x21e19e0c9bab2400000" + }, + "d7ebddb9f93987779b680155375438db65afcb6a": { + "balance": "0x5741afeff944c0000" + }, + "d7ef340e66b0d7afcce20a19cb7bfc81da33d94e": { + "balance": "0xa2a15d09519be00000" + }, + "d7f370d4bed9d57c6f49c999de729ee569d3f4e4": { + "balance": "0xad78ebc5ac6200000" + }, + "d7fa5ffb6048f96fb1aba09ef87b1c11dd7005e4": { + "balance": "0x3635c9adc5dea00000" + }, + "d8069f84b521493f4715037f3226b25f33b60586": { + "balance": "0x678a932062e4180000" + }, + "d815e1d9f4e2b5e57e34826b7cfd8881b8546890": { + "balance": "0xf015f25736420000" + }, + "d81bd54ba2c44a6f6beb1561d68b80b5444e6dc6": { + "balance": "0x3f170d7ee43c430000" + }, + "d82251456dc1380f8f5692f962828640ab9f2a03": { + "balance": "0x1088b53b2c202be0000" + }, + "d82c6fedbdac98af2eed10b00f32b00056ca5a6d": { + "balance": "0xad78ebc5ac6200000" + }, + "d82fd9fdf6996bedad2843159c06f37e0924337d": { + "balance": "0x5b8ccedc5aa7b00000" + }, + "d83ad260e9a6f432fb6ea28743299b4a09ad658c": { + "balance": "0x6c6b935b8bbd400000" + }, + "d843ee0863ce933e22f89c802d31287b9671e81c": { + "balance": "0xb98bc829a6f90000" + }, + "d84b922f7841fc5774f00e14604ae0df42c8551e": { + "balance": "0xd96fce90cfabcc0000" + }, + "d855b03ccb029a7747b1f07303e0a664793539c8": { + "balance": "0x6c6b935b8bbd400000" + }, + "d85fdeaf2a61f95db902f9b5a53c9b8f9266c3ac": { + "balance": "0x6cf65a7e9047280000" + }, + "d8715ef9176f850b2e30eb8e382707f777a6fbe9": { + "balance": "0x6c6b935b8bbd400000" + }, + "d874b9dfae456a929ba3b1a27e572c9b2cecdfb3": { + "balance": "0x93739534d28680000" + }, + "d8930a39c77357c30ad3a060f00b06046331fd62": { + "balance": "0x2c73c937742c500000" + }, + "d89bc271b27ba3ab6962c94a559006ae38d5f56a": { + "balance": "0x6c6b935b8bbd400000" + }, + "d8b77db9b81bbe90427b62f702b201ffc29ff618": { + "balance": "0x326d1e4396d45c0000" + }, + "d8cd64e0284eec53aa4639afc4750810b97fab56": { + "balance": "0x1158e460913d00000" + }, + "d8d64384249b776794063b569878d5e3b530a4b2": { + "balance": "0x9a043d0b2f9568000" + }, + "d8d65420c18c2327cc5af97425f857e4a9fd51b3": { + "balance": "0x5f68e8131ecf800000" + }, + "d8e5c9675ef4deed266b86956fc4590ea7d4a27d": { + "balance": "0x3635c9adc5dea00000" + }, + "d8e8474292e7a051604ca164c0707783bb2885e8": { + "balance": "0x2d4ca05e2b43ca80000" + }, + "d8eb78503ec31a54a90136781ae109004c743257": { + "balance": "0x3635c9adc5dea00000" + }, + "d8eef4cf4beb01ee20d111748b61cb4d3f641a01": { + "balance": "0x9489237adb9a500000" + }, + "d8f4bae6f84d910d6d7d5ac914b1e68372f94135": { + "balance": "0x56bc75e2d63100000" + }, + "d8f62036f03b7635b858f1103f8a1d9019a892b6": { + "balance": "0x2b5e3af16b1880000" + }, + "d8f665fd8cd5c2bcc6ddc0a8ae521e4dc6aa6060": { + "balance": "0x5c283d410394100000" + }, + "d8f9240c55cff035523c6d5bd300d370dc8f0c95": { + "balance": "0xf732b66015a540000" + }, + "d8f94579496725b5cb53d7985c989749aff849c0": { + "balance": "0x39992648a23c8a00000" + }, + "d8fdf546674738c984d8fab857880b3e4280c09e": { + "balance": "0x1158e460913d00000" + }, + "d8fe088fffce948f5137ee23b01d959e84ac4223": { + "balance": "0xc5b54a94fc0170000" + }, + "d90f3009db437e4e11c780bec8896f738d65ef0d": { + "balance": "0xd8d726b7177a800000" + }, + "d9103bb6b67a55a7fece2d1af62d457c2178946d": { + "balance": "0x3635c9adc5dea00000" + }, + "d913f0771949753c4726acaa2bd3619c5c20ff77": { + "balance": "0xa2a15d09519be00000" + }, + "d91d889164479ce436ece51763e22cda19b22d6b": { + "balance": "0xb66d88126800880000" + }, + "d929c65d69d5bbaea59762662ef418bc21ad924a": { + "balance": "0x3635c9adc5dea00000" + }, + "d930b27a78876485d0f48b70dd5336549679ca8f": { + "balance": "0x22b1c8c1227a00000" + }, + "d931ac2668ba6a84481ab139735aec14b7bfbabf": { + "balance": "0x6c6b935b8bbd400000" + }, + "d9383d4b6d17b3f9cd426e10fb944015c0d44bfb": { + "balance": "0x2b5e3af16b18800000" + }, + "d942de4784f7a48716c0fd4b9d54a6e54c5f2f3e": { + "balance": "0x43c33c1937564800000" + }, + "d944c8a69ff2ca1249690c1229c7192f36251062": { + "balance": "0x6acb3df27e1f880000" + }, + "d94a57882a52739bbe2a0647c80c24f58a2b4f1c": { + "balance": "0x48b54e2adbe12b0000" + }, + "d95342953c8a21e8b635eefac7819bea30f17047": { + "balance": "0x13f06c7ffef05d400000" + }, + "d95c90ffbe5484864780b867494a83c89256d6e4": { + "balance": "0x58e7926ee858a00000" + }, + "d96711540e2e998343d4f590b6fc8fac3bb8b31d": { + "balance": "0x5f5a4068b71cb00000" + }, + "d96ac2507409c7a383ab2eee1822a5d738b36b56": { + "balance": "0xad78ebc5ac6200000" + }, + "d96db33b7b5a950c3efa2dc31b10ba10a532ef87": { + "balance": "0x6c6b935b8bbd400000" + }, + "d9775965b716476675a8d513eb14bbf7b07cd14a": { + "balance": "0x1132e6d2d23c5e40000" + }, + "d97bc84abd47c05bbf457b2ef659d61ca5e5e48f": { + "balance": "0x69d17119dc5a80000" + }, + "d97f4526dea9b163f8e8e33a6bcf92fb907de6ec": { + "balance": "0xf654aaf4db2f00000" + }, + "d97fe6f53f2a58f6d76d752adf74a8a2c18e9074": { + "balance": "0x10cdf9b69a43570000" + }, + "d99999a2490d9494a530cae4daf38554f4dd633e": { + "balance": "0x68155a43676e00000" + }, + "d99df7421b9382e42c89b006c7f087702a0757c0": { + "balance": "0x1a055690d9db800000" + }, + "d9b783d31d32adc50fa3eacaa15d92b568eaeb47": { + "balance": "0x733af90374c1b280000" + }, + "d9d370fec63576ab15b318bf9e58364dc2a3552a": { + "balance": "0x56bc75e2d63100000" + }, + "d9d42fd13ebd4bf69cac5e9c7e82483ab46dd7e9": { + "balance": "0x121ea68c114e5100000" + }, + "d9e27eb07dfc71a706060c7f079238ca93e88539": { + "balance": "0x3635c9adc5dea00000" + }, + "d9e3857efd1e202a441770a777a49dcc45e2e0d3": { + "balance": "0xc1daf81d8a3ce0000" + }, + "d9ec2efe99ff5cf00d03a8317b92a24aef441f7e": { + "balance": "0x6c6b935b8bbd400000" + }, + "d9ec8fe69b7716c0865af888a11b2b12f720ed33": { + "balance": "0xd8d726b7177a800000" + }, + "d9f1b26408f0ec67ad1d0d6fe22e8515e1740624": { + "balance": "0x14d1120d7b1600000" + }, + "d9f547f2c1de0ed98a53d161df57635dd21a00bd": { + "balance": "0x556f64c1fe7fa0000" + }, + "d9ff115d01266c9f73b063c1c238ef3565e63b36": { + "balance": "0x24dce54d34a1a00000" + }, + "da06044e293c652c467fe74146bf185b21338a1c": { + "balance": "0x3635c9adc5dea00000" + }, + "da0b48e489d302b4b7bf204f957c1c9be383b0df": { + "balance": "0x6c6b935b8bbd400000" + }, + "da0d4b7ef91fb55ad265f251142067f10376ced6": { + "balance": "0x43c33c1937564800000" + }, + "da10978a39a46ff0bb848cf65dd9c77509a6d70e": { + "balance": "0x6c6b935b8bbd400000" + }, + "da16dd5c3d1a2714358fe3752cae53dbab2be98c": { + "balance": "0x41bad155e6512200000" + }, + "da214c023e2326ff696c00393168ce46ffac39ec": { + "balance": "0x3635c9adc5dea00000" + }, + "da2a14f9724015d79014ed8e5909681d596148f1": { + "balance": "0x2a10f0f8a91ab8000" + }, + "da2ad58e77deddede2187646c465945a8dc3f641": { + "balance": "0x23c757072b8dd00000" + }, + "da3017c150dd0dce7fcf881b0a48d0d1c756c4c7": { + "balance": "0x56bf91b1a65eb0000" + }, + "da34b2eae30bafe8daeccde819a794cd89e09549": { + "balance": "0x6c6b935b8bbd400000" + }, + "da4a5f557f3bab390a92f49b9b900af30c46ae80": { + "balance": "0x21e19e0c9bab2400000" + }, + "da505537537ffb33c415fec64e69bae090c5f60f": { + "balance": "0x8ac7230489e800000" + }, + "da698d64c65c7f2b2c7253059cd3d181d899b6b7": { + "balance": "0x1004e2e45fb7ee0000" + }, + "da7732f02f2e272eaf28df972ecc0ddeed9cf498": { + "balance": "0xb20bfbf6967890000" + }, + "da7ad025ebde25d22243cb830ea1d3f64a566323": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "da855d53477f505ec4c8d5e8bb9180d38681119c": { + "balance": "0x12f939c99edab800000" + }, + "da875e4e2f3cabe4f37e0eaed7d1f6dcc6ffef43": { + "balance": "0x6c6b935b8bbd400000" + }, + "da8bbee182e455d2098acb338a6d45b4b17ed8b6": { + "balance": "0x6c6b935b8bbd400000" + }, + "da982e9643ffece723075a40fe776e5ace04b29b": { + "balance": "0x8b8b6c9999bf20000" + }, + "da9f55460946d7bfb570ddec757ca5773b58429a": { + "balance": "0x1b845d769eb4480000" + }, + "daa1bd7a9148fb865cd612dd35f162861d0f3bdc": { + "balance": "0xa638ab72d92c138000" + }, + "daa63cbda45dd487a3f1cd4a746a01bb5e060b90": { + "balance": "0x10416d9b02a89240000" + }, + "daa776a6754469d7b9267a89b86725e740da0fa0": { + "balance": "0x6acb3df27e1f880000" + }, + "daac91c1e859d5e57ed3084b50200f9766e2c52b": { + "balance": "0x15af1d78b58c400000" + }, + "daacdaf42226d15cb1cf98fa15048c7f4ceefe69": { + "balance": "0x1043561a8829300000" + }, + "dab6bcdb83cf24a0ae1cb21b3b5b83c2f3824927": { + "balance": "0xa968163f0a57b400000" + }, + "dabb0889fc042926b05ef57b2520910abc4b4149": { + "balance": "0x6c6b935b8bbd400000" + }, + "dabc225042a6592cfa13ebe54efa41040878a5a2": { + "balance": "0xe11fad5d85ca30000" + }, + "dac0c177f11c5c3e3e78f2efd663d13221488574": { + "balance": "0x3635c9adc5dea00000" + }, + "dad136b88178b4837a6c780feba226b98569a94c": { + "balance": "0xad78ebc5ac6200000" + }, + "dadbfafd8b62b92a24efd75256dd83abdbd7bbdb": { + "balance": "0x11164759ffb320000" + }, + "dadc00ab7927603c2fcf31cee352f80e6c4d6351": { + "balance": "0x6c66e9a55378b80000" + }, + "dae0d33eaa341569fa9ff5982684854a4a328a6e": { + "balance": "0x3635c9adc5dea00000" + }, + "dae7201eab8c063302930d693929d07f95e71962": { + "balance": "0x91aec028b419810000" + }, + "daedd4ad107b271e89486cbf80ebd621dd974578": { + "balance": "0x6c6b935b8bbd400000" + }, + "db04fad9c49f9e880beb8fcf1d3a3890e4b3846f": { + "balance": "0x435ae6cc0c58e50000" + }, + "db0cc78f74d9827bdc8a6473276eb84fdc976212": { + "balance": "0x6c6b935b8bbd400000" + }, + "db1293a506e90cad2a59e1b8561f5e66961a6788": { + "balance": "0x6c6b935b8bbd400000" + }, + "db19a3982230368f0177219cb10cb259cdb2257c": { + "balance": "0x6c6b935b8bbd400000" + }, + "db23a6fef1af7b581e772cf91882deb2516fc0a7": { + "balance": "0xad78ebc5ac6200000" + }, + "db244f97d9c44b158a40ed9606d9f7bd38913331": { + "balance": "0x58788cb94b1d80000" + }, + "db288f80ffe232c2ba47cc94c763cf6fc9b82b0d": { + "balance": "0x49b9ca9a694340000" + }, + "db2a0c9ab64df58ddfb1dbacf8ba0d89c85b31b4": { + "balance": "0xd8d726b7177a800000" + }, + "db34745ede8576b499db01beb7c1ecda85cf4abe": { + "balance": "0x4563918244f400000" + }, + "db3f258ab2a3c2cf339c4499f75a4bd1d3472e9e": { + "balance": "0x5150ae84a8cdf00000" + }, + "db4bc83b0e6baadb1156c5cf06e0f721808c52c7": { + "balance": "0x2fb474098f67c00000" + }, + "db63122de7037da4971531fae9af85867886c692": { + "balance": "0xf0425b0641f340000" + }, + "db6c2a73dac7424ab0d031b66761122566c01043": { + "balance": "0xa2a15d09519be00000" + }, + "db6e560c9bc620d4bea3a94d47f7880bf47f2d5f": { + "balance": "0x4da0fdfcf05760000" + }, + "db6ff71b3db0928f839e05a7323bfb57d29c87aa": { + "balance": "0x3154c9729d05780000" + }, + "db73460b59d8e85045d5e752e62559875e42502e": { + "balance": "0x36330322d5238c0000" + }, + "db77b88dcb712fd17ee91a5b94748d720c90a994": { + "balance": "0x6c6b935b8bbd400000" + }, + "db7d4037081f6c65f9476b0687d97f1e044d0a1d": { + "balance": "0x23c757072b8dd00000" + }, + "db882eacedd0eff263511b312adbbc59c6b8b25b": { + "balance": "0x1ed4fde7a2236b00000" + }, + "db9371b30c4c844e59e03e924be606a938d1d310": { + "balance": "0x6c6b935b8bbd400000" + }, + "dba4796d0ceb4d3a836b84c96f910afc103f5ba0": { + "balance": "0x908f493f737410000" + }, + "dbadc61ed5f0460a7f18e51b2fb2614d9264a0e0": { + "balance": "0x22b1c8c1227a00000" + }, + "dbb6ac484027041642bbfd8d80f9d0c1cf33c1eb": { + "balance": "0x6c6b935b8bbd400000" + }, + "dbbcbb79bf479a42ad71dbcab77b5adfaa872c58": { + "balance": "0x5dc892aa1131c80000" + }, + "dbc1ce0e49b1a705d22e2037aec878ee0d75c703": { + "balance": "0xd8d726b7177a80000" + }, + "dbc1d0ee2bab531140de137722cd36bdb4e47194": { + "balance": "0xad78ebc5ac6200000" + }, + "dbc59ed88973dead310884223af49763c05030f1": { + "balance": "0x1158e460913d00000" + }, + "dbc66965e426ff1ac87ad6eb78c1d95271158f9f": { + "balance": "0xfc936392801c0000" + }, + "dbcbcd7a57ea9db2349b878af34b1ad642a7f1d1": { + "balance": "0xad78ebc5ac6200000" + }, + "dbd51cdf2c3bfacdff106221de2e19ad6d420414": { + "balance": "0x5f68e8131ecf800000" + }, + "dbd71efa4b93c889e76593de609c3b04cbafbe08": { + "balance": "0x1158e460913d00000" + }, + "dbf5f061a0f48e5e69618739a77d2ec19768d201": { + "balance": "0x83d6c7aab63600000" + }, + "dbf8b13967f55125272de0562536c450ba5655a0": { + "balance": "0x6ef578f06e0ccb0000" + }, + "dbfb1bb464b8a58e500d2ed8de972c45f5f1c0fb": { + "balance": "0x56bc75e2d631000000" + }, + "dc067ed3e12d711ed475f5156ef7e71a80d934b9": { + "balance": "0x205b4dfa1ee74780000" + }, + "dc087f9390fb9e976ac23ab689544a0942ec2021": { + "balance": "0x62a992e53a0af00000" + }, + "dc1eb9b6e64351f56424509645f83e79eee76cf4": { + "balance": "0xd8d726b7177a800000" + }, + "dc1f1979615f082140b8bb78c67b27a1942713b1": { + "balance": "0x340aad21b3b700000" + }, + "dc23b260fcc26e7d10f4bd044af794579460d9da": { + "balance": "0x1b1b6bd7af64c70000" + }, + "dc29119745d2337320da51e19100c948d980b915": { + "balance": "0x8ac7230489e800000" + }, + "dc2d15a69f6bb33b246aef40450751c2f6756ad2": { + "balance": "0x6c341080bd1fb00000" + }, + "dc3dae59ed0fe18b58511e6fe2fb69b219689423": { + "balance": "0x56bc75e2d63100000" + }, + "dc3f0e7672f71fe7525ba30b9755183a20b9166a": { + "balance": "0x2089cf57b5b3e968000" + }, + "dc4345d6812e870ae90c568c67d2c567cfb4f03c": { + "balance": "0x16b352da5e0ed300000" + }, + "dc44275b1715baea1b0345735a29ac42c9f51b4f": { + "balance": "0x3f19beb8dd1ab00000" + }, + "dc46c13325cd8edf0230d068896486f007bf4ef1": { + "balance": "0x487a9a304539440000" + }, + "dc51b2dc9d247a1d0e5bc36ca3156f7af21ff9f6": { + "balance": "0x3635c9adc5dea00000" + }, + "dc5305b4020a06b49d657c7ca34c35c91c5f2c56": { + "balance": "0x17df6c10dbeba970000" + }, + "dc57345b38e0f067c9a31d9deac5275a10949321": { + "balance": "0xad78ebc5ac6200000" + }, + "dc57477dafa42f705c7fe40eae9c81756e0225f1": { + "balance": "0x1b1b8128a7416e0000" + }, + "dc5f5ad663a6f263327d64cac9cb133d2c960597": { + "balance": "0x6c6b935b8bbd400000" + }, + "dc703a5f3794c84d6cb3544918cae14a35c3bd4f": { + "balance": "0x6449e84e47a8a80000" + }, + "dc738fb217cead2f69594c08170de1af10c419e3": { + "balance": "0x152d02c7e14af6800000" + }, + "dc76e85ba50b9b31ec1e2620bce6e7c8058c0eaf": { + "balance": "0x1158e460913d00000" + }, + "dc83b6fd0d512131204707eaf72ea0c8c9bef976": { + "balance": "0x6c6b935b8bbd400000" + }, + "dc8c2912f084a6d184aa73638513ccbc326e0102": { + "balance": "0x4633bc36cbc2dc0000" + }, + "dc911cf7dc5dd0813656670528e9338e67034786": { + "balance": "0x6c6b935b8bbd400000" + }, + "dcb03bfa6c1131234e56b7ea7c4f721487546b7a": { + "balance": "0x487a9a304539440000" + }, + "dcb64df43758c7cf974fa660484fbb718f8c67c1": { + "balance": "0x43c33c1937564800000" + }, + "dcc52d8f8d9fc742a8b82767f0555387c563efff": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "dccb370ed68aa922283043ef7cad1b9d403fc34a": { + "balance": "0xd8d726b7177a800000" + }, + "dccca42045ec3e16508b603fd936e7fd7de5f36a": { + "balance": "0x11164759ffb320000" + }, + "dcd10c55bb854f754434f1219c2c9a98ace79f03": { + "balance": "0xd8d8583fa2d52f0000" + }, + "dcd5bca2005395b675fde5035659b26bfefc49ee": { + "balance": "0xaadec983fcff40000" + }, + "dcdbbd4e2604e40e1710cc6730289dccfad3892d": { + "balance": "0xf95dd2ec27cce00000" + }, + "dce30c31f3ca66721ecb213c809aab561d9b52e4": { + "balance": "0x6c6b935b8bbd400000" + }, + "dcf33965531380163168fc11f67e89c6f1bc178a": { + "balance": "0x122776853406b08000" + }, + "dcf6b657266e91a4dae6033ddac15332dd8d2b34": { + "balance": "0x5f68e8131ecf800000" + }, + "dcf9719be87c6f46756db4891db9b611d2469c50": { + "balance": "0x3635c9adc5dea00000" + }, + "dcfff3e8d23c2a34b56bd1b3bd45c79374432239": { + "balance": "0x10f0cf064dd59200000" + }, + "dd04eee74e0bf30c3f8d6c2c7f52e0519210df93": { + "balance": "0x4563918244f400000" + }, + "dd26b429fd43d84ec179825324bad5bfb916b360": { + "balance": "0x116bf95bc8432980000" + }, + "dd2a233adede66fe1126d6c16823b62a021feddb": { + "balance": "0x6c6b935b8bbd400000" + }, + "dd2bdfa917c1f310e6fa35aa8af16939c233cd7d": { + "balance": "0x15af1d78b58c400000" + }, + "dd35cfdbcb993395537aecc9f59085a8d5ddb6f5": { + "balance": "0x3635c9adc5dea00000" + }, + "dd47189a3e64397167f0620e484565b762bfbbf4": { + "balance": "0x6449e84e47a8a80000" + }, + "dd4dd6d36033b0636fcc8d0938609f4dd64f4a86": { + "balance": "0x340aad21b3b700000" + }, + "dd4f5fa2111db68f6bde3589b63029395b69a92d": { + "balance": "0x8963dd8c2c5e00000" + }, + "dd63042f25ed32884ad26e3ad959eb94ea36bf67": { + "balance": "0x484d7fde7d593f00000" + }, + "dd65f6e17163b5d203641f51cc7b24b00f02c8fb": { + "balance": "0xad78ebc5ac6200000" + }, + "dd6c062193eac23d2fdbf997d5063a346bb3b470": { + "balance": "0x1158e460913d00000" + }, + "dd7bcda65924aaa49b80984ae173750258b92847": { + "balance": "0x21e19e0c9bab2400000" + }, + "dd7ff441ba6ffe3671f3c0dabbff1823a5043370": { + "balance": "0x6c6b935b8bbd400000" + }, + "dd8254121a6e942fc90828f2431f511dad7f32e6": { + "balance": "0xa39b29e1f360e80000" + }, + "dd8af9e7765223f4446f44d3d509819a3d3db411": { + "balance": "0x21e19e0c9bab2400000" + }, + "dd95dbe30f1f1877c5dd7684aeef302ab6885192": { + "balance": "0x1c5d8d6eb3e32500000" + }, + "dd967c4c5f8ae47e266fb416aad1964ee3e7e8c3": { + "balance": "0x1a420db02bd7d580000" + }, + "dd9b485a3b1cd33a6a9c62f1e5bee92701856d25": { + "balance": "0xc3383ed031b7e8000" + }, + "dda371e600d30688d4710e088e02fdf2b9524d5f": { + "balance": "0x177224aa844c7200000" + }, + "dda4ed2a58a8dd20a73275347b580d71b95bf99a": { + "balance": "0x15a13cc201e4dc0000" + }, + "dda4ff7de491c687df4574dd1b17ff8f246ba3d1": { + "balance": "0x42684a41abfd8400000" + }, + "ddab6b51a9030b40fb95cf0b748a059c2417bec7": { + "balance": "0x6c6b935b8bbd400000" + }, + "ddab75fb2ff9fecb88f89476688e2b00e367ebf9": { + "balance": "0x41bad155e6512200000" + }, + "ddabf13c3c8ea4e3d73d78ec717afafa430e5479": { + "balance": "0x8cf23f909c0fa000000" + }, + "ddac312a9655426a9c0c9efa3fd82559ef4505bf": { + "balance": "0x15be6174e1912e0000" + }, + "ddac6bf4bbdd7d597d9c686d0695593bedccc7fa": { + "balance": "0x2ee449550898e40000" + }, + "ddbd2b932c763ba5b1b7ae3b362eac3e8d40121a": { + "balance": "0x21e19e0c9bab2400000" + }, + "ddbddd1bbd38ffade0305d30f02028d92e9f3aa8": { + "balance": "0x6c6b935b8bbd400000" + }, + "ddbee6f094eae63420b003fb4757142aea6cd0fd": { + "balance": "0x6c6b935b8bbd400000" + }, + "ddd69c5b9bf5eb5a39cee7c3341a120d973fdb34": { + "balance": "0x6bc14b8f8e1b350000" + }, + "dddd7b9e6eab409b92263ac272da801b664f8a57": { + "balance": "0x69e10de76676d0800000" + }, + "dde670d01639667576a22dd05d3246d61f06e083": { + "balance": "0x1731790534df20000" + }, + "dde77a4740ba08e7f73fbe3a1674912931742eeb": { + "balance": "0x434fe4d4382f1d48000" + }, + "dde8f0c31b7415511dced1cd7d46323e4bd12232": { + "balance": "0x57473d05dabae80000" + }, + "dde969aef34ea87ac299b7597e292b4a0155cc8a": { + "balance": "0x1032f2594a01738000" + }, + "ddf0cce1fe996d917635f00712f4052091dff9ea": { + "balance": "0x6c6b935b8bbd400000" + }, + "ddf3ad76353810be6a89d731b787f6f17188612b": { + "balance": "0x43c33c1937564800000" + }, + "ddf5810a0eb2fb2e32323bb2c99509ab320f24ac": { + "balance": "0x3ca5c66d9bc44300000" + }, + "ddf95c1e99ce2f9f5698057c19d5c94027ee4a6e": { + "balance": "0x14542ba12a337c00000" + }, + "ddfafdbc7c90f1320e54b98f374617fbd01d109f": { + "balance": "0xb98bc829a6f90000" + }, + "ddfcca13f934f0cfbe231da13039d70475e6a1d0": { + "balance": "0x3638221660a5aa8000" + }, + "de027efbb38503226ed871099cb30bdb02af1335": { + "balance": "0x3635c9adc5dea00000" + }, + "de06d5ea777a4eb1475e605dbcbf43444e8037ea": { + "balance": "0xa968163f0a57b400000" + }, + "de07fb5b7a464e3ba7fbe09e9acb271af5338c58": { + "balance": "0x2b5e3af16b1880000" + }, + "de1121829c9a08284087a43fbd2fc1142a3233b4": { + "balance": "0x3635c9adc5dea00000" + }, + "de176b5284bcee3a838ba24f67fc7cbf67d78ef6": { + "balance": "0x209ce08c962b00000" + }, + "de212293f8f1d231fa10e609470d512cb8ffc512": { + "balance": "0x6c6b935b8bbd400000" + }, + "de30e49e5ab313214d2f01dcabce8940b81b1c76": { + "balance": "0xaadec983fcff40000" + }, + "de33d708a3b89e909eaf653b30fdc3a5d5ccb4b3": { + "balance": "0x99c88229fd4c20000" + }, + "de374299c1d07d79537385190f442ef9ca24061f": { + "balance": "0x73f75d1a085ba0000" + }, + "de42fcd24ce4239383304367595f068f0c610740": { + "balance": "0x2722a70f1a9a00000" + }, + "de50868eb7e3c71937ec73fa89dd8b9ee10d45aa": { + "balance": "0x3635c9adc5dea00000" + }, + "de55de0458f850b37e4d78a641dd2eb2dd8f38ce": { + "balance": "0xd8d726b7177a800000" + }, + "de5b005fe8daae8d1f05de3eda042066c6c4691c": { + "balance": "0x3ba1910bf341b00000" + }, + "de612d0724e84ea4a7feaa3d2142bd5ee82d3201": { + "balance": "0x1158e460913d00000" + }, + "de6d363106cc6238d2f092f0f0372136d1cd50c6": { + "balance": "0x121ea68c114e5100000" + }, + "de7dee220f0457a7187d56c1c41f2eb00ac56021": { + "balance": "0x2225f39c85052a0000" + }, + "de82cc8d4a1bb1d9434392965b3e80bad3c03d4f": { + "balance": "0x50186e75de97a60000" + }, + "de97f4330700b48c496d437c91ca1de9c4b01ba4": { + "balance": "0x9dcc0515b56e0c0000" + }, + "de9eff4c798811d968dccb460d9b069cf30278e0": { + "balance": "0x15af1d78b58c400000" + }, + "deb1bc34d86d4a4dde2580d8beaf074eb0e1a244": { + "balance": "0x55a6e79ccd1d300000" + }, + "deb2495d6aca7b2a6a2d138b6e1a42e2dc311fdd": { + "balance": "0x6c6b935b8bbd400000" + }, + "deb97254474c0d2f5a7970dcdb2f52fb1098b896": { + "balance": "0x3635c9adc5dea00000" + }, + "deb9a49a43873020f0759185e20bbb4cf381bb8f": { + "balance": "0xb78edb0bf2e5e0000" + }, + "debbdd831e0f20ae6e378252decdf92f7cf0c658": { + "balance": "0x6c6b935b8bbd400000" + }, + "dec3eec2640a752c466e2b7e7ee685afe9ac41f4": { + "balance": "0x47c99753596b288000" + }, + "dec82373ade8ebcf2acb6f8bc2414dd7abb70d77": { + "balance": "0xad78ebc5ac6200000" + }, + "dec8a1a898f1b895d8301fe64ab3ad5de941f689": { + "balance": "0x2ab4f67e8a730f8000" + }, + "dec99e972fca7177508c8e1a47ac22d768acab7c": { + "balance": "0x6c6b935b8bbd400000" + }, + "ded877378407b94e781c4ef4af7cfc5bc220b516": { + "balance": "0x143179d86911020000" + }, + "dee942d5caf5fac11421d86b010b458e5c392990": { + "balance": "0xd8d726b7177a800000" + }, + "deee2689fa9006b59cf285237de53b3a7fd01438": { + "balance": "0x186579f29e20250000" + }, + "defddfd59b8d2c154eecf5c7c167bf0ba2905d3e": { + "balance": "0x512cb5e2647420000" + }, + "defe9141f4704599159d7b223de42bffd80496b3": { + "balance": "0x56bc75e2d63100000" + }, + "df098f5e4e3dffa51af237bda8652c4f73ed9ca6": { + "balance": "0x1b36a6444a3e180000" + }, + "df0d08617bd252a911df8bd41a39b83ddf809673": { + "balance": "0x21e19e0c9bab2400000" + }, + "df0ff1f3d27a8ec9fb8f6b0cb254a63bba8224a5": { + "balance": "0xecc5202945d0020000" + }, + "df1fa2e20e31985ebe2c0f0c93b54c0fb67a264b": { + "balance": "0xad78ebc5ac6200000" + }, + "df211cd21288d6c56fae66c3ff54625dd4b15427": { + "balance": "0x8786cd764e1f2c0000" + }, + "df236bf6abf4f3293795bf0c28718f93e3b1b36b": { + "balance": "0x487a9a304539440000" + }, + "df31025f5649d2c6eea41ed3bdd3471a790f759a": { + "balance": "0x1158e460913d00000" + }, + "df37c22e603aedb60a627253c47d8ba866f6d972": { + "balance": "0x5150ae84a8cdf000000" + }, + "df3b72c5bd71d4814e88a62321a93d4011e3578b": { + "balance": "0xd8d726b7177a800000" + }, + "df3f57b8ee6434d047223def74b20f63f9e4f955": { + "balance": "0xd9462c6cb4b5a0000" + }, + "df44c47fc303ac76e74f97194cca67b5bb3c023f": { + "balance": "0x2009c5c8bf6fdc0000" + }, + "df47a61b72535193c561cccc75c3f3ce0804a20e": { + "balance": "0x15935c0b4e3d780000" + }, + "df47a8ef95f2f49f8e6f58184154145d11f72797": { + "balance": "0x678a932062e4180000" + }, + "df53003346d65c5e7a646bc034f2b7d32fcbe56a": { + "balance": "0x6c6b935b8bbd400000" + }, + "df57353aaff2aadb0a04f9014e8da7884e86589c": { + "balance": "0x84886a66e4fb00000" + }, + "df60f18c812a11ed4e2776e7a80ecf5e5305b3d6": { + "balance": "0x30ca024f987b900000" + }, + "df6485c4297ac152b289b19dde32c77ec417f47d": { + "balance": "0x3635c9adc5dea00000" + }, + "df660a91dab9f730f6190d50c8390561500756ca": { + "balance": "0x6c6b935b8bbd400000" + }, + "df6ed6006a6abe886ed33d95a4de28fc12183927": { + "balance": "0x3154c9729d05780000" + }, + "df8510793eee811c2dab1c93c6f4473f30fbef5b": { + "balance": "0x3635c9adc5dea00000" + }, + "df8d48b1eb07b3c217790e6c2df04dc319e7e848": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "dfa6b8b8ad3184e357da282951d79161cfb089bc": { + "balance": "0x15af1d78b58c400000" + }, + "dfaf31e622c03d9e18a0ddb8be60fbe3e661be0a": { + "balance": "0x21e171a3ec9f72c0000" + }, + "dfb1626ef48a1d7d7552a5e0298f1fc23a3b482d": { + "balance": "0x5ce895dd949efa0000" + }, + "dfb4d4ade52fcc818acc7a2c6bb2b00224658f78": { + "balance": "0x1a420db02bd7d580000" + }, + "dfbd4232c17c407a980db87ffbcda03630e5c459": { + "balance": "0x1dfc7f924923530000" + }, + "dfcbdf09454e1a5e4a40d3eef7c5cf1cd3de9486": { + "balance": "0xd8d726b7177a800000" + }, + "dfdbcec1014b96da2158ca513e9c8d3b9af1c3d0": { + "balance": "0x6c6b935b8bbd400000" + }, + "dfded2574b27d1613a7d98b715159b0d00baab28": { + "balance": "0x43c33c1937564800000" + }, + "dfdf43393c649caebe1bb18059decb39f09fb4e8": { + "balance": "0x15af1d78b58c400000" + }, + "dfe3c52a92c30396a4e33a50170dc900fcf8c9cf": { + "balance": "0x2b5e3af16b1880000" + }, + "dfe549fe8430e552c6d07cc3b92ccd43b12fb50f": { + "balance": "0x48875eaf6562a0000" + }, + "dfe929a61c1b38eddbe82c25c2d6753cb1e12d68": { + "balance": "0x15d1cf4176aeba0000" + }, + "dff1b220de3d8e9ca4c1b5be34a799bcded4f61c": { + "balance": "0x14e4e353ea39420000" + }, + "dff4007931786593b229efe5959f3a4e219e51af": { + "balance": "0x10afc1ade3b4ed40000" + }, + "dffcea5421ec15900c6ecfc777184e140e209e24": { + "balance": "0x115473824344e0000" + }, + "e001aba77c02e172086c1950fffbcaa30b83488f": { + "balance": "0x6acb3df27e1f880000" + }, + "e00484788db50fc6a48e379d123e508b0f6e5ab1": { + "balance": "0x3635c9adc5dea00000" + }, + "e0060462c47ff9679baef07159cae08c29f274a9": { + "balance": "0x6c6b935b8bbd400000" + }, + "e00d153b10369143f97f54b8d4ca229eb3e8f324": { + "balance": "0x83d6c7aab63600000" + }, + "e012db453827a58e16c1365608d36ed658720507": { + "balance": "0x6c6b935b8bbd400000" + }, + "e01547ba42fcafaf93938becf7699f74290af74f": { + "balance": "0x6c6b935b8bbd400000" + }, + "e016dc138e25815b90be3fe9eee8ffb2e105624f": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e01859f242f1a0ec602fa8a3b0b57640ec89075e": { + "balance": "0x1e162c177be5cc0000" + }, + "e020e86362b487752836a6de0bc02cd8d89a8b6a": { + "balance": "0x14542ba12a337c00000" + }, + "e023f09b2887612c7c9cf1988e3a3a602b3394c9": { + "balance": "0x6c6b935b8bbd400000" + }, + "e0272213e8d2fd3e96bd6217b24b4ba01b617079": { + "balance": "0x1158e460913d00000" + }, + "e02b74a47628be315b1f76b315054ad44ae9716f": { + "balance": "0xd8d726b7177a800000" + }, + "e03220c697bcd28f26ef0b74404a8beb06b2ba7b": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "e0352fdf819ba265f14c06a6315c4ac1fe131b2e": { + "balance": "0x3635c9adc5dea00000" + }, + "e0388aeddd3fe2ad56f85748e80e710a34b7c92e": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e03c00d00388ecbf4f263d0ac778bb41a57a40d9": { + "balance": "0x3636c9796436740000" + }, + "e04920dc6ecc1d6ecc084f88aa0af5db97bf893a": { + "balance": "0x9ddc1e3b901180000" + }, + "e04972a83ca4112bc871c72d4ae1616c2f0728db": { + "balance": "0xe81c77f29a32f0000" + }, + "e04ff5e5a7e2af995d8857ce0290b53a2b0eda5d": { + "balance": "0x3635c9adc5dea00000" + }, + "e05029aceb0778675bef1741ab2cd2931ef7c84b": { + "balance": "0x10f0dbae61009528000" + }, + "e056bf3ff41c26256fef51716612b9d39ade999c": { + "balance": "0x56be757a12e0a8000" + }, + "e061a4f2fc77b296d19ada238e49a5cb8ecbfa70": { + "balance": "0xd8d726b7177a800000" + }, + "e0663e8cd66792a641f56e5003660147880f018e": { + "balance": "0x6c6b935b8bbd400000" + }, + "e0668fa82c14d6e8d93a53113ef2862fa81581bc": { + "balance": "0x2f2f39fc6c54000000" + }, + "e069c0173352b10bf6834719db5bed01adf97bbc": { + "balance": "0x10634f8e5323b0000" + }, + "e06c29a81517e0d487b67fb0b6aabc4f57368388": { + "balance": "0x15be6174e1912e0000" + }, + "e06cb6294704eea7437c2fc3d30773b7bf38889a": { + "balance": "0x116dc3a8994b30000" + }, + "e07137ae0d116d033533c4eab496f8a9fb09569c": { + "balance": "0x4be4e7267b6ae00000" + }, + "e076db30ab486f79194ebbc45d8fab9a9242f654": { + "balance": "0x106607e3494baa00000" + }, + "e07ebbc7f4da416e42c8d4f842aba16233c12580": { + "balance": "0x6c6b935b8bbd400000" + }, + "e081ca1f4882db6043d5a9190703fde0ab3bf56d": { + "balance": "0x15af1d78b58c400000" + }, + "e083d34863e0e17f926b7928edff317e998e9c4b": { + "balance": "0x15af1d78b58c400000" + }, + "e08b9aba6bd9d28bc2056779d2fbf0f2855a3d9d": { + "balance": "0x6c6b935b8bbd400000" + }, + "e08bc29c2b48b169ff2bdc16714c586e6cb85ccf": { + "balance": "0x1158e460913d00000" + }, + "e08c60313106e3f9334fe6f7e7624d211130c077": { + "balance": "0x22b1c8c1227a00000" + }, + "e09c68e61998d9c81b14e4ee802ba7adf6d74cdb": { + "balance": "0xd8d726b7177a800000" + }, + "e09fea755aee1a44c0a89f03b5deb762ba33006f": { + "balance": "0x3ba289bc944ff70000" + }, + "e0a254ac09b9725bebc8e460431dd0732ebcabbf": { + "balance": "0x14542ba12a337c00000" + }, + "e0aa69365555b73f282333d1e30c1bbd072854e8": { + "balance": "0x17b7883c06916600000" + }, + "e0bad98eee9698dbf6d76085b7923de5754e906d": { + "balance": "0x90d972f32323c0000" + }, + "e0c4ab9072b4e6e3654a49f8a8db026a4b3386a9": { + "balance": "0x6c6b935b8bbd400000" + }, + "e0ce80a461b648a501fd0b824690c8868b0e4de8": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e0cf698a053327ebd16b7d7700092fe2e8542446": { + "balance": "0x52a34cbb61f578000" + }, + "e0d231e144ec9107386c7c9b02f1702ceaa4f700": { + "balance": "0x10f0dbae61009528000" + }, + "e0d76b7166b1f3a12b4091ee2b29de8caa7d07db": { + "balance": "0x6c6b935b8bbd400000" + }, + "e0e0b2e29dde73af75987ee4446c829a189c95bc": { + "balance": "0x813ca56906d340000" + }, + "e0e978753d982f7f9d1d238a18bd4889aefe451b": { + "balance": "0x20dd68aaf3289100000" + }, + "e0f372347c96b55f7d4306034beb83266fd90966": { + "balance": "0x15af1d78b58c400000" + }, + "e0f903c1e48ac421ab48528f3d4a2648080fe043": { + "balance": "0x3708baed3d68900000" + }, + "e0ff0bd9154439c4a5b7233e291d7d868af53f33": { + "balance": "0x1579216a51bbfb0000" + }, + "e10ac19c546fc2547c61c139f5d1f45a6666d5b0": { + "balance": "0x102da6fd0f73a3c0000" + }, + "e10c540088113fa6ec00b4b2c8824f8796e96ec4": { + "balance": "0x320f4509ab1ec7c00000" + }, + "e1173a247d29d8238df0922f4df25a05f2af77c3": { + "balance": "0x878c95d560f30478000" + }, + "e1203eb3a723e99c2220117ca6afeb66fa424f61": { + "balance": "0x200ef929e3256fe0000" + }, + "e131f87efc5ef07e43f0f2f4a747b551d750d9e6": { + "balance": "0x43c25e0dcc1bd1c0000" + }, + "e1334e998379dfe983177062791b90f80ee22d8d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e13540ecee11b212e8b775dc8e71f374aae9b3f8": { + "balance": "0x6c6b935b8bbd400000" + }, + "e13b3d2bbfdcbc8772a23315724c1425167c5688": { + "balance": "0x37f379141ed04b8000" + }, + "e1443dbd95cc41237f613a48456988a04f683282": { + "balance": "0xd8d8583fa2d52f0000" + }, + "e14617f6022501e97e7b3e2d8836aa61f0ff2dba": { + "balance": "0xad78ebc5ac6200000" + }, + "e149b5726caf6d5eb5bf2acc41d4e2dc328de182": { + "balance": "0x692ae8897081d00000" + }, + "e154daeadb545838cbc6aa0c55751902f528682a": { + "balance": "0x10afc1ade3b4ed40000" + }, + "e16ce35961cd74bd590d04c4ad4a1989e05691c6": { + "balance": "0x7ea28327577080000" + }, + "e172dfc8f80cd1f8cd8539dc26082014f5a8e3e8": { + "balance": "0xa2a15d09519be00000" + }, + "e177e0c201d335ba3956929c571588b51c5223ae": { + "balance": "0x6c6b935b8bbd400000" + }, + "e17812f66c5e65941e186c46922b6e7b2f0eeb46": { + "balance": "0x62a992e53a0af00000" + }, + "e180de9e86f57bafacd7904f9826b6b4b26337a3": { + "balance": "0x2d041d705a2c600000" + }, + "e192489b85a982c1883246d915b229cb13207f38": { + "balance": "0x10f0cf064dd59200000" + }, + "e1953c6e975814c571311c34c0f6a99cdf48ab82": { + "balance": "0x2b5e3af16b1880000" + }, + "e1ae029b17e373cde3de5a9152201a14cac4e119": { + "balance": "0x56b55ae58ca400000" + }, + "e1b2aca154b8e0766c4eba30bc10c7f35036f368": { + "balance": "0x115473824344e0000" + }, + "e1b39b88d9900dbc4a6cdc481e1060080a8aec3c": { + "balance": "0x6c6b935b8bbd400000" + }, + "e1b63201fae1f129f95c7a116bd9dde5159c6cda": { + "balance": "0x4d60573a2f0c9ef0000" + }, + "e1bfaa5a45c504428923c4a61192a55b1400b45d": { + "balance": "0x90f534608a72880000" + }, + "e1c607c0a8a060da8f02a8eb38a013ea8cda5b8c": { + "balance": "0x2ba39e82ed5d740000" + }, + "e1cb83ec5eb6f1eeb85e99b2fc63812fde957184": { + "balance": "0x43c33c1937564800000" + }, + "e1d91b0954cede221d6f24c7985fc59965fb98b8": { + "balance": "0x6c6b935b8bbd400000" + }, + "e1dfb5cc890ee8b2877e885d267c256187d019e6": { + "balance": "0x56bc75e2d63100000" + }, + "e1e8c50b80a352b240ce7342bbfdf5690cc8cb14": { + "balance": "0x155bd9307f9fe80000" + }, + "e1f63ebbc62c7b7444040eb99623964f7667b376": { + "balance": "0x1158e460913d00000" + }, + "e206fb7324e9deb79e19903496d6961b9be56603": { + "balance": "0x56bc75e2d63100000" + }, + "e207578e1f4ddb8ff6d5867b39582d71b9812ac5": { + "balance": "0xd255d112e103a00000" + }, + "e208812a684098f3da4efe6aba256256adfe3fe6": { + "balance": "0x6c6b935b8bbd400000" + }, + "e20954d0f4108c82d4dcb2148d26bbd924f6dd24": { + "balance": "0x21e19e0c9bab2400000" + }, + "e20bb9f3966419e14bbbaaaa6789e92496cfa479": { + "balance": "0xbbd825030752760000" + }, + "e20d1bcb71286dc7128a9fc7c6ed7f733892eef5": { + "balance": "0x3664f8e7c24af40000" + }, + "e2191215983f33fd33e22cd4a2490054da53fddc": { + "balance": "0xdb44e049bb2c0000" + }, + "e2198c8ca1b399f7521561fd5384a7132fba486b": { + "balance": "0x3708baed3d68900000" + }, + "e21c778ef2a0d7f751ea8c074d1f812243863e4e": { + "balance": "0x11fc70e2c8c8ae18000" + }, + "e229e746a83f2ce253b0b03eb1472411b57e5700": { + "balance": "0x1369fb96128ac480000" + }, + "e22b20c77894463baf774cc256d5bddbbf7ddd09": { + "balance": "0x3635c9adc5dea00000" + }, + "e230fe1bff03186d0219f15d4c481b7d59be286a": { + "balance": "0x1fd741e8088970000" + }, + "e237baa4dbc9926e32a3d85d1264402d54db012f": { + "balance": "0x6c6b935b8bbd400000" + }, + "e24109be2f513d87498e926a286499754f9ed49e": { + "balance": "0x300ea8ad1f27ca0000" + }, + "e246683cc99db7c4a52bcbacaab0b32f6bfc93d7": { + "balance": "0x6c6b935b8bbd400000" + }, + "e25a167b031e84616d0f013f31bda95dcc6350b9": { + "balance": "0x23c757072b8dd000000" + }, + "e25b9f76b8ad023f057eb11ad94257a0862e4e8c": { + "balance": "0x6c6b935b8bbd400000" + }, + "e26657f0ed201ea2392c9222b80a7003608ddf30": { + "balance": "0x22b1c8c1227a00000" + }, + "e26bf322774e18288769d67e3107deb7447707b8": { + "balance": "0x6c6b935b8bbd400000" + }, + "e2728a3e8c2aaac983d05dc6877374a8f446eee9": { + "balance": "0xab640391201300000" + }, + "e28b062259e96eeb3c8d4104943f9eb325893cf5": { + "balance": "0x487a9a304539440000" + }, + "e28dbc8efd5e416a762ec0e018864bb9aa83287b": { + "balance": "0x531f200ab3e030a8000" + }, + "e2904b1aefa056398b6234cb35811288d736db67": { + "balance": "0x22b1c8c1227a00000" + }, + "e29d8ae452dcf3b6ac645e630409385551faae0a": { + "balance": "0x45a0da4adf5420000" + }, + "e2bbf84641e3541f6c33e6ed683a635a70bde2ec": { + "balance": "0x1b413cfcbf59b78000" + }, + "e2cf360aa2329eb79d2bf7ca04a27a17c532e4d8": { + "balance": "0x58788cb94b1d80000" + }, + "e2df23f6ea04becf4ab701748dc0963184555cdb": { + "balance": "0x6c6b935b8bbd400000" + }, + "e2e15c60dd381e3a4be25071ab249a4c5c5264da": { + "balance": "0x7f6bc49b81b5370000" + }, + "e2e26e4e1dcf30d048cc6ecf9d51ec1205a4e926": { + "balance": "0xd8d726b7177a800000" + }, + "e2ee691f237ee6529b6557f2fcdd3dcf0c59ec63": { + "balance": "0x127729c14687c200000" + }, + "e2efa5fca79538ce6068bf31d2c516d4d53c08e5": { + "balance": "0x71cc408df63400000" + }, + "e2efd0a9bc407ece03d67e8ec8e9d283f48d2a49": { + "balance": "0x299b33bf9c584e00000" + }, + "e2f40d358f5e3fe7463ec70480bd2ed398a7063b": { + "balance": "0x1158e460913d00000" + }, + "e2f9383d5810ea7b43182b8704b62b27f5925d39": { + "balance": "0x15af1d78b58c400000" + }, + "e2ff9ee4b6ecc14141cc74ca52a9e7a2ee14d908": { + "balance": "0x4be4e7267b6ae00000" + }, + "e30212b2011bb56bdbf1bc35690f3a4e0fd905ea": { + "balance": "0x1b2df9d219f57980000" + }, + "e303167f3d4960fe881b32800a2b4aeff1b088d4": { + "balance": "0x6c6b935b8bbd400000" + }, + "e304a32f05a83762744a9542976ff9b723fa31ea": { + "balance": "0x5572f240a346200000" + }, + "e308435204793764f5fcbe65eb510f5a744a655a": { + "balance": "0xad78ebc5ac6200000" + }, + "e309974ce39d60aadf2e69673251bf0e04760a10": { + "balance": "0xdc55fdb17647b0000" + }, + "e31b4eef184c24ab098e36c802714bd4743dd0d4": { + "balance": "0xad78ebc5ac6200000" + }, + "e321bb4a946adafdade4571fb15c0043d39ee35f": { + "balance": "0x556475382b4c9e0000" + }, + "e3263ce8af6db3e467584502ed7109125eae22a5": { + "balance": "0x6c6b935b8bbd400000" + }, + "e32b1c4725a1875449e98f970eb3e54062d15800": { + "balance": "0xad78ebc5ac6200000" + }, + "e32f95766d57b5cd4b173289d6876f9e64558194": { + "balance": "0x56bc75e2d63100000" + }, + "e33840d8bca7da98a6f3d096d83de78b70b71ef8": { + "balance": "0x6c6b935b8bbd400000" + }, + "e338e859fe2e8c15554848b75caecda877a0e832": { + "balance": "0x61acff81a78ad40000" + }, + "e33d980220fab259af6a1f4b38cf0ef3c6e2ea1a": { + "balance": "0x6c6b935b8bbd400000" + }, + "e33df4ce80ccb62a76b12bcdfcecc46289973aa9": { + "balance": "0x14542ba12a337c00000" + }, + "e33ff987541dde5cdee0a8a96dcc3f33c3f24cc2": { + "balance": "0x2a5a058fc295ed000000" + }, + "e3410bb7557cf91d79fa69d0dfea0aa075402651": { + "balance": "0x6c6b935b8bbd400000" + }, + "e341642d40d2afce2e9107c67079ac7a2660086c": { + "balance": "0x15af1d78b58c400000" + }, + "e35453eef2cc3c7a044d0ac134ba615908fa82ee": { + "balance": "0x7ff1ccb7561df0000" + }, + "e36a8ea87f1e99e8a2dc1b2608d166667c9dfa01": { + "balance": "0x56bc75e2d63100000" + }, + "e3712701619ca7623c55db3a0ad30e867db0168b": { + "balance": "0x1158e460913d00000" + }, + "e37f5fdc6ec97d2f866a1cfd0d3a4da4387b22b5": { + "balance": "0x21e19e0c9bab2400000" + }, + "e3878f91ca86053fced5444686a330e09cc388fb": { + "balance": "0xa844a7424d9c80000" + }, + "e38b91b35190b6d9deed021c30af094b953fdcaa": { + "balance": "0x1ceaf795b6b860000" + }, + "e38ef28a5ed984a7db24a1ae782dfb87f397dfc6": { + "balance": "0x7c0860e5a80dc0000" + }, + "e3925509c8d0b2a6738c5f6a72f35314491248ce": { + "balance": "0x36e9a8669a44768000" + }, + "e3933d61b77dcdc716407f8250bc91e4ffaeb09d": { + "balance": "0x1256986c95891c200000" + }, + "e3951de5aefaf0458768d774c254f7157735e505": { + "balance": "0x56c95de8e8ca1d0000" + }, + "e399c81a1d701b44f0b66f3399e66b275aaaf8c1": { + "balance": "0x3635c9adc5dea00000" + }, + "e39b11a8ab1ff5e22e5ae6517214f73c5b9b55dc": { + "balance": "0x6c6b935b8bbd400000" + }, + "e39e46e15d22ce56e0c32f1877b7d1a264cf94f3": { + "balance": "0x43c33c1937564800000" + }, + "e3a4621b66004588e31206f718cb00a319889cf0": { + "balance": "0x6c6b935b8bbd400000" + }, + "e3a4f83c39f85af9c8b1b312bfe5fc3423afa634": { + "balance": "0x18d993f34aef10000" + }, + "e3a89a1927cc4e2d43fbcda1e414d324a7d9e057": { + "balance": "0xb23e2a936dec60000" + }, + "e3ab3ca9b870e3f548517306bba4de2591afafc2": { + "balance": "0x410e34aecc8cd30000" + }, + "e3b3d2c9bf570be6a2f72adca1862c310936a43c": { + "balance": "0x56d2aa3a5c09a0000" + }, + "e3c0c128327a9ad80148139e269773428e638cb0": { + "balance": "0x6c6b935b8bbd400000" + }, + "e3c812737ac606baf7522ad817428a36050e7a34": { + "balance": "0x692ae8897081d00000" + }, + "e3cffe239c64e7e20388e622117391301b298696": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e3d3eaa299887865569e88be219be507189be1c9": { + "balance": "0x18ba6fa92e93160000" + }, + "e3d8bf4efe84b1616d1b89e427ddc6c8830685ae": { + "balance": "0x6c6b935b8bbd400000" + }, + "e3d915eda3b825d6ee4af9328d32ac18ada35497": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e3da4f3240844c9b6323b4996921207122454399": { + "balance": "0x27190a952df4be58000" + }, + "e3eb2c0a132a524f72ccc0d60fee8b41685d39e2": { + "balance": "0x6acb3df27e1f880000" + }, + "e3ec18a74ed43855409a26ade7830de8e42685ef": { + "balance": "0x11164759ffb320000" + }, + "e3ece1f632711d13bfffa1f8f6840871ee58fb27": { + "balance": "0xd8d726b7177a800000" + }, + "e3f80b40fb83fb97bb0d5230af4f6ed59b1c7cc8": { + "balance": "0x487a9a304539440000" + }, + "e3ffb02cb7d9ea5243701689afd5d417d7ed2ece": { + "balance": "0x43a77aabd00780000" + }, + "e400d651bb3f2d23d5f849e6f92d9c5795c43a8a": { + "balance": "0x90f534608a72880000" + }, + "e406f5dd72cab66d8a6ecbd6bfb494a7b6b09afe": { + "balance": "0x56bc75e2d63100000" + }, + "e408aa99835307eea4a6c5eb801fe694117f707d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e408fceaa1b98f3c640f48fcba39f056066d6308": { + "balance": "0x21e19e0c9bab2400000" + }, + "e40a7c82e157540a0b00901dbb86c716e1a062da": { + "balance": "0x2b31d2425f6740000" + }, + "e41aea250b877d423a63ba2bce2f3a61c0248d56": { + "balance": "0xe18398e7601900000" + }, + "e430c0024fdbf73a82e21fccf8cbd09138421c21": { + "balance": "0xd8d726b7177a800000" + }, + "e4324912d64ea3aef76b3c2ff9df82c7e13ae991": { + "balance": "0x6c6b935b8bbd400000" + }, + "e4368bc1420b35efda95fafbc73090521916aa34": { + "balance": "0xd8d726b7177a800000" + }, + "e437acbe0f6227b0e36f36e4bcf7cf613335fb68": { + "balance": "0xad78ebc5ac6200000" + }, + "e44b7264dd836bee8e87970340ed2b9aed8ed0a5": { + "balance": "0x138e7faa01a803a0000" + }, + "e44ea51063405154aae736be2bf1ee3b9be639ae": { + "balance": "0xd8d726b7177a800000" + }, + "e4625501f52b7af52b19ed612e9d54fdd006b492": { + "balance": "0xb5a905a56ddd00000" + }, + "e4715956f52f15306ee9506bf82bccc406b3895e": { + "balance": "0xee79d4f48c5000000" + }, + "e47fbaed99fc209962604ebd20e240f74f4591f1": { + "balance": "0x6c6b935b8bbd400000" + }, + "e482d255ede56b04c3e8df151f56e9ca62aaa8c2": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e48e65125421880d42bdf1018ab9778d96928f3f": { + "balance": "0xe3aeb5737240a00000" + }, + "e492818aa684e5a676561b725d42f3cc56ae5198": { + "balance": "0x2b5e3af16b18800000" + }, + "e49936a92a8ccf710eaac342bc454b9b14ebecb1": { + "balance": "0x6c6b935b8bbd400000" + }, + "e49af4f34adaa2330b0e49dc74ec18ab2f92f827": { + "balance": "0x6c6b935b8bbd400000" + }, + "e49ba0cd96816c4607773cf8a5970bb5bc16a1e6": { + "balance": "0x5a87e7d7f5f6580000" + }, + "e4a47e3933246c3fd62979a1ea19ffdf8c72ef37": { + "balance": "0x809b383ea7d7e8000" + }, + "e4b6ae22c7735f5b89f34dd77ad0975f0acc9181": { + "balance": "0x3635c9adc5dea00000" + }, + "e4ca0a5238564dfc91e8bf22bade2901619a1cd4": { + "balance": "0x3635c9adc5dea00000" + }, + "e4cafb727fb5c6b70bb27533b8a9ccc9ef6888e1": { + "balance": "0x10497bf4af4caf8000" + }, + "e4dc22ed595bf0a337c01e03cc6be744255fc9e8": { + "balance": "0xa5aa85009e39c0000" + }, + "e4fb26d1ca1eecba3d8298d9d148119ac2bbf580": { + "balance": "0x15af1d78b58c400000" + }, + "e4fc13cfcbac1b17ce7783acd423a845943f6b3a": { + "balance": "0x1158e460913d00000" + }, + "e50b464ac9de35a5618b7cbf254674182b81b97e": { + "balance": "0xde42ee1544dd900000" + }, + "e5102c3b711b810344197419b1cd8a7059f13e32": { + "balance": "0x1043528d0984698000" + }, + "e510d6797fba3d6693835a844ea2ad540691971b": { + "balance": "0x3ae39d47383e8740000" + }, + "e51421f8ee2210c71ed870fe618276c8954afbe9": { + "balance": "0x487a9a304539440000" + }, + "e51eb87e7fb7311f5228c479b48ec9878831ac4c": { + "balance": "0x6c6b935b8bbd400000" + }, + "e5215631b14248d45a255296bed1fbfa0330ff35": { + "balance": "0x4703e6eb5291b80000" + }, + "e528a0e5a267d667e9393a6584e19b34dc9be973": { + "balance": "0x12f939c99edab800000" + }, + "e53425d8df1f11c341ff58ae5f1438abf1ca53cf": { + "balance": "0x1174a5cdf88bc80000" + }, + "e53c68796212033e4e6f9cff56e19c461eb454f9": { + "balance": "0x3635c9adc5dea00000" + }, + "e54102534de8f23effb093b31242ad3b233facfd": { + "balance": "0xd8d726b7177a800000" + }, + "e545ee84ea48e564161e9482d59bcf406a602ca2": { + "balance": "0x6449e84e47a8a80000" + }, + "e5481a7fed42b901bbed20789bd4ade50d5f83b9": { + "balance": "0x6c6b935b8bbd400000" + }, + "e559b5fd337b9c5572a9bf9e0f2521f7d446dbe4": { + "balance": "0xad78ebc5ac6200000" + }, + "e55c80520a1b0f755b9a2cd3ce214f7625653e8a": { + "balance": "0x6c6b935b8bbd400000" + }, + "e56d431324c92911a1749df292709c14b77a65cd": { + "balance": "0x1bc85dc2a89bb200000" + }, + "e57d2995b0ebdf3f3ca6c015eb04260dbb98b7c6": { + "balance": "0x6c6b935b8bbd400000" + }, + "e587b16abc8a74081e3613e14342c03375bf0847": { + "balance": "0x6c6b935b8bbd400000" + }, + "e589fa76984db5ec4004b46ee8a59492c30744ce": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "e58dd23238ee6ea7c2138d385df500c325f376be": { + "balance": "0x62a992e53a0af00000" + }, + "e5953fea497104ef9ad2d4e5841c271f073519c2": { + "balance": "0x2629f66e0c53000000" + }, + "e5968797468ef767101b761d431fce14abffdbb4": { + "balance": "0x1b3d969fa411ca00000" + }, + "e597f083a469c4591c3d2b1d2c772787befe27b2": { + "balance": "0xf2dc7d47f15600000" + }, + "e59b3bd300893f97233ef947c46f7217e392f7e9": { + "balance": "0x3635c9adc5dea00000" + }, + "e5a365343cc4eb1e770368e1f1144a77b832d7e0": { + "balance": "0x1158e460913d00000" + }, + "e5a3d7eb13b15c100177236d1beb30d17ee15420": { + "balance": "0x6c6b935b8bbd400000" + }, + "e5aa0b833bb916dc19a8dd683f0ede241d988eba": { + "balance": "0xa2a15d09519be00000" + }, + "e5b7af146986c0ff8f85d22e6cc334077d84e824": { + "balance": "0x6c6b935b8bbd400000" + }, + "e5b826196c0e1bc1119b021cf6d259a610c99670": { + "balance": "0xad78ebc5ac6200000" + }, + "e5b96fc9ac03d448c1613ac91d15978145dbdfd1": { + "balance": "0xad78ebc5ac6200000" + }, + "e5b980d28eece2c06fca6c9473068b37d4a6d6e9": { + "balance": "0x25afd68cac2b900000" + }, + "e5bab4f0afd8a9d1a381b45761aa18f3d3cce105": { + "balance": "0x51bfd7c13878d10000" + }, + "e5bcc88c3b256f6ed5fe550e4a18198b943356ad": { + "balance": "0x6c6b935b8bbd400000" + }, + "e5bdf34f4ccc483e4ca530cc7cf2bb18febe92b3": { + "balance": "0x6d835a10bbcd20000" + }, + "e5dc9349cb52e161196122cf87a38936e2c57f34": { + "balance": "0x6c6b935b8bbd400000" + }, + "e5e33800a1b2e96bde1031630a959aa007f26e51": { + "balance": "0x487a9a304539440000" + }, + "e5e37e19408f2cfbec83349dd48153a4a795a08f": { + "balance": "0xe3aeb5737240a00000" + }, + "e5edc73e626f5d3441a45539b5f7a398c593edf6": { + "balance": "0x2ee449550898e40000" + }, + "e5edf8123f2403ce1a0299becf7aac744d075f23": { + "balance": "0xada55474b81340000" + }, + "e5f8ef6d970636b0dcaa4f200ffdc9e75af1741c": { + "balance": "0x6c6b935b8bbd400000" + }, + "e5fb31a5caee6a96de393bdbf89fbe65fe125bb3": { + "balance": "0x3635c9adc5dea00000" + }, + "e5fbe34984b637196f331c679d0c0c47d83410e1": { + "balance": "0x6c6c44fe47ec050000" + }, + "e60955dc0bc156f6c41849f6bd776ba44b0ef0a1": { + "balance": "0x10431627a0933b0000" + }, + "e60a55f2df996dc3aedb696c08dde039b2641de8": { + "balance": "0x6c6b935b8bbd400000" + }, + "e6115b13f9795f7e956502d5074567dab945ce6b": { + "balance": "0x152d02c7e14af6800000" + }, + "e61f280915c774a31d223cf80c069266e5adf19b": { + "balance": "0x2fb474098f67c00000" + }, + "e62f98650712eb158753d82972b8e99ca3f61877": { + "balance": "0x6c6b935b8bbd400000" + }, + "e62f9d7c64e8e2635aeb883dd73ba684ee7c1079": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "e63e787414b9048478a50733359ecdd7e3647aa6": { + "balance": "0x55a6e79ccd1d300000" + }, + "e646665872e40b0d7aa2ff82729caaba5bc3e89e": { + "balance": "0x15af1d78b58c400000" + }, + "e64ef012658d54f8e8609c4e9023c09fe865c83b": { + "balance": "0x18493fba64ef00000" + }, + "e64f6e1d6401b56c076b64a1b0867d0b2f310d4e": { + "balance": "0x2cbad71c53ae50000" + }, + "e667f652f957c28c0e66d0b63417c80c8c9db878": { + "balance": "0x209d922f5259c50000" + }, + "e677c31fd9cb720075dca49f1abccd59ec33f734": { + "balance": "0x1a6d6beb1d42ee00000" + }, + "e67c2c1665c88338688187629f49e99b60b2d3ba": { + "balance": "0xad78ebc5ac6200000" + }, + "e69a6cdb3a8a7db8e1f30c8b84cd73bae02bc0f8": { + "balance": "0x394fdc2e452f6718000" + }, + "e69d1c378b771e0feff051db69d966ac6779f4ed": { + "balance": "0x1dfa6aaa1497040000" + }, + "e69fcc26ed225f7b2e379834c524d70c1735e5bc": { + "balance": "0x6c6b935b8bbd400000" + }, + "e6a3010f0201bc94ff67a2f699dfc206f9e76742": { + "balance": "0x2fa7cbf66464980000" + }, + "e6a6f6dd6f70a456f4ec15ef7ad5e5dbb68bd7dc": { + "balance": "0xad78ebc5ac6200000" + }, + "e6b20f980ad853ad04cbfc887ce6601c6be0b24c": { + "balance": "0xd8d726b7177a800000" + }, + "e6b3ac3f5d4da5a8857d0b3f30fc4b2b692b77d7": { + "balance": "0x4f2591f896a6500000" + }, + "e6b9545f7ed086e552924639f9a9edbbd5540b3e": { + "balance": "0xcbd47b6eaa8cc00000" + }, + "e6bcd30a8fa138c5d9e5f6c7d2da806992812dcd": { + "balance": "0x370ea0d47cf61a800000" + }, + "e6c81ffcecb47ecdc55c0b71e4855f3e5e97fc1e": { + "balance": "0x121ea68c114e510000" + }, + "e6cb260b716d4c0ab726eeeb07c8707204e276ae": { + "balance": "0x3635c9adc5dea00000" + }, + "e6cb3f3124c9c9cc3834b1274bc3336456a38bac": { + "balance": "0x172b1de0a213ff0000" + }, + "e6d22209ffd0b87509ade3a8e2ef429879cb89b5": { + "balance": "0x3a7aa9e1899ca300000" + }, + "e6d49f86c228f47367a35e886caacb271e539429": { + "balance": "0x165ec09da7a1980000" + }, + "e6e621eaab01f20ef0836b7cad47464cb5fd3c96": { + "balance": "0x11219342afa24b0000" + }, + "e6e886317b6a66a5b4f81bf164c538c264351765": { + "balance": "0x6c6b935b8bbd400000" + }, + "e6e9a39d750fe994394eb68286e5ea62a6997882": { + "balance": "0x2086ac351052600000" + }, + "e6ec5cf0c49b9c317e1e706315ef9eb7c0bf11a7": { + "balance": "0x3a469f3467e8ec00000" + }, + "e6f5eb649afb99599c414b27a9c9c855357fa878": { + "balance": "0x90f534608a72880000" + }, + "e6fe0afb9dcedd37b2e22c451ba6feab67348033": { + "balance": "0x21e19e0c9bab2400000" + }, + "e710dcd09b8101f9437bd97db90a73ef993d0bf4": { + "balance": "0x14ee36c05ac2520000" + }, + "e727e67ef911b81f6cf9c73fcbfebc2b02b5bfc6": { + "balance": "0x6c6b935b8bbd400000" + }, + "e72e1d335cc29a96b9b1c02f003a16d971e90b9d": { + "balance": "0x55a6e79ccd1d300000" + }, + "e7311c9533f0092c7248c9739b5b2c864a34b1ce": { + "balance": "0x97f97d6cc26dfe0000" + }, + "e73bfeada6f0fd016fbc843ebcf6e370a65be70c": { + "balance": "0x6acb3df27e1f880000" + }, + "e73ccf436725c151e255ccf5210cfce5a43f13e3": { + "balance": "0x1154e53217ddb0000" + }, + "e742b1e6069a8ffc3c4767235defb0d49cbed222": { + "balance": "0x2b5e3af16b18800000" + }, + "e74608f506866ada6bfbfdf20fea440be76989ef": { + "balance": "0x6c6acc67d7b1d40000" + }, + "e7533e270cc61fa164ac1553455c105d04887e14": { + "balance": "0x696d8590020bb0000" + }, + "e75c1fb177089f3e58b1067935a6596ef1737fb5": { + "balance": "0x56a879fa775470000" + }, + "e75c3b38a58a3f33d55690a5a59766be185e0284": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e761d27fa3502cc76bb1a608740e1403cf9dfc69": { + "balance": "0xf2dc7d47f15600000" + }, + "e766f34ff16f3cfcc97321721f43ddf5a38b0cf4": { + "balance": "0x54069233bf7f780000" + }, + "e76d945aa89df1e457aa342b31028a5e9130b2ce": { + "balance": "0x3708baed3d68900000" + }, + "e7735ec76518fc6aa92da8715a9ee3f625788f13": { + "balance": "0x6c4d160bafa1b78000" + }, + "e77a89bd45dc04eeb4e41d7b596b707e6e51e74c": { + "balance": "0x28a857425466f800000" + }, + "e77d7deab296c8b4fa07ca3be184163d5a6d606c": { + "balance": "0x5043904b671190000" + }, + "e77febabdf080f0f5dca1d3f5766f2a79c0ffa7c": { + "balance": "0x4b229d28a843680000" + }, + "e780a56306ba1e6bb331952c22539b858af9f77d": { + "balance": "0xa968163f0a57b400000" + }, + "e781ec732d401202bb9bd13860910dd6c29ac0b6": { + "balance": "0x433874f632cc600000" + }, + "e784dcc873aa8c1513ec26ff36bc92eac6d4c968": { + "balance": "0xad78ebc5ac6200000" + }, + "e7912d4cf4562c573ddc5b71e37310e378ef86c9": { + "balance": "0x155bd9307f9fe80000" + }, + "e791d585b89936b25d298f9d35f9f9edc25a2932": { + "balance": "0x6c6b935b8bbd400000" + }, + "e792349ce9f6f14f81d0674096befa1f9221cdea": { + "balance": "0x5b5d234a0db4388000" + }, + "e796fd4e839b4c95d7510fb7c5c72b83c6c3e3c7": { + "balance": "0x1bc433f23f83140000" + }, + "e7a42f59fee074e4fb13ea9e57ecf1cc48282249": { + "balance": "0x43c33c1937564800000" + }, + "e7a4560c84b20e0fb54c49670c2903b0a96c42a4": { + "balance": "0x206aeac7a903980000" + }, + "e7a8e471eafb798f4554cc6e526730fd56e62c7d": { + "balance": "0x3635c9adc5dea00000" + }, + "e7be82c6593c1eeddd2ae0b15001ff201ab57b2f": { + "balance": "0x10910d4cdc9f60000" + }, + "e7c6b5fc05fc748e5b4381726449a1c0ad0fb0f1": { + "balance": "0x6c6b935b8bbd400000" + }, + "e7d17524d00bad82497c0f27156a647ff51d2792": { + "balance": "0x1158e460913d00000" + }, + "e7d213947fcb904ad738480b1eed2f5c329f27e8": { + "balance": "0x103c3b1d3e9c30000" + }, + "e7d6240620f42c5edbb2ede6aec43da4ed9b5757": { + "balance": "0x3635c9adc5dea00000" + }, + "e7da609d40cde80f00ce5b4ffb6aa9d0b03494fc": { + "balance": "0x3635c9adc5dea00000" + }, + "e7f06f699be31c440b43b4db0501ec0e25261644": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e7f4d7fe6f561f7fa1da3005fd365451ad89df89": { + "balance": "0xad78ebc5ac6200000" + }, + "e7fd8fd959aed2767ea7fa960ce1db53af802573": { + "balance": "0x3635c9adc5dea00000" + }, + "e80e7fef18a5db15b01473f3ad6b78b2a2f8acd9": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e8137fc1b2ec7cc7103af921899b4a39e1d959a1": { + "balance": "0x50c5e761a444080000" + }, + "e81c2d346c0adf4cc56708f6394ba6c8c8a64a1e": { + "balance": "0x6c6b935b8bbd400000" + }, + "e82c58c579431b673546b53a86459acaf1de9b93": { + "balance": "0x3635c9adc5dea00000" + }, + "e834c64318205ca7dd4a21abcb08266cb21ff02c": { + "balance": "0x3635c6204739d98000" + }, + "e83604e4ff6be7f96f6018d3ec3072ec525dff6b": { + "balance": "0x9ddc1e3b901180000" + }, + "e845e387c4cbdf982280f6aa01c40e4be958ddb2": { + "balance": "0x54b40b1f852bda00000" + }, + "e848ca7ebff5c24f9b9c316797a43bf7c356292d": { + "balance": "0x62e115c008a880000" + }, + "e84b55b525f1039e744b918cb3332492e45eca7a": { + "balance": "0xad78ebc5ac6200000" + }, + "e84f8076a0f2969ecd333eef8de41042986291f2": { + "balance": "0x176b344f2a78c00000" + }, + "e864fec07ed1214a65311e11e329de040d04f0fd": { + "balance": "0x59ca83f5c404968000" + }, + "e87dbac636a37721df54b08a32ef4959b5e4ff82": { + "balance": "0x6c6b935b8bbd400000" + }, + "e87e9bbfbbb71c1a740c74c723426df55d063dd9": { + "balance": "0x1b1928c00c7a6380000" + }, + "e87eac6d602b4109c9671bf57b950c2cfdb99d55": { + "balance": "0x2b4f21972ecce0000" + }, + "e881bbbe69722d81efecaa48d1952a10a2bfac8f": { + "balance": "0x3635c9adc5dea000000" + }, + "e89249738b7eced7cb666a663c49cbf6de8343ea": { + "balance": "0x6c6b935b8bbd400000" + }, + "e89c22f1a4e1d4746ecfaa59ed386fee12d51e37": { + "balance": "0x26f8e87f0a7da0000" + }, + "e89da96e06beaf6bd880b378f0680c43fd2e9d30": { + "balance": "0x209a1a01a56fec0000" + }, + "e8a91da6cf1b9d65c74a02ec1f96eecb6dd241f3": { + "balance": "0x692ae8897081d00000" + }, + "e8a9a41740f44f54c3688b53e1ddd42e43c9fe94": { + "balance": "0xd8d726b7177a800000" + }, + "e8b28acda971725769db8f563d28666d41ddab6c": { + "balance": "0x21e19e0c9bab2400000" + }, + "e8be24f289443ee473bc76822f55098d89b91cc5": { + "balance": "0x6c6b935b8bbd400000" + }, + "e8c3d3b0e17f97d1e756e684f94e1470f99c95a1": { + "balance": "0x15af1d78b58c400000" + }, + "e8c3f045bb7d38c9d2f395b0ba8492b253230901": { + "balance": "0x1e7e4171bf4d3a00000" + }, + "e8cc43bc4f8acf39bff04ebfbf42aac06a328470": { + "balance": "0x15af1d78b58c400000" + }, + "e8d942d82f175ecb1c16a405b10143b3f46b963a": { + "balance": "0x1ed2e8ff6d971c0000" + }, + "e8ddbed732ebfe754096fde9086b8ea4a4cdc616": { + "balance": "0x6c6b935b8bbd400000" + }, + "e8de725eca5def805ff7941d31ac1c2e342dfe95": { + "balance": "0x857e0d6f1da76a0000" + }, + "e8e9850586e94f5299ab494bb821a5f40c00bd04": { + "balance": "0xcf152640c5c8300000" + }, + "e8ead1bb90ccc3aea2b0dcc5b58056554655d1d5": { + "balance": "0x1a4aba225c207400000" + }, + "e8eaf12944092dc3599b3953fa7cb1c9761cc246": { + "balance": "0x6194049f30f7200000" + }, + "e8ed51bbb3ace69e06024b33f86844c47348db9e": { + "balance": "0x22f9ea89f4a7d6c40000" + }, + "e8ef100d7ce0895832f2678df72d4acf8c28b8e3": { + "balance": "0x1b1b6bd7af64c70000" + }, + "e8f29969e75c65e01ce3d86154207d0a9e7c76f2": { + "balance": "0xa22fa9a73a27198000" + }, + "e8fc36b0131ec120ac9e85afc10ce70b56d8b6ba": { + "balance": "0xad78ebc5ac6200000" + }, + "e90a354cec04d69e5d96ddc0c5138d3d33150aa0": { + "balance": "0x1b1a7dcf8a44d38000" + }, + "e9133e7d31845d5f2b66a2618792e869311acf66": { + "balance": "0x517c0cbf9a390880000" + }, + "e91dac0195b19e37b59b53f7c017c0b2395ba44c": { + "balance": "0x65ea3db75546600000" + }, + "e91fa0badaddb9a97e88d3f4db7c55d6bb7430fe": { + "balance": "0x14620c57dddae00000" + }, + "e923c06177b3427ea448c0a6ff019b54cc548d95": { + "balance": "0x1f780014667f28000" + }, + "e93d47a8ca885d540c4e526f25d5c6f2c108c4b8": { + "balance": "0x17da3a04c7b3e0000000" + }, + "e9458f68bb272cb5673a04f781b403556fd3a387": { + "balance": "0x34e8b88cee2d40000" + }, + "e94941b6036019b4016a30c1037d5a6903babaad": { + "balance": "0x2a48acab6204b00000" + }, + "e9495ba5842728c0ed97be37d0e422b98d69202c": { + "balance": "0x6c6b935b8bbd400000" + }, + "e94ded99dcb572b9bb1dcba32f6dee91e057984e": { + "balance": "0x155bd9307f9fe80000" + }, + "e95179527deca5916ca9a38f215c1e9ce737b4c9": { + "balance": "0x21e19e0c9bab2400000" + }, + "e9559185f166fc9513cc71116144ce2deb0f1d4b": { + "balance": "0x43c33c1937564800000" + }, + "e95e92bbc6de07bf3a660ebf5feb1c8a3527e1c5": { + "balance": "0xfc936392801c0000" + }, + "e965daa34039f7f0df62375a37e5ab8a72b301e7": { + "balance": "0x103fddecdb3f5700000" + }, + "e969ea1595edc5c4a707cfde380929633251a2b0": { + "balance": "0xad78ebc5ac6200000" + }, + "e96b184e1f0f54924ac874f60bbf44707446b72b": { + "balance": "0x9dcc0515b56e0c0000" + }, + "e96d7d4cdd15553a4e4d316d6d6480ca3cea1e38": { + "balance": "0x2955d02e1a135a00000" + }, + "e96e2d3813efd1165f12f602f97f4a62909d3c66": { + "balance": "0x7caee97613e6700000" + }, + "e97fde0b67716325cf0ecce8a191a3761b2c791d": { + "balance": "0x3677036edf0af60000" + }, + "e982e6f28c548f5f96f45e63f7ab708724f53fa1": { + "balance": "0x157ae829a41f3b0000" + }, + "e9864c1afc8eaad37f3ba56fcb7477cc622009b7": { + "balance": "0x448586170a7dc0000" + }, + "e987e6139e6146a717fef96bc24934a5447fe05d": { + "balance": "0x6c6b935b8bbd400000" + }, + "e989733ca1d58d9e7b5029ba5d444858bec03172": { + "balance": "0x1f87408313df4f8000" + }, + "e98c91cadd924c92579e11b41217b282956cdaa1": { + "balance": "0x75c9a8480320c0000" + }, + "e99aece90541cae224b87da673965e0aeb296afd": { + "balance": "0x31df9095a18f600000" + }, + "e99de258a4173ce9ac38ede26c0b3bea3c0973d5": { + "balance": "0x59d0b805e5bb300000" + }, + "e9a2b4914e8553bf0d7c00ca532369b879f931bf": { + "balance": "0x6c6b935b8bbd400000" + }, + "e9a39a8bac0f01c349c64cedb69897f633234ed2": { + "balance": "0xd7c198710e66b00000" + }, + "e9a5ae3c9e05977dd1069e9fd9d3aefbae04b8df": { + "balance": "0x6acb3df27e1f880000" + }, + "e9ac36376efa06109d40726307dd1a57e213eaa9": { + "balance": "0xa844a7424d9c80000" + }, + "e9b1f1fca3fa47269f21b061c353b7f5e96d905a": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "e9b36fe9b51412ddca1a521d6e94bc901213dda8": { + "balance": "0x21e19e0c9bab2400000" + }, + "e9b4a4853577a9dbcc2e795be0310d1bed28641a": { + "balance": "0x3635c9adc5dea00000" + }, + "e9b6a790009bc16642c8d820b7cde0e9fd16d8f5": { + "balance": "0xc55325ca7415e00000" + }, + "e9b9a2747510e310241d2ece98f56b3301d757e0": { + "balance": "0x6c6b935b8bbd400000" + }, + "e9c35c913ca1fceab461582fe1a5815164b4fd21": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "e9c6dfae97f7099fc5f4e94b784db802923a1419": { + "balance": "0x2a53c6d724f100000" + }, + "e9c758f8da41e3346e4350e5ac3976345c6c1082": { + "balance": "0x68a0d3092826ad0000" + }, + "e9caf827be9d607915b365c83f0d3b7ea8c79b50": { + "balance": "0xa2a15d09519be00000" + }, + "e9cafe41a5e8bbd90ba02d9e06585b4eb546c57f": { + "balance": "0x6c6b935b8bbd400000" + }, + "e9d599456b2543e6db80ea9b210e908026e2146e": { + "balance": "0xad78ebc5ac6200000" + }, + "e9e1f7cb00a110edd0ebf8b377ef8a7bb856117f": { + "balance": "0xad78ebc5ac6200000" + }, + "ea14bfda0a6e76668f8788321f07df37824ec5df": { + "balance": "0x2a5a058fc295ed000000" + }, + "ea1ea0c599afb9cd36caacbbb52b5bbb97597377": { + "balance": "0x39fbae8d042dd00000" + }, + "ea1efb3ce789bedec3d67c3e1b3bc0e9aa227f90": { + "balance": "0x27ca4bd719f0b80000" + }, + "ea2c197d26e98b0da83e1b72c787618c979d3db0": { + "balance": "0x11164759ffb320000" + }, + "ea3779d14a13f6c78566bcde403591413a6239db": { + "balance": "0x29b76432b94451200000" + }, + "ea4e809e266ae5f13cdbe38f9d0456e6386d1274": { + "balance": "0xf3f20b8dfa69d00000" + }, + "ea53c954f4ed97fd4810111bdab69ef981ef25b9": { + "balance": "0x3a9d5baa4abf1d00000" + }, + "ea53d26564859d9e90bb0e53b7abf560e0162c38": { + "balance": "0x15af1d78b58c400000" + }, + "ea60436912de6bf187d3a472ff8f5333a0f7ed06": { + "balance": "0x11164759ffb320000" + }, + "ea60549ec7553f511d2149f2d4666cbd9243d93c": { + "balance": "0x6c6b935b8bbd400000" + }, + "ea66e7b84dcdbf36eea3e75b85382a75f1a15d96": { + "balance": "0x5dbc9191266f118000" + }, + "ea686c5057093c171c66db99e01b0ececb308683": { + "balance": "0x14dda85d2ce1478000" + }, + "ea6afe2cc928ac8391eb1e165fc40040e37421e7": { + "balance": "0xa27fa063b2e2e68000" + }, + "ea79057dabef5e64e7b44f7f18648e7e533718d2": { + "balance": "0xad78ebc5ac6200000" + }, + "ea7c4d6dc729cd6b157c03ad237ca19a209346c3": { + "balance": "0x6c6b935b8bbd400000" + }, + "ea8168fbf225e786459ca6bb18d963d26b505309": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ea81ca8638540cd9d4d73d060f2cebf2241ffc3e": { + "balance": "0x6acb3df27e1f880000" + }, + "ea8317197959424041d9d7c67a3ece1dbb78bb55": { + "balance": "0x155bd9307f9fe80000" + }, + "ea8527febfa1ade29e26419329d393b940bbb7dc": { + "balance": "0x6c6acc67d7b1d40000" + }, + "ea8f30b6e4c5e65290fb9864259bc5990fa8ee8a": { + "balance": "0x1158e460913d00000" + }, + "ea94f32808a2ef8a9bf0861d1d2404f7b7be258a": { + "balance": "0x1158e460913d00000" + }, + "eaa45cea02d87d2cc8fda9434e2d985bd4031584": { + "balance": "0x681fc2cc6e2b8b0000" + }, + "eab0bd148309186cf8cbd13b7232d8095acb833a": { + "balance": "0x2439a881c6a717c0000" + }, + "eabb90d37989aab31feae547e0e6f3999ce6a35d": { + "balance": "0x6c6b935b8bbd400000" + }, + "eac0827eff0c6e3ff28a7d4a54f65cb7689d7b99": { + "balance": "0x9ad9e69f9d47520000" + }, + "eac1482826acb6111e19d340a45fb851576bed60": { + "balance": "0x1be8bab04d9be8000" + }, + "eac17b81ed5191fb0802aa54337313834107aaa4": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "eac3af5784927fe9a598fc4eec38b8102f37bc58": { + "balance": "0x3635c9adc5dea00000" + }, + "eac6b98842542ea10bb74f26d7c7488f698b6452": { + "balance": "0x43c33c1937564800000" + }, + "eac768bf14b8f9432e69eaa82a99fbeb94cd0c9c": { + "balance": "0x14dbb2195ca228900000" + }, + "ead21c1deccfbf1c5cd96688a2476b69ba07ce4a": { + "balance": "0x3f24d8e4a00700000" + }, + "ead4d2eefb76abae5533961edd11400406b298fc": { + "balance": "0xd255d112e103a00000" + }, + "ead65262ed5d122df2b2751410f98c32d1238f51": { + "balance": "0x58317ed46b9b80000" + }, + "ead75016e3a0815072b6b108bcc1b799acf0383e": { + "balance": "0x6c6b935b8bbd400000" + }, + "eaea23aa057200e7c9c15e8ff190d0e66c0c0e83": { + "balance": "0x6c6b935b8bbd400000" + }, + "eaed16eaf5daab5bf0295e5e077f59fb8255900b": { + "balance": "0xd8d726b7177a800000" + }, + "eaedcc6b8b6962d5d9288c156c579d47c0a9fcff": { + "balance": "0x49b9ca9a694340000" + }, + "eaf52388546ec35aca6f6c6393d8d609de3a4bf3": { + "balance": "0x1158e460913d00000" + }, + "eb10458daca79e4a6b24b29a8a8ada711b7f2eb6": { + "balance": "0xd8bb6549b02bb80000" + }, + "eb1cea7b45d1bd4d0e2a007bd3bfb354759e2c16": { + "balance": "0xabbcd4ef377580000" + }, + "eb25481fcd9c221f1ac7e5fd1ecd9307a16215b8": { + "balance": "0xaadec983fcff40000" + }, + "eb2ef3d38fe652403cd4c9d85ed7f0682cd7c2de": { + "balance": "0x90f534608a728800000" + }, + "eb3bdd59dcdda5a9bb2ac1641fd02180f5f36560": { + "balance": "0x165c96647b38a200000" + }, + "eb3ce7fc381c51db7d5fbd692f8f9e058a4c703d": { + "balance": "0xad78ebc5ac6200000" + }, + "eb453f5a3adddd8ab56750fadb0fe7f94d9c89e7": { + "balance": "0x1158e460913d00000" + }, + "eb4f00e28336ea09942588eeac921811c522143c": { + "balance": "0x6c6b935b8bbd400000" + }, + "eb52ab10553492329c1c54833ae610f398a65b9d": { + "balance": "0x83d6c7aab63600000" + }, + "eb570dba975227b1c42d6e8dea2c56c9ad960670": { + "balance": "0x6c6b935b8bbd400000" + }, + "eb6394a7bfa4d28911d5a5b23e93f35e340c2294": { + "balance": "0x43a77aabd00780000" + }, + "eb6810691d1ae0d19e47bd22cebee0b3ba27f88a": { + "balance": "0x87856315d878150000" + }, + "eb76424c0fd597d3e341a9642ad1ee118b2b579d": { + "balance": "0xd8d726b7177a800000" + }, + "eb7c202b462b7cc5855d7484755f6e26ef43a115": { + "balance": "0x6c6b935b8bbd400000" + }, + "eb835c1a911817878a33d167569ea3cdd387f328": { + "balance": "0x3635c9adc5dea00000" + }, + "eb89a882670909cf377e9e78286ee97ba78d46c2": { + "balance": "0x2b7cc2e9c3225c0000" + }, + "eb90c793b3539761e1c814a29671148692193eb4": { + "balance": "0x28a857425466f800000" + }, + "eb9cc9fe0869d2dab52cc7aae8fd57adb35f9feb": { + "balance": "0x6a93bb17af81f80000" + }, + "eba388b0da27c87b1cc0eac6c57b2c5a0b459c1a": { + "balance": "0x170a0f5040e50400000" + }, + "ebaa216de9cc5a43031707d36fe6d5bedc05bdf0": { + "balance": "0x6ac5c62d9486070000" + }, + "ebac2b4408ef5431a13b8508e86250982114e145": { + "balance": "0xd8d726b7177a800000" + }, + "ebb62cf8e22c884b1b28c6fa88fbbc17938aa787": { + "balance": "0x2b42798403c9b80000" + }, + "ebb7d2e11bc6b58f0a8d45c2f6de3010570ac891": { + "balance": "0x1731790534df20000" + }, + "ebbb4f2c3da8be3eb62d1ffb1f950261cf98ecda": { + "balance": "0x6c6b935b8bbd400000" + }, + "ebbd4db9019952d68b1b0f6d8cf0683c00387bb5": { + "balance": "0x120401563d7d910000" + }, + "ebbeeb259184a6e01cccfc2207bbd883785ac90a": { + "balance": "0x219bc1b04783d30000" + }, + "ebd356156a383123343d48843bffed6103e866b3": { + "balance": "0x6acb3df27e1f880000" + }, + "ebd37b256563e30c6f9289a8e2702f0852880833": { + "balance": "0x6c6acc67d7b1d40000" + }, + "ebe46cc3c34c32f5add6c3195bb486c4713eb918": { + "balance": "0x3635c9adc5dea00000" + }, + "ebff84bbef423071e604c361bba677f5593def4e": { + "balance": "0x21e19e0c9bab2400000" + }, + "ec0927bac7dc36669c28354ab1be83d7eec30934": { + "balance": "0x6c6b935b8bbd400000" + }, + "ec0e18a01dc4dc5daae567c3fa4c7f8f9b590205": { + "balance": "0x111ffe404a41e60000" + }, + "ec11362cec810985d0ebbd7b73451444985b369f": { + "balance": "0x65a4e49577057318000" + }, + "ec2cb8b9378dff31aec3c22e0e6dadff314ab5dd": { + "balance": "0x6c6b935b8bbd400000" + }, + "ec30addd895b82ee319e54fb04cb2bb03971f36b": { + "balance": "0x6c6b935b8bbd400000" + }, + "ec3b8b58a12703e581ce5ffd7e21c57d1e5c663f": { + "balance": "0x5c283d410394100000" + }, + "ec4867d2175ab5b9469361595546554684cda460": { + "balance": "0xa2a15d09519be00000" + }, + "ec4d08aa2e47496dca87225de33f2b40a8a5b36f": { + "balance": "0x890b0c2e14fb80000" + }, + "ec58bc0d0c20d8f49465664153c5c196fe59e6be": { + "balance": "0x15af1d78b58c400000" + }, + "ec5b198a00cfb55a97b5d53644cffa8a04d2ab45": { + "balance": "0x6c6b935b8bbd400000" + }, + "ec5df227bfa85d7ad76b426e1cee963bc7f519dd": { + "balance": "0x3635c9adc5dea00000" + }, + "ec5feafe210c12bfc9a5d05925a123f1e73fbef8": { + "balance": "0x608fcf3d88748d000000" + }, + "ec6904bae1f69790591709b0609783733f2573e3": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ec73114c5e406fdbbe09b4fa621bd70ed54ea1ef": { + "balance": "0x53025cd216fce500000" + }, + "ec73833de4b810bb027810fc8f69f544e83c12d1": { + "balance": "0x3635c9adc5dea00000" + }, + "ec75b4a47513120ba5f86039814f1998e3817ac3": { + "balance": "0x9b0bce2e8fdba0000" + }, + "ec76f12e57a65504033f2c0bce6fc03bd7fa0ac4": { + "balance": "0xc2127af858da700000" + }, + "ec8014efc7cbe5b0ce50f3562cf4e67f8593cd32": { + "balance": "0xf015f25736420000" + }, + "ec82f50d06475f684df1b392e00da341aa145444": { + "balance": "0x6c6b935b8bbd400000" + }, + "ec83e798c396b7a55e2a2224abcd834b27ea459c": { + "balance": "0x28a857425466f800000" + }, + "ec89f2b678a1a15b9134ec5eb70c6a62071fbaf9": { + "balance": "0xad78ebc5ac6200000" + }, + "ec8c1d7b6aaccd429db3a91ee4c9eb1ca4f6f73c": { + "balance": "0xe664992288f2280000" + }, + "ec9851bd917270610267d60518b54d3ca2b35b17": { + "balance": "0x878678326eac9000000" + }, + "ec99e95dece46ffffb175eb6400fbebb08ee9b95": { + "balance": "0x56bc75e2d63100000" + }, + "eca5f58792b8c62d2af556717ee3ee3028be4dce": { + "balance": "0x6c6b935b8bbd400000" + }, + "ecab5aba5b828de1705381f38bc744b32ba1b437": { + "balance": "0x32f51edbaaa3300000" + }, + "ecaf3350b7ce144d068b186010852c84dd0ce0f0": { + "balance": "0x6c6b935b8bbd400000" + }, + "ecb94c568bfe59ade650645f4f26306c736cace4": { + "balance": "0xe7eeba3410b740000" + }, + "ecbe425e670d39094e20fb5643a9d818eed236de": { + "balance": "0x10f0cf064dd59200000" + }, + "ecbe5e1c9ad2b1dccf0a305fc9522f4669dd3ae7": { + "balance": "0x10f0cf064dd59200000" + }, + "eccf7a0457b566b346ca673a180f444130216ac3": { + "balance": "0x56bc75e2d63100000" + }, + "ecd1a62802351a41568d23033004acc6c005a5d3": { + "balance": "0x2b5e3af16b1880000" + }, + "ecd276af64c79d1bd9a92b86b5e88d9a95eb88f8": { + "balance": "0x1158e460913d00000" + }, + "ecd486fc196791b92cf612d348614f9156488b7e": { + "balance": "0x28a857425466f800000" + }, + "ecdaf93229b45ee672f65db506fb5eca00f7fce6": { + "balance": "0x5701f96dcc40ee8000" + }, + "ece111670b563ccdbebca52384290ecd68fe5c92": { + "balance": "0x1158e460913d00000" + }, + "ece1152682b7598fe2d1e21ec15533885435ac85": { + "balance": "0xd8d726b7177a800000" + }, + "ece1290877b583e361a2d41b009346e6274e2538": { + "balance": "0x1043561a8829300000" + }, + "ecf05d07ea026e7ebf4941002335baf2fed0f002": { + "balance": "0xad78ebc5ac6200000" + }, + "ecf24cdd7c22928c441e694de4aa31b0fab59778": { + "balance": "0x2086ac351052600000" + }, + "ecfd004d02f36cd4d8b4a8c1a9533b6af85cd716": { + "balance": "0x10f41acb4bb3b9c0000" + }, + "ed0206cb23315128f8caff26f6a30b985467d022": { + "balance": "0x878678326eac9000000" + }, + "ed1065dbcf9d73c04ffc7908870d881468c1e132": { + "balance": "0x6c6b935b8bbd400000" + }, + "ed1276513b6fc68628a74185c2e20cbbca7817bf": { + "balance": "0xa5aa85009e39c0000" + }, + "ed12a1ba1fb8adfcb20dfa19582e525aa3b74524": { + "balance": "0x16a6502f15a1e540000" + }, + "ed16ce39feef3bd7f5d162045e0f67c0f00046bb": { + "balance": "0x1158e460913d00000" + }, + "ed1a5c43c574d4e934299b24f1472cdc9fd6f010": { + "balance": "0xad78ebc5ac6200000" + }, + "ed1b24b6912d51b334ac0de6e771c7c0454695ea": { + "balance": "0x22b1c8c1227a00000" + }, + "ed1f1e115a0d60ce02fb25df014d289e3a0cbe7d": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "ed31305c319f9273d3936d8f5b2f71e9b1b22963": { + "balance": "0x56bc75e2d63100000" + }, + "ed327a14d5cfadd98103fc0999718d7ed70528ea": { + "balance": "0x4e1003b28d92800000" + }, + "ed3cbc3782cebd67989b305c4133b2cde32211eb": { + "balance": "0x15af1d78b58c400000" + }, + "ed4014538cee664a2fbcb6dc669f7ab16d0ba57c": { + "balance": "0xad78ebc5ac6200000" + }, + "ed41e1a28f5caa843880ef4e8b08bd6c33141edf": { + "balance": "0x2ad5ddfa7a8d830000" + }, + "ed4be04a052d7accb3dcce90319dba4020ab2c68": { + "balance": "0x7f37a70eaf362178000" + }, + "ed52a2cc0869dc9e9f842bd0957c47a8e9b0c9ff": { + "balance": "0x205b4dfa1ee74780000" + }, + "ed5b4c41e762d942404373caf21ed4615d25e6c1": { + "balance": "0x6d2d4f3d9525b40000" + }, + "ed60c4ab6e540206317e35947a63a9ca6b03e2cb": { + "balance": "0x31ad9ad0b467f8000" + }, + "ed641e06368fb0efaa1703e01fe48f4a685309eb": { + "balance": "0xad78ebc5ac6200000" + }, + "ed6643c0e8884b2d3211853785a08bf8f33ed29f": { + "balance": "0x487a9a304539440000" + }, + "ed70a37cdd1cbda9746d939658ae2a6181288578": { + "balance": "0x2086ac3510526000000" + }, + "ed7346766e1a676d0d06ec821867a276a083bf31": { + "balance": "0xd98a0931cc2d490000" + }, + "ed862616fcbfb3becb7406f73c5cbff00c940755": { + "balance": "0x5c283d410394100000" + }, + "ed9e030ca75cb1d29ea01d0d4cdfdccd3844b6e4": { + "balance": "0x1acc116cfafb18000" + }, + "ed9ebccba42f9815e78233266dd6e835b6afc31b": { + "balance": "0x14542ba12a337c00000" + }, + "ed9fb1f5af2fbf7ffc5029cee42b70ff5c275bf5": { + "balance": "0xf2dc7d47f15600000" + }, + "eda4b2fa59d684b27a810df8978a73df308a63c2": { + "balance": "0xd8d726b7177a800000" + }, + "edb473353979a206879de144c10a3c51d7d7081a": { + "balance": "0x14542ba12a337c00000" + }, + "edb71ec41bda7dce86e766e6e8c3e9907723a69b": { + "balance": "0x1158e460913d00000" + }, + "edbac9527b54d6df7ae2e000cca3613ba015cae3": { + "balance": "0x6acb3df27e1f880000" + }, + "edc22fb92c638e1e21ff5cf039daa6e734dafb29": { + "balance": "0x102794ad20da680000" + }, + "eddacd94ec89a2ef968fcf977a08f1fae2757869": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "eddbaafbc21be8f25562f1ed6d05d6afb58f02c2": { + "balance": "0x6c6b935b8bbd400000" + }, + "ede0147ec032c3618310c1ff25690bf172193dac": { + "balance": "0x6c6b935b8bbd400000" + }, + "ede5de7c7fb7eee0f36e64530a41440edfbefacf": { + "balance": "0x21755ee1ef2b180000" + }, + "ede79ae1ff4f1606d59270216fa46ab2ddd4ecaa": { + "balance": "0x7ea28327577080000" + }, + "ede8c2cb876fbe8a4cca8290361a7ea01a69fdf8": { + "balance": "0x1a78c6b44f841838000" + }, + "edeb4894aadd0081bbddd3e8846804b583d19f27": { + "balance": "0x6c6b935b8bbd400000" + }, + "edf603890228d7d5de9309942b5cad4219ef9ad7": { + "balance": "0x10f0cf064dd59200000" + }, + "edf8a3e1d40f13b79ec8e3e1ecf262fd92116263": { + "balance": "0x890b0c2e14fb80000" + }, + "edfda2d5db98f9380714664d54b4ee971a1cae03": { + "balance": "0x22bb8ddd679be0000" + }, + "ee0007b0960d00908a94432a737557876aac7c31": { + "balance": "0x2e0421e69c4cc8000" + }, + "ee049af005974dd1c7b3a9ca8d9aa77175ba53aa": { + "balance": "0x1211ecb56d13488000" + }, + "ee25b9a7032679b113588ed52c137d1a053a1e94": { + "balance": "0xad50f3f4eea8e0000" + }, + "ee31167f9cc93b3c6465609d79db0cde90e8484c": { + "balance": "0x6c6b935b8bbd400000" + }, + "ee34c7e7995db9f187cff156918cfb6f13f6e003": { + "balance": "0x6a4076cf7995a00000" + }, + "ee3564f5f1ba0f94ec7bac164bddbf31c6888b55": { + "balance": "0x56bc75e2d63100000" + }, + "ee58fb3db29070d0130188ce472be0a172b89055": { + "balance": "0x21f42dcdc58e39c0000" + }, + "ee655bb4ee0e8d5478526fb9f15e4064e09ff3dd": { + "balance": "0xad78ebc5ac6200000" + }, + "ee6959de2b67967b71948c891ab00d8c8f38c7dc": { + "balance": "0x6685ac1bfe32c0000" + }, + "ee6c03429969ca1262cb3f0a4a54afa7d348d7f5": { + "balance": "0xde219f91fc18a0000" + }, + "ee71793e3acf12a7274f563961f537529d89c7de": { + "balance": "0x6c6b935b8bbd400000" + }, + "ee7288d91086d9e2eb910014d9ab90a02d78c2a0": { + "balance": "0x6c6b935b8bbd400000" + }, + "ee7c3ded7c28f459c92fe13b4d95bafbab02367d": { + "balance": "0x25f273933db5700000" + }, + "ee867d20916bd2e9c9ece08aa04385db667c912e": { + "balance": "0xa968163f0a57b400000" + }, + "ee899b02cbcb3939cd61de1342d50482abb68532": { + "balance": "0x5f68e8131ecf800000" + }, + "ee906d7d5f1748258174be4cbc38930302ab7b42": { + "balance": "0xad78ebc5ac6200000" + }, + "ee97aa8ac69edf7a987d6d70979f8ec1fbca7a94": { + "balance": "0x14620c57dddae00000" + }, + "eea1e97988de75d821cd28ad6822b22cce988b31": { + "balance": "0x1c30731cec03200000" + }, + "eed28c3f068e094a304b853c950a6809ebcb03e0": { + "balance": "0x3a9d5baa4abf1d00000" + }, + "eed384ef2d41d9d203974e57c12328ea760e08ea": { + "balance": "0x3635c9adc5dea00000" + }, + "eedf6c4280e6eb05b934ace428e11d4231b5905b": { + "balance": "0xad78ebc5ac6200000" + }, + "eee761847e33fd61d99387ee14628694d1bfd525": { + "balance": "0x6c6b935b8bbd400000" + }, + "eee9d0526eda01e43116a395322dda8970578f39": { + "balance": "0x21e1999bbd5d2be0000" + }, + "eef1bbb1e5a83fde8248f88ee3018afa2d1332eb": { + "balance": "0xad78ebc5ac6200000" + }, + "eefba12dfc996742db790464ca7d273be6e81b3e": { + "balance": "0x3635c9adc5dea00000" + }, + "eefd05b0e3c417d55b3343060486cdd5e92aa7a6": { + "balance": "0x4d853c8f8908980000" + }, + "ef0dc7dd7a53d612728bcbd2b27c19dd4d7d666f": { + "balance": "0x26411c5b35f05a0000" + }, + "ef115252b1b845cd857f002d630f1b6fa37a4e50": { + "balance": "0x6acb3df27e1f880000" + }, + "ef1c0477f1184d60accab374d374557a0a3e10f3": { + "balance": "0x83d6c7aab63600000" + }, + "ef2c34bb487d3762c3cca782ccdd7a8fbb0a9931": { + "balance": "0x9c2007651b2500000" + }, + "ef35f6d4b1075e6aa139151c974b2f4658f70538": { + "balance": "0x3c3bc33f94e50d8000" + }, + "ef39ca9173df15531d73e6b72a684b51ba0f2bb4": { + "balance": "0x56a0b4756ee2380000" + }, + "ef463c2679fb279164e20c3d2691358773a0ad95": { + "balance": "0x6c6b935b8bbd400000" + }, + "ef47cf073e36f271d522d7fa4e7120ad5007a0bc": { + "balance": "0x878678326eac900000" + }, + "ef61155ba009dcdebef10b28d9da3d1bc6c9ced4": { + "balance": "0x3342d60dff1960000" + }, + "ef69781f32ffce33346f2c9ae3f08493f3e82f89": { + "balance": "0xfc936392801c0000" + }, + "ef76a4cd8febcbc9b818f17828f8d93473f3f3cb": { + "balance": "0xd8d726b7177a800000" + }, + "ef93818f684db0c3675ec81332b3183ecc28a495": { + "balance": "0x54069233bf7f780000" + }, + "ef9f59aeda418c1494682d941aab4924b5f4929a": { + "balance": "0x152d02c7e14af6800000" + }, + "efa6b1f0db603537826891b8b4bc163984bb40cd": { + "balance": "0x35659ef93f0fc40000" + }, + "efbd52f97da5fd3a673a46cbf330447b7e8aad5c": { + "balance": "0x56c3c9b80a0a68000" + }, + "efc8cf1963c9a95267b228c086239889f4dfd467": { + "balance": "0x21e19e0c9bab2400000" + }, + "efcaae9ff64d2cd95b5249dcffe7faa0a0c0e44d": { + "balance": "0x15be6174e1912e0000" + }, + "efcce06bd6089d0e458ef561f5a689480afe7000": { + "balance": "0x2086ac351052600000" + }, + "efe0675da98a5dda70cd96196b87f4e726b43348": { + "balance": "0x3f19beb8dd1ab00000" + }, + "efe8ff87fc260e0767638dd5d02fc4672e0ec06d": { + "balance": "0x6c6b935b8bbd400000" + }, + "efeb1997aad277cc33430e6111ed0943594048b8": { + "balance": "0x6c6b935b8bbd400000" + }, + "efeea010756f81da4ba25b721787f058170befbd": { + "balance": "0x1c29c9cf770ef0000" + }, + "eff51d72adfae143edf3a42b1aec55a2ccdd0b90": { + "balance": "0x1043561a8829300000" + }, + "eff86b5123bcdc17ed4ce8e05b7e12e51393a1f7": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "effc15e487b1beda0a8d1325bdb4172240dc540a": { + "balance": "0x3853939eee1de0000" + }, + "f01195d657ef3c942e6cb83949e5a20b5cfa8b1e": { + "balance": "0x57473d05dabae800000" + }, + "f02796295101674288c1d93467053d042219b794": { + "balance": "0x281d901f4fdd100000" + }, + "f039683d7b3d225bc7d8dfadef63163441be41e2": { + "balance": "0x1dd1e4bd8d1ee0000" + }, + "f04a6a379708b9428d722aa2b06b77e88935cf89": { + "balance": "0x1043561a8829300000" + }, + "f04d2c91efb6e9c45ffbe74b434c8c5f2b028f1f": { + "balance": "0x3635c9adc5dea00000" + }, + "f057aa66ca767ede124a1c5b9cc5fc94ef0b0137": { + "balance": "0x70a24bcab6f45d0000" + }, + "f05ba8d7b68539d933300bc9289c3d9474d0419e": { + "balance": "0x6da27024dd9600000" + }, + "f05ceeab65410564709951773c8445ad9f4ec797": { + "balance": "0x10431627a0933b0000" + }, + "f05fcd4c0d73aa167e5553c8c0d6d4f2faa39757": { + "balance": "0x2d2d66c3170b2980000" + }, + "f067e1f1d683556a4cc4fd0c0313239f32c4cfd8": { + "balance": "0x3635c9adc5dea00000" + }, + "f067fb10dfb293e998abe564c055e3348f9fbf1e": { + "balance": "0x6c6b935b8bbd400000" + }, + "f068dfe95d15cd3a7f98ffa688b4346842be2690": { + "balance": "0x440ad819e0974c0000" + }, + "f06a854a3c5dc36d1c49f4c87d6db333b57e4add": { + "balance": "0x21e19e0c9bab2400000" + }, + "f079e1b1265f50e8c8a98ec0c7815eb3aeac9eb4": { + "balance": "0x116dc3a8994b30000" + }, + "f07bd0e5c2ce69c7c4a724bd26bbfa9d2a17ca03": { + "balance": "0x14061b9d77a5e980000" + }, + "f0832a6bb25503eeca435be31b0bf905ca1fcf57": { + "balance": "0x16a6502f15a1e540000" + }, + "f09b3e87f913ddfd57ae8049c731dba9b636dfc3": { + "balance": "0x20f5b1eaad8d800000" + }, + "f0b1340b996f6f0bf0d9561c849caf7f4430befa": { + "balance": "0x56bc75e2d63100000" + }, + "f0b1f9e27832c6de6914d70afc238c749995ace4": { + "balance": "0x6c6b935b8bbd400000" + }, + "f0b469eae89d400ce7d5d66a9695037036b88903": { + "balance": "0x43c33c1937564800000" + }, + "f0b9d683cea12ba600baace219b0b3c97e8c00e4": { + "balance": "0x56bc75e2d63100000" + }, + "f0be0faf4d7923fc444622d1980cf2d990aab307": { + "balance": "0x6c6b935b8bbd400000" + }, + "f0c081da52a9ae36642adf5e08205f05c54168a6": { + "balance": "0x6046f37e5945c0000" + }, + "f0c70d0d6dab7663aa9ed9ceea567ee2c6b02765": { + "balance": "0x71438ac5a791a08000" + }, + "f0cbef84e169630098d4e301b20208ef05846ac9": { + "balance": "0xe0b8345506b4e0000" + }, + "f0d21663d8b0176e05fde1b90ef31f8530fda95f": { + "balance": "0x6c6acc67d7b1d40000" + }, + "f0d5c31ccb6cbe30c7c9ea19f268d159851f8c9c": { + "balance": "0x3894f0e6f9b9f700000" + }, + "f0d64cf9df09741133d170485fd24b005011d520": { + "balance": "0x1b089341e14fcc0000" + }, + "f0d858105e1b648101ac3f85a0f8222bf4f81d6a": { + "balance": "0x2086ac351052600000" + }, + "f0dc43f205619127507b2b1c1cfdf32d28310920": { + "balance": "0x105eb79b9417088000" + }, + "f0e1dfa42adeac2f17f6fdf584c94862fd563393": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f0e2649c7e6a3f2c5dfe33bbfbd927ca3c350a58": { + "balance": "0x6c6b935b8bbd400000" + }, + "f0e7fb9e420a5340d536f40408344feaefc06aef": { + "balance": "0x3635c9adc5dea00000" + }, + "f10462e58fcc07f39584a187639451167e859201": { + "balance": "0x934dd5d33bc970000" + }, + "f10661ff94140f203e7a482572437938bec9c3f7": { + "balance": "0x43c33c1937564800000" + }, + "f114ff0d0f24eff896edde5471dea484824a99b3": { + "balance": "0xbe202d6a0eda0000" + }, + "f116b0b4680f53ab72c968ba802e10aa1be11dc8": { + "balance": "0x1158e460913d00000" + }, + "f11cf5d363746fee6864d3ca336dd80679bb87ae": { + "balance": "0x878678326eac9000000" + }, + "f11e01c7a9d12499005f4dae7716095a34176277": { + "balance": "0x15af1d78b58c400000" + }, + "f13b083093ba564e2dc631568cf7540d9a0ec719": { + "balance": "0x6c6acc67d7b1d40000" + }, + "f14f0eb86db0eb68753f16918e5d4b807437bd3e": { + "balance": "0xad78ebc5ac6200000" + }, + "f15178ffc43aa8070ece327e930f809ab1a54f9d": { + "balance": "0xab640391201300000" + }, + "f156dc0b2a981e5b55d3f2f03b8134e331dbadb7": { + "balance": "0x56bc75e2d63100000" + }, + "f15d9d5a21b1929e790371a17f16d95f0c69655c": { + "balance": "0x6c6b935b8bbd400000" + }, + "f15e182c4fbbad79bd93342242d4dccf2be58925": { + "balance": "0x692ae8897081d00000" + }, + "f1624d980b65336feac5a6d54125005cfcf2aacb": { + "balance": "0x6c6b935b8bbd400000" + }, + "f167f5868dcf4233a7830609682caf2df4b1b807": { + "balance": "0x81e542e1a7383f0000" + }, + "f16de1891d8196461395f9b136265b3b9546f6ef": { + "balance": "0x1b28e1f98bbce8000" + }, + "f17a92e0361dbacecdc5de0d1894955af6a9b606": { + "balance": "0x6c6b935b8bbd400000" + }, + "f17adb740f45cbbde3094e7e13716f8103f563bd": { + "balance": "0x6c6b935b8bbd400000" + }, + "f18b14cbf6694336d0fe12ac1f25df2da0c05dbb": { + "balance": "0xd8d4602c26bf6c0000" + }, + "f19b39389d47b11b8a2c3f1da9124decffbefaf7": { + "balance": "0x6c6b935b8bbd400000" + }, + "f19f193508393e4d2a127b20b2031f39c82581c6": { + "balance": "0xbdbd7a83bd2f6c0000" + }, + "f1a1f320407964fd3c8f2e2cc8a4580da94f01ea": { + "balance": "0x6c6c2177557c440000" + }, + "f1b4ecc63525f7432c3d834ffe2b970fbeb87212": { + "balance": "0xa2a24068facd800000" + }, + "f1b58faffa8794f50af8e88309c7a6265455d51a": { + "balance": "0x36330322d5238c0000" + }, + "f1c8c4a941b4628c0d6c30fda56452d99c7e1b64": { + "balance": "0x4e8cea1ede75040000" + }, + "f1da40736f99d5df3b068a5d745fafc6463fc9b1": { + "balance": "0x696ca23058da10000" + }, + "f1dc8ac81042c67a9c3c6792b230c46ac016ca10": { + "balance": "0xad78ebc5ac6200000" + }, + "f1df55dcc34a051012b575cb968bc9c458ea09c9": { + "balance": "0xd8d726b7177a800000" + }, + "f1e980c559a1a8e5e50a47f8fffdc773b7e06a54": { + "balance": "0x65ffbcdea04b7480000" + }, + "f1f391ca92808817b755a8b8f4e2ca08d1fd1108": { + "balance": "0x14542ba12a337c00000" + }, + "f1f766b0e46d73fcd4d52e7a72e1b9190cc632b3": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "f2049532fd458a83ca1bff2eebacb6d5ca63f4a4": { + "balance": "0xc48c991dc1545c8000" + }, + "f206d328e471d0117b246d2a4619827709e96df3": { + "balance": "0xa2af3dc00543440000" + }, + "f20c9a99b74759d782f25c1ceca802a27e0b436c": { + "balance": "0x5a87e7d7f5f6580000" + }, + "f2127d54188fedef0f338a5f38c7ff73ad9f6f42": { + "balance": "0x43c33c1937564800000" + }, + "f2133431d1d9a37ba2f0762bc40c5acc8aa6978e": { + "balance": "0x6c6b935b8bbd400000" + }, + "f21549bdd1487912f900a7523db5f7626121bba3": { + "balance": "0x21e19e0c9bab2400000" + }, + "f218bd848ee7f9d38bfdd1c4eb2ed2496ae4305f": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f224eb900b37b4490eee6a0b6420d85c947d8733": { + "balance": "0x34957444b840e80000" + }, + "f2294adbb6f0dcc76e632ebef48ab49f124dbba4": { + "balance": "0x4e43393600a7b10000" + }, + "f22f4078febbbaa8b0e78e642c8a42f35d433905": { + "balance": "0x6c6acc67d7b1d40000" + }, + "f237ef05261c34d79cc22b860de0f17f793c3860": { + "balance": "0xad78ebc5ac6200000" + }, + "f23c7b0cb8cd59b82bd890644a57daf40c85e278": { + "balance": "0x2b66aafe326ff0000" + }, + "f23d01589eb12d439f7448ff54307529f191858d": { + "balance": "0x6c6b935b8bbd400000" + }, + "f23e5c633221a8f7363e65870c9f287424d2a960": { + "balance": "0x4acf58e07257100000" + }, + "f242da845d42d4bf779a00f295b40750fe49ea13": { + "balance": "0x3635c9adc5dea00000" + }, + "f25259a5c939cd25966c9b6303d3731c53ddbc4c": { + "balance": "0xad78ebc5ac6200000" + }, + "f25e4c70bc465632c89e5625a832a7722f6bffab": { + "balance": "0xf34b82fd8e91200000" + }, + "f26bcedce3feadcea3bc3e96eb1040dfd8ffe1a0": { + "balance": "0x2a034919dfbfbc0000" + }, + "f270792576f05d514493ffd1f5e84bec4b2df810": { + "balance": "0x3635c9adc5dea00000" + }, + "f2732cf2c13b8bb8e7492a988f5f89e38273ddc8": { + "balance": "0x2086ac351052600000" + }, + "f2742e6859c569d5f2108351e0bf4dca352a48a8": { + "balance": "0x21e19e0c9bab2400000" + }, + "f2813a64c5265d020235cb9c319b6c96f906c41e": { + "balance": "0x12f939c99edab80000" + }, + "f287ff52f461117adb3e1daa71932d1493c65f2e": { + "balance": "0xc55325ca7415e00000" + }, + "f2ab1161750244d0ecd048ee0d3e51abb143a2fd": { + "balance": "0x42fe2b907373bc0000" + }, + "f2b4ab2c9427a9015ef6eefff5edb60139b719d1": { + "balance": "0x26db992a3b18000000" + }, + "f2c03e2a38998c21648760f1e5ae7ea3077d8522": { + "balance": "0x8f3f7193ab079c0000" + }, + "f2c2904e9fa664a11ee25656d8fd2cc0d9a522a0": { + "balance": "0xb98bc829a6f90000" + }, + "f2c362b0ef991bc82fb36e66ff75932ae8dd8225": { + "balance": "0x402f4cfee62e80000" + }, + "f2d0e986d814ea13c8f466a0538c53dc922651f0": { + "balance": "0x4acf58e07257100000" + }, + "f2d1b7357724ec4c03185b879b63f57e26589153": { + "balance": "0x14542ba12a337c00000" + }, + "f2d5763ce073127e2cedde6faba786c73ca94141": { + "balance": "0x1ac4286100191f00000" + }, + "f2d59c8923759073d6f415aaf8eb065ff2f3b685": { + "balance": "0x1ab2cf7c9f87e200000" + }, + "f2e99f5cbb836b7ad36247571a302cbe4b481c69": { + "balance": "0x6acb3df27e1f880000" + }, + "f2ed3e77254acb83231dc0860e1a11242ba627db": { + "balance": "0x6b56051582a9700000" + }, + "f2edde37f9a8c39ddea24d79f4015757d06bf786": { + "balance": "0x152d02c7e14af6800000" + }, + "f2efe96560c9d97b72bd36447843885c1d90c231": { + "balance": "0x6c6b935b8bbd400000" + }, + "f2fbb6d887f8b8cc3a869aba847f3d1f643c53d6": { + "balance": "0xd8c9460063d31c0000" + }, + "f3034367f87d24d3077fa9a2e38a8b0ccb1104ef": { + "balance": "0x3635c9adc5dea00000" + }, + "f303d5a816affd97e83d9e4dac2f79072bb0098f": { + "balance": "0x340aad21b3b7000000" + }, + "f3159866c2bc86bba40f9d73bb99f1eee57bb9d7": { + "balance": "0x3635c9adc5dea00000" + }, + "f316ef1df2ff4d6c1808dba663ec8093697968e0": { + "balance": "0x61464d6cdc80f00000" + }, + "f32d25eb0ea2b8b3028a4c7a155dc1aae865784d": { + "balance": "0x13593a9297fdad60000" + }, + "f332c0f3e05a27d9126fd0b641a8c2d4060608fd": { + "balance": "0x10f1b62c4d9644e8000" + }, + "f338459f32a159b23db30ac335769ab2351aa63c": { + "balance": "0x65a4da25d3016c00000" + }, + "f33efc6397aa65fb53a8f07a0f893aae30e8bcee": { + "balance": "0x7cf2381f619f150000" + }, + "f34083ecea385017aa40bdd35ef7effb4ce7762d": { + "balance": "0x15af1d78b58c400000" + }, + "f346d7de92741c08fc58a64db55b062dde012d14": { + "balance": "0xfff6b1f761e6d0000" + }, + "f355d3ec0cfb907d8dbb1bf3464e458128190bac": { + "balance": "0x10b046e7f0d80100000" + }, + "f36df02fbd89607347afce2969b9c4236a58a506": { + "balance": "0x6c6b935b8bbd400000" + }, + "f373e9daac0c8675f53b797a160f6fc034ae6b23": { + "balance": "0x56bc75e2d63100000" + }, + "f37b426547a1642d8033324814f0ede3114fc212": { + "balance": "0x15be6174e1912e0000" + }, + "f37bf78c5875154711cb640d37ea6d28cfcb1259": { + "balance": "0xad78ebc5ac6200000" + }, + "f382df583155d8548f3f93440cd5f68cb79d6026": { + "balance": "0x38757d027fc1fd5c0000" + }, + "f382e4c20410b951089e19ba96a2fee3d91cce7e": { + "balance": "0x111fa56eec2a8380000" + }, + "f38a6ca80168537e974d14e1c3d13990a44c2c1b": { + "balance": "0x14542ba12a337c00000" + }, + "f39a9d7aa3581df07ee4279ae6c312ef21033658": { + "balance": "0xd8d726b7177a800000" + }, + "f3b668b3f14d920ebc379092db98031b67b219b3": { + "balance": "0xad6eedd17cf3b8000" + }, + "f3be99b9103ce7550aa74ff1db18e09dfe32e005": { + "balance": "0x6c6b935b8bbd400000" + }, + "f3c1abd29dc57b41dc192d0e384d021df0b4f6d4": { + "balance": "0x97ae0cdf8f86f80000" + }, + "f3c4716d1ee5279a86d0163a14618181e16136c7": { + "balance": "0x3635c9adc5dea00000" + }, + "f3cc8bcb559465f81bfe583bd7ab0a2306453b9e": { + "balance": "0x43c33c1937564800000" + }, + "f3d688f06bbdbf50f9932c4145cbe48ecdf68904": { + "balance": "0x1158e460913d00000" + }, + "f3dbcf135acb9dee1a489c593c024f03c2bbaece": { + "balance": "0x6c6b935b8bbd400000" + }, + "f3de5f26ef6aded6f06d3b911346ee70401da4a0": { + "balance": "0x133ab37d9f9d030000" + }, + "f3df63a97199933330383b3ed7570b96c4812334": { + "balance": "0x6c6b935b8bbd400000" + }, + "f3e74f470c7d3a3f0033780f76a89f3ef691e6cb": { + "balance": "0xa3cfe631d143640000" + }, + "f3eb1948b951e22df1617829bf3b8d8680ec6b68": { + "balance": "0xd8d726b7177a800000" + }, + "f3f1fa3918ca34e2cf7e84670b1f4d8eca160db3": { + "balance": "0x24dce54d34a1a00000" + }, + "f3f24fc29e20403fc0e8f5ebbb553426f78270a2": { + "balance": "0x56bc75e2d63100000" + }, + "f3fa723552a5d0512e2b62f48dca7b2b8105305b": { + "balance": "0x76d41c62494840000" + }, + "f3fe51fde34413c73318b9c85437fe7e820f561a": { + "balance": "0x3662325cd18fe00000" + }, + "f400f93d5f5c7e3fc303129ac8fb0c2f786407fa": { + "balance": "0x6c6b935b8bbd400000" + }, + "f40b134fea22c6b29c8457f49f000f9cda789adb": { + "balance": "0x2086ac351052600000" + }, + "f41557dfdfb1a1bdcefefe2eba1e21fe0a4a9942": { + "balance": "0x6acb3df27e1f880000" + }, + "f4177a0d85d48b0e264211ce2aa2efd3f1b47f08": { + "balance": "0xc2ccca26b7e80e8000" + }, + "f42f905231c770f0a406f2b768877fb49eee0f21": { + "balance": "0xaadec983fcff40000" + }, + "f432b9dbaf11bdbd73b6519fc0a904198771aac6": { + "balance": "0x83d6c7aab63600000" + }, + "f43da3a4e3f5fab104ca9bc1a0f7f3bb4a56f351": { + "balance": "0x6c6acc67d7b1d40000" + }, + "f447108b98df64b57e871033885c1ad71db1a3f9": { + "balance": "0x176f49ead3483508000" + }, + "f44f8551ace933720712c5c491cdb6f2f951736c": { + "balance": "0xd8d726b7177a800000" + }, + "f456055a11ab91ff668e2ec922961f2a23e3db25": { + "balance": "0xfc936392801c0000" + }, + "f456a75bb99655a7412ce97da081816dfdb2b1f2": { + "balance": "0xad78ebc5ac6200000" + }, + "f45b1dcb2e41dc27ffa024daadf619c11175c087": { + "balance": "0x11164759ffb320000" + }, + "f463a90cb3f13e1f0643423636beab84c123b06d": { + "balance": "0x22b1c8c1227a00000" + }, + "f468906e7edf664ab0d8be3d83eb7ab3f7ffdc78": { + "balance": "0x5c283d410394100000" + }, + "f46980e3a4a9d29a6a6e90604537a3114bcb2897": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f46b6b9c7cb552829c1d3dfd8ffb11aabae782f6": { + "balance": "0x1236efcbcbb340000" + }, + "f476e1267f86247cc908816f2e7ad5388c952db0": { + "balance": "0xd8d726b7177a800000" + }, + "f476f2cb7208a32e051fd94ea8662992638287a2": { + "balance": "0x56bc75e2d63100000" + }, + "f47bb134da30a812d003af8dccb888f44bbf5724": { + "balance": "0x11959b7fe3395580000" + }, + "f483f607a21fcc28100a018c568ffbe140380410": { + "balance": "0x3635c9adc5dea00000" + }, + "f48e1f13f6af4d84b371d7de4b273d03a263278e": { + "balance": "0x2086ac351052600000" + }, + "f49c47b3efd86b6e6a5bc9418d1f9fec814b69ef": { + "balance": "0x43c33c1937564800000" + }, + "f49f6f9baabc018c8f8e119e0115f491fc92a8a4": { + "balance": "0x21e19e0c9bab2400000" + }, + "f4a367b166d2991a2bfda9f56463a09f252c1b1d": { + "balance": "0x6acb3df27e1f880000" + }, + "f4a51fce4a1d5b94b0718389ba4e7814139ca738": { + "balance": "0x1043561a8829300000" + }, + "f4a9d00cefa97b7a58ef9417fc6267a5069039ee": { + "balance": "0x12e89287fa7840000" + }, + "f4aaa3a6163e3706577b49c0767e948a681e16ee": { + "balance": "0x6c6b935b8bbd400000" + }, + "f4b1626e24f30bcad9273c527fcc714b5d007b8f": { + "balance": "0xad78ebc5ac6200000" + }, + "f4b49100757772f33c177b9a76ba95226c8f3dd8": { + "balance": "0x16b352da5e0ed300000" + }, + "f4b6cdcfcb24230b337d770df6034dfbd4e1503f": { + "balance": "0x405fdf7e5af85e00000" + }, + "f4b759cc8a1c75f80849ebbcda878dc8f0d66de4": { + "balance": "0x15af1d78b58c400000" + }, + "f4ba6a46d55140c439cbcf076cc657136262f4f8": { + "balance": "0x6c6b935b8bbd400000" + }, + "f4d67a9044b435b66e8977ff39a28dc4bd53729a": { + "balance": "0xad78ebc5ac6200000" + }, + "f4d97664cc4eec9edbe7fa09f4750a663b507d79": { + "balance": "0xd8d726b7177a800000" + }, + "f4dc7ba85480bbb3f535c09568aaa3af6f3721c6": { + "balance": "0x1871fb6307e35e50000" + }, + "f4ebf50bc7e54f82e9b9bd24baef29438e259ce6": { + "balance": "0x21e19e0c9bab2400000" + }, + "f4ec8e97a20aa5f8dd206f55207e06b813df2cc0": { + "balance": "0xad78ebc5ac6200000" + }, + "f4ed848ec961739c2c7e352f435ba70a7cd5db38": { + "balance": "0x6acb3df27e1f880000" + }, + "f4fc4d39bc0c2c4068a36de50e4ab4d4db7e340a": { + "balance": "0x16037df87ef6a0000" + }, + "f504943aaf16796e0b341bbcdf21d11cc586cdd1": { + "balance": "0x1e7e4171bf4d3a00000" + }, + "f5061ee2e5ee26b815503677130e1de07a52db07": { + "balance": "0x56bc75e2d63100000" + }, + "f509557e90183fbf0f0651a786487bcc428ba175": { + "balance": "0xa844a7424d9c80000" + }, + "f50abbd4aa45d3eb88515465a8ba0b310fd9b521": { + "balance": "0x16a6502f15a1e540000" + }, + "f50ae7fab4cfb5a646ee04ceadf9bf9dd5a8e540": { + "balance": "0xd8d67c2f5895480000" + }, + "f50cbafd397edd556c0678988cb2af5c2617e0a2": { + "balance": "0x26d07efe782bb00000" + }, + "f51fded80acb502890e87369741f3722514cefff": { + "balance": "0x43c3456ca3c6d110000" + }, + "f52a5882e8927d944b359b26366ba2b9cacfbae8": { + "balance": "0x54b41ce2fe63ba80000" + }, + "f52c0a7877345fe0c233bb0f04fd6ab18b6f14ba": { + "balance": "0x54cbe55989f38de00000" + }, + "f5437e158090b2a2d68f82b54a5864b95dd6dbea": { + "balance": "0xd96c16703b2bfe0000" + }, + "f54c19d9ef3873bfd1f7a622d02d86249a328f06": { + "balance": "0x960ae127af32fb28000" + }, + "f5500178cb998f126417831a08c2d7abfff6ab5f": { + "balance": "0x46f4f4a5875a9f8000" + }, + "f5534815dc635efa5cc84b2ac734723e21b29372": { + "balance": "0x55a6e79ccd1d300000" + }, + "f555a27bb1e2fd4e2cc784caee92939fc06e2fc9": { + "balance": "0x6c6b935b8bbd400000" + }, + "f558a2b2dd26dd9593aae04531fd3c3cc3854b67": { + "balance": "0xabbcd4ef377580000" + }, + "f56048dd2181d4a36f64fcecc6215481e42abc15": { + "balance": "0xad78ebc5ac6200000" + }, + "f56442f60e21691395d0bffaa9194dcaff12e2b7": { + "balance": "0xe18398e7601900000" + }, + "f579714a45eb8f52c3d57bbdefd2c15b2e2f11df": { + "balance": "0x54915956c409600000" + }, + "f593c65285ee6bbd6637f3be8f89ad40d489f655": { + "balance": "0xa2a15d09519be00000" + }, + "f598db2e09a8a5ee7d720d2b5c43bb126d11ecc2": { + "balance": "0xad78ebc5ac6200000" + }, + "f59dab1bf8df11327e61f9b7a14b563a96ec3554": { + "balance": "0x14542ba12a337c00000" + }, + "f59f9f02bbc98efe097eabb78210979021898bfd": { + "balance": "0x21e171a3ec9f72c0000" + }, + "f5a5459fcdd5e5b273830df88eea4cb77ddadfb9": { + "balance": "0x409e52b48369a0000" + }, + "f5a7676ad148ae9c1ef8b6f5e5a0c2c473be850b": { + "balance": "0xad78ebc5ac6200000" + }, + "f5b068989df29c253577d0405ade6e0e7528f89e": { + "balance": "0x57473d05dabae80000" + }, + "f5b6e9061a4eb096160777e26762cf48bdd8b55d": { + "balance": "0xdc55fdb17647b0000" + }, + "f5cffbba624e7eb321bc83c60ca68199b4e36671": { + "balance": "0x6c6b935b8bbd400000" + }, + "f5d14552b1dce0d6dc1f320da6ffc8a331cd6f0c": { + "balance": "0x487a9a304539440000" + }, + "f5d61ac4ca95475e5b7bffd5f2f690b316759615": { + "balance": "0x692ae8897081d000000" + }, + "f5d9cf00d658dd45517a48a9d3f5f633541a533d": { + "balance": "0x64f5fdf494f780000" + }, + "f5eadcd2d1b8657a121f33c458a8b13e76b65526": { + "balance": "0xd8b0f5a5ac24a0000" + }, + "f607c2150d3e1b99f24fa1c7d540add35c4ebe1e": { + "balance": "0xa7f1aa07fc8faa0000" + }, + "f60bd735543e6bfd2ea6f11bff627340bc035a23": { + "balance": "0x6c6b935b8bbd400000" + }, + "f60c1b45f164b9580e20275a5c39e1d71e35f891": { + "balance": "0x6c6b935b8bbd400000" + }, + "f60f62d73937953fef35169e11d872d2ea317eec": { + "balance": "0x121ea68c114e5100000" + }, + "f61283b4bd8504058ca360e993999b62cbc8cd67": { + "balance": "0xdd2d5fcf3bc9c0000" + }, + "f617b967b9bd485f7695d2ef51fb7792d898f500": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f618d9b104411480a863e623fc55232d1a4f48aa": { + "balance": "0xe689e6d44b1668000" + }, + "f622e584a6623eaaf99f2be49e5380c5cbcf5cd8": { + "balance": "0xad78ebc5ac6200000" + }, + "f632adff490da4b72d1236d04b510f74d2faa3cd": { + "balance": "0x4be4e7267b6ae00000" + }, + "f639ac31da9f67271bd10402b7654e5ce763bd47": { + "balance": "0x15af0f42baf9260000" + }, + "f63a579bc3eac2a9490410128dbcebe6d9de8243": { + "balance": "0x50c5e761a444080000" + }, + "f645dd7c890093e8e4c8aa92a6bb353522d3dc98": { + "balance": "0x7439fa2099e580000" + }, + "f648ea89c27525710172944e79edff847803b775": { + "balance": "0x152d02c7e14af6800000" + }, + "f64a4ac8d540a9289c68d960d5fb7cc45a77831c": { + "balance": "0x6c6b935b8bbd400000" + }, + "f64ecf2117931c6d535a311e4ffeaef9d49405b8": { + "balance": "0x90f534608a72880000" + }, + "f64fe0939a8d1eea2a0ecd9a9730fd7958e33109": { + "balance": "0x11de1e6db450c0000" + }, + "f65616be9c8b797e7415227c9138faa0891742d7": { + "balance": "0x2ad373ce668e980000" + }, + "f657fcbe682eb4e8db152ecf892456000b513d15": { + "balance": "0x692ae8897081d00000" + }, + "f65819ac4cc14c137f05dd7977c7dae08d1a4ab5": { + "balance": "0x58788cb94b1d80000" + }, + "f67bb8e2118bbcd59027666eedf6943ec9f880a5": { + "balance": "0xd8d726b7177a800000" + }, + "f68464bf64f2411356e4d3250efefe5c50a5f65b": { + "balance": "0x1158e460913d00000" + }, + "f686785b89720b61145fea80978d6acc8e0bc196": { + "balance": "0xd8d726b7177a800000" + }, + "f68c5e33fa97139df5b2e63886ce34ebf3e4979c": { + "balance": "0xb3fa4169e2d8e00000" + }, + "f6a8635757c5e8c134d20d028cf778cf8609e46a": { + "balance": "0x4f1d772faec17c0000" + }, + "f6b782f4dcd745a6c0e2e030600e04a24b25e542": { + "balance": "0x15af1d78b58c400000" + }, + "f6bc37b1d2a3788d589b6de212dc1713b2f6e78e": { + "balance": "0x10f0cf064dd59200000" + }, + "f6c3c48a1ac0a34799f04db86ec7a975fe7768f3": { + "balance": "0x6acb3df27e1f880000" + }, + "f6d25d3f3d846d239f525fa8cac97bc43578dbac": { + "balance": "0x30927f74c9de000000" + }, + "f6eaac7032d492ef17fd6095afc11d634f56b382": { + "balance": "0x1b1b6bd7af64c70000" + }, + "f6ead67dbf5b7eb13358e10f36189d53e643cfcf": { + "balance": "0x878678326eac9000000" + }, + "f6f1a44309051c6b25e47dff909b179bb9ab591c": { + "balance": "0x692ae8897081d00000" + }, + "f70328ef97625fe745faa49ee0f9d4aa3b0dfb69": { + "balance": "0x3635c9adc5dea00000" + }, + "f70a998a717b338d1dd99854409b1a338deea4b0": { + "balance": "0x6c6b935b8bbd400000" + }, + "f70d637a845c06db6cdc91e6371ce7c4388a628e": { + "balance": "0x1158e460913d00000" + }, + "f7155213449892744bc60f2e04400788bd041fdd": { + "balance": "0x39fbae8d042dd0000" + }, + "f71b4534f286e43093b1e15efea749e7597b8b57": { + "balance": "0x161c13d3341c87280000" + }, + "f734ec03724ddee5bb5279aa1afcf61b0cb448a1": { + "balance": "0xe5bf2cc9b097800000" + }, + "f736dc96760012388fe88b66c06efe57e0d7cf0a": { + "balance": "0x71d75ab9b920500000" + }, + "f73ac46c203be1538111b151ec8220c786d84144": { + "balance": "0xff7377817b82b8000" + }, + "f73dd9c142b71bce11d06e30e7e7d032f2ec9c9e": { + "balance": "0x6acb3df27e1f880000" + }, + "f7418aa0e713d248228776b2e7434222ae75e3a5": { + "balance": "0x6c6b935b8bbd400000" + }, + "f74e6e145382b4db821fe0f2d98388f45609c69f": { + "balance": "0x56bc75e2d63100000" + }, + "f7500c166f8bea2f82347606e5024be9e4f4ce99": { + "balance": "0x1158e460913d00000" + }, + "f757fc8720d3c4fa5277075e60bd5c411aebd977": { + "balance": "0x6c6b935b8bbd400000" + }, + "f75bb39c799779ebc04a336d260da63146ed98d0": { + "balance": "0x15af1d78b58c40000" + }, + "f768f321fd6433d96b4f354d3cc1652c1732f57f": { + "balance": "0x21e19e0c9bab2400000" + }, + "f76f69cee4faa0a63b30ae1e7881f4f715657010": { + "balance": "0xad78ebc5ac6200000" + }, + "f777361a3dd8ab62e5f1b9b047568cc0b555704c": { + "balance": "0x3635c9adc5dea00000" + }, + "f77c7b845149efba19e261bc7c75157908afa990": { + "balance": "0x6c6b935b8bbd400000" + }, + "f77f9587ff7a2d7295f1f571c886bd33926a527c": { + "balance": "0x6c68ccd09b022c0000" + }, + "f78258c12481bcdddbb72a8ca0c043097261c6c5": { + "balance": "0x1158e460913d00000" + }, + "f798d16da4e460c460cd485fae0fa0599708eb82": { + "balance": "0x3635c9adc5dea00000" + }, + "f7a1ade2d0f529123d1055f19b17919f56214e67": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f7acff934b84da0969dc37a8fcf643b7d7fbed41": { + "balance": "0x6c6acc67d7b1d40000" + }, + "f7b151cc5e571c17c76539dbe9964cbb6fe5de79": { + "balance": "0x74717cfb6883100000" + }, + "f7b29b82195c882dab7897c2ae95e77710f57875": { + "balance": "0x7735416132dbfc0000" + }, + "f7bc4c44910d5aedd66ed2355538a6b193c361ec": { + "balance": "0x541de2c2d8d620000" + }, + "f7c00cdb1f020310d5acab7b496aaa44b779085e": { + "balance": "0x5a87e7d7f5f6580000" + }, + "f7c1b443968b117b5dd9b755572fcd39ca5ec04b": { + "balance": "0x18b968c292f1b50000" + }, + "f7c50f922ad16b61c6d1baa045ed816815bac48f": { + "balance": "0x2a9396a9784ad7d0000" + }, + "f7c708015071d4fb0a3a2a09a45d156396e3349e": { + "balance": "0xa2a15d09519be00000" + }, + "f7cbdba6be6cfe68dbc23c2b0ff530ee05226f84": { + "balance": "0x1158e460913d00000" + }, + "f7d0d310acea18406138baaabbfe0571e80de85f": { + "balance": "0x487a9a304539440000" + }, + "f7d7af204c56f31fd94398e40df1964bd8bf123c": { + "balance": "0x821d221b5291f8000" + }, + "f7dc251196fbcbb77c947d7c1946b0ff65021cea": { + "balance": "0x3635c9adc5dea00000" + }, + "f7e45a12aa711c709acefe95f33b78612d2ad22a": { + "balance": "0xe0655e2f26bc9180000" + }, + "f7f4898c4c526d955f21f055cb6e47b915e51964": { + "balance": "0x7c0860e5a80dc00000" + }, + "f7f91e7acb5b8129a306877ce3168e6f438b66a1": { + "balance": "0x98a7d9b8314c00000" + }, + "f7fc45abf76f5088e2e5b5a8d132f28a4d4ec1c0": { + "balance": "0x6c6b935b8bbd400000" + }, + "f8063af4cc1dd9619ab5d8bff3fcd1faa8488221": { + "balance": "0x6c6b935b8bbd400000" + }, + "f8086e42661ea929d2dda1ab6c748ce3055d111e": { + "balance": "0x3635c9adc5dea00000" + }, + "f8087786b42da04ed6d1e0fe26f6c0eefe1e9f5a": { + "balance": "0x21e19e0c9bab2400000" + }, + "f80d3619702fa5838c48391859a839fb9ce7160f": { + "balance": "0x6c07a7d1b16e700000" + }, + "f814799f6ddf4dcb29c7ee870e75f9cc2d35326d": { + "balance": "0x3635c9adc5dea00000" + }, + "f815c10a032d13c34b8976fa6e3bd2c9131a8ba9": { + "balance": "0x487a9a304539440000" + }, + "f81622e55757daea6675975dd93538da7d16991e": { + "balance": "0x6c6b935b8bbd400000" + }, + "f824ee331e4ac3cc587693395b57ecf625a6c0c2": { + "balance": "0x56c95de8e8ca1d0000" + }, + "f827d56ed2d32720d4abf103d6d0ef4d3bcd559b": { + "balance": "0x16c80065791a28000" + }, + "f8298591523e50b103f0b701d623cbf0f74556f6": { + "balance": "0xad78ebc5ac6200000" + }, + "f848fce9ab611c7d99206e23fac69ad488b94fe1": { + "balance": "0x2a1129d0936720000" + }, + "f84f090adf3f8db7e194b350fbb77500699f66fd": { + "balance": "0x6acb3df27e1f880000" + }, + "f851b010f633c40af1a8f06a73ebbaab65077ab5": { + "balance": "0xee86442fcd06c00000" + }, + "f858171a04d357a13b4941c16e7e55ddd4941329": { + "balance": "0x246a5218f2a000000" + }, + "f85bab1cb3710fc05fa19ffac22e67521a0ba21d": { + "balance": "0x6c95357fa6b36c0000" + }, + "f86a3ea8071f7095c7db8a05ae507a8929dbb876": { + "balance": "0x1236efcbcbb3400000" + }, + "f8704c16d2fd5ba3a2c01d0eb20484e6ecfa3109": { + "balance": "0xad78ebc5ac6200000" + }, + "f870995fe1e522321d754337a45c0c9d7b38951c": { + "balance": "0x1158e460913d00000" + }, + "f873e57a65c93b6e18cb75f0dc077d5b8933dc5c": { + "balance": "0xaadec983fcff40000" + }, + "f875619d8a23e45d8998d184d480c0748970822a": { + "balance": "0xd8d726b7177a800000" + }, + "f87bb07b289df7301e54c0efda6a2cf291e89200": { + "balance": "0x4be4e7267b6ae00000" + }, + "f88900db737955b1519b1a7d170a18864ce590eb": { + "balance": "0xfc936392801c0000" + }, + "f88b58db37420b464c0be88b45ee2b95290f8cfa": { + "balance": "0x22b1c8c1227a00000" + }, + "f8962b75db5d24c7e8b7cef1068c3e67cebb30a5": { + "balance": "0xf2dc7d47f15600000" + }, + "f8a065f287d91d77cd626af38ffa220d9b552a2b": { + "balance": "0x678a932062e4180000" + }, + "f8a49ca2390c1f6d5c0e62513b079571743f7cc6": { + "balance": "0xa2a15d09519be00000" + }, + "f8a50cee2e688ceee3aca4d4a29725d4072cc483": { + "balance": "0x6c6b935b8bbd400000" + }, + "f8ac4a39b53c11307820973b441365cffe596f66": { + "balance": "0x6c6b935b8bbd400000" + }, + "f8ae857b67a4a2893a3fbe7c7a87ff1c01c6a6e7": { + "balance": "0xd8d726b7177a800000" + }, + "f8bf9c04874e5a77f38f4c38527e80c676f7b887": { + "balance": "0x6c6b935b8bbd400000" + }, + "f8c7f34a38b31801da43063477b12b27d0f203ff": { + "balance": "0x1ad2baba6fef480000" + }, + "f8ca336c8e91bd20e314c20b2dd4608b9c8b9459": { + "balance": "0x2ddc9bc5b32c780000" + }, + "f8d17424c767bea31205739a2b57a7277214eebe": { + "balance": "0x246ddf97976680000" + }, + "f8d52dcc5f96cc28007b3ecbb409f7e22a646caa": { + "balance": "0x81690e18128480000" + }, + "f8dce867f0a39c5bef9eeba609229efa02678b6c": { + "balance": "0x6c6b935b8bbd400000" + }, + "f8f226142a428434ab17a1864a2597f64aab2f06": { + "balance": "0x9598b2fb2e9f28000" + }, + "f8f6645e0dee644b3dad81d571ef9baf840021ad": { + "balance": "0x6c6b935b8bbd400000" + }, + "f901c00fc1db88b69c4bc3252b5ca70ea6ee5cf6": { + "balance": "0x15af1d78b58c400000" + }, + "f93d5bcb0644b0cce5fcdda343f5168ffab2877d": { + "balance": "0xb6207b67d26f90000" + }, + "f9570e924c95debb7061369792cf2efec2a82d5e": { + "balance": "0x1158e460913d00000" + }, + "f9642086b1fbae61a6804dbe5fb15ec2d2b537f4": { + "balance": "0x6c6b935b8bbd400000" + }, + "f96488698590dc3b2c555642b871348dfa067ad5": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f964d98d281730ba35b2e3a314796e7b42fedf67": { + "balance": "0x53b0876098d80c0000" + }, + "f9650d6989f199ab1cc479636ded30f241021f65": { + "balance": "0x2e141ea081ca080000" + }, + "f96883582459908c827627e86f28e646f9c7fc7a": { + "balance": "0x1c4a78737cdcfb80000" + }, + "f96b4c00766f53736a8574f822e6474c2f21da2d": { + "balance": "0x15af1d78b58c400000" + }, + "f9729d48282c9e87166d5eef2d01eda9dbf78821": { + "balance": "0x56b83ddc728548000" + }, + "f9767e4ecb4a5980527508d7bec3d45e4c649c13": { + "balance": "0x678a932062e4180000" + }, + "f978b025b64233555cc3c19ada7f4199c9348bf7": { + "balance": "0x54b40b1f852bda000000" + }, + "f97b56ebd5b77abc9fbacbabd494b9d2c221cd03": { + "balance": "0x6acb3df27e1f880000" + }, + "f9811fa19dadbf029f8bfe569adb18228c80481a": { + "balance": "0xad78ebc5ac6200000" + }, + "f98250730c4c61c57f129835f2680894794542f3": { + "balance": "0xd8d726b7177a800000" + }, + "f989346772995ec1906faffeba2a7fe7de9c6bab": { + "balance": "0x16a6502f15a1e540000" + }, + "f998ca3411730a6cd10e7455b0410fb0f6d3ff80": { + "balance": "0x6c6b935b8bbd400000" + }, + "f99aee444b5783c093cfffd1c4632cf93c6f050c": { + "balance": "0x15af1d78b58c400000" + }, + "f99eeece39fa7ef5076d855061384009792cf2e0": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f9a59c3cc5ffacbcb67be0fc7256f64c9b127cb4": { + "balance": "0x6c6b935b8bbd400000" + }, + "f9a94bd56198da245ed01d1e6430b24b2708dcc0": { + "balance": "0x28a77afda87ee50000" + }, + "f9b37825f03073d31e249378c30c795c33f83af2": { + "balance": "0xad9aabf8c9bfc0000" + }, + "f9b617f752edecae3e909fbb911d2f8192f84209": { + "balance": "0x90f534608a72880000" + }, + "f9bfb59d538afc4874d4f5941b08c9730e38e24b": { + "balance": "0x22b1c8c1227a00000" + }, + "f9dd239008182fb519fb30eedd2093fed1639be8": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "f9debaecb5f339beea4894e5204bfa340d067f25": { + "balance": "0x5a42844673b1640000" + }, + "f9e37447406c412197b2e2aebc001d6e30c98c60": { + "balance": "0x1c479bb4349c0ee0000" + }, + "f9e7222faaf0f4da40c1c4a40630373a09bed7b6": { + "balance": "0x9b4fdcb09456240000" + }, + "f9ece022bccd2c92346911e79dd50303c01e0188": { + "balance": "0x3635c9adc5dea00000" + }, + "fa00c376e89c05e887817a9dd0748d96f341aa89": { + "balance": "0x104d0d00d2b7f60000" + }, + "fa0c1a988c8a17ad3528eb28b3409daa58225f26": { + "balance": "0xad78ebc5ac6200000" + }, + "fa105f1a11b6e4b1f56012a27922e2ac2da4812f": { + "balance": "0x205b4dfa1ee74780000" + }, + "fa142fe47eda97e6503b386b18a2bedd73ccb5b1": { + "balance": "0x2e153ad81548100000" + }, + "fa14b566234abee73042c31d21717182cba14aa1": { + "balance": "0x11c7ea162e78200000" + }, + "fa19d6f7a50f4f079893d167bf14e21d0073d196": { + "balance": "0x1cbb3a3ff08d080000" + }, + "fa1f1971a775c3504fef5079f640c2c4bce7ac05": { + "balance": "0x6c6b935b8bbd400000" + }, + "fa279bfd8767f956bf7fa0bd5660168da75686bd": { + "balance": "0x90f534608a72880000" + }, + "fa27cc49d00b6c987336a875ae39da58fb041b2e": { + "balance": "0x21e19e0c9bab2400000" + }, + "fa283299603d8758e8cab082125d2c8f7d445429": { + "balance": "0x15bcacb1e0501ae8000" + }, + "fa2bbca15d3fe39f8a328e91f90da14f7ac6253d": { + "balance": "0xad78ebc5ac6200000" + }, + "fa2fd29d03fee9a07893df3a269f56b72f2e1e64": { + "balance": "0x21e19e0c9bab2400000" + }, + "fa33553285a973719a0d5f956ff861b2d89ed304": { + "balance": "0x1158e460913d00000" + }, + "fa3a0c4b903f6ea52ea7ab7b8863b6a616ad6650": { + "balance": "0x1158e460913d00000" + }, + "fa3a1aa4488b351aa7560cf5ee630a2fd45c3222": { + "balance": "0x2fa47e6aa7340d0000" + }, + "fa410971ad229c3036f41acf852f2ac999281950": { + "balance": "0xd8b311a8ddfa7c0000" + }, + "fa44a855e404c86d0ca8ef3324251dfb349c539e": { + "balance": "0x542253a126ce400000" + }, + "fa5201fe1342af11307b9142a041243ca92e2f09": { + "balance": "0x2038116a3ac043980000" + }, + "fa60868aafd4ff4c5c57914b8ed58b425773dfa9": { + "balance": "0x1cfe5c808f39fbc0000" + }, + "fa67b67b4f37a0150915110ede073b05b853bda2": { + "balance": "0x2319ba947371ad0000" + }, + "fa68e0cb3edf51f0a6f211c9b2cb5e073c9bffe6": { + "balance": "0xfc936392801c00000" + }, + "fa6a37f018e97967937fc5e8617ba1d786dd5f77": { + "balance": "0x43c30fb0884a96c0000" + }, + "fa7606435b356cee257bd2fcd3d9eacb3cd1c4e1": { + "balance": "0x56bc75e2d63100000" + }, + "fa7adf660b8d99ce15933d7c5f072f3cbeb99d33": { + "balance": "0x14061b9d77a5e980000" + }, + "fa86ca27bf2854d98870837fb6f6dfe4bf6453fc": { + "balance": "0x11757e8525cf148000" + }, + "fa8cf4e627698c5d5788abb7880417e750231399": { + "balance": "0xe61a3696eef6100000" + }, + "fa8e3b1f13433900737daaf1f6299c4887f85b5f": { + "balance": "0x26c29e47c4844c0000" + }, + "fa9ec8efe08686fa58c181335872ba698560ecab": { + "balance": "0x6c6acc67d7b1d40000" + }, + "faad905d847c7b23418aeecbe3addb8dd3f8924a": { + "balance": "0x6acb3df27e1f880000" + }, + "faaeba8fc0bbda553ca72e30ef3d732e26e82041": { + "balance": "0x488d282aafc9f68000" + }, + "fab487500df20fb83ebed916791d561772adbebf": { + "balance": "0x6c6b4c4da6ddbe0000" + }, + "fac5ca94758078fbfccd19db3558da7ee8a0a768": { + "balance": "0x3728a62b0dcff60000" + }, + "fad96ab6ac768ad5099452ac4777bd1a47edc48f": { + "balance": "0x56bc75e2d63100000" + }, + "fae76719d97eac41870428e940279d97dd57b2f6": { + "balance": "0x14dbb2195ca228900000" + }, + "fae881937047895a660cf229760f27e66828d643": { + "balance": "0x9ddc1e3b901180000" + }, + "fae92c1370e9e1859a5df83b56d0f586aa3b404c": { + "balance": "0x5c5b4f3d843980000" + }, + "faf5f0b7b6d558f5090d9ea1fb2d42259c586078": { + "balance": "0x15affb8420c6b640000" + }, + "fb126f0ec769f49dcefca2f200286451583084b8": { + "balance": "0x10fcbc2350396bf0000" + }, + "fb135eb15a8bac72b69915342a60bbc06b7e077c": { + "balance": "0x43c33c1937564800000" + }, + "fb223c1e22eac1269b32ee156a5385922ed36fb8": { + "balance": "0x6c6b935b8bbd400000" + }, + "fb37cf6b4f81a9e222fba22e9bd24b5098b733cf": { + "balance": "0x21a754a6dc5280000" + }, + "fb3860f4121c432ebdc8ec6a0331b1b709792e90": { + "balance": "0x208c394af1c8880000" + }, + "fb39189af876e762c71d6c3e741893df226cedd6": { + "balance": "0xd8d726b7177a800000" + }, + "fb3a0b0d6b6a718f6fc0292a825dc9247a90a5d0": { + "balance": "0xad6dd199e975b0000" + }, + "fb3fa1ac08aba9cc3bf0fe9d483820688f65b410": { + "balance": "0x65a4da25d3016c00000" + }, + "fb3fe09bb836861529d7518da27635f538505615": { + "balance": "0x4be39216fda0700000" + }, + "fb5125bf0f5eb0b6f020e56bfc2fdf3d402c097e": { + "balance": "0x14061b9d77a5e980000" + }, + "fb5518714cefc36d04865de5915ef0ff47dfe743": { + "balance": "0x6c6b935b8bbd400000" + }, + "fb5ffaa0f7615726357891475818939d2037cf96": { + "balance": "0x1158e460913d00000" + }, + "fb685c15e439965ef626bf0d834cd1a89f2b5695": { + "balance": "0xd5967be4fc3f100000" + }, + "fb744b951d094b310262c8f986c860df9ab1de65": { + "balance": "0x2d1c515f1cb4a8000" + }, + "fb79abdb925c55b9f98efeef64cfc9eb61f51bb1": { + "balance": "0x6140c056fb0ac80000" + }, + "fb8113f94d9173eefd5a3073f516803a10b286ae": { + "balance": "0x4563918244f400000" + }, + "fb842ca2c5ef133917a236a0d4ac40690110b038": { + "balance": "0x10969a62be15880000" + }, + "fb91fb1a695553f0c68e21276decf0b83909b86d": { + "balance": "0x56c003617af780000" + }, + "fb9473cf7712350a1fa0395273fc80560752e4fb": { + "balance": "0x6af2198ba85aa0000" + }, + "fb949c647fdcfd2514c7d58e31f28a532d8c5833": { + "balance": "0x43c33c1937564800000" + }, + "fba5486d53c6e240494241abf87e43c7600d413a": { + "balance": "0x6bbf61494948340000" + }, + "fbb161fe875f09290a4b262bc60110848f0d2226": { + "balance": "0x6c6b935b8bbd400000" + }, + "fbbbebcfbe235e57dd2306ad1a9ec581c7f9f48f": { + "balance": "0x22b1c8c1227a00000" + }, + "fbc01db54e47cdc3c438694ab717a856c23fe6e9": { + "balance": "0x1ca7150ab174f470000" + }, + "fbcfcc4a7b0f26cf26e9f3332132e2fc6a230766": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "fbe71622bcbd31c1a36976e7e5f670c07ffe16de": { + "balance": "0x15af1d78b58c400000" + }, + "fbede32c349f3300ef4cd33b4de7dc18e443d326": { + "balance": "0xab4dcf399a3a600000" + }, + "fbf204c813f836d83962c7870c7808ca347fd33e": { + "balance": "0x1158e460913d00000" + }, + "fbf75933e01b75b154ef0669076be87f62dffae1": { + "balance": "0x10846372f249d4c00000" + }, + "fc0096b21e95acb8d619d176a4a1d8d529badbef": { + "balance": "0x14d9693bcbec028000" + }, + "fc00a420a36107dfd5f495128a5fe5abb2db0f34": { + "balance": "0x143179d869110200000" + }, + "fc018a690ad6746dbe3acf9712ddca52b6250039": { + "balance": "0x21e19e0c9bab2400000" + }, + "fc02734033e57f70517e0afc7ee62461f06fad8e": { + "balance": "0x155bd9307f9fe80000" + }, + "fc0ee6f7c2b3714ae9916c45566605b656f32441": { + "balance": "0x5f68e8131ecf800000" + }, + "fc10b7a67b3268d5331bfb6a14def5ea4a162ca3": { + "balance": "0xad78ebc5ac6200000" + }, + "fc15cb99a8d1030b12770add033a79ee0d0c908c": { + "balance": "0x12fa00bd52e6240000" + }, + "fc2952b4c49fedd0bc0528a308495e6d6a1c71d6": { + "balance": "0x6c6b935b8bbd400000" + }, + "fc2c1f88961d019c3e9ea33009152e0693fbf88a": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "fc361105dd90f9ede566499d69e9130395f12ac8": { + "balance": "0x53a4fe2f204e80e00000" + }, + "fc372ff6927cb396d9cf29803500110da632bc52": { + "balance": "0x6c6b935b8bbd400000" + }, + "fc39be41094b1997d2169e8264c2c3baa6c99bc4": { + "balance": "0x6c6b935b8bbd400000" + }, + "fc3d226bb36a58f526568857b0bb12d109ec9301": { + "balance": "0x6c6b935b8bbd400000" + }, + "fc43829ac787ff88aaf183ba352aadbf5a15b193": { + "balance": "0xd6ac0a2b0552e00000" + }, + "fc49c1439a41d6b3cf26bb67e0365224e5e38f5f": { + "balance": "0x3636d7af5ec98e0000" + }, + "fc5500825105cf712a318a5e9c3bfc69c89d0c12": { + "balance": "0xd8d726b7177a800000" + }, + "fc66faba277f4b5de64ad45eb19c31e00ced3ed5": { + "balance": "0x131beb925ffd3200000" + }, + "fc7e22a503ec5abe9b08c50bd14999f520fa4884": { + "balance": "0x15a477dfbe1ea148000" + }, + "fc8215a0a69913f62a43bf1c8590b9ddcd0d8ddb": { + "balance": "0x6c6b935b8bbd400000" + }, + "fc989cb487bf1a7d17e4c1b7c4b7aafdda6b0a8d": { + "balance": "0x1158e460913d00000" + }, + "fc9b347464b2f9929d807e039dae48d3d98de379": { + "balance": "0x2f6f10780d22cc00000" + }, + "fca43bbc23a0d321ba9e46b929735ce7d8ef0c18": { + "balance": "0x1158e460913d00000" + }, + "fca73eff8771c0103ba3cc1a9c259448c72abf0b": { + "balance": "0x3635c9adc5dea00000" + }, + "fcada300283f6bcc134a91456760b0d77de410e0": { + "balance": "0x6c6b935b8bbd400000" + }, + "fcbc5c71ace79741450b012cf6b8d3f17db68a70": { + "balance": "0x205b4dfa1ee74780000" + }, + "fcbd85feea6a754fcf3449449e37ff9784f7773c": { + "balance": "0xa74ada69abd7780000" + }, + "fcc9d4a4262e7a027ab7519110d802c495ceea39": { + "balance": "0x1595182224b26480000" + }, + "fccd0d1ecee27addea95f6857aeec8c7a04b28ee": { + "balance": "0x21e19e0c9bab2400000" + }, + "fcd0b4827cd208ffbf5e759dba8c3cc61d8c2c3c": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "fce089635ce97abac06b44819be5bb0a3e2e0b37": { + "balance": "0x503920a7630a78000" + }, + "fcf199f8b854222f182e4e1d099d4e323e2aae01": { + "balance": "0x3635c9adc5dea00000" + }, + "fcfc3a5004d678613f0b36a642269a7f371c3f6a": { + "balance": "0x3635c9adc5dea00000" + }, + "fd191a35157d781373fb411bf9f25290047c5eef": { + "balance": "0x3635c9adc5dea00000" + }, + "fd1faa347b0fcc804c2da86c36d5f1d18b7087bb": { + "balance": "0x2d6eb247a96f60000" + }, + "fd1fb5a89a89a721b8797068fbc47f3e9d52e149": { + "balance": "0xcd0b5837fc6580000" + }, + "fd204f4f4aba2525ba728afdf78792cbdeb735ae": { + "balance": "0x6c6b935b8bbd400000" + }, + "fd2757cc3551a095878d97875615fe0c6a32aa8a": { + "balance": "0x206db15299beac0000" + }, + "fd2872d19e57853cfa16effe93d0b1d47b4f93fb": { + "balance": "0xd8d726b7177a800000" + }, + "fd2929271e9d2095a264767e7b0df52ea0d1d400": { + "balance": "0xa2a1eb251b5ae40000" + }, + "fd377a385272900cb436a3bb7962cdffe93f5dad": { + "balance": "0x6c6b935b8bbd400000" + }, + "fd40242bb34a70855ef0fd90f3802dec2136b327": { + "balance": "0x68a875073e29240000" + }, + "fd452c3969ece3801c542020f1cdcaa1c71ed23d": { + "balance": "0x152d02c7e14af6800000" + }, + "fd4b551f6fdbcda6c511b5bb372250a6b783e534": { + "balance": "0x11de1e6db450c0000" + }, + "fd4b989558ae11be0c3b36e2d6f2a54a9343ca2e": { + "balance": "0x6c6b935b8bbd400000" + }, + "fd4de8e3748a289cf7d060517d9d38388db01fb8": { + "balance": "0xd8d726b7177a80000" + }, + "fd5a63157f914fd398eab19c137dd9550bb7715c": { + "balance": "0x56bc75e2d63100000" + }, + "fd60d2b5af3d35f7aaf0c393052e79c4d823d985": { + "balance": "0x30eb50d2e14080000" + }, + "fd686de53fa97f99639e2568549720bc588c9efc": { + "balance": "0x6ac5c62d9486070000" + }, + "fd7ede8f5240a06541eb699d782c2f9afb2170f6": { + "balance": "0x487a9a304539440000" + }, + "fd812bc69fb170ef57e2327e80affd14f8e4b6d2": { + "balance": "0x6c6b935b8bbd400000" + }, + "fd88d114220f081cb3d5e15be8152ab07366576a": { + "balance": "0x1043561a8829300000" + }, + "fd918536a8efa6f6cefe1fa1153995fef5e33d3b": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "fd920f722682afb5af451b0544d4f41b3b9d5742": { + "balance": "0x7e52056a123f3c0000" + }, + "fd9579f119bbc819a02b61e38d8803c942f24d32": { + "balance": "0x5b97e9081d9400000" + }, + "fda0ce15330707f10bce3201172d2018b9ddea74": { + "balance": "0x2d041d705a2c60000" + }, + "fda3042819af3e662900e1b92b4358eda6e92590": { + "balance": "0x1907a284d58f63e00000" + }, + "fda6810ea5ac985d6ffbf1c511f1c142edcfddf7": { + "balance": "0xd8d726b7177a800000" + }, + "fdb33944f2360615e5be239577c8a19ba52d9887": { + "balance": "0x209d922f5259c50000" + }, + "fdba5359f7ec3bc770ac49975d844ec9716256f1": { + "balance": "0x3635c9adc5dea00000" + }, + "fdc4d4765a942f5bf96931a9e8cc7ab8b757ff4c": { + "balance": "0x126c478a0e3ea8600000" + }, + "fdcd5d80b105897a57abc47865768b2900524295": { + "balance": "0x15af1d78b58c4000000" + }, + "fdd1195f797d4f35717d15e6f9810a9a3ff55460": { + "balance": "0xfc936392801c0000" + }, + "fdd502a74e813bcfa355ceda3c176f6a6871af7f": { + "balance": "0x15af1d78b58c400000" + }, + "fde395bc0b6d5cbb4c1d8fea3e0b4bff635e9db7": { + "balance": "0x6c6b935b8bbd400000" + }, + "fdeaac2acf1d138e19f2fc3f9fb74592e3ed818a": { + "balance": "0x243d4d18229ca20000" + }, + "fdecc82ddfc56192e26f563c3d68cb544a96bfed": { + "balance": "0x17da3a04c7b3e00000" + }, + "fdf42343019b0b0c6bf260b173afab7e45b9d621": { + "balance": "0x6c6acc67d7b1d40000" + }, + "fdf449f108c6fb4f5a2b081eed7e45e6919e4d25": { + "balance": "0x6c6b935b8bbd400000" + }, + "fdfd6134c04a8ab7eb16f00643f8fed7daaaecb2": { + "balance": "0x15af1d78b58c400000" + }, + "fe00bf439911a553982db638039245bcf032dbdc": { + "balance": "0x155bd9307f9fe80000" + }, + "fe016ec17ec5f10e3bb98ff4a1eda045157682ab": { + "balance": "0x145f5402e7b2e60000" + }, + "fe0e30e214290d743dd30eb082f1f0a5225ade61": { + "balance": "0xad78ebc5ac6200000" + }, + "fe210b8f04dc6d4f76216acfcbd59ba83be9b630": { + "balance": "0x1158e460913d00000" + }, + "fe22a0b388668d1ae2643e771dacf38a434223cc": { + "balance": "0xd8db5ebd7b26380000" + }, + "fe362688845fa244cc807e4b1130eb3741a8051e": { + "balance": "0x3635c9adc5dea00000" + }, + "fe3827d57630cf8761d512797b0b858e478bbd12": { + "balance": "0x1158e460913d00000" + }, + "fe418b421a9c6d373602790475d2303e11a75930": { + "balance": "0x3708baed3d68900000" + }, + "fe4249127950e2f896ec0e7e2e3d055aab10550f": { + "balance": "0x243d4d18229ca20000" + }, + "fe4d8403216fd571572bf1bdb01d00578978d688": { + "balance": "0x215f835bc769da80000" + }, + "fe53b94989d89964da2061539526bbe979dd2ea9": { + "balance": "0x68a875073e29240000" + }, + "fe549bbfe64740189892932538daaf46d2b61d4f": { + "balance": "0x22b1c8c1227a00000" + }, + "fe615d975c0887e0c9113ec7298420a793af8b96": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "fe65c4188d7922576909642044fdc52395560165": { + "balance": "0xd8d726b7177a800000" + }, + "fe697ff22ca547bfc95e33d960da605c6763f35b": { + "balance": "0x47d4119fd960940000" + }, + "fe6a895b795cb4bf85903d3ce09c5aa43953d3bf": { + "balance": "0xb8507a820728200000" + }, + "fe6f5f42b6193b1ad16206e4afb5239d4d7db45e": { + "balance": "0x5dc892aa1131c80000" + }, + "fe7011b698bf3371132d7445b19eb5b094356aee": { + "balance": "0x6c6b935b8bbd400000" + }, + "fe80e9232deaff19baf99869883a4bdf0004e53c": { + "balance": "0x2e62f20a69be400000" + }, + "fe8e6e3665570dff7a1bda697aa589c0b4e9024a": { + "balance": "0x6c6b935b8bbd400000" + }, + "fe8f1fdcab7fbec9a6a3fcc507619600505c36a3": { + "balance": "0x11164759ffb320000" + }, + "fe91eccf2bd566afa11696c5049fa84c69630a52": { + "balance": "0x692ae8897081d00000" + }, + "fe96c4cd381562401aa32a86e65b9d52fa8aee27": { + "balance": "0x8f1d5c1cae37400000" + }, + "fe98c664c3e447a95e69bd582171b7176ea2a685": { + "balance": "0xd8d726b7177a800000" + }, + "fe9ad12ef05d6d90261f96c8340a0381974df477": { + "balance": "0x6c6b935b8bbd400000" + }, + "fe9c0fffefb803081256c0cf4d6659e6d33eb4fb": { + "balance": "0x52d542804f1ce00000" + }, + "fe9cfc3bb293ddb285e625f3582f74a6b0a5a6cd": { + "balance": "0x6acb3df27e1f880000" + }, + "fe9e1197d7974a7648dcc7a03112a88edbc9045d": { + "balance": "0x10afc1ade3b4ed40000" + }, + "feaca2ac74624bf348dac9985143cfd652a4be55": { + "balance": "0x5897fcbb02914088000" + }, + "fead1803e5e737a68e18472d9ac715f0994cc2be": { + "balance": "0x1b1ae4d6e2ef500000" + }, + "feb8b8e2af716ae41fc7c04bcf29540156461e6b": { + "balance": "0x545174a528a77a0000" + }, + "feb92d30bf01ff9a1901666c5573532bfa07eeec": { + "balance": "0x3635c9adc5dea00000" + }, + "febc3173bc9072136354002b7b4fb3bfc53f22f1": { + "balance": "0x140ec80fa7ee880000" + }, + "febd48d0ffdbd5656cd5e686363a61145228f279": { + "balance": "0x97c9ce4cf6d5c00000" + }, + "febd9f81cf78bd5fb6c4b9a24bd414bb9bfa4c4e": { + "balance": "0x6be10fb8ed6e138000" + }, + "fec06fe27b44c784b2396ec92f7b923ad17e9077": { + "balance": "0x6c6b935b8bbd400000" + }, + "fec14e5485de2b3eef5e74c46146db8e454e0335": { + "balance": "0x9b41fbf9e0aec0000" + }, + "fed8476d10d584b38bfa6737600ef19d35c41ed8": { + "balance": "0x62a992e53a0af00000" + }, + "feef3b6eabc94affd3310c1c4d0e65375e131119": { + "balance": "0x1158e460913d00000" + }, + "fef09d70243f39ed8cd800bf9651479e8f4aca3c": { + "balance": "0xad78ebc5ac6200000" + }, + "fef3b3dead1a6926d49aa32b12c22af54d9ff985": { + "balance": "0x3635c9adc5dea00000" + }, + "ff0b7cb71da9d4c1ea6ecc28ebda504c63f82fd1": { + "balance": "0x388a885df2fc6c0000" + }, + "ff0c3c7798e8733dd2668152891bab80a8be955c": { + "balance": "0x45946b0f9e9d60000" + }, + "ff0cb06c42e3d88948e45bd7b0d4e291aefeea51": { + "balance": "0x678a932062e4180000" + }, + "ff0cc8dac824fa24fc3caa2169e6e057cf638ad6": { + "balance": "0xd8d726b7177a800000" + }, + "ff0e2fec304207467e1e3307f64cbf30af8fd9cd": { + "balance": "0x6c6b935b8bbd400000" + }, + "ff128f4b355be1dc4a6f94fa510d7f15d53c2aff": { + "balance": "0x93739534d286800000" + }, + "ff12e49d8e06aa20f886293c0b98ed7eff788805": { + "balance": "0xd8d726b7177a800000" + }, + "ff207308ced238a6c01ad0213ca9eb4465d42590": { + "balance": "0x6c6acc67d7b1d40000" + }, + "ff26138330274df4e0a3081e6df7dd983ec6e78f": { + "balance": "0x6c6b935b8bbd400000" + }, + "ff2726294148b86c78a9372497e459898ed3fee3": { + "balance": "0x6acb3df27e1f880000" + }, + "ff3ded7a40d3aff0d7a8c45fa6136aa0433db457": { + "balance": "0x6c68ccd09b022c0000" + }, + "ff3eee57c34d6dae970d8b311117c53586cd3502": { + "balance": "0x5c283d410394100000" + }, + "ff3ef6ba151c21b59986ae64f6e8228bc9a2c733": { + "balance": "0x6c6b935b8bbd400000" + }, + "ff41d9e1b4effe18d8b0d1f63fc4255fb4e06c3d": { + "balance": "0x487a9a304539440000" + }, + "ff45cb34c928364d9cc9d8bb00373474618f06f3": { + "balance": "0x56bc75e2d63100000" + }, + "ff49a775814ec00051a795a875de24592ea400d4": { + "balance": "0x2a5a058fc295ed000000" + }, + "ff4a408f50e9e72146a28ce4fc8d90271f116e84": { + "balance": "0x6acb3df27e1f880000" + }, + "ff4d9c8484c43c42ff2c5ab759996498d323994d": { + "balance": "0xd8d726b7177a800000" + }, + "ff4fc66069046c525658c337a917f2d4b832b409": { + "balance": "0x6c6b935b8bbd400000" + }, + "ff5162f2354dc492c75fd6e3a107268660eecb47": { + "balance": "0x5c283d410394100000" + }, + "ff545bbb66fbd00eb5e6373ff4e326f5feb5fe12": { + "balance": "0x1158e460913d00000" + }, + "ff5e7ee7d5114821e159dca5e81f18f1bfffbff9": { + "balance": "0x6c6b935b8bbd400000" + }, + "ff61c9c1b7a3d8b53bba20b34466544b7b216644": { + "balance": "0x6c6b935b8bbd400000" + }, + "ff65511cada259260c1ddc41974ecaecd32d6357": { + "balance": "0x5f68e8131ecf800000" + }, + "ff7843c7010aa7e61519b762dfe49124a76b0e4e": { + "balance": "0xc5b17924412b9bb00000" + }, + "ff78541756ab2b706e0d70b18adb700fc4f1643d": { + "balance": "0x92896529baddc880000" + }, + "ff83855051ee8ffb70b4817dba3211ed2355869d": { + "balance": "0x15af1d78b58c400000" + }, + "ff850e3be1eb6a4d726c08fa73aad358f39706da": { + "balance": "0x692ae8897081d00000" + }, + "ff86e5e8e15b53909600e41308dab75f0e24e46b": { + "balance": "0x30eb50d2e140800000" + }, + "ff88ebacc41b3687f39e4b59e159599b80cba33f": { + "balance": "0x15af1d78b58c400000" + }, + "ff8a2ca5a81333f19998255f203256e1a819c0aa": { + "balance": "0xc249fdd3277800000" + }, + "ff8eb07de3d49d9d52bbe8e5b26dbe1d160fa834": { + "balance": "0xd814dcb94453080000" + }, + "ffa4aff1a37f984b0a67272149273ae9bd41e3bc": { + "balance": "0x21e19e0c9bab2400000" + }, + "ffa696ecbd787e66abae4fe87b635f07ca57d848": { + "balance": "0x487a9a304539440000" + }, + "ffac3db879a6c7158e8dec603b407463ba0d31cf": { + "balance": "0x6acb3df27e1f880000" + }, + "ffad3dd74e2c1f796ac640de56dc99b4c792a402": { + "balance": "0x10f0cf064dd59200000" + }, + "ffb04726dfa41afdc819168418610472970d7bfc": { + "balance": "0xd8d726b7177a800000" + }, + "ffb3bcc3196a8c3cb834cec94c34fed35b3e1054": { + "balance": "0x48a43c54602f700000" + }, + "ffb974673367f5c07be5fd270dc4b7138b074d57": { + "balance": "0x85ebc8bdb9066d8000" + }, + "ffb9c7217e66743031eb377af65c77db7359dcda": { + "balance": "0x22b1c8c1227a00000" + }, + "ffbc3da0381ec339c1c049eb1ed9ee34fdcea6ca": { + "balance": "0xd8d726b7177a800000" + }, + "ffc5fc4b7e8a0293ff39a3a0f7d60d2646d37a74": { + "balance": "0x6c6b935b8bbd400000" + }, + "ffc9cc3094b041ad0e076f968a0de3b167255866": { + "balance": "0x1770c1650beee80000" + }, + "ffd5170fd1a8118d558e7511e364b24906c4f6b3": { + "balance": "0x341d8cd27f1588000" + }, + "ffd6da958eecbc016bab91058440d39b41c7be83": { + "balance": "0x43c33c1937564800000" + }, + "ffe0e997f1977a615f5a315af413fd4869343ba0": { + "balance": "0x56cd55fc64dfe0000" + }, + "ffe28db53c9044b4ecd4053fd1b4b10d7056c688": { + "balance": "0x56bc75e2d63100000" + }, + "ffe2e28c3fb74749d7e780dc8a5d422538e6e451": { + "balance": "0xdbb81e05bc12d8000" + }, + "ffe8cbc1681e5e9db74a0f93f8ed25897519120f": { + "balance": "0x51b1d3839261ac0000" + }, + "ffeac0305ede3a915295ec8e61c7f881006f4474": { + "balance": "0x556f64c1fe7fa0000" + }, + "ffec0913c635baca2f5e57a37aa9fb7b6c9b6e26": { + "balance": "0x2ba39e82ed5d740000" + }, + "fff33a3bd36abdbd412707b8e310d6011454a7ae": { + "balance": "0x1b1ae4d6e2ef5000000" + }, + "fff4bad596633479a2a29f9a8b3f78eefd07e6ee": { + "balance": "0x56bc75e2d63100000" + }, + "fff7ac99c8e4feb60c9750054bdc14ce1857f181": { + "balance": "0x3635c9adc5dea00000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/ethereum/core/src/main/resources/rinkeby.json b/ethereum/core/src/main/resources/rinkeby.json new file mode 100755 index 00000000000..1508923164b --- /dev/null +++ b/ethereum/core/src/main/resources/rinkeby.json @@ -0,0 +1,798 @@ +{ + "config": { + "chainId": 4, + "homesteadBlock": 1, + "eip150Block": 2, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 3, + "eip158Block": 3, + "byzantiumBlock": 1035301, + "clique": { + "period": 15, + "epoch": 30000 + } + }, + "nonce": "0x0", + "timestamp": "0x58ee40ba", + "extraData": "0x52657370656374206d7920617574686f7269746168207e452e436172746d616e42eb768f2244c8811c63729a21a3569731535f067ffc57839b00206d1ad20c69a1981b489f772031b279182d99e65703f0076e4812653aab85fca0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x47b760", + "difficulty": "0x1", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0000000000000000000000000000000000000000": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000001": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000002": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000003": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000004": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000005": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000006": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000007": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000008": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000009": { + "balance": "0x1" + }, + "000000000000000000000000000000000000000a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000000b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000000c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000000d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000000e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000000f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000010": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000011": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000012": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000013": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000014": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000015": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000016": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000017": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000018": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000019": { + "balance": "0x1" + }, + "000000000000000000000000000000000000001a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000001b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000001c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000001d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000001e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000001f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000020": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000021": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000022": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000023": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000024": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000025": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000026": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000027": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000028": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000029": { + "balance": "0x1" + }, + "000000000000000000000000000000000000002a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000002b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000002c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000002d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000002e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000002f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000030": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000031": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000032": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000033": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000034": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000035": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000036": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000037": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000038": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000039": { + "balance": "0x1" + }, + "000000000000000000000000000000000000003a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000003b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000003c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000003d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000003e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000003f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000040": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000041": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000042": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000043": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000044": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000045": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000046": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000047": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000048": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000049": { + "balance": "0x1" + }, + "000000000000000000000000000000000000004a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000004b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000004c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000004d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000004e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000004f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000050": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000051": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000052": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000053": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000054": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000055": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000056": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000057": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000058": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000059": { + "balance": "0x1" + }, + "000000000000000000000000000000000000005a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000005b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000005c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000005d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000005e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000005f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000060": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000061": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000062": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000063": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000064": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000065": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000066": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000067": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000068": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000069": { + "balance": "0x1" + }, + "000000000000000000000000000000000000006a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000006b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000006c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000006d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000006e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000006f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000070": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000071": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000072": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000073": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000074": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000075": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000076": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000077": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000078": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000079": { + "balance": "0x1" + }, + "000000000000000000000000000000000000007a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000007b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000007c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000007d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000007e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000007f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000080": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000081": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000082": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000083": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000084": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000085": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000086": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000087": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000088": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000089": { + "balance": "0x1" + }, + "000000000000000000000000000000000000008a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000008b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000008c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000008d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000008e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000008f": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000090": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000091": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000092": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000093": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000094": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000095": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000096": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000097": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000098": { + "balance": "0x1" + }, + "0000000000000000000000000000000000000099": { + "balance": "0x1" + }, + "000000000000000000000000000000000000009a": { + "balance": "0x1" + }, + "000000000000000000000000000000000000009b": { + "balance": "0x1" + }, + "000000000000000000000000000000000000009c": { + "balance": "0x1" + }, + "000000000000000000000000000000000000009d": { + "balance": "0x1" + }, + "000000000000000000000000000000000000009e": { + "balance": "0x1" + }, + "000000000000000000000000000000000000009f": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a0": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a1": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a2": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a3": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a4": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a5": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a6": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a7": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a8": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000a9": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000aa": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ab": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ac": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ad": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ae": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000af": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b0": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b1": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b2": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b3": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b4": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b5": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b6": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b7": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b8": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000b9": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ba": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000bb": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000bc": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000bd": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000be": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000bf": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c0": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c1": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c2": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c3": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c4": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c5": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c6": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c7": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c8": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000c9": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ca": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000cb": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000cc": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000cd": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ce": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000cf": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d0": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d1": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d2": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d3": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d4": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d5": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d6": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d7": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d8": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000d9": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000da": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000db": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000dc": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000dd": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000de": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000df": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e0": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e1": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e2": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e3": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e4": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e5": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e6": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e7": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e8": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000e9": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ea": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000eb": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ec": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ed": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ee": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ef": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f0": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f1": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f2": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f3": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f4": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f5": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f6": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f7": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f8": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000f9": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000fa": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000fb": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000fc": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000fd": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000fe": { + "balance": "0x1" + }, + "00000000000000000000000000000000000000ff": { + "balance": "0x1" + }, + "31b98d14007bdee637298086988a0bbd31184523": { + "balance": "0x200000000000000000000000000000000000000000000000000000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/ethereum/core/src/main/resources/ropsten.json b/ethereum/core/src/main/resources/ropsten.json new file mode 100755 index 00000000000..1d03885d7bc --- /dev/null +++ b/ethereum/core/src/main/resources/ropsten.json @@ -0,0 +1,875 @@ +{ + "config": { + "chainId": 3, + "homesteadBlock": 0, + "daoForkBlock": 0, + "eip150Block": 0, + "eip158Block": 10, + "byzantiumBlock": 1700000, + "ethash": { + } + }, + "nonce": "0x0000000000000042", + "timestamp": "0x00", + "extraData": "0x3535353535353535353535353535353535353535353535353535353535353535", + "gasLimit": "0x1000000", + "difficulty": "0x100000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0000000000000000000000000000000000000000": { + "balance": "1" + }, + "0000000000000000000000000000000000000001": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "ecrecover", + "pricing": { + "linear": { + "base": 3000, + "word": 0 + } + } + } + }, + "0000000000000000000000000000000000000002": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "sha256", + "pricing": { + "linear": { + "base": 60, + "word": 12 + } + } + } + }, + "0000000000000000000000000000000000000003": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "ripemd160", + "pricing": { + "linear": { + "base": 600, + "word": 120 + } + } + } + }, + "0000000000000000000000000000000000000004": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "identity", + "pricing": { + "linear": { + "base": 15, + "word": 3 + } + } + } + }, + "0000000000000000000000000000000000000005": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "modexp", + "activate_at": 1700000, + "pricing": { + "modexp": { + "divisor": 20 + } + } + } + }, + "0000000000000000000000000000000000000006": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "alt_bn128_add", + "activate_at": 1700000, + "pricing": { + "linear": { + "base": 500, + "word": 0 + } + } + } + }, + "0000000000000000000000000000000000000007": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "alt_bn128_mul", + "activate_at": 1700000, + "pricing": { + "linear": { + "base": 40000, + "word": 0 + } + } + } + }, + "0000000000000000000000000000000000000008": { + "balance": "1", + "nonce": "0", + "builtin": { + "name": "alt_bn128_pairing", + "activate_at": 1700000, + "pricing": { + "alt_bn128_pairing": { + "base": 100000, + "pair": 80000 + } + } + } + }, + "0000000000000000000000000000000000000009": { + "balance": "1" + }, + "000000000000000000000000000000000000000a": { + "balance": "0" + }, + "000000000000000000000000000000000000000b": { + "balance": "0" + }, + "000000000000000000000000000000000000000c": { + "balance": "0" + }, + "000000000000000000000000000000000000000d": { + "balance": "0" + }, + "000000000000000000000000000000000000000e": { + "balance": "0" + }, + "000000000000000000000000000000000000000f": { + "balance": "0" + }, + "0000000000000000000000000000000000000010": { + "balance": "0" + }, + "0000000000000000000000000000000000000011": { + "balance": "0" + }, + "0000000000000000000000000000000000000012": { + "balance": "0" + }, + "0000000000000000000000000000000000000013": { + "balance": "0" + }, + "0000000000000000000000000000000000000014": { + "balance": "0" + }, + "0000000000000000000000000000000000000015": { + "balance": "0" + }, + "0000000000000000000000000000000000000016": { + "balance": "0" + }, + "0000000000000000000000000000000000000017": { + "balance": "0" + }, + "0000000000000000000000000000000000000018": { + "balance": "0" + }, + "0000000000000000000000000000000000000019": { + "balance": "0" + }, + "000000000000000000000000000000000000001a": { + "balance": "0" + }, + "000000000000000000000000000000000000001b": { + "balance": "0" + }, + "000000000000000000000000000000000000001c": { + "balance": "0" + }, + "000000000000000000000000000000000000001d": { + "balance": "0" + }, + "000000000000000000000000000000000000001e": { + "balance": "0" + }, + "000000000000000000000000000000000000001f": { + "balance": "0" + }, + "0000000000000000000000000000000000000020": { + "balance": "0" + }, + "0000000000000000000000000000000000000021": { + "balance": "0" + }, + "0000000000000000000000000000000000000022": { + "balance": "0" + }, + "0000000000000000000000000000000000000023": { + "balance": "0" + }, + "0000000000000000000000000000000000000024": { + "balance": "0" + }, + "0000000000000000000000000000000000000025": { + "balance": "0" + }, + "0000000000000000000000000000000000000026": { + "balance": "0" + }, + "0000000000000000000000000000000000000027": { + "balance": "0" + }, + "0000000000000000000000000000000000000028": { + "balance": "0" + }, + "0000000000000000000000000000000000000029": { + "balance": "0" + }, + "000000000000000000000000000000000000002a": { + "balance": "0" + }, + "000000000000000000000000000000000000002b": { + "balance": "0" + }, + "000000000000000000000000000000000000002c": { + "balance": "0" + }, + "000000000000000000000000000000000000002d": { + "balance": "0" + }, + "000000000000000000000000000000000000002e": { + "balance": "0" + }, + "000000000000000000000000000000000000002f": { + "balance": "0" + }, + "0000000000000000000000000000000000000030": { + "balance": "0" + }, + "0000000000000000000000000000000000000031": { + "balance": "0" + }, + "0000000000000000000000000000000000000032": { + "balance": "0" + }, + "0000000000000000000000000000000000000033": { + "balance": "0" + }, + "0000000000000000000000000000000000000034": { + "balance": "0" + }, + "0000000000000000000000000000000000000035": { + "balance": "0" + }, + "0000000000000000000000000000000000000036": { + "balance": "0" + }, + "0000000000000000000000000000000000000037": { + "balance": "0" + }, + "0000000000000000000000000000000000000038": { + "balance": "0" + }, + "0000000000000000000000000000000000000039": { + "balance": "0" + }, + "000000000000000000000000000000000000003a": { + "balance": "0" + }, + "000000000000000000000000000000000000003b": { + "balance": "0" + }, + "000000000000000000000000000000000000003c": { + "balance": "0" + }, + "000000000000000000000000000000000000003d": { + "balance": "0" + }, + "000000000000000000000000000000000000003e": { + "balance": "0" + }, + "000000000000000000000000000000000000003f": { + "balance": "0" + }, + "0000000000000000000000000000000000000040": { + "balance": "0" + }, + "0000000000000000000000000000000000000041": { + "balance": "0" + }, + "0000000000000000000000000000000000000042": { + "balance": "0" + }, + "0000000000000000000000000000000000000043": { + "balance": "0" + }, + "0000000000000000000000000000000000000044": { + "balance": "0" + }, + "0000000000000000000000000000000000000045": { + "balance": "0" + }, + "0000000000000000000000000000000000000046": { + "balance": "0" + }, + "0000000000000000000000000000000000000047": { + "balance": "0" + }, + "0000000000000000000000000000000000000048": { + "balance": "0" + }, + "0000000000000000000000000000000000000049": { + "balance": "0" + }, + "000000000000000000000000000000000000004a": { + "balance": "0" + }, + "000000000000000000000000000000000000004b": { + "balance": "0" + }, + "000000000000000000000000000000000000004c": { + "balance": "0" + }, + "000000000000000000000000000000000000004d": { + "balance": "0" + }, + "000000000000000000000000000000000000004e": { + "balance": "0" + }, + "000000000000000000000000000000000000004f": { + "balance": "0" + }, + "0000000000000000000000000000000000000050": { + "balance": "0" + }, + "0000000000000000000000000000000000000051": { + "balance": "0" + }, + "0000000000000000000000000000000000000052": { + "balance": "0" + }, + "0000000000000000000000000000000000000053": { + "balance": "0" + }, + "0000000000000000000000000000000000000054": { + "balance": "0" + }, + "0000000000000000000000000000000000000055": { + "balance": "0" + }, + "0000000000000000000000000000000000000056": { + "balance": "0" + }, + "0000000000000000000000000000000000000057": { + "balance": "0" + }, + "0000000000000000000000000000000000000058": { + "balance": "0" + }, + "0000000000000000000000000000000000000059": { + "balance": "0" + }, + "000000000000000000000000000000000000005a": { + "balance": "0" + }, + "000000000000000000000000000000000000005b": { + "balance": "0" + }, + "000000000000000000000000000000000000005c": { + "balance": "0" + }, + "000000000000000000000000000000000000005d": { + "balance": "0" + }, + "000000000000000000000000000000000000005e": { + "balance": "0" + }, + "000000000000000000000000000000000000005f": { + "balance": "0" + }, + "0000000000000000000000000000000000000060": { + "balance": "0" + }, + "0000000000000000000000000000000000000061": { + "balance": "0" + }, + "0000000000000000000000000000000000000062": { + "balance": "0" + }, + "0000000000000000000000000000000000000063": { + "balance": "0" + }, + "0000000000000000000000000000000000000064": { + "balance": "0" + }, + "0000000000000000000000000000000000000065": { + "balance": "0" + }, + "0000000000000000000000000000000000000066": { + "balance": "0" + }, + "0000000000000000000000000000000000000067": { + "balance": "0" + }, + "0000000000000000000000000000000000000068": { + "balance": "0" + }, + "0000000000000000000000000000000000000069": { + "balance": "0" + }, + "000000000000000000000000000000000000006a": { + "balance": "0" + }, + "000000000000000000000000000000000000006b": { + "balance": "0" + }, + "000000000000000000000000000000000000006c": { + "balance": "0" + }, + "000000000000000000000000000000000000006d": { + "balance": "0" + }, + "000000000000000000000000000000000000006e": { + "balance": "0" + }, + "000000000000000000000000000000000000006f": { + "balance": "0" + }, + "0000000000000000000000000000000000000070": { + "balance": "0" + }, + "0000000000000000000000000000000000000071": { + "balance": "0" + }, + "0000000000000000000000000000000000000072": { + "balance": "0" + }, + "0000000000000000000000000000000000000073": { + "balance": "0" + }, + "0000000000000000000000000000000000000074": { + "balance": "0" + }, + "0000000000000000000000000000000000000075": { + "balance": "0" + }, + "0000000000000000000000000000000000000076": { + "balance": "0" + }, + "0000000000000000000000000000000000000077": { + "balance": "0" + }, + "0000000000000000000000000000000000000078": { + "balance": "0" + }, + "0000000000000000000000000000000000000079": { + "balance": "0" + }, + "000000000000000000000000000000000000007a": { + "balance": "0" + }, + "000000000000000000000000000000000000007b": { + "balance": "0" + }, + "000000000000000000000000000000000000007c": { + "balance": "0" + }, + "000000000000000000000000000000000000007d": { + "balance": "0" + }, + "000000000000000000000000000000000000007e": { + "balance": "0" + }, + "000000000000000000000000000000000000007f": { + "balance": "0" + }, + "0000000000000000000000000000000000000080": { + "balance": "0" + }, + "0000000000000000000000000000000000000081": { + "balance": "0" + }, + "0000000000000000000000000000000000000082": { + "balance": "0" + }, + "0000000000000000000000000000000000000083": { + "balance": "0" + }, + "0000000000000000000000000000000000000084": { + "balance": "0" + }, + "0000000000000000000000000000000000000085": { + "balance": "0" + }, + "0000000000000000000000000000000000000086": { + "balance": "0" + }, + "0000000000000000000000000000000000000087": { + "balance": "0" + }, + "0000000000000000000000000000000000000088": { + "balance": "0" + }, + "0000000000000000000000000000000000000089": { + "balance": "0" + }, + "000000000000000000000000000000000000008a": { + "balance": "0" + }, + "000000000000000000000000000000000000008b": { + "balance": "0" + }, + "000000000000000000000000000000000000008c": { + "balance": "0" + }, + "000000000000000000000000000000000000008d": { + "balance": "0" + }, + "000000000000000000000000000000000000008e": { + "balance": "0" + }, + "000000000000000000000000000000000000008f": { + "balance": "0" + }, + "0000000000000000000000000000000000000090": { + "balance": "0" + }, + "0000000000000000000000000000000000000091": { + "balance": "0" + }, + "0000000000000000000000000000000000000092": { + "balance": "0" + }, + "0000000000000000000000000000000000000093": { + "balance": "0" + }, + "0000000000000000000000000000000000000094": { + "balance": "0" + }, + "0000000000000000000000000000000000000095": { + "balance": "0" + }, + "0000000000000000000000000000000000000096": { + "balance": "0" + }, + "0000000000000000000000000000000000000097": { + "balance": "0" + }, + "0000000000000000000000000000000000000098": { + "balance": "0" + }, + "0000000000000000000000000000000000000099": { + "balance": "0" + }, + "000000000000000000000000000000000000009a": { + "balance": "0" + }, + "000000000000000000000000000000000000009b": { + "balance": "0" + }, + "000000000000000000000000000000000000009c": { + "balance": "0" + }, + "000000000000000000000000000000000000009d": { + "balance": "0" + }, + "000000000000000000000000000000000000009e": { + "balance": "0" + }, + "000000000000000000000000000000000000009f": { + "balance": "0" + }, + "00000000000000000000000000000000000000a0": { + "balance": "0" + }, + "00000000000000000000000000000000000000a1": { + "balance": "0" + }, + "00000000000000000000000000000000000000a2": { + "balance": "0" + }, + "00000000000000000000000000000000000000a3": { + "balance": "0" + }, + "00000000000000000000000000000000000000a4": { + "balance": "0" + }, + "00000000000000000000000000000000000000a5": { + "balance": "0" + }, + "00000000000000000000000000000000000000a6": { + "balance": "0" + }, + "00000000000000000000000000000000000000a7": { + "balance": "0" + }, + "00000000000000000000000000000000000000a8": { + "balance": "0" + }, + "00000000000000000000000000000000000000a9": { + "balance": "0" + }, + "00000000000000000000000000000000000000aa": { + "balance": "0" + }, + "00000000000000000000000000000000000000ab": { + "balance": "0" + }, + "00000000000000000000000000000000000000ac": { + "balance": "0" + }, + "00000000000000000000000000000000000000ad": { + "balance": "0" + }, + "00000000000000000000000000000000000000ae": { + "balance": "0" + }, + "00000000000000000000000000000000000000af": { + "balance": "0" + }, + "00000000000000000000000000000000000000b0": { + "balance": "0" + }, + "00000000000000000000000000000000000000b1": { + "balance": "0" + }, + "00000000000000000000000000000000000000b2": { + "balance": "0" + }, + "00000000000000000000000000000000000000b3": { + "balance": "0" + }, + "00000000000000000000000000000000000000b4": { + "balance": "0" + }, + "00000000000000000000000000000000000000b5": { + "balance": "0" + }, + "00000000000000000000000000000000000000b6": { + "balance": "0" + }, + "00000000000000000000000000000000000000b7": { + "balance": "0" + }, + "00000000000000000000000000000000000000b8": { + "balance": "0" + }, + "00000000000000000000000000000000000000b9": { + "balance": "0" + }, + "00000000000000000000000000000000000000ba": { + "balance": "0" + }, + "00000000000000000000000000000000000000bb": { + "balance": "0" + }, + "00000000000000000000000000000000000000bc": { + "balance": "0" + }, + "00000000000000000000000000000000000000bd": { + "balance": "0" + }, + "00000000000000000000000000000000000000be": { + "balance": "0" + }, + "00000000000000000000000000000000000000bf": { + "balance": "0" + }, + "00000000000000000000000000000000000000c0": { + "balance": "0" + }, + "00000000000000000000000000000000000000c1": { + "balance": "0" + }, + "00000000000000000000000000000000000000c2": { + "balance": "0" + }, + "00000000000000000000000000000000000000c3": { + "balance": "0" + }, + "00000000000000000000000000000000000000c4": { + "balance": "0" + }, + "00000000000000000000000000000000000000c5": { + "balance": "0" + }, + "00000000000000000000000000000000000000c6": { + "balance": "0" + }, + "00000000000000000000000000000000000000c7": { + "balance": "0" + }, + "00000000000000000000000000000000000000c8": { + "balance": "0" + }, + "00000000000000000000000000000000000000c9": { + "balance": "0" + }, + "00000000000000000000000000000000000000ca": { + "balance": "0" + }, + "00000000000000000000000000000000000000cb": { + "balance": "0" + }, + "00000000000000000000000000000000000000cc": { + "balance": "0" + }, + "00000000000000000000000000000000000000cd": { + "balance": "0" + }, + "00000000000000000000000000000000000000ce": { + "balance": "0" + }, + "00000000000000000000000000000000000000cf": { + "balance": "0" + }, + "00000000000000000000000000000000000000d0": { + "balance": "0" + }, + "00000000000000000000000000000000000000d1": { + "balance": "0" + }, + "00000000000000000000000000000000000000d2": { + "balance": "0" + }, + "00000000000000000000000000000000000000d3": { + "balance": "0" + }, + "00000000000000000000000000000000000000d4": { + "balance": "0" + }, + "00000000000000000000000000000000000000d5": { + "balance": "0" + }, + "00000000000000000000000000000000000000d6": { + "balance": "0" + }, + "00000000000000000000000000000000000000d7": { + "balance": "0" + }, + "00000000000000000000000000000000000000d8": { + "balance": "0" + }, + "00000000000000000000000000000000000000d9": { + "balance": "0" + }, + "00000000000000000000000000000000000000da": { + "balance": "0" + }, + "00000000000000000000000000000000000000db": { + "balance": "0" + }, + "00000000000000000000000000000000000000dc": { + "balance": "0" + }, + "00000000000000000000000000000000000000dd": { + "balance": "0" + }, + "00000000000000000000000000000000000000de": { + "balance": "0" + }, + "00000000000000000000000000000000000000df": { + "balance": "0" + }, + "00000000000000000000000000000000000000e0": { + "balance": "0" + }, + "00000000000000000000000000000000000000e1": { + "balance": "0" + }, + "00000000000000000000000000000000000000e2": { + "balance": "0" + }, + "00000000000000000000000000000000000000e3": { + "balance": "0" + }, + "00000000000000000000000000000000000000e4": { + "balance": "0" + }, + "00000000000000000000000000000000000000e5": { + "balance": "0" + }, + "00000000000000000000000000000000000000e6": { + "balance": "0" + }, + "00000000000000000000000000000000000000e7": { + "balance": "0" + }, + "00000000000000000000000000000000000000e8": { + "balance": "0" + }, + "00000000000000000000000000000000000000e9": { + "balance": "0" + }, + "00000000000000000000000000000000000000ea": { + "balance": "0" + }, + "00000000000000000000000000000000000000eb": { + "balance": "0" + }, + "00000000000000000000000000000000000000ec": { + "balance": "0" + }, + "00000000000000000000000000000000000000ed": { + "balance": "0" + }, + "00000000000000000000000000000000000000ee": { + "balance": "0" + }, + "00000000000000000000000000000000000000ef": { + "balance": "0" + }, + "00000000000000000000000000000000000000f0": { + "balance": "0" + }, + "00000000000000000000000000000000000000f1": { + "balance": "0" + }, + "00000000000000000000000000000000000000f2": { + "balance": "0" + }, + "00000000000000000000000000000000000000f3": { + "balance": "0" + }, + "00000000000000000000000000000000000000f4": { + "balance": "0" + }, + "00000000000000000000000000000000000000f5": { + "balance": "0" + }, + "00000000000000000000000000000000000000f6": { + "balance": "0" + }, + "00000000000000000000000000000000000000f7": { + "balance": "0" + }, + "00000000000000000000000000000000000000f8": { + "balance": "0" + }, + "00000000000000000000000000000000000000f9": { + "balance": "0" + }, + "00000000000000000000000000000000000000fa": { + "balance": "0" + }, + "00000000000000000000000000000000000000fb": { + "balance": "0" + }, + "00000000000000000000000000000000000000fc": { + "balance": "0" + }, + "00000000000000000000000000000000000000fd": { + "balance": "0" + }, + "00000000000000000000000000000000000000fe": { + "balance": "0" + }, + "00000000000000000000000000000000000000ff": { + "balance": "0" + }, + "874b54a8bd152966d63f706bae1ffeb0411921e5": { + "balance": "1000000000000000000000000000000" + } + } +} \ No newline at end of file diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/AddressHelpers.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/AddressHelpers.java new file mode 100755 index 00000000000..7ca617de208 --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/AddressHelpers.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.core; + +import java.math.BigInteger; + +public class AddressHelpers { + + /** + * Creates a new address based on the provided src, and an integer offset. This is required for + * managing ordered address lists. + * + * @param src The address from which a new address is to be derived. + * @param offset The distance and polarity of the offset from src address. + * @return A new address 'offset' away from the original src. + */ + public static Address calculateAddressWithRespectTo(final Address src, final int offset) { + + // Need to crop the "0x" from the start of the hex string. + final BigInteger inputValue = new BigInteger(src.toString().substring(2), 16); + final BigInteger bigIntOffset = BigInteger.valueOf(offset); + + final BigInteger result = inputValue.add(bigIntOffset); + + return Address.fromHexString(result.toString(16)); + } + + public static Address ofValue(final int value) { + return Address.fromHexString(String.format("%020x", value)); + } +} diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockHeaderTestFixture.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockHeaderTestFixture.java new file mode 100755 index 00000000000..8f7933c4185 --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockHeaderTestFixture.java @@ -0,0 +1,125 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +public class BlockHeaderTestFixture { + + private Hash parentHash = Hash.EMPTY; + private Hash ommersHash = Hash.EMPTY_LIST_HASH; + private Address coinbase = Address.ECREC; + + private Hash stateRoot = Hash.EMPTY_TRIE_HASH; + private Hash transactionsRoot = Hash.EMPTY; + private Hash receiptsRoot = Hash.EMPTY; + + private LogsBloomFilter logsBloom = LogsBloomFilter.empty(); + private UInt256 difficulty = UInt256.ZERO; + private long number = 0; + + private long gasLimit = 0; + private long gasUsed = 0; + private long timestamp = 0; + private BytesValue extraData = BytesValue.EMPTY; + + private Hash mixHash = Hash.EMPTY; + private long nonce = 0; + + public BlockHeader buildHeader() { + final BlockHeaderBuilder builder = BlockHeaderBuilder.create(); + builder.parentHash(parentHash); + builder.ommersHash(ommersHash); + builder.coinbase(coinbase); + builder.stateRoot(stateRoot); + builder.transactionsRoot(transactionsRoot); + builder.receiptsRoot(receiptsRoot); + builder.logsBloom(logsBloom); + builder.difficulty(difficulty); + builder.number(number); + builder.gasLimit(gasLimit); + builder.gasUsed(gasUsed); + builder.timestamp(timestamp); + builder.extraData(extraData); + builder.mixHash(mixHash); + builder.nonce(nonce); + builder.blockHashFunction(MainnetBlockHashFunction::createHash); + + return builder.buildBlockHeader(); + } + + public BlockHeaderTestFixture parentHash(final Hash parentHash) { + this.parentHash = parentHash; + return this; + } + + public BlockHeaderTestFixture ommersHash(final Hash ommersHash) { + this.ommersHash = ommersHash; + return this; + } + + public BlockHeaderTestFixture coinbase(final Address coinbase) { + this.coinbase = coinbase; + return this; + } + + public BlockHeaderTestFixture stateRoot(final Hash stateRoot) { + this.stateRoot = stateRoot; + return this; + } + + public BlockHeaderTestFixture transactionsRoot(final Hash transactionsRoot) { + this.transactionsRoot = transactionsRoot; + return this; + } + + public BlockHeaderTestFixture receiptsRoot(final Hash receiptsRoot) { + this.receiptsRoot = receiptsRoot; + return this; + } + + public BlockHeaderTestFixture logsBloom(final LogsBloomFilter logsBloom) { + this.logsBloom = logsBloom; + return this; + } + + public BlockHeaderTestFixture difficulty(final UInt256 difficulty) { + this.difficulty = difficulty; + return this; + } + + public BlockHeaderTestFixture number(final long number) { + this.number = number; + return this; + } + + public BlockHeaderTestFixture gasLimit(final long gasLimit) { + this.gasLimit = gasLimit; + return this; + } + + public BlockHeaderTestFixture gasUsed(final long gasUsed) { + this.gasUsed = gasUsed; + return this; + } + + public BlockHeaderTestFixture timestamp(final long timestamp) { + this.timestamp = timestamp; + return this; + } + + public BlockHeaderTestFixture extraData(final BytesValue extraData) { + this.extraData = extraData; + return this; + } + + public BlockHeaderTestFixture mixHash(final Hash mixHash) { + this.mixHash = mixHash; + return this; + } + + public BlockHeaderTestFixture nonce(final long nonce) { + this.nonce = nonce; + return this; + } +} diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockSyncTestUtils.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockSyncTestUtils.java new file mode 100755 index 00000000000..bf294ef110c --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/BlockSyncTestUtils.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.util.RawBlockIterator; +import net.consensys.pantheon.testutil.BlockTestUtil; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.junit.rules.TemporaryFolder; + +public final class BlockSyncTestUtils { + + private BlockSyncTestUtils() { + // Utility Class + } + + public static List firstBlocks(final int count) { + final List result = new ArrayList<>(count); + final TemporaryFolder temp = new TemporaryFolder(); + try { + temp.create(); + final Path blocks = temp.newFile().toPath(); + BlockTestUtil.write1000Blocks(blocks); + try (final RawBlockIterator iterator = + new RawBlockIterator( + blocks, rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash))) { + for (int i = 0; i < count; ++i) { + result.add(iterator.next()); + } + } + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } finally { + temp.delete(); + } + return result; + } +} diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/ExecutionContextTestFixture.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/ExecutionContextTestFixture.java new file mode 100755 index 00000000000..86c72c1f1e1 --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/ExecutionContextTestFixture.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.worldstate.DefaultMutableWorldState; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; + +public class ExecutionContextTestFixture { + + private final Block genesis = GenesisConfig.mainnet().getBlock(); + private final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + private final MutableBlockchain blockchain = + new DefaultMutableBlockchain(genesis, keyValueStorage, MainnetBlockHashFunction::createHash); + private final WorldStateArchive stateArchive = + new WorldStateArchive(new KeyValueStorageWorldStateStorage(keyValueStorage)); + + ProtocolSchedule protocolSchedule = + MainnetProtocolSchedule.create(2, 3, 10, 11, 12, -1, 42); + ProtocolContext protocolContext = new ProtocolContext<>(blockchain, stateArchive, null); + + public ExecutionContextTestFixture() { + GenesisConfig.mainnet() + .writeStateTo( + new DefaultMutableWorldState(new KeyValueStorageWorldStateStorage(keyValueStorage))); + } + + public Block getGenesis() { + return genesis; + } + + public KeyValueStorage getKeyValueStorage() { + return keyValueStorage; + } + + public MutableBlockchain getBlockchain() { + return blockchain; + } + + public WorldStateArchive getStateArchive() { + return stateArchive; + } + + public ProtocolSchedule getProtocolSchedule() { + return protocolSchedule; + } + + public ProtocolContext getProtocolContext() { + return protocolContext; + } +} diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/HeaderDecodingHelpers.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/HeaderDecodingHelpers.java new file mode 100755 index 00000000000..f575c02bdce --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/HeaderDecodingHelpers.java @@ -0,0 +1,97 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Map; + +import com.fasterxml.jackson.core.JsonParser.Feature; +import com.google.common.base.Splitter; +import com.google.common.collect.Maps; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; + +public class HeaderDecodingHelpers { + + public static class LoadedBlockHeader { + private final BlockHeader header; + private final Hash parsedBlockHash; + + public LoadedBlockHeader(final BlockHeader header, final Hash parsedBlockHash) { + this.header = header; + this.parsedBlockHash = parsedBlockHash; + } + + public BlockHeader getHeader() { + return header; + } + + public Hash getParsedBlockHash() { + return parsedBlockHash; + } + } + + /** + * @param blockHeaderStr A block header string as generated by BlockHeader.toString() + * @return a data object representing the string passed in. + */ + public static LoadedBlockHeader fromString(final String blockHeaderStr) { + final Map kv = Maps.newHashMap(); + final Iterable items = Splitter.on(", ").split(blockHeaderStr); + for (final String item : items) { + kv.put(item.replaceAll("=.*", ""), item.replaceAll(".*=", "")); + } + final BlockHeaderBuilder builder = new BlockHeaderBuilder(); + builder + .parentHash(Hash.fromHexString(kv.get("parentHash"))) + .ommersHash(Hash.fromHexString(kv.get("ommersHash"))) + .coinbase(Address.fromHexString(kv.get("coinbase"))) + .stateRoot(Hash.fromHexString(kv.get("stateRoot"))) + .transactionsRoot(Hash.fromHexString(kv.get("transactionsRoot"))) + .receiptsRoot(Hash.fromHexString(kv.get("receiptsRoot"))) + .logsBloom(LogsBloomFilter.fromHexString(kv.get("logsBloom"))) + .difficulty(UInt256.of(Long.parseLong(kv.get("difficulty")))) + .number(Long.parseLong(kv.get("number"))) + .gasLimit(Long.parseLong(kv.get("gasLimit"))) + .gasUsed(Long.parseLong(kv.get("gasUsed"))) + .timestamp(Long.parseLong(kv.get("timestamp"))) + .extraData(BytesValue.fromHexString(kv.get("extraData"))) + .mixHash(Hash.fromHexString(kv.get("mixHash"))) + .nonce(Long.parseLong(kv.get("nonce"))) + .blockHashFunction(MainnetBlockHashFunction::createHash); + + final Hash parsedHash = Hash.fromHexString(kv.get("hash")); + + builder.blockHashFunction(MainnetBlockHashFunction::createHash); + return new LoadedBlockHeader(builder.buildBlockHeader(), parsedHash); + } + + public static LoadedBlockHeader fromGethConsoleBlockDump(final String ethBlockDump) { + Json.mapper.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + final JsonObject jsonObj = new JsonObject(ethBlockDump); + + final BlockHeaderBuilder builder = new BlockHeaderBuilder(); + builder.difficulty(UInt256.of(jsonObj.getLong("difficulty"))); + builder.extraData(BytesValue.fromHexString(jsonObj.getString("extraData"))); + builder.gasLimit(jsonObj.getLong("gasLimit")); + builder.gasUsed(jsonObj.getLong("gasUsed")); + // Do not do Hash. + builder.logsBloom(LogsBloomFilter.fromHexString(jsonObj.getString("logsBloom"))); + builder.coinbase(Address.fromHexString(jsonObj.getString("miner"))); + builder.mixHash(Hash.fromHexString(jsonObj.getString("mixHash"))); + builder.nonce(Long.decode(jsonObj.getString("nonce"))); + builder.number(jsonObj.getLong("number")); + builder.parentHash(Hash.fromHexString(jsonObj.getString("parentHash"))); + builder.receiptsRoot(Hash.fromHexString(jsonObj.getString("receiptsRoot"))); + builder.ommersHash(Hash.fromHexString(jsonObj.getString("sha3Uncles"))); + builder.stateRoot(Hash.fromHexString(jsonObj.getString("stateRoot"))); + builder.timestamp(jsonObj.getLong("timestamp")); + builder.transactionsRoot(Hash.fromHexString(jsonObj.getString("transactionsRoot"))); + + final Hash parsedHash = Hash.fromHexString(jsonObj.getString("hash")); + + builder.blockHashFunction(MainnetBlockHashFunction::createHash); + return new LoadedBlockHeader(builder.buildBlockHeader(), parsedHash); + } +} diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/InMemoryWorldState.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/InMemoryWorldState.java new file mode 100755 index 00000000000..95eb26ed2d3 --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/InMemoryWorldState.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; + +public class InMemoryWorldState { + + public static WorldStateArchive createInMemoryWorldStateArchive() { + return new WorldStateArchive( + new KeyValueStorageWorldStateStorage(new InMemoryKeyValueStorage())); + } +} diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/MiningParametersTestBuilder.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/MiningParametersTestBuilder.java new file mode 100755 index 00000000000..f6b1c7f25b0 --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/MiningParametersTestBuilder.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class MiningParametersTestBuilder { + + private Address coinbase = AddressHelpers.ofValue(1); + private Wei minTransactionGasPrice = Wei.of(1000); + private BytesValue extraData = BytesValue.EMPTY; + private Boolean enabled = false; + + public MiningParametersTestBuilder coinbase(final Address coinbase) { + this.coinbase = coinbase; + return this; + } + + public MiningParametersTestBuilder minTransactionGasPrice(final Wei minTransactionGasPrice) { + this.minTransactionGasPrice = minTransactionGasPrice; + return this; + } + + public MiningParametersTestBuilder extraData(final BytesValue extraData) { + this.extraData = extraData; + return this; + } + + public MiningParametersTestBuilder enabled(final Boolean enabled) { + this.enabled = enabled; + return this; + } + + public MiningParameters build() { + return new MiningParameters(coinbase, minTransactionGasPrice, extraData, enabled); + } +} diff --git a/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/TransactionTestFixture.java b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/TransactionTestFixture.java new file mode 100755 index 00000000000..e0a7b6a204f --- /dev/null +++ b/ethereum/core/src/test-support/java/net/consensys/pantheon/ethereum/core/TransactionTestFixture.java @@ -0,0 +1,80 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +public class TransactionTestFixture { + + private long nonce = 0; + + private Wei gasPrice = Wei.of(5); + + private long gasLimit = 5000; + + private Optional
to = Optional.empty(); + private Address sender = Address.fromHexString(String.format("%020x", 1)); + + private Wei value = Wei.of(4); + + private BytesValue payload = BytesValue.EMPTY; + + private int chainId = 2018; + + public Transaction createTransaction(final KeyPair keys) { + final Transaction.Builder builder = Transaction.builder(); + builder + .gasLimit(gasLimit) + .gasPrice(gasPrice) + .nonce(nonce) + .payload(payload) + .value(value) + .sender(sender) + .chainId(chainId); + + to.ifPresent(builder::to); + + return builder.signAndBuild(keys); + } + + public TransactionTestFixture nonce(final long nonce) { + this.nonce = nonce; + return this; + } + + public TransactionTestFixture gasPrice(final Wei gasPrice) { + this.gasPrice = gasPrice; + return this; + } + + public TransactionTestFixture gasLimit(final long gasLimit) { + this.gasLimit = gasLimit; + return this; + } + + public TransactionTestFixture to(final Optional
to) { + this.to = to; + return this; + } + + public TransactionTestFixture sender(final Address sender) { + this.sender = sender; + return this; + } + + public TransactionTestFixture value(final Wei value) { + this.value = value; + return this; + } + + public TransactionTestFixture payload(final BytesValue payload) { + this.payload = payload; + return this; + } + + public TransactionTestFixture chainId(final int chainId) { + this.chainId = chainId; + return this; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelectorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelectorTest.java new file mode 100755 index 00000000000..a8202c0d2fe --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/BlockTransactionSelectorTest.java @@ -0,0 +1,550 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.NONCE_TOO_LOW; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.AddressHelpers; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogSeries; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.ProcessableBlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.TransactionTestFixture; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.development.DevelopmentProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.MainnetTransactionProcessor; +import net.consensys.pantheon.ethereum.mainnet.MainnetTransactionProcessor.Result; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; +import net.consensys.pantheon.ethereum.vm.TestBlockchain; +import net.consensys.pantheon.ethereum.worldstate.DefaultMutableWorldState; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.time.Instant; +import java.util.List; +import java.util.function.Supplier; + +import com.google.common.collect.Lists; +import io.vertx.core.json.JsonObject; +import org.junit.Test; + +public class BlockTransactionSelectorTest { + + private static final KeyPair keyPair = KeyPair.generate(); + + @Test + public void emptyPendingTransactionsResultsInEmptyVettingResult() { + final ProtocolSchedule protocolSchedule = + DevelopmentProtocolSchedule.create(new JsonObject()); + final Blockchain blockchain = new TestBlockchain(); + final TransactionProcessor transactionProcessor = + protocolSchedule.getByBlockNumber(0).getTransactionProcessor(); + final DefaultMutableWorldState worldState = inMemoryWorldState(); + final PendingTransactions pendingTransactions = new PendingTransactions(5); + final Supplier isCancelled = () -> false; + + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(5000) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.ZERO, + isCancelled, + miningBeneficiary); + + final BlockTransactionSelector.TransactionSelectionResults results = + selector.buildTransactionListForBlock(); + + assertThat(results.getTransactions().size()).isEqualTo(0); + assertThat(results.getReceipts().size()).isEqualTo(0); + assertThat(results.getCumulativeGasUsed()).isEqualTo(0); + } + + @Test + public void failedTransactionsAreIncludedInTheBlock() { + final PendingTransactions pendingTransactions = new PendingTransactions(5); + + final Transaction transaction = createTransaction(1); + pendingTransactions.addRemoteTransaction(transaction); + + final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + + when(transactionProcessor.processTransaction(any(), any(), any(), eq(transaction), any())) + .thenReturn(MainnetTransactionProcessor.Result.failed(5, ValidationResult.valid())); + + final Blockchain blockchain = new TestBlockchain(); + final DefaultMutableWorldState worldState = inMemoryWorldState(); + final Supplier isCancelled = () -> false; + + // The block should fit 3 transactions only + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(5000) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.ZERO, + isCancelled, + miningBeneficiary); + + final BlockTransactionSelector.TransactionSelectionResults results = + selector.buildTransactionListForBlock(); + + assertThat(results.getTransactions().size()).isEqualTo(1); + assertThat(results.getTransactions()).contains(transaction); + assertThat(results.getReceipts().size()).isEqualTo(1); + assertThat(results.getCumulativeGasUsed()).isEqualTo(95L); + } + + @Test + public void invalidTransactionsTransactionProcessingAreSkippedButBlockStillFills() { + final PendingTransactions pendingTransactions = new PendingTransactions(5); + + final List transactionsToInject = Lists.newArrayList(); + for (int i = 0; i < 5; i++) { + final Transaction tx = createTransaction(i); + transactionsToInject.add(tx); + pendingTransactions.addRemoteTransaction(tx); + } + + final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + when(transactionProcessor.processTransaction(any(), any(), any(), any(), any())) + .thenReturn( + MainnetTransactionProcessor.Result.successful( + new LogSeries(Lists.newArrayList()), + 0, + BytesValue.EMPTY, + ValidationResult.valid())); + when(transactionProcessor.processTransaction( + any(), any(), any(), eq(transactionsToInject.get(1)), any())) + .thenReturn( + MainnetTransactionProcessor.Result.invalid(ValidationResult.invalid(NONCE_TOO_LOW))); + + final Blockchain blockchain = new TestBlockchain(); + final DefaultMutableWorldState worldState = inMemoryWorldState(); + final Supplier isCancelled = () -> false; + + // The block should fit 3 transactions only + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(5000) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.ZERO, + isCancelled, + miningBeneficiary); + + final BlockTransactionSelector.TransactionSelectionResults results = + selector.buildTransactionListForBlock(); + + assertThat(results.getTransactions().size()).isEqualTo(4); + assertThat(results.getTransactions().contains(transactionsToInject.get(1))).isFalse(); + assertThat(results.getReceipts().size()).isEqualTo(4); + assertThat(results.getCumulativeGasUsed()).isEqualTo(400); + } + + @Test + public void subsetOfPendingTransactionsIncludedWhenBlockGasLimitHit() { + final PendingTransactions pendingTransactions = new PendingTransactions(5); + + final List transactionsToInject = Lists.newArrayList(); + // Transactions are reported in reverse order. + for (int i = 0; i < 5; i++) { + final Transaction tx = createTransaction(i); + transactionsToInject.add(tx); + pendingTransactions.addRemoteTransaction(tx); + } + final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + when(transactionProcessor.processTransaction(any(), any(), any(), any(), any())) + .thenReturn( + MainnetTransactionProcessor.Result.successful( + new LogSeries(Lists.newArrayList()), + 0, + BytesValue.EMPTY, + ValidationResult.valid())); + + final Blockchain blockchain = new TestBlockchain(); + + final DefaultMutableWorldState worldState = inMemoryWorldState(); + + final Supplier isCancelled = () -> false; + + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(301) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.ZERO, + isCancelled, + miningBeneficiary); + + final BlockTransactionSelector.TransactionSelectionResults results = + selector.buildTransactionListForBlock(); + + assertThat(results.getTransactions().size()).isEqualTo(3); + + assertThat(results.getTransactions().containsAll(transactionsToInject.subList(0, 3))).isTrue(); + assertThat(results.getReceipts().size()).isEqualTo(3); + assertThat(results.getCumulativeGasUsed()).isEqualTo(300); + + // Ensure receipts have the correct cumulative gas + assertThat(results.getReceipts().get(0).getCumulativeGasUsed()).isEqualTo(100); + assertThat(results.getReceipts().get(1).getCumulativeGasUsed()).isEqualTo(200); + assertThat(results.getReceipts().get(2).getCumulativeGasUsed()).isEqualTo(300); + } + + @Test + public void transactionOfferingGasPriceLessThanMinimumIsIdentifiedAndRemovedFromPending() { + final PendingTransactions pendingTransactions = new PendingTransactions(5); + + final Blockchain blockchain = new TestBlockchain(); + + final DefaultMutableWorldState worldState = inMemoryWorldState(); + + final Supplier isCancelled = () -> false; + + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(301) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.of(6), + isCancelled, + miningBeneficiary); + + final Transaction tx = createTransaction(1); + pendingTransactions.addRemoteTransaction(tx); + + final BlockTransactionSelector.TransactionSelectionResults results = + selector.buildTransactionListForBlock(); + + assertThat(results.getTransactions().size()).isEqualTo(0); + assertThat(pendingTransactions.size()).isEqualTo(0); + } + + @Test + public void transactionTooLargeForBlockDoesNotPreventMoreBeingAddedIfBlockOccupancyNotReached() { + final PendingTransactions pendingTransactions = new PendingTransactions(5); + final Blockchain blockchain = new TestBlockchain(); + final DefaultMutableWorldState worldState = inMemoryWorldState(); + final Supplier isCancelled = () -> false; + + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(300) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + // TransactionProcessor mock assumes all gas in the transaction was used (i.e. gasLimit). + final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + when(transactionProcessor.processTransaction(any(), any(), any(), any(), any())) + .thenReturn( + MainnetTransactionProcessor.Result.successful( + new LogSeries(Lists.newArrayList()), + 0, + BytesValue.EMPTY, + ValidationResult.valid())); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.ZERO, + isCancelled, + miningBeneficiary); + + final TransactionTestFixture txTestFixture = new TransactionTestFixture(); + // Add 3 transactions to the Pending Transactions, 79% of block, 100% of block and 10% of block + // should end up selecting the first and third only. + // NOTE - PendingTransactions outputs these in nonce order + final List transactionsToInject = Lists.newArrayList(); + transactionsToInject.add( + txTestFixture + .gasLimit((long) (blockHeader.getGasLimit() * 0.79)) + .nonce(1) + .createTransaction(keyPair)); + transactionsToInject.add( + txTestFixture.gasLimit(blockHeader.getGasLimit()).nonce(2).createTransaction(keyPair)); + transactionsToInject.add( + txTestFixture + .gasLimit((long) (blockHeader.getGasLimit() * 0.1)) + .nonce(3) + .createTransaction(keyPair)); + + for (final Transaction tx : transactionsToInject) { + pendingTransactions.addRemoteTransaction(tx); + } + + final BlockTransactionSelector.TransactionSelectionResults results = + selector.buildTransactionListForBlock(); + + assertThat(results.getTransactions().size()).isEqualTo(2); + assertThat(results.getTransactions().get(0)).isEqualTo(transactionsToInject.get(0)); + assertThat(results.getTransactions().get(1)).isEqualTo(transactionsToInject.get(2)); + } + + @Test + public void transactionSelectionStopsWhenSufficientBlockOccupancyIsReached() { + final PendingTransactions pendingTransactions = new PendingTransactions(10); + final Blockchain blockchain = new TestBlockchain(); + final DefaultMutableWorldState worldState = inMemoryWorldState(); + final Supplier isCancelled = () -> false; + + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(300) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + // TransactionProcessor mock assumes all gas in the transaction was used (i.e. gasLimit). + final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + when(transactionProcessor.processTransaction(any(), any(), any(), any(), any())) + .thenReturn( + MainnetTransactionProcessor.Result.successful( + new LogSeries(Lists.newArrayList()), + 0, + BytesValue.EMPTY, + ValidationResult.valid())); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.ZERO, + isCancelled, + miningBeneficiary); + + final TransactionTestFixture txTestFixture = new TransactionTestFixture(); + // Add 4 transactions to the Pending Transactions 15% (ok), 79% (ok), 25% (too large), 10% + // (not included, it would fit, however previous transaction was too large and block was + // suitably populated). + // NOTE - PendingTransactions will output these in nonce order. + final Transaction transaction1 = + txTestFixture + .gasLimit((long) (blockHeader.getGasLimit() * 0.15)) + .nonce(1) + .createTransaction(keyPair); + final Transaction transaction2 = + txTestFixture + .gasLimit((long) (blockHeader.getGasLimit() * 0.79)) + .nonce(2) + .createTransaction(keyPair); + final Transaction transaction3 = + txTestFixture + .gasLimit((long) (blockHeader.getGasLimit() * 0.25)) + .nonce(3) + .createTransaction(keyPair); + final Transaction transaction4 = + txTestFixture + .gasLimit((long) (blockHeader.getGasLimit() * 0.1)) + .nonce(4) + .createTransaction(keyPair); + + pendingTransactions.addRemoteTransaction(transaction1); + pendingTransactions.addRemoteTransaction(transaction2); + pendingTransactions.addRemoteTransaction(transaction3); + pendingTransactions.addRemoteTransaction(transaction4); + + final BlockTransactionSelector.TransactionSelectionResults results = + selector.buildTransactionListForBlock(); + + assertThat(results.getTransactions().size()).isEqualTo(2); + assertThat(results.getTransactions().get(0)).isEqualTo(transaction1); + assertThat(results.getTransactions().get(1)).isEqualTo(transaction2); + assertThat(results.getTransactions().contains(transaction4)).isFalse(); + assertThat(results.getTransactions().contains(transaction3)).isFalse(); + } + + @Test + public void shouldDiscardTransactionsThatFailValidation() { + final PendingTransactions pendingTransactions = new PendingTransactions(10); + final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + final Blockchain blockchain = new TestBlockchain(); + final DefaultMutableWorldState worldState = inMemoryWorldState(); + final Supplier isCancelled = () -> false; + + final ProcessableBlockHeader blockHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.EMPTY) + .coinbase(Address.fromHexString(String.format("%020x", 1))) + .difficulty(UInt256.ONE) + .number(1) + .gasLimit(300) + .timestamp(Instant.now().toEpochMilli()) + .buildProcessableBlockHeader(); + + final Address miningBeneficiary = AddressHelpers.ofValue(1); + final BlockTransactionSelector selector = + new BlockTransactionSelector( + transactionProcessor, + blockchain, + worldState, + pendingTransactions, + blockHeader, + this::createReceipt, + Wei.ZERO, + isCancelled, + miningBeneficiary); + + final TransactionTestFixture txTestFixture = new TransactionTestFixture(); + final Transaction validTransaction = + txTestFixture.nonce(1).gasLimit(1).createTransaction(keyPair); + final Transaction invalidTransaction = + txTestFixture.nonce(2).gasLimit(2).createTransaction(keyPair); + + pendingTransactions.addRemoteTransaction(validTransaction); + pendingTransactions.addRemoteTransaction(invalidTransaction); + + when(transactionProcessor.processTransaction( + eq(blockchain), any(WorldUpdater.class), eq(blockHeader), eq(validTransaction), any())) + .thenReturn( + Result.successful( + LogSeries.empty(), 10000, BytesValue.EMPTY, ValidationResult.valid())); + when(transactionProcessor.processTransaction( + eq(blockchain), + any(WorldUpdater.class), + eq(blockHeader), + eq(invalidTransaction), + any())) + .thenReturn( + Result.invalid( + ValidationResult.invalid(TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT))); + + selector.buildTransactionListForBlock(); + + assertThat(pendingTransactions.getTransactionByHash(validTransaction.hash())).isPresent(); + assertThat(pendingTransactions.getTransactionByHash(invalidTransaction.hash())).isNotPresent(); + } + + private Transaction createTransaction(final int transactionNumber) { + return Transaction.builder() + .gasLimit(100) + .gasPrice(Wei.of(5)) + .nonce(transactionNumber) + .payload(BytesValue.EMPTY) + .to(Address.ID) + .value(Wei.of(transactionNumber)) + .sender(Address.ID) + .chainId(1) + .signAndBuild(keyPair); + } + + // This is a duplicate of the MainnetProtocolSpec::frontierTransactionReceiptFactory + private TransactionReceipt createReceipt( + final TransactionProcessor.Result result, final WorldState worldState, final long gasUsed) { + return new TransactionReceipt(worldState.rootHash(), gasUsed, Lists.newArrayList()); + } + + private DefaultMutableWorldState inMemoryWorldState() { + return new DefaultMutableWorldState( + new KeyValueStorageWorldStateStorage(new InMemoryKeyValueStorage())); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockSchedulerTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockSchedulerTest.java new file mode 100755 index 00000000000..cb08acdf7ae --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/DefaultBlockSchedulerTest.java @@ -0,0 +1,87 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.blockcreation.BaseBlockScheduler.BlockCreationTimeResult; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.util.time.Clock; + +import org.junit.Test; + +public class DefaultBlockSchedulerTest { + + private final long interBlockSeconds = 1L; + private final long acceptableClockDrift = 10L; + private final long parentTimeStamp = 500L; + + @Test + public void canMineBlockOnLimitOfClockDrift() { + Clock clock = mock(Clock.class); + DefaultBlockScheduler scheduler = + new DefaultBlockScheduler(interBlockSeconds, acceptableClockDrift, clock); + + // Determine the system time of parent block creation, which means child will occur on + // the limit of clock drift. + final long parentBlockSystemTimeCreation = parentTimeStamp - acceptableClockDrift + 1; + when(clock.millisecondsSinceEpoch()).thenReturn(parentBlockSystemTimeCreation * 1000); + + BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + BlockHeader parentBlock = headerBuilder.timestamp(parentTimeStamp).buildHeader(); + BlockCreationTimeResult result = scheduler.getNextTimestamp(parentBlock); + + assertThat(result.getTimestampForHeader()).isEqualTo(parentTimeStamp + interBlockSeconds); + assertThat(result.getMillisecondsUntilValid()).isEqualTo(0); + } + + @Test + public void childBlockWithinClockDriftReportsTimeToValidOfZero() { + Clock clock = mock(Clock.class); + DefaultBlockScheduler scheduler = + new DefaultBlockScheduler(interBlockSeconds, acceptableClockDrift, clock); + + when(clock.millisecondsSinceEpoch()).thenReturn(parentTimeStamp * 1000); + + BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + BlockHeader parentBlock = headerBuilder.timestamp(parentTimeStamp).buildHeader(); + BlockCreationTimeResult result = scheduler.getNextTimestamp(parentBlock); + + assertThat(result.getMillisecondsUntilValid()).isEqualTo(0); + } + + @Test + public void mustWaitForNextBlockIfTooFarAheadOfSystemTime() { + final Clock clock = mock(Clock.class); + final DefaultBlockScheduler scheduler = + new DefaultBlockScheduler(interBlockSeconds, acceptableClockDrift, clock); + + // Set the clock such that the parenttimestamp is on the limit of acceptability + when(clock.millisecondsSinceEpoch()) + .thenReturn((parentTimeStamp - acceptableClockDrift) * 1000); + + BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + BlockHeader parentBlock = headerBuilder.timestamp(parentTimeStamp).buildHeader(); + BlockCreationTimeResult result = scheduler.getNextTimestamp(parentBlock); + + assertThat(result.getMillisecondsUntilValid()).isEqualTo(interBlockSeconds * 1000); + } + + @Test + public void ifParentTimestampIsBehindCurrentTimeChildUsesCurrentTime() { + final long secondsSinceEpoch = parentTimeStamp + 5L; // i.e. time is ahead of blockchain + + Clock clock = mock(Clock.class); + DefaultBlockScheduler scheduler = + new DefaultBlockScheduler(interBlockSeconds, acceptableClockDrift, clock); + + when(clock.millisecondsSinceEpoch()).thenReturn(secondsSinceEpoch * 1000); + + BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + BlockHeader parentBlock = headerBuilder.timestamp(parentTimeStamp).buildHeader(); + BlockCreationTimeResult result = scheduler.getNextTimestamp(parentBlock); + + assertThat(result.getTimestampForHeader()).isEqualTo(secondsSinceEpoch); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMinerTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMinerTest.java new file mode 100755 index 00000000000..18171283b7f --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashBlockMinerTest.java @@ -0,0 +1,120 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator.MinedBlockObserver; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.mainnet.EthHashBlockCreator; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.util.Subscribers; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class EthHashBlockMinerTest { + + @Test + @SuppressWarnings("unchecked") + public void blockCreatedIsAddedToBlockChain() throws InterruptedException { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + final Block blockToCreate = + new Block( + headerBuilder.buildHeader(), new BlockBody(Lists.newArrayList(), Lists.newArrayList())); + + final ProtocolContext protocolContext = new ProtocolContext<>(null, null, null); + + final EthHashBlockCreator blockCreator = mock(EthHashBlockCreator.class); + when(blockCreator.createBlock(anyLong())).thenReturn(blockToCreate); + + final BlockImporter blockImporter = mock(BlockImporter.class); + final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); + + final ProtocolSchedule protocolSchedule = singleSpecSchedule(protocolSpec); + + when(protocolSpec.getBlockImporter()).thenReturn(blockImporter); + when(blockImporter.importBlock(any(), any(), any())).thenReturn(true); + + final MinedBlockObserver observer = mock(MinedBlockObserver.class); + final DefaultBlockScheduler scheduler = mock(DefaultBlockScheduler.class); + when(scheduler.waitUntilNextBlockCanBeMined(any())).thenReturn(5L); + final EthHashBlockMiner miner = + new EthHashBlockMiner( + blockCreator, + protocolSchedule, + protocolContext, + subscribersContaining(observer), + scheduler, + headerBuilder.buildHeader()); // parent header is arbitrary for the test. + + miner.run(); + verify(blockImporter).importBlock(protocolContext, blockToCreate, HeaderValidationMode.FULL); + verify(observer, times(1)).blockMined(blockToCreate); + } + + @Test + @SuppressWarnings("unchecked") + public void failureToImportDoesNotTriggerObservers() throws InterruptedException { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + final Block blockToCreate = + new Block( + headerBuilder.buildHeader(), new BlockBody(Lists.newArrayList(), Lists.newArrayList())); + + final ProtocolContext protocolContext = new ProtocolContext<>(null, null, null); + + final EthHashBlockCreator blockCreator = mock(EthHashBlockCreator.class); + when(blockCreator.createBlock(anyLong())).thenReturn(blockToCreate); + + final BlockImporter blockImporter = mock(BlockImporter.class); + final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); + final ProtocolSchedule protocolSchedule = singleSpecSchedule(protocolSpec); + + when(protocolSpec.getBlockImporter()).thenReturn(blockImporter); + when(blockImporter.importBlock(any(), any(), any())).thenReturn(false, false, true); + + final MinedBlockObserver observer = mock(MinedBlockObserver.class); + final DefaultBlockScheduler scheduler = mock(DefaultBlockScheduler.class); + when(scheduler.waitUntilNextBlockCanBeMined(any())).thenReturn(5L); + final EthHashBlockMiner miner = + new EthHashBlockMiner( + blockCreator, + protocolSchedule, + protocolContext, + subscribersContaining(observer), + scheduler, + headerBuilder.buildHeader()); // parent header is arbitrary for the test. + + miner.run(); + verify(blockImporter, times(3)) + .importBlock(protocolContext, blockToCreate, HeaderValidationMode.FULL); + verify(observer, times(1)).blockMined(blockToCreate); + } + + private static Subscribers subscribersContaining( + final MinedBlockObserver... observers) { + final Subscribers result = new Subscribers<>(); + for (final MinedBlockObserver obs : observers) { + result.subscribe(obs); + } + return result; + } + + private ProtocolSchedule singleSpecSchedule(final ProtocolSpec protocolSpec) { + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone(0, protocolSpec); + return protocolSchedule; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutorTest.java new file mode 100755 index 00000000000..f1b2125e214 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/EthHashMinerExecutorTest.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import net.consensys.pantheon.ethereum.core.MiningParametersTestBuilder; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.util.Subscribers; +import net.consensys.pantheon.util.time.SystemClock; + +import java.util.concurrent.Executors; + +import org.junit.Test; + +public class EthHashMinerExecutorTest { + + @Test + public void startingMiningWithoutCoinbaseThrowsException() { + final MiningParameters miningParameters = + new MiningParametersTestBuilder().coinbase(null).build(); + + final EthHashMinerExecutor executor = + new EthHashMinerExecutor( + null, + Executors.newCachedThreadPool(), + null, + new PendingTransactions(1), + miningParameters, + new DefaultBlockScheduler(1, 10, new SystemClock())); + + assertThatExceptionOfType(CoinbaseNotSetException.class) + .isThrownBy(() -> executor.startAsyncMining(new Subscribers<>(), null)) + .withMessageContaining("Unable to start mining without a coinbase."); + } + + @Test + public void settingCoinbaseToNullThrowsException() { + final MiningParameters miningParameters = new MiningParametersTestBuilder().build(); + + final EthHashMinerExecutor executor = + new EthHashMinerExecutor( + null, + Executors.newCachedThreadPool(), + null, + new PendingTransactions(1), + miningParameters, + new DefaultBlockScheduler(1, 10, new SystemClock())); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> executor.setCoinbase(null)) + .withMessageContaining("Coinbase cannot be unset."); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGeneratorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGeneratorTest.java new file mode 100755 index 00000000000..c114600d807 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/IncrementingNonceGeneratorTest.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class IncrementingNonceGeneratorTest { + + @Test + public void firstValueProvidedIsSuppliedAtConstruction() { + final Long initialValue = 0L; + final IncrementingNonceGenerator generator = new IncrementingNonceGenerator(initialValue); + + assertThat(generator.iterator().next()).isEqualTo(initialValue); + } + + @Test + public void rollOverFromMaxResetsToZero() { + final Long initialValue = 0xFFFFFFFFFFFFFFFFL; + final IncrementingNonceGenerator generator = new IncrementingNonceGenerator(initialValue); + + assertThat(generator.iterator().next()).isEqualTo(initialValue); + final Long nextValue = generator.iterator().next(); + assertThat(Long.compareUnsigned(nextValue, 0)).isEqualTo(0); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinatorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinatorTest.java new file mode 100755 index 00000000000..e4db31728e6 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/blockcreation/MiningCoordinatorTest.java @@ -0,0 +1,57 @@ +package net.consensys.pantheon.ethereum.blockcreation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.ExecutionContextTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolution; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.Optional; + +import org.junit.Test; + +public class MiningCoordinatorTest { + + private final ExecutionContextTestFixture executionContext = new ExecutionContextTestFixture(); + + @Test + public void miningCoordinatorIsCreatedDisabledWithNoReportableMiningStatistics() { + final EthHashMinerExecutor executor = mock(EthHashMinerExecutor.class); + final MiningCoordinator miningCoordinator = + new MiningCoordinator(executionContext.getBlockchain(), executor); + final EthHashSolution solution = new EthHashSolution(1L, Hash.EMPTY, new byte[Bytes32.SIZE]); + + assertThat(miningCoordinator.isRunning()).isFalse(); + assertThat(miningCoordinator.hashesPerSecond()).isEqualTo(Optional.empty()); + assertThat(miningCoordinator.getWorkDefinition()).isEqualTo(Optional.empty()); + assertThat(miningCoordinator.submitWork(solution)).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + public void reportedHashRateIsCachedIfNoCurrentDataInMiner() throws InterruptedException { + + final EthHashBlockMiner miner = mock(EthHashBlockMiner.class); + + final Optional hashRate1 = Optional.of(10L); + final Optional hashRate2 = Optional.empty(); + final Optional hashRate3 = Optional.of(20L); + + when(miner.getHashesPerSecond()).thenReturn(hashRate1, hashRate2, hashRate3); + + final EthHashMinerExecutor executor = mock(EthHashMinerExecutor.class); + when(executor.startAsyncMining(any(), any())).thenReturn(miner); + + final MiningCoordinator miningCoordinator = + new MiningCoordinator(executionContext.getBlockchain(), executor); + + miningCoordinator.enable(); // Must enable prior returning data + assertThat(miningCoordinator.hashesPerSecond()).isEqualTo(hashRate1); + assertThat(miningCoordinator.hashesPerSecond()).isEqualTo(hashRate1); + assertThat(miningCoordinator.hashesPerSecond()).isEqualTo(hashRate3); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/chain/GenesisConfigTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/chain/GenesisConfigTest.java new file mode 100755 index 00000000000..0f1fae11e5b --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/chain/GenesisConfigTest.java @@ -0,0 +1,91 @@ +package net.consensys.pantheon.ethereum.chain; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.worldstate.DefaultMutableWorldState; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +public final class GenesisConfigTest { + + /** Known RLP encoded bytes of the Olympic Genesis Block. */ + private static final String OLYMPIC_RLP = + "f901f8f901f3a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a09178d0f23c965d81f0834a4c72c6253ce6830f4022b1359aaebfc1ecba442d4ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000080832fefd8808080a0000000000000000000000000000000000000000000000000000000000000000088000000000000002ac0c0"; + + /** Known Hash of the Olympic Genesis Block. */ + private static final String OLYMPIC_HASH = + "fd4af92a79c7fc2fd8bf0d342f2e832e1d4f485c85b9152d2039e03bc604fdca"; + + @Test + public void createFromJsonWithAllocs() throws Exception { + final GenesisConfig genesisConfig = + GenesisConfig.fromJson( + Resources.toString( + GenesisConfigTest.class.getResource("genesis1.json"), Charsets.UTF_8), + MainnetProtocolSchedule.create()); + final BlockHeader header = genesisConfig.getBlock().getHeader(); + assertThat(header.getStateRoot()) + .isEqualTo( + Hash.fromHexString( + "0x92683e6af0f8a932e5fe08c870f2ae9d287e39d4518ec544b0be451f1035fd39")); + assertThat(header.getTransactionsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH); + assertThat(header.getReceiptsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH); + assertThat(header.getOmmersHash()).isEqualTo(Hash.EMPTY_LIST_HASH); + assertThat(header.getExtraData()).isEqualTo(BytesValue.EMPTY); + assertThat(header.getParentHash()).isEqualTo(Hash.ZERO); + final DefaultMutableWorldState worldState = + new DefaultMutableWorldState( + new KeyValueStorageWorldStateStorage(new InMemoryKeyValueStorage())); + genesisConfig.writeStateTo(worldState); + final Account first = + worldState.get(Address.fromHexString("0x0000000000000000000000000000000000000001")); + final Account second = + worldState.get(Address.fromHexString("0x0000000000000000000000000000000000000002")); + assertThat(first).isNotNull(); + assertThat(first.getBalance().toLong()).isEqualTo(111111111L); + assertThat(second).isNotNull(); + assertThat(second.getBalance().toLong()).isEqualTo(222222222L); + } + + @Test + public void createFromJsonNoAllocs() throws Exception { + final GenesisConfig genesisConfig = + GenesisConfig.fromJson( + Resources.toString( + GenesisConfigTest.class.getResource("genesis2.json"), Charsets.UTF_8), + MainnetProtocolSchedule.create()); + final BlockHeader header = genesisConfig.getBlock().getHeader(); + assertThat(header.getStateRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH); + assertThat(header.getTransactionsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH); + assertThat(header.getReceiptsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH); + assertThat(header.getOmmersHash()).isEqualTo(Hash.EMPTY_LIST_HASH); + assertThat(header.getExtraData()).isEqualTo(BytesValue.EMPTY); + assertThat(header.getParentHash()).isEqualTo(Hash.ZERO); + } + + @Test + public void encodeOlympicBlock() throws Exception { + final GenesisConfig genesisConfig = + GenesisConfig.fromJson( + Resources.toString( + GenesisConfigTest.class.getResource("genesis-olympic.json"), Charsets.UTF_8), + MainnetProtocolSchedule.create()); + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + genesisConfig.getBlock().writeTo(tmp); + assertThat(Hex.toHexString(genesisConfig.getBlock().getHeader().getHash().extractArray())) + .isEqualTo(OLYMPIC_HASH); + assertThat(Hex.toHexString(tmp.encoded().extractArray())).isEqualTo(OLYMPIC_RLP); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrderTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrderTest.java new file mode 100755 index 00000000000..05defd9c744 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/AccountTransactionOrderTest.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; + +import java.util.stream.Stream; + +import org.junit.Test; + +public class AccountTransactionOrderTest { + + private static final KeyPair KEYS = KeyPair.generate(); + + private final Transaction transaction1 = transaction(1); + private final Transaction transaction2 = transaction(2); + private final Transaction transaction3 = transaction(3); + private final Transaction transaction4 = transaction(4); + private final AccountTransactionOrder accountTransactionOrder = + new AccountTransactionOrder( + Stream.of(transaction1, transaction2, transaction3, transaction4)); + + @Test + public void shouldProcessATransactionImmediatelyIfItsTheLowestNonce() { + assertThat(accountTransactionOrder.transactionsToProcess(transaction1)) + .containsExactly(transaction1); + } + + @Test + public void shouldDeferProcessingATransactionIfItIsNotTheLowestNonce() { + assertThat(accountTransactionOrder.transactionsToProcess(transaction2)).isEmpty(); + } + + @Test + public void shouldProcessDeferredTransactionsAfterPrerequisiteIsProcessed() { + assertThat(accountTransactionOrder.transactionsToProcess(transaction2)).isEmpty(); + assertThat(accountTransactionOrder.transactionsToProcess(transaction3)).isEmpty(); + + assertThat(accountTransactionOrder.transactionsToProcess(transaction1)) + .containsExactly(transaction1, transaction2, transaction3); + } + + @Test + public void shouldNotProcessDeferredTransactionsThatAreNotYetDue() { + assertThat(accountTransactionOrder.transactionsToProcess(transaction2)).isEmpty(); + assertThat(accountTransactionOrder.transactionsToProcess(transaction4)).isEmpty(); + + assertThat(accountTransactionOrder.transactionsToProcess(transaction1)) + .containsExactly(transaction1, transaction2); + + assertThat(accountTransactionOrder.transactionsToProcess(transaction3)) + .containsExactly(transaction3, transaction4); + } + + private Transaction transaction(final int nonce) { + return new TransactionTestFixture().nonce(nonce).createTransaction(KEYS); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/BlockHeaderMock.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/BlockHeaderMock.java new file mode 100755 index 00000000000..db72d6a98bc --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/BlockHeaderMock.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** A memory mock for testing. */ +@JsonIgnoreProperties("previousHash") +public class BlockHeaderMock extends BlockHeader { + + /** + * Public constructor. + * + * @param coinbase The beneficiary address. + * @param gasLimit The gas limit of the current block. + * @param number The number to execute. + */ + @JsonCreator + public BlockHeaderMock( + @JsonProperty("currentCoinbase") final String coinbase, + @JsonProperty("currentDifficulty") final String difficulty, + @JsonProperty("currentGasLimit") final String gasLimit, + @JsonProperty("currentNumber") final String number, + @JsonProperty("currentTimestamp") final String timestamp) { + super( + Hash.EMPTY, // parentHash + Hash.EMPTY, // ommersHash + Address.fromHexString(coinbase), + Hash.EMPTY, // stateRoot + Hash.EMPTY, // transactionsRoot + Hash.EMPTY, // receiptsRoot + new LogsBloomFilter(), + UInt256.fromHexString(difficulty), + Long.decode(number), + Long.decode(gasLimit), + 0L, + Long.decode(timestamp), + BytesValue.EMPTY, + Hash.ZERO, + 0L, + MainnetBlockHashFunction::createHash); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogTest.java new file mode 100755 index 00000000000..b3e14a0baef --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogTest.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.core; + +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import org.junit.Test; + +public class LogTest { + + @Test + public void toFromRlp() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final Log log = gen.log(); + final Log copy = Log.readFrom(RLP.input(RLP.encode(log::writeTo))); + assertEquals(log, copy); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTest.java new file mode 100755 index 00000000000..d455c334843 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTest.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.core; + +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class LogsBloomFilterTest { + + @Test + public void logsBloomFilter() { + final Address address = Address.fromHexString("0x095e7baea6a6c7c4c2dfeb977efac326af552d87"); + final BytesValue data = BytesValue.fromHexString("0x0102"); + final List topics = new ArrayList<>(); + topics.add( + LogTopic.of( + BytesValue.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000000"))); + + final Log log = new Log(address, data, topics); + final LogsBloomFilter bloom = LogsBloomFilter.empty(); + bloom.insertLog(log); + + assertEquals( + BytesValue.fromHexString( + "0x00000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000040000000000000000000000000000000000000000000000000000000"), + bloom.getBytes()); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTestCaseSpec.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTestCaseSpec.java new file mode 100755 index 00000000000..5498dfd484c --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/LogsBloomFilterTestCaseSpec.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.vm.LogMock; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A VM test case specification. + * + *

Note: this class will be auto-generated with the JSON test specification. + */ +@JsonIgnoreProperties("_info") +public class LogsBloomFilterTestCaseSpec { + + public LogsBloomFilter logsBloomFilter; + + public LogsBloomFilter finalBloom; + + public List logs; + + /** Public constructor. */ + @JsonCreator + public LogsBloomFilterTestCaseSpec( + @JsonProperty("logs") final List logs, + @JsonProperty("bloom") final String finalBloom) { + this.logs = logs; + this.finalBloom = new LogsBloomFilter(BytesValue.fromHexString(finalBloom)); + } + + public List getLogs() { + return logs; + } + + /** @return - 2048-bit representation of each log entry, except data, of each transaction. */ + public LogsBloomFilter getLogsBloomFilter() { + return logsBloomFilter; + } + + public LogsBloomFilter getFinalBloom() { + return finalBloom; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/PendingTransactionsTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/PendingTransactionsTest.java new file mode 100755 index 00000000000..5408508a7e0 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/PendingTransactionsTest.java @@ -0,0 +1,337 @@ +package net.consensys.pantheon.ethereum.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.core.PendingTransactions.TransactionSelectionResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalLong; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class PendingTransactionsTest { + + private static final int MAX_TRANSACTIONS = 5; + private static final KeyPair KEYS1 = KeyPair.generate(); + private static final KeyPair KEYS2 = KeyPair.generate(); + private final PendingTransactions transactions = new PendingTransactions(MAX_TRANSACTIONS); + private final Transaction transaction1 = createTransaction(2); + private final Transaction transaction2 = createTransaction(1); + + private final PendingTransactionListener listener = mock(PendingTransactionListener.class); + private static final Address SENDER1 = Util.publicKeyToAddress(KEYS1.getPublicKey()); + private static final Address SENDER2 = Util.publicKeyToAddress(KEYS2.getPublicKey()); + + @Test + public void shouldAddATransaction() { + transactions.addRemoteTransaction(transaction1); + assertThat(transactions.size()).isEqualTo(1); + + transactions.addRemoteTransaction(transaction2); + assertThat(transactions.size()).isEqualTo(2); + } + + @Test + public void shouldReturnEmptyOptionalWhenNoTransactionWithGivenHashExists() { + assertThat(transactions.getTransactionByHash(Hash.EMPTY_TRIE_HASH)).isEmpty(); + } + + @Test + public void shouldGetTransactionByHash() { + transactions.addRemoteTransaction(transaction1); + assertTransactionPending(transaction1); + } + + @Test + public void shouldDropOldestTransactionWhenLimitExceeded() { + final Transaction oldestTransaction = createTransaction(0); + transactions.addRemoteTransaction(oldestTransaction); + for (int i = 1; i < MAX_TRANSACTIONS; i++) { + transactions.addRemoteTransaction(createTransaction(i)); + } + assertThat(transactions.size()).isEqualTo(MAX_TRANSACTIONS); + + transactions.addRemoteTransaction(createTransaction(MAX_TRANSACTIONS + 1)); + assertThat(transactions.size()).isEqualTo(MAX_TRANSACTIONS); + assertTransactionNotPending(oldestTransaction); + } + + @Test + public void shouldHandleMaximumTransactionLimitCorrectlyWhenSameTransactionAddedMultipleTimes() { + transactions.addRemoteTransaction(createTransaction(0)); + transactions.addRemoteTransaction(createTransaction(0)); + + for (int i = 1; i < MAX_TRANSACTIONS; i++) { + transactions.addRemoteTransaction(createTransaction(i)); + } + assertThat(transactions.size()).isEqualTo(MAX_TRANSACTIONS); + + transactions.addRemoteTransaction(createTransaction(MAX_TRANSACTIONS + 1)); + transactions.addRemoteTransaction(createTransaction(MAX_TRANSACTIONS + 2)); + assertThat(transactions.size()).isEqualTo(MAX_TRANSACTIONS); + } + + @Test + public void shouldPrioritizeLocalTransaction() { + final Transaction localTransaction = createTransaction(0); + transactions.addLocalTransaction(localTransaction); + + for (int i = 1; i <= MAX_TRANSACTIONS; i++) { + transactions.addRemoteTransaction(createTransaction(i)); + } + assertThat(transactions.size()).isEqualTo(MAX_TRANSACTIONS); + assertTransactionPending(localTransaction); + } + + @Test + public void shouldStartDroppingLocalTransactionsWhenPoolIsFullOfLocalTransactions() { + final Transaction firstLocalTransaction = createTransaction(0); + transactions.addLocalTransaction(firstLocalTransaction); + + for (int i = 1; i <= MAX_TRANSACTIONS; i++) { + transactions.addLocalTransaction(createTransaction(i)); + } + assertThat(transactions.size()).isEqualTo(MAX_TRANSACTIONS); + assertTransactionNotPending(firstLocalTransaction); + } + + @Test + public void shouldNotifyListenerWhenRemoteTransactionAdded() { + transactions.addTransactionListener(listener); + + transactions.addRemoteTransaction(transaction1); + + verify(listener).onTransactionAdded(transaction1); + } + + @Test + public void shouldNotifyListenerWhenLocalTransactionAdded() { + transactions.addTransactionListener(listener); + + transactions.addLocalTransaction(transaction1); + + verify(listener).onTransactionAdded(transaction1); + } + + @Test + public void selectTransactionsUntilSelectorRequestsNoMore() { + transactions.addRemoteTransaction(transaction1); + transactions.addRemoteTransaction(transaction2); + + final List parsedTransactions = Lists.newArrayList(); + transactions.selectTransactions( + transaction -> { + parsedTransactions.add(transaction); + return TransactionSelectionResult.COMPLETE_OPERATION; + }); + + assertThat(parsedTransactions.size()).isEqualTo(1); + assertThat(parsedTransactions.get(0)).isEqualTo(transaction2); + } + + @Test + public void selectTransactionsUntilPendingIsEmpty() { + transactions.addRemoteTransaction(transaction1); + transactions.addRemoteTransaction(transaction2); + + final List parsedTransactions = Lists.newArrayList(); + transactions.selectTransactions( + transaction -> { + parsedTransactions.add(transaction); + return TransactionSelectionResult.CONTINUE; + }); + + assertThat(parsedTransactions.size()).isEqualTo(2); + assertThat(parsedTransactions.get(0)).isEqualTo(transaction2); + assertThat(parsedTransactions.get(1)).isEqualTo(transaction1); + } + + @Test + public void invalidTransactionIsDeletedFromPendingTransactions() { + transactions.addRemoteTransaction(transaction1); + transactions.addRemoteTransaction(transaction2); + + final List parsedTransactions = Lists.newArrayList(); + transactions.selectTransactions( + transaction -> { + parsedTransactions.add(transaction); + return TransactionSelectionResult.DELETE_TRANSACTION_AND_CONTINUE; + }); + + assertThat(parsedTransactions.size()).isEqualTo(2); + assertThat(parsedTransactions.get(0)).isEqualTo(transaction2); + assertThat(parsedTransactions.get(1)).isEqualTo(transaction1); + + assertThat(transactions.size()).isZero(); + } + + @Test + public void shouldReturnEmptyOptionalAsMaximumNonceWhenNoTransactionsPresent() { + assertThat(transactions.getNextNonceForSender(SENDER1)).isEmpty(); + } + + @Test + public void shouldReturnEmptyOptionalAsMaximumNonceWhenLastTransactionForSenderRemoved() { + final Transaction transaction = transactionWithNonceAndSender(1, KEYS1); + transactions.addRemoteTransaction(transaction); + transactions.removeTransaction(transaction); + assertThat(transactions.getNextNonceForSender(SENDER1)).isEmpty(); + } + + @Test + public void shouldReplaceTransactionWithSameSenderAndNonce() { + final Transaction transaction1 = transactionWithNonceSenderAndGasPrice(1, KEYS1, 1); + final Transaction transaction1b = transactionWithNonceSenderAndGasPrice(1, KEYS1, 2); + final Transaction transaction2 = transactionWithNonceSenderAndGasPrice(2, KEYS1, 1); + assertThat(transactions.addRemoteTransaction(transaction1)).isTrue(); + assertThat(transactions.addRemoteTransaction(transaction2)).isTrue(); + assertThat(transactions.addRemoteTransaction(transaction1b)).isTrue(); + + assertTransactionNotPending(transaction1); + assertTransactionPending(transaction1b); + assertTransactionPending(transaction2); + assertThat(transactions.size()).isEqualTo(2); + } + + @Test + public void shouldReplaceOnlyTransactionFromSenderWhenItHasTheSameNonce() { + final Transaction transaction1 = transactionWithNonceSenderAndGasPrice(1, KEYS1, 1); + final Transaction transaction1b = transactionWithNonceSenderAndGasPrice(1, KEYS1, 2); + assertThat(transactions.addRemoteTransaction(transaction1)).isTrue(); + assertThat(transactions.addRemoteTransaction(transaction1b)).isTrue(); + + assertTransactionNotPending(transaction1); + assertTransactionPending(transaction1b); + assertThat(transactions.size()).isEqualTo(1); + } + + @Test + public void shouldNotReplaceTransactionWithSameSenderAndNonceWhenGasPriceIsLower() { + final Transaction transaction1 = transactionWithNonceSenderAndGasPrice(1, KEYS1, 2); + final Transaction transaction1b = transactionWithNonceSenderAndGasPrice(1, KEYS1, 1); + assertThat(transactions.addRemoteTransaction(transaction1)).isTrue(); + + transactions.addTransactionListener(listener); + assertThat(transactions.addRemoteTransaction(transaction1b)).isFalse(); + + assertTransactionNotPending(transaction1b); + assertTransactionPending(transaction1); + assertThat(transactions.size()).isEqualTo(1); + verifyZeroInteractions(listener); + } + + @Test + public void shouldTrackMaximumNonceForEachSender() { + transactions.addRemoteTransaction(transactionWithNonceAndSender(0, KEYS1)); + assertMaximumNonceForSender(SENDER1, 1); + + transactions.addRemoteTransaction(transactionWithNonceAndSender(1, KEYS1)); + assertMaximumNonceForSender(SENDER1, 2); + + transactions.addRemoteTransaction(transactionWithNonceAndSender(2, KEYS1)); + assertMaximumNonceForSender(SENDER1, 3); + + transactions.addRemoteTransaction(transactionWithNonceAndSender(20, KEYS2)); + assertMaximumNonceForSender(SENDER2, 21); + assertMaximumNonceForSender(SENDER1, 3); + } + + @Test + public void shouldIterateTransactionsFromSameSenderInNonceOrder() { + final Transaction transaction1 = transactionWithNonceAndSender(0, KEYS1); + final Transaction transaction2 = transactionWithNonceAndSender(1, KEYS1); + final Transaction transaction3 = transactionWithNonceAndSender(2, KEYS1); + + transactions.addLocalTransaction(transaction1); + transactions.addLocalTransaction(transaction2); + transactions.addLocalTransaction(transaction3); + + final List iterationOrder = new ArrayList<>(); + transactions.selectTransactions( + transaction -> { + iterationOrder.add(transaction); + return TransactionSelectionResult.CONTINUE; + }); + + assertThat(iterationOrder).containsExactly(transaction1, transaction2, transaction3); + } + + @Test + public void shouldNotForceNonceOrderWhenSendersDiffer() { + final Transaction transaction1 = transactionWithNonceAndSender(0, KEYS1); + final Transaction transaction2 = transactionWithNonceAndSender(1, KEYS2); + + transactions.addLocalTransaction(transaction1); + transactions.addLocalTransaction(transaction2); + + final List iterationOrder = new ArrayList<>(); + transactions.selectTransactions( + transaction -> { + iterationOrder.add(transaction); + return TransactionSelectionResult.CONTINUE; + }); + + assertThat(iterationOrder).containsExactly(transaction2, transaction1); + } + + @Test + public void shouldNotIncreasePriorityOfTransactionsBecauseOfNonceOrder() { + final Transaction transaction1 = transactionWithNonceAndSender(0, KEYS1); + final Transaction transaction2 = transactionWithNonceAndSender(1, KEYS1); + final Transaction transaction3 = transactionWithNonceAndSender(2, KEYS1); + final Transaction transaction4 = transactionWithNonceAndSender(5, KEYS2); + + transactions.addLocalTransaction(transaction1); + transactions.addLocalTransaction(transaction4); + transactions.addLocalTransaction(transaction2); + transactions.addLocalTransaction(transaction3); + + final List iterationOrder = new ArrayList<>(); + transactions.selectTransactions( + transaction -> { + iterationOrder.add(transaction); + return TransactionSelectionResult.CONTINUE; + }); + + // Ignoring nonces, the order would be 3, 2, 4, 1 but we have to delay 3 and 2 until after 1. + assertThat(iterationOrder) + .containsExactly(transaction4, transaction1, transaction2, transaction3); + } + + private void assertMaximumNonceForSender(final Address sender1, final int i) { + assertThat(transactions.getNextNonceForSender(sender1)).isEqualTo(OptionalLong.of(i)); + } + + private Transaction transactionWithNonceAndSender(final int nonce, final KeyPair keyPair) { + return new TransactionTestFixture().nonce(nonce).createTransaction(keyPair); + } + + private Transaction transactionWithNonceSenderAndGasPrice( + final int nonce, final KeyPair keyPair, final long gasPrice) { + return new TransactionTestFixture() + .nonce(nonce) + .gasPrice(Wei.of(gasPrice)) + .createTransaction(keyPair); + } + + private void assertTransactionPending(final Transaction t) { + assertThat(transactions.getTransactionByHash(t.hash())).contains(t); + } + + private void assertTransactionNotPending(final Transaction t) { + assertThat(transactions.getTransactionByHash(t.hash())).isEmpty(); + } + + private Transaction createTransaction(final int transactionNumber) { + return new TransactionTestFixture() + .value(Wei.of(transactionNumber)) + .nonce(transactionNumber) + .createTransaction(KEYS1); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionIntegrationTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionIntegrationTest.java new file mode 100755 index 00000000000..3a9380871d5 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionIntegrationTest.java @@ -0,0 +1,43 @@ +package net.consensys.pantheon.ethereum.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class TransactionIntegrationTest { + + @Test + public void + shouldDecodeInterestingRlpFromARealTransactionGeneratedUsingRemixAndASimpleContract() { + final String encodedString = + "0xf902560c843b9aca00832dc6c08080b90202608060405234801561001057600080fd5b506040516020806101e283398101604052516000556101ae806100346000396000f30060806040526004361061006c5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632113522a81146100715780632a1afcd9146100af5780633bc5de30146100d657806360fe47b1146100eb578063db613e8114610105575b600080fd5b34801561007d57600080fd5b5061008661011a565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b3480156100bb57600080fd5b506100c4610136565b60408051918252519081900360200190f35b3480156100e257600080fd5b506100c461013c565b3480156100f757600080fd5b50610103600435610142565b005b34801561011157600080fd5b50610086610166565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60005481565b60005490565b6000556001805473ffffffffffffffffffffffffffffffffffffffff191633179055565b60015473ffffffffffffffffffffffffffffffffffffffff16905600a165627a7a723058208293fac83e9cc01039adf5e41eefd557d1324a3a4c830a4802fa1dd2515227a20029000000000000000000000000000000000000000000000000000000000000007b820fe8a009e7b69af4c318e7fb915f53649bd9f99e5423d41c2cd6a01ab69bb34a951b2fa01b5d39b7c9041ec022d13e6e89eec2cddbe27572eda7956ada5de1032cd5da15"; + final BytesValue encoded = BytesValue.fromHexString(encodedString); + final RLPInput input = RLP.input(encoded); + final Transaction transaction = Transaction.readFrom(input); + assertNotNull(transaction); + assertTrue(transaction.isContractCreation()); + assertEquals(2018, transaction.getChainId().getAsInt()); + assertEquals( + Address.fromHexString("0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"), + transaction.getSender()); + } + + @Test + public void shouldDecodeAndEncodeTransactionCorrectly() { + final String encodedString = + "0xf9025780843b9aca00832dc6c08080b90202608060405234801561001057600080fd5b506040516020806101e283398101604052516000556101ae806100346000396000f30060806040526004361061006c5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632113522a81146100715780632a1afcd9146100af5780633bc5de30146100d657806360fe47b1146100eb578063db613e8114610105575b600080fd5b34801561007d57600080fd5b5061008661011a565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b3480156100bb57600080fd5b506100c4610136565b60408051918252519081900360200190f35b3480156100e257600080fd5b506100c461013c565b3480156100f757600080fd5b50610103600435610142565b005b34801561011157600080fd5b50610086610166565b60015473ffffffffffffffffffffffffffffffffffffffff1681565b60005481565b60005490565b6000556001805473ffffffffffffffffffffffffffffffffffffffff191633179055565b60015473ffffffffffffffffffffffffffffffffffffffff16905600a165627a7a723058208293fac83e9cc01039adf5e41eefd557d1324a3a4c830a4802fa1dd2515227a20029000000000000000000000000000000000000000000000000000000000000000c830628cba0482ba9b1136cd9337408938eea6b991fd153900a014867da2f4bb113d4003888a00c5a2f8f279fe2c86831afb5c9578dd1c3be457e3aca3abe439b1a5dd122e676"; + final BytesValue encoded = BytesValue.fromHexString(encodedString); + final RLPInput input = RLP.input(encoded); + final Transaction transaction = Transaction.readFrom(input); + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + transaction.writeTo(output); + assertEquals(encodedString, output.encoded().toString()); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionPoolTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionPoolTest.java new file mode 100755 index 00000000000..cc4dd172c68 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionPoolTest.java @@ -0,0 +1,415 @@ +package net.consensys.pantheon.ethereum.core; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.NONCE_TOO_LOW; +import static net.consensys.pantheon.ethereum.mainnet.ValidationResult.valid; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.TransactionPool.TransactionBatchAddedListener; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.OptionalLong; + +import org.junit.Before; +import org.junit.Test; + +public class TransactionPoolTest { + + private static final int MAX_TRANSACTIONS = 5; + private static final KeyPair KEY_PAIR1 = KeyPair.generate(); + + private final PendingTransactionListener listener = mock(PendingTransactionListener.class); + private final TransactionBatchAddedListener batchAddedListener = + mock(TransactionBatchAddedListener.class); + + @SuppressWarnings("unchecked") + private final ProtocolSchedule protocolSchedule = mock(ProtocolSchedule.class); + + @SuppressWarnings("unchecked") + private final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); + + private final TransactionValidator transactionValidator = mock(TransactionValidator.class); + private MutableBlockchain blockchain; + private final PendingTransactions transactions = new PendingTransactions(MAX_TRANSACTIONS); + private final Transaction transaction1 = createTransaction(1); + private final Transaction transaction2 = createTransaction(2); + private TransactionPool transactionPool; + private long genesisBlockGasLimit; + + @Before + public void setUp() { + final GenesisConfig genesisConfig = GenesisConfig.development(); + final Block genesisBlock = genesisConfig.getBlock(); + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockchain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + final WorldStateArchive worldStateArchive = createInMemoryWorldStateArchive(); + final ProtocolContext protocolContext = + new ProtocolContext<>(blockchain, worldStateArchive, null); + genesisConfig.writeStateTo(worldStateArchive.getMutable(Hash.EMPTY_TRIE_HASH)); + when(protocolSchedule.getByBlockNumber(anyLong())).thenReturn(protocolSpec); + when(protocolSpec.getTransactionValidator()).thenReturn(transactionValidator); + genesisBlockGasLimit = genesisBlock.getHeader().getGasLimit(); + + transactionPool = + new TransactionPool(transactions, protocolSchedule, protocolContext, batchAddedListener); + blockchain.observeBlockAdded(transactionPool); + } + + @Test + public void shouldRemoveTransactionsFromPendingListWhenIncludedInBlockOnChain() { + transactions.addRemoteTransaction(transaction1); + assertTransactionPending(transaction1); + appendBlock(transaction1); + + assertTransactionNotPending(transaction1); + } + + @Test + public void shouldRemoveMultipleTransactionsAddedInOneBlock() { + transactions.addRemoteTransaction(transaction1); + transactions.addRemoteTransaction(transaction2); + appendBlock(transaction1, transaction2); + + assertTransactionNotPending(transaction1); + assertTransactionNotPending(transaction2); + assertThat(transactions.size()).isZero(); + } + + @Test + public void shouldIgnoreUnknownTransactionsThatAreAddedInABlock() { + transactions.addRemoteTransaction(transaction1); + appendBlock(transaction1, transaction2); + + assertTransactionNotPending(transaction1); + assertTransactionNotPending(transaction2); + assertThat(transactions.size()).isZero(); + } + + @Test + public void shouldNotRemovePendingTransactionsWhenABlockAddedToAFork() { + transactions.addRemoteTransaction(transaction1); + final BlockHeader commonParent = getHeaderForCurrentChainHead(); + final Block canonicalHead = appendBlock(UInt256.of(1000), commonParent); + appendBlock(UInt256.ONE, commonParent, transaction1); + + verifyChainHeadIs(canonicalHead); + + assertTransactionPending(transaction1); + } + + @Test + public void shouldRemovePendingTransactionsFromAllBlocksOnAForkWhenItBecomesTheCanonicalChain() { + transactions.addRemoteTransaction(transaction1); + transactions.addRemoteTransaction(transaction2); + final BlockHeader commonParent = getHeaderForCurrentChainHead(); + final Block originalChainHead = appendBlock(UInt256.of(1000), commonParent); + + final Block forkBlock1 = appendBlock(UInt256.ONE, commonParent, transaction1); + verifyChainHeadIs(originalChainHead); + + final Block forkBlock2 = appendBlock(UInt256.of(2000), forkBlock1.getHeader(), transaction2); + verifyChainHeadIs(forkBlock2); + + assertTransactionNotPending(transaction1); + assertTransactionNotPending(transaction2); + } + + @Test + public void shouldReaddTransactionsFromThePreviousCanonicalHeadWhenAReorgOccurs() { + givenTransactionIsValid(transaction1); + givenTransactionIsValid(transaction2); + transactions.addRemoteTransaction(transaction1); + transactions.addRemoteTransaction(transaction2); + final BlockHeader commonParent = getHeaderForCurrentChainHead(); + final Block originalFork1 = appendBlock(UInt256.of(1000), commonParent, transaction1); + final Block originalFork2 = appendBlock(UInt256.ONE, originalFork1.getHeader(), transaction2); + assertTransactionNotPending(transaction1); + assertTransactionNotPending(transaction2); + + final Block reorgFork1 = appendBlock(UInt256.ONE, commonParent); + verifyChainHeadIs(originalFork2); + + transactions.addTransactionListener(listener); + final Block reorgFork2 = appendBlock(UInt256.of(2000), reorgFork1.getHeader()); + verifyChainHeadIs(reorgFork2); + + assertTransactionPending(transaction1); + assertTransactionPending(transaction2); + verify(listener).onTransactionAdded(transaction1); + verify(listener).onTransactionAdded(transaction2); + verifyNoMoreInteractions(listener); + } + + @Test + public void shouldNotReaddTransactionsThatAreInBothForksWhenReorgHappens() { + givenTransactionIsValid(transaction1); + givenTransactionIsValid(transaction2); + transactions.addRemoteTransaction(transaction1); + transactions.addRemoteTransaction(transaction2); + final BlockHeader commonParent = getHeaderForCurrentChainHead(); + final Block originalFork1 = appendBlock(UInt256.of(1000), commonParent, transaction1); + final Block originalFork2 = appendBlock(UInt256.ONE, originalFork1.getHeader(), transaction2); + assertTransactionNotPending(transaction1); + assertTransactionNotPending(transaction2); + + final Block reorgFork1 = appendBlock(UInt256.ONE, commonParent, transaction1); + verifyChainHeadIs(originalFork2); + + final Block reorgFork2 = appendBlock(UInt256.of(2000), reorgFork1.getHeader()); + verifyChainHeadIs(reorgFork2); + + assertTransactionNotPending(transaction1); + assertTransactionPending(transaction2); + } + + @Test + public void shouldNotAddRemoteTransactionsThatAreInvalidAccordingToInvariantChecks() { + givenTransactionIsValid(transaction2); + when(transactionValidator.validate(transaction1)) + .thenReturn(ValidationResult.invalid(NONCE_TOO_LOW)); + + transactionPool.addRemoteTransactions(asList(transaction1, transaction2)); + + assertTransactionNotPending(transaction1); + assertTransactionPending(transaction2); + verify(batchAddedListener).onTransactionsAdded(singleton(transaction2)); + } + + @Test + public void shouldNotAddRemoteTransactionsThatAreInvalidAccordingToStateDependentChecks() { + givenTransactionIsValid(transaction2); + when(transactionValidator.validate(transaction1)).thenReturn(valid()); + when(transactionValidator.validateForSender(transaction1, null, OptionalLong.empty())) + .thenReturn(ValidationResult.invalid(NONCE_TOO_LOW)); + + transactionPool.addRemoteTransactions(asList(transaction1, transaction2)); + + assertTransactionNotPending(transaction1); + assertTransactionPending(transaction2); + verify(batchAddedListener).onTransactionsAdded(singleton(transaction2)); + } + + @Test + public void shouldAllowSequenceOfTransactionsWithIncreasingNonceFromSameSender() { + final TransactionTestFixture builder = new TransactionTestFixture(); + final Transaction transaction1 = builder.nonce(1).createTransaction(KEY_PAIR1); + final Transaction transaction2 = builder.nonce(2).createTransaction(KEY_PAIR1); + final Transaction transaction3 = builder.nonce(3).createTransaction(KEY_PAIR1); + + when(transactionValidator.validate(any(Transaction.class))).thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction1), nullable(Account.class), eq(OptionalLong.empty()))) + .thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction2), nullable(Account.class), eq(OptionalLong.of(2)))) + .thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction3), nullable(Account.class), eq(OptionalLong.of(3)))) + .thenReturn(valid()); + + assertThat(transactionPool.addLocalTransaction(transaction1)).isEqualTo(valid()); + assertThat(transactionPool.addLocalTransaction(transaction2)).isEqualTo(valid()); + assertThat(transactionPool.addLocalTransaction(transaction3)).isEqualTo(valid()); + + assertTransactionPending(transaction1); + assertTransactionPending(transaction2); + assertTransactionPending(transaction3); + } + + @Test + public void + shouldAllowSequenceOfTransactionsWithIncreasingNonceFromSameSenderWhenSentInBatchOutOfOrder() { + final TransactionTestFixture builder = new TransactionTestFixture(); + final Transaction transaction1 = builder.nonce(1).createTransaction(KEY_PAIR1); + final Transaction transaction2 = builder.nonce(2).createTransaction(KEY_PAIR1); + final Transaction transaction3 = builder.nonce(3).createTransaction(KEY_PAIR1); + + when(transactionValidator.validate(any(Transaction.class))).thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction1), nullable(Account.class), eq(OptionalLong.empty()))) + .thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction2), nullable(Account.class), eq(OptionalLong.of(2)))) + .thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction3), nullable(Account.class), eq(OptionalLong.of(3)))) + .thenReturn(valid()); + + transactionPool.addRemoteTransactions(asList(transaction3, transaction1, transaction2)); + + assertTransactionPending(transaction1); + assertTransactionPending(transaction2); + assertTransactionPending(transaction3); + } + + @Test + public void shouldNotNotifyBatchListenerWhenRemoteTransactionDoesNotReplaceExisting() { + final TransactionTestFixture builder = new TransactionTestFixture(); + final Transaction transaction1 = + builder.nonce(1).gasPrice(Wei.of(10)).createTransaction(KEY_PAIR1); + final Transaction transaction2 = + builder.nonce(1).gasPrice(Wei.of(5)).createTransaction(KEY_PAIR1); + + when(transactionValidator.validate(any(Transaction.class))).thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction1), nullable(Account.class), eq(OptionalLong.empty()))) + .thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction2), nullable(Account.class), eq(OptionalLong.of(2)))) + .thenReturn(valid()); + + transactionPool.addRemoteTransactions(singletonList(transaction1)); + transactionPool.addRemoteTransactions(singletonList(transaction2)); + + assertTransactionPending(transaction1); + verify(batchAddedListener).onTransactionsAdded(singleton(transaction1)); + verify(batchAddedListener, never()).onTransactionsAdded(singleton(transaction2)); + } + + @Test + public void shouldNotNotifyBatchListenerWhenLocalTransactionDoesNotReplaceExisting() { + final TransactionTestFixture builder = new TransactionTestFixture(); + final Transaction transaction1 = + builder.nonce(1).gasPrice(Wei.of(10)).createTransaction(KEY_PAIR1); + final Transaction transaction2 = + builder.nonce(1).gasPrice(Wei.of(5)).createTransaction(KEY_PAIR1); + + when(transactionValidator.validate(any(Transaction.class))).thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction1), nullable(Account.class), eq(OptionalLong.empty()))) + .thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction2), nullable(Account.class), eq(OptionalLong.of(2)))) + .thenReturn(valid()); + + transactionPool.addLocalTransaction(transaction1); + transactionPool.addLocalTransaction(transaction2); + + assertTransactionPending(transaction1); + verify(batchAddedListener).onTransactionsAdded(singletonList(transaction1)); + verify(batchAddedListener, never()).onTransactionsAdded(singletonList(transaction2)); + } + + @Test + public void shouldRejectLocalTransactionsWhereGasLimitExceedBlockGasLimit() { + final TransactionTestFixture builder = new TransactionTestFixture(); + final Transaction transaction1 = + builder.gasLimit(genesisBlockGasLimit + 1).createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1); + + assertThat(transactionPool.addLocalTransaction(transaction1)) + .isEqualTo(ValidationResult.invalid(EXCEEDS_BLOCK_GAS_LIMIT)); + + assertTransactionNotPending(transaction1); + verifyZeroInteractions(batchAddedListener); + } + + @Test + public void shouldRejectRemoteTransactionsWhereGasLimitExceedBlockGasLimit() { + final TransactionTestFixture builder = new TransactionTestFixture(); + final Transaction transaction1 = + builder.gasLimit(genesisBlockGasLimit + 1).createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1); + + transactionPool.addRemoteTransactions(singleton(transaction1)); + + assertTransactionNotPending(transaction1); + verifyZeroInteractions(batchAddedListener); + } + + @Test + public void shouldNotNotifyBatchListenerIfNoTransactionsAreAdded() { + transactionPool.addRemoteTransactions(emptyList()); + verifyZeroInteractions(batchAddedListener); + } + + private void assertTransactionPending(final Transaction t) { + assertThat(transactions.getTransactionByHash(t.hash())).contains(t); + } + + private void assertTransactionNotPending(final Transaction transaction) { + assertThat(transactions.getTransactionByHash(transaction.hash())).isEmpty(); + } + + private void verifyChainHeadIs(final Block forkBlock2) { + assertThat(blockchain.getChainHeadHash()).isEqualTo(forkBlock2.getHash()); + } + + private void appendBlock(final Transaction... transactionsToAdd) { + appendBlock(UInt256.ONE, getHeaderForCurrentChainHead(), transactionsToAdd); + } + + private BlockHeader getHeaderForCurrentChainHead() { + return blockchain.getBlockHeader(blockchain.getChainHeadHash()).get(); + } + + private Block appendBlock( + final UInt256 difficulty, + final BlockHeader parentBlock, + final Transaction... transactionsToAdd) { + final List transactionList = asList(transactionsToAdd); + final Block block = + new Block( + new BlockHeaderTestFixture() + .difficulty(difficulty) + .parentHash(parentBlock.getHash()) + .number(parentBlock.getNumber() + 1) + .buildHeader(), + new BlockBody(transactionList, emptyList())); + final List transactionReceipts = + transactionList + .stream() + .map(transaction -> new TransactionReceipt(1, 1, emptyList())) + .collect(toList()); + blockchain.appendBlock(block, transactionReceipts); + return block; + } + + private Transaction createTransaction(final int transactionNumber) { + return new TransactionTestFixture() + .nonce(transactionNumber) + .gasLimit(0) + .createTransaction(KEY_PAIR1); + } + + private void givenTransactionIsValid(final Transaction transaction) { + when(transactionValidator.validate(transaction)).thenReturn(valid()); + when(transactionValidator.validateForSender( + eq(transaction), nullable(Account.class), any(OptionalLong.class))) + .thenReturn(valid()); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionReceiptTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionReceiptTest.java new file mode 100755 index 00000000000..68f07c9da4b --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionReceiptTest.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.core; + +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import org.junit.Test; + +public class TransactionReceiptTest { + + @Test + public void toFromRlp() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final TransactionReceipt receipt = gen.receipt(); + final TransactionReceipt copy = + TransactionReceipt.readFrom(RLP.input(RLP.encode(receipt::writeTo))); + assertEquals(receipt, copy); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTest.java new file mode 100755 index 00000000000..8f3574d9abd --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTest.java @@ -0,0 +1,100 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.vm.ReferenceTestProtocolSchedules; +import net.consensys.pantheon.testutil.JsonTestParameters; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collection; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TransactionTest { + + private static final ReferenceTestProtocolSchedules REFERENCE_TEST_PROTOCOL_SCHEDULES = + ReferenceTestProtocolSchedules.create(); + + private static TransactionValidator transactionValidator(final String name) { + return REFERENCE_TEST_PROTOCOL_SCHEDULES + .getByName(name) + .getByBlockNumber(0) + .getTransactionValidator(); + } + + private final TransactionTestCaseSpec spec; + + private static final String TEST_CONFIG_FILE_DIR_PATH = "TransactionTests/"; + + @Parameters(name = "Name: {0}") + public static Collection getTestParametersForConfig() { + return JsonTestParameters.create(TransactionTestCaseSpec.class) + // Blacklist tests that expect transactions with large gasLimits to properly decode + .blacklist( + "TransactionWithGasLimitOverflow(2|63)", "TransactionWithGasLimitxPriceOverflow$") + // Nonce is tracked with type long, large valued nonces can't currently be decoded + .blacklist("TransactionWithHighNonce256") + .generator((name, spec, collector) -> collector.add(name, spec)) + .generate(TEST_CONFIG_FILE_DIR_PATH); + } + + public TransactionTest(final String name, final TransactionTestCaseSpec spec) { + this.spec = spec; + } + + @Test + public void frontier() { + milestone("Frontier"); + } + + @Test + public void homestead() { + milestone("Homestead"); + } + + @Test + public void eIP150() { + milestone("EIP150"); + } + + @Test + public void eIP158() { + milestone("EIP158"); + } + + @Test + public void byzantium() { + milestone("Byzantium"); + } + + public void milestone(final String milestone) { + + final TransactionTestCaseSpec.Expectation expected = spec.expectation(milestone); + + try { + final BytesValue rlp = spec.getRlp(); + + // Test transaction deserialization (will throw an exception if it fails). + final Transaction transaction = Transaction.readFrom(RLP.input(rlp)); + if (!transactionValidator(milestone).validate(transaction).isValid()) { + throw new RuntimeException(String.format("Transaction is invalid %s", transaction)); + } + + // Test rlp encoding + final BytesValue actualRlp = RLP.encode(transaction::writeTo); + Assert.assertTrue(expected.isSucceeds()); + + Assert.assertEquals(rlp, actualRlp); + + Assert.assertEquals(expected.getSender(), transaction.getSender()); + Assert.assertEquals(expected.getHash(), transaction.hash()); + } catch (final Exception e) { + Assert.assertFalse(expected.isSucceeds()); + } + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTestCaseSpec.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTestCaseSpec.java new file mode 100755 index 00000000000..513c4b98882 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/core/TransactionTestCaseSpec.java @@ -0,0 +1,92 @@ +package net.consensys.pantheon.ethereum.core; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.HashMap; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** A Transaction test case specification. */ +@JsonIgnoreProperties({"_info"}) +public class TransactionTestCaseSpec { + + public static class Expectation { + + private final Hash hash; + + private final Address sender; + + private final boolean succeeds; + + Expectation( + @JsonProperty("hash") final String hash, @JsonProperty("sender") final String sender) { + this.succeeds = hash != null && sender != null; + if (succeeds) { + this.hash = Hash.fromHexString(hash); + this.sender = Address.fromHexString(sender); + } else { + this.hash = null; + this.sender = null; + } + } + + public boolean isSucceeds() { + return this.succeeds; + } + + public Hash getHash() { + return this.hash; + } + + public Address getSender() { + return this.sender; + } + } + + private final HashMap expectations; + + private final BytesValue rlp; + + @JsonCreator + public TransactionTestCaseSpec( + @JsonProperty("Frontier") final Expectation frontierExpectation, + @JsonProperty("Homestead") final Expectation homesteadExpectation, + @JsonProperty("EIP150") final Expectation EIP150Expectation, + @JsonProperty("EIP158") final Expectation EIP158Expectation, + @JsonProperty("Byzantium") final Expectation byzantiumExpectation, + @JsonProperty("Constantinople") final Expectation constantinopleExpectation, + @JsonProperty("rlp") final String rlp) { + expectations = new HashMap<>(); + expectations.put("Frontier", frontierExpectation); + expectations.put("Homestead", homesteadExpectation); + expectations.put("EIP150", EIP150Expectation); + expectations.put("EIP158", EIP158Expectation); + expectations.put("Byzantium", byzantiumExpectation); + expectations.put("Constantinople", constantinopleExpectation); + + BytesValue parsedRlp = null; + try { + parsedRlp = BytesValue.fromHexString(rlp); + } catch (final IllegalArgumentException e) { + // Some test cases include rlp "hex strings" with invalid characters + // In this case, just set rlp to null + } + this.rlp = parsedRlp; + } + + public BytesValue getRlp() { + return rlp; + } + + public Expectation expectation(final String milestone) { + final Expectation expectation = expectations.get(milestone); + + if (expectation == null) { + throw new IllegalStateException("Expectation for milestone %s not found" + milestone); + } + + return expectation; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchainTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchainTest.java new file mode 100755 index 00000000000..dd96899e1ac --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/db/DefaultMutableBlockchainTest.java @@ -0,0 +1,715 @@ +package net.consensys.pantheon.ethereum.db; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator.BlockOptions; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import org.junit.Test; + +public class DefaultMutableBlockchainTest { + + @Test + public void initializeNew() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + assertBlockDataIsStored(blockchain, genesisBlock, Collections.emptyList()); + assertBlockIsHead(blockchain, genesisBlock); + assertTotalDifficultiesAreConsistent(blockchain, genesisBlock); + assertThat(blockchain.getForks()).isEmpty(); + } + + @Test + public void initializeExisting() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + // Write to kv store + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + // Initialize a new blockchain store with kvStore that already contains data + blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + assertBlockDataIsStored(blockchain, genesisBlock, Collections.emptyList()); + assertBlockIsHead(blockchain, genesisBlock); + assertTotalDifficultiesAreConsistent(blockchain, genesisBlock); + assertThat(blockchain.getForks()).isEmpty(); + } + + @Test(expected = IllegalArgumentException.class) + public void initializeExistingWithWrongGenesisBlock() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + // Write to kv store + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + // Initialize a new blockchain store with same kvStore, but different genesis block + new DefaultMutableBlockchain(gen.genesisBlock(), kvStore, MainnetBlockHashFunction::createHash); + } + + @Test + public void appendBlock() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final BlockOptions options = + new BlockOptions().setBlockNumber(1L).setParentHash(genesisBlock.getHash()); + final Block newBlock = gen.block(options); + final List receipts = gen.receipts(newBlock); + blockchain.appendBlock(newBlock, receipts); + + assertBlockIsHead(blockchain, newBlock); + assertTotalDifficultiesAreConsistent(blockchain, newBlock); + assertThat(blockchain.getForks()).isEmpty(); + } + + @Test(expected = IllegalArgumentException.class) + public void appendUnconnectedBlock() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final BlockOptions options = new BlockOptions().setBlockNumber(1L).setParentHash(Hash.ZERO); + final Block newBlock = gen.block(options); + final List receipts = gen.receipts(newBlock); + blockchain.appendBlock(newBlock, receipts); + } + + @Test(expected = IllegalArgumentException.class) + public void appendBlockWithMismatchedReceipts() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final BlockOptions options = + new BlockOptions().setBlockNumber(1L).setParentHash(genesisBlock.getHash()); + final Block newBlock = gen.block(options); + final List receipts = gen.receipts(newBlock); + receipts.add(gen.receipt()); + blockchain.appendBlock(newBlock, receipts); + } + + @Test + public void createSmallChain() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final List chain = gen.blockSequence(3); + final List> blockReceipts = + chain.stream().map(gen::receipts).collect(Collectors.toList()); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(chain.get(0), kvStore, MainnetBlockHashFunction::createHash); + for (int i = 1; i < chain.size(); i++) { + blockchain.appendBlock(chain.get(i), blockReceipts.get(i)); + } + + for (int i = 1; i < chain.size(); i++) { + assertBlockDataIsStored(blockchain, chain.get(i), blockReceipts.get(i)); + } + + final Block head = chain.get(chain.size() - 1); + assertBlockIsHead(blockchain, head); + assertTotalDifficultiesAreConsistent(blockchain, head); + assertThat(blockchain.getForks()).isEmpty(); + } + + @Test + public void appendBlockWithReorgToChainAtEqualHeight() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + + // Setup an initial blockchain + final int chainLength = 3; + final List chain = gen.blockSequence(chainLength); + final List> blockReceipts = + chain.stream().map(gen::receipts).collect(Collectors.toList()); + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(chain.get(0), kvStore, MainnetBlockHashFunction::createHash); + for (int i = 1; i < chain.size(); i++) { + blockchain.appendBlock(chain.get(i), blockReceipts.get(i)); + } + assertThat(blockchain.getForks()).isEmpty(); + final Block originalHead = chain.get(chainLength - 1); + + // Create parallel fork of length 1 + final int forkBlock = 2; + final int commonAncestor = 1; + final BlockOptions options = + new BlockOptions() + .setParentHash(chain.get(commonAncestor).getHash()) + .setBlockNumber(forkBlock) + .setDifficulty(chain.get(forkBlock).getHeader().getDifficulty().plus(10L)); + final Block fork = gen.block(options); + final List forkReceipts = gen.receipts(fork); + final List reorgedChain = new ArrayList<>(chain.subList(0, forkBlock)); + reorgedChain.add(fork); + final List> reorgedReceipts = + new ArrayList<>(blockReceipts.subList(0, forkBlock)); + reorgedReceipts.add(forkReceipts); + + // Add fork + blockchain.appendBlock(fork, forkReceipts); + + // Check chain has reorganized + for (int i = 0; i < reorgedChain.size(); i++) { + assertBlockDataIsStored(blockchain, reorgedChain.get(i), reorgedReceipts.get(i)); + } + // Check old transactions have been removed + for (final Transaction tx : originalHead.getBody().getTransactions()) { + assertThat(blockchain.getTransactionByHash(tx.hash())).isNotPresent(); + } + + assertBlockIsHead(blockchain, fork); + assertTotalDifficultiesAreConsistent(blockchain, fork); + // Old chain head should now be tracked as a fork. + final Set forks = blockchain.getForks(); + assertThat(forks.size()).isEqualTo(1); + assertThat(forks.stream().anyMatch(f -> f.equals(originalHead.getHash()))).isTrue(); + // Old chain should not be on canonical chain. + for (int i = commonAncestor + 1; i < chainLength; i++) { + assertThat(blockchain.blockIsOnCanonicalChain(chain.get(i).getHash())).isFalse(); + } + } + + @Test + public void appendBlockWithReorgToShorterChain() { + final BlockDataGenerator gen = new BlockDataGenerator(2); + + // Setup an initial blockchain + final int originalChainLength = 4; + final List chain = gen.blockSequence(originalChainLength); + final List> blockReceipts = + chain.stream().map(gen::receipts).collect(Collectors.toList()); + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(chain.get(0), kvStore, MainnetBlockHashFunction::createHash); + for (int i = 1; i < chain.size(); i++) { + blockchain.appendBlock(chain.get(i), blockReceipts.get(i)); + } + final Block originalHead = chain.get(originalChainLength - 1); + + // Create parallel fork of length 2 from 3 blocks back + final List forkBlocks = new ArrayList<>(); + final int forkStart = 1; + final int commonAncestor = 0; + // Generate first block + BlockOptions options = + new BlockOptions() + .setParentHash(chain.get(commonAncestor).getHash()) + .setBlockNumber(forkStart) + .setDifficulty(chain.get(forkStart).getHeader().getDifficulty().minus(5L)); + forkBlocks.add(gen.block(options)); + // Generate second block + final UInt256 remainingDifficultyToOutpace = + chain + .get(forkStart + 1) + .getHeader() + .getDifficulty() + .plus(chain.get(forkStart + 2).getHeader().getDifficulty()); + options = + new BlockOptions() + .setParentHash(forkBlocks.get(0).getHash()) + .setBlockNumber(forkStart + 1) + .setDifficulty(remainingDifficultyToOutpace.plus(10L)); + forkBlocks.add(gen.block(options)); + // Generate corresponding receipts + final List> forkReceipts = + forkBlocks.stream().map(gen::receipts).collect(Collectors.toList()); + + // Collect fork data + final List reorgedChain = new ArrayList<>(chain.subList(0, forkStart)); + reorgedChain.addAll(forkBlocks); + final List> reorgedReceipts = + new ArrayList<>(blockReceipts.subList(0, forkStart)); + reorgedReceipts.addAll(forkReceipts); + + // Add first block in fork, which should not cause a reorg + blockchain.appendBlock(forkBlocks.get(0), forkReceipts.get(0)); + // Check chain has not reorganized + for (int i = 0; i < chain.size(); i++) { + assertBlockDataIsStored(blockchain, chain.get(i), blockReceipts.get(i)); + } + assertBlockIsHead(blockchain, originalHead); + assertTotalDifficultiesAreConsistent(blockchain, originalHead); + // Check transactions were not indexed + for (final Transaction tx : forkBlocks.get(0).getBody().getTransactions()) { + assertThat(blockchain.getTransactionByHash(tx.hash())).isNotPresent(); + } + // Appended block should be tracked as a fork + assertThat(blockchain.blockIsOnCanonicalChain(forkBlocks.get(0).getHash())).isFalse(); + Set forks = blockchain.getForks(); + assertThat(forks.size()).isEqualTo(1); + assertThat(forks.stream().anyMatch(f -> f.equals(forkBlocks.get(0).getHash()))).isTrue(); + + // Add second block in fork, which should cause a reorg + blockchain.appendBlock(forkBlocks.get(1), forkReceipts.get(1)); + // Check chain has reorganized + for (int i = 0; i < reorgedChain.size(); i++) { + assertBlockDataIsStored(blockchain, reorgedChain.get(i), reorgedReceipts.get(i)); + } + assertBlockIsHead(blockchain, forkBlocks.get(1)); + assertTotalDifficultiesAreConsistent(blockchain, forkBlocks.get(1)); + // Check old transactions have been removed + final List removedTransactions = new ArrayList<>(); + for (int i = forkStart; i < originalChainLength; i++) { + removedTransactions.addAll(chain.get(i).getBody().getTransactions()); + } + for (final Transaction tx : removedTransactions) { + assertThat(blockchain.getTransactionByHash(tx.hash())).isNotPresent(); + } + + // Check that blockNumber index for previous chain head has been removed + assertThat(blockchain.getBlockHashByNumber(originalChainLength - 1)).isNotPresent(); + // Old chain head should now be tracked as a fork. + forks = blockchain.getForks(); + assertThat(forks.size()).isEqualTo(1); + assertThat(forks.stream().anyMatch(f -> f.equals(originalHead.getHash()))).isTrue(); + // Old chain should not be on canonical chain. + for (int i = commonAncestor + 1; i < originalChainLength; i++) { + assertThat(blockchain.blockIsOnCanonicalChain(chain.get(i).getHash())).isFalse(); + } + } + + @Test + public void appendBlockWithReorgToLongerChain() { + final BlockDataGenerator gen = new BlockDataGenerator(2); + + // Setup an initial blockchain + final int originalChainLength = 4; + final List chain = gen.blockSequence(originalChainLength); + final List> blockReceipts = + chain.stream().map(gen::receipts).collect(Collectors.toList()); + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(chain.get(0), kvStore, MainnetBlockHashFunction::createHash); + for (int i = 1; i < chain.size(); i++) { + blockchain.appendBlock(chain.get(i), blockReceipts.get(i)); + } + final Block originalHead = chain.get(originalChainLength - 1); + + // Create parallel fork of length 2 from 3 blocks back + final List forkBlocks = new ArrayList<>(); + final int forkStart = 3; + final int commonAncestor = 2; + // Generate first block + BlockOptions options = + new BlockOptions() + .setParentHash(chain.get(commonAncestor).getHash()) + .setBlockNumber(forkStart) + .setDifficulty(chain.get(forkStart).getHeader().getDifficulty().minus(5L)); + forkBlocks.add(gen.block(options)); + // Generate second block + options = + new BlockOptions() + .setParentHash(forkBlocks.get(0).getHash()) + .setBlockNumber(forkStart + 1) + .setDifficulty(UInt256.of(10L)); + forkBlocks.add(gen.block(options)); + // Generate corresponding receipts + final List> forkReceipts = + forkBlocks.stream().map(gen::receipts).collect(Collectors.toList()); + + // Collect fork data + final List reorgedChain = new ArrayList<>(chain.subList(0, forkStart)); + reorgedChain.addAll(forkBlocks); + final List> reorgedReceipts = + new ArrayList<>(blockReceipts.subList(0, forkStart)); + reorgedReceipts.addAll(forkReceipts); + + // Add first block in fork, which should not cause a reorg + blockchain.appendBlock(forkBlocks.get(0), forkReceipts.get(0)); + // Check chain has not reorganized + for (int i = 0; i < chain.size(); i++) { + assertBlockDataIsStored(blockchain, chain.get(i), blockReceipts.get(i)); + } + assertBlockIsHead(blockchain, originalHead); + assertTotalDifficultiesAreConsistent(blockchain, originalHead); + // Check transactions were not indexed + for (final Transaction tx : forkBlocks.get(0).getBody().getTransactions()) { + assertThat(blockchain.getTransactionByHash(tx.hash())).isNotPresent(); + } + // Appended block should be tracked as a fork + assertThat(blockchain.blockIsOnCanonicalChain(forkBlocks.get(0).getHash())).isFalse(); + Set forks = blockchain.getForks(); + assertThat(forks.size()).isEqualTo(1); + assertThat(forks.stream().anyMatch(f -> f.equals(forkBlocks.get(0).getHash()))).isTrue(); + + // Add second block in fork, which should cause a reorg + blockchain.appendBlock(forkBlocks.get(1), forkReceipts.get(1)); + // Check chain has reorganized + for (int i = 0; i < reorgedChain.size(); i++) { + assertBlockDataIsStored(blockchain, reorgedChain.get(i), reorgedReceipts.get(i)); + } + assertBlockIsHead(blockchain, forkBlocks.get(1)); + assertTotalDifficultiesAreConsistent(blockchain, forkBlocks.get(1)); + // Check old transactions have been removed + final List removedTransactions = new ArrayList<>(); + for (int i = forkStart; i < originalChainLength; i++) { + removedTransactions.addAll(chain.get(i).getBody().getTransactions()); + } + for (final Transaction tx : removedTransactions) { + assertThat(blockchain.getTransactionByHash(tx.hash())).isNotPresent(); + } + // Old chain head should now be tracked as a fork. + forks = blockchain.getForks(); + assertThat(forks.size()).isEqualTo(1); + assertThat(forks.stream().anyMatch(f -> f.equals(originalHead.getHash()))).isTrue(); + // Old chain should not be on canonical chain. + for (int i = commonAncestor + 1; i < originalChainLength; i++) { + assertThat(blockchain.blockIsOnCanonicalChain(chain.get(i).getHash())).isFalse(); + } + } + + @Test + public void reorgWithOverlappingTransactions() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + + // Setup an initial blockchain + final int chainLength = 3; + final List chain = gen.blockSequence(chainLength); + final List> blockReceipts = + chain.stream().map(gen::receipts).collect(Collectors.toList()); + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(chain.get(0), kvStore, MainnetBlockHashFunction::createHash); + for (int i = 1; i < chain.size(); i++) { + blockchain.appendBlock(chain.get(i), blockReceipts.get(i)); + } + final Transaction overlappingTx = chain.get(chainLength - 1).getBody().getTransactions().get(0); + + // Create parallel fork of length 1 + final int forkBlock = 2; + final int commonAncestor = 1; + final BlockOptions options = + new BlockOptions() + .setParentHash(chain.get(commonAncestor).getHash()) + .setBlockNumber(forkBlock) + .setDifficulty(chain.get(forkBlock).getHeader().getDifficulty().plus(10L)) + .addTransaction(overlappingTx) + .addTransaction(gen.transaction()); + final Block fork = gen.block(options); + final List forkReceipts = gen.receipts(fork); + final List reorgedChain = new ArrayList<>(chain.subList(0, forkBlock)); + reorgedChain.add(fork); + final List> reorgedReceipts = + new ArrayList<>(blockReceipts.subList(0, forkBlock)); + reorgedReceipts.add(forkReceipts); + + // Add fork + blockchain.appendBlock(fork, forkReceipts); + + // Check chain has reorganized + for (int i = 0; i < reorgedChain.size(); i++) { + assertBlockDataIsStored(blockchain, reorgedChain.get(i), reorgedReceipts.get(i)); + } + + // Check old transactions have been removed + for (final Transaction tx : chain.get(chainLength - 1).getBody().getTransactions()) { + final Optional actualTransaction = blockchain.getTransactionByHash(tx.hash()); + if (tx.equals(overlappingTx)) { + assertThat(actualTransaction).isPresent(); + } else { + assertThat(actualTransaction).isNotPresent(); + } + } + } + + @Test + public void appendBlockForFork() { + final BlockDataGenerator gen = new BlockDataGenerator(2); + + // Setup an initial blockchain + final int originalChainLength = 4; + final List chain = gen.blockSequence(originalChainLength); + final List> blockReceipts = + chain.stream().map(gen::receipts).collect(Collectors.toList()); + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(chain.get(0), kvStore, MainnetBlockHashFunction::createHash); + for (int i = 1; i < chain.size(); i++) { + blockchain.appendBlock(chain.get(i), blockReceipts.get(i)); + } + final Block originalHead = chain.get(originalChainLength - 1); + + // Create fork of length 2 + final List forkBlocks = new ArrayList<>(); + final int forkStart = 2; + final int commonAncestor = 1; + // Generate first block + BlockOptions options = + new BlockOptions() + .setParentHash(chain.get(commonAncestor).getHash()) + .setBlockNumber(forkStart) + .setDifficulty(chain.get(forkStart).getHeader().getDifficulty().minus(5L)); + forkBlocks.add(gen.block(options)); + // Generate second block + options = + new BlockOptions() + .setParentHash(forkBlocks.get(0).getHash()) + .setBlockNumber(forkStart + 1) + .setDifficulty(chain.get(forkStart + 1).getHeader().getDifficulty().minus(5L)); + forkBlocks.add(gen.block(options)); + // Generate corresponding receipts + final List> forkReceipts = + forkBlocks.stream().map(gen::receipts).collect(Collectors.toList()); + + // Add fork blocks, which should not cause a reorg + for (int i = 0; i < forkBlocks.size(); i++) { + final Block forkBlock = forkBlocks.get(i); + blockchain.appendBlock(forkBlock, forkReceipts.get(i)); + // Check chain has not reorganized + for (int j = 0; j < chain.size(); j++) { + assertBlockDataIsStored(blockchain, chain.get(j), blockReceipts.get(j)); + } + assertBlockIsHead(blockchain, originalHead); + assertTotalDifficultiesAreConsistent(blockchain, originalHead); + // Check transactions were not indexed + for (final Transaction tx : forkBlock.getBody().getTransactions()) { + assertThat(blockchain.getTransactionByHash(tx.hash())).isNotPresent(); + } + // Appended block should be tracked as a fork + assertThat(blockchain.blockIsOnCanonicalChain(forkBlock.getHash())).isFalse(); + final Set forks = blockchain.getForks(); + assertThat(forks.size()).isEqualTo(1); + final Optional trackedFork = + forks.stream().filter(f -> f.equals(forkBlock.getHash())).findAny(); + assertThat(trackedFork).isPresent(); + } + + // Add another independent fork + options = + new BlockOptions() + .setParentHash(chain.get(commonAncestor).getHash()) + .setBlockNumber(forkStart) + .setDifficulty(chain.get(forkStart).getHeader().getDifficulty().minus(5L)); + final Block secondFork = gen.block(options); + blockchain.appendBlock(secondFork, gen.receipts(secondFork)); + + // We should now be tracking 2 forks + assertThat(blockchain.blockIsOnCanonicalChain(secondFork.getHash())).isFalse(); + final Set forks = blockchain.getForks(); + assertThat(forks.size()).isEqualTo(2); + final Optional trackedFork = + forks.stream().filter(f -> f.equals(secondFork.getHash())).findAny(); + assertThat(trackedFork).isPresent(); + + // Head should not have changed + assertBlockIsHead(blockchain, originalHead); + } + + @Test + public void blockAddedObserver_removeNonexistentObserver() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + assertThat(blockchain.removeObserver(7)).isFalse(); + } + + @Test + public void blockAddedObserver_addRemoveSingle() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final long observerId = blockchain.observeBlockAdded((block, chain) -> {}); + assertThat(blockchain.observerCount()).isEqualTo(1); + + assertThat(blockchain.removeObserver(observerId)).isTrue(); + assertThat(blockchain.observerCount()).isEqualTo(0); + } + + @Test(expected = NullPointerException.class) + public void blockAddedObserver_nullObserver() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + blockchain.observeBlockAdded(null); + } + + @Test + public void blockAddedObserver_addRemoveMultiple() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final long observerId1 = blockchain.observeBlockAdded((block, chain) -> {}); + assertThat(blockchain.observerCount()).isEqualTo(1); + + final long observerId2 = blockchain.observeBlockAdded((block, chain) -> {}); + assertThat(blockchain.observerCount()).isEqualTo(2); + + final long observerId3 = blockchain.observeBlockAdded((block, chain) -> {}); + assertThat(blockchain.observerCount()).isEqualTo(3); + + assertThat(blockchain.removeObserver(observerId1)).isTrue(); + assertThat(blockchain.observerCount()).isEqualTo(2); + + assertThat(blockchain.removeObserver(observerId2)).isTrue(); + assertThat(blockchain.observerCount()).isEqualTo(1); + + assertThat(blockchain.removeObserver(observerId3)).isTrue(); + assertThat(blockchain.observerCount()).isEqualTo(0); + } + + @Test + public void blockAddedObserver_invokedSingle() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final BlockOptions options = + new BlockOptions().setBlockNumber(1L).setParentHash(genesisBlock.getHash()); + final Block newBlock = gen.block(options); + final List receipts = gen.receipts(newBlock); + + final AtomicBoolean observerInvoked = new AtomicBoolean(false); + blockchain.observeBlockAdded( + (block, chain) -> { + observerInvoked.set(true); + }); + + blockchain.appendBlock(newBlock, receipts); + + assertThat(observerInvoked.get()).isTrue(); + } + + @Test + public void blockAddedObserver_invokedMultiple() { + final BlockDataGenerator gen = new BlockDataGenerator(); + + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final BlockOptions options = + new BlockOptions().setBlockNumber(1L).setParentHash(genesisBlock.getHash()); + final Block newBlock = gen.block(options); + final List receipts = gen.receipts(newBlock); + + final AtomicBoolean observer1Invoked = new AtomicBoolean(false); + blockchain.observeBlockAdded( + (block, chain) -> { + observer1Invoked.set(true); + }); + + final AtomicBoolean observer2Invoked = new AtomicBoolean(false); + blockchain.observeBlockAdded( + (block, chain) -> { + observer2Invoked.set(true); + }); + + final AtomicBoolean observer3Invoked = new AtomicBoolean(false); + blockchain.observeBlockAdded( + (block, chain) -> { + observer3Invoked.set(true); + }); + + blockchain.appendBlock(newBlock, receipts); + + assertThat(observer1Invoked.get()).isTrue(); + assertThat(observer2Invoked.get()).isTrue(); + assertThat(observer3Invoked.get()).isTrue(); + } + + /* + * Check that block header, block body, block number, transaction locations, and receipts for this + * block are all stored. + */ + private void assertBlockDataIsStored( + final Blockchain blockchain, final Block block, final List receipts) { + final Hash hash = block.getHash(); + assertEquals(hash, blockchain.getBlockHashByNumber(block.getHeader().getNumber()).get()); + assertEquals(block.getHeader(), blockchain.getBlockHeader(block.getHeader().getNumber()).get()); + assertEquals(block.getHeader(), blockchain.getBlockHeader(hash).get()); + assertEquals(block.getBody(), blockchain.getBlockBody(hash).get()); + assertThat(blockchain.blockIsOnCanonicalChain(block.getHash())).isTrue(); + + final List txs = block.getBody().getTransactions(); + for (int i = 0; i < txs.size(); i++) { + final Transaction expected = txs.get(i); + final Transaction actual = blockchain.getTransactionByHash(expected.hash()).get(); + assertEquals(expected, actual); + } + final List actualReceipts = blockchain.getTxReceipts(hash).get(); + assertEquals(receipts, actualReceipts); + } + + private void assertBlockIsHead(final Blockchain blockchain, final Block head) { + assertEquals(head.getHash(), blockchain.getChainHeadHash()); + assertEquals(head.getHeader().getNumber(), blockchain.getChainHeadBlockNumber()); + assertEquals(head.getHash(), blockchain.getChainHead().getHash()); + } + + private void assertTotalDifficultiesAreConsistent(final Blockchain blockchain, final Block head) { + // Check that total difficulties are summed correctly + long num = BlockHeader.GENESIS_BLOCK_NUMBER; + UInt256 td = UInt256.of(0); + while (num <= head.getHeader().getNumber()) { + final Hash curHash = blockchain.getBlockHashByNumber(num).get(); + final BlockHeader curHead = blockchain.getBlockHeader(curHash).get(); + td = td.plus(curHead.getDifficulty()); + assertEquals(td, blockchain.getTotalDifficultyByHash(curHash).get()); + + num += 1; + } + + // Check reported chainhead td + assertEquals(td, blockchain.getChainHead().getTotalDifficulty()); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolScheduleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolScheduleTest.java new file mode 100755 index 00000000000..155e9f58948 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/development/DevelopmentProtocolScheduleTest.java @@ -0,0 +1,45 @@ +package net.consensys.pantheon.ethereum.development; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +import io.vertx.core.json.JsonObject; +import org.junit.Test; + +public class DevelopmentProtocolScheduleTest { + + @Test + public void reportedDifficultyForAllBlocksIsAFixedValue() { + + final JsonObject config = new JsonObject(); + final ProtocolSchedule schedule = DevelopmentProtocolSchedule.create(config); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + final BlockHeader parentHeader = headerBuilder.number(1).buildHeader(); + + assertThat( + schedule + .getByBlockNumber(0) + .getDifficultyCalculator() + .nextDifficulty(1, parentHeader, null)) + .isEqualTo(DevelopmentDifficultyCalculators.MINIMUM_DIFFICULTY); + + assertThat( + schedule + .getByBlockNumber(500) + .getDifficultyCalculator() + .nextDifficulty(1, parentHeader, null)) + .isEqualTo(DevelopmentDifficultyCalculators.MINIMUM_DIFFICULTY); + + assertThat( + schedule + .getByBlockNumber(500_000) + .getDifficultyCalculator() + .nextDifficulty(1, parentHeader, null)) + .isEqualTo(DevelopmentDifficultyCalculators.MINIMUM_DIFFICULTY); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidatorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidatorTest.java new file mode 100755 index 00000000000..1a8e0667ebe --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BlockHeaderValidatorTest.java @@ -0,0 +1,257 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator.Builder; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +public class BlockHeaderValidatorTest { + + @SuppressWarnings("unchecked") + private final ProtocolContext protocolContext = mock(ProtocolContext.class); + + private final MutableBlockchain blockchain = mock(MutableBlockchain.class); + private final BlockDataGenerator generator = new BlockDataGenerator(); + + @Before + public void setUp() { + when(protocolContext.getBlockchain()).thenReturn(blockchain); + } + + @SuppressWarnings("unchecked") + private AttachedBlockHeaderValidationRule createFailingAttachedRule() { + final AttachedBlockHeaderValidationRule rule = + mock(AttachedBlockHeaderValidationRule.class); + when(rule.validate(notNull(), notNull(), eq(protocolContext))).thenReturn(false); + return rule; + } + + @SuppressWarnings("unchecked") + private AttachedBlockHeaderValidationRule createPassingAttachedRule() { + final AttachedBlockHeaderValidationRule rule = + mock(AttachedBlockHeaderValidationRule.class); + when(rule.validate(notNull(), notNull(), eq(protocolContext))).thenReturn(true); + return rule; + } + + @Test + public void validateHeader() { + final AttachedBlockHeaderValidationRule passing1 = createPassingAttachedRule(); + final AttachedBlockHeaderValidationRule passing2 = createPassingAttachedRule(); + + final BlockHeader blockHeader = generator.header(); + + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder().addRule(passing1).addRule(passing2).build(); + + assertThat( + validator.validateHeader( + blockHeader, blockHeader, protocolContext, HeaderValidationMode.FULL)) + .isTrue(); + verify(passing1).validate(blockHeader, blockHeader, protocolContext); + verify(passing2).validate(blockHeader, blockHeader, protocolContext); + } + + @Test + public void validateHeaderFailingAttachedRule() { + final AttachedBlockHeaderValidationRule passing1 = createPassingAttachedRule(); + final AttachedBlockHeaderValidationRule failing1 = createFailingAttachedRule(); + final AttachedBlockHeaderValidationRule passing2 = createPassingAttachedRule(); + + final BlockHeader blockHeader = generator.header(); + + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder() + .addRule(passing1) + .addRule(failing1) + .addRule(passing2) + .build(); + + assertThat( + validator.validateHeader( + blockHeader, blockHeader, protocolContext, HeaderValidationMode.FULL)) + .isFalse(); + verify(passing1).validate(blockHeader, blockHeader, protocolContext); + verify(failing1).validate(blockHeader, blockHeader, protocolContext); + verify(passing2, never()).validate(blockHeader, blockHeader, protocolContext); + } + + @Test + public void validateHeaderFailingDettachedRule() { + final DetachedBlockHeaderValidationRule passing1 = createPassingDetachedRule(true); + final DetachedBlockHeaderValidationRule failing1 = createFailingDetachedRule(true); + final AttachedBlockHeaderValidationRule passing2 = createPassingAttachedRule(); + + final BlockHeader blockHeader = generator.header(); + + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder() + .addRule(passing1) + .addRule(failing1) + .addRule(passing2) + .build(); + + assertThat( + validator.validateHeader( + blockHeader, blockHeader, protocolContext, HeaderValidationMode.FULL)) + .isFalse(); + verify(passing1).validate(blockHeader, blockHeader); + verify(failing1).validate(blockHeader, blockHeader); + verify(passing2, never()).validate(blockHeader, blockHeader, protocolContext); + } + + @Test + public void validateHeaderChain() { + final BlockHeader blockHeader = generator.header(); + + when(blockchain.getBlockHeader(blockHeader.getParentHash())) + .thenReturn(Optional.of(blockHeader)); + + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder().addRule(createPassingAttachedRule()).build(); + + assertThat(validator.validateHeader(blockHeader, protocolContext, HeaderValidationMode.FULL)) + .isTrue(); + } + + @Test + public void validateHeaderChainFailsWhenParentNotAvailable() { + final BlockHeader blockHeader = generator.header(); + + when(blockchain.getBlockHeader(blockHeader.getNumber() - 1)).thenReturn(Optional.empty()); + + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder().addRule(createPassingAttachedRule()).build(); + + assertThat(validator.validateHeader(blockHeader, protocolContext, HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void validateHeaderLightChainFailsWhenParentNotAvailable() { + final BlockHeader blockHeader = generator.header(); + + when(blockchain.getBlockHeader(blockHeader.getParentHash())).thenReturn(Optional.empty()); + + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder().addRule(createPassingAttachedRule()).build(); + + assertThat(validator.validateHeader(blockHeader, protocolContext, HeaderValidationMode.LIGHT)) + .isFalse(); + } + + @Test + public void shouldSkipAdditionalValidationRulesWhenDoingLightValidation() { + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder() + .addRule(createPassingDetachedRule(true)) + .addRule(createFailingDetachedRule(false)) + .build(); + + final BlockHeader header = generator.header(); + final BlockHeader parentHeader = generator.header(); + when(blockchain.getBlockHeader(header.getParentHash())).thenReturn(Optional.of(parentHeader)); + + assertThat(validator.validateHeader(header, protocolContext, HeaderValidationMode.LIGHT)) + .isTrue(); + } + + @Test + public void shouldPerformAdditionalValidationRulesWhenDoingFullValidation() { + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder() + .addRule(createPassingDetachedRule(true)) + .addRule(createFailingDetachedRule(false)) + .build(); + + assertThat( + validator.validateHeader( + generator.header(), generator.header(), protocolContext, HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void shouldStillPerformLightValidationRulesWhenDoingFullValidation() { + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder() + .addRule(createPassingDetachedRule(true)) + .addRule(createFailingDetachedRule(false)) + .build(); + + assertThat( + validator.validateHeader( + generator.header(), generator.header(), protocolContext, HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void shouldPerformAttachedValidationRulesWhenDoingLightValidation() { + final BlockHeaderValidator validator = + new BlockHeaderValidator.Builder() + .addRule(createFailingAttachedRule()) + .addRule(createPassingDetachedRule(true)) + .build(); + + assertThat( + validator.validateHeader( + generator.header(), generator.header(), protocolContext, HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void shouldRunRulesInOrderOfAdditionDuringFullValidation() { + final AttachedBlockHeaderValidationRule rule1 = createPassingAttachedRule(); + final DetachedBlockHeaderValidationRule rule2 = createPassingDetachedRule(true); + final DetachedBlockHeaderValidationRule rule3 = createPassingDetachedRule(false); + final AttachedBlockHeaderValidationRule rule4 = createPassingAttachedRule(); + + final BlockHeaderValidator validator = + new Builder().addRule(rule1).addRule(rule2).addRule(rule3).addRule(rule4).build(); + + final BlockHeader header = generator.header(); + final BlockHeader parent = generator.header(); + assertThat(validator.validateHeader(header, parent, protocolContext, HeaderValidationMode.FULL)) + .isTrue(); + + final InOrder inOrder = inOrder(rule1, rule2, rule3, rule4); + inOrder.verify(rule1).validate(header, parent, protocolContext); + inOrder.verify(rule2).validate(header, parent); + inOrder.verify(rule3).validate(header, parent); + inOrder.verify(rule4).validate(header, parent, protocolContext); + } + + private DetachedBlockHeaderValidationRule createPassingDetachedRule( + final boolean includeInLightValidation) { + return createDetachedRule(true, includeInLightValidation); + } + + private DetachedBlockHeaderValidationRule createFailingDetachedRule( + final boolean includeInLightValidation) { + return createDetachedRule(false, includeInLightValidation); + } + + private DetachedBlockHeaderValidationRule createDetachedRule( + final boolean passing, final boolean includeInLightValidation) { + final DetachedBlockHeaderValidationRule rule = mock(DetachedBlockHeaderValidationRule.class); + when(rule.validate(any(), any())).thenReturn(passing); + when(rule.includeInLightValidation()).thenReturn(includeInLightValidation); + return rule; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BodyValidationTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BodyValidationTest.java new file mode 100755 index 00000000000..02ac344c0a6 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/BodyValidationTest.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.io.IOException; +import java.util.Arrays; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +/** Tests for {@link BodyValidation}. */ +public final class BodyValidationTest { + + @Test + public void calculateTransactionsRoot() throws IOException { + for (final int block : Arrays.asList(300006, 4400002)) { + final BlockHeader header = ValidationTestUtils.readHeader(block); + final BlockBody body = ValidationTestUtils.readBody(block); + final Bytes32 transactionRoot = BodyValidation.transactionsRoot(body.getTransactions()); + Assertions.assertThat(header.getTransactionsRoot()).isEqualTo(transactionRoot); + } + } + + @Test + public void calculateOmmersHash() throws IOException { + for (final int block : Arrays.asList(300006, 4400002)) { + final BlockHeader header = ValidationTestUtils.readHeader(block); + final BlockBody body = ValidationTestUtils.readBody(block); + final Bytes32 ommersHash = BodyValidation.ommersHash(body.getOmmers()); + Assertions.assertThat(header.getOmmersHash()).isEqualTo(ommersHash); + } + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreatorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreatorTest.java new file mode 100755 index 00000000000..c0503cdff1a --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashBlockCreatorTest.java @@ -0,0 +1,56 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.ExecutionContextTestFixture; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; + +import com.google.common.collect.Lists; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class EthHashBlockCreatorTest { + + private final Address BLOCK_1_COINBASE = + Address.fromHexString("0x05a56e2d52c817161883f50c441c3228cfe54d9f"); + + private static final long BLOCK_1_TIMESTAMP = Long.parseUnsignedLong("55ba4224", 16); + + private static final long BLOCK_1_NONCE = Long.parseLong("539bd4979fef1ec4", 16); + + private static final BytesValue BLOCK_1_EXTRA_DATA = + BytesValue.fromHexString("0x476574682f76312e302e302f6c696e75782f676f312e342e32"); + + private final ExecutionContextTestFixture executionContextTestFixture = + new ExecutionContextTestFixture(); + + @Test + public void createMainnetBlock1() throws IOException { + final EthHashSolver solver = + new EthHashSolver(Lists.newArrayList(BLOCK_1_NONCE), new EthHasher.Light()); + final EthHashBlockCreator blockCreator = + new EthHashBlockCreator( + BLOCK_1_COINBASE, + parent -> BLOCK_1_EXTRA_DATA, + new PendingTransactions(1), + executionContextTestFixture.getProtocolContext(), + executionContextTestFixture.getProtocolSchedule(), + gasLimit -> gasLimit, + solver, + Wei.ZERO, + executionContextTestFixture.getBlockchain().getChainHeadHeader()); + + // A Hashrate should not exist in the block creator prior to creating a block + Assertions.assertThat(blockCreator.getHashesPerSecond().isPresent()).isFalse(); + + final Block actualBlock = blockCreator.createBlock(BLOCK_1_TIMESTAMP); + final Block expectedBlock = ValidationTestUtils.readBlock(1); + + Assertions.assertThat(actualBlock).isEqualTo(expectedBlock); + Assertions.assertThat(blockCreator.getHashesPerSecond().isPresent()).isTrue(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverTest.java new file mode 100755 index 00000000000..079af06addc --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashSolverTest.java @@ -0,0 +1,116 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.EthHashSolver.EthHashSolverJob; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.Lists; +import org.junit.Test; + +public class EthHashSolverTest { + + @Test + public void emptyHashRateAndWorkDefinitionIsReportedPriorToSolverStarting() { + final List noncesToTry = Arrays.asList(1L, 1L, 1L, 1L, 1L, 1L, 0L); + final EthHashSolver solver = new EthHashSolver(noncesToTry, new EthHasher.Light()); + + assertThat(solver.hashesPerSecond()).isEqualTo(Optional.empty()); + assertThat(solver.getWorkDefinition()).isEqualTo(Optional.empty()); + } + + @Test + public void hashRateIsProducedSuccessfully() throws InterruptedException, ExecutionException { + final List noncesToTry = Arrays.asList(1L, 1L, 1L, 1L, 1L, 1L, 0L); + + final EthHasher hasher = mock(EthHasher.class); + doAnswer( + invocation -> { + final Object[] args = invocation.getArguments(); + final byte[] headerHash = ((byte[]) args[0]); + final long nonce = ((long) args[1]); + headerHash[32] = (byte) (nonce & 0xFF); + return null; + }) + .when(hasher) + .hash(any(), anyLong(), anyLong(), any()); + + final EthHashSolver solver = new EthHashSolver(noncesToTry, hasher); + + final Stopwatch operationTimer = Stopwatch.createStarted(); + final EthHashSolverInputs inputs = new EthHashSolverInputs(UInt256.ONE, new byte[0], 5); + solver.solveFor(EthHashSolverJob.createFromInputs(inputs)); + final double runtimeSeconds = operationTimer.elapsed(TimeUnit.NANOSECONDS) / 1e9; + final long worstCaseHashesPerSecond = (long) (noncesToTry.size() / runtimeSeconds); + + final Optional hashesPerSecond = solver.hashesPerSecond(); + assertThat(hashesPerSecond.isPresent()).isTrue(); + assertThat(hashesPerSecond.get()).isGreaterThanOrEqualTo(worstCaseHashesPerSecond); + + assertThat(solver.getWorkDefinition().isPresent()).isTrue(); + assertThat(solver.getWorkDefinition().equals(Optional.of(inputs))).isTrue(); + } + + @Test + public void ifInvokedTwiceProducesCorrectAnswerForSecondInvocation() + throws InterruptedException, ExecutionException { + + final EthHashSolverInputs firstInputs = + new EthHashSolverInputs( + UInt256.fromHexString( + "0x0083126e978d4fdf3b645a1cac083126e978d4fdf3b645a1cac083126e978d4f"), + new byte[] { + 15, -114, -104, 87, -95, -36, -17, 120, 52, 1, 124, 61, -6, -66, 78, -27, -57, 118, + -18, -64, -103, -91, -74, -121, 42, 91, -14, -98, 101, 86, -43, -51 + }, + 468); + + final EthHashSolution expectedFirstOutput = + new EthHashSolution( + -6506032554016940193L, + Hash.fromHexString( + "0xc5e3c33c86d64d0641dd3c86e8ce4628fe0aac0ef7b4c087c5fcaa45d5046d90"), + firstInputs.getPrePowHash()); + + final EthHashSolverInputs secondInputs = + new EthHashSolverInputs( + UInt256.fromHexString( + "0x0083126e978d4fdf3b645a1cac083126e978d4fdf3b645a1cac083126e978d4f"), + new byte[] { + -62, 121, -81, -31, 55, -38, -68, 102, -32, 95, -94, -83, -3, -48, -122, -68, 14, + -125, -83, 84, -55, -23, -123, -57, -34, 25, -89, 23, 64, -9, -114, -3, + }, + 1); + + final EthHashSolution expectedSecondOutput = + new EthHashSolution( + 8855952212886464488L, + Hash.fromHexString( + "0x2adb0f375dd2d528689cb9e00473c3c9692737109d547130feafbefb2c6c5244"), + firstInputs.getPrePowHash()); + + // Nonces need to have a 0L inserted, as it is a "wasted" nonce in the solver. + final EthHashSolver solver = + new EthHashSolver( + Lists.newArrayList(expectedFirstOutput.getNonce(), 0L, expectedSecondOutput.getNonce()), + new EthHasher.Light()); + + EthHashSolution soln = solver.solveFor(EthHashSolverJob.createFromInputs(firstInputs)); + assertThat(soln.getMixHash()).isEqualTo(expectedFirstOutput.getMixHash()); + + soln = solver.solveFor(EthHashSolverJob.createFromInputs(secondInputs)); + assertThat(soln.getMixHash()).isEqualTo(expectedSecondOutput.getMixHash()); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashTest.java new file mode 100755 index 00000000000..7962078ab43 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHashTest.java @@ -0,0 +1,112 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +import com.google.common.io.Resources; +import org.assertj.core.api.Assertions; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +public final class EthHashTest { + + /** + * Verifies hashing against block 300005 of the public Ethereum chain. + * + * @throws Exception On Failure + */ + @Test + public void hashimotoLight() throws Exception { + final RLPInput input = + new BytesValueRLPInput( + BytesValue.wrap( + Resources.toByteArray(EthHashTest.class.getResource("block_300005.blocks"))), + false); + input.enterList(); + final BlockHeader header = BlockHeader.readFrom(input, MainnetBlockHashFunction::createHash); + final long blockNumber = header.getNumber(); + final long epoch = EthHash.epoch(blockNumber); + final long datasetSize = EthHash.datasetSize(epoch); + final long cacheSize = EthHash.cacheSize(epoch); + Assertions.assertThat(datasetSize).isEqualTo(1157627776); + Assertions.assertThat(cacheSize).isEqualTo(18087488); + final int[] cache = EthHash.mkCache((int) cacheSize, blockNumber); + Assertions.assertThat( + Hash.wrap( + Bytes32.wrap( + Arrays.copyOf( + EthHash.hashimotoLight( + datasetSize, cache, EthHash.hashHeader(header), header.getNonce()), + 32)))) + .isEqualTo(header.getMixHash()); + } + + @Test + public void hashimotoLightExample() { + final int[] cache = EthHash.mkCache(1024, 1L); + Assertions.assertThat( + Hex.toHexString( + EthHash.hashimotoLight( + 32 * 1024, + cache, + Hex.decode("c9149cc0386e689d789a1c2f3d5d169a61a6218ed30e74414dc736e442ef3d1f"), + 0L))) + .isEqualTo( + "e4073cffaef931d37117cefd9afd27ea0f1cad6a981dd2605c4a1ac97c519800" + + "d3539235ee2e6f8db665c0a72169f55b7f6c605712330b778ec3944f0eb5a557"); + } + + @Test + public void prepareCache() { + final int[] cache = EthHash.mkCache(1024, 1L); + final ByteBuffer buffer = + ByteBuffer.allocate(cache.length * Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN); + for (final int i : cache) { + buffer.putInt(i); + } + Assertions.assertThat(Hex.toHexString(buffer.array())) + .isEqualTo( + new StringBuilder() + .append( + "7ce2991c951f7bf4c4c1bb119887ee07871eb5339d7b97b8588e85c742de90e5bafd5bbe6ce93a134fb6be9ad3e30db99d9528a2ea7846833f52e9ca119b6b54") + .append( + "8979480c46e19972bd0738779c932c1b43e665a2fd3122fc3ddb2691f353ceb0ed3e38b8f51fd55b6940290743563c9f8fa8822e611924657501a12aafab8a8d") + .append( + "88fb5fbae3a99d14792406672e783a06940a42799b1c38bc28715db6d37cb11f9f6b24e386dc52dd8c286bd8c36fa813dffe4448a9f56ebcbeea866b42f68d22") + .append( + "6c32aae4d695a23cab28fd74af53b0c2efcc180ceaaccc0b2e280103d097a03c1d1b0f0f26ce5f32a90238f9bc49f645db001ef9cd3d13d44743f841fad11a37") + .append( + "fa290c62c16042f703578921f30b9951465aae2af4a5dad43a7341d7b4a62750954965a47a1c3af638dc3495c4d62a9bab843168c9fc0114e79cffd1b2827b01") + .append( + "75d30ba054658f214e946cf24c43b40d3383fbb0493408e5c5392434ca21bbcf43200dfb876c713d201813934fa485f48767c5915745cf0986b1dc0f33e57748") + .append( + "bf483ee2aff4248dfe461ec0504a13628401020fc22638584a8f2f5206a13b2f233898c78359b21c8226024d0a7a93df5eb6c282bdbf005a4aab497e096f2847") + .append( + "76c71cee57932a8fb89f6d6b8743b60a4ea374899a94a2e0f218d5c55818cefb1790c8529a76dba31ebb0f4592d709b49587d2317970d39c086f18dd244291d9") + .append( + "eedb16705e53e3350591bd4ff4566a3595ac0f0ce24b5e112a3d033bc51b6fea0a92296dea7f5e20bf6ee6bc347d868fda193c395b9bb147e55e5a9f67cfe741") + .append( + "7eea7d699b155bd13804204df7ea91fa9249e4474dddf35188f77019c67d201e4c10d7079c5ad492a71afff9a23ca7e900ba7d1bdeaf3270514d8eb35eab8a0a") + .append( + "718bb7273aeb37768fa589ed8ab01fbf4027f4ebdbbae128d21e485f061c20183a9bc2e31edbda0727442e9d58eb0fe198440fe199e02e77c0f7b99973f1f74c") + .append( + "c9089a51ab96c94a84d66e6aa48b2d0a4543adb5a789039a2aa7b335ca85c91026c7d3c894da53ae364188c3fd92f78e01d080399884a47385aa792e38150cda") + .append( + "a8620b2ebeca41fbc773bb837b5e724d6eb2de570d99858df0d7d97067fb8103b21757873b735097b35d3bea8fd1c359a9e8a63c1540c76c9784cf8d975e995c") + .append( + "778401b94a2e66e6993ad67ad3ecdc2acb17779f1ea8606827ec92b11c728f8c3b6d3f04a3e6ed05ff81dd76d5dc5695a50377bc135aaf1671cf68b750315493") + .append( + "6c64510164d53312bf3c41740c7a237b05faf4a191bd8a95dafa068dbcf370255c725900ce5c934f36feadcfe55b687c440574c1f06f39d207a8553d39156a24") + .append( + "845f64fd8324bb85312979dead74f764c9677aab89801ad4f927f1c00f12e28f22422bb44200d1969d9ab377dd6b099dc6dbc3222e9321b2c1e84f8e2f07731c") + .toString()); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHasherTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHasherTest.java new file mode 100755 index 00000000000..6cd88fb1346 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/EthHasherTest.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.util.ByteArrayUtil; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.google.common.io.Resources; +import org.assertj.core.api.Assertions; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link EthHasher}. */ +public final class EthHasherTest { + + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + // TODO: Find a faster way to test HashimotoFull, this test takes almost 2 minutes. + @Test + @Ignore + public void hashimotoFull() throws Exception { + try (final EthHasher.Full hasher = new EthHasher.Full(folder.newFile().toPath())) { + final RLPInput input = + new BytesValueRLPInput( + BytesValue.wrap( + Resources.toByteArray(EthHashTest.class.getResource("block_300005.blocks"))), + false); + input.enterList(); + final BlockHeader header = BlockHeader.readFrom(input, MainnetBlockHashFunction::createHash); + final byte[] buffer = new byte[64]; + hasher.hash(buffer, header.getNonce(), header.getNumber(), EthHash.hashHeader(header)); + Assertions.assertThat( + ByteArrayUtil.compare(buffer, 0, 32, header.getMixHash().extractArray(), 0, 32)) + .isEqualTo(0); + } + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidatorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidatorTest.java new file mode 100755 index 00000000000..16aaeae1fa8 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockHeaderValidatorTest.java @@ -0,0 +1,67 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.ProtocolContext; + +import org.junit.Test; + +/** Tests for {@link MainnetBlockHeaderValidator}. */ +public final class MainnetBlockHeaderValidatorTest { + + @SuppressWarnings("unchecked") + private final ProtocolContext protocolContext = mock(ProtocolContext.class); + + @Test + public void validHeaderFrontier() throws Exception { + final BlockHeaderValidator headerValidator = + MainnetBlockHeaderValidator.create(MainnetDifficultyCalculators.FRONTIER); + assertThat( + headerValidator.validateHeader( + ValidationTestUtils.readHeader(300006), + ValidationTestUtils.readHeader(300005), + protocolContext, + HeaderValidationMode.FULL)) + .isTrue(); + } + + @Test + public void validHeaderHomestead() throws Exception { + final BlockHeaderValidator headerValidator = + MainnetBlockHeaderValidator.create(MainnetDifficultyCalculators.HOMESTEAD); + assertThat( + headerValidator.validateHeader( + ValidationTestUtils.readHeader(1200001), + ValidationTestUtils.readHeader(1200000), + protocolContext, + HeaderValidationMode.FULL)) + .isTrue(); + } + + @Test + public void invalidParentHash() throws Exception { + final BlockHeaderValidator headerValidator = + MainnetBlockHeaderValidator.create(MainnetDifficultyCalculators.HOMESTEAD); + assertThat( + headerValidator.validateHeader( + ValidationTestUtils.readHeader(1200001), + ValidationTestUtils.readHeader(4400000), + protocolContext, + HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void validHeaderByzantium() throws Exception { + final BlockHeaderValidator headerValidator = + MainnetBlockHeaderValidator.create(MainnetDifficultyCalculators.BYZANTIUM); + assertThat( + headerValidator.validateHeader( + ValidationTestUtils.readHeader(4400001), + ValidationTestUtils.readHeader(4400000), + protocolContext, + HeaderValidationMode.FULL)) + .isTrue(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessorTest.java new file mode 100755 index 00000000000..ccbc8f27e2d --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetBlockProcessorTest.java @@ -0,0 +1,42 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockProcessor.TransactionReceiptFactory; +import net.consensys.pantheon.ethereum.vm.TestBlockchain; +import net.consensys.pantheon.ethereum.vm.WorldStateMock; + +import org.junit.Test; + +public class MainnetBlockProcessorTest { + + private final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + private final TransactionReceiptFactory transactionReceiptFactory = + mock(TransactionReceiptFactory.class); + private final MainnetBlockProcessor blockProcessor = + new MainnetBlockProcessor( + transactionProcessor, transactionReceiptFactory, Wei.ZERO, BlockHeader::getCoinbase); + + @Test + public void noAccountCreatedWhenBlockRewardIsZero() { + final Blockchain blockchain = new TestBlockchain(); + + final MutableWorldState worldState = WorldStateMock.create(emptyMap()); + final Hash initialHash = worldState.rootHash(); + + final BlockHeader emptyBlockHeader = GenesisConfig.mainnet().getBlock().getHeader(); + blockProcessor.processBlock(blockchain, worldState, emptyBlockHeader, emptyList(), emptyList()); + + // An empty block with 0 reward should not change the world state + assertThat(worldState.rootHash()).isEqualTo(initialHash); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolScheduleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolScheduleTest.java new file mode 100755 index 00000000000..1baa9d1e244 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetProtocolScheduleTest.java @@ -0,0 +1,85 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import io.vertx.core.json.JsonObject; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class MainnetProtocolScheduleTest { + + @Test + public void shouldReturnDefaultProtocolSpecsWhenCustomNumbersAreNotUsed() { + final ProtocolSchedule sched = MainnetProtocolSchedule.create(); + Assertions.assertThat(sched.getByBlockNumber(1L).getName()).isEqualTo("Frontier"); + Assertions.assertThat(sched.getByBlockNumber(1_150_000L).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(1_920_000L).getName()) + .isEqualTo("DaoRecoveryInit"); + Assertions.assertThat(sched.getByBlockNumber(1_920_001L).getName()) + .isEqualTo("DaoRecoveryTransition"); + Assertions.assertThat(sched.getByBlockNumber(1_920_010L).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(2_463_000L).getName()) + .isEqualTo("TangerineWhistle"); + Assertions.assertThat(sched.getByBlockNumber(2_675_000L).getName()).isEqualTo("SpuriousDragon"); + Assertions.assertThat(sched.getByBlockNumber(4_730_000L).getName()).isEqualTo("Byzantium"); + Assertions.assertThat(sched.getByBlockNumber(Long.MAX_VALUE).getName()).isEqualTo("Byzantium"); + } + + @Test + public void shouldReturnCorrectProtocolSpecsWhenCustomNumbersAreUsed() { + final ProtocolSchedule sched = MainnetProtocolSchedule.create(2, 3, 14, 15, 16, 18, 1); + Assertions.assertThat(sched.getByBlockNumber(1).getName()).isEqualTo("Frontier"); + Assertions.assertThat(sched.getByBlockNumber(2).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(3).getName()).isEqualTo("DaoRecoveryInit"); + Assertions.assertThat(sched.getByBlockNumber(4).getName()).isEqualTo("DaoRecoveryTransition"); + Assertions.assertThat(sched.getByBlockNumber(13).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(14).getName()).isEqualTo("TangerineWhistle"); + Assertions.assertThat(sched.getByBlockNumber(15).getName()).isEqualTo("SpuriousDragon"); + Assertions.assertThat(sched.getByBlockNumber(16).getName()).isEqualTo("Byzantium"); + Assertions.assertThat(sched.getByBlockNumber(18).getName()).isEqualTo("Constantinople"); + } + + @Test + public void shouldReturnDefaultProtocolSpecsWhenEmptyJsonConfigIsUsed() { + final JsonObject json = new JsonObject("{}"); + final ProtocolSchedule sched = MainnetProtocolSchedule.fromConfig(json); + Assertions.assertThat(sched.getByBlockNumber(1L).getName()).isEqualTo("Frontier"); + Assertions.assertThat(sched.getByBlockNumber(1_150_000L).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(1_920_000L).getName()) + .isEqualTo("DaoRecoveryInit"); + Assertions.assertThat(sched.getByBlockNumber(1_920_001L).getName()) + .isEqualTo("DaoRecoveryTransition"); + Assertions.assertThat(sched.getByBlockNumber(1_920_010L).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(2_463_000L).getName()) + .isEqualTo("TangerineWhistle"); + Assertions.assertThat(sched.getByBlockNumber(2_675_000L).getName()).isEqualTo("SpuriousDragon"); + Assertions.assertThat(sched.getByBlockNumber(4_730_000L).getName()).isEqualTo("Byzantium"); + Assertions.assertThat(sched.getByBlockNumber(Long.MAX_VALUE).getName()).isEqualTo("Byzantium"); + } + + @Test + public void createFromConfigWithSettings() { + final JsonObject json = + new JsonObject( + "{\"homesteadBlock\": 2, \"daoForkBlock\": 3, \"eip150Block\": 14, \"eip158Block\": 15, \"byzantiumBlock\": 16, \"constantinopleBlock\": 18, \"chainId\":1234}"); + final ProtocolSchedule sched = MainnetProtocolSchedule.fromConfig(json); + Assertions.assertThat(sched.getByBlockNumber(1).getName()).isEqualTo("Frontier"); + Assertions.assertThat(sched.getByBlockNumber(2).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(3).getName()).isEqualTo("DaoRecoveryInit"); + Assertions.assertThat(sched.getByBlockNumber(4).getName()).isEqualTo("DaoRecoveryTransition"); + Assertions.assertThat(sched.getByBlockNumber(13).getName()).isEqualTo("Homestead"); + Assertions.assertThat(sched.getByBlockNumber(14).getName()).isEqualTo("TangerineWhistle"); + Assertions.assertThat(sched.getByBlockNumber(15).getName()).isEqualTo("SpuriousDragon"); + Assertions.assertThat(sched.getByBlockNumber(16).getName()).isEqualTo("Byzantium"); + Assertions.assertThat(sched.getByBlockNumber(18).getName()).isEqualTo("Constantinople"); + } + + @Test + public void shouldCreateRopstenConfig() { + final ProtocolSchedule sched = + MainnetProtocolSchedule.create(0, 0, 0, 10, 1700000, -1, 3); + Assertions.assertThat(sched.getByBlockNumber(0).getName()).isEqualTo("TangerineWhistle"); + Assertions.assertThat(sched.getByBlockNumber(1).getName()).isEqualTo("TangerineWhistle"); + Assertions.assertThat(sched.getByBlockNumber(10).getName()).isEqualTo("SpuriousDragon"); + Assertions.assertThat(sched.getByBlockNumber(1700000).getName()).isEqualTo("Byzantium"); + Assertions.assertThat(sched.getByBlockNumber(Long.MAX_VALUE).getName()).isEqualTo("Byzantium"); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java new file mode 100755 index 00000000000..42da41fbb1e --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/MainnetTransactionValidatorTest.java @@ -0,0 +1,157 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.INCORRECT_NONCE; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.NONCE_TOO_LOW; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE; +import static net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason.WRONG_CHAIN_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionTestFixture; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.vm.GasCalculator; + +import java.util.OptionalLong; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MainnetTransactionValidatorTest { + + private static final KeyPair senderKeys = KeyPair.generate(); + + @Mock private GasCalculator gasCalculator; + + private final Transaction basicTransaction = + new TransactionTestFixture().chainId(1).createTransaction(senderKeys); + + @Test + public void shouldRejectTransactionIfIntrinsicGasExceedsGasLimit() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false); + final Transaction transaction = + new TransactionTestFixture().gasLimit(10).chainId(0).createTransaction(senderKeys); + when(gasCalculator.transactionIntrinsicGasCost(transaction)).thenReturn(Gas.of(50)); + + assertThat(validator.validate(transaction)) + .isEqualTo(ValidationResult.invalid(INTRINSIC_GAS_EXCEEDS_GAS_LIMIT)); + } + + @Test + public void shouldRejectTransactionWhenTransactionHasChainIdAndValidatorDoesNot() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false); + assertThat(validator.validate(basicTransaction)) + .isEqualTo(ValidationResult.invalid(REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED)); + } + + @Test + public void shouldRejectTransactionWhenTransactionHasIncorrectChainId() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 2); + assertThat(validator.validate(basicTransaction)) + .isEqualTo(ValidationResult.invalid(WRONG_CHAIN_ID)); + } + + @Test + public void shouldRejectTransactionWhenSenderAccountDoesNotExist() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 1); + assertThat(validator.validateForSender(basicTransaction, null, OptionalLong.empty())) + .isEqualTo(ValidationResult.invalid(UPFRONT_COST_EXCEEDS_BALANCE)); + } + + @Test + public void shouldRejectTransactionWhenSenderAccountHasInsufficentBalance() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 1); + + final Account account = accountWithBalance(basicTransaction.getUpfrontCost().minus(Wei.of(1))); + assertThat(validator.validateForSender(basicTransaction, account, OptionalLong.empty())) + .isEqualTo(ValidationResult.invalid(UPFRONT_COST_EXCEEDS_BALANCE)); + } + + @Test + public void shouldRejectTransactionWhenTransactionNonceBelowAccountNonce() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 1); + + final Account account = accountWithNonce(basicTransaction.getNonce() + 1); + assertThat(validator.validateForSender(basicTransaction, account, OptionalLong.empty())) + .isEqualTo(ValidationResult.invalid(NONCE_TOO_LOW)); + } + + @Test + public void shouldRejectTransactionWhenTransactionNonceAboveAccountNonce() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 1); + + final Account account = accountWithNonce(basicTransaction.getNonce() - 1); + assertThat(validator.validateForSender(basicTransaction, account, OptionalLong.empty())) + .isEqualTo(ValidationResult.invalid(INCORRECT_NONCE)); + } + + @Test + public void shouldAcceptTransactionWhenNonceBetweenAccountNonceAndMaximumAllowedNonce() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 1); + + final Transaction transaction = + new TransactionTestFixture().nonce(10).createTransaction(senderKeys); + final Account account = accountWithNonce(5); + + assertThat(validator.validateForSender(transaction, account, OptionalLong.of(15))) + .isEqualTo(ValidationResult.valid()); + } + + @Test + public void shouldAcceptTransactionWhenNonceEqualsMaximumAllowedNonce() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 1); + + final Transaction transaction = + new TransactionTestFixture().nonce(10).createTransaction(senderKeys); + final Account account = accountWithNonce(5); + + assertThat(validator.validateForSender(transaction, account, OptionalLong.of(10))) + .isEqualTo(ValidationResult.valid()); + } + + @Test + public void shouldRejectTransactionWhenNonceExceedsMaximumAllowedNonce() { + final MainnetTransactionValidator validator = + new MainnetTransactionValidator(gasCalculator, false, 1); + + final Transaction transaction = + new TransactionTestFixture().nonce(11).createTransaction(senderKeys); + final Account account = accountWithNonce(5); + + assertThat(validator.validateForSender(transaction, account, OptionalLong.of(10))) + .isEqualTo(ValidationResult.invalid(INCORRECT_NONCE)); + } + + private Account accountWithNonce(final long nonce) { + return account(basicTransaction.getUpfrontCost(), nonce); + } + + private Account accountWithBalance(final Wei balance) { + return account(balance, basicTransaction.getNonce()); + } + + private Account account(final Wei balance, final long nonce) { + final Account account = mock(Account.class); + when(account.getBalance()).thenReturn(balance); + when(account.getNonce()).thenReturn(nonce); + return account; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ProtocolScheduleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ProtocolScheduleTest.java new file mode 100755 index 00000000000..f3d4f705a07 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ProtocolScheduleTest.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class ProtocolScheduleTest { + + @SuppressWarnings("unchecked") + @Test + public void getByBlockNumber() { + final ProtocolSpec spec1 = mock(ProtocolSpec.class); + final ProtocolSpec spec2 = mock(ProtocolSpec.class); + final ProtocolSpec spec3 = mock(ProtocolSpec.class); + final ProtocolSpec spec4 = mock(ProtocolSpec.class); + + final MutableProtocolSchedule schedule = new MutableProtocolSchedule<>(); + schedule.putMilestone(20, spec3); + schedule.putMilestone(0, spec1); + schedule.putMilestone(30, spec4); + schedule.putMilestone(10, spec2); + + assertThat(schedule.getByBlockNumber(0)).isEqualTo(spec1); + assertThat(schedule.getByBlockNumber(15)).isEqualTo(spec2); + assertThat(schedule.getByBlockNumber(35)).isEqualTo(spec4); + assertThat(schedule.getByBlockNumber(105)).isEqualTo(spec4); + } + + @Test + public void emptySchedule() { + Assertions.assertThatThrownBy(() -> new MutableProtocolSchedule<>().getByBlockNumber(0)) + .hasMessage("At least 1 milestone must be provided to the protocol schedule"); + } + + @SuppressWarnings("unchecked") + @Test + public void conflictingSchedules() { + final ProtocolSpec spec1 = mock(ProtocolSpec.class); + final ProtocolSpec spec2 = mock(ProtocolSpec.class); + + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone(0, spec1); + protocolSchedule.putMilestone(0, spec2); + assertThat(protocolSchedule.getByBlockNumber(0)).isSameAs(spec2); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationResultTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationResultTest.java new file mode 100755 index 00000000000..0a62077f5ff --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationResultTest.java @@ -0,0 +1,62 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import org.junit.Test; + +public class ValidationResultTest { + + private final Runnable action = mock(Runnable.class); + + @Test + public void validResulthouldBeValid() { + assertThat(ValidationResult.valid().isValid()).isTrue(); + } + + @Test + public void invalidResultsShouldBeInvalid() { + assertThat(ValidationResult.invalid("foo").isValid()).isFalse(); + } + + @Test + public void shouldRunIfValidActionWhenValid() { + ValidationResult.valid().ifValid(action); + + verify(action).run(); + } + + @Test + public void shouldNotRunIfValidActionWhenInvalid() { + ValidationResult.invalid("foo").ifValid(action); + + verifyZeroInteractions(action); + } + + @Test + public void eitherShouldReturnWhenValidSupplierWhenValid() { + assertThat( + ValidationResult.valid() + .either( + () -> Boolean.TRUE, + error -> { + throw new IllegalStateException( + "Should not have executed whenInvalid function"); + })) + .isTrue(); + } + + @Test + public void eitherShouldUseWhenInvalidFunctionWhenInvalid() { + final ValidationResult result = ValidationResult.invalid("foo"); + assertThat( + result.either( + () -> { + throw new IllegalStateException("Should not have executed whenInvalid function"); + }, + error -> Boolean.TRUE)) + .isTrue(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationTestUtils.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationTestUtils.java new file mode 100755 index 00000000000..9d754e87732 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/ValidationTestUtils.java @@ -0,0 +1,59 @@ +package net.consensys.pantheon.ethereum.mainnet; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.util.List; + +import com.google.common.io.Resources; + +public final class ValidationTestUtils { + + public static BlockHeader readHeader(final long num) throws IOException { + final RLPInput input = + new BytesValueRLPInput( + BytesValue.wrap( + Resources.toByteArray( + EthHashTest.class.getResource(String.format("block_%d.blocks", num)))), + false); + input.enterList(); + return BlockHeader.readFrom(input, MainnetBlockHashFunction::createHash); + } + + public static BlockBody readBody(final long num) throws IOException { + final RLPInput input = + new BytesValueRLPInput( + BytesValue.wrap( + Resources.toByteArray( + EthHashTest.class.getResource(String.format("block_%d.blocks", num)))), + false); + input.enterList(); + input.skipNext(); + final List transactions = input.readList(Transaction::readFrom); + final List ommers = + input.readList(rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash)); + return new BlockBody(transactions, ommers); + } + + public static Block readBlock(final long num) throws IOException { + final RLPInput input = + new BytesValueRLPInput( + BytesValue.wrap( + Resources.toByteArray( + EthHashTest.class.getResource(String.format("block_%d.blocks", num)))), + false); + input.enterList(); + final BlockHeader header = BlockHeader.readFrom(input, MainnetBlockHashFunction::createHash); + final List transactions = input.readList(Transaction::readFrom); + final List ommers = + input.readList(rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash)); + final BlockBody body = new BlockBody(transactions, ommers); + return new Block(header, body); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRuleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRuleTest.java new file mode 100755 index 00000000000..1f7f9124461 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/AncestryValidationRuleTest.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; + +import org.junit.Test; + +public class AncestryValidationRuleTest { + + @Test + public void incrementingNumberAndLinkedHashesReturnTrue() { + final AncestryValidationRule uut = new AncestryValidationRule(); + + final BlockHeader parentHeader = new BlockHeaderTestFixture().buildHeader(); + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + headerBuilder.parentHash(parentHeader.getHash()); + headerBuilder.number(parentHeader.getNumber() + 1); + + final BlockHeader header = headerBuilder.buildHeader(); + assertThat(uut.validate(header, parentHeader)).isTrue(); + } + + @Test + public void mismatchedHashesReturnsFalse() { + final AncestryValidationRule uut = new AncestryValidationRule(); + + final BlockHeader parentHeader = new BlockHeaderTestFixture().buildHeader(); + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + headerBuilder.parentHash(Hash.EMPTY); + headerBuilder.number(parentHeader.getNumber() + 1); + + final BlockHeader header = headerBuilder.buildHeader(); + assertThat(uut.validate(header, parentHeader)).isFalse(); + } + + @Test + public void nonIncrementingBlockNumberReturnsFalse() { + final AncestryValidationRule uut = new AncestryValidationRule(); + + final BlockHeader parentHeader = new BlockHeaderTestFixture().buildHeader(); + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + headerBuilder.parentHash(parentHeader.getHash()); + headerBuilder.number(parentHeader.getNumber() + 2); + + final BlockHeader header = headerBuilder.buildHeader(); + assertThat(uut.validate(header, parentHeader)).isFalse(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRuleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRuleTest.java new file mode 100755 index 00000000000..ea578e09ad7 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ConstantFieldValidationRuleTest.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.uint.UInt256; + +import org.junit.Test; + +public class ConstantFieldValidationRuleTest { + + @Test + public void ommersFieldValidatesCorrectly() { + + final ConstantFieldValidationRule uut = + new ConstantFieldValidationRule<>( + "OmmersHash", BlockHeader::getOmmersHash, Hash.EMPTY_LIST_HASH); + + final BlockHeaderTestFixture blockHeaderBuilder = new BlockHeaderTestFixture(); + blockHeaderBuilder.ommersHash(Hash.EMPTY_LIST_HASH); + BlockHeader header = blockHeaderBuilder.buildHeader(); + + assertThat(uut.validate(header, null)).isTrue(); + + blockHeaderBuilder.ommersHash(Hash.ZERO); + header = blockHeaderBuilder.buildHeader(); + assertThat(uut.validate(header, null)).isFalse(); + } + + @Test + public void difficultyFieldIsValidatedCorrectly() { + final ConstantFieldValidationRule uut = + new ConstantFieldValidationRule<>("Difficulty", BlockHeader::getDifficulty, UInt256.ONE); + + final BlockHeaderTestFixture blockHeaderBuilder = new BlockHeaderTestFixture(); + blockHeaderBuilder.difficulty(UInt256.ONE); + BlockHeader header = blockHeaderBuilder.buildHeader(); + + assertThat(uut.validate(header, null)).isTrue(); + + blockHeaderBuilder.difficulty(UInt256.ZERO); + header = blockHeaderBuilder.buildHeader(); + assertThat(uut.validate(header, null)).isFalse(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRuleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRuleTest.java new file mode 100755 index 00000000000..50d89ad8099 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/ExtraDataMaxLengthValidationRuleTest.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class ExtraDataMaxLengthValidationRuleTest { + + @Test + public void sufficientlySmallExtraDataBlockValidateSuccessfully() { + final ExtraDataMaxLengthValidationRule uut = new ExtraDataMaxLengthValidationRule(1); + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.extraData(BytesValue.wrap(new byte[1])); + + final BlockHeader header = builder.buildHeader(); + + // Note: The parentHeader is not required for this validator. + assertThat(uut.validate(header, null)).isTrue(); + } + + @Test + public void tooLargeExtraDataCausesValidationFailure() { + final ExtraDataMaxLengthValidationRule uut = new ExtraDataMaxLengthValidationRule(1); + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.extraData(BytesValue.wrap(new byte[2])); + + final BlockHeader header = builder.buildHeader(); + + // Note: The parentHeader is not required for this validator. + assertThat(uut.validate(header, null)).isFalse(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java new file mode 100755 index 00000000000..b77fb249556 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasLimitRangeAndDeltaValidationRuleTest.java @@ -0,0 +1,62 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class GasLimitRangeAndDeltaValidationRuleTest { + + @Parameter public long headerGasLimit; + + @Parameter(1) + public long parentGasLimit; + + @Parameter(2) + public GasLimitRangeAndDeltaValidationRule uut; + + @Parameter(3) + public boolean expectedResult; + + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {4096, 4096, new GasLimitRangeAndDeltaValidationRule(4095, 4097), true}, + // In Range, no change = valid, + {4096, 4096, new GasLimitRangeAndDeltaValidationRule(4094, 4095), false}, + // Out of Range, no change = invalid, + {4099, 4096, new GasLimitRangeAndDeltaValidationRule(4000, 4200), true}, + // In Range, <1/1024 change = valid, + {4093, 4096, new GasLimitRangeAndDeltaValidationRule(4000, 4200), true}, + // In Range, ,1/1024 change = valid, + {4092, 4096, new GasLimitRangeAndDeltaValidationRule(4000, 4200), false}, + // In Range, >1/1024 change = invalid, + {4100, 4096, new GasLimitRangeAndDeltaValidationRule(4000, 4200), false} + // In Range, >1/1024 change = invalid, + }); + } + + @Test + public void test() { + final BlockHeaderTestFixture blockHeaderBuilder = new BlockHeaderTestFixture(); + + blockHeaderBuilder.gasLimit(headerGasLimit); + final BlockHeader header = blockHeaderBuilder.buildHeader(); + + blockHeaderBuilder.gasLimit(parentGasLimit); + final BlockHeader parent = blockHeaderBuilder.buildHeader(); + + assertThat(uut.validate(header, parent)).isEqualTo(expectedResult); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java new file mode 100755 index 00000000000..1a3c4c21ba5 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/GasUsageValidationRuleTest.java @@ -0,0 +1,50 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class GasUsageValidationRuleTest { + + @Parameters + public static Collection data() { + return Arrays.asList( + new Object[][] { + {5, 6, true}, // gasUsed is less than gasLimit is valid + {5, 5, true}, // gasUsed is the same as gaslimit is valid + {5, 4, false}, // gasUsed is less than gasLimit + }); + } + + @Parameter public long gasUsed; + + @Parameter(1) + public long gasLimit; + + @Parameter(2) + public boolean expectedResult; + + @Test + public void test() { + final GasUsageValidationRule uut = new GasUsageValidationRule(); + final BlockHeaderTestFixture blockBuilder = new BlockHeaderTestFixture(); + + blockBuilder.gasLimit(gasLimit); + blockBuilder.gasUsed(gasUsed); + + final BlockHeader header = blockBuilder.buildHeader(); + + assertThat(uut.validate(header, null)).isEqualTo(expectedResult); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRuleTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRuleTest.java new file mode 100755 index 00000000000..42e74950800 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/mainnet/headervalidationrules/TimestampValidationRuleTest.java @@ -0,0 +1,107 @@ +package net.consensys.pantheon.ethereum.mainnet.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; + +import java.util.concurrent.TimeUnit; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class TimestampValidationRuleTest { + + @Test + public void headerTimestampSufficientlyFarIntoFutureVadidatesSuccessfully() { + final TimestampValidationRule uut = new TimestampValidationRule(0, 10); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + // Note: This is 10 seconds after Unix epoch (i.e. long way in the past.) + headerBuilder.timestamp(10); + final BlockHeader parent = headerBuilder.buildHeader(); + + headerBuilder.timestamp(parent.getTimestamp() + 11); + final BlockHeader header = headerBuilder.buildHeader(); + + assertThat(uut.validate(header, parent)).isTrue(); + } + + @Test + public void headerTimestampDifferenceMustBePositive() { + Assertions.assertThatThrownBy(() -> new TimestampValidationRule(0, -1)) + .hasMessage("minimumSecondsSinceParent must be positive"); + } + + @Test + public void headerTimestampTooCloseToParentFailsValidation() { + final TimestampValidationRule uut = new TimestampValidationRule(0, 10); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + // Note: This is 10 seconds after Unix epoch (i.e. long way in the past.) + headerBuilder.timestamp(10); + final BlockHeader parent = headerBuilder.buildHeader(); + + headerBuilder.timestamp(parent.getTimestamp() + 1); + final BlockHeader header = headerBuilder.buildHeader(); + + assertThat(uut.validate(header, parent)).isFalse(); + } + + @Test + public void headerTimestampIsBehindParentFailsValidation() { + final TimestampValidationRule uut = new TimestampValidationRule(0, 10); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + // Note: This is 100 seconds after Unix epoch (i.e. long way in the past.) + headerBuilder.timestamp(100); + final BlockHeader parent = headerBuilder.buildHeader(); + + headerBuilder.timestamp(parent.getTimestamp() - 11); + final BlockHeader header = headerBuilder.buildHeader(); + + assertThat(uut.validate(header, parent)).isFalse(); + } + + @Test + public void headerNewerThanCurrentSystemFailsValidation() { + final long acceptableClockDrift = 5; + final TimestampValidationRule uut = new TimestampValidationRule(acceptableClockDrift, 10); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + // Create Parent Header @ 'now' + headerBuilder.timestamp( + TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)); + final BlockHeader parent = headerBuilder.buildHeader(); + + // Create header for validation with a timestamp in the future (1 second too far away) + headerBuilder.timestamp(parent.getTimestamp() + acceptableClockDrift + 1); + final BlockHeader header = headerBuilder.buildHeader(); + + assertThat(uut.validate(header, parent)).isFalse(); + } + + @Test + public void futureHeadersAreValidIfTimestampWithinTolerance() { + final long acceptableClockDrift = 5; + final TimestampValidationRule uut = new TimestampValidationRule(acceptableClockDrift, 10); + + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + // Create Parent Header @ 'now' + headerBuilder.timestamp( + TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)); + final BlockHeader parent = headerBuilder.buildHeader(); + + // Create header for validation with a timestamp in the future (1 second too far away) + // (-1) to prevent spurious failures + headerBuilder.timestamp(parent.getTimestamp() + acceptableClockDrift - 1); + final BlockHeader header = headerBuilder.buildHeader(); + + assertThat(uut.validate(header, parent)).isFalse(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/testutil/BlockDataGenerator.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/testutil/BlockDataGenerator.java new file mode 100755 index 00000000000..4accb2d5ab1 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/testutil/BlockDataGenerator.java @@ -0,0 +1,366 @@ +package net.consensys.pantheon.ethereum.testutil; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.trie.MerklePatriciaTrie; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Random; + +public class BlockDataGenerator { + private final Random random; + + public BlockDataGenerator(final int seed) { + this.random = new Random(seed); + } + + public BlockDataGenerator() { + this(1); + } + + /** + * Generates a sequence of blocks with some accounts and account storage pre-populated with random + * data. + */ + private List blockSequence( + final int count, + final long nextBlock, + final Hash parent, + final WorldStateArchive worldStateArchive, + final List

accountsToSetup, + final List storageKeys) { + final List seq = new ArrayList<>(count); + + final MutableWorldState worldState = + worldStateArchive.getMutable(Hash.wrap(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH)); + + long nextBlockNumber = nextBlock; + Hash parentHash = parent; + + for (int i = 0; i < count; i++) { + final WorldUpdater stateUpdater = worldState.updater(); + if (i == 0) { + // Set up some accounts + accountsToSetup.forEach(stateUpdater::createAccount); + stateUpdater.commit(); + } else { + // Mutate accounts + accountsToSetup.forEach( + hash -> { + final MutableAccount a = stateUpdater.getMutable(hash); + a.incrementNonce(); + a.setBalance(Wei.of(positiveLong())); + storageKeys.forEach(key -> a.setStorageValue(key, UInt256.ONE)); + }); + stateUpdater.commit(); + } + final BlockOptions options = + new BlockOptions() + .setBlockNumber(nextBlockNumber) + .setParentHash(parentHash) + .setStateRoot(worldState.rootHash()); + final Block next = block(options); + seq.add(next); + parentHash = next.getHash(); + nextBlockNumber = nextBlockNumber + 1L; + worldState.persist(); + } + + return seq; + } + + public List blockSequence(final int count) { + final WorldStateArchive worldState = createInMemoryWorldStateArchive(); + return blockSequence(count, worldState, Collections.emptyList(), Collections.emptyList()); + } + + public List blockSequence( + final int count, + final WorldStateArchive worldStateArchive, + final List
accountsToSetup, + final List storageKeys) { + final long blockNumber = BlockHeader.GENESIS_BLOCK_NUMBER; + final Hash parentHash = Hash.ZERO; + return blockSequence( + count, blockNumber, parentHash, worldStateArchive, accountsToSetup, storageKeys); + } + + public Block genesisBlock() { + final BlockOptions options = + new BlockOptions() + .setBlockNumber(BlockHeader.GENESIS_BLOCK_NUMBER) + .setParentHash(Hash.ZERO); + return block(options); + } + + public Block block(final BlockOptions options) { + final long blockNumber = options.getBlockNumber(positiveLong()); + final BlockHeader header = header(blockNumber, options); + final BlockBody body = + blockNumber == BlockHeader.GENESIS_BLOCK_NUMBER ? BlockBody.empty() : body(options); + return new Block(header, body); + } + + public Block block() { + return block(new BlockOptions()); + } + + public BlockOptions nextBlockOptions(final Block afterBlock) { + return new BlockOptions() + .setBlockNumber(afterBlock.getHeader().getNumber() + 1) + .setParentHash(afterBlock.getHash()); + } + + public Block nextBlock(final Block afterBlock) { + final BlockOptions options = nextBlockOptions(afterBlock); + return block(options); + } + + public BlockHeader header(final long blockNumber) { + return header(blockNumber, new BlockOptions()); + } + + public BlockHeader header() { + return header(positiveLong(), new BlockOptions()); + } + + public BlockHeader header(final long number, final BlockOptions options) { + final int gasLimit = random.nextInt() & Integer.MAX_VALUE; + final int gasUsed = Math.max(0, gasLimit - 1); + final long blockNonce = random.nextLong(); + return BlockHeaderBuilder.create() + .parentHash(options.getParentHash(hash())) + .ommersHash(hash()) + .coinbase(address()) + .stateRoot(options.getStateRoot(hash())) + .transactionsRoot(hash()) + .receiptsRoot(hash()) + .logsBloom(logsBloom()) + .difficulty(options.getDifficulty(uint256(4))) + .number(number) + .gasLimit(gasLimit) + .gasUsed(gasUsed) + .timestamp(Instant.now().truncatedTo(ChronoUnit.SECONDS).getEpochSecond()) + .extraData(bytes32()) + .mixHash(hash()) + .nonce(blockNonce) + .blockHashFunction(MainnetBlockHashFunction::createHash) + .buildBlockHeader(); + } + + public BlockBody body() { + return body(new BlockOptions()); + } + + public BlockBody body(final BlockOptions options) { + final List ommers = new ArrayList<>(); + final int ommerCount = random.nextInt(3); + for (int i = 0; i < ommerCount; i++) { + ommers.add(header()); + } + final List defaultTxs = new ArrayList<>(); + defaultTxs.add(transaction()); + defaultTxs.add(transaction()); + + return new BlockBody(options.getTransactions(defaultTxs), ommers); + } + + public Transaction transaction() { + return Transaction.builder() + .nonce(positiveLong()) + .gasPrice(Wei.wrap(bytes32())) + .gasLimit(positiveLong()) + .to(address()) + .value(Wei.wrap(bytes32())) + .payload(bytes32()) + .chainId(1) + .signAndBuild(SECP256K1.KeyPair.generate()); + } + + public TransactionReceipt receipt(final long cumulativeGasUsed) { + return new TransactionReceipt(hash(), cumulativeGasUsed, Arrays.asList(log(), log())); + } + + public TransactionReceipt receipt() { + return receipt(positiveLong()); + } + + public UInt256 storageKey() { + return uint256(); + } + + public List receipts(final Block block) { + final long totalGas = block.getHeader().getGasUsed(); + final int receiptCount = block.getBody().getTransactions().size(); + + final List receipts = new ArrayList<>(receiptCount); + for (int i = 0; i < receiptCount; i++) { + receipts.add(receipt((totalGas * (i + 1)) / (receiptCount))); + } + + return receipts; + } + + public Log log() { + return new Log(address(), bytesValue(5 + random.nextInt(10)), Collections.emptyList()); + } + + private Bytes32 bytes32() { + return Bytes32.wrap(bytes(Bytes32.SIZE)); + } + + private BytesValue bytesValue(final int size) { + return BytesValue.wrap(bytes(size)); + } + + /** + * Creates a UInt256 with a value that fits within maxByteSize + * + * @param maxByteSize The byte size to cap this value to + * @return + */ + private UInt256 uint256(final int maxByteSize) { + assert maxByteSize <= 32; + return Bytes32.wrap(bytes(32, 32 - maxByteSize)).asUInt256(); + } + + private UInt256 uint256() { + return bytes32().asUInt256(); + } + + private long positiveLong() { + final long l = random.nextLong(); + return l < 0 ? Math.abs(l + 1) : l; + } + + public Hash hash() { + return Hash.wrap(bytes32()); + } + + public Address address() { + return Address.wrap(bytesValue(Address.SIZE)); + } + + public LogsBloomFilter logsBloom() { + return new LogsBloomFilter(BytesValue.of(bytes(LogsBloomFilter.BYTE_SIZE))); + } + + public BytesValue bytesValue() { + return bytesValue(1, 20); + } + + public BytesValue bytesValue(final int minSize, final int maxSize) { + checkArgument(minSize >= 0); + checkArgument(maxSize >= 0); + checkArgument(maxSize > minSize); + final int size = random.nextInt(maxSize - minSize) + minSize; + return BytesValue.wrap(bytes(size)); + } + + private byte[] bytes(final int size) { + return bytes(size, 0); + } + + /** + * Creates a byte sequence with leading zeros. + * + * @param size The size of the byte array to return + * @param zerofill The number of lower-order bytes to fill with zero (creating a smaller big + * endian integer value) + * @return + */ + private byte[] bytes(final int size, final int zerofill) { + final byte[] bytes = new byte[size]; + random.nextBytes(bytes); + Arrays.fill(bytes, 0, zerofill, (byte) 0x0); + return bytes; + } + + public static class BlockOptions { + private OptionalLong blockNumber = OptionalLong.empty(); + private Optional parentHash = Optional.empty(); + private Optional stateRoot = Optional.empty(); + private Optional difficulty = Optional.empty(); + private Optional> transactions = Optional.empty(); + + public static BlockOptions create() { + return new BlockOptions(); + } + + public List getTransactions(final List defaultValue) { + return transactions.orElse(defaultValue); + } + + public long getBlockNumber(final long defaultValue) { + return blockNumber.orElse(defaultValue); + } + + public Hash getParentHash(final Hash defaultValue) { + return parentHash.orElse(defaultValue); + } + + public Hash getStateRoot(final Hash defaultValue) { + return stateRoot.orElse(defaultValue); + } + + public UInt256 getDifficulty(final UInt256 defaultValue) { + return difficulty.orElse(defaultValue); + } + + public BlockOptions addTransaction(final Transaction... tx) { + if (!transactions.isPresent()) { + transactions = Optional.of(new ArrayList<>()); + } + transactions.get().addAll(Arrays.asList(tx)); + return this; + } + + public BlockOptions setBlockNumber(final long blockNumber) { + this.blockNumber = OptionalLong.of(blockNumber); + return this; + } + + public BlockOptions setParentHash(final Hash parentHash) { + this.parentHash = Optional.of(parentHash); + return this; + } + + public BlockOptions setStateRoot(final Hash stateRoot) { + this.stateRoot = Optional.of(stateRoot); + return this; + } + + public BlockOptions setDifficulty(final UInt256 difficulty) { + this.difficulty = Optional.of(difficulty); + return this; + } + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/util/BlockchainUtilTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/util/BlockchainUtilTest.java new file mode 100755 index 00000000000..d0aac782462 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/util/BlockchainUtilTest.java @@ -0,0 +1,119 @@ +package net.consensys.pantheon.ethereum.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static sun.security.krb5.Confounder.bytes; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderBuilder; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.OptionalInt; + +import org.junit.Test; + +public class BlockchainUtilTest { + + @Test + public void shouldReturnIndexOfCommonBlockForAscendingOrder() { + BlockDataGenerator blockDataGenerator = new BlockDataGenerator(); + + BlockHeader genesisHeader = + BlockHeaderBuilder.create() + .parentHash(Hash.ZERO) + .ommersHash(Hash.ZERO) + .coinbase(Address.fromHexString("0x0000000000000000000000000000000000000000")) + .stateRoot(Hash.ZERO) + .transactionsRoot(Hash.ZERO) + .receiptsRoot(Hash.ZERO) + .logsBloom(new LogsBloomFilter(BytesValue.of(bytes(LogsBloomFilter.BYTE_SIZE)))) + .difficulty(UInt256.ZERO) + .number(0L) + .gasLimit(1L) + .gasUsed(1L) + .timestamp(Instant.now().truncatedTo(ChronoUnit.SECONDS).getEpochSecond()) + .extraData(Bytes32.wrap(bytes(Bytes32.SIZE))) + .mixHash(Hash.ZERO) + .nonce(0L) + .blockHashFunction(MainnetBlockHashFunction::createHash) + .buildBlockHeader(); + BlockBody genesisBody = new BlockBody(Collections.emptyList(), Collections.emptyList()); + Block genesisBlock = new Block(genesisHeader, genesisBody); + + KeyValueStorage kvStoreLocal = new InMemoryKeyValueStorage(); + KeyValueStorage kvStoreRemote = new InMemoryKeyValueStorage(); + + DefaultMutableBlockchain blockchainLocal = + new DefaultMutableBlockchain( + genesisBlock, kvStoreLocal, MainnetBlockHashFunction::createHash); + DefaultMutableBlockchain blockchainRemote = + new DefaultMutableBlockchain( + genesisBlock, kvStoreRemote, MainnetBlockHashFunction::createHash); + + // Common chain segment + Block commonBlock = null; + for (long i = 1; i <= 3; i++) { + BlockDataGenerator.BlockOptions options = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(blockchainLocal.getBlockHashByNumber(i - 1).get()); + commonBlock = blockDataGenerator.block(options); + List receipts = blockDataGenerator.receipts(commonBlock); + blockchainLocal.appendBlock(commonBlock, receipts); + blockchainRemote.appendBlock(commonBlock, receipts); + } + + // Populate local chain + for (long i = 4; i <= 9; i++) { + BlockDataGenerator.BlockOptions optionsLocal = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ZERO) // differentiator + .setBlockNumber(i) + .setParentHash(blockchainLocal.getBlockHashByNumber(i - 1).get()); + Block blockLocal = blockDataGenerator.block(optionsLocal); + List receiptsLocal = blockDataGenerator.receipts(blockLocal); + blockchainLocal.appendBlock(blockLocal, receiptsLocal); + } + + // Populate remote chain + for (long i = 4; i <= 9; i++) { + BlockDataGenerator.BlockOptions optionsRemote = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ONE) + .setBlockNumber(i) + .setParentHash(blockchainRemote.getBlockHashByNumber(i - 1).get()); + Block blockRemote = blockDataGenerator.block(optionsRemote); + List receiptsRemote = blockDataGenerator.receipts(blockRemote); + blockchainRemote.appendBlock(blockRemote, receiptsRemote); + } + + // Create a list of headers... + List headers = new ArrayList<>(); + for (long i = 0L; i < blockchainRemote.getChainHeadBlockNumber(); i++) { + headers.add(blockchainRemote.getBlockHeader(i).get()); + } + + OptionalInt maybeAncestorNumber = + BlockchainUtil.findHighestKnownBlockIndex(blockchainLocal, headers, true); + + assertThat(maybeAncestorNumber.getAsInt()) + .isEqualTo(Math.toIntExact(commonBlock.getHeader().getNumber())); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/.gitignore b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/.gitignore new file mode 100755 index 00000000000..4e12a0a271c --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/.gitignore @@ -0,0 +1,2 @@ +blockchain/*.java +generalstate/*.java diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AbstractRetryingTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AbstractRetryingTest.java new file mode 100755 index 00000000000..6fab230ae19 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AbstractRetryingTest.java @@ -0,0 +1,67 @@ +package net.consensys.pantheon.ethereum.vm; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.junit.Before; +import org.junit.Test; + +/** + * This class can be used to extend tests. It allows to rerun tests with trace tests enabled + * whenever they failed. + * + *

The test only reruns if the logging is not configured to use trace logging. + * + *

To turn trace logging manually, you can set 2 system properties during execution of the tests: + * + *

    + *
  • -Devm.log.level=trace + *
  • -Droot.log.level=trace + *
+ */ +public abstract class AbstractRetryingTest { + + private static final String originalEvmLogLevel = System.getProperty("evm.log.level"); + private static final String originalRootLogLevel = System.getProperty("root.log.level"); + + /** Sets the logging system back to the original parameters with which the tests were launched. */ + @Before + public void resetLoggingToOriginalConfiguration() { + if (originalRootLogLevel == null) { + System.clearProperty("root.log.level"); + } else { + System.setProperty("root.log.level", originalRootLogLevel); + } + if (originalEvmLogLevel == null) { + System.clearProperty("evm.log.level"); + } else { + System.setProperty("evm.log.level", originalEvmLogLevel); + } + resetLogging(); + } + + /** Run the test case. */ + @Test + public void execution() { + try { + runTest(); + } catch (final AssertionError e) { + if (!"trace".equalsIgnoreCase(originalRootLogLevel) + || !"trace".equalsIgnoreCase(originalEvmLogLevel)) { + // try again, this time with more logging so we can capture more information. + System.setProperty("root.log.level", "trace"); + System.setProperty("evm.log.level", "trace"); + resetLogging(); + runTest(); + } else { + throw e; + } + } + } + + private void resetLogging() { + ((LoggerContext) LogManager.getContext(false)).reconfigure(); + } + + /** Subclasses should implement this method to run the actual JUnit test. */ + protected abstract void runTest(); +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressMock.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressMock.java new file mode 100755 index 00000000000..a92388aacc0 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressMock.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Address; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** A AccountAddress mock for testing. */ +public class AddressMock extends Address { + + /** + * Public constructor. + * + * @param value The value the AccountAddress represents. + */ + @JsonCreator + public AddressMock(final String value) { + super(Address.fromHexString(value)); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressTest.java new file mode 100755 index 00000000000..3151859e88c --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/AddressTest.java @@ -0,0 +1,42 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Assert; +import org.junit.Test; + +public class AddressTest { + + @Test + public void accountAddressToString() { + final Address addr = + Address.wrap(BytesValue.fromHexString("0x0000000000000000000000000000000000101010")); + Assert.assertEquals("0x0000000000000000000000000000000000101010", addr.toString()); + } + + @Test + public void accountAddressEquals() { + final Address addr = + Address.wrap(BytesValue.fromHexString("0x0000000000000000000000000000000000101010")); + final Address addr2 = + Address.wrap(BytesValue.fromHexString("0x0000000000000000000000000000000000101010")); + + Assert.assertEquals(addr, addr2); + } + + @Test + public void accountAddresHashCode() { + final Address addr = + Address.wrap(BytesValue.fromHexString("0x0000000000000000000000000000000000101010")); + final Address addr2 = + Address.wrap(BytesValue.fromHexString("0x0000000000000000000000000000000000101010")); + + Assert.assertEquals(addr.hashCode(), addr2.hashCode()); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidAccountAddress() { + Address.wrap(BytesValue.fromHexString("0x00101010")); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestCaseSpec.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestCaseSpec.java new file mode 100755 index 00000000000..394b8e9dcf1 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestCaseSpec.java @@ -0,0 +1,221 @@ +package net.consensys.pantheon.ethereum.vm; + +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static net.consensys.pantheon.ethereum.vm.WorldStateMock.insertAccount; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties({"_info", "postState"}) +public class BlockchainReferenceTestCaseSpec { + + private final String network; + + private final CandidateBlock[] candidateBlocks; + + private final BlockHeaderMock genesisBlockHeader; + + private final BytesValue genesisRLP; + + private final Hash lastBlockHash; + + private final WorldStateArchive worldStateArchive; + + private final MutableBlockchain blockchain; + + private final ProtocolContext protocolContext; + + private static WorldStateArchive buildWorldStateArchive( + final Map accounts) { + final WorldStateArchive worldStateArchive = createInMemoryWorldStateArchive(); + + final MutableWorldState worldState = worldStateArchive.getMutable(); + final WorldUpdater updater = worldState.updater(); + + for (final Map.Entry entry : accounts.entrySet()) { + insertAccount(updater, Address.fromHexString(entry.getKey()), entry.getValue()); + } + + updater.commit(); + worldState.persist(); + + return worldStateArchive; + } + + private static MutableBlockchain buildBlockchain(final BlockHeader genesisBlockHeader) { + final Block genesisBlock = new Block(genesisBlockHeader, BlockBody.empty()); + return new DefaultMutableBlockchain( + genesisBlock, new InMemoryKeyValueStorage(), MainnetBlockHashFunction::createHash); + } + + @JsonCreator + public BlockchainReferenceTestCaseSpec( + @JsonProperty("network") final String network, + @JsonProperty("blocks") final CandidateBlock[] candidateBlocks, + @JsonProperty("genesisBlockHeader") final BlockHeaderMock genesisBlockHeader, + @JsonProperty("genesisRLP") final String genesisRLP, + @JsonProperty("pre") final Map accounts, + @JsonProperty("lastblockhash") final String lastBlockHash) { + this.network = network; + this.candidateBlocks = candidateBlocks; + this.genesisBlockHeader = genesisBlockHeader; + this.genesisRLP = genesisRLP == null ? null : BytesValue.fromHexString(genesisRLP); + this.lastBlockHash = Hash.fromHexString(lastBlockHash); + this.worldStateArchive = buildWorldStateArchive(accounts); + this.blockchain = buildBlockchain(genesisBlockHeader); + this.protocolContext = new ProtocolContext<>(this.blockchain, this.worldStateArchive, null); + } + + public String getNetwork() { + return network; + } + + public CandidateBlock[] getCandidateBlocks() { + return candidateBlocks; + } + + public WorldStateArchive getWorldStateArchive() { + return worldStateArchive; + } + + public BlockHeader getGenesisBlockHeader() { + return genesisBlockHeader; + } + + public MutableBlockchain getBlockchain() { + return blockchain; + } + + public ProtocolContext getProtocolContext() { + return protocolContext; + } + + public Hash getLastBlockHash() { + return lastBlockHash; + } + + public static class BlockHeaderMock extends BlockHeader { + + @JsonCreator + public BlockHeaderMock( + @JsonProperty("parentHash") final String parentHash, + @JsonProperty("uncleHash") final String uncleHash, + @JsonProperty("coinbase") final String coinbase, + @JsonProperty("stateRoot") final String stateRoot, + @JsonProperty("transactionsTrie") final String transactionsTrie, + @JsonProperty("receiptTrie") final String receiptTrie, + @JsonProperty("bloom") final String bloom, + @JsonProperty("difficulty") final String difficulty, + @JsonProperty("number") final String number, + @JsonProperty("gasLimit") final String gasLimit, + @JsonProperty("gasUsed") final String gasUsed, + @JsonProperty("timestamp") final String timestamp, + @JsonProperty("extraData") final String extraData, + @JsonProperty("mixHash") final String mixHash, + @JsonProperty("nonce") final String nonce, + @JsonProperty("hash") final String hash) { + super( + Hash.fromHexString(parentHash), // parentHash + Hash.fromHexString(uncleHash), // ommersHash + Address.fromHexString(coinbase), // coinbase + Hash.fromHexString(stateRoot), // stateRoot + Hash.fromHexString(transactionsTrie), // transactionsRoot + Hash.fromHexString(receiptTrie), // receiptTrie + LogsBloomFilter.fromHexString(bloom), // bloom + UInt256.fromHexString(difficulty), // difficulty + Long.decode(number), // number + Long.decode(gasLimit), // gasLimit + Long.decode(gasUsed), // gasUsed + Long.decode(timestamp), // timestamp + BytesValue.fromHexString(extraData), // extraData + Hash.fromHexString(mixHash), // mixHash + BytesValue.fromHexString(nonce).getLong(0), + header -> Hash.fromHexString(hash)); // nonce + } + } + + @JsonIgnoreProperties({ + "expectExceptionByzantium", + "expectExceptionConstantinople", + "expectExceptionEIP150", + "expectExceptionEIP158", + "expectExceptionFrontier", + "expectExceptionHomestead", + "blocknumber", + "chainname", + "expectExceptionALL", + "chainnetwork" + }) + public static class CandidateBlock { + + private final BytesValue rlp; + + private final Boolean valid; + + @JsonCreator + public CandidateBlock( + @JsonProperty("rlp") final String rlp, + @JsonProperty("blockHeader") final Object blockHeader, + @JsonProperty("transactions") final Object transactions, + @JsonProperty("uncleHeaders") final Object uncleHeaders) { + Boolean valid = true; + // The BLOCK__WrongCharAtRLP_0 test has an invalid character in its rlp string. + BytesValue rlpAttempt = null; + try { + rlpAttempt = BytesValue.fromHexString(rlp); + } catch (final IllegalArgumentException e) { + valid = false; + } + this.rlp = rlpAttempt; + + if (blockHeader == null && transactions == null && uncleHeaders == null) { + valid = false; + } + + this.valid = valid; + } + + public boolean isValid() { + return valid; + } + + public boolean isExecutable() { + return rlp != null; + } + + public Block getBlock() { + final RLPInput input = new BytesValueRLPInput(rlp, false); + input.enterList(); + final BlockHeader header = BlockHeader.readFrom(input, MainnetBlockHashFunction::createHash); + final BlockBody body = + new BlockBody( + input.readList(Transaction::readFrom), + input.readList( + rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash))); + return new Block(header, body); + } + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestTools.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestTools.java new file mode 100755 index 00000000000..c2d61fb8d84 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTestTools.java @@ -0,0 +1,100 @@ +package net.consensys.pantheon.ethereum.vm; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.testutil.JsonTestParameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; + +public class BlockchainReferenceTestTools { + private static final ReferenceTestProtocolSchedules REFERENCE_TEST_PROTOCOL_SCHEDULES = + ReferenceTestProtocolSchedules.create(); + + private static final List NETWORKS_TO_RUN; + + static { + final String networks = + System.getProperty( + "test.ethereum.blockchain.eips", + "FrontierToHomesteadAt5,HomesteadToEIP150At5,HomesteadToDaoAt5,EIP158ToByzantiumAt5," + + "Frontier,Homestead,EIP150,EIP158,Byzantium"); + NETWORKS_TO_RUN = Arrays.asList(networks.split(",")); + } + + private static final JsonTestParameters params = + JsonTestParameters.create(BlockchainReferenceTestCaseSpec.class) + .generator( + (testName, spec, collector) -> { + final String eip = spec.getNetwork(); + if (NETWORKS_TO_RUN.contains(eip)) { + collector.add(testName + "[" + eip + "]", spec); + } + }); + + static { + if (NETWORKS_TO_RUN.isEmpty()) { + params.blacklistAll(); + } + + // TODO: Determine and implement cross-chain validation prevention. + params.blacklist("ChainAtoChainB_BlockHash_(Frontier|Homestead|EIP150|EIP158|Byzantium)"); + // Known bad test. + params.blacklist("RevertPrecompiledTouch_d0g0v0_(EIP158|Byzantium)"); + // Consumes a huge amount of memory + params.blacklist("static_Call1MB1024Calldepth_d1g0v0_Byzantium"); + } + + public static Collection generateTestParametersForConfig(final String[] filePath) { + return params.generate(filePath); + } + + public static void executeTest(final BlockchainReferenceTestCaseSpec spec) { + final MutableWorldState worldState = + spec.getWorldStateArchive().getMutable(spec.getGenesisBlockHeader().getStateRoot()); + final BlockHeader genesisBlockHeader = spec.getGenesisBlockHeader(); + assertThat(worldState.rootHash()).isEqualTo(genesisBlockHeader.getStateRoot()); + + final ProtocolSchedule schedule = + REFERENCE_TEST_PROTOCOL_SCHEDULES.getByName(spec.getNetwork()); + + final MutableBlockchain blockchain = spec.getBlockchain(); + final ProtocolContext context = spec.getProtocolContext(); + + for (final BlockchainReferenceTestCaseSpec.CandidateBlock candidateBlock : + spec.getCandidateBlocks()) { + if (!candidateBlock.isExecutable()) { + return; + } + + try { + final Block block = candidateBlock.getBlock(); + + final ProtocolSpec protocolSpec = + schedule.getByBlockNumber(block.getHeader().getNumber()); + final BlockImporter blockImporter = protocolSpec.getBlockImporter(); + final boolean imported = + blockImporter.importBlock(context, block, HeaderValidationMode.FULL); + + assertThat(imported).isEqualTo(candidateBlock.isValid()); + } catch (final RLPException e) { + Assert.assertFalse(candidateBlock.isValid()); + } + } + + assertThat(blockchain.getChainHeadHash()).isEqualTo(spec.getLastBlockHash()); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/CodeMock.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/CodeMock.java new file mode 100755 index 00000000000..4c971099882 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/CodeMock.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** A mock for representing EVM Code associated with an account. */ +public class CodeMock extends Code { + + /** + * Public constructor. + * + * @param bytes - A hex string representation of the code. + */ + @JsonCreator + public CodeMock(final String bytes) { + super(BytesValue.fromHexString(bytes)); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracerTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracerTest.java new file mode 100755 index 00000000000..3d88954b3f2 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/DebugOperationTracerTest.java @@ -0,0 +1,227 @@ +package net.consensys.pantheon.ethereum.vm; + +import static net.consensys.pantheon.ethereum.core.AddressHelpers.calculateAddressWithRespectTo; +import static net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason.INSUFFICIENT_GAS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.debug.TraceOptions; +import net.consensys.pantheon.ethereum.vm.OperationTracer.ExecuteOperation; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltException; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayDeque; +import java.util.EnumSet; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DebugOperationTracerTest { + + private static final int DEPTH = 4; + private static final Gas INITIAL_GAS = Gas.of(1000); + + @Mock private WorldUpdater worldUpdater; + + @Mock private ExecuteOperation executeOperationAction; + + private final Operation anOperation = + new AbstractOperation(0x02, "MUL", 2, 1, false, 1, null) { + @Override + public Gas cost(final MessageFrame frame) { + return Gas.of(20); + } + + @Override + public void execute(final MessageFrame frame) {} + }; + + @Test + public void shouldRecordProgramCounter() throws Exception { + final MessageFrame frame = validMessageFrame(); + frame.setPC(10); + final TraceFrame traceFrame = traceFrame(frame, Gas.of(50)); + + assertThat(traceFrame.getPc()).isEqualTo(10); + } + + @Test + public void shouldRecordOpcode() throws Exception { + final MessageFrame frame = validMessageFrame(); + final TraceFrame traceFrame = traceFrame(frame, Gas.of(50)); + assertThat(traceFrame.getOpcode()).isEqualTo("MUL"); + } + + @Test + public void shouldRecordDepth() throws Exception { + final MessageFrame frame = validMessageFrame(); + final TraceFrame traceFrame = traceFrame(frame, Gas.of(50)); + assertThat(traceFrame.getDepth()).isEqualTo(DEPTH); + } + + @Test + public void shouldRecordRemainingGas() throws Exception { + final MessageFrame frame = validMessageFrame(); + final Gas currentGasCost = Gas.of(50); + final TraceFrame traceFrame = traceFrame(frame, currentGasCost); + assertThat(traceFrame.getGasRemaining()).isEqualTo(INITIAL_GAS); + } + + @Test + public void shouldRecordStackWhenEnabled() throws Exception { + final MessageFrame frame = validMessageFrame(); + final Bytes32 stackItem1 = Bytes32.fromHexString("0x01"); + final Bytes32 stackItem2 = Bytes32.fromHexString("0x02"); + final Bytes32 stackItem3 = Bytes32.fromHexString("0x03"); + frame.pushStackItem(stackItem1); + frame.pushStackItem(stackItem2); + frame.pushStackItem(stackItem3); + final TraceFrame traceFrame = traceFrame(frame, Gas.ZERO, new TraceOptions(false, false, true)); + assertThat(traceFrame.getStack()).isPresent(); + assertThat(traceFrame.getStack().get()).containsExactly(stackItem1, stackItem2, stackItem3); + } + + @Test + public void shouldNotRecordStackWhenDisabled() throws Exception { + final TraceFrame traceFrame = + traceFrame(validMessageFrame(), Gas.ZERO, new TraceOptions(false, false, false)); + assertThat(traceFrame.getStack()).isEmpty(); + } + + @Test + public void shouldRecordMemoryWhenEnabled() throws Exception { + final MessageFrame frame = validMessageFrame(); + final Bytes32 word1 = Bytes32.fromHexString("0x01"); + final Bytes32 word2 = Bytes32.fromHexString("0x02"); + final Bytes32 word3 = Bytes32.fromHexString("0x03"); + frame.writeMemory(UInt256.ZERO, UInt256.of(32), word1); + frame.writeMemory(UInt256.of(32), UInt256.of(32), word2); + frame.writeMemory(UInt256.of(64), UInt256.of(32), word3); + final TraceFrame traceFrame = traceFrame(frame, Gas.ZERO, new TraceOptions(false, true, false)); + assertThat(traceFrame.getMemory()).isPresent(); + assertThat(traceFrame.getMemory().get()).containsExactly(word1, word2, word3); + } + + @Test + public void shouldNotRecordMemoryWhenDisabled() throws Exception { + final TraceFrame traceFrame = + traceFrame(validMessageFrame(), Gas.ZERO, new TraceOptions(false, false, false)); + assertThat(traceFrame.getMemory()).isEmpty(); + } + + @Test + public void shouldRecordStorageWhenEnabled() throws Exception { + final MessageFrame frame = validMessageFrame(); + final Map updatedStorage = setupStorageForCapture(frame); + final TraceFrame traceFrame = traceFrame(frame, Gas.ZERO, new TraceOptions(true, false, false)); + assertThat(traceFrame.getStorage()).isPresent(); + assertThat(traceFrame.getStorage().get()).isEqualTo(updatedStorage); + } + + @Test + public void shouldNotRecordStorageWhenDisabled() throws Exception { + final TraceFrame traceFrame = + traceFrame(validMessageFrame(), Gas.ZERO, new TraceOptions(false, false, false)); + assertThat(traceFrame.getStorage()).isEmpty(); + } + + @Test + public void shouldCaptureFrameWhenExceptionalHaltOccurs() throws Exception { + final EnumSet expectedHaltReasons = EnumSet.of(INSUFFICIENT_GAS); + final ExceptionalHaltException expectedException = + new ExceptionalHaltException(expectedHaltReasons); + doThrow(expectedException).when(executeOperationAction).execute(); + final MessageFrame frame = validMessageFrame(); + final Map updatedStorage = setupStorageForCapture(frame); + + final DebugOperationTracer tracer = + new DebugOperationTracer(new TraceOptions(true, true, true)); + assertThatThrownBy( + () -> tracer.traceExecution(frame, Optional.of(Gas.of(50)), executeOperationAction)) + .isSameAs(expectedException); + + final TraceFrame traceFrame = getOnlyTraceFrame(tracer); + assertThat(traceFrame.getStorage()).contains(updatedStorage); + } + + private TraceFrame traceFrame(final MessageFrame frame, final Gas currentGasCost) + throws Exception { + return traceFrame(frame, currentGasCost, new TraceOptions(false, false, false)); + } + + private TraceFrame traceFrame( + final MessageFrame frame, final Gas currentGasCost, final TraceOptions traceOptions) + throws Exception { + final DebugOperationTracer tracer = new DebugOperationTracer(traceOptions); + tracer.traceExecution(frame, Optional.of(currentGasCost), executeOperationAction); + return getOnlyTraceFrame(tracer); + } + + private MessageFrame validMessageFrame() { + final MessageFrame frame = validMessageFrameBuilder().build(); + frame.setCurrentOperation(anOperation); + frame.setPC(10); + return frame; + } + + private TraceFrame getOnlyTraceFrame(final DebugOperationTracer tracer) { + assertThat(tracer.getTraceFrames()).hasSize(1); + return tracer.getTraceFrames().get(0); + } + + private MessageFrame.Builder validMessageFrameBuilder() { + return MessageFrame.builder() + .type(MessageFrame.Type.MESSAGE_CALL) + .messageFrameStack(new ArrayDeque<>()) + .blockchain(new TestBlockchain()) + .worldState(worldUpdater) + .initialGas(INITIAL_GAS) + .contract(calculateAddressWithRespectTo(Address.ID, 1)) + .address(calculateAddressWithRespectTo(Address.ID, 2)) + .originator(calculateAddressWithRespectTo(Address.ID, 3)) + .gasPrice(Wei.of(25)) + .inputData(BytesValue.EMPTY) + .sender(calculateAddressWithRespectTo(Address.ID, 4)) + .value(Wei.of(30)) + .apparentValue(Wei.of(35)) + .code(new Code()) + .blockHeader(new BlockHeaderTestFixture().buildHeader()) + .depth(DEPTH) + .completer(c -> {}); + } + + private Map setupStorageForCapture(final MessageFrame frame) { + final MutableAccount account = mock(MutableAccount.class); + when(worldUpdater.getMutable(frame.getRecipientAddress())).thenReturn(account); + + final Map updatedStorage = new TreeMap<>(); + updatedStorage.put(UInt256.ZERO, UInt256.of(233)); + updatedStorage.put(UInt256.ONE, UInt256.of(2424)); + when(account.getUpdatedStorage()).thenReturn(updatedStorage); + final Bytes32 word1 = Bytes32.fromHexString("0x01"); + final Bytes32 word2 = Bytes32.fromHexString("0x02"); + final Bytes32 word3 = Bytes32.fromHexString("0x03"); + frame.writeMemory(UInt256.ZERO, UInt256.of(32), word1); + frame.writeMemory(UInt256.of(32), UInt256.of(32), word2); + frame.writeMemory(UInt256.of(64), UInt256.of(32), word3); + return updatedStorage; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/EnvironmentInformation.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/EnvironmentInformation.java new file mode 100755 index 00000000000..d2805532e35 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/EnvironmentInformation.java @@ -0,0 +1,175 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A wrapper for the environmental information that corresponds to a particular message call or + * contract creation. + * + *

Note: this is meant to map to I in Section 9.3 "Execution Environment" in the Yellow Paper + * Revision 59dccd. Its implementation will be completed as the VM implementation itself becomes + * more complete. + */ +public class EnvironmentInformation { + + private final Address accountAddress; + + private BlockHeader blockHeader; + + private final Address callerAddress; + + private final Code code; + + private final BytesValue data; + + private final int depth; + + private final Wei gasPrice; + + private final Address originAddress; + + private final Wei value; + + private final Gas gas; + + /** + * Public constructor. + * + * @param code The code to be executed. + * @param account The address of the currently executing account. + * @param caller The caller address. + * @param origin The sender address of the original transaction. + * @param data The data of the current environment that pertains to the input data passed with the + * message call instruction or transaction. + * @param value The deposited value by the instruction/transaction responsible for this execution. + * @param gasPrice The gas price specified by the originating transaction. + */ + @JsonCreator + public EnvironmentInformation( + @JsonProperty("address") final String account, + @JsonProperty("caller") final String caller, + @JsonProperty("code") final CodeMock code, + @JsonProperty("data") final String data, + @JsonProperty("gas") final String gas, + @JsonProperty("gasPrice") final String gasPrice, + @JsonProperty("origin") final String origin, + @JsonProperty("value") final String value) { + this( + code, + 0, + account == null ? null : Address.fromHexString(account), + caller == null ? null : Address.fromHexString(caller), + origin == null ? null : Address.fromHexString(origin), + data == null ? null : BytesValue.fromHexString(data), + value == null ? null : Wei.fromHexString(value), + gasPrice == null ? null : Wei.fromHexString(gasPrice), + gas == null ? null : Gas.fromHexString(gas)); + } + + private EnvironmentInformation( + final Code code, + final int depth, + final Address accountAddress, + final Address callerAddress, + final Address originAddress, + final BytesValue data, + final Wei value, + final Wei gasPrice, + final Gas gas) { + this.code = code; + this.depth = depth; + this.accountAddress = accountAddress; + this.callerAddress = callerAddress; + this.originAddress = originAddress; + this.data = data; + this.value = value; + this.gasPrice = gasPrice; + this.gas = gas; + } + + /** + * Assigns the block header. + * + * @param blockHeader A @{link BlockHeader}. + */ + public void setBlockHeader(final BlockHeader blockHeader) { + this.blockHeader = blockHeader; + } + + /** @return The block header. */ + public BlockHeader getBlockHeader() { + return blockHeader; + } + + /** @return The address of the currently executing account. */ + public Address getAccountAddress() { + return accountAddress; + } + + /** @return Address of the caller. */ + public Address getCallerAddress() { + return callerAddress; + } + + /** @return The call value. */ + public Wei getValue() { + return value; + } + + /** @return Code to be executed. */ + public Code getCode() { + return code; + } + + /** @return The input data to be used. */ + public BytesValue getData() { + return data; + } + + /** @return The call depth of the current message-call/contract creation. */ + public int getDepth() { + return depth; + } + + /** @return The gas price specified by the originating transaction. */ + public Wei getGasPrice() { + return gasPrice; + } + + /** @return The amount of gas available. */ + public Gas getGas() { + return gas; + } + + /** @return The sender address of the original transaction. */ + public Address getOriginAddress() { + return originAddress; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder + .append("Executing ") + .append(code.toString()) + .append("\nCode: ") + .append(code) + .append("\nData: ") + .append(data) + .append("\nAccount: ") + .append(accountAddress) + .append("\nBlock header: \n ") + .append(blockHeader.toString().replaceAll("\n", "\n ")) + .append("\nCaller: ") + .append(callerAddress); + + return builder.toString(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTestTools.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTestTools.java new file mode 100755 index 00000000000..a52c01dbf3c --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTestTools.java @@ -0,0 +1,124 @@ +package net.consensys.pantheon.ethereum.vm; + +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogSeries; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.worldstate.DebuggableMutableWorldState; +import net.consensys.pantheon.testutil.JsonTestParameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class GeneralStateReferenceTestTools { + private static final ReferenceTestProtocolSchedules REFERENCE_TEST_PROTOCOL_SCHEDULES = + ReferenceTestProtocolSchedules.create(); + + private static TransactionProcessor transactionProcessor(final String name) { + return REFERENCE_TEST_PROTOCOL_SCHEDULES + .getByName(name) + .getByBlockNumber(0) + .getTransactionProcessor(); + } + + private static final List EIPS_TO_RUN; + + static { + final String eips = + System.getProperty( + "test.ethereum.state.eips", "Frontier,Homestead,EIP150,EIP158,Byzantium"); + EIPS_TO_RUN = Arrays.asList(eips.split(",")); + } + + private static final JsonTestParameters params = + JsonTestParameters.create(GeneralStateTestCaseSpec.class, GeneralStateTestCaseEipSpec.class) + .generator( + (testName, stateSpec, collector) -> { + final String prefix = testName + "-"; + for (final Map.Entry> entry : + stateSpec.finalStateSpecs().entrySet()) { + final String eip = entry.getKey(); + if (!EIPS_TO_RUN.contains(eip)) { + continue; + } + final List eipSpecs = entry.getValue(); + if (eipSpecs.size() == 1) { + collector.add(prefix + eip, eipSpecs.get(0)); + } else { + for (int i = 0; i < eipSpecs.size(); i++) { + collector.add(prefix + eip + '[' + i + ']', eipSpecs.get(i)); + } + } + } + }); + + static { + if (EIPS_TO_RUN.isEmpty()) { + params.blacklistAll(); + } + // Known incorrect test. + params.blacklist("RevertPrecompiledTouch-(EIP158|Byzantium)"); + // Gas integer value is too large to construct a valid transaction. + params.blacklist("OverflowGasRequire"); + // Consumes a huge amount of memory + params.blacklist("static_Call1MB1024Calldepth-Byzantium"); + } + + public static Collection generateTestParametersForConfig(final String[] filePath) { + return params.generate(filePath); + } + + public static void executeTest(final GeneralStateTestCaseEipSpec spec) { + final BlockHeader blockHeader = spec.blockHeader(); + final WorldState initialWorldState = spec.initialWorldState(); + final Transaction transaction = spec.transaction(); + + final MutableWorldState worldState = new DebuggableMutableWorldState(initialWorldState); + + // Several of the GeneralStateTests check if the transaction could potentially + // consume more gas than is left for the block it's attempted to be included in. + // This check is performed within the `BlockImporter` rather than inside the + // `TransactionProcessor`, so these tests are skipped. + if (transaction.getGasLimit() > blockHeader.getGasLimit() - blockHeader.getGasUsed()) { + return; + } + + final TransactionProcessor processor = transactionProcessor(spec.eip()); + final WorldUpdater worldStateUpdater = worldState.updater(); + final TransactionProcessor.Result result = + processor.processTransaction( + new TestBlockchain(), + worldStateUpdater, + blockHeader, + transaction, + blockHeader.getCoinbase()); + + if (!result.isInvalid()) { + worldStateUpdater.commit(); + } + + // Check the world state root hash. + final Hash expectedRootHash = spec.expectedRootHash(); + assertEquals( + "Unexpected world state root hash; computed state: " + worldState, + expectedRootHash, + worldState.rootHash()); + + // Check the logs. + final Hash expectedLogsHash = spec.expectedLogsHash(); + final LogSeries logs = result.getLogs(); + assertEquals( + "Unmatched logs hash. Generated logs: " + logs, + expectedLogsHash, + Hash.hash(RLP.encode(logs::writeTo))); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseEipSpec.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseEipSpec.java new file mode 100755 index 00000000000..b325f327933 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseEipSpec.java @@ -0,0 +1,72 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.WorldState; + +import java.util.function.Supplier; + +public class GeneralStateTestCaseEipSpec { + + private final String eip; + + // Creating the actual transaction is expensive because the json test file does not give us the + // transaction but rather the private key to sign, and so we have to do the signing. And we don't + // want to do this for 22k general state tests up-front (during + // GeneralStateReferenceTest.getTestParametersForConfig) because 1) that makes the parameters + // generation of + // GeneralStateReferenceTest take more than a minute, which means that much time waiting before + // anything + // is run, which isn't friendly and 2) this makes it harder to parallelize this step. Anyway, this + // is why this is a supplier: calling get() actually does the signing. + private final Supplier transactionSupplier; + + private final WorldState initialWorldState; + + private final Hash expectedRootHash; + + // The keccak256 hash of the RLP encoding of the log series + private final Hash expectedLogsHash; + + private final BlockHeader blockHeader; + + GeneralStateTestCaseEipSpec( + final String eip, + final Supplier transactionSupplier, + final WorldState initialWorldState, + final Hash expectedRootHash, + final Hash expectedLogsHash, + final BlockHeader blockHeader) { + this.eip = eip; + this.transactionSupplier = transactionSupplier; + this.initialWorldState = initialWorldState; + this.expectedRootHash = expectedRootHash; + this.expectedLogsHash = expectedLogsHash; + this.blockHeader = blockHeader; + } + + String eip() { + return eip; + } + + WorldState initialWorldState() { + return initialWorldState; + } + + Hash expectedRootHash() { + return expectedRootHash; + } + + Hash expectedLogsHash() { + return expectedLogsHash; + } + + Transaction transaction() { + return transactionSupplier.get(); + } + + BlockHeader blockHeader() { + return blockHeader; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseSpec.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseSpec.java new file mode 100755 index 00000000000..2118bad4539 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/GeneralStateTestCaseSpec.java @@ -0,0 +1,114 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderMock; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** A Transaction test case specification. */ +@JsonIgnoreProperties("_info") +public class GeneralStateTestCaseSpec { + + private final Map> finalStateSpecs; + + @JsonCreator + public GeneralStateTestCaseSpec( + @JsonProperty("env") final BlockHeaderMock blockHeader, + @JsonProperty("pre") final WorldStateMock initialWorldState, + @JsonProperty("post") final Map> postSection, + @JsonProperty("transaction") final StateTestVersionedTransaction versionedTransaction) { + this.finalStateSpecs = + generate(blockHeader, initialWorldState, postSection, versionedTransaction); + } + + private Map> generate( + final BlockHeader blockHeader, + final WorldStateMock initialWorldState, + final Map> postSections, + final StateTestVersionedTransaction versionedTransaction) { + + initialWorldState.persist(); + final Map> res = new HashMap<>(postSections.size()); + for (final Map.Entry> entry : postSections.entrySet()) { + final String eip = entry.getKey(); + final List post = entry.getValue(); + final List specs = new ArrayList<>(post.size()); + for (final PostSection p : post) { + final Supplier txSupplier = () -> versionedTransaction.get(p.indexes); + specs.add( + new GeneralStateTestCaseEipSpec( + eip, txSupplier, initialWorldState, p.rootHash, p.logsHash, blockHeader)); + } + res.put(eip, specs); + } + return res; + } + + Map> finalStateSpecs() { + return finalStateSpecs; + } + + /** + * Indexes in the "transaction" part of the general state spec json, which allow tests to vary the + * input transaction of the tests based on the hard-fork. + */ + public static class Indexes { + + public final int gas; + public final int data; + public final int value; + + @JsonCreator + public Indexes( + @JsonProperty("gas") final int gas, + @JsonProperty("data") final int data, + @JsonProperty("value") final int value) { + this.gas = gas; + this.data = data; + this.value = value; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + final Indexes other = (Indexes) obj; + return data == other.data && gas == other.gas && value == other.value; + } + + @Override + public int hashCode() { + return Objects.hash(data, gas, value); + } + } + + /** Represents the "post" part of a general state test json _for a specific hard-fork_. */ + public static class PostSection { + + private final Hash rootHash; + private final Hash logsHash; + private final Indexes indexes; + + @JsonCreator + public PostSection( + @JsonProperty("hash") final String hash, + @JsonProperty("logs") final String logs, + @JsonProperty("indexes") final Indexes indexes) { + this.rootHash = Hash.fromHexString(hash); + this.logsHash = Hash.fromHexString(logs); + this.indexes = indexes; + } + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/LogMock.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/LogMock.java new file mode 100755 index 00000000000..ca51b304db4 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/LogMock.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.LogTopic; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class LogMock extends Log { + + /** + * Represents a mock log object for testing. Populated from json data. + * + * @param address The address of the account generating the log. + * @param data The data associated with the log. + * @param topics The topics associated with the log. + */ + @JsonCreator + public LogMock( + @JsonProperty("address") final String address, + @JsonProperty("data") final String data, + @JsonProperty("topics") final String[] topics) { + super( + Address.fromHexString(address), + BytesValue.fromHexString(data), + Arrays.stream(topics) + .map(s -> LogTopic.wrap(BytesValue.fromHexString(s))) + .collect(Collectors.toList())); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/MemoryTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/MemoryTest.java new file mode 100755 index 00000000000..ca84e9791f6 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/MemoryTest.java @@ -0,0 +1,117 @@ +package net.consensys.pantheon.ethereum.vm; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.uint.UInt256; + +import com.google.common.base.Strings; +import org.junit.Test; + +public class MemoryTest { + + private final Memory memory = new Memory(); + private static final Bytes32 WORD1 = fillBytes32(1); + private static final Bytes32 WORD2 = fillBytes32(2); + private static final Bytes32 WORD3 = fillBytes32(3); + private static final Bytes32 WORD4 = fillBytes32(4); + + @Test + public void shouldSetAndGetMemoryByWord() { + final UInt256 index = UInt256.of(20); + final Bytes32 value = Bytes32.fromHexString("0xABCDEF"); + memory.setWord(index, value); + assertThat(memory.getWord(index)).isEqualTo(value); + } + + @Test + public void shouldSetMemoryWhenLengthEqualToSourceLength() { + final BytesValue value = BytesValues.concatenate(WORD1, WORD2, WORD3); + memory.setBytes(UInt256.ZERO, UInt256.of(value.size()), value); + assertThat(memory.getWord(UInt256.of(0))).isEqualTo(WORD1); + assertThat(memory.getWord(UInt256.of(32))).isEqualTo(WORD2); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(WORD3); + } + + @Test + public void shouldSetMemoryWhenLengthLessThanSourceLength() { + final BytesValue value = BytesValues.concatenate(WORD1, WORD2, WORD3); + memory.setBytes(UInt256.ZERO, UInt256.of(64), value); + assertThat(memory.getWord(UInt256.of(0))).isEqualTo(WORD1); + assertThat(memory.getWord(UInt256.of(32))).isEqualTo(WORD2); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(Bytes32.ZERO); + } + + @Test + public void shouldSetMemoryWhenLengthGreaterThanSourceLength() { + final BytesValue value = BytesValues.concatenate(WORD1, WORD2); + memory.setBytes(UInt256.ZERO, UInt256.of(96), value); + assertThat(memory.getWord(UInt256.of(0))).isEqualTo(WORD1); + assertThat(memory.getWord(UInt256.of(32))).isEqualTo(WORD2); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(Bytes32.ZERO); + } + + @Test + public void shouldClearMemoryAfterSourceDataWhenLengthGreaterThanSourceLength() { + memory.setWord(UInt256.of(64), WORD3); + memory.setWord(UInt256.of(96), WORD4); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(WORD3); + assertThat(memory.getWord(UInt256.of(96))).isEqualTo(WORD4); + + final BytesValue value = BytesValues.concatenate(WORD1, WORD2); + memory.setBytes(UInt256.ZERO, UInt256.of(96), value); + assertThat(memory.getWord(UInt256.of(0))).isEqualTo(WORD1); + assertThat(memory.getWord(UInt256.of(32))).isEqualTo(WORD2); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(Bytes32.ZERO); + assertThat(memory.getWord(UInt256.of(96))).isEqualTo(WORD4); + } + + @Test + public void shouldClearMemoryAfterSourceDataWhenLengthGreaterThanSourceLengthWithMemoryOffset() { + memory.setWord(UInt256.of(64), WORD3); + memory.setWord(UInt256.of(96), WORD4); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(WORD3); + assertThat(memory.getWord(UInt256.of(96))).isEqualTo(WORD4); + + final BytesValue value = BytesValues.concatenate(WORD1, WORD2); + memory.setBytes(UInt256.of(10), UInt256.of(96), value); + assertThat(memory.getWord(UInt256.of(10))).isEqualTo(WORD1); + assertThat(memory.getWord(UInt256.of(42))).isEqualTo(WORD2); + assertThat(memory.getWord(UInt256.of(74))).isEqualTo(Bytes32.ZERO); + // Word 4 got partially cleared because of the starting offset. + assertThat(memory.getWord(UInt256.of(106))) + .isEqualTo( + Bytes32.fromHexString( + "0x4444444444444444444444444444444444444444444400000000000000000000")); + } + + @Test + public void shouldClearMemoryAfterSourceDataWhenSourceOffsetPlusLengthGreaterThanSourceLength() { + memory.setWord(UInt256.of(64), WORD3); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(WORD3); + + final BytesValue value = BytesValues.concatenate(WORD1, WORD2); + memory.setBytes(UInt256.ZERO, UInt256.of(32), UInt256.of(64), value); + assertThat(memory.getWord(UInt256.of(0))).isEqualTo(WORD2); + assertThat(memory.getWord(UInt256.of(32))).isEqualTo(Bytes32.ZERO); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(WORD3); + } + + @Test + public void shouldClearMemoryWhenSourceOffsetIsGreaterThanSourceLength() { + memory.setWord(UInt256.of(64), WORD3); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(WORD3); + + final BytesValue value = BytesValues.concatenate(WORD1, WORD2); + memory.setBytes(UInt256.ZERO, UInt256.of(94), UInt256.of(64), value); + assertThat(memory.getWord(UInt256.of(0))).isEqualTo(Bytes32.ZERO); + assertThat(memory.getWord(UInt256.of(32))).isEqualTo(Bytes32.ZERO); + assertThat(memory.getWord(UInt256.of(64))).isEqualTo(WORD3); + } + + private static Bytes32 fillBytes32(final long value) { + return Bytes32.fromHexString(Strings.repeat(Long.toString(value), 64)); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStackTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStackTest.java new file mode 100755 index 00000000000..c54a86ab0d5 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/PreAllocatedOperandStackTest.java @@ -0,0 +1,93 @@ +package net.consensys.pantheon.ethereum.vm; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.util.bytes.Bytes32; + +import org.junit.Test; + +public class PreAllocatedOperandStackTest { + + @Test + public void construction() { + final OperandStack stack = new PreAllocatedOperandStack(1); + assertThat(stack.size()).isEqualTo(0); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_NegativeMaximumSize() { + new PreAllocatedOperandStack(-1); + } + + @Test(expected = IllegalStateException.class) + public void push_StackOverflow() { + final OperandStack stack = new PreAllocatedOperandStack(1); + stack.push(Bytes32.fromHexString("0x01")); + stack.push(Bytes32.fromHexString("0x02")); + } + + @Test(expected = IllegalStateException.class) + public void pop_StackUnderflow() { + final OperandStack stack = new PreAllocatedOperandStack(1); + stack.pop(); + } + + @Test + public void pushPop() { + final OperandStack stack = new PreAllocatedOperandStack(1); + stack.push(Bytes32.fromHexString("0x01")); + assertThat(stack.size()).isEqualTo(1); + assertThat(stack.pop()).isEqualTo(Bytes32.fromHexString("0x01")); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void get_NegativeOffset() { + final OperandStack stack = new PreAllocatedOperandStack(1); + stack.get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void get_IndexGreaterThanSize() { + final OperandStack stack = new PreAllocatedOperandStack(1); + stack.push(Bytes32.fromHexString("0x01")); + stack.get(2); + } + + @Test + public void get() { + final OperandStack stack = new PreAllocatedOperandStack(3); + stack.push(Bytes32.fromHexString("0x01")); + stack.push(Bytes32.fromHexString("0x02")); + stack.push(Bytes32.fromHexString("0x03")); + assertThat(stack.size()).isEqualTo(3); + assertThat(stack.get(0)).isEqualTo(Bytes32.fromHexString("0x03")); + assertThat(stack.get(1)).isEqualTo(Bytes32.fromHexString("0x02")); + assertThat(stack.get(2)).isEqualTo(Bytes32.fromHexString("0x01")); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void set_NegativeOffset() { + final OperandStack stack = new PreAllocatedOperandStack(1); + stack.get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void set_IndexGreaterThanSize() { + final OperandStack stack = new PreAllocatedOperandStack(1); + stack.push(Bytes32.fromHexString("0x01")); + stack.get(2); + } + + @Test + public void set() { + final OperandStack stack = new PreAllocatedOperandStack(3); + stack.push(Bytes32.fromHexString("0x01")); + stack.push(Bytes32.fromHexString("0x02")); + stack.push(Bytes32.fromHexString("0x03")); + stack.set(2, Bytes32.fromHexString("0x04")); + assertThat(stack.size()).isEqualTo(3); + assertThat(stack.get(0)).isEqualTo(Bytes32.fromHexString("0x03")); + assertThat(stack.get(1)).isEqualTo(Bytes32.fromHexString("0x02")); + assertThat(stack.get(2)).isEqualTo(Bytes32.fromHexString("0x04")); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/ReferenceTestProtocolSchedules.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/ReferenceTestProtocolSchedules.java new file mode 100755 index 00000000000..d04cdd773af --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/ReferenceTestProtocolSchedules.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs; +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; + +import java.util.Map; +import java.util.function.Function; + +import com.google.common.collect.ImmutableMap; + +public class ReferenceTestProtocolSchedules { + + private static final int CHAIN_ID = 1; + + public static ReferenceTestProtocolSchedules create() { + final ImmutableMap.Builder> builder = ImmutableMap.builder(); + builder.put("Frontier", createSchedule(MainnetProtocolSpecs::frontier)); + builder.put("FrontierToHomesteadAt5", frontierToHomesteadAt5()); + builder.put("Homestead", createSchedule(MainnetProtocolSpecs::homestead)); + builder.put("HomesteadToEIP150At5", homesteadToEip150At5()); + builder.put("HomesteadToDaoAt5", homesteadToDaoAt5()); + builder.put("EIP150", createSchedule(MainnetProtocolSpecs::tangerineWhistle)); + builder.put( + "EIP158", + createSchedule( + protocolSpecLookup -> + MainnetProtocolSpecs.spuriousDragon(CHAIN_ID, protocolSpecLookup))); + builder.put("EIP158ToByzantiumAt5", eip158ToByzantiumAt5()); + builder.put( + "Byzantium", + createSchedule( + protocolSpecLookup -> MainnetProtocolSpecs.byzantium(CHAIN_ID, protocolSpecLookup))); + return new ReferenceTestProtocolSchedules(builder.build()); + } + + private final Map> schedules; + + private ReferenceTestProtocolSchedules(final Map> schedules) { + this.schedules = schedules; + } + + public ProtocolSchedule getByName(final String name) { + return schedules.get(name); + } + + private static ProtocolSchedule createSchedule( + final Function, ProtocolSpec> specCreator) { + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone(0, specCreator.apply(protocolSchedule)); + return protocolSchedule; + } + + private static ProtocolSchedule frontierToHomesteadAt5() { + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone(0, MainnetProtocolSpecs.frontier(protocolSchedule)); + protocolSchedule.putMilestone(5, MainnetProtocolSpecs.homestead(protocolSchedule)); + return protocolSchedule; + } + + private static ProtocolSchedule homesteadToEip150At5() { + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone(0, MainnetProtocolSpecs.homestead(protocolSchedule)); + protocolSchedule.putMilestone(5, MainnetProtocolSpecs.tangerineWhistle(protocolSchedule)); + return protocolSchedule; + } + + private static ProtocolSchedule homesteadToDaoAt5() { + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + final ProtocolSpec homestead = MainnetProtocolSpecs.homestead(protocolSchedule); + protocolSchedule.putMilestone(0, homestead); + protocolSchedule.putMilestone(5, MainnetProtocolSpecs.daoRecoveryInit(protocolSchedule)); + protocolSchedule.putMilestone(6, MainnetProtocolSpecs.daoRecoveryTransition(protocolSchedule)); + protocolSchedule.putMilestone(15, homestead); + return protocolSchedule; + } + + private static ProtocolSchedule eip158ToByzantiumAt5() { + final MutableProtocolSchedule protocolSchedule = new MutableProtocolSchedule<>(); + protocolSchedule.putMilestone( + 0, MainnetProtocolSpecs.spuriousDragon(CHAIN_ID, protocolSchedule)); + protocolSchedule.putMilestone(5, MainnetProtocolSpecs.byzantium(CHAIN_ID, protocolSchedule)); + return protocolSchedule; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/StateTestVersionedTransaction.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/StateTestVersionedTransaction.java new file mode 100755 index 00000000000..213b432565e --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/StateTestVersionedTransaction.java @@ -0,0 +1,88 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.PrivateKey; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the "transaction" part of the JSON of a general state tests. + * + *

This contains information for a transaction, indirectly versioned by milestone. More + * precisely, this there is 2 steps to transform this class (which again, represent what is in the + * JSON) to an actual transaction to test: + * + *

    + *
  • in the state test json, gas, value and data for the transaction are arrays. This is how + * state tests deal with milestone versioning: for a given milestone, the actual value to use + * is defined by the indexes of the "post" section of the json. Those indexes are passed to + * this class in {@link #get(Indexes)}. + *
  • the signature of the transaction is not provided in the json directly. Instead, the private + * key of the sender is provided, and the transaction must thus be signed (also in {@link + * #get(Indexes)}) through {@link Transaction.Builder#signAndBuild(KeyPair)}. + *
+ */ +public class StateTestVersionedTransaction { + + private final long nonce; + private final Wei gasPrice; + @Nullable private final Address to; + + private final KeyPair keys; + + private final List gasLimits; + private final List values; + private final List payloads; + + /** Constructor for populating a mock account with json data. */ + @JsonCreator + public StateTestVersionedTransaction( + @JsonProperty("nonce") final String nonce, + @JsonProperty("gasPrice") final String gasPrice, + @JsonProperty("gasLimit") final String[] gasLimit, + @JsonProperty("to") final String to, + @JsonProperty("value") final String[] value, + @JsonProperty("secretKey") final String secretKey, + @JsonProperty("data") final String[] data) { + + this.nonce = Long.decode(nonce); + this.gasPrice = Wei.fromHexString(gasPrice); + this.to = to.isEmpty() ? null : Address.fromHexString(to); + this.keys = KeyPair.create(PrivateKey.create(Bytes32.fromHexString(secretKey))); + + this.gasLimits = parseArray(gasLimit, Gas::fromHexString); + this.values = parseArray(value, Wei::fromHexString); + this.payloads = parseArray(data, BytesValue::fromHexString); + } + + private static List parseArray(final String[] array, final Function parseFct) { + final List res = new ArrayList<>(array.length); + for (final String str : array) { + res.add(parseFct.apply(str)); + } + return res; + } + + public Transaction get(final GeneralStateTestCaseSpec.Indexes indexes) { + return Transaction.builder() + .nonce(nonce) + .gasPrice(gasPrice) + .gasLimit(gasLimits.get(indexes.gas).asUInt256().toLong()) + .to(to) + .value(values.get(indexes.value)) + .payload(payloads.get(indexes.data)) + .signAndBuild(keys); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/TestBlockchain.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/TestBlockchain.java new file mode 100755 index 00000000000..b5d525560a6 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/TestBlockchain.java @@ -0,0 +1,98 @@ +package net.consensys.pantheon.ethereum.vm; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import net.consensys.pantheon.ethereum.chain.BlockAddedObserver; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.ChainHead; +import net.consensys.pantheon.ethereum.chain.TransactionLocation; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.Optional; + +/** + * A blockchain mock for the Ethereum reference tests. + * + *

The only method this class is used for is {@link TestBlockchain#getBlockHashByNumber} The + * Ethereum reference tests for VM exection (VMTests) and transaction processing (GeneralStateTests) + * require a block's hash to be to be the hash of the string of it's block number. + */ +public class TestBlockchain implements Blockchain { + + private static Hash generateTestBlockHash(final long number) { + final byte[] bytes = Long.toString(number).getBytes(UTF_8); + return Hash.hash(BytesValue.wrap(bytes)); + } + + @Override + public Optional getBlockHashByNumber(final long number) { + return Optional.of(generateTestBlockHash(number)); + } + + @Override + public ChainHead getChainHead() { + throw new UnsupportedOperationException(); + } + + @Override + public long getChainHeadBlockNumber() { + throw new UnsupportedOperationException(); + } + + @Override + public Hash getChainHeadHash() { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getTransactionLocation(final Hash transactionHash) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getBlockHeader(final long blockNumber) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getBlockHeader(final Hash blockHeaderHash) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getBlockBody(final Hash blockHeaderHash) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional> getTxReceipts(final Hash blockHeaderHash) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getTotalDifficultyByHash(final Hash blockHeaderHash) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getTransactionByHash(final Hash transactionHash) { + throw new UnsupportedOperationException(); + } + + @Override + public long observeBlockAdded(final BlockAddedObserver observer) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeObserver(final long observerId) { + throw new UnsupportedOperationException(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTest.java new file mode 100755 index 00000000000..c41aa91365f --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTest.java @@ -0,0 +1,164 @@ +package net.consensys.pantheon.ethereum.vm; + +import static net.consensys.pantheon.ethereum.vm.OperationTracer.NO_TRACING; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSpecs; +import net.consensys.pantheon.ethereum.mainnet.MutableProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.vm.ehalt.ExceptionalHaltException; +import net.consensys.pantheon.ethereum.worldstate.DefaultMutableWorldState; +import net.consensys.pantheon.testutil.JsonTestParameters; + +import java.util.ArrayDeque; +import java.util.Collection; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** The VM operation testing framework entry point. */ +@RunWith(Parameterized.class) +public class VMReferenceTest extends AbstractRetryingTest { + + /** The path where all of the VM test configuration files live. */ + private static final String[] TEST_CONFIG_FILE_DIR_PATHS = { + "VMTests/vmArithmeticTest", + "VMTests/vmBitwiseLogicOperation", + "VMTests/vmBlockInfoTest", + "VMTests/vmEnvironmentalInfo", + "VMTests/vmIOandFlowOperations", + "VMTests/vmLogTest", + "VMTests/vmSha3Test", + "VMTests/vmSystemOperations" + }; + + // The blacklisted test cases fall into two categories: + // + // 1. Incorrect Test Cases: The VMTests have known bugs with accessing + // non-existent accounts. This corresponds to test cases involving + // the BALANCE, EXTCODESIZE, EXTCODECOPY, and SELFDESTRUCT operations. + // + // 2. Test Cases for CALL, CALLCODE, and CALLCREATE: The VMTests do not + // fully test these operations and the mocking does not add much value. + // Additionally, the GeneralStateTests provide coverage of these + // operations so the proper functionality does get tested somewhere. + private static final String[] BLACKLISTED_TESTS = { + "balance0", + "balanceAddressInputTooBig", + "balanceCaller3", + "balanceAddressInputTooBigRightMyAddress", + "ExtCodeSizeAddressInputTooBigRightMyAddress", + "env1", + "extcodecopy0AddressTooBigRight", + "PostToNameRegistrator0", + "CallToReturn1", + "CallRecursiveBomb0", + "createNameRegistratorValueTooHigh", + "suicideNotExistingAccount", + "callstatelessToReturn1", + "CallRecursiveBomb1", + "ABAcallsSuicide1", + "suicideSendEtherToMe", + "suicide0", + "CallToNameRegistrator0", + "callstatelessToNameRegistrator0", + "PostToReturn1", + "callcodeToReturn1", + "ABAcalls0", + "CallRecursiveBomb2", + "CallRecursiveBomb3", + "ABAcallsSuicide0", + "callcodeToNameRegistrator0", + "CallToPrecompiledContract", + "createNameRegistrator" + }; + private final String name; + + private final VMReferenceTestCaseSpec spec; + + @Parameters(name = "Name: {0}") + public static Collection getTestParametersForConfig() throws Exception { + return JsonTestParameters.create(VMReferenceTestCaseSpec.class) + .blacklist(BLACKLISTED_TESTS) + .generate(TEST_CONFIG_FILE_DIR_PATHS); + } + + public VMReferenceTest(final String name, final VMReferenceTestCaseSpec spec) { + this.name = name; + this.spec = spec; + } + + @Override + protected void runTest() { + final MutableWorldState worldState = new DefaultMutableWorldState(spec.getInitialWorldState()); + final EnvironmentInformation execEnv = spec.getExec(); + + final ProtocolSpec protocolSpec = + MainnetProtocolSpecs.frontier(new MutableProtocolSchedule<>()); + + final MessageFrame frame = + MessageFrame.builder() + .type(MessageFrame.Type.MESSAGE_CALL) + .messageFrameStack(new ArrayDeque<>()) + .blockchain(new TestBlockchain()) + .worldState(worldState.updater()) + .initialGas(spec.getExec().getGas()) + .contract(execEnv.getAccountAddress()) + .address(execEnv.getAccountAddress()) + .originator(execEnv.getOriginAddress()) + .gasPrice(execEnv.getGasPrice()) + .inputData(execEnv.getData()) + .sender(execEnv.getCallerAddress()) + .value(execEnv.getValue()) + .apparentValue(execEnv.getValue()) + .code(execEnv.getCode()) + .blockHeader(execEnv.getBlockHeader()) + .depth(execEnv.getDepth()) + .completer(c -> {}) + .build(); + + // This is normally set inside the containing message executing the code. + frame.setState(MessageFrame.State.CODE_EXECUTING); + + try { + protocolSpec.getEvm().runToHalt(frame, NO_TRACING); + } catch (final ExceptionalHaltException ehe) { + if (!spec.isExceptionHaltExpected()) + System.err.println( + String.format( + "Test %s incurred in an exceptional halt exception for reasons: %s.", + name, ehe.getReasons())); + } + + if (spec.isExceptionHaltExpected()) { + assertTrue( + "VM should have exceptionally halted", + frame.getState() == MessageFrame.State.EXCEPTIONAL_HALT); + } else { + // This is normally performed when the message processor executing the VM + // executes to completion successfuly. + frame.getWorldState().commit(); + + assertFalse( + "VM should not have exceptionally halted", + frame.getState() == MessageFrame.State.EXCEPTIONAL_HALT); + assertEquals("VM output differs", spec.getOut(), frame.getOutputData()); + assertEquals( + "Final world state differs", spec.getFinalWorldState().rootHash(), worldState.rootHash()); + + final Gas actualGas = frame.getRemainingGas(); + final Gas expectedGas = spec.getFinalGas(); + final Gas difference = + (expectedGas.compareTo(actualGas) > 0) + ? expectedGas.minus(actualGas) + : actualGas.minus(expectedGas); + assertEquals( + "Final gas does not match, with difference of " + difference, expectedGas, actualGas); + } + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTestCaseSpec.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTestCaseSpec.java new file mode 100755 index 00000000000..0e65c071366 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/VMReferenceTestCaseSpec.java @@ -0,0 +1,106 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.BlockHeaderMock; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A VM test case specification. + * + *

Note: this class will be auto-generated with the JSON test specification. + */ +@JsonIgnoreProperties({"_info", "callcreates", "logs"}) +public class VMReferenceTestCaseSpec { + + /** The environment information to execute. */ + private final EnvironmentInformation exec; + + /** The VM output. */ + private final BytesValue out; + + private final Gas finalGas; + + private final WorldStateMock initialWorldState; + + private final boolean exceptionalHaltExpected; + + private final WorldStateMock finalWorldState; + + @JsonCreator + public VMReferenceTestCaseSpec( + @JsonProperty("exec") final EnvironmentInformation exec, + @JsonProperty("env") final BlockHeaderMock env, + @JsonProperty("gas") final String finalGas, + @JsonProperty("out") final String out, + @JsonProperty("pre") final WorldStateMock initialWorldState, + @JsonProperty("post") final WorldStateMock finalWorldState) { + this.exec = exec; + this.initialWorldState = initialWorldState; + this.initialWorldState.persist(); + exec.setBlockHeader(env); + + if (finalGas != null && out != null && finalWorldState != null) { + this.finalGas = Gas.fromHexString(finalGas); + this.finalWorldState = finalWorldState; + this.out = BytesValue.fromHexString(out); + this.exceptionalHaltExpected = false; + } else { + this.exceptionalHaltExpected = true; + // These values should never be checked if this is a test case that + // exceptionally halts. + this.finalGas = null; + this.finalWorldState = null; + this.out = null; + } + } + + /** + * Returns the environment information to execute. + * + * @return The environment information to execute. + */ + public EnvironmentInformation getExec() { + return exec; + } + + /** + * Returns the initial world state. + * + * @return The initial world state to use when setting up the test. + */ + public WorldState getInitialWorldState() { + return initialWorldState; + } + + /** + * Returns the final world state. + * + * @return The final world state to use when setting up the test. + */ + public WorldState getFinalWorldState() { + return finalWorldState; + } + + public Gas getFinalGas() { + return finalGas; + } + + /** + * Return the expected VM return value. + * + * @return The expected VM return value. + */ + public BytesValue getOut() { + return out; + } + + /** @return True if this test case should expect the VM to exceptionally halt; otherwise false. */ + public boolean isExceptionHaltExpected() { + return exceptionalHaltExpected; + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/WorldStateMock.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/WorldStateMock.java new file mode 100755 index 00000000000..8ce7e81d39d --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/WorldStateMock.java @@ -0,0 +1,89 @@ +package net.consensys.pantheon.ethereum.vm; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.worldstate.DebuggableMutableWorldState; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** Represent a mock worldState for testing. */ +public class WorldStateMock extends DebuggableMutableWorldState { + + public static class AccountMock { + private final long nonce; + private final Wei balance; + private final BytesValue code; + private final Map storage; + + private static final Map parseStorage(final Map values) { + final Map storage = new HashMap<>(); + for (final Map.Entry entry : values.entrySet()) { + storage.put(UInt256.fromHexString(entry.getKey()), UInt256.fromHexString(entry.getValue())); + } + return storage; + } + + public AccountMock( + @JsonProperty("nonce") final String nonce, + @JsonProperty("balance") final String balance, + @JsonProperty("storage") final Map storage, + @JsonProperty("code") final String code) { + this.nonce = Long.decode(nonce); + this.balance = Wei.fromHexString(balance); + this.code = BytesValue.fromHexString(code); + this.storage = parseStorage(storage); + } + + public long nonce() { + return nonce; + } + + public Wei balance() { + return balance; + } + + public BytesValue code() { + return code; + } + + public Map storage() { + return storage; + } + } + + public static void insertAccount( + final WorldUpdater updater, final Address address, final AccountMock toCopy) { + final MutableAccount account = updater.getOrCreate(address); + account.setNonce(toCopy.nonce()); + account.setBalance(toCopy.balance()); + account.setCode(toCopy.code()); + for (final Map.Entry entry : toCopy.storage().entrySet()) { + account.setStorageValue(entry.getKey(), entry.getValue()); + } + } + + @JsonCreator + public static WorldStateMock create(final Map accounts) { + final WorldStateMock worldState = new WorldStateMock(); + final WorldUpdater updater = worldState.updater(); + + for (final Map.Entry entry : accounts.entrySet()) { + insertAccount(updater, Address.fromHexString(entry.getKey()), entry.getValue()); + } + + updater.commit(); + return worldState; + } + + private WorldStateMock() { + super(); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/blockchain/.keep b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/blockchain/.keep new file mode 100755 index 00000000000..e69de29bb2d diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/generalstate/.keep b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/generalstate/.keep new file mode 100755 index 00000000000..e69de29bb2d diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/Create2OperationTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/Create2OperationTest.java new file mode 100755 index 00000000000..927551e3ae2 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/Create2OperationTest.java @@ -0,0 +1,126 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.mainnet.ConstantinopleGasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class Create2OperationTest { + + private final String sender; + private final String salt; + private final String code; + private final String expectedAddress; + private final int expectedGas; + private final MessageFrame messageFrame = mock(MessageFrame.class); + private final Create2Operation operation = + new Create2Operation(new ConstantinopleGasCalculator()); + + @Parameters(name = "sender: {0}, salt: {1}, code: {2}") + public static Object[][] params() { + return new Object[][] { + { + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00", + "0x4D1A2e2bB4F88F0250f26Ffff098B0b30B26BF38", + 32006 + }, + { + "0xdeadbeef00000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00", + "0xB928f69Bb1D91Cd65274e3c79d8986362984fDA3", + 32006 + }, + { + "0xdeadbeef00000000000000000000000000000000", + "0x000000000000000000000000feed000000000000000000000000000000000000", + "0x00", + "0xD04116cDd17beBE565EB2422F2497E06cC1C9833", + 32006 + }, + { + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xdeadbeef", + "0x70f2b2914A2a4b783FaEFb75f459A580616Fcb5e", + 32006 + }, + { + "0x00000000000000000000000000000000deadbeef", + "0x00000000000000000000000000000000000000000000000000000000cafebabe", + "0xdeadbeef", + "0x60f3f640a8508fC6a86d45DF051962668E1e8AC7", + 32006 + }, + { + "0x00000000000000000000000000000000deadbeef", + "0x00000000000000000000000000000000000000000000000000000000cafebabe", + "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "0x1d8bfDC5D46DC4f61D6b6115972536eBE6A8854C", + 32012 + }, + { + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x", + "0xE33C0C7F7df4809055C3ebA6c09CFe4BaF1BD9e0", + 32000 + } + }; + } + + public Create2OperationTest( + final String sender, + final String salt, + final String code, + final String expectedAddress, + final int expectedGas) { + this.sender = sender; + this.salt = salt; + this.code = code; + this.expectedAddress = expectedAddress; + this.expectedGas = expectedGas; + } + + @Before + public void setUp() { + when(messageFrame.getSenderAddress()).thenReturn(Address.fromHexString(sender)); + final Bytes32 memoryOffset = Bytes32.fromHexString("0xFF"); + final BytesValue codeBytes = BytesValue.fromHexString(code); + final UInt256 memoryLength = UInt256.of(codeBytes.size()); + when(messageFrame.getStackItem(1)).thenReturn(memoryOffset); + when(messageFrame.getStackItem(2)).thenReturn(memoryLength.getBytes()); + when(messageFrame.getStackItem(3)).thenReturn(Bytes32.fromHexString(salt)); + when(messageFrame.readMemory(memoryOffset.asUInt256(), memoryLength)).thenReturn(codeBytes); + when(messageFrame.memoryWordSize()).thenReturn(UInt256.of(500)); + when(messageFrame.calculateMemoryExpansion(any(), any())).thenReturn(UInt256.of(500)); + } + + @Test + public void shouldCalculateAddress() { + final Address targetContractAddress = operation.targetContractAddress(messageFrame); + assertThat(targetContractAddress).isEqualTo(Address.fromHexString(expectedAddress)); + } + + @Test + public void shouldCalculateGasPrice() { + assertThat(operation.cost(messageFrame)).isEqualTo(Gas.of(expectedGas)); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/SarOperationTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/SarOperationTest.java new file mode 100755 index 00000000000..d023ed4f3e3 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/SarOperationTest.java @@ -0,0 +1,153 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.mainnet.SpuriousDragonGasCalculator; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class SarOperationTest { + + private final String number; + private final String shift; + private final String expectedResult; + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final SarOperation operation = new SarOperation(gasCalculator); + + private MessageFrame frame; + + static String[][] testData = { + { + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0xc000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x100", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x101", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x0", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x100", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x4000000000000000000000000000000000000000000000000000000000000000", + "0xfe", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xf8", + "0x000000000000000000000000000000000000000000000000000000000000007f" + }, + { + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xfe", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x100", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + }; + + @Parameterized.Parameters(name = "{index}: {0}, {1}, {2}") + public static Iterable data() { + return Arrays.asList((Object[][]) testData); + } + + public SarOperationTest(final String number, final String shift, final String expectedResult) { + this.number = number; + this.shift = shift; + this.expectedResult = expectedResult; + } + + @Test + public void shiftOperation() { + frame = mock(MessageFrame.class); + when(frame.popStackItem()) + .thenReturn(Bytes32.fromHexStringLenient(shift)) + .thenReturn(Bytes32.fromHexString(number)); + operation.execute(frame); + verify(frame).pushStackItem(Bytes32.fromHexString(expectedResult)); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperationTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperationTest.java new file mode 100755 index 00000000000..31cba3712f9 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShlOperationTest.java @@ -0,0 +1,103 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.mainnet.SpuriousDragonGasCalculator; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class ShlOperationTest { + + private final String number; + private final String shift; + private final String expectedResult; + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final ShlOperation operation = new ShlOperation(gasCalculator); + + private MessageFrame frame; + + static String[][] testData = { + { + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000008" + }, + { + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x000000000000000000000000000000000000000000000000000000000000001e" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000010" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x100", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe" + } + }; + + @Parameterized.Parameters(name = "{index}: {0}, {1}, {2}") + public static Iterable data() { + return Arrays.asList((Object[][]) testData); + } + + public ShlOperationTest(final String number, final String shift, final String expectedResult) { + this.number = number; + this.shift = shift; + this.expectedResult = expectedResult; + } + + @Test + public void shiftOperation() { + frame = mock(MessageFrame.class); + when(frame.popStackItem()) + .thenReturn(Bytes32.fromHexStringLenient(shift)) + .thenReturn(Bytes32.fromHexString(number)); + operation.execute(frame); + verify(frame).pushStackItem(Bytes32.fromHexString(expectedResult)); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperationTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperationTest.java new file mode 100755 index 00000000000..67949ec0f12 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/vm/operations/ShrOperationTest.java @@ -0,0 +1,122 @@ +package net.consensys.pantheon.ethereum.vm.operations; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.mainnet.SpuriousDragonGasCalculator; +import net.consensys.pantheon.ethereum.vm.GasCalculator; +import net.consensys.pantheon.ethereum.vm.MessageFrame; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class ShrOperationTest { + private final String number; + private final String shift; + private final String expectedResult; + + private final GasCalculator gasCalculator = new SpuriousDragonGasCalculator(); + private final ShrOperation operation = new ShrOperation(gasCalculator); + + private MessageFrame frame; + + static String[][] testData = { + { + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x00", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "0x000000000000000000000000000000000000000000000000000000000000000f", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x100", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x8000000000000000000000000000000000000000000000000000000000000000", + "0x101", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x0", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x01", + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0xff", + "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "0x100", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x01", + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + }; + + @Parameterized.Parameters(name = "{index}: {0}, {1}, {2}") + public static Iterable data() { + return Arrays.asList((Object[][]) testData); + } + + public ShrOperationTest(final String number, final String shift, final String expectedResult) { + this.number = number; + this.shift = shift; + this.expectedResult = expectedResult; + } + + @Test + public void shiftOperation() { + frame = mock(MessageFrame.class); + when(frame.popStackItem()) + .thenReturn(Bytes32.fromHexStringLenient(shift)) + .thenReturn(Bytes32.fromHexString(number)); + operation.execute(frame); + verify(frame).pushStackItem(Bytes32.fromHexString(expectedResult)); + } +} diff --git a/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldStateTest.java b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldStateTest.java new file mode 100755 index 00000000000..d6918cb0a22 --- /dev/null +++ b/ethereum/core/src/test/java/net/consensys/pantheon/ethereum/worldstate/DefaultMutableWorldStateTest.java @@ -0,0 +1,371 @@ +package net.consensys.pantheon.ethereum.worldstate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.trie.MerklePatriciaTrie; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.NavigableMap; +import java.util.TreeMap; + +import org.junit.Test; + +// TODO: make that an abstract mutable world state test, and create sub-class for all world state +// implementations. +public class DefaultMutableWorldStateTest { + // The following test cases are loosely derived from the testTransactionToItself + // GeneralStateReferenceTest. + + private static final Address ADDRESS = + Address.fromHexString("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + + private static MutableWorldState createEmpty(final KeyValueStorage storage) { + return new DefaultMutableWorldState(new KeyValueStorageWorldStateStorage(storage)); + } + + private static MutableWorldState createEmpty() { + return createEmpty(new InMemoryKeyValueStorage()); + } + + @Test + public void rootHash_Empty() { + final MutableWorldState worldState = createEmpty(); + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + + worldState.persist(); + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + } + + @Test + public void containsAccount_AccountDoesNotExist() { + final WorldState worldState = createEmpty(); + assertNull(worldState.get(ADDRESS)); + } + + @Test + public void containsAccount_AccountExists() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + updater.createAccount(ADDRESS).setBalance(Wei.of(100000)); + updater.commit(); + assertNotNull(worldState.get(ADDRESS)); + assertEquals( + Hash.fromHexString("0xa3e1c133a5a51b03399ed9ad0380f3182e9e18322f232b816dd4b9094f871e1b"), + worldState.rootHash()); + } + + @Test + public void removeAccount_AccountDoesNotExist() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + updater.deleteAccount(ADDRESS); + updater.commit(); + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + + worldState.persist(); + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + } + + @Test + public void removeAccount_UpdatedAccount() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + updater.createAccount(ADDRESS).setBalance(Wei.of(100000)); + updater.deleteAccount(ADDRESS); + updater.commit(); + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + + worldState.persist(); + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + } + + @Test + public void removeAccount_AccountExists() { + // Create a world state with one account + final MutableWorldState worldState = createEmpty(); + WorldUpdater updater = worldState.updater(); + updater.createAccount(ADDRESS).setBalance(Wei.of(100000)); + updater.commit(); + assertNotNull(worldState.get(ADDRESS)); + assertNotEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + + // Delete account + updater = worldState.updater(); + updater.deleteAccount(ADDRESS); + assertNull(updater.get(ADDRESS)); + assertNull(updater.getMutable(ADDRESS)); + updater.commit(); + assertNull(updater.get(ADDRESS)); + + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + } + + @Test + public void removeAccount_AccountExistsAndIsPersisted() { + // Create a world state with one account + final MutableWorldState worldState = createEmpty(); + WorldUpdater updater = worldState.updater(); + updater.createAccount(ADDRESS).setBalance(Wei.of(100000)); + updater.commit(); + worldState.persist(); + assertNotNull(worldState.get(ADDRESS)); + assertNotEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + + // Delete account + updater = worldState.updater(); + updater.deleteAccount(ADDRESS); + assertNull(updater.get(ADDRESS)); + assertNull(updater.getMutable(ADDRESS)); + // Check account is gone after committing + updater.commit(); + assertNull(updater.get(ADDRESS)); + // And after persisting + worldState.persist(); + assertNull(updater.get(ADDRESS)); + + assertEquals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, worldState.rootHash()); + } + + @Test + public void commitAndPersist() { + final KeyValueStorage storage = new InMemoryKeyValueStorage(); + final MutableWorldState worldState = createEmpty(storage); + final WorldUpdater updater = worldState.updater(); + final Wei newBalance = Wei.of(100000); + final Hash expectedRootHash = + Hash.fromHexString("0xa3e1c133a5a51b03399ed9ad0380f3182e9e18322f232b816dd4b9094f871e1b"); + + // Update account and assert we get the expected response from updater + updater.createAccount(ADDRESS).setBalance(newBalance); + assertNotNull(updater.get(ADDRESS)); + assertEquals(newBalance, updater.get(ADDRESS).getBalance()); + + // Commit and check assertions + updater.commit(); + assertEquals(expectedRootHash, worldState.rootHash()); + assertNotNull(worldState.get(ADDRESS)); + assertEquals(newBalance, worldState.get(ADDRESS).getBalance()); + + // Check that storage is empty before persisting + assertEquals(0, storage.entries().count()); + + // Persist and re-run assertions + worldState.persist(); + assertNotEquals(0, storage.entries().count()); + assertEquals(expectedRootHash, worldState.rootHash()); + assertNotNull(worldState.get(ADDRESS)); + assertEquals(newBalance, worldState.get(ADDRESS).getBalance()); + + // Create new world state and check that it can access modified address + final MutableWorldState newWorldState = + new DefaultMutableWorldState( + expectedRootHash, new KeyValueStorageWorldStateStorage(storage)); + assertEquals(expectedRootHash, newWorldState.rootHash()); + assertNotNull(newWorldState.get(ADDRESS)); + assertEquals(newBalance, newWorldState.get(ADDRESS).getBalance()); + } + + @Test + public void getAccountNonce_AccountExists() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + updater.createAccount(ADDRESS).setNonce(1L); + updater.commit(); + assertEquals(1L, worldState.get(ADDRESS).getNonce()); + assertEquals( + Hash.fromHexString("0x9648b05cc2eef5513ae2edfe16bfcedb3d1c60ffb5dff3fc501bd3e4ae39f536"), + worldState.rootHash()); + } + + @Test + public void replaceAccountNonce() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.createAccount(ADDRESS); + account.setNonce(1L); + account.setNonce(2L); + updater.commit(); + assertEquals(2L, worldState.get(ADDRESS).getNonce()); + assertEquals( + Hash.fromHexString("0x7f64d13e61301a5154a5f06483a38572629e977b316cbe5a28b5f0522010a4bf"), + worldState.rootHash()); + } + + @Test + public void getAccountBalance_AccountExists() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + updater.createAccount(ADDRESS).setBalance(Wei.of(100000)); + updater.commit(); + assertEquals(Wei.of(100000), worldState.get(ADDRESS).getBalance()); + } + + @Test + public void replaceAccountBalance() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.createAccount(ADDRESS); + account.setBalance(Wei.of(100000)); + account.setBalance(Wei.of(200000)); + updater.commit(); + assertEquals(Wei.of(200000), worldState.get(ADDRESS).getBalance()); + assertEquals( + Hash.fromHexString("0xbfa4e0598cc2b810a8ccc4a2d9a4c575574d05c9c4a7f915e6b8545953a5051e"), + worldState.rootHash()); + } + + @Test + public void setStorageValue_ZeroValue() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.createAccount(ADDRESS); + account.setBalance(Wei.of(100000)); + account.setStorageValue(UInt256.ZERO, UInt256.ZERO); + updater.commit(); + assertEquals(UInt256.ZERO, worldState.get(ADDRESS).getStorageValue(UInt256.ZERO)); + assertEquals( + Hash.fromHexString("0xa3e1c133a5a51b03399ed9ad0380f3182e9e18322f232b816dd4b9094f871e1b"), + worldState.rootHash()); + } + + @Test + public void setStorageValue_NonzeroValue() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.createAccount(ADDRESS); + account.setBalance(Wei.of(100000)); + account.setStorageValue(UInt256.ONE, UInt256.of(2)); + updater.commit(); + assertEquals(UInt256.of(2), worldState.get(ADDRESS).getStorageValue(UInt256.ONE)); + assertEquals( + Hash.fromHexString("0xd31ce0bf3bf8790083a8ebde418244fda3b1cca952d7119ed244f86d03044656"), + worldState.rootHash()); + } + + @Test + public void replaceStorageValue_NonzeroValue() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.createAccount(ADDRESS); + account.setBalance(Wei.of(100000)); + account.setStorageValue(UInt256.ONE, UInt256.of(2)); + account.setStorageValue(UInt256.ONE, UInt256.of(3)); + updater.commit(); + assertEquals(UInt256.of(3), worldState.get(ADDRESS).getStorageValue(UInt256.ONE)); + assertEquals( + Hash.fromHexString("0x1d0ddb5079fe5b8689124b68c9e5bb3f4d8e13c2f7489d24f088c78fd45e058d"), + worldState.rootHash()); + } + + @Test + public void replaceStorageValue_ZeroValue() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.createAccount(ADDRESS); + account.setBalance(Wei.of(100000)); + account.setStorageValue(UInt256.ONE, UInt256.of(2)); + account.setStorageValue(UInt256.ONE, UInt256.ZERO); + updater.commit(); + assertEquals( + Hash.fromHexString("0xa3e1c133a5a51b03399ed9ad0380f3182e9e18322f232b816dd4b9094f871e1b"), + worldState.rootHash()); + } + + @Test + public void replaceAccountCode() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.createAccount(ADDRESS); + account.setBalance(Wei.of(100000)); + account.setCode(BytesValue.of(1, 2, 3)); + account.setCode(BytesValue.of(3, 2, 1)); + updater.commit(); + assertEquals(BytesValue.of(3, 2, 1), worldState.get(ADDRESS).getCode()); + assertEquals( + Hash.fromHexString("0xc14f5e30581de9155ea092affa665fad83bcd9f98e45c4a42885b9b36d939702"), + worldState.rootHash()); + } + + @Test + public void revert() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater1 = worldState.updater(); + final MutableAccount account1 = updater1.createAccount(ADDRESS); + account1.setBalance(Wei.of(200000)); + updater1.commit(); + + final WorldUpdater updater2 = worldState.updater(); + final MutableAccount account2 = updater2.getMutable(ADDRESS); + account2.setBalance(Wei.of(300000)); + assertEquals(Wei.of(300000), updater2.get(ADDRESS).getBalance()); + + updater2.revert(); + assertEquals(Wei.of(200000), updater2.get(ADDRESS).getBalance()); + + updater2.commit(); + assertEquals(Wei.of(200000), worldState.get(ADDRESS).getBalance()); + + assertEquals( + Hash.fromHexString("0xbfa4e0598cc2b810a8ccc4a2d9a4c575574d05c9c4a7f915e6b8545953a5051e"), + worldState.rootHash()); + } + + @Test + public void shouldReturnNullForGetMutableWhenAccountDeletedInAncestor() { + final MutableWorldState worldState = createEmpty(); + final WorldUpdater updater1 = worldState.updater(); + final MutableAccount account1 = updater1.createAccount(ADDRESS); + updater1.commit(); + assertThat(updater1.get(ADDRESS)).isEqualTo(account1); + updater1.deleteAccount(ADDRESS); + + final WorldUpdater updater2 = updater1.updater(); + assertThat(updater2.get(ADDRESS)).isEqualTo(null); + + final WorldUpdater updater3 = updater2.updater(); + assertThat(updater3.getMutable(ADDRESS)).isEqualTo(null); + } + + @Test + public void shouldCombineUnchangedAndChangedValuesWhenRetrievingStorageEntries() { + final MutableWorldState worldState = createEmpty(); + WorldUpdater updater = worldState.updater(); + MutableAccount account = updater.createAccount(ADDRESS); + account.setBalance(Wei.of(100000)); + account.setStorageValue(UInt256.ONE, UInt256.of(2)); + account.setStorageValue(UInt256.of(2), UInt256.of(5)); + updater.commit(); + + updater = worldState.updater(); + account = updater.getMutable(ADDRESS); + account.setStorageValue(UInt256.ONE, UInt256.of(3)); + account.setStorageValue(UInt256.of(3), UInt256.of(6)); + + final NavigableMap storage = account.storageEntriesFrom(Hash.ZERO, 10); + + final NavigableMap expected = new TreeMap<>(); + expected.put(hash(UInt256.ONE), UInt256.of(3)); + expected.put(hash(UInt256.of(2)), UInt256.of(5)); + expected.put(hash(UInt256.of(3)), UInt256.of(6)); + assertThat(storage).isEqualTo(expected); + } + + private Hash hash(final UInt256 key) { + return Hash.hash(key.getBytes()); + } +} diff --git a/ethereum/core/src/test/resources/log4j2.xml b/ethereum/core/src/test/resources/log4j2.xml new file mode 100755 index 00000000000..82f9c0b4cb2 --- /dev/null +++ b/ethereum/core/src/test/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + INFO + + + + + + + + + + + + diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_1200000.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_1200000.blocks new file mode 100755 index 0000000000000000000000000000000000000000..c967730cd18119c286cf2719e7c2fc02fa87269d GIT binary patch literal 541 zcmey#B=wU?bivbTF+YA;mQ2axF_CIZ`;%@LEI8=({{R=mg@U7{KY4u@$e!_iaqoC+ z)#}!*>D$g+k-9A8Dm|sEMEJu(r@szYCc973N?o($>YwPC)(I1q9rhJ-G_`C}TOh!z z-nDv$+!BUmWk)N_Hr*7~eUx>x;&N^@a0C)4Dtvj5Gl&n6xCkr(ap zKze4*+5?P9`dc3-Z&4(uU?(HPNC$0#Z@Q09HE0&{Z)|pdex#u#>=UEcjO)#e%&aZ$ zsU;ch>G_6wrh100IhlE-6$^f8f0;DvpUv^5KA+aRcV5`?eSg``hG}ndR#?Bg72Pbp Rt>fLy$%j)OY7`zg005xGjhFxc literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_300005.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_300005.blocks new file mode 100755 index 0000000000000000000000000000000000000000..ca0f10e8151b146a2c09b5a7cd25344336f1784f GIT binary patch literal 655 zcmey#)bo=`e8IQAbM=3A-D3DUWy8-c-#@J05SSdWlH1VMu>X3--jxfT7Ra9QeR1!2 zY}M-4t?Ap&T#>pg$2LZ`nDS0=kp3EJaw)x4SWoz{L)%l(D-q-(1mZ(R_5 z``N4WD<++Lb>}S4?dfy(y)8by(&c--)tp-OBBph>ofpI#yis1Qzd_fjNmV_pI4*Z~ z!ZNd|SO4ESQMNkQpfF~|0uyP|ZBtJ@pR&36TZf$d>33VzCv*SE|B>t3 z%Du$8gaK&k6r+76)>D2Y2>BlQ_o(9U>B~I=x*eS=ySGh!CZET^&>+1axibI47puc_ zV)D&W)cs#P6h6(UnC@)wLa$=$57!IT4;MJhGp?1E+_3+h`TNO>FFR_8Io_PLu{ihm O-on(2_x`>WI{*MJgv+x4 literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_300006.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_300006.blocks new file mode 100755 index 0000000000000000000000000000000000000000..3f4f785798f446e019300839c83677c8df6447e9 GIT binary patch literal 655 zcmey#)bo=`e8DfuOROyl0xpx@tTq%57YYQ`r8W%E}D7PM5(*!F4qoA{zxGVyj(o*teQ z|G;FoqtmKB;drYHCZ_`zRJ?f>dG$?dn1@o;EUTh3r?!`D`*AR@Mz(i~y5RY@)fGD# z8Adv2W1HMOQ zSWp_J5*b)({-IMiPl*HtCLl>KXvce)39ijO4*hhKk|R%^0jg= zu`Xd~W{jUO<$twE%`V+3Ix)^ut6rOMy;;j5J+H$`_1>8mj~N&mq!+C6`=Pe6)tBGv zv!B5I9sI>Jj_mpOwdVLT_lA~LEw|aHEl~C~J+t%i>ccBk4tH*}Y!i`x&!Axa*6s1q NgRKw6>`%`>0008C!Y=>- literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400000.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400000.blocks new file mode 100755 index 0000000000000000000000000000000000000000..88580907b31869f9c92394dccae901a36ef2536c GIT binary patch literal 8419 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 literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400001.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400001.blocks new file mode 100755 index 0000000000000000000000000000000000000000..dcb491f0831043f4e0626bdddfdfc3f991c7c35f GIT binary patch literal 21790 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 C2}1?| literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400002.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/blockvalidation/block_4400002.blocks new file mode 100755 index 0000000000000000000000000000000000000000..00ffe382d5431a80ca1349cd160e562e5f6a827f GIT binary patch literal 6918 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 literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis-olympic.json b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis-olympic.json new file mode 100755 index 00000000000..69d7a04c24e --- /dev/null +++ b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis-olympic.json @@ -0,0 +1,48 @@ +{ + "alloc": { + "0000000000000000000000000000000000000001": { + "balance": "01" + }, + "0000000000000000000000000000000000000002": { + "balance": "01" + }, + "0000000000000000000000000000000000000003": { + "balance": "01" + }, + "0000000000000000000000000000000000000004": { + "balance": "01" + }, + "dbdbdb2cbd23b783741e8d7fcf51e459b497e4a6": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + }, + "e6716f9544a56c530d868e4bfbacb172315bdead": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + }, + "b9c015918bdaba24b4ff057a92a3873d6eb201be": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + }, + "1a26338f0d905e295fccb71fa9ea849ffa12aaf4": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + }, + "2ef47100e0787b915105fd5e3f4ff6752079d5cb": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + }, + "cd2a3d9f938e13cd947ec05abc7fe734df8dd826": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + }, + "6c386a4b26f73c802f34673f7248bb118f97424a": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + }, + "e4157b34ea9615cfbde6b4fda419828124b70c78": { + "balance": "1606938044258990275541962092341162602522202993782792835301376" + } + }, + "nonce": "0x000000000000002a", + "difficulty": "0x020000", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "timestamp": "0x00", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0x", + "gasLimit": "0x2fefd8" +} diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis1.json b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis1.json new file mode 100755 index 00000000000..4e2e5739b2c --- /dev/null +++ b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis1.json @@ -0,0 +1,24 @@ +{ + "config": { + "chainId": 15, + "homesteadBlock": 0, + "eip155Block": 0, + "eip158Block": 0 + }, + "alloc": { + "0x0000000000000000000000000000000000000001": { + "balance": "111111111" + }, + "0x0000000000000000000000000000000000000002": { + "balance": "222222222" + } + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x0000001", + "extraData": "", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000107", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x00" +} diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis2.json b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis2.json new file mode 100755 index 00000000000..ac4b57b8ce3 --- /dev/null +++ b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/chain/genesis2.json @@ -0,0 +1,17 @@ +{ + "config": { + "chainId": 15, + "homesteadBlock": 0, + "eip155Block": 0, + "eip158Block": 0 + }, + "alloc": {}, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x0000001", + "extraData": "", + "gasLimit": "0x2fefd8", + "nonce": "0xb5f308680190e572", + "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x00" +} diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_1.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_1.blocks new file mode 100755 index 0000000000000000000000000000000000000000..d286fafde746d2cd9e53a107d3f70cb2d94ef99e GIT binary patch literal 537 zcmey#B=(a@aKV+Q=?*{2*8Mmjup`Ui>eqDFg%TGgWSKmh^`?h+u1H-La+RLaRU-Uhq0?W7E0f))urAHh4LTt%Cei$r$3@0SD$g+k-9A8Dm|sEMEJu(r@szYCc973N?o($>YwPC)(I1q9rhJ-G_`C}TOh!z z-nDv$+!BUmWk)N_Hr*7~eUx>x;&N^@a0C)4Dtvj5Gl&n6xCkr(ap zKze4*+5?P9`dc3-Z&4(uU?(HPNC$0#Z@Q09HE0&{Z)|pdex#u#>=UEcjO)#e%&aZ$ zsU;ch>G_6wrh100IhlE-6$^f8f0;DvpUv^5KA+aRcV5`?eSg``hG}ndR#?Bg72Pbp Rt>fLy$%j)OY7`zg005xGjhFxc literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_300005.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_300005.blocks new file mode 100755 index 0000000000000000000000000000000000000000..ca0f10e8151b146a2c09b5a7cd25344336f1784f GIT binary patch literal 655 zcmey#)bo=`e8IQAbM=3A-D3DUWy8-c-#@J05SSdWlH1VMu>X3--jxfT7Ra9QeR1!2 zY}M-4t?Ap&T#>pg$2LZ`nDS0=kp3EJaw)x4SWoz{L)%l(D-q-(1mZ(R_5 z``N4WD<++Lb>}S4?dfy(y)8by(&c--)tp-OBBph>ofpI#yis1Qzd_fjNmV_pI4*Z~ z!ZNd|SO4ESQMNkQpfF~|0uyP|ZBtJ@pR&36TZf$d>33VzCv*SE|B>t3 z%Du$8gaK&k6r+76)>D2Y2>BlQ_o(9U>B~I=x*eS=ySGh!CZET^&>+1axibI47puc_ zV)D&W)cs#P6h6(UnC@)wLa$=$57!IT4;MJhGp?1E+_3+h`TNO>FFR_8Io_PLu{ihm O-on(2_x`>WI{*MJgv+x4 literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_300006.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_300006.blocks new file mode 100755 index 0000000000000000000000000000000000000000..3f4f785798f446e019300839c83677c8df6447e9 GIT binary patch literal 655 zcmey#)bo=`e8DfuOROyl0xpx@tTq%57YYQ`r8W%E}D7PM5(*!F4qoA{zxGVyj(o*teQ z|G;FoqtmKB;drYHCZ_`zRJ?f>dG$?dn1@o;EUTh3r?!`D`*AR@Mz(i~y5RY@)fGD# z8Adv2W1HMOQ zSWp_J5*b)({-IMiPl*HtCLl>KXvce)39ijO4*hhKk|R%^0jg= zu`Xd~W{jUO<$twE%`V+3Ix)^ut6rOMy;;j5J+H$`_1>8mj~N&mq!+C6`=Pe6)tBGv zv!B5I9sI>Jj_mpOwdVLT_lA~LEw|aHEl~C~J+t%i>ccBk4tH*}Y!i`x&!Axa*6s1q NgRKw6>`%`>0008C!Y=>- literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400000.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400000.blocks new file mode 100755 index 0000000000000000000000000000000000000000..88580907b31869f9c92394dccae901a36ef2536c GIT binary patch literal 8419 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 literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400001.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400001.blocks new file mode 100755 index 0000000000000000000000000000000000000000..dcb491f0831043f4e0626bdddfdfc3f991c7c35f GIT binary patch literal 21790 zcmeIabwE|i`aZrl-K~OjgHlqG(xrfuv@{4((jncgq|yuNkOnCQq$H$M>F$nA$#>&9 zUhp3Docp_<``>T>vuECS)_R_K--9SDShL`DOqBSXU`gYy7^ zJUl3nt{DR`AdnEuOBfIq2n2&gW{d_0#uKgc03m?L@IT>`d4N!`B*{P?(A$!M$;e(q zuOov20WcxLSTN8uI|vEHhXeyg#)BavLjudez=3?oKx7~gm7s?Q^v-Y~dr52$4^J{L zaeN;OFHbBF5SAAe6$r`4qgV+GBnK@i2m+yjsbM{!cM^nVK`?M&kT8e{fz zfrL5W;JoNyKzM>6Bz&PKh#&=M(U&lCAP*3nhsQ0j0$8+;?6qX8jt&TPA1d*O=aWXC ziOcX7q4e}NveWl|(6HvCwmby;Vz$D2?tS(`deUrl>{3w;u=`x*Ha~u7+YX@Zb_C*G zNX|baAlwT;r#C0F*Q|Fr4QT3I@jw_IzJI6)2eI%Hs`W*ONT`Cm!P+mQ+L#M?OHlI!mT z`xs&YR%1RR)iQX9!#aaH22>h}?mR90QwH9WV+8B6ZnL;v5KAwix=VPU6ZJdBRfOWU zRtAD4sfTIf;f#s{U`E-zhKN1R0XyHy^etdYBq5XC+i5{(X>v_^-Jm#Q&p0KAn@M?; z0S92p)5mM-jKWXzB7CiO!!^$|<1SHKtmiCoeFT3~9#{Fig7udGSKe%`3$~Qb1|Ou? z>P%*w(3jb-`E%qM_haIp<9Y5akPGf*@IqX5mv|}GoSHf{?izC(t$6MjSC5raTLbzB zUe|Q`_SUe=H*TeF*wdLgeKPS8IZBpQq&NwZi?m z*6Uf;SAJM@3z1I971EnZ+s-9-lhc@)z6@$m`W0W)G+tYfH%u@}l=zJ0we06#Psl}7 zOdQMUR>Ncu^aH@2XH0iv3(o=5)x@1S?T5l5hh5obrx)Vh9uIg)NwX65zdpPk_i_OM zP`>itE{>`%TIKid!m;}@>D9si2Da$v>x}a;SkCBR^&D{Z5} z)fNZtRZarPAy_ViHyNd&oBYcVL|G8b!upvICG3?jRxte%0bv|PK zCg23|N%4iVON$_PV{t0Q{q}&I@Pvy@vKPP*+%xW-=b`9JW$zz4B;OJ`Q96)#9vIl( zrAZPa@z4j7<8X=V|G8bkh!mef)wd%ApaaB|~lj-=y7>fdjIfU(pF*zHm@ywlhWUb=WVBUfMwOuhq;lKxz_#hB9 za1&p|>4ZbnhjKZQWpExV4u%nqGI;Ea&Eg`OP_Kks3fyG-|T+MAkc-QVim#I1K_Yy>c+Jv!XV z7<{zq@)>TJ@b>PZ?qC=O$O@DBjsj0KeYeRvj*-^hi1<8)v23}G5t#4l-17J(vr=;k zdL2F_&x_!_FTx3B9>^OmaxzLtxsRPulS|#d*WeIA(S{=e1n+qTTg%fVJx8X;+9q3; zZG&~oyPr3C8qsaLfzuoz2|L zdaA%mKxX?j6S@7_vcsm&PA4Q+zay)d!goexPI9@bIKqkFWHPj z8zw^woED&VGE;xh=gE z*`bLAy-4MB0Ak?{PD#A*2h~#~tnK_r>o!p1UJ(D+&^N z=gxZS4xs_O_V^>ilU51m=K7A<`$FAlU&AOCM(gU}GKaED>WrvG@=^B5%jJQY4l z<)%lE-J*Zt4tVL>uO5PonRY3{5tfpOg(vwJ=JxRFhTqr3f2=@1N zJQDo&SJL3|B%GAhTk$kp;_A&kMQdZc+jZM5`wBn5nTY0mNB97JkcvId;Tn4HE!d}WOkKlJ?1@Ru7egjYu~7}v&^J;YXie%Q4=&Zzy;2cQ%%>(oYdx7oZ=+M zY+1f z|EwvSWYS=bfi~@%+}q$HV$aL^E7F9sD91`3q(~qWN4N~@GWEl$&nJOZFh|qnTCA$n z4Y{IdCkx&t4S8}5cwu+pR@nJf0-Ys8QoHqo;O1LzmRdf$;-23{KkP#da1uF&e@hN* z8<4rh`kEo00hWrMwiDAQnIKm(`Cv+71>p3qvfngW+W5R8#lnJ@^@;ag%ypGQcer#- zqCsbiqHU-*_5a7-Y->mzi(GdUyj+`q?oHTPsYe(lpKJ>+U;fv$$bGKA3NIu|D={k22tAr;w`cZCXNI0$B5uhK#1 zXr6Zh5YToctW`h8Vs~ce)a(>8e@Q%2uqnWXhcH0?SJhSe{TGye__A=Q|4%5v0kZXDuhb z99fWTf1SFrB09G) z`LiW2=iowUx#gf#X^m!#;u)Ww-4R>yzZD^Fi?9cuflWi;2(cP>@uh!;D?V5uWw>jgF;8q0&BDQnVIS%_AAmkt zUXlr2>3dKE3GeT#@_xjtTrQWaqT<(Ev-jB2bg+I5HtCITIxy6k1kh`HdXLpLhjXPL z*-Z|YNH{Q|*fmjTO3V)`z?f|f89>b7|8xX&&f01?pz$I`T8C}oEVIK0ca>wTq#*!~ zmMV)>u?oq95J<5elh`)3q4jOB7!c&!LM1NsExm=cQI;_#s!(`Kj|jLeiCH429#u%x z=5O;r-8QIMJSE%_!MBhE{@WbB#&rUSnTIc2){RepgfCYgj0_GBt+*0SQetYAp;FW8gH4@k;Z{_+eh5MHZl``4=<*W3`Es+mNMlZYytWA8nmk_$PD-_-CcGL(N1FXS z^i7lWbzgBj3V2;U@R@%Q%6&2t89+ehW?FQCz}DPIU}?;|@w@Y(u^Fg(GlzqC`Ypqm z*=G>b%Z9NvV`)O*zvaUtxgXd~iRe4jlLYv~Wg&M}WStu(MKD0TW{QXqZGvNuFB~CPs)uU9({OKI?fuaZfpo@T0|r7v17kf#i_> z>4o!EXY2`8B}H99fI$UwDwlV9bg#%J8`Ln}PZHTLDhmR=^O|)h_g{vWWZKAv^FiGN z9)~Yq)?bl^Ap#ujLiXs5P+mU_U+>jdFWvY5s5I7tt1v^fFi?q2Y9#|O;Rm^#(js6M z6627tf3i30w9vHjT*4t93#p>RfowuU-aiZj3+;Wj&8uc{3D!UxCj&kn4KS(jny(QOEkhY9(tQwa1ljWpcUvZ(-P@h;|JdGjt# zscBwrD+&7u(dE^|8gx5j#tc140$g*5`47DFsb4q-S#}iH;H}Y_n|8kS1GeKk{Coj0 zcH>B6;NA1?S9}<)f!$DEp6$$q7xCgvv>DGQHC^pjkExk4mK6*_KLa9l@5@gXK41S5 zV~^Y3Va*ll#Ny4w%(^#Rf$5*_IG_E^0Wz-@z3vnqRpw@(4e`yM zv2ag#RKoahRUgTTtvJ*W5P$QtSCEhie@lR0ByNq+DrD^DvvrMq{o9?$D|~QSq;olQ zMH;w5c3A{d`*$Py<7XFg@#mA%lY$;+`1=Ha+5q?Px~NU_xNWp7oiQQ_}XikDA&u z=YAS>WL1Njg3jG<&^^3sxUs(Z#$J}o|D<(7XuKCwP7{yq`D{~JWLkEcp9F}H*zFNc ztB%&w?mac~@iR}xdl^3}2SX;@HYqICG&tcv+R&_}Z%>OHyvjdhoj;``2hIuPK)eH` zD-%}0w0E|>5oQWE$1>AzOehEDRaua9`+Lz=`g2_#pU}n@AjE z^Yo9pIuKoI`xFD=1C#dQ1do*BgnZ2etw!&=1}Z*se;)qc#Qff@9B{WhLWN}l-Dfv@vh>}qrzq_hkPuDza!28wItUH82cFR}p zTJCbtmju2orr;_7T_SDB@THK3OdzM0+UHbpzy#q_b4bRt+Sp_MPMTCK0=teeoZQK$ zJXJtdOj!^Q_RsxGt}SPG1-@%JU=cs01ZrD8{92@{-jHO247)&Y#{e_ zoL|UXSYGN@`|UHsX=N-qZ@9&Pv5)rv^qbuwVgqJMcVL4f992?A89Z~+u16_~%gmWK z1$$Ru{cFJSf(~!bj@|^Bh8okS70pZaTC~1NjZec30MD3BB|wL6Xo#elJJ~mleZG;W z>UGM_8&bTa0f{yr2jyg4Sm~$baMS#NlNmm`>CPQ2FCCxVURWy&4i#*L0%!;;(2rz` zyAUu`^Q=u&)Ee}p2y8q>>k5g-9AP>5Y9j^C0o{-_@?{RY+A_sr7gwW8Az%I zpbCB}gC;Y26uS{kFmq(3@jMCdvD<>2Mx3Ecv^zhE+{J_S{6UwcUKe%dorcT3B_LO+ zA3iAKp4b(LG89&$-c0s=qEXwqyZ}jonGxQ9Xe5Km#+iS@{mlxsrGL%477_STs}8Uj ztlJn-g&%Ce;#b%$_p=AKP*=b7Gs^54`=U&Zf-dy;o*;S_A{2e>3xZBG2!gN`XOFk# z_Ff9@S_>KHSt}SZq-MUgX)LLdNB;&f0rb$4$0w|&%}qe}T=6Sn9B}5v7fM!NvBFS^ zG9`#NH9&Gad|}#!9RHYTl#uZktAmf61ZwJN=eNh-2yuuUG7O^SH_tp{O`}zBI zn3eB(^M(ihn+GmuVU$p3p<_QKb?4~g2Q|eHCRN%ijoJ;T`bxUztNZA&A2Hl{ME+0% zaIp6+3|yt2TFu1g(>R&&Z5X{>MIM$atcS4k;cYO11SALipK~2R_!v9vOH=ID7DA%B zQ|2%{aJ%m^xqnZivRA*P2F5?ED)DzmjVw{tV?n679_u{pSRHjl4TUrEimA216#$s@ zV$Ry>Q5RFKlX3_IHa>6)F@#7VD=4ihc+lnz>g%6(-|zMQzAL~=6+$OWxiyUKz0E>) zQz0*oFFPv$EX`QY!cIGJFBYeAqtg;I>m}+S4iH=G4)j z6;7fTfX1s%K^GH+0EJ`Q^NZXpEsS2NK0Q4HK+Z4$>Xz7b&t8UUr>AH6y%`7e**a$g zL!aJDZ1ukBs;yi^6L-9FLcQyDQv@SOFi^+sd#gG3syCP@E6VQ(?zPG!SzPAG{Plws zUS7x5)50rSjR|9#BV(+2o~u!r7c-yS!zW*Sel-vydu2dWN)Q>K0Zs#TS6YekIQiQj zp48!E92a(;-hY8AJT~6!TwMnzKw1Epp@M;E{=U5IHu!t+4`f7wvG%(^Z@?Hs|O zQ6|5mHxArJQRLyoy+AxZwDGFod5GKzMbK1xBQzuueA^d1Iw<#>RrD3{O%(f_UINKw zl3j^j^Y!p_k!Mr#lmFhX|I8EKoE@Crko$%9ABbV6Kl*SJ{Y^m;shNu3f8 zI29stt^Don)0(MmWXWhfJwo8=&3n8{bFby|WrE$S5qekg6~3jo2;|LBx!9F@qG9(zyO`BTw)hVCt!%1SbY9{AGtK?78XfA5X|D%^;Cqh_sQ@aMWe zCg0Ge&i3U(HT<+B0w6K30bhN7F7dD2tJY5G{eilF<*!bN-2QEV%5&~5#pMf+4@MD{ zv+YCCtn$+#!lb+;`3pUl^;e_`)g!r=Z~{Z&NBLV^4q@M!i&c_12}P}cE>xEAeC40B z;CSlNFzvY}!B3KGJgq>`r zn_!WQG3}{OKLodF{^?B^ipgK?9sF0`lvC$asQ3Q4E>{I6elGDNhoCO=<}u6lx*z#p z{EOU&`-SGan39J2C%qzz#<2-XyfW=qRWTiG}*P zyMMo~Zk%Bcnn2QA@RuxO1f`l!3e&Ih*ClD_sQS$&s*Q5kYOOXp0$^{=2b?9jzGv-nI)IGT(w_xS$~lR zFU;N`-0cI-XOQ6xZN$NCA4=sz{2nBw3W^)9gz@yF&}^nQK38Q_d2<8cXuGH808-+G z+^5yuo_N39d7!jDzCEVoZ;m4wb#&kiF=PM16}mLnv2SwFVh1S&==5B}t%+#s2E`}& zCr=3P&w8BC zdqQ${VUWM17W>n&{v!OR0@$yaej6|TrnG4GF9J$`Tt{7$aQAQSK=inCCXdWoB(@5% zcx}3K+Er~4%Y=YvyOLWsTqxeP>=Culx3#r!TCIqexQ%JbywSyd%}DpZRTFRslWWlk zAigC8956#Mt-7~&VtTB4D^KrWk?2-UT!|sgxf(?P1KjzqsoygK5?JZiA`8jZbPxLh9^!yhC z(BCuH-%b6#fEX8)XYJLI_Texjc!|)=Bk6=XpN8?_I&5?4)JpuDZ?1qQ8v;|<`>=55 zJon0+L+ie}Z~|pY$LI(Z_;?Duppu#k?Ue4`S=PgxorUCjfqi_z ziXhENuVtO6u~#yhO!Rc)=?8?bsVmCGjZJ$&SCF-&HBHut5a+@yqOK;Ogpt(W+ku|g zz4}L3Z~jE~(|N%2`T@`(8-W=(BKp*aJU8KpVfR$km|$&=hiEHhT}rPHVDHU(w_3wB z)~^Av@Uu`1cff5tfsf^EfvF>C5FQpkI|K*a^I12g`_E>CSJV21v?z%MF}Uz2r7Uft zAJ?K$@>{`4{1bd)nsrC$ncrXkcsS@qf2}>&h}^gMU4bl~8^2pYo?lADs2x4Vgs}I> zYOLtYdhm^rpwE;|xV?#uCmpsja*xK!ZRreJj^QMc_AN0u61-SAJ3~GC;!Vaev3smF zO6)_+M8I7h@czWWSuhyv?NxO6Y1|I(CM#ov4BqfU#F?`WS>tj)Q`Zg1 z9JtyPU`(`4gE2L-^FgORFvo|QG+uBI>#aZ2LspvbGcN>&l#fg-Ix>atG5V$e#y@NvuH4gdnV@Wo{u2*N3(w=^+u}lqg1?u1PM_48>(qcH(h0}W&%CsP3$wVxaogqZ5)f`br&i;{mAlaRV^1+RS6*1hv5;vjyM;YXAd}!k3)V{@pcj<;v#|cvPcr z-PvFVKddvlOHYZF45gfRx9LB*NZ*YLRW}*+O0$a!z){{_hunBZ`!E&rithI18f^R* z?Lqm_KEfOUGda+$Nxaoxk-fIv^oTm)WRno!W?iu&k{DI1TLZ2K+-Y)>c)@t1usiuA zI#AJ#ZBL^MU^Ju|6Vsf31bO`8Ks{>v6CqD!U9FMqVA-az63$*#B;*;~@68r~is>zW zdg1}{>7GF^Bm`J}*cZs6kVe(H+X2XA-D%H?`##9^O@Wj zuBv6$Gzu^K6q)U8T8fiAF~im|{k47o0^KF}-oMYoOGta!lp07kc#uoUio_7m4OLbLim<1&+ViY8ybB1gPxw29eB>E5E7~q zDsZaa#sO&6*7RC7u2Yef)*E%#IcFy3(`-_FXpiLFo2Iz86wwdeD7>(0V0e1tbx+x_ z&=N1hsd-81f@{*g{~guyCTyBLv{*+2Y$wQ5VYhui=tMRLyNtics(Q;p18I(H`Mm8D z7NXOk0I2cb-3smqjn=^b$S9A4PqSFh*4)l=J@HlIEULK)3IKWbC)Hn_6bgXTG9GZ? z!uz8m(*RLlz@J%we(A`us3iU1S936xUIni=(C@L-pK5sutBV5KT=nK#@Xl8G<%nF; z4u^9o-tDNSt|T42YDNv#t3|T3x(W&#Ris0a)ggGKb@% zce4;%+Dz-SN_sdJ=@%(8cKqIfe&vb;n~-d@3r34)nhAE}CE|H`QhefkfpQ;UC#s+h z`~SX5!ssod_PIGxcfLL^GUe&FP<`f-&XwUcnxn-P{ej(>04t0=jRWJ=hWoA|pvH@d zPLmbH5_6h`xO<~|z4er3*P5K-%}Mu~NHylB86+G1g6dT(H7M18A6u`g(bZPHph^fV zdmOTn1&x{9Ok`}*_Sdr#GU?LPk7L9&4ar?PZX)~$=ok7K33C#jRFSt4$0>KkEuotmL78)Bek>VSUoKTNv#He$egaG`d#`xm`emG<(V6Q zb)(!1A?@DBG*2}|Zh|3>7-J9cD-r;nQb*FVs90&(}m=n_K~ zY~2SLm1d@c2Q#l|ue?3$8z=8WA{gEd44IO9jaT!f7yN=O%x$6&99rOfVlDwQh0)x% zi#8Zj_5`K!NmpFPe7E?C6bW08*$HQ11p>gd5_^iVvV^rSg@lEmQvn%*6bPoO3ybXI zO_91&4%__nEW~^c9C#O#W46?dhl0qmhi!R>g|D#de>QG|hx86=q2qRgjPd^S-ue0) zN#Fub?YdX^hMwQXy!6i6=fBZCkR?3@xR$4|{32@0eXu5%e5>0CV+dUp*$kgL43St6 z^tf+Y|1`q^JVUZKWMeFw`~@cdLkvtNi>)z`1wOiPG;+a}l!-UrHp@7+0tpQrbd zbC&OQ02q`AHhp_)^-Z&aX^d76J~9&AaEZi7%NaENkX90MN_UQybAd+akJ%W+WaDl7 zkh}Il-V1q1JMG&aXf(iN7jA2bh3dS??OKIo=DUGQ%+~BMG7F<=r5+d=H>6Mi<=X;{ z9u7`{bspMb-uB%2rpeE5jM9mPh~*^pCtJ%koulPmplz}Y^nlQ~$28@X;QRC$2E8%a zn}48D1D1wmLtZLrZ44sxM3o{K?79`XMwvE?65$6HVdPR+uE4V`?iKU=y0&=bCsYCy zLWvFJJBG8u)2ddJ;?zMs#y#g~c^7D*nsnTm_EW6PXTsAnK4SHZx1Jqdp!I!F|H8tM z`@*K$Sd?j2@=febYTdKziXM$3J^g7C6uE$$Wx)$-nbnTImQ3YnOCY(Ws6hw)ae$$f zf12QmVvo`}+S?1Xh{@g5jU&O7Ic+QyneS>?L@l4~XkBgj}T~6w#>^%&@XLLn`aEcgwuN%|O(ef|Q z*hcMX#7$JFZWohdL^G+^q_cAs{6M1tI3ODzAJubd-F{(McWl}kf2{CI=jJ_w+g>8+5LuaoD}aFAPTXqpfvTzTTPK;QV$ATt{JvO=Iv>i(Sy;wHb4 zgKa2E*US^fw#16q zRCJ?+Tjnf{q~S;1 zps%aq`_x6oedDKGshBS;IOD3jo0zM{rYDM#XS`P%GA9X$4P}5GQcZ%|o+b8__vTiw zA&7b_ZdNNwKkpn!imwztM=QHP`<*3e03vZuHTQveJnGZL6XnNgFWL3X43-*pQCBT^ zUX7Q(js^JT-Wl{S9XMn-=AlzRD(xzI^0Z1sZ@=;t>+u{O3tP=OTKNUq?;py0Ihx3G zMo9OHOS0fTrI|$TH#rd-!m7Sn&vO^LRwLpJAh%R7@8&m_z*~4Ws-IEZv+AE$I1O1e z$84a-YUN#p9)R!yzq>%gzVksSBfRM8eb)3Bo=Cl=3>eSWepr$kAg9-&R5-KJ(Z)cG zeh=$x@tS$HjGasQKH>}5{bJt^e?V_(SP+f1M%OZghxjZIo2Gsq!!0xa_UiuKxAk-E zO-1Ku6&Glsu~yT)k3<31d>@W?CA3@0XZ`vYXtv&O1U!Tf9;5I|NiPd3C!#2Xs4IM zbf5X|5Mz{$qaau3%IY3p#~07S?E^3=a-)a!0#@|b(kI?$HH9DfdBGUpx{e6{sgYF= zv+VF3t@;A(o2H_4;`Ks>kA^ReV3jZwoKuS;F3`By4hnLlPb7$mz8UgKI*7dNz`B!B z=tQqALzNM3t$7bHcK6$mq}w!FtkJT_Ia5pNwHY2cyK<3Hm%;K8>AQ&cH4LJX-w;-W!xDz#7Nvxhp^YBCBdMz*4Z( zEQt7Mk{q?FP`%#S3=90|}BA=Ci8kSkOxN4aeeqHJU zMrV@yzn*een;LEhXPn2M0;U230>(7OQr(;r@IQ|7sBh>d8#+32Bg20n4#Saq1U%Je z5wK_)Y^Z5^*GYXA*|9`)sQ*q`lao&i%*aCEw+*rUn@EX&f_$R_bkq3Hi09hfz~^S1 z!F@Uy=?|r|%-UaXcbrYQ%x9RRe(4WlqyYdvrh8Z%B%kr<6e(YzD{FAFm^jULv8}PZo;rPCNNffkD&)SJpX*^{wf*-eQoeBTmLV-$P233Vncm>-V~}xRoOH+M+W_3 zVaZwdvb)VV;1}9|Acm|ZIWwbsRyZQsu1jx-R`;O zznDe4Wvy@j%{tI+5vzz^)m6%?9#qkBhzR$a`p#CCdLhoCOyPxV>K!UxQ{$Q8p5lj zXA?^MrdS2sbJ{r4)G5vdR+=S=+~Zf=^IY^V`_KTefr0kR_<8C?v4N`Z7zS@oKo$r@ zf{vuCa&fR-0KRg-3~lSN7G(!96+6o*TTJR8g?f6m&h^i0_g`kUSTKomK(Z=zYP>dTnh+Et%HuWK9-y{BfJ+a5K2Q$hDNqam`hLdzH z0U>8)u{6qExk_`wM$Z(YkIJu6vvD{FA68cASJBokLmwk;96k;lQNP`a{mWD27dE_F zoXJ5~B!%~rZk&5=E~Rk2q+sF3U^jN#Vkm*w?+)-dHSy&Cbp2m4qHk>eBD*9Al`bZ} zlm$_FFzl01u1viYm}Q+=Y{;z%2z->@uBgYeZ0k#a8AG07t52jxptNY zJ{r>rs=gk)l2~-;i$vdj>7_~^^;sk(DSh_vWSf3M5%NUhErT-dhXBHQQMC?5=;?ZD z04>@CLqyK^?Y?V;pCg}eU;EiO9jr|(y~gSm3EsiebwF&Vm=;ccH z7=bDrlE^}bLR%|qTeyD?MgzI`G``?Qkl-jt zhe6*0V&+-lbN0K0qP_);;SxVlwH=nw8tj`{qK`Z$XN^$g!T2gOm2vs8FMlGQ23nl1?b*{NS^#H1t%maWc~@W_>w~2!a^{rKDbKa6 z3|eChW}_F*V^W7OIkMl;yy48cqi&(0f9P3nYG1vI$({V=BiRbOF7GhgJ4UE}muK-C)gJID! zq|!y8fHKTG=uAlHAIvFBUBj1~=d>Lh{kyX)KUut5vorO_b*K^UBLdnMzuo|&eVPn@ zI~5~vJ9lYVM^SQwypuMRT5{ZGJEt3XD3uMuVeWDj5(*<-q&%GVhd1P5ObU-8Tces$ zdWB#HF(v+2&j4FBD}QcOArR+}A0yzu=}c{Dt`!i}LJ1eR2tDaW1L*1P<{_A@S$cDl z%1L@UX^(H~(Z3;qO(S9my4BYeYXB(W#pdi~l5X{7Cwbfxyup`0Q?s{OYiv+9GQHVz z^a2_!0{;Q#IvHj0_&&Y&J^3sS?sQw-McA4%!IyG@uz0#?-3y$h=U&Q2mIp`%l~|K1p5D%E&q7g@=T_jmG+6B*X@OoXd8BpN9DY%`PMGb z*D|~Byz;sExd$LY`5aw!3wa+WR{8h{e>_UP+$h@AUqbLTlH3^z$(*3M6?J3Oti078!|}OT=04+Sj-YO0;;=7g#;$P*vYHy8aJH5)%`zz C2}1?| literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400002.blocks b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/mainnet/block_4400002.blocks new file mode 100755 index 0000000000000000000000000000000000000000..00ffe382d5431a80ca1349cd160e562e5f6a827f GIT binary patch literal 6918 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 literal 0 HcmV?d00001 diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTest.java.template b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTest.java.template new file mode 100755 index 00000000000..6fc8382b606 --- /dev/null +++ b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/BlockchainReferenceTest.java.template @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.vm.blockchain; + +import static net.consensys.pantheon.ethereum.vm.BlockchainReferenceTestTools.executeTest; +import static net.consensys.pantheon.ethereum.vm.BlockchainReferenceTestTools.generateTestParametersForConfig; + +import net.consensys.pantheon.ethereum.vm.BlockchainReferenceTestCaseSpec; + +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** The blockchain test operation testing framework entry point. */ +@RunWith(Parameterized.class) +public class %%TESTS_NAME%% { + + private static final String[] TEST_CONFIG_FILE_DIR_PATH = new String[] {%%TESTS_FILE%%}; + + @Parameters(name = "Name: {0}") + public static Collection getTestParametersForConfig() { + return generateTestParametersForConfig(TEST_CONFIG_FILE_DIR_PATH); + } + + private final String name; + private final BlockchainReferenceTestCaseSpec spec; + + public %%TESTS_NAME%%(String name, BlockchainReferenceTestCaseSpec spec) { + this.name = name; + this.spec = spec; + } + + @Test + public void execution() { + executeTest(spec); + } +} diff --git a/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTest.java.template b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTest.java.template new file mode 100755 index 00000000000..3f6e36cc2ac --- /dev/null +++ b/ethereum/core/src/test/resources/net/consensys/pantheon/ethereum/vm/GeneralStateReferenceTest.java.template @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.vm.generalstate; + +import static net.consensys.pantheon.ethereum.vm.GeneralStateReferenceTestTools.executeTest; +import static net.consensys.pantheon.ethereum.vm.GeneralStateReferenceTestTools.generateTestParametersForConfig; + +import net.consensys.pantheon.ethereum.vm.GeneralStateTestCaseEipSpec; + +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** The general state test operation testing framework entry point. */ +@RunWith(Parameterized.class) +public class %%TESTS_NAME%% { + + private static final String[] TEST_CONFIG_FILE_DIR_PATH = new String[] {%%TESTS_FILE%%}; + + @Parameters(name = "Name: {0}") + public static Collection getTestParametersForConfig() { + return generateTestParametersForConfig(TEST_CONFIG_FILE_DIR_PATH); + } + + private final String name; + private final GeneralStateTestCaseEipSpec spec; + + public %%TESTS_NAME%%(String name, GeneralStateTestCaseEipSpec spec) { + this.name = name; + this.spec = spec; + } + + @Test + public void execution() { + executeTest(spec); + } +} diff --git a/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldBeDeletedWhenEmptyAndTouchedTransactionSucceedsPostEIP158.json b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldBeDeletedWhenEmptyAndTouchedTransactionSucceedsPostEIP158.json new file mode 100755 index 00000000000..0d734c8be64 --- /dev/null +++ b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldBeDeletedWhenEmptyAndTouchedTransactionSucceedsPostEIP158.json @@ -0,0 +1,110 @@ +{ + "ripeMdAccountShouldBeDeletedWhenEmptyAndTouchedTransactionSucceeds" : { + "_info" : { + "comment" : "Makes a successful zero-valued call to the ripemd precompile when empty and the transaction succeeds, so the empty account is deleted during and post EIP158." + }, + "env" : { + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x20000", + "currentGasLimit" : "0x01c9c380", + "currentNumber" : "0x01", + "currentTimestamp" : "0x03e8", + "previousHash" : "0x5e20a0453cecd065ea59c37ac63e079ee08998b6045136a8ce6635c7912ec0b6" + }, + "post" : { + "Byzantium" : [ + { + "hash" : "0xbe15923e22756d1f8313f2d0e3e6a4996b8d14b19f3fb13f381d041604f9966b", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP150" : [ + { + "hash" : "0xd2df3bcec4e14d351490a2bbc15c3ae5c498246d5b1ea1c9b1ef030281f1d573", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP158" : [ + { + "hash" : "0xbe15923e22756d1f8313f2d0e3e6a4996b8d14b19f3fb13f381d041604f9966b", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Frontier" : [ + { + "hash" : "0x63c0de32fbc2a51c7e30e5e2356caf0b52d5a363e72777dde694817393037f46", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Homestead" : [ + { + "hash" : "0x63c0de32fbc2a51c7e30e5e2356caf0b52d5a363e72777dde694817393037f46", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "pre" : { + "0x0000000000000000000000000000000000000003" : { + "balance" : "0x00", + "code" : "0x", + "nonce" : "0x00", + "storage" : { + } + }, + "0x1000000000000000000000000000000000000000" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x60006000600060006000600361c350f1", + "nonce" : "0x00", + "storage" : { + } + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "", + "nonce" : "0x00", + "storage" : { + } + } + }, + "transaction" : { + "data" : [ + "" + ], + "gasLimit" : [ + "0x2dc6c0" + ], + "gasPrice" : "0x01", + "nonce" : "0x00", + "secretKey" : "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + "to" : "0x1000000000000000000000000000000000000000", + "value" : [ + "0x00" + ] + } + } +} \ No newline at end of file diff --git a/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenEmptyAndTouchedTransactionFails.json b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenEmptyAndTouchedTransactionFails.json new file mode 100755 index 00000000000..29645247736 --- /dev/null +++ b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenEmptyAndTouchedTransactionFails.json @@ -0,0 +1,110 @@ +{ + "ripeMdAccountShouldNotBeDeletedWhenEmptyAndTouchedTransactionFails" : { + "_info" : { + "comment" : "Makes a successful zero-valued call to the ripemd precompile when empty, but the transaction fails due to an invalid jump destination so the account is unchanged." + }, + "env" : { + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x20000", + "currentGasLimit" : "0x01c9c380", + "currentNumber" : "0x01", + "currentTimestamp" : "0x03e8", + "previousHash" : "0x5e20a0453cecd065ea59c37ac63e079ee08998b6045136a8ce6635c7912ec0b6" + }, + "post" : { + "Byzantium" : [ + { + "hash" : "0x44531d0f86ec82aaa9cdc1165dc24815bbea1d02349c59ef35ad1b0a244dafa9", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP150" : [ + { + "hash" : "0x44531d0f86ec82aaa9cdc1165dc24815bbea1d02349c59ef35ad1b0a244dafa9", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP158" : [ + { + "hash" : "0x44531d0f86ec82aaa9cdc1165dc24815bbea1d02349c59ef35ad1b0a244dafa9", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Frontier" : [ + { + "hash" : "0x44531d0f86ec82aaa9cdc1165dc24815bbea1d02349c59ef35ad1b0a244dafa9", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Homestead" : [ + { + "hash" : "0x44531d0f86ec82aaa9cdc1165dc24815bbea1d02349c59ef35ad1b0a244dafa9", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "pre" : { + "0x0000000000000000000000000000000000000003" : { + "balance" : "0x00", + "code" : "0x", + "nonce" : "0x00", + "storage" : { + } + }, + "0x1000000000000000000000000000000000000000" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x60006000600060006000600361c350f1600056", + "nonce" : "0x00", + "storage" : { + } + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "", + "nonce" : "0x00", + "storage" : { + } + } + }, + "transaction" : { + "data" : [ + "" + ], + "gasLimit" : [ + "0x2dc6c0" + ], + "gasPrice" : "0x01", + "nonce" : "0x00", + "secretKey" : "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + "to" : "0x1000000000000000000000000000000000000000", + "value" : [ + "0x00" + ] + } + } +} \ No newline at end of file diff --git a/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionFails.json b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionFails.json new file mode 100755 index 00000000000..bbdc7c8a170 --- /dev/null +++ b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionFails.json @@ -0,0 +1,110 @@ +{ + "ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionFails" : { + "_info" : { + "comment" : "Makes a successful zero-valued call to the ripemd precompile when not empty, but the transaction fails due to an invalid jump destination so the account is unchanged." + }, + "env" : { + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x20000", + "currentGasLimit" : "0x01c9c380", + "currentNumber" : "0x01", + "currentTimestamp" : "0x03e8", + "previousHash" : "0x5e20a0453cecd065ea59c37ac63e079ee08998b6045136a8ce6635c7912ec0b6" + }, + "post" : { + "Byzantium" : [ + { + "hash" : "0x76695d3c3e7eba40f9b874d6b6c94f3f0bc8b8bce0ce6ad30e42e8763325cf07", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP150" : [ + { + "hash" : "0x76695d3c3e7eba40f9b874d6b6c94f3f0bc8b8bce0ce6ad30e42e8763325cf07", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP158" : [ + { + "hash" : "0x76695d3c3e7eba40f9b874d6b6c94f3f0bc8b8bce0ce6ad30e42e8763325cf07", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Frontier" : [ + { + "hash" : "0x76695d3c3e7eba40f9b874d6b6c94f3f0bc8b8bce0ce6ad30e42e8763325cf07", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Homestead" : [ + { + "hash" : "0x76695d3c3e7eba40f9b874d6b6c94f3f0bc8b8bce0ce6ad30e42e8763325cf07", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "pre" : { + "0x0000000000000000000000000000000000000003" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x", + "nonce" : "0x00", + "storage" : { + } + }, + "0x1000000000000000000000000000000000000000" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x60006000600060006000600361c350f1600056", + "nonce" : "0x00", + "storage" : { + } + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "", + "nonce" : "0x00", + "storage" : { + } + } + }, + "transaction" : { + "data" : [ + "" + ], + "gasLimit" : [ + "0x2dc6c0" + ], + "gasPrice" : "0x01", + "nonce" : "0x00", + "secretKey" : "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + "to" : "0x1000000000000000000000000000000000000000", + "value" : [ + "0x00" + ] + } + } +} \ No newline at end of file diff --git a/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionSucceeds.json b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionSucceeds.json new file mode 100755 index 00000000000..187dec4eabb --- /dev/null +++ b/ethereum/core/src/test/resources/regressions/generalstate/ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionSucceeds.json @@ -0,0 +1,110 @@ +{ + "ripeMdAccountShouldNotBeDeletedWhenNonEmptyAndTouchedTransactionSucceeds" : { + "_info" : { + "comment" : "Makes a successful zero-valued call to the ripemd precompile when non empty and the transaction succeeds leaving the account intact." + }, + "env" : { + "currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + "currentDifficulty" : "0x20000", + "currentGasLimit" : "0x01c9c380", + "currentNumber" : "0x01", + "currentTimestamp" : "0x03e8", + "previousHash" : "0x5e20a0453cecd065ea59c37ac63e079ee08998b6045136a8ce6635c7912ec0b6" + }, + "post" : { + "Byzantium" : [ + { + "hash" : "0x333b2032e9eb9156bf411d7620a95eab6fe3b37254cef3ef19973a80a8fc9b39", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP150" : [ + { + "hash" : "0x333b2032e9eb9156bf411d7620a95eab6fe3b37254cef3ef19973a80a8fc9b39", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "EIP158" : [ + { + "hash" : "0x333b2032e9eb9156bf411d7620a95eab6fe3b37254cef3ef19973a80a8fc9b39", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Frontier" : [ + { + "hash" : "0xcafca3c569988d519841bba3e9a97d511b183800717234c49bef1ea2938fa6ec", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ], + "Homestead" : [ + { + "hash" : "0xcafca3c569988d519841bba3e9a97d511b183800717234c49bef1ea2938fa6ec", + "indexes" : { + "data" : 0, + "gas" : 0, + "value" : 0 + }, + "logs" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + } + ] + }, + "pre" : { + "0x0000000000000000000000000000000000000003" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x", + "nonce" : "0x00", + "storage" : { + } + }, + "0x1000000000000000000000000000000000000000" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "0x60006000600060006000600361c350f1", + "nonce" : "0x00", + "storage" : { + } + }, + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : { + "balance" : "0x0de0b6b3a7640000", + "code" : "", + "nonce" : "0x00", + "storage" : { + } + } + }, + "transaction" : { + "data" : [ + "" + ], + "gasLimit" : [ + "0x2dc6c0" + ], + "gasPrice" : "0x01", + "nonce" : "0x00", + "secretKey" : "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8", + "to" : "0x1000000000000000000000000000000000000000", + "value" : [ + "0x00" + ] + } + } +} \ No newline at end of file diff --git a/ethereum/eth/build.gradle b/ethereum/eth/build.gradle new file mode 100755 index 00000000000..9eb2905c667 --- /dev/null +++ b/ethereum/eth/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-eth' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + implementation project(':ethereum:core') + implementation project(':ethereum:p2p') + implementation project(':ethereum:rlp') + implementation project(':services:kvstore') + + implementation 'io.vertx:vertx-core' + implementation 'com.google.guava:guava' + + testImplementation project(':crypto') + testImplementation project( path: ':ethereum:core', configuration: 'testArtifacts') + testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts') + testImplementation project(':ethereum:mock-p2p') + testImplementation project(':testutil') + + testImplementation 'junit:junit' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.awaitility:awaitility' + testImplementation 'org.mockito:mockito-core' +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/EthProtocol.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/EthProtocol.java new file mode 100755 index 00000000000..47e31404a7f --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/EthProtocol.java @@ -0,0 +1,74 @@ +package net.consensys.pantheon.ethereum.eth; + +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.EthPV63; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class EthProtocol implements SubProtocol { + public static final String NAME = "eth"; + public static final Capability ETH62 = Capability.create(NAME, EthVersion.V62); + public static final Capability ETH63 = Capability.create(NAME, EthVersion.V63); + private static final EthProtocol INSTANCE = new EthProtocol(); + + private static final List eth62Messages = + Arrays.asList( + EthPV62.STATUS, + EthPV62.NEW_BLOCK_HASHES, + EthPV62.TRANSACTIONS, + EthPV62.GET_BLOCK_HEADERS, + EthPV62.BLOCK_HEADERS, + EthPV62.GET_BLOCK_BODIES, + EthPV62.BLOCK_BODIES, + EthPV62.NEW_BLOCK); + + private static final List eth63Messages = new ArrayList<>(eth62Messages); + + static { + eth63Messages.addAll( + Arrays.asList( + EthPV63.GET_NODE_DATA, EthPV63.NODE_DATA, EthPV63.GET_RECEIPTS, EthPV63.RECEIPTS)); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public int messageSpace(final int protocolVersion) { + switch (protocolVersion) { + case EthVersion.V62: + return 8; + case EthVersion.V63: + return 17; + default: + return 0; + } + } + + @Override + public boolean isValidMessageCode(final int protocolVersion, final int code) { + switch (protocolVersion) { + case EthVersion.V62: + return eth62Messages.contains(code); + case EthVersion.V63: + return eth63Messages.contains(code); + default: + return false; + } + } + + public static EthProtocol get() { + return INSTANCE; + } + + public static class EthVersion { + public static final int V62 = 62; + public static final int V63 = 63; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTask.java new file mode 100755 index 00000000000..dc46c38d70d --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTask.java @@ -0,0 +1,114 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import java.util.Collection; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public abstract class AbstractEthTask implements EthTask { + + protected final AtomicReference> result = new AtomicReference<>(); + protected volatile Collection> subTaskFutures = + new ConcurrentLinkedDeque<>(); + + @Override + public final CompletableFuture run() { + if (result.compareAndSet(null, new CompletableFuture<>())) { + executeTask(); + result + .get() + .whenComplete( + (r, t) -> { + cleanup(); + }); + } + return result.get(); + } + + @Override + public final void cancel() { + synchronized (result) { + result.compareAndSet(null, new CompletableFuture<>()); + result.get().cancel(false); + } + } + + public final boolean isDone() { + return result.get() != null && result.get().isDone(); + } + + public final boolean isSucceeded() { + return isDone() && !result.get().isCompletedExceptionally(); + } + + public final boolean isFailed() { + return isDone() && result.get().isCompletedExceptionally(); + } + + public final boolean isCancelled() { + return isDone() && result.get().isCancelled(); + } + + /** + * Utility for executing completable futures that handles cleanup if this EthTask is cancelled. + * + * @param subTask a subTask to execute + * @param the type of data returned from the CompletableFuture + * @return The completableFuture that was executed + */ + protected final CompletableFuture executeSubTask( + final Supplier> subTask) { + synchronized (result) { + if (!isCancelled()) { + final CompletableFuture subTaskFuture = subTask.get(); + subTaskFutures.add(subTaskFuture); + subTaskFuture.whenComplete( + (r, t) -> { + subTaskFutures.remove(subTaskFuture); + }); + return subTaskFuture; + } else { + final CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new CancellationException()); + return future; + } + } + } + + /** + * Helper method for sending subTask to worker that will clean up if this EthTask is cancelled. + * + * @param scheduler the scheduler that will run worker task + * @param subTask a subTask to execute + * @param the type of data returned from the CompletableFuture + * @return The completableFuture that was executed + */ + protected final CompletableFuture executeWorkerSubTask( + final EthScheduler scheduler, final Supplier> subTask) { + return executeSubTask(() -> scheduler.scheduleWorkerTask(subTask)); + } + + public final T result() { + if (!isSucceeded()) { + return null; + } + try { + return result.get().get(); + } catch (InterruptedException | ExecutionException e) { + return null; + } + } + + /** Execute core task logic. */ + protected abstract void executeTask(); + + /** Cleanup any resources when task completes. */ + protected void cleanup() { + for (final CompletableFuture subTaskFuture : subTaskFutures) { + subTaskFuture.cancel(false); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerRequestTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerRequestTask.java new file mode 100755 index 00000000000..c83a1c67599 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerRequestTask.java @@ -0,0 +1,82 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.PeerBreachedProtocolException; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.util.ExceptionUtils; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; + +public abstract class AbstractPeerRequestTask extends AbstractPeerTask { + + private final int requestCode; + private volatile ResponseStream responseStream; + + protected AbstractPeerRequestTask(final EthContext ethContext, final int requestCode) { + super(ethContext); + this.requestCode = requestCode; + } + + @Override + protected final void executeTaskWithPeer(final EthPeer peer) throws PeerNotConnected { + final CompletableFuture promise = new CompletableFuture<>(); + responseStream = + sendRequest(peer) + .then( + (streamClosed, message, peer1) -> + handleMessage(promise, streamClosed, message, peer1)); + + promise.whenComplete( + (r, t) -> { + if (t != null) { + t = ExceptionUtils.rootCause(t); + if (t instanceof TimeoutException) { + peer.recordRequestTimeout(requestCode); + } + result.get().completeExceptionally(t); + } else if (r != null) { + result.get().complete(new PeerTaskResult<>(peer, r)); + } + }); + + ethContext.getScheduler().failAfterTimeout(promise); + } + + private void handleMessage( + final CompletableFuture promise, + final boolean streamClosed, + final MessageData message, + final EthPeer peer) { + if (promise.isDone()) { + // We've already got our response, don't pass on the stream closed event. + return; + } + try { + final Optional result = processResponse(streamClosed, message, peer); + result.ifPresent(promise::complete); + } catch (final RLPException e) { + // Peer sent us malformed data - disconnect + peer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + promise.completeExceptionally(new PeerBreachedProtocolException()); + } + } + + @Override + protected void cleanup() { + super.cleanup(); + final ResponseStream stream = responseStream; + if (stream != null) { + stream.close(); + } + } + + protected abstract ResponseStream sendRequest(EthPeer peer) throws PeerNotConnected; + + protected abstract Optional processResponse( + boolean streamClosed, MessageData message, EthPeer peer); +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerTask.java new file mode 100755 index 00000000000..6ac2665cc4a --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractPeerTask.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.NoAvailablePeersException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.PeerDisconnectedException; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; + +import java.util.Optional; + +public abstract class AbstractPeerTask extends AbstractEthTask> { + protected Optional assignedPeer = Optional.empty(); + protected final EthContext ethContext; + + protected AbstractPeerTask(final EthContext ethContext) { + this.ethContext = ethContext; + } + + @Override + protected void executeTask() { + EthPeer peer; + if (assignedPeer.isPresent()) { + peer = assignedPeer.get(); + } else { + // Try to find a peer + final Optional maybePeer = findSuitablePeer(); + if (!maybePeer.isPresent()) { + result.get().completeExceptionally(new NoAvailablePeersException()); + return; + } + peer = maybePeer.get(); + } + + try { + executeTaskWithPeer(peer); + } catch (final PeerNotConnected e) { + result.get().completeExceptionally(new PeerDisconnectedException()); + } + } + + protected Optional findSuitablePeer() { + return this.ethContext.getEthPeers().idlePeer(); + } + + protected abstract void executeTaskWithPeer(EthPeer peer) throws PeerNotConnected; + + public AbstractPeerTask assignPeer(final EthPeer peer) { + assignedPeer = Optional.of(peer); + return this; + } + + public static class PeerTaskResult { + private final EthPeer peer; + private final T result; + + public PeerTaskResult(final EthPeer peer, final T result) { + this.peer = peer; + this.result = result; + } + + public EthPeer getPeer() { + return peer; + } + + public T getResult() { + return result; + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractRetryingPeerTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractRetryingPeerTask.java new file mode 100755 index 00000000000..7bd0e007ced --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/AbstractRetryingPeerTask.java @@ -0,0 +1,77 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.eth.manager.exceptions.NoAvailablePeersException; +import net.consensys.pantheon.ethereum.eth.sync.tasks.WaitForPeerTask; +import net.consensys.pantheon.util.ExceptionUtils; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public abstract class AbstractRetryingPeerTask extends AbstractEthTask { + + private static final Logger LOG = LogManager.getLogger(); + private final EthContext ethContext; + + public AbstractRetryingPeerTask(final EthContext ethContext) { + this.ethContext = ethContext; + } + + @Override + protected void executeTask() { + if (result.get().isDone()) { + // Return if task is done + return; + } + + executePeerTask() + .whenComplete( + (peerResult, error) -> { + if (error != null) { + handleTaskError(error); + } else { + executeTask(); + } + }); + } + + protected abstract CompletableFuture executePeerTask(); + + private void handleTaskError(final Throwable error) { + final Throwable cause = ExceptionUtils.rootCause(error); + if (!isRetryableError(cause)) { + // Complete exceptionally + result.get().completeExceptionally(cause); + return; + } + + if (cause instanceof NoAvailablePeersException) { + LOG.info("No peers available, wait for peer."); + // Wait for new peer to connect + final WaitForPeerTask waitTask = WaitForPeerTask.create(ethContext); + executeSubTask( + () -> + ethContext + .getScheduler() + .timeout(waitTask, Duration.ofSeconds(5)) + .whenComplete( + (r, t) -> { + executeTask(); + })); + return; + } + + LOG.info( + "Retrying after recoverable failure from peer task {}: {}", + this.getClass().getSimpleName(), + cause.getMessage()); + // Wait before retrying on failure + executeSubTask( + () -> + ethContext.getScheduler().scheduleFutureTask(this::executeTask, Duration.ofSeconds(1))); + } + + protected abstract boolean isRetryableError(Throwable error); +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/ChainState.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/ChainState.java new file mode 100755 index 00000000000..1f42fd18cad --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/ChainState.java @@ -0,0 +1,106 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.util.uint.UInt256; + +import com.google.common.base.MoreObjects; + +public class ChainState { + // The best block by total difficulty that we know about + private final BestBlock bestBlock = new BestBlock(); + // The highest block that we've seen + private volatile long estimatedHeight = 0L; + private volatile boolean estimatedHeightKnown = false; + + public boolean hasEstimatedHeight() { + return estimatedHeightKnown; + } + + public long getEstimatedHeight() { + return estimatedHeight; + } + + public BestBlock getBestBlock() { + return bestBlock; + } + + public void statusReceived(final Hash bestBlockHash, final UInt256 bestBlockTotalDifficulty) { + synchronized (this) { + bestBlock.totalDifficulty = bestBlockTotalDifficulty; + bestBlock.hash = bestBlockHash; + } + } + + public void update(final Hash blockHash, final long blockNumber) { + synchronized (this) { + if (bestBlock.hash.equals(blockHash)) { + bestBlock.number = blockNumber; + } + updateHeightEstimate(blockNumber); + } + } + + public void update(final BlockHeader header) { + synchronized (this) { + if (bestBlock.hash.equals(header.getHash())) { + bestBlock.number = header.getNumber(); + } + updateHeightEstimate(header.getNumber()); + } + } + + public void update(final BlockHeader blockHeader, final UInt256 totalDifficulty) { + synchronized (this) { + if (totalDifficulty.compareTo(bestBlock.totalDifficulty) >= 0) { + bestBlock.totalDifficulty = totalDifficulty; + bestBlock.hash = blockHeader.getHash(); + bestBlock.number = blockHeader.getNumber(); + } + updateHeightEstimate(blockHeader.getNumber()); + } + } + + private void updateHeightEstimate(final long blockNumber) { + estimatedHeightKnown = true; + if (blockNumber > estimatedHeight) { + estimatedHeight = blockNumber; + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("estimatedHeight", estimatedHeight) + .add("bestBlock", bestBlock) + .toString(); + } + + // Represent the best block by totalDifficulty + public static class BestBlock { + volatile long number = 0L; + volatile Hash hash = null; + volatile UInt256 totalDifficulty = UInt256.ZERO; + + public long getNumber() { + return number; + } + + public Hash getHash() { + return hash; + } + + public UInt256 getTotalDifficulty() { + return totalDifficulty; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("totalDifficulty", totalDifficulty) + .add("blockHash", hash) + .add("number", number) + .toString(); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthContext.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthContext.java new file mode 100755 index 00000000000..0c0ea4404c8 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthContext.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +public class EthContext { + + private final String protocolName; + private final EthPeers ethPeers; + private final EthMessages ethMessages; + private final EthScheduler scheduler; + + public EthContext( + final String protocolName, + final EthPeers ethPeers, + final EthMessages ethMessages, + final EthScheduler scheduler) { + this.protocolName = protocolName; + this.ethPeers = ethPeers; + this.ethMessages = ethMessages; + this.scheduler = scheduler; + } + + public String getProtocolName() { + return protocolName; + } + + public EthPeers getEthPeers() { + return ethPeers; + } + + public EthMessages getEthMessages() { + return ethMessages; + } + + public EthScheduler getScheduler() { + return scheduler; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessage.java new file mode 100755 index 00000000000..1a30101fd35 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessage.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; + +public class EthMessage { + + private final EthPeer peer; + private final MessageData data; + + public EthMessage(final EthPeer peer, final MessageData data) { + this.peer = peer; + this.data = data; + } + + public EthPeer getPeer() { + return peer; + } + + public MessageData getData() { + return data; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessages.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessages.java new file mode 100755 index 00000000000..f5413a0fa65 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthMessages.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.util.Subscribers; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +public class EthMessages { + private final Map> listenersByCode = + new ConcurrentHashMap<>(); + + void dispatch(final EthMessage message) { + final Subscribers listeners = listenersByCode.get(message.getData().getCode()); + if (listeners == null) { + return; + } + + listeners.forEach(callback -> callback.exec(message)); + } + + public long subscribe(final int messageCode, final MessageCallback callback) { + return listenersByCode + .computeIfAbsent(messageCode, key -> new Subscribers<>()) + .subscribe(callback); + } + + public void unsubscribe(final long listenerId) { + for (final Entry> entry : listenersByCode.entrySet()) { + if (entry.getValue().unsubscribe(listenerId)) { + break; + } + } + } + + @FunctionalInterface + public interface MessageCallback { + void exec(EthMessage message); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeer.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeer.java new file mode 100755 index 00000000000..a892f95c744 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeer.java @@ -0,0 +1,292 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.EthPV63; +import net.consensys.pantheon.ethereum.eth.messages.GetBlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.GetBlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.GetReceiptsMessage; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.Subscribers; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class EthPeer { + private static final Logger LOG = LogManager.getLogger(); + private final PeerConnection connection; + + private final int maxTrackedSeenBlocks = 30_000; + + private final Set knownBlocks; + private final String protocolName; + private final ChainState chainHeadState; + private final AtomicBoolean statusHasBeenSentToPeer = new AtomicBoolean(false); + private final AtomicBoolean statusHasBeenReceivedFromPeer = new AtomicBoolean(false); + + private final RequestManager headersRequestManager = new RequestManager(this); + private final RequestManager bodiesRequestManager = new RequestManager(this); + private final RequestManager receiptsRequestManager = new RequestManager(this); + + private final AtomicReference> onStatusesExchanged = new AtomicReference<>(); + private final PeerReputation reputation = new PeerReputation(); + private final Subscribers disconnectCallbacks = new Subscribers<>(); + + EthPeer( + final PeerConnection connection, + final String protocolName, + final Consumer onStatusesExchanged) { + this.connection = connection; + this.protocolName = protocolName; + knownBlocks = + Collections.newSetFromMap( + Collections.synchronizedMap( + new LinkedHashMap(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > maxTrackedSeenBlocks; + } + })); + this.chainHeadState = new ChainState(); + this.onStatusesExchanged.set(onStatusesExchanged); + } + + public void recordRequestTimeout(final int requestCode) { + LOG.debug("Timed out while waiting for response from peer {}", this); + reputation.recordRequestTimeout(requestCode).ifPresent(this::disconnect); + } + + public void recordUselessResponse() { + LOG.debug("Received useless response from peer {}", this); + reputation.recordUselessResponse(System.currentTimeMillis()).ifPresent(this::disconnect); + } + + public void disconnect(final DisconnectReason reason) { + connection.disconnect(reason); + } + + public long subscribeDisconnect(final DisconnectCallback callback) { + return disconnectCallbacks.subscribe(callback); + } + + public void unsubscribeDisconnect(final long id) { + disconnectCallbacks.unsubscribe(id); + } + + public ResponseStream send(final MessageData messageData) throws PeerNotConnected { + switch (messageData.getCode()) { + case EthPV62.GET_BLOCK_HEADERS: + return sendHeadersRequest(messageData); + case EthPV62.GET_BLOCK_BODIES: + return sendBodiesRequest(messageData); + case EthPV63.GET_RECEIPTS: + return sendReceiptsRequest(messageData); + default: + connection.sendForProtocol(protocolName, messageData); + return null; + } + } + + public ResponseStream getHeadersByHash( + final Hash hash, final int maxHeaders, final boolean reverse, final int skip) + throws PeerNotConnected { + final GetBlockHeadersMessage message = + GetBlockHeadersMessage.create(hash, maxHeaders, reverse, skip); + return sendHeadersRequest(message); + } + + public ResponseStream getHeadersByNumber( + final long blockNumber, final int maxHeaders, final boolean reverse, final int skip) + throws PeerNotConnected { + final GetBlockHeadersMessage message = + GetBlockHeadersMessage.create(blockNumber, maxHeaders, reverse, skip); + return sendHeadersRequest(message); + } + + private ResponseStream sendHeadersRequest(final MessageData messageData) throws PeerNotConnected { + return headersRequestManager.dispatchRequest( + () -> connection.sendForProtocol(protocolName, messageData)); + } + + public ResponseStream getBodies(final List blockHashes) throws PeerNotConnected { + final GetBlockBodiesMessage message = GetBlockBodiesMessage.create(blockHashes); + return sendBodiesRequest(message); + } + + private ResponseStream sendBodiesRequest(final MessageData messageData) throws PeerNotConnected { + return bodiesRequestManager.dispatchRequest( + () -> connection.sendForProtocol(protocolName, messageData)); + } + + public ResponseStream getReceipts(final List blockHashes) throws PeerNotConnected { + final GetReceiptsMessage message = GetReceiptsMessage.create(blockHashes); + return sendReceiptsRequest(message); + } + + private ResponseStream sendReceiptsRequest(final MessageData messageData) + throws PeerNotConnected { + return receiptsRequestManager.dispatchRequest( + () -> connection.sendForProtocol(protocolName, messageData)); + } + + boolean validateReceivedMessage(final EthMessage message) { + checkArgument(message.getPeer().equals(this), "Mismatched message sent to peer for dispatch"); + switch (message.getData().getCode()) { + case EthPV62.BLOCK_HEADERS: + if (headersRequestManager.outstandingRequests() == 0) { + LOG.warn("Unsolicited headers received."); + return false; + } + break; + case EthPV62.BLOCK_BODIES: + if (bodiesRequestManager.outstandingRequests() == 0) { + LOG.warn("Unsolicited bodies received."); + return false; + } + break; + case EthPV63.RECEIPTS: + if (receiptsRequestManager.outstandingRequests() == 0) { + LOG.warn("Unsolicited receipts received."); + return false; + } + break; + default: + // Nothing to do + } + return true; + } + + /** + * Routes messages originating from this peer to listeners. + * + * @param message the message to dispatch + */ + void dispatch(final EthMessage message) { + checkArgument(message.getPeer().equals(this), "Mismatched message sent to peer for dispatch"); + switch (message.getData().getCode()) { + case EthPV62.BLOCK_HEADERS: + reputation.resetTimeoutCount(EthPV62.GET_BLOCK_HEADERS); + headersRequestManager.dispatchResponse(message); + break; + case EthPV62.BLOCK_BODIES: + reputation.resetTimeoutCount(EthPV62.GET_BLOCK_BODIES); + bodiesRequestManager.dispatchResponse(message); + break; + case EthPV63.RECEIPTS: + reputation.resetTimeoutCount(EthPV63.GET_RECEIPTS); + receiptsRequestManager.dispatchResponse(message); + break; + default: + // Nothing to do + } + } + + public Map timeoutCounts() { + return reputation.timeoutCounts(); + } + + void handleDisconnect() { + headersRequestManager.close(); + bodiesRequestManager.close(); + receiptsRequestManager.close(); + disconnectCallbacks.forEach(callback -> callback.onDisconnect(this)); + } + + public void registerKnownBlock(final Hash hash) { + knownBlocks.add(hash); + } + + public void registerStatusSent() { + statusHasBeenSentToPeer.set(true); + maybeExecuteStatusesExchangedCallback(); + } + + public void registerStatusReceived(final Hash hash, final UInt256 td) { + chainHeadState.statusReceived(hash, td); + statusHasBeenReceivedFromPeer.set(true); + maybeExecuteStatusesExchangedCallback(); + } + + private void maybeExecuteStatusesExchangedCallback() { + if (readyForRequests()) { + final Consumer callback = onStatusesExchanged.getAndSet(null); + if (callback == null) { + return; + } + callback.accept(this); + } + } + + /** + * Wait until status has been received and verified before using a peer. + * + * @return true if the peer is ready to accept requests for data. + */ + public boolean readyForRequests() { + return statusHasBeenSentToPeer.get() && statusHasBeenReceivedFromPeer.get(); + } + + /** + * True if the peer has sent its initial status message to us. + * + * @return true if the peer has sent its initial status message to us. + */ + public boolean statusHasBeenReceived() { + return statusHasBeenReceivedFromPeer.get(); + } + + /** @return true if we have sent a status message to this peer. */ + public boolean statusHasBeenSentToPeer() { + return statusHasBeenSentToPeer.get(); + } + + public boolean hasSeenBlock(final Hash hash) { + return knownBlocks.contains(hash); + } + + public ChainState chainState() { + return chainHeadState; + } + + public void registerHeight(final Hash blockHash, final long height) { + chainHeadState.update(blockHash, height); + } + + public int outstandingRequests() { + return headersRequestManager.outstandingRequests() + + bodiesRequestManager.outstandingRequests() + + receiptsRequestManager.outstandingRequests(); + } + + public BytesValue nodeId() { + return connection.getPeer().getNodeId(); + } + + @Override + public String toString() { + return nodeId().toString().substring(0, 20) + "..."; + } + + @FunctionalInterface + public interface DisconnectCallback { + void onDisconnect(EthPeer peer); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeers.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeers.java new file mode 100755 index 00000000000..fd11c4d7934 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthPeers.java @@ -0,0 +1,117 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.eth.manager.EthPeer.DisconnectCallback; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.util.Subscribers; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EthPeers { + public static final Comparator TOTAL_DIFFICULTY = + Comparator.comparing( + ((final EthPeer p) -> p.chainState().getBestBlock().getTotalDifficulty())); + + public static final Comparator CHAIN_HEIGHT = + Comparator.comparing(((final EthPeer p) -> p.chainState().getEstimatedHeight())); + + public static final Comparator BEST_CHAIN = CHAIN_HEIGHT.thenComparing(TOTAL_DIFFICULTY); + + public static final Comparator LEAST_TO_MOST_BUSY = + Comparator.comparing(EthPeer::outstandingRequests); + + private final int maxOutstandingRequests = 5; + private final Map connections = new ConcurrentHashMap<>(); + private final String protocolName; + private final Subscribers connectCallbacks = new Subscribers<>(); + private final Subscribers disconnectCallbacks = new Subscribers<>(); + + public EthPeers(final String protocolName) { + this.protocolName = protocolName; + } + + void registerConnection(final PeerConnection peerConnection) { + final EthPeer peer = new EthPeer(peerConnection, protocolName, this::invokeConnectionCallbacks); + connections.putIfAbsent(peerConnection, peer); + } + + void registerDisconnect(final PeerConnection connection) { + final EthPeer peer = connections.remove(connection); + if (peer != null) { + disconnectCallbacks.forEach(callback -> callback.onDisconnect(peer)); + peer.handleDisconnect(); + } + } + + EthPeer peer(final PeerConnection peerConnection) { + return connections.get(peerConnection); + } + + public long subscribeConnect(final ConnectCallback callback) { + return connectCallbacks.subscribe(callback); + } + + public void unsubscribeConnect(final long id) { + connectCallbacks.unsubscribe(id); + } + + public long subscribeDisconnect(final DisconnectCallback callback) { + return disconnectCallbacks.subscribe(callback); + } + + public int peerCount() { + return connections.size(); + } + + public int availablePeerCount() { + return (int) availablePeers().count(); + } + + public Stream availablePeers() { + return connections.values().stream().filter(EthPeer::readyForRequests); + } + + public Optional bestPeer() { + return availablePeers().max(BEST_CHAIN); + } + + public Optional idlePeer() { + return idlePeers().min(LEAST_TO_MOST_BUSY); + } + + private Stream idlePeers() { + final List peers = + availablePeers() + .filter(p -> p.outstandingRequests() < maxOutstandingRequests) + .collect(Collectors.toList()); + Collections.shuffle(peers); + return peers.stream(); + } + + public Optional idlePeer(final long withBlocksUpTo) { + return idlePeers().filter(p -> p.chainState().getEstimatedHeight() >= withBlocksUpTo).findAny(); + } + + @FunctionalInterface + public interface ConnectCallback { + void onPeerConnected(EthPeer newPeer); + } + + @Override + public String toString() { + final String connectionsList = + String.join( + ",", connections.values().stream().map(EthPeer::toString).collect(Collectors.toList())); + return "EthPeers{" + "connections=" + connectionsList + '}'; + } + + private void invokeConnectionCallbacks(final EthPeer peer) { + connectCallbacks.forEach(cb -> cb.onPeerConnected(peer)); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManager.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManager.java new file mode 100755 index 00000000000..2faa5ede12f --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManager.java @@ -0,0 +1,267 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator.MinedBlockObserver; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockMessage; +import net.consensys.pantheon.ethereum.eth.messages.StatusMessage; +import net.consensys.pantheon.ethereum.p2p.api.Message; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class EthProtocolManager implements ProtocolManager, MinedBlockObserver { + static final int DEFAULT_REQUEST_LIMIT = 200; + private static final Logger LOG = LogManager.getLogger(); + private static final List FAST_SYNC_CAPS = + Collections.singletonList(EthProtocol.ETH63); + private static final List FULL_SYNC_CAPS = + Arrays.asList(EthProtocol.ETH62, EthProtocol.ETH63); + + private final EthScheduler scheduler; + private final CountDownLatch shutdown; + private final AtomicBoolean stopped = new AtomicBoolean(false); + + private final Hash genesisHash; + private final int networkId; + private final EthPeers ethPeers; + private final EthMessages ethMessages; + private final EthContext ethContext; + private final boolean fastSyncEnabled; + private List supportedCapabilities; + private final Blockchain blockchain; + + EthProtocolManager( + final Blockchain blockchain, + final int networkId, + final boolean fastSyncEnabled, + final int requestLimit, + final EthScheduler scheduler) { + this.networkId = networkId; + + this.scheduler = scheduler; + this.blockchain = blockchain; + this.fastSyncEnabled = fastSyncEnabled; + + this.shutdown = new CountDownLatch(1); + genesisHash = blockchain.getBlockHashByNumber(0L).get(); + + ethPeers = new EthPeers(getSupportedProtocol()); + ethMessages = new EthMessages(); + ethContext = new EthContext(getSupportedProtocol(), ethPeers, ethMessages, scheduler); + + // Set up request handlers + new EthServer(blockchain, ethMessages, requestLimit); + } + + EthProtocolManager( + final Blockchain blockchain, + final int networkId, + final boolean fastSyncEnabled, + final int workers, + final int requestLimit) { + this(blockchain, networkId, fastSyncEnabled, requestLimit, new EthScheduler(workers)); + } + + public EthProtocolManager( + final Blockchain blockchain, + final int networkId, + final boolean fastSyncEnabled, + final int workers) { + this(blockchain, networkId, fastSyncEnabled, workers, DEFAULT_REQUEST_LIMIT); + } + + public EthContext ethContext() { + return ethContext; + } + + @Override + public String getSupportedProtocol() { + return EthProtocol.NAME; + } + + @Override + public List getSupportedCapabilities() { + if (supportedCapabilities == null) { + supportedCapabilities = fastSyncEnabled ? FAST_SYNC_CAPS : FULL_SYNC_CAPS; + } + return supportedCapabilities; + } + + @Override + public void stop() { + if (stopped.compareAndSet(false, true)) { + LOG.info("Stopping {} Subprotocol.", getSupportedProtocol()); + scheduler.stop(); + shutdown.countDown(); + } else { + LOG.error("Attempted to stop already stopped {} Subprotocol.", getSupportedProtocol()); + } + } + + @Override + public void awaitStop() throws InterruptedException { + shutdown.await(); + scheduler.awaitStop(); + LOG.info("{} Subprotocol stopped.", getSupportedProtocol()); + } + + @Override + public void processMessage(final Capability cap, final Message message) { + checkArgument( + getSupportedCapabilities().contains(cap), + "Unsupported capability passed to processMessage(): " + cap); + LOG.trace("Process message {}, {}", cap, message.getData().getCode()); + final EthPeer peer = ethPeers.peer(message.getConnection()); + if (peer == null) { + LOG.error("Message received from unknown peer connection: " + message.getConnection()); + return; + } + + // Handle STATUS processing + if (message.getData().getCode() == EthPV62.STATUS) { + handleStatusMessage(peer, message.getData()); + return; + } else if (!peer.statusHasBeenReceived()) { + // Peers are required to send status messages before any other message type + peer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + return; + } + + // Dispatch eth message + final EthMessage ethMessage = new EthMessage(peer, message.getData()); + if (!peer.validateReceivedMessage(ethMessage)) { + LOG.warn("Unsolicited message received from {}, disconnecting", peer); + peer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + return; + } + peer.dispatch(ethMessage); + ethMessages.dispatch(ethMessage); + } + + @Override + public void handleNewConnection(final PeerConnection connection) { + ethPeers.registerConnection(connection); + final EthPeer peer = ethPeers.peer(connection); + if (peer.statusHasBeenSentToPeer()) { + return; + } + + final Capability cap = connection.capability(getSupportedProtocol()); + final StatusMessage status = + StatusMessage.create( + cap.getVersion(), + networkId, + blockchain.getChainHead().getTotalDifficulty(), + blockchain.getChainHeadHash(), + genesisHash); + try { + LOG.info("Sending status message to {}.", peer); + peer.send(status); + peer.registerStatusSent(); + } catch (final PeerNotConnected peerNotConnected) { + // Nothing to do. + } + } + + @Override + public void handleDisconnect( + final PeerConnection connection, + final DisconnectReason reason, + final boolean initiatedByPeer) { + ethPeers.registerDisconnect(connection); + if (initiatedByPeer) { + LOG.info( + "Peer requested to be disconnected ({}), {} peers left: {}", + reason, + ethPeers.peerCount(), + ethPeers); + } else { + LOG.info( + "Disconnecting from peer ({}), {} peers left: {}", + reason, + ethPeers.peerCount(), + ethPeers); + } + } + + @Override + public boolean hasSufficientPeers() { + return ethPeers.availablePeerCount() > 0; + } + + private void handleStatusMessage(final EthPeer peer, final MessageData data) { + final StatusMessage status = StatusMessage.readFrom(data); + try { + if (status.networkId() != networkId) { + LOG.info("Disconnecting from peer with mismatched network id: {}", status.networkId()); + peer.disconnect(DisconnectReason.SUBPROTOCOL_TRIGGERED); + } else if (!status.genesisHash().equals(genesisHash)) { + LOG.warn( + "Disconnecting from peer with matching network id ({}), but non-matching genesis hash: {}", + networkId, + status.genesisHash()); + peer.disconnect(DisconnectReason.SUBPROTOCOL_TRIGGERED); + } else { + LOG.info("Received status message from {}: {}", peer, status); + peer.registerStatusReceived(status.bestHash(), status.totalDifficulty()); + } + } catch (final RLPException e) { + LOG.info("Unable to parse status message, disconnecting from peer."); + // Parsing errors can happen when clients broadcast network ids outside of the int range, + // So just disconnect with "subprotocol" error rather than "breach of protocol". + peer.disconnect(DisconnectReason.SUBPROTOCOL_TRIGGERED); + } finally { + status.release(); + } + } + + @Override + public void blockMined(final Block block) { + // This assumes the block has already been included in the chain + + final Optional totalDifficulty = blockchain.getTotalDifficultyByHash(block.getHash()); + if (!totalDifficulty.isPresent()) { + throw new IllegalStateException( + "Unable to get total difficulty from blockchain for mined block."); + } + + final NewBlockMessage newBlockMessage = NewBlockMessage.create(block, totalDifficulty.get()); + + ethPeers + .availablePeers() + .forEach( + peer -> { + try { + // Send(msg) will release the NewBlockMessage's internal buffer, thus it must be + // retained + // prior to transmission - then released on exit from function. + newBlockMessage.retain(); + peer.send(newBlockMessage); + } catch (final PeerNotConnected ex) { + // Peers may disconnect while traversing the list, this is a normal occurrence. + } + }); + newBlockMessage.release(); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthScheduler.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthScheduler.java new file mode 100755 index 00000000000..7b2cc66308d --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthScheduler.java @@ -0,0 +1,215 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.util.ExceptionUtils; + +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class EthScheduler { + private static final Logger LOG = LogManager.getLogger(); + + private final Duration defaultTimeout = Duration.ofSeconds(5); + + private final AtomicBoolean stopped = new AtomicBoolean(false); + private final CountDownLatch shutdown = new CountDownLatch(1); + + protected final ExecutorService workerExecutor; + protected final ScheduledExecutorService scheduler; + + EthScheduler(final int workerCount) { + this( + Executors.newFixedThreadPool( + workerCount, + new ThreadFactoryBuilder() + .setNameFormat(EthScheduler.class.getSimpleName() + "-Workers") + .build()), + Executors.newScheduledThreadPool( + 1, + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat(EthScheduler.class.getSimpleName() + "Timer") + .build())); + } + + protected EthScheduler( + final ExecutorService workerExecutor, final ScheduledExecutorService scheduler) { + this.workerExecutor = workerExecutor; + this.scheduler = scheduler; + } + + public CompletableFuture scheduleWorkerTask(final Supplier> future) { + final CompletableFuture promise = new CompletableFuture<>(); + final Future workerFuture = + workerExecutor.submit( + () -> { + future + .get() + .whenComplete( + (r, t) -> { + if (t != null) { + promise.completeExceptionally(t); + } else { + promise.complete(r); + } + }); + }); + // If returned promise is cancelled, cancel the worker future + promise.whenComplete( + (r, t) -> { + if (t instanceof CancellationException) { + workerFuture.cancel(false); + } + }); + return promise; + } + + public Future scheduleWorkerTask(final Runnable command) { + return workerExecutor.submit(command); + } + + public CompletableFuture scheduleFutureTask( + final Runnable command, final Duration duration) { + final CompletableFuture promise = new CompletableFuture<>(); + final ScheduledFuture scheduledFuture = + scheduler.schedule( + () -> { + try { + command.run(); + promise.complete(null); + } catch (final Exception e) { + promise.completeExceptionally(e); + } + }, + duration.toMillis(), + TimeUnit.MILLISECONDS); + // If returned promise is cancelled, cancel scheduled task + promise.whenComplete( + (r, t) -> { + if (t instanceof CancellationException) { + scheduledFuture.cancel(false); + } + }); + return promise; + } + + public CompletableFuture scheduleFutureTask( + final Supplier> future, final Duration duration) { + final CompletableFuture promise = new CompletableFuture<>(); + final ScheduledFuture scheduledFuture = + scheduler.schedule( + () -> { + future + .get() + .whenComplete( + (r, t) -> { + if (t != null) { + promise.completeExceptionally(t); + } else { + promise.complete(r); + } + }); + }, + duration.toMillis(), + TimeUnit.MILLISECONDS); + // If returned promise is cancelled, cancel scheduled task + promise.whenComplete( + (r, t) -> { + if (t instanceof CancellationException) { + scheduledFuture.cancel(false); + } + }); + return promise; + } + + public CompletableFuture timeout(final EthTask task) { + return timeout(task, defaultTimeout); + } + + public CompletableFuture timeout(final EthTask task, final Duration timeout) { + final CompletableFuture future = task.run(); + final CompletableFuture result = timeout(future, timeout); + result.whenComplete( + (r, error) -> { + if (errorIsTimeoutOrCancellation(error)) { + task.cancel(); + } + }); + return result; + } + + private boolean errorIsTimeoutOrCancellation(final Throwable error) { + final Throwable cause = ExceptionUtils.rootCause(error); + return cause instanceof TimeoutException || cause instanceof CancellationException; + } + + private CompletableFuture timeout( + final CompletableFuture future, final Duration delay) { + final CompletableFuture timeout = failAfterTimeout(delay); + return future.applyToEither(timeout, Function.identity()); + } + + public void stop() { + if (stopped.compareAndSet(false, true)) { + LOG.trace("Stopping " + getClass().getSimpleName()); + workerExecutor.shutdown(); + scheduler.shutdown(); + shutdown.countDown(); + } else { + LOG.trace("Attempted to stop already stopped " + getClass().getSimpleName()); + } + } + + public void awaitStop() throws InterruptedException { + shutdown.await(); + if (!workerExecutor.awaitTermination(2L, TimeUnit.MINUTES)) { + LOG.error("{} worker executor did not shutdown cleanly.", this.getClass().getSimpleName()); + workerExecutor.shutdownNow(); + workerExecutor.awaitTermination(2L, TimeUnit.MINUTES); + } + if (!scheduler.awaitTermination(2L, TimeUnit.MINUTES)) { + LOG.error("{} scheduler did not shutdown cleanly.", this.getClass().getSimpleName()); + scheduler.shutdownNow(); + scheduler.awaitTermination(2L, TimeUnit.MINUTES); + } + LOG.trace("{} stopped.", this.getClass().getSimpleName()); + } + + private CompletableFuture failAfterTimeout(final Duration timeout) { + final CompletableFuture promise = new CompletableFuture<>(); + failAfterTimeout(promise, timeout); + return promise; + } + + public void failAfterTimeout(final CompletableFuture promise) { + failAfterTimeout(promise, defaultTimeout); + } + + public void failAfterTimeout(final CompletableFuture promise, final Duration timeout) { + final long delay = timeout.toMillis(); + final TimeUnit unit = TimeUnit.MILLISECONDS; + scheduler.schedule( + () -> { + final TimeoutException ex = + new TimeoutException("Timeout after " + delay + " " + unit.name()); + return promise.completeExceptionally(ex); + }, + delay, + unit); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthServer.java new file mode 100755 index 00000000000..722ca18982e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthServer.java @@ -0,0 +1,217 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.eth.messages.BlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.BlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.EthPV63; +import net.consensys.pantheon.ethereum.eth.messages.GetBlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.GetBlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.GetNodeDataMessage; +import net.consensys.pantheon.ethereum.eth.messages.GetReceiptsMessage; +import net.consensys.pantheon.ethereum.eth.messages.NodeDataMessage; +import net.consensys.pantheon.ethereum.eth.messages.ReceiptsMessage; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +class EthServer { + private static final Logger LOG = LogManager.getLogger(); + + private final Blockchain blockchain; + private final EthMessages ethMessages; + private final int requestLimit; + + EthServer(final Blockchain blockchain, final EthMessages ethMessages, final int requestLimit) { + this.blockchain = blockchain; + this.ethMessages = ethMessages; + this.requestLimit = requestLimit; + this.setupListeners(); + } + + private void setupListeners() { + ethMessages.subscribe(EthPV62.GET_BLOCK_HEADERS, this::handleGetBlockHeaders); + ethMessages.subscribe(EthPV62.GET_BLOCK_BODIES, this::handleGetBlockBodies); + ethMessages.subscribe(EthPV63.GET_RECEIPTS, this::handleGetReceipts); + ethMessages.subscribe(EthPV63.GET_NODE_DATA, this::handleGetNodeData); + } + + private void handleGetBlockHeaders(final EthMessage message) { + LOG.info("Responding to GET_BLOCK_HEADERS request"); + try { + final MessageData response = + constructGetHeadersResponse(blockchain, message.getData(), requestLimit); + message.getPeer().send(response); + } catch (final RLPException e) { + message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + } catch (final PeerNotConnected peerNotConnected) { + // Peer disconnected before we could respond - nothing to do + } + } + + private void handleGetBlockBodies(final EthMessage message) { + LOG.info("Responding to GET_BLOCK_BODIES request"); + try { + final MessageData response = + constructGetBodiesResponse(blockchain, message.getData(), requestLimit); + message.getPeer().send(response); + } catch (final RLPException e) { + message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + } catch (final PeerNotConnected peerNotConnected) { + // Peer disconnected before we could respond - nothing to do + } + } + + private void handleGetReceipts(final EthMessage message) { + LOG.info("Responding to GET_RECEIPTS request"); + try { + final MessageData response = + constructGetReceiptsResponse(blockchain, message.getData(), requestLimit); + message.getPeer().send(response); + } catch (final RLPException e) { + message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + } catch (final PeerNotConnected peerNotConnected) { + // Peer disconnected before we could respond - nothing to do + } + } + + private void handleGetNodeData(final EthMessage message) { + LOG.info("Responding to GET_NODE_DATA request"); + try { + final MessageData response = constructGetNodeDataResponse(message.getData(), requestLimit); + message.getPeer().send(response); + } catch (final RLPException e) { + message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + } catch (final PeerNotConnected peerNotConnected) { + // Peer disconnected before we could respond - nothing to do + } + } + + static MessageData constructGetHeadersResponse( + final Blockchain blockchain, final MessageData message, final int requestLimit) { + final GetBlockHeadersMessage getHeaders = GetBlockHeadersMessage.readFrom(message); + try { + final Optional hash = getHeaders.hash(); + final int skip = getHeaders.skip(); + final int maxHeaders = Math.min(requestLimit, getHeaders.maxHeaders()); + final boolean reversed = getHeaders.reverse(); + BlockHeader firstHeader; + if (hash.isPresent()) { + final Hash startHash = hash.get(); + firstHeader = blockchain.getBlockHeader(startHash).orElse(null); + } else { + final long firstNumber = getHeaders.blockNumber().getAsLong(); + firstHeader = blockchain.getBlockHeader(firstNumber).orElse(null); + } + final Collection resp; + if (firstHeader == null) { + resp = Collections.emptyList(); + } else { + resp = new ArrayList<>(Arrays.asList(firstHeader)); + final int numberDelta = reversed ? -(skip + 1) : (skip + 1); + for (int i = 1; i < maxHeaders; i++) { + final long blockNumber = firstHeader.getNumber() + i * numberDelta; + if (blockNumber < BlockHeader.GENESIS_BLOCK_NUMBER) { + break; + } + final Optional maybeHeader = blockchain.getBlockHeader(blockNumber); + if (maybeHeader.isPresent()) { + resp.add(maybeHeader.get()); + } else { + break; + } + } + } + return BlockHeadersMessage.create(resp); + } finally { + getHeaders.release(); + } + } + + static MessageData constructGetBodiesResponse( + final Blockchain blockchain, final MessageData message, final int requestLimit) { + final GetBlockBodiesMessage getBlockBodiesMessage = GetBlockBodiesMessage.readFrom(message); + try { + final Iterable hashes = getBlockBodiesMessage.hashes(); + + final Collection bodies = new ArrayList<>(); + int count = 0; + for (final Hash hash : hashes) { + if (count >= requestLimit) { + break; + } + count++; + final Optional maybeBody = blockchain.getBlockBody(hash); + if (!maybeBody.isPresent()) { + continue; + } + bodies.add(maybeBody.get()); + } + return BlockBodiesMessage.create(bodies); + } finally { + getBlockBodiesMessage.release(); + } + } + + static MessageData constructGetReceiptsResponse( + final Blockchain blockchain, final MessageData message, final int requestLimit) { + final GetReceiptsMessage getReceipts = GetReceiptsMessage.readFrom(message); + try { + final Iterable hashes = getReceipts.hashes(); + + final List> receipts = new ArrayList<>(); + int count = 0; + for (final Hash hash : hashes) { + if (count >= requestLimit) { + break; + } + count++; + final Optional> maybeReceipts = blockchain.getTxReceipts(hash); + if (!maybeReceipts.isPresent()) { + continue; + } + receipts.add(maybeReceipts.get()); + } + return ReceiptsMessage.create(receipts); + } finally { + getReceipts.release(); + } + } + + static MessageData constructGetNodeDataResponse( + final MessageData message, final int requestLimit) { + final GetNodeDataMessage getNodeDataMessage = GetNodeDataMessage.readFrom(message); + try { + final Iterable hashes = getNodeDataMessage.hashes(); + + final List nodeData = new ArrayList<>(); + int count = 0; + for (final Hash hash : hashes) { + if (count >= requestLimit) { + break; + } + count++; + // TODO: Lookup node data and add it to the list + } + return NodeDataMessage.create(nodeData); + } finally { + getNodeDataMessage.release(); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthTask.java new file mode 100755 index 00000000000..7eab5b014f2 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/EthTask.java @@ -0,0 +1,10 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import java.util.concurrent.CompletableFuture; + +public interface EthTask { + + CompletableFuture run(); + + void cancel(); +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputation.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputation.java new file mode 100755 index 00000000000..b1c916022eb --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputation.java @@ -0,0 +1,67 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.logging.log4j.Logger; + +public class PeerReputation { + private static final Logger LOG = getLogger(); + private static final int TIMEOUT_THRESHOLD = 3; + private static final int USELESS_RESPONSE_THRESHOLD = 5; + static final long USELESS_RESPONSE_WINDOW_IN_MILLIS = + TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); + + private final ConcurrentMap timeoutCountByRequestType = + new ConcurrentHashMap<>(); + private final Queue uselessResponseTimes = new ConcurrentLinkedQueue<>(); + + public Optional recordRequestTimeout(final int requestCode) { + final int newTimeoutCount = getOrCreateTimeoutCount(requestCode).incrementAndGet(); + if (newTimeoutCount >= TIMEOUT_THRESHOLD) { + LOG.info("Disconnection triggered by repeated timeouts"); + return Optional.of(DisconnectReason.TIMEOUT); + } else { + return Optional.empty(); + } + } + + public void resetTimeoutCount(final int requestCode) { + timeoutCountByRequestType.remove(requestCode); + } + + private AtomicInteger getOrCreateTimeoutCount(final int requestCode) { + return timeoutCountByRequestType.computeIfAbsent(requestCode, code -> new AtomicInteger()); + } + + public Map timeoutCounts() { + return timeoutCountByRequestType; + } + + public Optional recordUselessResponse(final long timestamp) { + uselessResponseTimes.add(timestamp); + while (shouldRemove(uselessResponseTimes.peek(), timestamp)) { + uselessResponseTimes.poll(); + } + if (uselessResponseTimes.size() >= USELESS_RESPONSE_THRESHOLD) { + LOG.info("Disconnection triggered by exceeding useless response threshold"); + return Optional.of(DisconnectReason.USELESS_PEER); + } else { + return Optional.empty(); + } + } + + private boolean shouldRemove(final Long timestamp, final long currentTimestamp) { + return timestamp != null && timestamp + USELESS_RESPONSE_WINDOW_IN_MILLIS < currentTimestamp; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/RequestManager.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/RequestManager.java new file mode 100755 index 00000000000..25fa059d1af --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/RequestManager.java @@ -0,0 +1,166 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class RequestManager { + private final AtomicLong responseStreamId = new AtomicLong(0L); + private final Map responseStreams = new ConcurrentHashMap<>(); + private final EthPeer peer; + + private final AtomicInteger outstandingRequests = new AtomicInteger(0); + + public RequestManager(final EthPeer peer) { + this.peer = peer; + } + + public int outstandingRequests() { + return outstandingRequests.get(); + } + + public ResponseStream dispatchRequest(final RequestSender sender) throws PeerNotConnected { + outstandingRequests.incrementAndGet(); + final ResponseStream stream = createStream(); + sender.send(); + return stream; + } + + public void dispatchResponse(final EthMessage message) { + final Collection streams = new ArrayList<>(responseStreams.values()); + final int count = outstandingRequests.decrementAndGet(); + + streams.forEach(s -> s.processMessage(message.getData())); + if (count == 0) { + // No possibility of any remaining outstanding messages + closeOutstandingStreams(streams); + } + } + + public void close() { + closeOutstandingStreams(responseStreams.values()); + } + + private ResponseStream createStream() { + final long listenerId = nextStreamId(); + final ResponseStream stream = new ResponseStream(peer, () -> deregisterStream(listenerId)); + responseStreams.put(listenerId, stream); + return stream; + } + + /** Close all current streams. This will be called when the peer disconnects. */ + private void closeOutstandingStreams(final Collection outstandingStreams) { + outstandingStreams.forEach(ResponseStream::close); + } + + private void deregisterStream(final long id) { + responseStreams.remove(id); + } + + private long nextStreamId() { + return responseStreamId.incrementAndGet(); + } + + @FunctionalInterface + public interface RequestSender { + void send() throws PeerNotConnected; + } + + @FunctionalInterface + public interface ResponseCallback { + + /** + * Process a potential message response + * + * @param streamClosed True if the ResponseStream is being shut down and will no longer deliver + * messages. + * @param message the message to be processed + * @param peer the peer that owns this response stream + */ + void exec(boolean streamClosed, MessageData message, EthPeer peer); + } + + @FunctionalInterface + public interface DeregistrationProcessor { + void exec(); + } + + private static class Response { + final boolean closed; + final MessageData message; + + private Response(final boolean closed, final MessageData message) { + this.closed = closed; + this.message = message; + } + } + + public static class ResponseStream { + private final EthPeer peer; + private final DeregistrationProcessor deregisterCallback; + private final Queue bufferedResponses = new ConcurrentLinkedQueue<>(); + private volatile boolean closed = false; + private volatile ResponseCallback responseCallback = null; + + public ResponseStream(final EthPeer peer, final DeregistrationProcessor deregisterCallback) { + this.peer = peer; + this.deregisterCallback = deregisterCallback; + } + + public ResponseStream then(final ResponseCallback callback) { + if (responseCallback != null) { + // For now just manage a single callback for simplicity. We could expand this to support + // multiple listeners in the future. + throw new IllegalStateException("Response streams expect only a single callback"); + } + responseCallback = callback; + dispatchBufferedResponses(); + return this; + } + + public void close() { + if (closed) { + return; + } + closed = true; + deregisterCallback.exec(); + bufferedResponses.add(new Response(true, null)); + dispatchBufferedResponses(); + } + + public EthPeer peer() { + return peer; + } + + private void processMessage(final MessageData message) { + if (closed) { + return; + } + message.retain(); + bufferedResponses.add(new Response(false, message)); + dispatchBufferedResponses(); + } + + private void dispatchBufferedResponses() { + if (responseCallback == null) { + return; + } + Response response = bufferedResponses.poll(); + while (response != null) { + responseCallback.exec(response.closed, response.message, peer); + if (response.message != null) { + response.message.release(); + } + response = bufferedResponses.poll(); + } + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/EthTaskException.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/EthTaskException.java new file mode 100755 index 00000000000..934cdfc9acf --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/EthTaskException.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.ethereum.eth.manager.exceptions; + +public class EthTaskException extends RuntimeException { + + private final FailureReason failureReason; + + EthTaskException(final FailureReason failureReason) { + super("Task failed: " + failureReason.name()); + this.failureReason = failureReason; + } + + public FailureReason reason() { + return failureReason; + } + + public enum FailureReason { + PEER_DISCONNECTED, + NO_AVAILABLE_PEERS, + PEER_BREACHED_PROTOCOL, + INCOMPLETE_RESULTS + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/IncompleteResultsException.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/IncompleteResultsException.java new file mode 100755 index 00000000000..5eb04adf29d --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/IncompleteResultsException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.eth.manager.exceptions; + +public class IncompleteResultsException extends EthTaskException { + + public IncompleteResultsException() { + super(FailureReason.INCOMPLETE_RESULTS); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/NoAvailablePeersException.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/NoAvailablePeersException.java new file mode 100755 index 00000000000..37d062b760e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/NoAvailablePeersException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.eth.manager.exceptions; + +public class NoAvailablePeersException extends EthTaskException { + + public NoAvailablePeersException() { + super(FailureReason.NO_AVAILABLE_PEERS); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerBreachedProtocolException.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerBreachedProtocolException.java new file mode 100755 index 00000000000..21074477ece --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerBreachedProtocolException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.eth.manager.exceptions; + +public class PeerBreachedProtocolException extends EthTaskException { + + public PeerBreachedProtocolException() { + super(FailureReason.PEER_BREACHED_PROTOCOL); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerDisconnectedException.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerDisconnectedException.java new file mode 100755 index 00000000000..435f21da6ff --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/manager/exceptions/PeerDisconnectedException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.eth.manager.exceptions; + +public class PeerDisconnectedException extends EthTaskException { + + public PeerDisconnectedException() { + super(FailureReason.PEER_DISCONNECTED); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessage.java new file mode 100755 index 00000000000..c662c53dd4c --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessage.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import io.netty.buffer.ByteBuf; + +public final class BlockBodiesMessage extends AbstractMessageData { + + public static BlockBodiesMessage readFrom(final MessageData message) { + if (message instanceof BlockBodiesMessage) { + message.retain(); + return (BlockBodiesMessage) message; + } + final int code = message.getCode(); + if (code != EthPV62.BLOCK_BODIES) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a BlockBodiesMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new BlockBodiesMessage(data); + } + + public static BlockBodiesMessage create(final Iterable bodies) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + bodies.forEach(body -> body.writeTo(tmp)); + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new BlockBodiesMessage(data); + } + + private BlockBodiesMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV62.BLOCK_BODIES; + } + + public Iterable bodies(final ProtocolSchedule protocolSchedule) { + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + final byte[] tmp = new byte[data.readableBytes()]; + data.getBytes(0, tmp); + return new BytesValueRLPInput(BytesValue.wrap(tmp), false) + .readList(rlp -> BlockBody.readFrom(rlp, blockHashFunction)); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessage.java new file mode 100755 index 00000000000..c4bd0bbaf66 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessage.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Iterator; + +import io.netty.buffer.ByteBuf; + +public final class BlockHeadersMessage extends AbstractMessageData { + + public static BlockHeadersMessage readFrom(final MessageData message) { + if (message instanceof BlockHeadersMessage) { + message.retain(); + return (BlockHeadersMessage) message; + } + final int code = message.getCode(); + if (code != EthPV62.BLOCK_HEADERS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a BlockHeadersMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new BlockHeadersMessage(data); + } + + public static BlockHeadersMessage create(final Iterable headers) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + for (final BlockHeader header : headers) { + header.writeTo(tmp); + } + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new BlockHeadersMessage(data); + } + + private BlockHeadersMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV62.BLOCK_HEADERS; + } + + public Iterator getHeaders(final ProtocolSchedule protocolSchedule) { + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + final byte[] headers = new byte[data.readableBytes()]; + data.getBytes(0, headers); + return new BytesValueRLPInput(BytesValue.wrap(headers), false) + .readList(rlp -> BlockHeader.readFrom(rlp, blockHashFunction)) + .iterator(); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV62.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV62.java new file mode 100755 index 00000000000..90ea27f3b8b --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV62.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +public final class EthPV62 { + + public static final int STATUS = 0x00; + + public static final int NEW_BLOCK_HASHES = 0x01; + + public static final int TRANSACTIONS = 0x02; + + public static final int GET_BLOCK_HEADERS = 0x03; + + public static final int BLOCK_HEADERS = 0x04; + + public static final int GET_BLOCK_BODIES = 0x05; + + public static final int BLOCK_BODIES = 0x06; + + public static final int NEW_BLOCK = 0X07; + + private EthPV62() { + // Holder for constants only + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV63.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV63.java new file mode 100755 index 00000000000..303fb01b840 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/EthPV63.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +public final class EthPV63 { + + // Eth63 includes all message types from Eth62 (so see EthPV62 for where the live) + + // Plus some new message types + public static final int GET_NODE_DATA = 0x0D; + + public static final int NODE_DATA = 0x0E; + + public static final int GET_RECEIPTS = 0x0F; + + public static final int RECEIPTS = 0x10; + + private EthPV63() { + // Holder for constants only + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessage.java new file mode 100755 index 00000000000..77499878a0c --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessage.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collection; + +import io.netty.buffer.ByteBuf; + +public final class GetBlockBodiesMessage extends AbstractMessageData { + + public static GetBlockBodiesMessage readFrom(final MessageData message) { + if (message instanceof GetBlockBodiesMessage) { + message.retain(); + return (GetBlockBodiesMessage) message; + } + final int code = message.getCode(); + if (code != EthPV62.GET_BLOCK_BODIES) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a GetBlockBodiesMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new GetBlockBodiesMessage(data); + } + + public static GetBlockBodiesMessage create(final Iterable hashes) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + hashes.forEach(tmp::writeBytesValue); + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new GetBlockBodiesMessage(data); + } + + private GetBlockBodiesMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV62.GET_BLOCK_BODIES; + } + + public Iterable hashes() { + final byte[] tmp = new byte[data.readableBytes()]; + data.getBytes(0, tmp); + final RLPInput input = new BytesValueRLPInput(BytesValue.wrap(tmp), false); + input.enterList(); + final Collection hashes = new ArrayList<>(); + while (!input.isEndOfCurrentList()) { + hashes.add(Hash.wrap(input.readBytes32())); + } + input.leaveList(); + return hashes; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessage.java new file mode 100755 index 00000000000..a8c8b2a9b1e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessage.java @@ -0,0 +1,151 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.OptionalLong; + +import com.google.common.primitives.Ints; +import io.netty.buffer.ByteBuf; + +/** PV62 GetBlockHeaders Message. */ +public final class GetBlockHeadersMessage extends AbstractMessageData { + + public static GetBlockHeadersMessage readFrom(final MessageData message) { + if (message instanceof GetBlockHeadersMessage) { + message.retain(); + return (GetBlockHeadersMessage) message; + } + final int code = message.getCode(); + if (code != EthPV62.GET_BLOCK_HEADERS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a GetBlockHeadersMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new GetBlockHeadersMessage(data); + } + + public static GetBlockHeadersMessage create( + final long blockNum, final int maxHeaders, final boolean reverse, final int skip) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + tmp.writeLongScalar(blockNum); + return create(maxHeaders, reverse, skip, tmp); + } + + public static GetBlockHeadersMessage create( + final Hash hash, final int maxHeaders, final boolean reverse, final int skip) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + tmp.writeBytesValue(hash); + return create(maxHeaders, reverse, skip, tmp); + } + + public static GetBlockHeadersMessage createForSingleHeader(final Hash hash) { + return create(hash, 1, false, 0); + } + + public static GetBlockHeadersMessage createForContiguousHeaders( + final long blockNum, final int maxHeaders) { + return create(blockNum, maxHeaders, false, 0); + } + + public static GetBlockHeadersMessage createForContiguousHeaders( + final Hash blockHash, final int maxHeaders) { + return create(blockHash, maxHeaders, false, 0); + } + + private GetBlockHeadersMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV62.GET_BLOCK_HEADERS; + } + + /** + * Returns the block number that the message requests or {@link OptionalLong#EMPTY} if the request + * specifies a block hash. + * + * @return Block Number Requested or {@link OptionalLong#EMPTY} + */ + public OptionalLong blockNumber() { + final ByteBuffer raw = data.nioBuffer(); + final int offsetList = RlpUtils.decodeOffset(raw, 0); + final int lengthFirst = RlpUtils.decodeLength(raw, offsetList); + final int offsetFirst = RlpUtils.decodeOffset(raw, offsetList); + if (lengthFirst - offsetFirst == Bytes32.SIZE) { + return OptionalLong.empty(); + } else { + final byte[] tmp = new byte[lengthFirst]; + raw.position(offsetList); + raw.get(tmp); + return OptionalLong.of(RlpUtils.readLong(0, lengthFirst, tmp)); + } + } + + /** + * Returns the block hash that the message requests or {@link Optional#EMPTY} if the request + * specifies a block number. + * + * @return Block Hash Requested or {@link Optional#EMPTY} + */ + public Optional hash() { + final ByteBuffer raw = data.nioBuffer(); + final int offsetList = RlpUtils.decodeOffset(raw, 0); + final int lengthFirst = RlpUtils.decodeLength(raw, offsetList); + final int offsetFirst = RlpUtils.decodeOffset(raw, offsetList); + if (lengthFirst - offsetFirst == Bytes32.SIZE) { + final byte[] hashBytes = new byte[Bytes32.SIZE]; + raw.position(offsetFirst + offsetList); + raw.get(hashBytes); + return Optional.of(Hash.wrap(Bytes32.wrap(hashBytes))); + } else { + return Optional.empty(); + } + } + + public int maxHeaders() { + final ByteBuffer raw = data.nioBuffer(); + final int offsetList = RlpUtils.decodeOffset(raw, 0); + final byte[] tmp = new byte[raw.capacity()]; + raw.get(tmp); + final int offsetMaxHeaders = RlpUtils.nextOffset(tmp, offsetList); + final int lenMaxHeaders = RlpUtils.decodeLength(tmp, offsetMaxHeaders); + return Ints.checkedCast(RlpUtils.readLong(offsetMaxHeaders, lenMaxHeaders, tmp)); + } + + public int skip() { + final ByteBuffer raw = data.nioBuffer(); + final int offsetList = RlpUtils.decodeOffset(raw, 0); + final byte[] tmp = new byte[raw.capacity()]; + raw.get(tmp); + final int offsetSkip = RlpUtils.nextOffset(tmp, RlpUtils.nextOffset(tmp, offsetList)); + final int lenSkip = RlpUtils.decodeLength(tmp, offsetSkip); + return Ints.checkedCast(RlpUtils.readLong(offsetSkip, lenSkip, tmp)); + } + + public boolean reverse() { + return (data.getByte(this.getSize() - 1) & 0xff) != RlpUtils.RLP_ZERO; + } + + private static GetBlockHeadersMessage create( + final int maxHeaders, final boolean reverse, final int skip, final BytesValueRLPOutput tmp) { + tmp.writeIntScalar(maxHeaders); + tmp.writeIntScalar(skip); + tmp.writeIntScalar(reverse ? 1 : 0); + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new GetBlockHeadersMessage(data); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessage.java new file mode 100755 index 00000000000..bf4a44e3e1f --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessage.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collection; + +import io.netty.buffer.ByteBuf; + +public final class GetNodeDataMessage extends AbstractMessageData { + + public static GetNodeDataMessage readFrom(final MessageData message) { + if (message instanceof GetNodeDataMessage) { + message.retain(); + return (GetNodeDataMessage) message; + } + final int code = message.getCode(); + if (code != EthPV63.GET_NODE_DATA) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a GetNodeDataMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new GetNodeDataMessage(data); + } + + public static GetNodeDataMessage create(final Iterable hashes) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + hashes.forEach(tmp::writeBytesValue); + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new GetNodeDataMessage(data); + } + + private GetNodeDataMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV63.GET_NODE_DATA; + } + + public Iterable hashes() { + final byte[] tmp = new byte[data.readableBytes()]; + data.getBytes(0, tmp); + final RLPInput input = new BytesValueRLPInput(BytesValue.wrap(tmp), false); + input.enterList(); + final Collection hashes = new ArrayList<>(); + while (!input.isEndOfCurrentList()) { + hashes.add(Hash.wrap(input.readBytes32())); + } + input.leaveList(); + return hashes; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessage.java new file mode 100755 index 00000000000..87bc1f374f4 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessage.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collection; + +import io.netty.buffer.ByteBuf; + +public final class GetReceiptsMessage extends AbstractMessageData { + + public static GetReceiptsMessage readFrom(final MessageData message) { + if (message instanceof GetReceiptsMessage) { + message.retain(); + return (GetReceiptsMessage) message; + } + final int code = message.getCode(); + if (code != EthPV63.GET_RECEIPTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a GetReceipts.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new GetReceiptsMessage(data); + } + + public static GetReceiptsMessage create(final Iterable hashes) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + hashes.forEach(tmp::writeBytesValue); + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new GetReceiptsMessage(data); + } + + private GetReceiptsMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV63.GET_RECEIPTS; + } + + public Iterable hashes() { + final byte[] tmp = new byte[data.readableBytes()]; + data.getBytes(0, tmp); + final RLPInput input = new BytesValueRLPInput(BytesValue.wrap(tmp), false); + input.enterList(); + final Collection hashes = new ArrayList<>(); + while (!input.isEndOfCurrentList()) { + hashes.add(Hash.wrap(input.readBytes32())); + } + input.leaveList(); + return hashes; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessage.java new file mode 100755 index 00000000000..6172afb87b9 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessage.java @@ -0,0 +1,121 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Iterator; +import java.util.Objects; + +import com.google.common.collect.Iterators; +import io.netty.buffer.ByteBuf; + +public final class NewBlockHashesMessage extends AbstractMessageData { + + public static NewBlockHashesMessage readFrom(final MessageData message) { + if (message instanceof NewBlockHashesMessage) { + message.retain(); + return (NewBlockHashesMessage) message; + } + final int code = message.getCode(); + if (code != EthPV62.NEW_BLOCK_HASHES) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a NewBlockHashesMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new NewBlockHashesMessage(data); + } + + public static NewBlockHashesMessage create( + final Iterable hashes) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + for (final NewBlockHashesMessage.NewBlockHash hash : hashes) { + tmp.startList(); + tmp.writeBytesValue(hash.hash()); + tmp.writeLongScalar(hash.number()); + tmp.endList(); + } + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new NewBlockHashesMessage(data); + } + + private NewBlockHashesMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV62.NEW_BLOCK_HASHES; + } + + public Iterator getNewHashes() { + final byte[] hashes = new byte[data.readableBytes()]; + data.getBytes(0, hashes); + return new BytesValueRLPInput(BytesValue.wrap(hashes), false) + .readList( + rlpInput -> { + rlpInput.enterList(); + final NewBlockHashesMessage.NewBlockHash res = + new NewBlockHashesMessage.NewBlockHash( + Hash.wrap(rlpInput.readBytes32()), rlpInput.readLongScalar()); + rlpInput.leaveList(); + return res; + }) + .iterator(); + } + + @Override + public String toString() { + return String.format("NewBlockHashesMessage: [%s]", Iterators.toString(getNewHashes())); + } + + public static final class NewBlockHash { + + private final Hash hash; + + private final long number; + + public NewBlockHash(final Hash hash, final long number) { + this.hash = hash; + this.number = number; + } + + public long number() { + return number; + } + + public Hash hash() { + return hash; + } + + @Override + public String toString() { + return String.format("New Block Hash [%d: %s]", number, hash); + } + + @Override + public boolean equals(final Object that) { + if (this == that) { + return true; + } + if (!(that instanceof NewBlockHashesMessage.NewBlockHash)) { + return false; + } + final NewBlockHashesMessage.NewBlockHash other = (NewBlockHashesMessage.NewBlockHash) that; + return other.hash.equals(hash) && other.number == number; + } + + @Override + public int hashCode() { + return Objects.hash(hash, number); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessage.java new file mode 100755 index 00000000000..abbfaafad67 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessage.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.utils.ByteBufUtils; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import io.netty.buffer.ByteBuf; + +public class NewBlockMessage extends AbstractMessageData { + + private static final int MESSAGE_CODE = EthPV62.NEW_BLOCK; + + private NewBlockMessageData messageFields = null; + + private NewBlockMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return MESSAGE_CODE; + } + + public static NewBlockMessage create(final Block block, final UInt256 totalDifficulty) { + final NewBlockMessageData msgData = new NewBlockMessageData(block, totalDifficulty); + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + msgData.writeTo(out); + final ByteBuf data = ByteBufUtils.fromRLPOutput(out); + return new NewBlockMessage(data); + } + + public static NewBlockMessage readFrom(final MessageData message) { + if (message instanceof NewBlockMessage) { + message.retain(); + return (NewBlockMessage) message; + } + final int code = message.getCode(); + if (code != NewBlockMessage.MESSAGE_CODE) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a NewBlockMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new NewBlockMessage(data); + } + + public Block block(final ProtocolSchedule protocolSchedule) { + return messageFields(protocolSchedule).block(); + } + + public UInt256 totalDifficulty(final ProtocolSchedule protocolSchedule) { + return messageFields(protocolSchedule).totalDifficulty(); + } + + private NewBlockMessageData messageFields(final ProtocolSchedule protocolSchedule) { + if (messageFields == null) { + final RLPInput input = RLP.input(BytesValue.wrap(ByteBufUtils.toByteArray(data))); + messageFields = NewBlockMessageData.readFrom(input, protocolSchedule); + } + return messageFields; + } + + public static class NewBlockMessageData { + + private final Block block; + private final UInt256 totalDifficulty; + + public NewBlockMessageData(final Block block, final UInt256 totalDifficulty) { + this.block = block; + this.totalDifficulty = totalDifficulty; + } + + public Block block() { + return block; + } + + public UInt256 totalDifficulty() { + return totalDifficulty; + } + + public void writeTo(final RLPOutput out) { + out.startList(); + block.writeTo(out); + out.writeUInt256Scalar(totalDifficulty); + out.endList(); + } + + public static NewBlockMessageData readFrom( + final RLPInput in, final ProtocolSchedule protocolSchedule) { + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + in.enterList(); + final Block block = Block.readFrom(in, blockHashFunction); + final UInt256 totaldifficulty = in.readUInt256Scalar(); + return new NewBlockMessageData(block, totaldifficulty); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessage.java new file mode 100755 index 00000000000..b0a541d0e8a --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessage.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collection; + +import io.netty.buffer.ByteBuf; + +public final class NodeDataMessage extends AbstractMessageData { + + public static NodeDataMessage readFrom(final MessageData message) { + if (message instanceof NodeDataMessage) { + message.retain(); + return (NodeDataMessage) message; + } + final int code = message.getCode(); + if (code != EthPV63.NODE_DATA) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a NodeDataMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new NodeDataMessage(data); + } + + public static NodeDataMessage create(final Iterable nodeData) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + nodeData.forEach(tmp::writeBytesValue); + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new NodeDataMessage(data); + } + + private NodeDataMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV63.NODE_DATA; + } + + public Iterable nodeData() { + final byte[] tmp = new byte[data.readableBytes()]; + data.getBytes(0, tmp); + final RLPInput input = new BytesValueRLPInput(BytesValue.wrap(tmp), false); + input.enterList(); + final Collection nodeData = new ArrayList<>(); + while (!input.isEndOfCurrentList()) { + nodeData.add(input.readBytesValue()); + } + input.leaveList(); + return nodeData; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessage.java new file mode 100755 index 00000000000..3d81286f65d --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessage.java @@ -0,0 +1,76 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +import io.netty.buffer.ByteBuf; + +public final class ReceiptsMessage extends AbstractMessageData { + + public static ReceiptsMessage readFrom(final MessageData message) { + if (message instanceof ReceiptsMessage) { + message.retain(); + return (ReceiptsMessage) message; + } + final int code = message.getCode(); + if (code != EthPV63.RECEIPTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a ReceiptsMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new ReceiptsMessage(data); + } + + public static ReceiptsMessage create(final List> receipts) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + receipts.forEach( + (receiptSet) -> { + tmp.startList(); + receiptSet.forEach(r -> r.writeTo(tmp)); + tmp.endList(); + }); + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new ReceiptsMessage(data); + } + + private ReceiptsMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV63.RECEIPTS; + } + + public List> receipts() { + final byte[] tmp = new byte[data.readableBytes()]; + data.getBytes(0, tmp); + final RLPInput input = new BytesValueRLPInput(BytesValue.wrap(tmp), false); + input.enterList(); + final List> receipts = new ArrayList<>(); + while (input.nextIsList()) { + final int setSize = input.enterList(); + final List receiptSet = new ArrayList<>(setSize); + for (int i = 0; i < setSize; i++) { + receiptSet.add(TransactionReceipt.readFrom(input)); + } + input.leaveList(); + receipts.add(receiptSet); + } + input.leaveList(); + return receipts; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessage.java new file mode 100755 index 00000000000..a8d5342751f --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessage.java @@ -0,0 +1,142 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.utils.ByteBufUtils; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import io.netty.buffer.ByteBuf; + +public final class StatusMessage extends AbstractMessageData { + + private EthStatus status; + + public StatusMessage(final ByteBuf data) { + super(data); + } + + public static StatusMessage create( + final int protocolVersion, + final int networkId, + final UInt256 totalDifficulty, + final Hash bestHash, + final Hash genesisHash) { + final EthStatus status = + new EthStatus(protocolVersion, networkId, totalDifficulty, bestHash, genesisHash); + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + status.writeTo(out); + final ByteBuf data = ByteBufUtils.fromRLPOutput(out); + + return new StatusMessage(data); + } + + public static StatusMessage readFrom(final MessageData message) { + if (message instanceof StatusMessage) { + message.retain(); + return (StatusMessage) message; + } + final int code = message.getCode(); + if (code != EthPV62.STATUS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a StatusMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new StatusMessage(data); + } + + @Override + public int getCode() { + return EthPV62.STATUS; + } + + /** @return The eth protocol version the associated node is running. */ + public int protocolVersion() { + return status().protocolVersion; + } + + /** @return The id of the network the associated node is participating in. */ + public int networkId() { + return status().networkId; + } + + /** @return The total difficulty of the head of the associated node's local blockchain. */ + public UInt256 totalDifficulty() { + return status().totalDifficulty; + } + + /** @return The hash of the head of the associated node's local blockchian. */ + public Hash bestHash() { + return status().bestHash; + } + + /** + * @return The hash of the genesis block of the network the associated node is participating in. + */ + public Bytes32 genesisHash() { + return status().genesisHash; + } + + private EthStatus status() { + if (status == null) { + final RLPInput input = RLP.input(BytesValue.wrap(ByteBufUtils.toByteArray(data))); + status = EthStatus.readFrom(input); + } + return status; + } + + private static class EthStatus { + private final int protocolVersion; + private final int networkId; + private final UInt256 totalDifficulty; + private final Hash bestHash; + private final Hash genesisHash; + + public EthStatus( + final int protocolVersion, + final int networkId, + final UInt256 totalDifficulty, + final Hash bestHash, + final Hash genesisHash) { + this.protocolVersion = protocolVersion; + this.networkId = networkId; + this.totalDifficulty = totalDifficulty; + this.bestHash = bestHash; + this.genesisHash = genesisHash; + } + + public void writeTo(final RLPOutput out) { + out.startList(); + + out.writeIntScalar(protocolVersion); + out.writeIntScalar(networkId); + out.writeUInt256Scalar(totalDifficulty); + out.writeBytesValue(bestHash); + out.writeBytesValue(genesisHash); + + out.endList(); + } + + public static EthStatus readFrom(final RLPInput in) { + in.enterList(); + + final int protocolVersion = in.readIntScalar(); + final int networkId = in.readIntScalar(); + final UInt256 totalDifficulty = in.readUInt256Scalar(); + final Hash bestHash = Hash.wrap(in.readBytes32()); + final Hash genesisHash = Hash.wrap(in.readBytes32()); + + in.leaveList(); + + return new EthStatus(protocolVersion, networkId, totalDifficulty, bestHash, genesisHash); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessage.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessage.java new file mode 100755 index 00000000000..872bdef06cc --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessage.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Iterator; +import java.util.function.Function; + +import io.netty.buffer.ByteBuf; + +public class TransactionsMessage extends AbstractMessageData { + + public static TransactionsMessage readFrom(final MessageData message) { + if (message instanceof TransactionsMessage) { + message.retain(); + return (TransactionsMessage) message; + } + final int code = message.getCode(); + if (code != EthPV62.TRANSACTIONS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a TransactionsMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new TransactionsMessage(data); + } + + public static TransactionsMessage create(final Iterable transactions) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + for (final Transaction transaction : transactions) { + transaction.writeTo(tmp); + } + tmp.endList(); + final ByteBuf data = NetworkMemoryPool.allocate(tmp.encodedSize()); + data.writeBytes(tmp.encoded().extractArray()); + return new TransactionsMessage(data); + } + + private TransactionsMessage(final ByteBuf data) { + super(data); + } + + @Override + public int getCode() { + return EthPV62.TRANSACTIONS; + } + + public Iterator transactions( + final Function transactionReader) { + final byte[] transactions = new byte[data.readableBytes()]; + data.getBytes(0, transactions); + return new BytesValueRLPInput(BytesValue.wrap(transactions), false) + .readList(transactionReader) + .iterator(); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManager.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManager.java new file mode 100755 index 00000000000..d8b8ed29cd8 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManager.java @@ -0,0 +1,263 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent.EventType; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthMessage; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockHashesMessage; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockHashesMessage.NewBlockHash; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockMessage; +import net.consensys.pantheon.ethereum.eth.sync.state.PendingBlocks; +import net.consensys.pantheon.ethereum.eth.sync.state.SyncState; +import net.consensys.pantheon.ethereum.eth.sync.tasks.GetBlockFromPeerTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.PersistBlockTask; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Range; +import io.netty.util.internal.ConcurrentSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class BlockPropagationManager { + private static final Logger LOG = LogManager.getLogger(); + + private final SynchronizerConfiguration config; + private final ProtocolSchedule protocolSchedule; + private final ProtocolContext protocolContext; + private final EthContext ethContext; + private final SyncState syncState; + + private final AtomicBoolean started = new AtomicBoolean(false); + + private final Set requestedBlocks = new ConcurrentSet<>(); + private final PendingBlocks pendingBlocks; + + BlockPropagationManager( + final SynchronizerConfiguration config, + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final SyncState syncState) { + this.config = config; + this.protocolSchedule = protocolSchedule; + this.protocolContext = protocolContext; + this.ethContext = ethContext; + + this.syncState = syncState; + pendingBlocks = syncState.pendingBlocks(); + } + + public void start() { + if (started.compareAndSet(false, true)) { + setupListeners(); + } else { + throw new IllegalStateException( + "Attempt to start an already started " + this.getClass().getSimpleName() + "."); + } + } + + private void setupListeners() { + protocolContext.getBlockchain().observeBlockAdded(this::onBlockAdded); + ethContext.getEthMessages().subscribe(EthPV62.NEW_BLOCK, this::handleNewBlockFromNetwork); + ethContext + .getEthMessages() + .subscribe(EthPV62.NEW_BLOCK_HASHES, this::handleNewBlockHashesFromNetwork); + } + + private void onBlockAdded(final BlockAddedEvent blockAddedEvent, final Blockchain blockchain) { + // Check to see if any of our pending blocks are now ready for import + final Block newBlock = blockAddedEvent.getBlock(); + + List readyForImport; + synchronized (pendingBlocks) { + // Remove block from pendingBlocks list + pendingBlocks.deregisterPendingBlock(newBlock); + + // Import any pending blocks that are children of the newly added block + readyForImport = pendingBlocks.childrenOf(newBlock.getHash()); + } + + if (!readyForImport.isEmpty()) { + final Supplier>> importBlocksTask = + PersistBlockTask.forUnorderedBlocks( + protocolSchedule, protocolContext, readyForImport, HeaderValidationMode.FULL); + ethContext + .getScheduler() + .scheduleWorkerTask(importBlocksTask) + .whenComplete( + (r, t) -> { + if (r != null) { + LOG.info("Imported {} pending blocks", r.size()); + } + }); + } + + if (blockAddedEvent.getEventType().equals(EventType.HEAD_ADVANCED)) { + final long head = blockchain.getChainHeadBlockNumber(); + final long cutoff = head + config.blockPropagationRange().lowerEndpoint(); + pendingBlocks.purgeBlocksOlderThan(cutoff); + } + } + + private void handleNewBlockFromNetwork(final EthMessage message) { + final Blockchain blockchain = protocolContext.getBlockchain(); + final NewBlockMessage newBlockMessage = NewBlockMessage.readFrom(message.getData()); + try { + final Block block = newBlockMessage.block(protocolSchedule); + final UInt256 totalDifficulty = newBlockMessage.totalDifficulty(protocolSchedule); + + message.getPeer().chainState().update(block.getHeader(), totalDifficulty); + + // Return early if we don't care about this block + final long localChainHeight = protocolContext.getBlockchain().getChainHeadBlockNumber(); + final long bestChainHeight = syncState.bestChainHeight(localChainHeight); + if (!shouldImportBlockAtHeight( + block.getHeader().getNumber(), localChainHeight, bestChainHeight)) { + return; + } + if (pendingBlocks.contains(block.getHash())) { + return; + } + if (blockchain.contains(block.getHash())) { + return; + } + + importOrSavePendingBlock(block); + } catch (final RLPException e) { + message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + } finally { + newBlockMessage.release(); + } + } + + private void handleNewBlockHashesFromNetwork(final EthMessage message) { + final Blockchain blockchain = protocolContext.getBlockchain(); + final NewBlockHashesMessage newBlockHashesMessage = + NewBlockHashesMessage.readFrom(message.getData()); + try { + // Register announced blocks + final List announcedBlocks = + Lists.newArrayList(newBlockHashesMessage.getNewHashes()); + for (final NewBlockHash announcedBlock : announcedBlocks) { + message.getPeer().registerKnownBlock(announcedBlock.hash()); + message.getPeer().registerHeight(announcedBlock.hash(), announcedBlock.number()); + } + + // Filter announced blocks for blocks we care to import + final long localChainHeight = protocolContext.getBlockchain().getChainHeadBlockNumber(); + final long bestChainHeight = syncState.bestChainHeight(localChainHeight); + final List relevantAnnouncements = + announcedBlocks + .stream() + .filter(a -> shouldImportBlockAtHeight(a.number(), localChainHeight, bestChainHeight)) + .collect(Collectors.toList()); + + // Filter for blocks we don't yet know about + final List newBlocks = new ArrayList<>(); + for (final NewBlockHash announcedBlock : relevantAnnouncements) { + if (requestedBlocks.contains(announcedBlock.hash())) { + continue; + } + if (pendingBlocks.contains(announcedBlock.hash())) { + continue; + } + if (blockchain.contains(announcedBlock.hash())) { + continue; + } + if (requestedBlocks.add(announcedBlock.hash())) { + newBlocks.add(announcedBlock); + } + } + + // Process known blocks we care about + for (final NewBlockHash newBlock : newBlocks) { + processAnnouncedBlock(message.getPeer(), newBlock) + .whenComplete((r, t) -> requestedBlocks.remove(newBlock.hash())); + } + } catch (final RLPException e) { + message.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + } finally { + newBlockHashesMessage.release(); + } + } + + private CompletableFuture processAnnouncedBlock( + final EthPeer peer, final NewBlockHash newBlock) { + final AbstractPeerTask getBlockTask = + GetBlockFromPeerTask.create(protocolSchedule, ethContext, newBlock.hash()).assignPeer(peer); + + return getBlockTask.run().thenCompose((r) -> importOrSavePendingBlock(r.getResult())); + } + + @VisibleForTesting + CompletableFuture importOrSavePendingBlock(final Block block) { + // Synchronize to avoid race condition where block import event fires after the + // blockchain.contains() check and before the block is registered, causing onBlockAdded() to be + // invoked for the parent of this block before we are able to register it. + synchronized (pendingBlocks) { + if (!protocolContext.getBlockchain().contains(block.getHeader().getParentHash())) { + // Block isn't connected to local chain, save it to pending blocks collection + if (pendingBlocks.registerPendingBlock(block)) { + LOG.info( + "Saving announced block {} ({}) for future import", + block.getHeader().getNumber(), + block.getHash()); + } + return CompletableFuture.completedFuture(block); + } + } + + // Import block + final PersistBlockTask importTask = + PersistBlockTask.create( + protocolSchedule, protocolContext, block, HeaderValidationMode.FULL); + return ethContext + .getScheduler() + .scheduleWorkerTask(importTask::run) + .whenComplete( + (r, t) -> { + if (t != null) { + LOG.warn( + "Failed to import announced block {} ({}).", + block.getHeader().getNumber(), + block.getHash()); + } else { + LOG.info( + "Successfully imported announced block {} ({}).", + block.getHeader().getNumber(), + block.getHash()); + } + }); + } + + // Only import blocks within a certain range of our head and sync target + private boolean shouldImportBlockAtHeight( + final long blockNumber, final long localHeight, final long bestChainHeight) { + final long distanceFromLocalHead = blockNumber - localHeight; + final long distanceFromBestPeer = blockNumber - bestChainHeight; + final Range importRange = config.blockPropagationRange(); + return importRange.contains(distanceFromLocalHead) + && importRange.contains(distanceFromBestPeer); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTracker.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTracker.java new file mode 100755 index 00000000000..56b24d2d11c --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTracker.java @@ -0,0 +1,70 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthPeers.ConnectCallback; +import net.consensys.pantheon.ethereum.eth.sync.tasks.GetHeadersFromPeerByHashTask; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import org.apache.logging.log4j.Logger; + +public class ChainHeadTracker implements ConnectCallback { + + private static final Logger LOG = getLogger(); + + private final EthContext ethContext; + private final ProtocolSchedule protocolSchedule; + private final TrailingPeerLimiter trailingPeerLimiter; + + public ChainHeadTracker( + final EthContext ethContext, + final ProtocolSchedule protocolSchedule, + final TrailingPeerLimiter trailingPeerLimiter) { + this.ethContext = ethContext; + this.protocolSchedule = protocolSchedule; + this.trailingPeerLimiter = trailingPeerLimiter; + } + + public static void trackChainHeadForPeers( + final EthContext ethContext, + final ProtocolSchedule protocolSchedule, + final Blockchain blockchain, + final SynchronizerConfiguration syncConfiguration) { + final TrailingPeerLimiter trailingPeerLimiter = + new TrailingPeerLimiter( + ethContext.getEthPeers(), + blockchain, + syncConfiguration.trailingPeerBlocksBehindThreshold(), + syncConfiguration.maxTrailingPeers()); + final ChainHeadTracker tracker = + new ChainHeadTracker(ethContext, protocolSchedule, trailingPeerLimiter); + ethContext.getEthPeers().subscribeConnect(tracker); + blockchain.observeBlockAdded(trailingPeerLimiter); + } + + @Override + public void onPeerConnected(final EthPeer peer) { + LOG.debug("Requesting chain head info for {}", peer); + GetHeadersFromPeerByHashTask.forSingleHash( + protocolSchedule, ethContext, Hash.wrap(peer.chainState().getBestBlock().getHash())) + .assignPeer(peer) + .run() + .whenComplete( + (peerResult, error) -> { + if (peerResult != null && !peerResult.getResult().isEmpty()) { + final BlockHeader chainHeadHeader = peerResult.getResult().get(0); + peer.chainState().update(chainHeadHeader); + trailingPeerLimiter.enforceTrailingPeerLimit(); + } else { + LOG.debug("Failed to retrieve chain head information for " + peer, error); + peer.disconnect(DisconnectReason.USELESS_PEER); + } + }); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java new file mode 100755 index 00000000000..45dd820089e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.SyncStatus; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.sync.state.PendingBlocks; +import net.consensys.pantheon.ethereum.eth.sync.state.SyncState; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class DefaultSynchronizer implements Synchronizer { + + private static final Logger LOG = LogManager.getLogger(); + + private final SyncState syncState; + private final AtomicBoolean started = new AtomicBoolean(false); + private final BlockPropagationManager blockPropagationManager; + private final Downloader downloader; + + public DefaultSynchronizer( + final SynchronizerConfiguration syncConfig, + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext) { + this.syncState = + new SyncState(protocolContext.getBlockchain(), ethContext, new PendingBlocks()); + this.blockPropagationManager = + new BlockPropagationManager<>( + syncConfig, protocolSchedule, protocolContext, ethContext, syncState); + this.downloader = + new Downloader<>(syncConfig, protocolSchedule, protocolContext, ethContext, syncState); + + ChainHeadTracker.trackChainHeadForPeers( + ethContext, protocolSchedule, protocolContext.getBlockchain(), syncConfig); + if (syncConfig.syncMode().equals(SyncMode.FAST)) { + LOG.info("Fast sync enabled."); + } + } + + @Override + public void start() { + if (started.compareAndSet(false, true)) { + LOG.info("Starting synchronizer."); + blockPropagationManager.start(); + downloader.start(); + } else { + throw new IllegalStateException("Attempt to start an already started synchronizer."); + } + } + + @Override + public Optional getSyncStatus() { + if (!started.get()) { + return Optional.empty(); + } + return Optional.of(syncState.syncStatus()); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/Downloader.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/Downloader.java new file mode 100755 index 00000000000..088362257a2 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/Downloader.java @@ -0,0 +1,396 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.ChainState; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthPeers; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.sync.state.SyncState; +import net.consensys.pantheon.ethereum.eth.sync.state.SyncTarget; +import net.consensys.pantheon.ethereum.eth.sync.tasks.DetermineCommonAncestorTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.GetHeadersFromPeerByHashTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.ImportBlocksTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.PipelinedImportChainSegmentTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.WaitForPeerTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.WaitForPeersTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.ExceptionUtils; +import net.consensys.pantheon.util.uint.UInt256; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.collect.Lists; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Downloader { + private static final Logger LOG = LogManager.getLogger(); + + private final SynchronizerConfiguration config; + private final ProtocolSchedule protocolSchedule; + private final ProtocolContext protocolContext; + private final EthContext ethContext; + private final SyncState syncState; + + private final Deque checkpointHeaders = new ConcurrentLinkedDeque<>(); + private int checkpointTimeouts = 0; + private int chainSegmentTimeouts = 0; + private volatile boolean syncTargetDisconnected = false; + + private final AtomicBoolean started = new AtomicBoolean(false); + private long syncTargetDisconnectListenerId; + protected CompletableFuture currentTask; + + Downloader( + final SynchronizerConfiguration config, + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final SyncState syncState) { + this.config = config; + this.protocolSchedule = protocolSchedule; + this.protocolContext = protocolContext; + this.ethContext = ethContext; + + this.syncState = syncState; + } + + public void start() { + if (started.compareAndSet(false, true)) { + executeDownload(); + } else { + throw new IllegalStateException( + "Attempt to start an already started " + this.getClass().getSimpleName() + "."); + } + } + + private CompletableFuture executeDownload() { + // Find target, pull checkpoint headers, import, repeat + currentTask = + waitForPeers() + .thenCompose(r -> findSyncTarget()) + .thenCompose(this::pullCheckpointHeaders) + .thenCompose(r -> importBlocks()) + .thenCompose(r -> checkSyncTarget()) + .whenComplete( + (r, t) -> { + if (t != null) { + LOG.error("Error encountered while downloading", t); + // On error, wait a bit before retrying + ethContext + .getScheduler() + .scheduleFutureTask(this::executeDownload, Duration.ofSeconds(2)); + } else { + executeDownload(); + } + }); + return currentTask; + } + + private CompletableFuture waitForPeers() { + return WaitForPeersTask.create(ethContext, 1).run(); + } + + private CompletableFuture waitForNewPeer() { + return ethContext + .getScheduler() + .timeout(WaitForPeerTask.create(ethContext), Duration.ofSeconds(5)); + } + + private CompletableFuture findSyncTarget() { + final Optional maybeSyncTarget = syncState.syncTarget(); + if (maybeSyncTarget.isPresent()) { + // Nothing to do + return CompletableFuture.completedFuture(maybeSyncTarget.get()); + } + + final Optional maybeBestPeer = ethContext.getEthPeers().bestPeer(); + if (!maybeBestPeer.isPresent()) { + LOG.info("No sync target, wait for peers."); + return waitForPeerAndThenSetSyncTarget(); + } else { + final EthPeer bestPeer = maybeBestPeer.get(); + final long peerHeight = bestPeer.chainState().getEstimatedHeight(); + final UInt256 peerTd = bestPeer.chainState().getBestBlock().getTotalDifficulty(); + if (peerTd.compareTo(syncState.chainHeadTotalDifficulty()) <= 0 + && peerHeight <= syncState.chainHeadNumber()) { + // We're caught up to our best peer, try again when a new peer connects + LOG.info("Caught up to best peer: " + bestPeer.chainState().getEstimatedHeight()); + return waitForPeerAndThenSetSyncTarget(); + } + return DetermineCommonAncestorTask.create( + protocolSchedule, + protocolContext, + ethContext, + bestPeer, + config.downloaderHeaderRequestSize()) + .run() + .handle((r, t) -> r) + .thenCompose( + (target) -> { + if (target == null) { + return waitForPeerAndThenSetSyncTarget(); + } + final SyncTarget syncTarget = syncState.setSyncTarget(bestPeer, target); + LOG.info("Found common ancestor with sync target at block {}", target.getNumber()); + LOG.info("Set sync target: {}.", syncTarget); + syncTargetDisconnectListenerId = + bestPeer.subscribeDisconnect(this::onSyncTargetPeerDisconnect); + return CompletableFuture.completedFuture(syncTarget); + }); + } + } + + private CompletableFuture waitForPeerAndThenSetSyncTarget() { + return waitForNewPeer().handle((r, t) -> r).thenCompose((r) -> findSyncTarget()); + } + + private void onSyncTargetPeerDisconnect(final EthPeer ethPeer) { + LOG.info("Sync target disconnected: {}", ethPeer); + syncTargetDisconnected = true; + } + + private CompletableFuture checkSyncTarget() { + final Optional maybeSyncTarget = syncState.syncTarget(); + if (!maybeSyncTarget.isPresent()) { + // Nothing to do + return CompletableFuture.completedFuture(null); + } + + final SyncTarget syncTarget = maybeSyncTarget.get(); + if (shouldSwitchSyncTarget(syncTarget)) { + LOG.info("Better sync target found, clear current sync target: {}.", syncTarget); + clearSyncTarget(syncTarget); + return CompletableFuture.completedFuture(null); + } + if (finishedSyncingToCurrentTarget()) { + LOG.info("Finished syncing to target: {}.", syncTarget); + clearSyncTarget(syncTarget); + // Wait a bit before checking for a new sync target + final CompletableFuture future = new CompletableFuture<>(); + ethContext + .getScheduler() + .scheduleFutureTask(() -> future.complete(null), Duration.ofSeconds(10)); + return future; + } + return CompletableFuture.completedFuture(null); + } + + private boolean shouldSwitchSyncTarget(final SyncTarget currentTarget) { + final EthPeer currentPeer = currentTarget.peer(); + final ChainState currentPeerChainState = currentPeer.chainState(); + final Optional maybeBestPeer = ethContext.getEthPeers().bestPeer(); + + return maybeBestPeer + .map( + bestPeer -> { + if (EthPeers.BEST_CHAIN.compare(bestPeer, currentPeer) <= 0) { + // Our current target is better or equal to the best peer + return false; + } + // Require some threshold to be exceeded before switching targets to keep some + // stability + // when multiple peers are in range of each other + final ChainState bestPeerChainState = bestPeer.chainState(); + final long heightDifference = + bestPeerChainState.getEstimatedHeight() + - currentPeerChainState.getEstimatedHeight(); + if (heightDifference == 0 && bestPeerChainState.getEstimatedHeight() == 0) { + // Only check td if we don't have a height metric + final UInt256 tdDifference = + bestPeerChainState + .getBestBlock() + .getTotalDifficulty() + .minus(currentPeerChainState.getBestBlock().getTotalDifficulty()); + return tdDifference.compareTo(config.downloaderChangeTargetThresholdByTd()) > 0; + } + return heightDifference > config.downloaderChangeTargetThresholdByHeight(); + }) + .orElse(false); + } + + private boolean finishedSyncingToCurrentTarget() { + return syncTargetDisconnected || checkpointsHaveTimedOut() || chainSegmentsHaveTimedOut(); + } + + private boolean checkpointsHaveTimedOut() { + // We have no more checkpoints, and have been unable to pull any new checkpoints for + // several cycles. + return checkpointHeaders.size() == 0 + && checkpointTimeouts >= config.downloaderCheckpointTimeoutsPermitted(); + } + + private boolean chainSegmentsHaveTimedOut() { + return chainSegmentTimeouts >= config.downloaderChainSegmentTimeoutsPermitted(); + } + + private void clearSyncTarget() { + syncState.syncTarget().ifPresent(this::clearSyncTarget); + } + + private void clearSyncTarget(final SyncTarget syncTarget) { + chainSegmentTimeouts = 0; + checkpointTimeouts = 0; + checkpointHeaders.clear(); + syncTarget.peer().unsubscribeDisconnect(syncTargetDisconnectListenerId); + syncTargetDisconnected = false; + syncState.clearSyncTarget(); + } + + private boolean shouldDownloadMoreCheckpoints() { + return !syncTargetDisconnected + && checkpointHeaders.size() < config.downloaderHeaderRequestSize() + && checkpointTimeouts < config.downloaderCheckpointTimeoutsPermitted(); + } + + private CompletableFuture pullCheckpointHeaders(final SyncTarget syncTarget) { + if (!shouldDownloadMoreCheckpoints()) { + return CompletableFuture.completedFuture(null); + } + + // Try to pull more checkpoint headers + return checkpointHeadersTask(syncTarget) + .run() + .handle( + (r, t) -> { + t = ExceptionUtils.rootCause(t); + if (t instanceof TimeoutException) { + checkpointTimeouts++; + return null; + } else if (t != null) { + return r; + } + final List headers = r.getResult(); + if (headers.size() > 0 + && checkpointHeaders.size() > 0 + && checkpointHeaders.getLast().equals(headers.get(0))) { + // Don't push header that is already tracked + headers.remove(0); + } + if (headers.isEmpty()) { + checkpointTimeouts++; + } else { + checkpointTimeouts = 0; + checkpointHeaders.addAll(headers); + LOG.info("Tracking {} checkpoint headers", checkpointHeaders.size()); + } + return r; + }); + } + + private EthTask>> checkpointHeadersTask( + final SyncTarget syncTarget) { + final BlockHeader lastHeader = + checkpointHeaders.size() > 0 ? checkpointHeaders.getLast() : syncTarget.commonAncestor(); + LOG.info("Requesting checkpoint headers from {}", lastHeader.getNumber()); + return GetHeadersFromPeerByHashTask.startingAtHash( + protocolSchedule, + ethContext, + lastHeader.getHash(), + lastHeader.getNumber(), + config.downloaderHeaderRequestSize() + 1, + config.downloaderChainSegmentSize() - 1) + .assignPeer(syncTarget.peer()); + } + + private CompletableFuture> importBlocks() { + if (checkpointHeaders.isEmpty()) { + // No checkpoints to download + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + CompletableFuture> importedBlocks; + if (checkpointHeaders.size() < 2) { + // Download blocks without constraining the end block + final ImportBlocksTask importTask = + ImportBlocksTask.fromHeader( + protocolSchedule, + protocolContext, + ethContext, + checkpointHeaders.getFirst(), + config.downloaderChainSegmentSize()); + importedBlocks = importTask.run().thenApply(PeerTaskResult::getResult); + } else { + final PipelinedImportChainSegmentTask importTask = + PipelinedImportChainSegmentTask.forCheckpoints( + protocolSchedule, + protocolContext, + ethContext, + config.downloaderParallelism(), + Lists.newArrayList(checkpointHeaders)); + importedBlocks = importTask.run(); + } + + return importedBlocks.whenComplete( + (r, t) -> { + t = ExceptionUtils.rootCause(t); + if (t instanceof InvalidBlockException) { + // Blocks were invalid, meaning our checkpoints are wrong + // Reset sync target + final Optional maybeSyncTarget = syncState.syncTarget(); + maybeSyncTarget.ifPresent( + target -> target.peer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL)); + final String peerDescriptor = + maybeSyncTarget + .map(SyncTarget::peer) + .map(EthPeer::toString) + .orElse("(unknown - already disconnected)"); + LOG.warn( + "Invalid block discovered while downloading from peer {}. Disconnect.", + peerDescriptor); + clearSyncTarget(); + } else if (t != null || r.isEmpty()) { + if (t != null) { + LOG.error("Encountered error importing blocks", t); + } + if (clearImportedCheckpointHeaders()) { + chainSegmentTimeouts = 0; + } + if (t instanceof TimeoutException || r != null) { + // Download timed out, or returned no new blocks + chainSegmentTimeouts++; + } + } else { + chainSegmentTimeouts = 0; + final BlockHeader lastImportedCheckpoint = checkpointHeaders.getLast(); + checkpointHeaders.clear(); + syncState + .syncTarget() + .ifPresent(target -> target.setCommonAncestor(lastImportedCheckpoint)); + } + }); + } + + private boolean clearImportedCheckpointHeaders() { + final Blockchain blockchain = protocolContext.getBlockchain(); + // Update checkpoint headers to reflect if any checkpoints were imported. + final List imported = new ArrayList<>(); + while (!checkpointHeaders.isEmpty() + && blockchain.contains(checkpointHeaders.peekFirst().getHash())) { + imported.add(checkpointHeaders.removeFirst()); + } + final BlockHeader lastImportedCheckpointHeader = imported.get(imported.size() - 1); + // The first checkpoint header is always present in the blockchain. + checkpointHeaders.addFirst(lastImportedCheckpointHeader); + syncState + .syncTarget() + .ifPresent(target -> target.setCommonAncestor(lastImportedCheckpointHeader)); + return imported.size() > 1; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SyncMode.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SyncMode.java new file mode 100755 index 00000000000..a44496e03ab --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SyncMode.java @@ -0,0 +1,17 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +public enum SyncMode { + // Fully validate all blocks as they sync + FULL, + // Perform light validation on older blocks, and switch to full validation for more recent blocks + FAST; + + public static SyncMode fromString(final String str) { + for (final SyncMode mode : SyncMode.values()) { + if (mode.name().equalsIgnoreCase(str)) { + return mode; + } + } + return null; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SynchronizerConfiguration.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SynchronizerConfiguration.java new file mode 100755 index 00000000000..f66c79b4721 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/SynchronizerConfiguration.java @@ -0,0 +1,308 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Optional; + +import com.google.common.collect.Range; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SynchronizerConfiguration { + private static final Logger LOG = LogManager.getLogger(); + + // TODO: Determine reasonable defaults here + public static int DEFAULT_PIVOT_DISTANCE_FROM_HEAD = 500; + public static float DEFAULT_FULL_VALIDATION_RATE = .1f; + + // Fast sync config + private final int fastSyncPivotDistance; + private final float fastSyncFullValidationRate; + + // Block propagation config + private final Range blockPropagationRange; + + // General config + private final SyncMode requestedSyncMode; + private final Optional syncMode; + + // Downloader config + private final long downloaderChangeTargetThresholdByHeight; + private final UInt256 downloaderChangeTargetThresholdByTd; + private final int downloaderHeaderRequestSize; + private final int downloaderCheckpointTimeoutsPermitted; + private final int downloaderChainSegmentTimeoutsPermitted; + private final int downloaderChainSegmentSize; + private final long trailingPeerBlocksBehindThreshold; + private final int maxTrailingPeers; + private final int downloaderParallelism; + + private SynchronizerConfiguration( + final SyncMode requestedSyncMode, + final int fastSyncPivotDistance, + final float fastSyncFullValidationRate, + final Range blockPropagationRange, + final Optional syncMode, + final long downloaderChangeTargetThresholdByHeight, + final UInt256 downloaderChangeTargetThresholdByTd, + final int downloaderHeaderRequestSize, + final int downloaderCheckpointTimeoutsPermitted, + final int downloaderChainSegmentTimeoutsPermitted, + final int downloaderChainSegmentSize, + final long trailingPeerBlocksBehindThreshold, + final int maxTrailingPeers, + final int downloaderParallelism) { + this.requestedSyncMode = requestedSyncMode; + this.fastSyncPivotDistance = fastSyncPivotDistance; + this.fastSyncFullValidationRate = fastSyncFullValidationRate; + this.blockPropagationRange = blockPropagationRange; + this.syncMode = syncMode; + this.downloaderChangeTargetThresholdByHeight = downloaderChangeTargetThresholdByHeight; + this.downloaderChangeTargetThresholdByTd = downloaderChangeTargetThresholdByTd; + this.downloaderHeaderRequestSize = downloaderHeaderRequestSize; + this.downloaderCheckpointTimeoutsPermitted = downloaderCheckpointTimeoutsPermitted; + this.downloaderChainSegmentTimeoutsPermitted = downloaderChainSegmentTimeoutsPermitted; + this.downloaderChainSegmentSize = downloaderChainSegmentSize; + this.trailingPeerBlocksBehindThreshold = trailingPeerBlocksBehindThreshold; + this.maxTrailingPeers = maxTrailingPeers; + this.downloaderParallelism = downloaderParallelism; + } + + /** + * Validates the sync configuration against the blockchain, to define the actual sync mode. + * + * @param blockchain the local blockchain + * @return a new, validated config instance + */ + public SynchronizerConfiguration validated(final Blockchain blockchain) { + if (syncMode.isPresent()) { + return this; + } + + SyncMode actualSyncMode; + if (requestedSyncMode.equals(SyncMode.FAST)) { + final boolean blockchainIsEmpty = + blockchain.getChainHeadBlockNumber() != BlockHeader.GENESIS_BLOCK_NUMBER; + actualSyncMode = blockchainIsEmpty ? SyncMode.FULL : SyncMode.FAST; + if (!actualSyncMode.equals(requestedSyncMode)) { + LOG.info( + "Fast sync was requested, but cannot be enabled because the local blockchain is not empty."); + } + } else { + actualSyncMode = requestedSyncMode; + } + + return new SynchronizerConfiguration( + requestedSyncMode, + fastSyncPivotDistance, + fastSyncFullValidationRate, + blockPropagationRange, + Optional.of(actualSyncMode), + downloaderChangeTargetThresholdByHeight, + downloaderChangeTargetThresholdByTd, + downloaderHeaderRequestSize, + downloaderCheckpointTimeoutsPermitted, + downloaderChainSegmentTimeoutsPermitted, + downloaderChainSegmentSize, + trailingPeerBlocksBehindThreshold, + maxTrailingPeers, + downloaderParallelism); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * The actual sync mode to be used. + * + * @return the sync mode + */ + public SyncMode syncMode() { + if (!syncMode.isPresent()) { + throw new IllegalStateException( + "Attempt to access sync mode without first validating configuration."); + } + return syncMode.get(); + } + + /** + * The range of block numbers (relative to the current chain head and the best network block) that + * are considered appropriate to import as new blocks are announced on the network. + * + * @return the range of blocks considered valid to import from the network, relative to the the + * current chain head. + */ + public Range blockPropagationRange() { + return blockPropagationRange; + } + + /** + * The distance from the chain head at which we should switch from fast sync to full sync. + * + * @return distance from the chain head at which we should switch from fast sync to full sync. + */ + public int fastSyncPivotDistance() { + return fastSyncPivotDistance; + } + + public long downloaderChangeTargetThresholdByHeight() { + return downloaderChangeTargetThresholdByHeight; + } + + public UInt256 downloaderChangeTargetThresholdByTd() { + return downloaderChangeTargetThresholdByTd; + } + + public int downloaderHeaderRequestSize() { + return downloaderHeaderRequestSize; + } + + public int downloaderCheckpointTimeoutsPermitted() { + return downloaderCheckpointTimeoutsPermitted; + } + + public int downloaderChainSegmentTimeoutsPermitted() { + return downloaderChainSegmentTimeoutsPermitted; + } + + public int downloaderChainSegmentSize() { + return downloaderChainSegmentSize; + } + + /** + * The number of blocks behind we allow a peer to be before considering them a trailing peer. + * + * @return the maximum number of blocks behind a peer can be while being considered current. + */ + public long trailingPeerBlocksBehindThreshold() { + return trailingPeerBlocksBehindThreshold; + } + + public int maxTrailingPeers() { + return maxTrailingPeers; + } + + public int downloaderParallelism() { + return downloaderParallelism; + } + + /** + * The rate at which blocks should be fully validated during fast sync. At a rate of 1f, all + * blocks are fully validated. At rates less than 1f, a subset of blocks will undergo light-weight + * validation. + * + * @return rate at which blocks should be fully validated during fast sync. + */ + public float fastSyncFullValidationRate() { + return fastSyncFullValidationRate; + } + + public static class Builder { + private int fastSyncPivotDistance = DEFAULT_PIVOT_DISTANCE_FROM_HEAD; + private float fastSyncFullValidationRate = DEFAULT_FULL_VALIDATION_RATE; + private SyncMode syncMode = SyncMode.FULL; + private Range blockPropagationRange = Range.closed(-10L, 30L); + private long downloaderChangeTargetThresholdByHeight = 20L; + private UInt256 downloaderChangeTargetThresholdByTd = UInt256.of(1_000_000_000L); + private int downloaderHeaderRequestSize = 10; + private int downloaderCheckpointTimeoutsPermitted = 5; + private int downloaderChainSegmentTimeoutsPermitted = 5; + private int downloaderChainSegmentSize = 20; + private long trailingPeerBlocksBehindThreshold; + private int maxTrailingPeers = Integer.MAX_VALUE; + private int downloaderParallelism = 2; + + public Builder fastSyncPivotDistance(final int distance) { + fastSyncPivotDistance = distance; + return this; + } + + public Builder fastSyncFastSyncFullValidationRate(final float rate) { + this.fastSyncFullValidationRate = rate; + return this; + } + + public Builder syncMode(final SyncMode mode) { + this.syncMode = mode; + return this; + } + + public Builder downloaderChangeTargetThresholdByHeight( + final long downloaderChangeTargetThresholdByHeight) { + this.downloaderChangeTargetThresholdByHeight = downloaderChangeTargetThresholdByHeight; + return this; + } + + public Builder downloaderChangeTargetThresholdByTd( + final UInt256 downloaderChangeTargetThresholdByTd) { + this.downloaderChangeTargetThresholdByTd = downloaderChangeTargetThresholdByTd; + return this; + } + + public Builder downloaderHeadersRequestSize(final int downloaderHeaderRequestSize) { + this.downloaderHeaderRequestSize = downloaderHeaderRequestSize; + return this; + } + + public Builder downloaderCheckpointTimeoutsPermitted( + final int downloaderCheckpointTimeoutsPermitted) { + this.downloaderCheckpointTimeoutsPermitted = downloaderCheckpointTimeoutsPermitted; + return this; + } + + public Builder downloaderChainSegmentTimeoutsPermitted( + final int downloaderChainSegmentTimeoutsPermitted) { + this.downloaderChainSegmentTimeoutsPermitted = downloaderChainSegmentTimeoutsPermitted; + return this; + } + + public Builder downloaderChainSegmentSize(final int downloaderChainSegmentSize) { + this.downloaderChainSegmentSize = downloaderChainSegmentSize; + return this; + } + + public Builder blockPropagationRange(final long min, final long max) { + checkArgument(min < max, "Invalid range: min must be less than max."); + blockPropagationRange = Range.closed(min, max); + return this; + } + + public Builder trailingPeerBlocksBehindThreshold(final long trailingPeerBlocksBehindThreshold) { + this.trailingPeerBlocksBehindThreshold = trailingPeerBlocksBehindThreshold; + return this; + } + + public Builder maxTrailingPeers(final int maxTrailingPeers) { + this.maxTrailingPeers = maxTrailingPeers; + return this; + } + + public Builder downloaderParallelisim(final int downloaderParallelism) { + this.downloaderParallelism = downloaderParallelism; + return this; + } + + public SynchronizerConfiguration build() { + return new SynchronizerConfiguration( + syncMode, + fastSyncPivotDistance, + fastSyncFullValidationRate, + blockPropagationRange, + Optional.empty(), + downloaderChangeTargetThresholdByHeight, + downloaderChangeTargetThresholdByTd, + downloaderHeaderRequestSize, + downloaderCheckpointTimeoutsPermitted, + downloaderChainSegmentTimeoutsPermitted, + downloaderChainSegmentSize, + trailingPeerBlocksBehindThreshold, + maxTrailingPeers, + downloaderParallelism); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiter.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiter.java new file mode 100755 index 00000000000..54adc12515c --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiter.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent.EventType; +import net.consensys.pantheon.ethereum.chain.BlockAddedObserver; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthPeers; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.Logger; + +public class TrailingPeerLimiter implements BlockAddedObserver { + + private static final Logger LOG = getLogger(); + + private static final Comparator BY_CHAIN_HEIGHT = + Comparator.comparing(peer -> peer.chainState().getEstimatedHeight()); + // Note rechecking only on blocks that are a multiple of 100 is just a simple way of limiting + // how often we rerun the check. + private static final int RECHECK_PEERS_WHEN_BLOCK_NUMBER_MULTIPLE_OF = 100; + private final EthPeers ethPeers; + private final Blockchain blockchain; + private final long trailingPeerBlocksBehindThreshold; + private final int maxTrailingPeers; + + public TrailingPeerLimiter( + final EthPeers ethPeers, + final Blockchain blockchain, + final long trailingPeerBlocksBehindThreshold, + final int maxTrailingPeers) { + this.ethPeers = ethPeers; + this.blockchain = blockchain; + this.trailingPeerBlocksBehindThreshold = trailingPeerBlocksBehindThreshold; + this.maxTrailingPeers = maxTrailingPeers; + } + + public void enforceTrailingPeerLimit() { + final List trailingPeers = + ethPeers + .availablePeers() + .filter(peer -> peer.chainState().hasEstimatedHeight()) + .filter( + peer -> + peer.chainState().getEstimatedHeight() + trailingPeerBlocksBehindThreshold + < blockchain.getChainHeadBlockNumber()) + .sorted(BY_CHAIN_HEIGHT) + .collect(Collectors.toList()); + + while (!trailingPeers.isEmpty() && trailingPeers.size() > maxTrailingPeers) { + final EthPeer peerToDisconnect = trailingPeers.remove(0); + LOG.info("Enforcing trailing peers limit by disconnecting {}", peerToDisconnect); + peerToDisconnect.disconnect(DisconnectReason.TOO_MANY_PEERS); + } + } + + @Override + public void onBlockAdded(final BlockAddedEvent event, final Blockchain blockchain) { + if (event.getEventType() != EventType.FORK + && event.getBlock().getHeader().getNumber() % RECHECK_PEERS_WHEN_BLOCK_NUMBER_MULTIPLE_OF + == 0) { + enforceTrailingPeerLimit(); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/FastSyncState.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/FastSyncState.java new file mode 100755 index 00000000000..3788b98738a --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/FastSyncState.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.eth.sync.state; + +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; + +public final class FastSyncState { + private long fastSyncTargetBlockNumber = -1; + + private final SynchronizerConfiguration config; + + public FastSyncState(final SynchronizerConfiguration config) { + this.config = config; + } + + /** + * Registers the chain height that we're trying to sync to. + * + * @param blockNumber the height of the chain we are syncing to. + */ + public void setFastSyncChainTarget(final long blockNumber) { + fastSyncTargetBlockNumber = blockNumber; + } + + /** @return the block number at which we switch from fast sync to full sync */ + public long pivot() { + return Math.max(fastSyncTargetBlockNumber - config.fastSyncPivotDistance(), 0); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocks.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocks.java new file mode 100755 index 00000000000..de193a0e97a --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocks.java @@ -0,0 +1,89 @@ +package net.consensys.pantheon.ethereum.eth.sync.state; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Hash; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import io.netty.util.internal.ConcurrentSet; + +public class PendingBlocks { + + private final Map pendingBlocks = new ConcurrentHashMap<>(); + private final Map> pendingBlocksByParentHash = new ConcurrentHashMap<>(); + + /** + * Track the given block. + * + * @param pendingBlock the block to track + * @return true if the block was added (was not previously present) + */ + public boolean registerPendingBlock(final Block pendingBlock) { + final Block previousValue = + this.pendingBlocks.putIfAbsent(pendingBlock.getHash(), pendingBlock); + if (previousValue != null) { + return false; + } + + pendingBlocksByParentHash + .computeIfAbsent( + pendingBlock.getHeader().getParentHash(), + h -> { + final ConcurrentSet set = new ConcurrentSet<>(); + // Go ahead and add our value at construction, so that we don't set an empty set which + // could be removed in deregisterPendingBlock + set.add(pendingBlock.getHash()); + return set; + }) + .add(pendingBlock.getHash()); + + return true; + } + + /** + * Stop tracking the given block. + * + * @param block the block that is no longer pending + * @return true if this block was removed + */ + public boolean deregisterPendingBlock(final Block block) { + final Hash parentHash = block.getHeader().getParentHash(); + final Block removed = pendingBlocks.remove(block.getHash()); + final Set blocksForParent = pendingBlocksByParentHash.get(parentHash); + if (blocksForParent != null) { + blocksForParent.remove(block.getHash()); + pendingBlocksByParentHash.remove(parentHash, Collections.emptySet()); + } + return removed != null; + } + + public void purgeBlocksOlderThan(final long blockNumber) { + pendingBlocks + .values() + .stream() + .filter(b -> b.getHeader().getNumber() < blockNumber) + .forEach(this::deregisterPendingBlock); + } + + public boolean contains(final Hash blockHash) { + return pendingBlocks.containsKey(blockHash); + } + + public List childrenOf(final Hash parentBlock) { + final Set blocksByParent = pendingBlocksByParentHash.get(parentBlock); + if (blocksByParent == null || blocksByParent.size() == 0) { + return Collections.emptyList(); + } + return blocksByParent + .stream() + .map(pendingBlocks::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncState.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncState.java new file mode 100755 index 00000000000..284b2fbcf2e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncState.java @@ -0,0 +1,76 @@ +package net.consensys.pantheon.ethereum.eth.sync.state; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.SyncStatus; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Optional; + +public class SyncState { + private final Blockchain blockchain; + private final EthContext ethContext; + + private final long startingBlock; + private final PendingBlocks pendingBlocks; + private Optional syncTarget = Optional.empty(); + + public SyncState( + final Blockchain blockchain, final EthContext ethContext, final PendingBlocks pendingBlocks) { + this.blockchain = blockchain; + this.ethContext = ethContext; + this.startingBlock = chainHeadNumber(); + this.pendingBlocks = pendingBlocks; + } + + public SyncStatus syncStatus() { + return new SyncStatus(startingBlock(), chainHeadNumber(), bestChainHeight()); + } + + public long startingBlock() { + return startingBlock; + } + + public long chainHeadNumber() { + return blockchain.getChainHeadBlockNumber(); + } + + public UInt256 chainHeadTotalDifficulty() { + return blockchain.getChainHead().getTotalDifficulty(); + } + + public PendingBlocks pendingBlocks() { + return pendingBlocks; + } + + public Optional syncTarget() { + return syncTarget; + } + + public SyncTarget setSyncTarget(final EthPeer peer, final BlockHeader commonAncestor) { + final SyncTarget target = new SyncTarget(peer, commonAncestor); + this.syncTarget = Optional.of(target); + return target; + } + + public void clearSyncTarget() { + this.syncTarget = Optional.empty(); + } + + public long bestChainHeight() { + final long localChainHeight = blockchain.getChainHeadBlockNumber(); + return bestChainHeight(localChainHeight); + } + + public long bestChainHeight(final long localChainHeight) { + return Math.max( + localChainHeight, + ethContext + .getEthPeers() + .bestPeer() + .map(p -> p.chainState().getEstimatedHeight()) + .orElse(localChainHeight)); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncTarget.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncTarget.java new file mode 100755 index 00000000000..f5eed6a7034 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/state/SyncTarget.java @@ -0,0 +1,42 @@ +package net.consensys.pantheon.ethereum.eth.sync.state; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.ChainState; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; + +import com.google.common.base.MoreObjects; + +public class SyncTarget { + + private final EthPeer peer; + private BlockHeader commonAncestor; + + public SyncTarget(final EthPeer peer, final BlockHeader commonAncestor) { + this.peer = peer; + this.commonAncestor = commonAncestor; + } + + public EthPeer peer() { + return peer; + } + + public BlockHeader commonAncestor() { + return commonAncestor; + } + + public void setCommonAncestor(final BlockHeader commonAncestor) { + this.commonAncestor = commonAncestor; + } + + @Override + public String toString() { + final ChainState chainState = peer.chainState(); + return MoreObjects.toStringHelper(this) + .add( + "height", + (chainState.getEstimatedHeight() == 0 ? "?" : chainState.getEstimatedHeight())) + .add("td", chainState.getBestBlock().getTotalDifficulty()) + .add("peer", peer) + .toString(); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/AbstractGetHeadersFromPeerTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/AbstractGetHeadersFromPeerTask.java new file mode 100755 index 00000000000..c21ddb7c0af --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/AbstractGetHeadersFromPeerTask.java @@ -0,0 +1,106 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerRequestTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.messages.BlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Retrieves a sequence of headers from a peer. */ +public abstract class AbstractGetHeadersFromPeerTask + extends AbstractPeerRequestTask> { + + private static final Logger LOG = LogManager.getLogger(); + + private final ProtocolSchedule protocolSchedule; + protected final int count; + protected final int skip; + protected final boolean reverse; + private final long minimumRequiredBlockNumber; + + protected AbstractGetHeadersFromPeerTask( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final long minimumRequiredBlockNumber, + final int count, + final int skip, + final boolean reverse) { + super(ethContext, EthPV62.GET_BLOCK_HEADERS); + checkArgument(count > 0); + this.protocolSchedule = protocolSchedule; + this.count = count; + this.skip = skip; + this.reverse = reverse; + this.minimumRequiredBlockNumber = minimumRequiredBlockNumber; + } + + @Override + protected Optional> processResponse( + final boolean streamClosed, final MessageData message, final EthPeer peer) { + if (streamClosed) { + // All outstanding requests have been responded to and we still haven't found the response + // we wanted. It must have been empty or contain data that didn't match. + peer.recordUselessResponse(); + return Optional.of(Collections.emptyList()); + } + + final BlockHeadersMessage headersMessage = BlockHeadersMessage.readFrom(message); + try { + final Iterator headers = headersMessage.getHeaders(protocolSchedule); + if (!headers.hasNext()) { + // Message contains no data - nothing to do + return Optional.empty(); + } + + final BlockHeader firstHeader = headers.next(); + if (!matchesFirstHeader(firstHeader)) { + // This isn't our message - nothing to do + return Optional.empty(); + } + + final List headersList = new ArrayList<>(); + headersList.add(firstHeader); + long prevNumber = firstHeader.getNumber(); + + final int expectedDelta = reverse ? -(skip + 1) : (skip + 1); + while (headers.hasNext()) { + final BlockHeader header = headers.next(); + if (header.getNumber() != prevNumber + expectedDelta) { + // Skip doesn't match, this isn't our data + return Optional.empty(); + } + prevNumber = header.getNumber(); + headersList.add(header); + if (headersList.size() == count) { + break; + } + } + + LOG.info("Received {} of {} headers requested from peer.", headersList.size(), count); + return Optional.of(headersList); + } finally { + headersMessage.release(); + } + } + + @Override + protected Optional findSuitablePeer() { + return ethContext.getEthPeers().idlePeer(minimumRequiredBlockNumber); + } + + protected abstract boolean matchesFirstHeader(BlockHeader firstHeader); +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTask.java new file mode 100755 index 00000000000..80f9e4be04e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTask.java @@ -0,0 +1,126 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.AbstractRetryingPeerTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.NoAvailablePeersException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.PeerBreachedProtocolException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.PeerDisconnectedException; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Given a set of headers, "completes" them by repeatedly requesting additional data (bodies) needed + * to create the blocks that correspond to the supplied headers. + * + * @param the consensus algorithm context + */ +public class CompleteBlocksTask extends AbstractRetryingPeerTask> { + private static final Logger LOG = LogManager.getLogger(); + + private final EthContext ethContext; + private final ProtocolSchedule protocolSchedule; + + private final List headers; + private final Map blocks; + private Optional assignedPeer = Optional.empty(); + + private CompleteBlocksTask( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final List headers) { + super(ethContext); + checkArgument(headers.size() > 0, "Must supply a non-empty headers list"); + this.protocolSchedule = protocolSchedule; + this.ethContext = ethContext; + + this.headers = headers; + this.blocks = new HashMap<>(); + } + + public static CompleteBlocksTask forHeaders( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final List headers) { + return new CompleteBlocksTask<>(protocolSchedule, ethContext, headers); + } + + @Override + protected CompletableFuture executePeerTask() { + return requestBodies().thenCompose(this::processBodiesResult); + } + + @Override + protected boolean isRetryableError(final Throwable error) { + final boolean isPeerError = + error instanceof PeerBreachedProtocolException + || error instanceof PeerDisconnectedException + || error instanceof NoAvailablePeersException; + + return error instanceof TimeoutException || (!assignedPeer.isPresent() && isPeerError); + } + + public CompleteBlocksTask assignPeer(final EthPeer peer) { + assignedPeer = Optional.of(peer); + return this; + } + + private CompletableFuture>> requestBodies() { + final List incompleteHeaders = incompleteHeaders(); + LOG.info( + "Requesting bodies to complete {} blocks, starting with {}.", + incompleteHeaders.size(), + incompleteHeaders.get(0).getNumber()); + return executeSubTask( + () -> { + final GetBodiesFromPeerTask task = + GetBodiesFromPeerTask.forHeaders(protocolSchedule, ethContext, incompleteHeaders); + assignedPeer.ifPresent(task::assignPeer); + return task.run(); + }); + } + + private CompletableFuture processBodiesResult( + final PeerTaskResult> blocksResult) { + blocksResult + .getResult() + .forEach( + (block) -> { + blocks.put(block.getHeader().getNumber(), block); + }); + + final boolean done = incompleteHeaders().size() == 0; + if (done) { + result + .get() + .complete( + headers.stream().map(h -> blocks.get(h.getNumber())).collect(Collectors.toList())); + } + + final CompletableFuture future = new CompletableFuture<>(); + future.complete(null); + return future; + } + + private List incompleteHeaders() { + return headers + .stream() + .filter(h -> blocks.get(h.getNumber()) == null) + .collect(Collectors.toList()); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTask.java new file mode 100755 index 00000000000..06159ed231e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTask.java @@ -0,0 +1,139 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractEthTask; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.util.BlockchainUtil; + +import java.util.List; +import java.util.OptionalInt; +import java.util.concurrent.CompletableFuture; + +import com.google.common.annotations.VisibleForTesting; + +public class DetermineCommonAncestorTask extends AbstractEthTask { + private final EthContext ethContext; + private final ProtocolSchedule protocolSchedule; + private final ProtocolContext protocolContext; + private final EthPeer peer; + private final int headerRequestSize; + + private long maximumPossibleCommonAncestorNumber; + private long minimumPossibleCommonAncestorNumber; + private BlockHeader commonAncestorCandidate; + private boolean initialQuery = true; + + private DetermineCommonAncestorTask( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final EthPeer peer, + final int headerRequestSize) { + this.protocolSchedule = protocolSchedule; + this.ethContext = ethContext; + this.protocolContext = protocolContext; + this.peer = peer; + this.headerRequestSize = headerRequestSize; + + maximumPossibleCommonAncestorNumber = protocolContext.getBlockchain().getChainHeadBlockNumber(); + minimumPossibleCommonAncestorNumber = BlockHeader.GENESIS_BLOCK_NUMBER; + commonAncestorCandidate = + protocolContext.getBlockchain().getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get(); + } + + public static DetermineCommonAncestorTask create( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final EthPeer peer, + final int headerRequestSize) { + return new DetermineCommonAncestorTask<>( + protocolSchedule, protocolContext, ethContext, peer, headerRequestSize); + } + + @Override + protected void executeTask() { + if (maximumPossibleCommonAncestorNumber == minimumPossibleCommonAncestorNumber) { + // Bingo, we found our common ancestor. + result.get().complete(commonAncestorCandidate); + return; + } + if (maximumPossibleCommonAncestorNumber < BlockHeader.GENESIS_BLOCK_NUMBER + && !result.get().isDone()) { + result.get().completeExceptionally(new IllegalStateException("No common ancestor.")); + return; + } + requestHeaders() + .thenCompose(this::processHeaders) + .whenComplete( + (peerResult, error) -> { + if (error != null) { + result.get().completeExceptionally(error); + } else if (!result.get().isDone()) { + executeTask(); + } + }); + } + + @VisibleForTesting + CompletableFuture>> requestHeaders() { + final long range = maximumPossibleCommonAncestorNumber - minimumPossibleCommonAncestorNumber; + final int skipInterval = initialQuery ? 0 : calculateSkipInterval(range, headerRequestSize); + final int count = + initialQuery ? headerRequestSize : calculateCount((double) range, skipInterval); + + return executeSubTask( + () -> + GetHeadersFromPeerByNumberTask.endingAtNumber( + protocolSchedule, + ethContext, + maximumPossibleCommonAncestorNumber, + count, + skipInterval) + .assignPeer(peer) + .run()); + } + + /** + * In the case where the remote chain contains 100 blocks, the initial count work out to 11, and + * the skip interval would be 9. This would yield the headers (0, 10, 20, 30, 40, 50, 60, 70, 80, + * 90, 100). + */ + @VisibleForTesting + static int calculateSkipInterval(final long range, final int headerRequestSize) { + return Math.max(0, Math.toIntExact(range / (headerRequestSize - 1) - 1) - 1); + } + + @VisibleForTesting + static int calculateCount(final double range, final int skipInterval) { + return Math.toIntExact((long) Math.ceil(range / (skipInterval + 1)) + 1); + } + + private CompletableFuture processHeaders( + final AbstractPeerTask.PeerTaskResult> headersResult) { + initialQuery = false; + List headers = headersResult.getResult(); + + OptionalInt maybeAncestorNumber = + BlockchainUtil.findHighestKnownBlockIndex(protocolContext.getBlockchain(), headers, false); + + // Means the insertion point is in the next header request. + if (!maybeAncestorNumber.isPresent()) { + maximumPossibleCommonAncestorNumber = headers.get(headers.size() - 1).getNumber() - 1L; + return CompletableFuture.completedFuture(null); + } + int ancestorNumber = maybeAncestorNumber.getAsInt(); + commonAncestorCandidate = headers.get(ancestorNumber); + + if (ancestorNumber - 1 >= 0) { + maximumPossibleCommonAncestorNumber = headers.get(ancestorNumber - 1).getNumber() - 1L; + } + minimumPossibleCommonAncestorNumber = headers.get(ancestorNumber).getNumber(); + + return CompletableFuture.completedFuture(null); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java new file mode 100755 index 00000000000..522088967df --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTask.java @@ -0,0 +1,184 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static java.util.Arrays.asList; +import static net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode.DETACHED_ONLY; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.AbstractRetryingPeerTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.NoAvailablePeersException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.PeerBreachedProtocolException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.PeerDisconnectedException; +import net.consensys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; + +import com.google.common.primitives.Ints; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Retrieves a sequence of headers, sending out requests repeatedly until all headers are fulfilled. + * Validates headers as they are received. + * + * @param the consensus algorithm context + */ +public class DownloadHeaderSequenceTask extends AbstractRetryingPeerTask> { + private static final Logger LOG = LogManager.getLogger(); + + private final EthContext ethContext; + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + + private final BlockHeader[] headers; + private final BlockHeader referenceHeader; + private final int segmentLength; + private final long startingBlockNumber; + + private int lastFilledHeaderIndex; + + private DownloadHeaderSequenceTask( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final BlockHeader referenceHeader, + final int segmentLength) { + super(ethContext); + this.protocolSchedule = protocolSchedule; + this.protocolContext = protocolContext; + this.ethContext = ethContext; + this.referenceHeader = referenceHeader; + this.segmentLength = segmentLength; + + startingBlockNumber = referenceHeader.getNumber() - segmentLength; + headers = new BlockHeader[segmentLength]; + lastFilledHeaderIndex = segmentLength; + } + + public static DownloadHeaderSequenceTask endingAtHeader( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final BlockHeader referenceHeader, + final int segmentLength) { + return new DownloadHeaderSequenceTask<>( + protocolSchedule, protocolContext, ethContext, referenceHeader, segmentLength); + } + + @Override + protected CompletableFuture executePeerTask() { + LOG.info( + "Downloading headers from {} to {}.", startingBlockNumber, referenceHeader.getNumber() - 1); + final CompletableFuture> task = + downloadHeaders().thenCompose(this::processHeaders); + return task.whenComplete( + (r, t) -> { + // We're done if we've filled all requested headers + if (r != null && r.size() == segmentLength) { + LOG.info( + "Finished downloading headers from {} to {}.", + startingBlockNumber, + referenceHeader.getNumber() - 1); + result.get().complete(Arrays.asList(headers)); + } + }); + } + + @Override + protected boolean isRetryableError(final Throwable error) { + return error instanceof NoAvailablePeersException + || error instanceof TimeoutException + || error instanceof PeerBreachedProtocolException + || error instanceof PeerDisconnectedException; + } + + private CompletableFuture>> downloadHeaders() { + // Figure out parameters for our headers request + final boolean partiallyFilled = lastFilledHeaderIndex < segmentLength; + final BlockHeader referenceHeaderForNextRequest = + partiallyFilled ? headers[lastFilledHeaderIndex] : referenceHeader; + final Hash referenceHash = referenceHeaderForNextRequest.getHash(); + final int count = partiallyFilled ? lastFilledHeaderIndex : segmentLength; + + return executeSubTask( + () -> { + // Ask for count + 1 because we'll retrieve the previous header as well + final AbstractGetHeadersFromPeerTask headersTask = + GetHeadersFromPeerByHashTask.endingAtHash( + protocolSchedule, + ethContext, + referenceHash, + referenceHeaderForNextRequest.getNumber(), + count + 1); + return headersTask.run(); + }); + } + + private CompletableFuture> processHeaders( + final PeerTaskResult> headersResult) { + return executeWorkerSubTask( + ethContext.getScheduler(), + () -> { + final CompletableFuture> future = new CompletableFuture<>(); + BlockHeader child = null; + boolean firstSkipped = false; + for (final BlockHeader header : headersResult.getResult()) { + final int headerIndex = + Ints.checkedCast( + segmentLength - (referenceHeader.getNumber() - header.getNumber())); + if (!firstSkipped) { + // Skip over reference header + firstSkipped = true; + continue; + } + if (child == null) { + child = + (headerIndex == segmentLength - 1) ? referenceHeader : headers[headerIndex + 1]; + } + + if (!validateHeader(child, header)) { + // Invalid headers - disconnect from peer + LOG.info( + "Received invalid headers from peer, disconnecting from: {}", + headersResult.getPeer()); + headersResult.getPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + future.completeExceptionally( + new InvalidBlockException( + "Invalid header", header.getNumber(), header.getHash())); + return future; + } + headers[headerIndex] = header; + lastFilledHeaderIndex = headerIndex; + child = header; + } + future.complete(asList(headers).subList(lastFilledHeaderIndex, segmentLength)); + return future; + }); + } + + private boolean validateHeader(final BlockHeader child, final BlockHeader header) { + final long finalBlockNumber = startingBlockNumber + segmentLength; + final boolean blockInRange = + header.getNumber() >= startingBlockNumber && header.getNumber() < finalBlockNumber; + if (!blockInRange) { + return false; + } + if (child == null) { + return false; + } + + final ProtocolSpec protocolSpec = protocolSchedule.getByBlockNumber(child.getNumber()); + final BlockHeaderValidator blockHeaderValidator = protocolSpec.getBlockHeaderValidator(); + return blockHeaderValidator.validateHeader(child, header, protocolContext, DETACHED_ONLY); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTask.java new file mode 100755 index 00000000000..2772ba8f59a --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTask.java @@ -0,0 +1,83 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.IncompleteResultsException; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Downloads a block from a peer. Will complete exceptionally if block cannot be downloaded. */ +public class GetBlockFromPeerTask extends AbstractPeerTask { + private static final Logger LOG = LogManager.getLogger(); + + private final ProtocolSchedule protocolSchedule; + private final Hash hash; + + protected GetBlockFromPeerTask( + final ProtocolSchedule protocolSchedule, final EthContext ethContext, final Hash hash) { + super(ethContext); + this.protocolSchedule = protocolSchedule; + this.hash = hash; + } + + public static GetBlockFromPeerTask create( + final ProtocolSchedule protocolSchedule, final EthContext ethContext, final Hash hash) { + return new GetBlockFromPeerTask(protocolSchedule, ethContext, hash); + } + + @Override + protected void executeTaskWithPeer(final EthPeer peer) throws PeerNotConnected { + LOG.info("Downloading block {} from peer {}.", hash, peer); + downloadHeader(peer) + .thenCompose(this::completeBlock) + .whenComplete( + (r, t) -> { + if (t != null) { + LOG.info("Failed to download block {} from peer {}.", hash, peer); + result.get().completeExceptionally(t); + } else if (r.getResult().isEmpty()) { + LOG.info("Failed to download block {} from peer {}.", hash, peer); + result.get().completeExceptionally(new IncompleteResultsException()); + } else { + LOG.info("Successfully downloaded block {} from peer {}.", hash, peer); + result.get().complete(new PeerTaskResult<>(r.getPeer(), r.getResult().get(0))); + } + }); + } + + private CompletableFuture>> downloadHeader(final EthPeer peer) { + return executeSubTask( + () -> + GetHeadersFromPeerByHashTask.forSingleHash(protocolSchedule, ethContext, hash) + .assignPeer(peer) + .run()); + } + + private CompletableFuture>> completeBlock( + final PeerTaskResult> headerResult) { + if (headerResult.getResult().isEmpty()) { + final CompletableFuture>> future = new CompletableFuture<>(); + future.completeExceptionally(new IncompleteResultsException()); + return future; + } + + return executeSubTask( + () -> { + final GetBodiesFromPeerTask task = + GetBodiesFromPeerTask.forHeaders( + protocolSchedule, ethContext, headerResult.getResult()); + task.assignPeer(headerResult.getPeer()); + return task.run(); + }); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTask.java new file mode 100755 index 00000000000..ebe849f693c --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTask.java @@ -0,0 +1,161 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerRequestTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; +import net.consensys.pantheon.ethereum.eth.messages.BlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.mainnet.BodyValidation; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Requests bodies from a peer by header, matches up headers to bodies, and returns blocks. + * + * @param the consensus algorithm context + */ +public class GetBodiesFromPeerTask extends AbstractPeerRequestTask> { + private static final Logger LOG = LogManager.getLogger(); + + private final ProtocolSchedule protocolSchedule; + private final List headers; + private final Map> bodyToHeaders = new HashMap<>(); + + private GetBodiesFromPeerTask( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final List headers) { + super(ethContext, EthPV62.GET_BLOCK_BODIES); + checkArgument(headers.size() > 0); + this.protocolSchedule = protocolSchedule; + + this.headers = headers; + headers.forEach( + (header) -> { + final BodyIdentifier bodyId = new BodyIdentifier(header); + bodyToHeaders.putIfAbsent(bodyId, new ArrayList<>()); + bodyToHeaders.get(bodyId).add(header); + }); + } + + public static GetBodiesFromPeerTask forHeaders( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final List headers) { + return new GetBodiesFromPeerTask<>(protocolSchedule, ethContext, headers); + } + + @Override + protected ResponseStream sendRequest(final EthPeer peer) throws PeerNotConnected { + final List blockHashes = + headers.stream().map(BlockHeader::getHash).collect(Collectors.toList()); + LOG.info("Requesting {} bodies from peer {}.", blockHashes.size(), peer); + return peer.getBodies(blockHashes); + } + + @Override + protected Optional> processResponse( + final boolean streamClosed, final MessageData message, final EthPeer peer) { + if (streamClosed) { + // All outstanding requests have been responded to and we still haven't found the response + // we wanted. It must have been empty or contain data that didn't match. + peer.recordUselessResponse(); + return Optional.of(Collections.emptyList()); + } + + final BlockBodiesMessage bodiesMessage = BlockBodiesMessage.readFrom(message); + try { + final List bodies = Lists.newArrayList(bodiesMessage.bodies(protocolSchedule)); + if (bodies.size() == 0) { + // Message contains no data - nothing to do + return Optional.empty(); + } else if (bodies.size() > headers.size()) { + // Message doesn't match our request - nothing to do + return Optional.empty(); + } + + final List blocks = new ArrayList<>(); + for (final BlockBody body : bodies) { + final List headers = bodyToHeaders.get(new BodyIdentifier(body)); + if (headers == null) { + // This message contains unrelated bodies - exit + return Optional.empty(); + } + headers.forEach(h -> blocks.add(new Block(h, body))); + // Clear processed headers + headers.clear(); + } + return Optional.of(blocks); + } finally { + bodiesMessage.release(); + } + } + + @Override + protected Optional findSuitablePeer() { + return this.ethContext.getEthPeers().idlePeer(headers.get(headers.size() - 1).getNumber()); + } + + private static class BodyIdentifier { + private final Bytes32 transactionsRoot; + private final Bytes32 ommersHash; + + public BodyIdentifier(final Bytes32 transactionsRoot, final Bytes32 ommersHash) { + this.transactionsRoot = transactionsRoot; + this.ommersHash = ommersHash; + } + + public BodyIdentifier(final BlockBody body) { + this(body.getTransactions(), body.getOmmers()); + } + + public BodyIdentifier(final List transactions, final List ommers) { + this(BodyValidation.transactionsRoot(transactions), BodyValidation.ommersHash(ommers)); + } + + public BodyIdentifier(final BlockHeader header) { + this(header.getTransactionsRoot(), header.getOmmersHash()); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final BodyIdentifier that = (BodyIdentifier) o; + return Objects.equals(transactionsRoot, that.transactionsRoot) + && Objects.equals(ommersHash, that.ommersHash); + } + + @Override + public int hashCode() { + return Objects.hash(transactionsRoot, ommersHash); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTask.java new file mode 100755 index 00000000000..e6634149597 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTask.java @@ -0,0 +1,83 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Retrieves a sequence of headers from a peer. */ +public class GetHeadersFromPeerByHashTask extends AbstractGetHeadersFromPeerTask { + private static final Logger LOG = LogManager.getLogger(); + + private final Hash referenceHash; + + @VisibleForTesting + GetHeadersFromPeerByHashTask( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final Hash referenceHash, + final long minimumRequiredBlockNumber, + final int count, + final int skip, + final boolean reverse) { + super(protocolSchedule, ethContext, minimumRequiredBlockNumber, count, skip, reverse); + checkNotNull(referenceHash); + this.referenceHash = referenceHash; + } + + public static AbstractGetHeadersFromPeerTask startingAtHash( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final Hash firstHash, + final long firstBlockNumber, + final int segmentLength) { + return new GetHeadersFromPeerByHashTask( + protocolSchedule, ethContext, firstHash, firstBlockNumber, segmentLength, 0, false); + } + + public static AbstractGetHeadersFromPeerTask startingAtHash( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final Hash firstHash, + final long firstBlockNumber, + final int segmentLength, + final int skip) { + return new GetHeadersFromPeerByHashTask( + protocolSchedule, ethContext, firstHash, firstBlockNumber, segmentLength, skip, false); + } + + public static AbstractGetHeadersFromPeerTask endingAtHash( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final Hash lastHash, + final long lastBlockNumber, + final int segmentLength) { + return new GetHeadersFromPeerByHashTask( + protocolSchedule, ethContext, lastHash, lastBlockNumber, segmentLength, 0, true); + } + + public static AbstractGetHeadersFromPeerTask forSingleHash( + final ProtocolSchedule protocolSchedule, final EthContext ethContext, final Hash hash) { + return new GetHeadersFromPeerByHashTask(protocolSchedule, ethContext, hash, 0, 1, 0, false); + } + + @Override + protected ResponseStream sendRequest(final EthPeer peer) throws PeerNotConnected { + LOG.info("Requesting {} headers from peer {}.", count, peer); + return peer.getHeadersByHash(referenceHash, count, reverse, skip); + } + + @Override + protected boolean matchesFirstHeader(final BlockHeader firstHeader) { + return firstHeader.getHash().equals(referenceHash); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTask.java new file mode 100755 index 00000000000..e2081bd3d71 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTask.java @@ -0,0 +1,78 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Retrieves a sequence of headers from a peer. */ +public class GetHeadersFromPeerByNumberTask extends AbstractGetHeadersFromPeerTask { + private static final Logger LOG = LogManager.getLogger(); + + private final long blockNumber; + + @VisibleForTesting + GetHeadersFromPeerByNumberTask( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final long blockNumber, + final int count, + final int skip, + final boolean reverse) { + super(protocolSchedule, ethContext, blockNumber, count, skip, reverse); + this.blockNumber = blockNumber; + } + + public static AbstractGetHeadersFromPeerTask startingAtNumber( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final long firstBlockNumber, + final int segmentLength) { + return new GetHeadersFromPeerByNumberTask( + protocolSchedule, ethContext, firstBlockNumber, segmentLength, 0, false); + } + + public static AbstractGetHeadersFromPeerTask endingAtNumber( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final long lastlockNumber, + final int segmentLength) { + return new GetHeadersFromPeerByNumberTask( + protocolSchedule, ethContext, lastlockNumber, segmentLength, 0, true); + } + + public static AbstractGetHeadersFromPeerTask endingAtNumber( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final long lastlockNumber, + final int segmentLength, + final int skip) { + return new GetHeadersFromPeerByNumberTask( + protocolSchedule, ethContext, lastlockNumber, segmentLength, skip, true); + } + + public static AbstractGetHeadersFromPeerTask forSingleNumber( + final ProtocolSchedule protocolSchedule, + final EthContext ethContext, + final long blockNumber) { + return new GetHeadersFromPeerByNumberTask( + protocolSchedule, ethContext, blockNumber, 1, 0, false); + } + + @Override + protected ResponseStream sendRequest(final EthPeer peer) throws PeerNotConnected { + LOG.info("Requesting {} headers from peer {}.", count, peer); + return peer.getHeadersByNumber(blockNumber, count, reverse, skip); + } + + @Override + protected boolean matchesFirstHeader(final BlockHeader firstHeader) { + return firstHeader.getNumber() == blockNumber; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTask.java new file mode 100755 index 00000000000..81bbb5486d7 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTask.java @@ -0,0 +1,117 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Download and import blocks from a peer. + * + * @param the consensus algorithm context + */ +public class ImportBlocksTask extends AbstractPeerTask> { + private static final Logger LOG = LogManager.getLogger(); + + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final long startNumber; + + private final BlockHeader referenceHeader; + private final int maxBlocks; + private EthPeer peer; + + protected ImportBlocksTask( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final BlockHeader referenceHeader, + final int maxBlocks) { + super(ethContext); + this.protocolSchedule = protocolSchedule; + this.protocolContext = protocolContext; + this.referenceHeader = referenceHeader; + this.maxBlocks = maxBlocks; + + this.startNumber = referenceHeader.getNumber(); + } + + public static ImportBlocksTask fromHeader( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final BlockHeader previousHeader, + final int maxBlocks) { + return new ImportBlocksTask<>( + protocolSchedule, protocolContext, ethContext, previousHeader, maxBlocks); + } + + @Override + protected void executeTaskWithPeer(final EthPeer peer) throws PeerNotConnected { + this.peer = peer; + LOG.info("Importing blocks from {}", startNumber); + downloadHeaders() + .thenCompose(this::completeBlocks) + .thenCompose(this::importBlocks) + .whenComplete( + (r, t) -> { + if (t != null) { + LOG.info("Import from block {} failed: {}.", startNumber, t); + result.get().completeExceptionally(t); + } else { + LOG.info("Import from block {} succeeded.", startNumber); + result.get().complete(new PeerTaskResult<>(peer, r)); + } + }); + } + + private CompletableFuture>> downloadHeaders() { + final AbstractPeerTask> task = + GetHeadersFromPeerByHashTask.startingAtHash( + protocolSchedule, + ethContext, + referenceHeader.getHash(), + referenceHeader.getNumber(), + maxBlocks) + .assignPeer(peer); + return executeSubTask(task::run); + } + + private CompletableFuture> completeBlocks( + final PeerTaskResult> headers) { + if (headers.getResult().isEmpty()) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + final CompleteBlocksTask task = + CompleteBlocksTask.forHeaders(protocolSchedule, ethContext, headers.getResult()) + .assignPeer(peer); + return executeSubTask(() -> ethContext.getScheduler().timeout(task)); + } + + private CompletableFuture> importBlocks(final List blocks) { + // Don't import reference block if we already know about it + if (protocolContext.getBlockchain().contains(referenceHeader.getHash())) { + blocks.removeIf(b -> b.getHash().equals(referenceHeader.getHash())); + } + if (blocks.isEmpty()) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + final Supplier>> task = + PersistBlockTask.forSequentialBlocks( + protocolSchedule, protocolContext, blocks, HeaderValidationMode.FULL); + return executeWorkerSubTask(ethContext.getScheduler(), task); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTask.java new file mode 100755 index 00000000000..15da3f9ef93 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTask.java @@ -0,0 +1,163 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.eth.manager.AbstractEthTask; +import net.consensys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class PersistBlockTask extends AbstractEthTask { + + private final ProtocolSchedule protocolSchedule; + private final ProtocolContext protocolContext; + private final Block block; + private final HeaderValidationMode validateHeaders; + + private PersistBlockTask( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final Block block, + final HeaderValidationMode headerValidationMode) { + this.protocolSchedule = protocolSchedule; + this.protocolContext = protocolContext; + this.block = block; + this.validateHeaders = headerValidationMode; + } + + public static PersistBlockTask create( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final Block block, + final HeaderValidationMode headerValidationMode) { + return new PersistBlockTask<>(protocolSchedule, protocolContext, block, headerValidationMode); + } + + public static Supplier>> forSequentialBlocks( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final List blocks, + final HeaderValidationMode headerValidationMode) { + checkArgument(blocks.size() > 0); + return () -> { + final List successfulImports = new ArrayList<>(); + CompletableFuture future = null; + for (final Block block : blocks) { + if (future == null) { + future = + importBlockAndAddToList( + protocolSchedule, + protocolContext, + block, + successfulImports, + headerValidationMode); + continue; + } + future = + future.thenCompose( + b -> + importBlockAndAddToList( + protocolSchedule, + protocolContext, + block, + successfulImports, + headerValidationMode)); + } + return future.thenApply((r) -> successfulImports); + }; + } + + private static CompletableFuture importBlockAndAddToList( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final Block block, + final List list, + final HeaderValidationMode headerValidationMode) { + return PersistBlockTask.create(protocolSchedule, protocolContext, block, headerValidationMode) + .run() + .whenComplete( + (r, t) -> { + if (r != null) { + list.add(r); + } + }); + } + + public static Supplier>> forUnorderedBlocks( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final List blocks, + final HeaderValidationMode headerValidationMode) { + checkArgument(blocks.size() > 0); + return () -> { + final CompletableFuture> finalResult = new CompletableFuture<>(); + final List successfulImports = new ArrayList<>(); + CompletableFuture future = null; + for (final Block block : blocks) { + if (future == null) { + future = + PersistBlockTask.create( + protocolSchedule, protocolContext, block, headerValidationMode) + .run(); + continue; + } + future = + future + .handle((r, t) -> r) + .thenCompose( + (r) -> { + if (r != null) { + successfulImports.add(r); + } + return PersistBlockTask.create( + protocolSchedule, protocolContext, block, headerValidationMode) + .run(); + }); + } + future.whenComplete( + (r, t) -> { + if (r != null) { + successfulImports.add(r); + } + if (successfulImports.size() > 0) { + finalResult.complete(successfulImports); + } else { + finalResult.completeExceptionally(t); + } + }); + + return finalResult; + }; + } + + @Override + protected void executeTask() { + try { + final ProtocolSpec protocolSpec = + protocolSchedule.getByBlockNumber(block.getHeader().getNumber()); + final BlockImporter blockImporter = protocolSpec.getBlockImporter(); + final boolean blockImported = + blockImporter.importBlock(protocolContext, block, validateHeaders); + if (!blockImported) { + result + .get() + .completeExceptionally( + new InvalidBlockException( + "Failed to import block", block.getHeader().getNumber(), block.getHash())); + return; + } + result.get().complete(block); + } catch (final Exception e) { + result.get().completeExceptionally(e); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTask.java new file mode 100755 index 00000000000..8cc0138286e --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTask.java @@ -0,0 +1,303 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.eth.manager.AbstractEthTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.Supplier; + +import com.google.common.collect.Lists; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class PipelinedImportChainSegmentTask extends AbstractEthTask> { + private static final Logger LOG = LogManager.getLogger(); + + private final EthContext ethContext; + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final List importedBlocks = new ArrayList<>(); + + // First header is assumed to already be imported + private final List checkpointHeaders; + private final int chunksInTotal; + private int chunksIssued; + private int chunksCompleted; + private final int maxActiveChunks; + + private final Deque>> downloadAndValidateHeadersTasks = + new ConcurrentLinkedDeque<>(); + private final Deque>> downloadBodiesTasks = + new ConcurrentLinkedDeque<>(); + private final Deque>> extractTransactionSendersTasks = + new ConcurrentLinkedDeque<>(); + private final Deque>> validateAndImportBlocksTasks = + new ConcurrentLinkedDeque<>(); + + protected PipelinedImportChainSegmentTask( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final int maxActiveChunks, + final List checkpointHeaders) { + this.protocolSchedule = protocolSchedule; + this.protocolContext = protocolContext; + this.ethContext = ethContext; + this.checkpointHeaders = checkpointHeaders; + this.chunksInTotal = checkpointHeaders.size() - 1; + this.chunksIssued = 0; + this.chunksCompleted = 0; + this.maxActiveChunks = maxActiveChunks; + } + + public static PipelinedImportChainSegmentTask forCheckpoints( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final int maxActiveChunks, + final BlockHeader... checkpointHeaders) { + return forCheckpoints( + protocolSchedule, + protocolContext, + ethContext, + maxActiveChunks, + Arrays.asList(checkpointHeaders)); + } + + public static PipelinedImportChainSegmentTask forCheckpoints( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext, + final int maxActiveChunks, + final List checkpointHeaders) { + return new PipelinedImportChainSegmentTask<>( + protocolSchedule, protocolContext, ethContext, maxActiveChunks, checkpointHeaders); + } + + @Override + protected void executeTask() { + LOG.info( + "Importing chain segment from {} to {}.", + firstHeader().getNumber(), + lastHeader().getNumber()); + for (int i = 0; i < chunksInTotal && i < maxActiveChunks; i++) { + createNextChunkPipeline(); + } + } + + private void createNextChunkPipeline() { + final BlockHeader firstChunkHeader = checkpointHeaders.get(chunksIssued); + final BlockHeader lastChunkHeader = checkpointHeaders.get(chunksIssued + 1); + + final CompletableFuture> downloadAndValidateHeadersTask = + lastDownloadAndValidateHeadersTask() + .thenCompose((ignore) -> downloadNextHeaders(firstChunkHeader, lastChunkHeader)) + .thenCompose(this::validateHeaders); + final CompletableFuture> downloadBodiesTask = + downloadAndValidateHeadersTask + .thenCombine(lastDownloadBodiesTask(), (headers, ignored) -> headers) + .thenCompose(this::downloadBlocks); + final CompletableFuture> extractTransactionSendersTask = + downloadBodiesTask + .thenCombine(lastExtractTransactionSendersTasks(), (blocks, ignored) -> blocks) + .thenCompose(this::extractTransactionSenders); + final CompletableFuture> validateAndImportBlocksTask = + extractTransactionSendersTask + .thenCombine(lastValidateAndImportBlocksTasks(), (blocks, ignored) -> blocks) + .thenCompose(this::validateAndImportBlocks); + validateAndImportBlocksTask.whenComplete(this::completeChunkPipelineAndMaybeLaunchNextOne); + + downloadAndValidateHeadersTasks.addLast(downloadAndValidateHeadersTask); + downloadBodiesTasks.addLast(downloadBodiesTask); + extractTransactionSendersTasks.addLast(extractTransactionSendersTask); + validateAndImportBlocksTasks.addLast(validateAndImportBlocksTask); + chunksIssued++; + } + + public void completeChunkPipelineAndMaybeLaunchNextOne( + final List blocks, final Throwable throwable) { + if (throwable != null) { + LOG.info( + "Import of chain segment ({} to {}) failed: {}.", + firstHeader().getNumber(), + lastHeader().getNumber()); + LOG.error("Error", throwable); + result.get().completeExceptionally(throwable); + } else { + importedBlocks.addAll(blocks); + final BlockHeader firstHeader = blocks.get(0).getHeader(); + final BlockHeader lastHeader = blocks.get(blocks.size() - 1).getHeader(); + chunksCompleted++; + LOG.info( + "Import chain segment from {} to {} succeeded (chunk {}/{}).", + firstHeader.getNumber(), + lastHeader.getNumber(), + chunksCompleted, + chunksInTotal); + if (chunksCompleted == chunksInTotal) { + LOG.info( + "Completed importing chain segment {} to {}", + firstHeader().getNumber(), + lastHeader().getNumber()); + result.get().complete(importedBlocks); + } else { + downloadAndValidateHeadersTasks.removeFirst(); + downloadBodiesTasks.removeFirst(); + extractTransactionSendersTasks.removeFirst(); + validateAndImportBlocksTasks.removeFirst(); + if (chunksIssued < chunksInTotal) { + createNextChunkPipeline(); + } + } + } + } + + private CompletableFuture> downloadNextHeaders( + final BlockHeader firstChunkHeader, final BlockHeader lastChunkHeader) { + // Download the headers we're missing (between first and last) + LOG.info( + "Downloading headers {} to {}", + firstChunkHeader.getNumber() + 1, + lastChunkHeader.getNumber()); + final int segmentLength = + Math.toIntExact(lastChunkHeader.getNumber() - firstChunkHeader.getNumber() - 1); + if (segmentLength == 0) { + return CompletableFuture.completedFuture( + Lists.newArrayList(firstChunkHeader, lastChunkHeader)); + } + final DownloadHeaderSequenceTask task = + DownloadHeaderSequenceTask.endingAtHeader( + protocolSchedule, protocolContext, ethContext, lastChunkHeader, segmentLength); + return executeSubTask(task::run) + .thenApply( + headers -> { + final List finalHeaders = Lists.newArrayList(firstChunkHeader); + finalHeaders.addAll(headers); + finalHeaders.add(lastChunkHeader); + return finalHeaders; + }); + } + + private CompletableFuture> validateHeaders(final List headers) { + // First header needs to be validated + return executeWorkerSubTask( + ethContext.getScheduler(), + () -> { + final CompletableFuture> result = new CompletableFuture<>(); + final BlockHeader parentHeader = headers.get(0); + final BlockHeader childHeader = headers.get(1); + final ProtocolSpec protocolSpec = + protocolSchedule.getByBlockNumber(childHeader.getNumber()); + final BlockHeaderValidator blockHeaderValidator = + protocolSpec.getBlockHeaderValidator(); + if (blockHeaderValidator.validateHeader( + childHeader, parentHeader, protocolContext, HeaderValidationMode.DETACHED_ONLY)) { + // The first header will be imported by the previous request range. + result.complete(headers.subList(1, headers.size())); + } else { + result.completeExceptionally( + new InvalidBlockException( + "Provided first header does not connect to last header.", + parentHeader.getNumber(), + parentHeader.getHash())); + } + return result; + }); + } + + private CompletableFuture> downloadBlocks(final List headers) { + LOG.info( + "Downloading bodies {} to {}", + headers.get(0).getNumber(), + headers.get(headers.size() - 1).getNumber()); + final CompleteBlocksTask task = + CompleteBlocksTask.forHeaders(protocolSchedule, ethContext, headers); + return executeSubTask(task::run); + } + + private CompletableFuture> validateAndImportBlocks(final List blocks) { + LOG.info( + "Validating and importing {} to {}", + blocks.get(0).getHeader().getNumber(), + blocks.get(blocks.size() - 1).getHeader().getNumber()); + final Supplier>> task = + PersistBlockTask.forSequentialBlocks( + protocolSchedule, protocolContext, blocks, HeaderValidationMode.SKIP_DETACHED); + return executeWorkerSubTask(ethContext.getScheduler(), task); + } + + private CompletableFuture> extractTransactionSenders(final List blocks) { + LOG.info( + "Extracting sender {} to {}", + blocks.get(0).getHeader().getNumber(), + blocks.get(blocks.size() - 1).getHeader().getNumber()); + return executeWorkerSubTask( + ethContext.getScheduler(), + () -> { + final CompletableFuture> result = new CompletableFuture<>(); + for (final Block block : blocks) { + for (final Transaction transaction : block.getBody().getTransactions()) { + // This method internally performs the transaction sender extraction. + transaction.getSender(); + } + } + result.complete(blocks); + return result; + }); + } + + private BlockHeader firstHeader() { + return checkpointHeaders.get(0); + } + + private BlockHeader lastHeader() { + return checkpointHeaders.get(checkpointHeaders.size() - 1); + } + + private CompletableFuture> lastDownloadAndValidateHeadersTask() { + if (downloadAndValidateHeadersTasks.isEmpty()) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } else { + return downloadAndValidateHeadersTasks.getLast(); + } + } + + private CompletableFuture> lastDownloadBodiesTask() { + if (downloadBodiesTasks.isEmpty()) { + return CompletableFuture.completedFuture(Lists.newArrayList()); + } else { + return downloadBodiesTasks.getLast(); + } + } + + private CompletableFuture> lastValidateAndImportBlocksTasks() { + if (validateAndImportBlocksTasks.isEmpty()) { + return CompletableFuture.completedFuture(Lists.newArrayList()); + } else { + return validateAndImportBlocksTasks.getLast(); + } + } + + private CompletableFuture> lastExtractTransactionSendersTasks() { + if (extractTransactionSendersTasks.isEmpty()) { + return CompletableFuture.completedFuture(Lists.newArrayList()); + } else { + return extractTransactionSendersTasks.getLast(); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTask.java new file mode 100755 index 00000000000..0d34b088ff8 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTask.java @@ -0,0 +1,50 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.eth.manager.AbstractEthTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeers; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Wait for a single new peer to connect. */ +public class WaitForPeerTask extends AbstractEthTask { + private static final Logger LOG = LogManager.getLogger(); + + private final EthContext ethContext; + private volatile Long peerListenerId; + + private WaitForPeerTask(final EthContext ethContext) { + this.ethContext = ethContext; + } + + public static WaitForPeerTask create(final EthContext ethContext) { + return new WaitForPeerTask(ethContext); + } + + @Override + protected void executeTask() { + final EthPeers ethPeers = ethContext.getEthPeers(); + LOG.info( + "Waiting for new peer connection. {} peers currently connected, {} idle.", + ethPeers.peerCount(), + ethPeers.idlePeer().isPresent() ? "Some peers" : "No peers"); + // Listen for peer connections and complete task when we hit our target + peerListenerId = + ethPeers.subscribeConnect( + (peer) -> { + LOG.info("Finished waiting for peer connection."); + // We hit our target + result.get().complete(null); + }); + } + + @Override + protected void cleanup() { + super.cleanup(); + final Long listenerId = peerListenerId; + if (listenerId != null) { + ethContext.getEthPeers().unsubscribeConnect(listenerId); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTask.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTask.java new file mode 100755 index 00000000000..6f6c5bf14a5 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTask.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.eth.manager.AbstractEthTask; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeers; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Waits for some number of peers to connect. */ +public class WaitForPeersTask extends AbstractEthTask { + private static final Logger LOG = LogManager.getLogger(); + + private final int targetPeerCount; + private final EthContext ethContext; + private volatile Long peerListenerId; + + private WaitForPeersTask(final EthContext ethContext, final int targetPeerCount) { + this.targetPeerCount = targetPeerCount; + this.ethContext = ethContext; + } + + public static WaitForPeersTask create(final EthContext ethContext, final int targetPeerCount) { + return new WaitForPeersTask(ethContext, targetPeerCount); + } + + @Override + protected void executeTask() { + final EthPeers ethPeers = ethContext.getEthPeers(); + if (ethPeers.peerCount() >= targetPeerCount) { + // We already hit our target + result.get().complete(null); + return; + } + + LOG.info("Waiting for {} peers to connect.", targetPeerCount); + // Listen for peer connections and complete task when we hit our target + peerListenerId = + ethPeers.subscribeConnect( + (peer) -> { + final int peerCount = ethPeers.peerCount(); + if (peerCount >= targetPeerCount) { + LOG.info("Finished waiting for peers to connect.", targetPeerCount); + // We hit our target + result.get().complete(null); + } else { + LOG.info("Waiting for {} peers to connect.", targetPeerCount - peerCount); + } + }); + } + + @Override + protected void cleanup() { + super.cleanup(); + final Long listenerId = peerListenerId; + if (listenerId != null) { + ethContext.getEthPeers().unsubscribeConnect(peerListenerId); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java new file mode 100755 index 00000000000..8c1326ad4d0 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/sync/tasks/exceptions/InvalidBlockException.java @@ -0,0 +1,10 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks.exceptions; + +import net.consensys.pantheon.ethereum.core.Hash; + +public class InvalidBlockException extends RuntimeException { + + public InvalidBlockException(final String message, final long blockNumber, final Hash blockHash) { + super(message + ": " + blockNumber + ", " + blockHash); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTracker.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTracker.java new file mode 100755 index 00000000000..d38943a9a1b --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTracker.java @@ -0,0 +1,72 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static java.util.Collections.emptySet; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer.DisconnectCallback; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +class PeerTransactionTracker implements DisconnectCallback { + private static final int MAX_TRACKED_SEEN_TRANSACTIONS = 30_000; + private final Map> seenTransactions = new ConcurrentHashMap<>(); + private final Map> transactionsToSend = new ConcurrentHashMap<>(); + + public synchronized void markTransactionsAsSeen( + final EthPeer peer, final Collection transactions) { + final Set seenTransactionsForPeer = getOrCreateSeenTransactionsForPeer(peer); + transactions.stream().map(Transaction::hash).forEach(seenTransactionsForPeer::add); + } + + public synchronized void addToPeerSendQueue(final EthPeer peer, final Transaction transaction) { + if (!hasPeerSeenTransaction(peer, transaction)) { + transactionsToSend.computeIfAbsent(peer, key -> createTransactionsSet()).add(transaction); + } + } + + public Iterable getEthPeersWithUnsentTransactions() { + return transactionsToSend.keySet(); + } + + public synchronized Set claimTransactionsToSendToPeer(final EthPeer peer) { + final Set transactionsToSend = this.transactionsToSend.remove(peer); + if (transactionsToSend != null) { + markTransactionsAsSeen(peer, transactionsToSend); + return transactionsToSend; + } else { + return emptySet(); + } + } + + private Set getOrCreateSeenTransactionsForPeer(final EthPeer peer) { + return seenTransactions.computeIfAbsent(peer, key -> createTransactionsSet()); + } + + private boolean hasPeerSeenTransaction(final EthPeer peer, final Transaction transaction) { + final Set seenTransactionsForPeer = seenTransactions.get(peer); + return seenTransactionsForPeer != null && seenTransactionsForPeer.contains(transaction.hash()); + } + + private Set createTransactionsSet() { + return Collections.newSetFromMap( + new LinkedHashMap(1 << 4, 0.75f, true) { + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > MAX_TRACKED_SEEN_TRANSACTIONS; + } + }); + } + + @Override + public void onDisconnect(final EthPeer peer) { + seenTransactions.remove(peer); + transactionsToSend.remove(peer); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolFactory.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolFactory.java new file mode 100755 index 00000000000..25faa5ec136 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolFactory.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +public class TransactionPoolFactory { + + public static TransactionPool createTransactionPool( + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext, + final EthContext ethContext) { + final PendingTransactions pendingTransactions = + new PendingTransactions(PendingTransactions.MAX_PENDING_TRANSACTIONS); + + final PeerTransactionTracker transactionTracker = new PeerTransactionTracker(); + final TransactionsMessageSender transactionsMessageSender = + new TransactionsMessageSender(transactionTracker); + + final TransactionPool transactionPool = + new TransactionPool( + pendingTransactions, + protocolSchedule, + protocolContext, + new TransactionSender(transactionTracker, transactionsMessageSender, ethContext)); + + final TransactionsMessageHandler transactionsMessageHandler = + new TransactionsMessageHandler( + ethContext.getScheduler(), + new TransactionsMessageProcessor(transactionTracker, transactionPool)); + + ethContext.getEthMessages().subscribe(EthPV62.TRANSACTIONS, transactionsMessageHandler); + protocolContext.getBlockchain().observeBlockAdded(transactionPool); + ethContext.getEthPeers().subscribeDisconnect(transactionTracker); + return transactionPool; + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionSender.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionSender.java new file mode 100755 index 00000000000..35c5dee49c0 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionSender.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool.TransactionBatchAddedListener; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; + +class TransactionSender implements TransactionBatchAddedListener { + + private final PeerTransactionTracker transactionTracker; + private final TransactionsMessageSender transactionsMessageSender; + private final EthContext ethContext; + + public TransactionSender( + final PeerTransactionTracker transactionTracker, + final TransactionsMessageSender transactionsMessageSender, + final EthContext ethContext) { + this.transactionTracker = transactionTracker; + this.transactionsMessageSender = transactionsMessageSender; + this.ethContext = ethContext; + } + + @Override + public void onTransactionsAdded(final Iterable transactions) { + ethContext + .getEthPeers() + .availablePeers() + .forEach( + peer -> + transactions.forEach( + transaction -> transactionTracker.addToPeerSendQueue(peer, transaction))); + ethContext + .getScheduler() + .scheduleWorkerTask(transactionsMessageSender::sendTransactionsToPeers); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageHandler.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageHandler.java new file mode 100755 index 00000000000..dc7d820f27b --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageHandler.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import net.consensys.pantheon.ethereum.eth.manager.EthMessage; +import net.consensys.pantheon.ethereum.eth.manager.EthMessages.MessageCallback; +import net.consensys.pantheon.ethereum.eth.manager.EthScheduler; +import net.consensys.pantheon.ethereum.eth.messages.TransactionsMessage; + +class TransactionsMessageHandler implements MessageCallback { + + private final TransactionsMessageProcessor transactionsMessageProcessor; + private final EthScheduler scheduler; + + public TransactionsMessageHandler( + final EthScheduler scheduler, + final TransactionsMessageProcessor transactionsMessageProcessor) { + this.scheduler = scheduler; + this.transactionsMessageProcessor = transactionsMessageProcessor; + } + + @Override + public void exec(final EthMessage message) { + final TransactionsMessage transactionsMessage = TransactionsMessage.readFrom(message.getData()); + scheduler.scheduleWorkerTask( + () -> + transactionsMessageProcessor.processTransactionsMessage( + message.getPeer(), transactionsMessage)); + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessor.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessor.java new file mode 100755 index 00000000000..b7bef55faf8 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessor.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.messages.TransactionsMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.rlp.RLPException; + +import java.util.Iterator; +import java.util.Set; + +import com.google.common.collect.Sets; +import org.apache.logging.log4j.Logger; + +class TransactionsMessageProcessor { + + private static final Logger LOG = getLogger(); + private final PeerTransactionTracker transactionTracker; + private final TransactionPool transactionPool; + + public TransactionsMessageProcessor( + final PeerTransactionTracker transactionTracker, final TransactionPool transactionPool) { + this.transactionTracker = transactionTracker; + this.transactionPool = transactionPool; + } + + void processTransactionsMessage( + final EthPeer peer, final TransactionsMessage transactionsMessage) { + try { + LOG.debug("Received transactions message from {}", peer); + + final Iterator readTransactions = + transactionsMessage.transactions(Transaction::readFrom); + final Set transactions = Sets.newHashSet(readTransactions); + transactionTracker.markTransactionsAsSeen(peer, transactions); + transactionPool.addRemoteTransactions(transactions); + } catch (final RLPException ex) { + if (peer != null) { + peer.disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + } + } finally { + transactionsMessage.release(); + } + } +} diff --git a/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSender.java b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSender.java new file mode 100755 index 00000000000..c528fe9dab0 --- /dev/null +++ b/ethereum/eth/src/main/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSender.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static java.util.stream.Collectors.toSet; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.messages.TransactionsMessage; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; + +import java.util.Set; + +class TransactionsMessageSender { + + private static final int MAX_BATCH_SIZE = 10; + private final PeerTransactionTracker transactionTracker; + + public TransactionsMessageSender(final PeerTransactionTracker transactionTracker) { + this.transactionTracker = transactionTracker; + } + + public void sendTransactionsToPeers() { + transactionTracker.getEthPeersWithUnsentTransactions().forEach(this::sendTransactionsToPeer); + } + + private void sendTransactionsToPeer(final EthPeer peer) { + final Set allTxToSend = transactionTracker.claimTransactionsToSendToPeer(peer); + while (!allTxToSend.isEmpty()) { + final Set subsetToSend = + allTxToSend.stream().limit(MAX_BATCH_SIZE).collect(toSet()); + allTxToSend.removeAll(subsetToSend); + try { + peer.send(TransactionsMessage.create(subsetToSend)); + } catch (final PeerNotConnected e) { + return; + } + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTaskTest.java new file mode 100755 index 00000000000..f1da55ee836 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/AbstractEthTaskTest.java @@ -0,0 +1,92 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class AbstractEthTaskTest { + + @Test + public void shouldCancelAllIncompleteSubtasksWhenMultipleIncomplete() { + final CompletableFuture subtask1 = new CompletableFuture<>(); + final CompletableFuture subtask2 = new CompletableFuture<>(); + final EthTaskWithMultipleSubtasks task = + new EthTaskWithMultipleSubtasks(Lists.newArrayList(subtask1, subtask2)); + + task.run(); + task.cancel(); + + assertThat(subtask1.isCancelled()).isTrue(); + assertThat(subtask2.isCancelled()).isTrue(); + } + + @Test + public void shouldAnyCancelIncompleteSubtasksWhenMultiple() { + final CompletableFuture subtask1 = new CompletableFuture<>(); + final CompletableFuture subtask2 = new CompletableFuture<>(); + final CompletableFuture subtask3 = new CompletableFuture<>(); + final EthTaskWithMultipleSubtasks task = + new EthTaskWithMultipleSubtasks(Lists.newArrayList(subtask1, subtask2, subtask3)); + + task.run(); + subtask1.complete(null); + task.cancel(); + + assertThat(subtask1.isCancelled()).isFalse(); + assertThat(subtask2.isCancelled()).isTrue(); + assertThat(subtask3.isCancelled()).isTrue(); + assertThat(task.isCancelled()).isTrue(); + } + + @Test + public void shouldCompleteWhenCancelNotCalled() { + final AtomicBoolean done = new AtomicBoolean(false); + final CompletableFuture subtask1 = new CompletableFuture<>(); + final CompletableFuture subtask2 = new CompletableFuture<>(); + final EthTaskWithMultipleSubtasks task = + new EthTaskWithMultipleSubtasks(Lists.newArrayList(subtask1, subtask2)); + + final CompletableFuture future = task.run(); + subtask1.complete(null); + subtask2.complete(null); + task.cancel(); + + future.whenComplete( + (result, error) -> { + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + } + + private class EthTaskWithMultipleSubtasks extends AbstractEthTask { + + private final List> subtasks; + + private EthTaskWithMultipleSubtasks(final List> subtasks) { + this.subtasks = subtasks; + } + + @Override + protected void executeTask() { + final List> completedSubTasks = Lists.newArrayList(); + for (final CompletableFuture subtask : subtasks) { + final CompletableFuture completedSubTask = executeSubTask(() -> subtask); + completedSubTasks.add(completedSubTask); + } + + final CompletableFuture executedAllSubtasks = + CompletableFuture.allOf( + completedSubTasks.toArray(new CompletableFuture[completedSubTasks.size()])); + executedAllSubtasks.whenComplete( + (r, t) -> { + result.get().complete(null); + }); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ChainStateTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ChainStateTest.java new file mode 100755 index 00000000000..3cce2b7855e --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ChainStateTest.java @@ -0,0 +1,219 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.util.uint.UInt256; + +import org.junit.Test; + +public class ChainStateTest { + + private static final UInt256 INITIAL_TOTAL_DIFFICULTY = UInt256.of(256); + private final ChainState chainState = new ChainState(); + + @Test + public void statusReceivedUpdatesBestBlock() { + final BlockHeader bestBlockHeader = new BlockHeaderTestFixture().number(12).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void updateBestBlockAndHeightFromHashAndHeight() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + chainState.update(bestBlockHeader.getHash(), blockNumber); + assertThat(chainState.getEstimatedHeight()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void updateHeightFromHashAndHeight() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + chainState.update(gen.hash(), blockNumber); + assertThat(chainState.getEstimatedHeight()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void doesNotUpdateFromOldHashAndHeight() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + chainState.update(gen.hash(), blockNumber); + chainState.update(gen.hash(), blockNumber - 1); + assertThat(chainState.getEstimatedHeight()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void updateBestBlockAndHeightFromHeader() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + chainState.update(bestBlockHeader); + assertThat(chainState.getEstimatedHeight()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void updateHeightFromHeader() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + final long newHeaderNumber = blockNumber + 1; + chainState.update(new BlockHeaderTestFixture().number(newHeaderNumber).buildHeader()); + assertThat(chainState.getEstimatedHeight()).isEqualTo(newHeaderNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void doesNotUpdateFromOldHeader() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + chainState.update(bestBlockHeader); + chainState.update(new BlockHeaderTestFixture().number(blockNumber - 5).buildHeader()); + assertThat(chainState.getEstimatedHeight()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void updateBestBlockAndHeightFromBestBlockHeaderAndTd() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + chainState.update(bestBlockHeader, INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void updateBestBlockAndHeightFromBetterBlockHeaderAndTd() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + final long betterBlockNumber = blockNumber + 2; + final UInt256 betterTd = INITIAL_TOTAL_DIFFICULTY.plus(100L); + final BlockHeader betterBlock = + new BlockHeaderTestFixture().number(betterBlockNumber).buildHeader(); + chainState.update(betterBlock, betterTd); + + assertThat(chainState.getEstimatedHeight()).isEqualTo(betterBlockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(betterBlockNumber); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(betterBlock.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(betterTd); + } + + @Test + public void updateHeightFromBlockHeaderAndTd() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + final long otherBlockNumber = blockNumber + 2; + final UInt256 otherTd = INITIAL_TOTAL_DIFFICULTY.minus(100L); + final BlockHeader otherBlock = + new BlockHeaderTestFixture().number(otherBlockNumber).buildHeader(); + chainState.update(otherBlock, otherTd); + + assertThat(chainState.getEstimatedHeight()).isEqualTo(otherBlockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void doNotUpdateFromOldBlockHeaderAndTd() { + final long blockNumber = 12; + final BlockHeader bestBlockHeader = + new BlockHeaderTestFixture().number(blockNumber).buildHeader(); + chainState.statusReceived(bestBlockHeader.getHash(), INITIAL_TOTAL_DIFFICULTY); + assertThat(chainState.getEstimatedHeight()).isEqualTo(0L); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(0L); + + chainState.update(bestBlockHeader, INITIAL_TOTAL_DIFFICULTY); + + final long otherBlockNumber = blockNumber - 2; + final UInt256 otherTd = INITIAL_TOTAL_DIFFICULTY.minus(100L); + final BlockHeader otherBlock = + new BlockHeaderTestFixture().number(otherBlockNumber).buildHeader(); + chainState.update(otherBlock, otherTd); + + assertThat(chainState.getEstimatedHeight()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getNumber()).isEqualTo(blockNumber); + assertThat(chainState.getBestBlock().getHash()).isEqualTo(bestBlockHeader.getHash()); + assertThat(chainState.getBestBlock().getTotalDifficulty()).isEqualTo(INITIAL_TOTAL_DIFFICULTY); + } + + @Test + public void shouldOnlyHaveHeightEstimateWhenHeightHasBeenSet() { + chainState.statusReceived(Hash.EMPTY_LIST_HASH, UInt256.ONE); + assertThat(chainState.hasEstimatedHeight()).isFalse(); + + chainState.update(new BlockHeaderTestFixture().number(12).buildHeader()); + + assertThat(chainState.hasEstimatedHeight()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/DeterministicEthScheduler.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/DeterministicEthScheduler.java new file mode 100755 index 00000000000..e53ac7bb64f --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/DeterministicEthScheduler.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** Schedules tasks that run immediately and synchronously for testing. */ +public class DeterministicEthScheduler extends EthScheduler { + + private final TimeoutPolicy timeoutPolicy; + + DeterministicEthScheduler() { + this(() -> false); + } + + DeterministicEthScheduler(final TimeoutPolicy timeoutPolicy) { + super(new MockExecutorService(), new MockScheduledExecutor()); + this.timeoutPolicy = timeoutPolicy; + } + + MockExecutorService mockWorkerExecutor() { + return (MockExecutorService) workerExecutor; + } + + MockScheduledExecutor mockScheduledExecutor() { + return (MockScheduledExecutor) scheduler; + } + + @Override + public void failAfterTimeout(final CompletableFuture promise, final Duration timeout) { + if (timeoutPolicy.shouldTimeout()) { + final TimeoutException timeoutException = + new TimeoutException( + "Mocked timeout after " + timeout.toMillis() + " " + TimeUnit.MILLISECONDS); + promise.completeExceptionally(timeoutException); + } + } + + @FunctionalInterface + public interface TimeoutPolicy { + boolean shouldTimeout(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeerTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeerTest.java new file mode 100755 index 00000000000..7d76468f898 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeerTest.java @@ -0,0 +1,266 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseCallback; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; +import net.consensys.pantheon.ethereum.eth.messages.BlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.BlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.ReceiptsMessage; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.junit.Test; + +public class EthPeerTest { + private static final BlockDataGenerator gen = new BlockDataGenerator(); + + @Test + public void getHeadersStream() throws PeerNotConnected { + final ResponseStreamSupplier getStream = + (peer) -> peer.getHeadersByHash(gen.hash(), 5, false, 0); + final MessageData targetMessage = + BlockHeadersMessage.create(Arrays.asList(gen.header(), gen.header())); + final MessageData otherMessage = + BlockBodiesMessage.create(Arrays.asList(gen.body(), gen.body())); + + messageStream(getStream, targetMessage, otherMessage); + } + + @Test + public void getBodiesStream() throws PeerNotConnected { + final ResponseStreamSupplier getStream = + (peer) -> peer.getBodies(Arrays.asList(gen.hash(), gen.hash())); + final MessageData targetMessage = + BlockBodiesMessage.create(Arrays.asList(gen.body(), gen.body())); + final MessageData otherMessage = + BlockHeadersMessage.create(Arrays.asList(gen.header(), gen.header())); + + messageStream(getStream, targetMessage, otherMessage); + } + + @Test + public void getReceiptsStream() throws PeerNotConnected { + final ResponseStreamSupplier getStream = + (peer) -> peer.getReceipts(Arrays.asList(gen.hash(), gen.hash())); + final MessageData targetMessage = + ReceiptsMessage.create(Collections.singletonList(gen.receipts(gen.block()))); + final MessageData otherMessage = + BlockHeadersMessage.create(Arrays.asList(gen.header(), gen.header())); + + messageStream(getStream, targetMessage, otherMessage); + } + + @Test + public void closeStreamsOnPeerDisconnect() throws PeerNotConnected { + final EthPeer peer = createPeer(); + // Setup headers stream + final AtomicInteger headersClosedCount = new AtomicInteger(0); + peer.getHeadersByHash(gen.hash(), 5, false, 0) + .then( + (closed, msg, p) -> { + if (closed) { + headersClosedCount.incrementAndGet(); + } + }); + // Bodies stream + final AtomicInteger bodiesClosedCount = new AtomicInteger(0); + peer.getBodies(Arrays.asList(gen.hash(), gen.hash())) + .then( + (closed, msg, p) -> { + if (closed) { + bodiesClosedCount.incrementAndGet(); + } + }); + // Receipts stream + final AtomicInteger receiptsClosedCount = new AtomicInteger(0); + peer.getReceipts(Arrays.asList(gen.hash(), gen.hash())) + .then( + (closed, msg, p) -> { + if (closed) { + receiptsClosedCount.incrementAndGet(); + } + }); + + // Sanity check + assertThat(headersClosedCount.get()).isEqualTo(0); + assertThat(bodiesClosedCount.get()).isEqualTo(0); + assertThat(receiptsClosedCount.get()).isEqualTo(0); + + // Disconnect and check + peer.handleDisconnect(); + assertThat(headersClosedCount.get()).isEqualTo(1); + assertThat(bodiesClosedCount.get()).isEqualTo(1); + assertThat(receiptsClosedCount.get()).isEqualTo(1); + } + + @Test + public void listenForMultipleStreams() throws PeerNotConnected { + // Setup peer and messages + final EthPeer peer = createPeer(); + final EthMessage headersMessage = + new EthMessage(peer, BlockHeadersMessage.create(Arrays.asList(gen.header(), gen.header()))); + final EthMessage bodiesMessage = + new EthMessage(peer, BlockBodiesMessage.create(Arrays.asList(gen.body(), gen.body()))); + final EthMessage otherMessage = + new EthMessage( + peer, ReceiptsMessage.create(Collections.singletonList(gen.receipts(gen.block())))); + + // Set up stream for headers + final AtomicInteger headersMessageCount = new AtomicInteger(0); + final AtomicInteger headersClosedCount = new AtomicInteger(0); + final ResponseStream headersStream = + peer.getHeadersByHash(gen.hash(), 5, false, 0) + .then( + (closed, msg, p) -> { + if (closed) { + headersClosedCount.incrementAndGet(); + } else { + headersMessageCount.incrementAndGet(); + assertThat(msg.getCode()).isEqualTo(headersMessage.getData().getCode()); + } + }); + // Set up stream for bodies + final AtomicInteger bodiesMessageCount = new AtomicInteger(0); + final AtomicInteger bodiesClosedCount = new AtomicInteger(0); + final ResponseStream bodiesStream = + peer.getBodies(Arrays.asList(gen.hash(), gen.hash())) + .then( + (closed, msg, p) -> { + if (closed) { + bodiesClosedCount.incrementAndGet(); + } else { + bodiesMessageCount.incrementAndGet(); + assertThat(msg.getCode()).isEqualTo(bodiesMessage.getData().getCode()); + } + }); + + // Dispatch some messages and check expectations + peer.dispatch(headersMessage); + assertThat(headersMessageCount.get()).isEqualTo(1); + assertThat(headersClosedCount.get()).isEqualTo(1); + assertThat(bodiesMessageCount.get()).isEqualTo(0); + assertThat(bodiesClosedCount.get()).isEqualTo(0); + + peer.dispatch(bodiesMessage); + assertThat(headersMessageCount.get()).isEqualTo(1); + assertThat(headersClosedCount.get()).isEqualTo(1); + assertThat(bodiesMessageCount.get()).isEqualTo(1); + assertThat(bodiesClosedCount.get()).isEqualTo(1); + + peer.dispatch(otherMessage); + assertThat(headersMessageCount.get()).isEqualTo(1); + assertThat(headersClosedCount.get()).isEqualTo(1); + assertThat(bodiesMessageCount.get()).isEqualTo(1); + assertThat(bodiesClosedCount.get()).isEqualTo(1); + + // Dispatch again after close and check that nothing fires + peer.dispatch(headersMessage); + peer.dispatch(bodiesMessage); + peer.dispatch(otherMessage); + assertThat(headersMessageCount.get()).isEqualTo(1); + assertThat(headersClosedCount.get()).isEqualTo(1); + assertThat(bodiesMessageCount.get()).isEqualTo(1); + assertThat(bodiesClosedCount.get()).isEqualTo(1); + } + + private void messageStream( + final ResponseStreamSupplier getStream, + final MessageData targetMessage, + final MessageData otherMessage) + throws PeerNotConnected { + // Setup peer and ask for stream + final EthPeer peer = createPeer(); + final AtomicInteger messageCount = new AtomicInteger(0); + final AtomicInteger closedCount = new AtomicInteger(0); + final int targetCode = targetMessage.getCode(); + final ResponseCallback responseHandler = + (closed, msg, p) -> { + if (closed) { + closedCount.incrementAndGet(); + } else { + messageCount.incrementAndGet(); + assertThat(msg.getCode()).isEqualTo(targetCode); + } + }; + + // Set up 1 stream + getStream.get(peer).then(responseHandler); + + final EthMessage targetEthMessage = new EthMessage(peer, targetMessage); + // Dispatch message and check that stream processes messages + peer.dispatch(targetEthMessage); + assertThat(messageCount.get()).isEqualTo(1); + assertThat(closedCount.get()).isEqualTo(1); + + // Check that no new messages are delivered + getStream.get(peer); + peer.dispatch(targetEthMessage); + assertThat(messageCount.get()).isEqualTo(1); + assertThat(closedCount.get()).isEqualTo(1); + + // Set up 2 streams + getStream.get(peer).then(responseHandler); + getStream.get(peer).then(responseHandler); + + // Reset counters + messageCount.set(0); + closedCount.set(0); + + // Dispatch message and check that stream processes messages + peer.dispatch(targetEthMessage); + assertThat(messageCount.get()).isEqualTo(2); + assertThat(closedCount.get()).isEqualTo(0); + + // Dispatch unrelated message and check that it is not process + final EthMessage otherEthMessage = new EthMessage(peer, otherMessage); + peer.dispatch(otherEthMessage); + assertThat(messageCount.get()).isEqualTo(2); + assertThat(closedCount.get()).isEqualTo(0); + + // Dispatch last oustanding message and check that streams are closed + peer.dispatch(targetEthMessage); + assertThat(messageCount.get()).isEqualTo(4); + assertThat(closedCount.get()).isEqualTo(2); + + // Check that no new messages are delivered + getStream.get(peer); + peer.dispatch(targetEthMessage); + assertThat(messageCount.get()).isEqualTo(4); + assertThat(closedCount.get()).isEqualTo(2); + + // Open stream, then close it and check no messages are processed + final ResponseStream stream = getStream.get(peer).then(responseHandler); + // Reset counters + messageCount.set(0); + closedCount.set(0); + stream.close(); + getStream.get(peer); + peer.dispatch(targetEthMessage); + assertThat(messageCount.get()).isEqualTo(0); + assertThat(closedCount.get()).isEqualTo(1); + } + + private EthPeer createPeer() { + final Set caps = new HashSet<>(Collections.singletonList(EthProtocol.ETH63)); + final PeerConnection peerConnection = new MockPeerConnection(caps); + final Consumer onPeerReady = (peer) -> {}; + return new EthPeer(peerConnection, EthProtocol.NAME, onPeerReady); + } + + @FunctionalInterface + interface ResponseStreamSupplier { + ResponseStream get(EthPeer peer) throws PeerNotConnected; + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeersTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeersTest.java new file mode 100755 index 00000000000..09f1ba8282e --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthPeersTest.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.util.uint.UInt256; + +import org.junit.Before; +import org.junit.Test; + +public class EthPeersTest { + + private EthProtocolManager ethProtocolManager; + private BlockDataGenerator gen; + + @Before + public void setup() { + gen = new BlockDataGenerator(); + ethProtocolManager = EthProtocolManagerTestUtil.create(); + } + + @Test + public void comparesPeersWithHeightAndTd() { + // Set peerA with better height, lower td + final EthPeer peerA = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, UInt256.of(50), 0).getEthPeer(); + final EthPeer peerB = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, UInt256.of(100), 0).getEthPeer(); + peerA.chainState().update(gen.hash(), 20); + peerB.chainState().update(gen.hash(), 10); + + // Sanity check + assertThat(peerA.chainState().getEstimatedHeight()).isEqualTo(20); + assertThat(peerB.chainState().getEstimatedHeight()).isEqualTo(10); + + assertThat(EthPeers.CHAIN_HEIGHT.compare(peerA, peerB)).isGreaterThan(0); + assertThat(EthPeers.TOTAL_DIFFICULTY.compare(peerA, peerB)).isLessThan(0); + + assertThat(EthPeers.BEST_CHAIN.compare(peerA, peerB)).isGreaterThan(0); + assertThat(EthPeers.BEST_CHAIN.compare(peerB, peerA)).isLessThan(0); + assertThat(EthPeers.BEST_CHAIN.compare(peerA, peerA)).isEqualTo(0); + assertThat(EthPeers.BEST_CHAIN.compare(peerB, peerB)).isEqualTo(0); + } + + @Test + public void comparesPeersWithTdAndNoHeight() { + final EthPeer peerA = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, UInt256.of(100), 0).getEthPeer(); + final EthPeer peerB = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, UInt256.of(50), 0).getEthPeer(); + + // Sanity check + assertThat(peerA.chainState().getEstimatedHeight()).isEqualTo(0); + assertThat(peerB.chainState().getEstimatedHeight()).isEqualTo(0); + + assertThat(EthPeers.CHAIN_HEIGHT.compare(peerA, peerB)).isEqualTo(0); + assertThat(EthPeers.TOTAL_DIFFICULTY.compare(peerA, peerB)).isGreaterThan(0); + + assertThat(EthPeers.BEST_CHAIN.compare(peerA, peerB)).isGreaterThan(0); + assertThat(EthPeers.BEST_CHAIN.compare(peerB, peerA)).isLessThan(0); + assertThat(EthPeers.BEST_CHAIN.compare(peerA, peerA)).isEqualTo(0); + assertThat(EthPeers.BEST_CHAIN.compare(peerB, peerB)).isEqualTo(0); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTest.java new file mode 100755 index 00000000000..804a5a0c658 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTest.java @@ -0,0 +1,752 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.EthProtocol.EthVersion; +import net.consensys.pantheon.ethereum.eth.manager.MockPeerConnection.PeerSendHandler; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.BlockchainSetupUtil; +import net.consensys.pantheon.ethereum.eth.messages.BlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.BlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.EthPV63; +import net.consensys.pantheon.ethereum.eth.messages.GetBlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.GetBlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.GetReceiptsMessage; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockMessage; +import net.consensys.pantheon.ethereum.eth.messages.ReceiptsMessage; +import net.consensys.pantheon.ethereum.eth.messages.StatusMessage; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.DefaultMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionTimeoutException; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public final class EthProtocolManagerTest { + + private static Blockchain blockchain; + private static ProtocolSchedule protocolSchedule; + private static BlockDataGenerator gen; + + @BeforeClass + public static void setup() { + gen = new BlockDataGenerator(0); + final BlockchainSetupUtil blockchainSetupUtil = BlockchainSetupUtil.forTesting(); + blockchainSetupUtil.importAllBlocks(); + blockchain = blockchainSetupUtil.getBlockchain(); + protocolSchedule = blockchainSetupUtil.getProtocolSchedule(); + assert (blockchainSetupUtil.getMaxBlockNumber() >= 20L); + } + + @Test + public void disconnectOnUnsolicitedMessage() { + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final MessageData messageData = + BlockHeadersMessage.create(Collections.singletonList(blockchain.getBlockHeader(1).get())); + final MockPeerConnection peer = setupPeer(ethManager, (cap, msg, conn) -> {}); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + assertThat(peer.isDisconnected()).isTrue(); + } + } + + @Test + public void disconnectOnFailureToSendStatusMessage() { + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final MessageData messageData = + BlockHeadersMessage.create(Collections.singletonList(blockchain.getBlockHeader(1).get())); + final MockPeerConnection peer = + setupPeerWithoutStatusExchange(ethManager, (cap, msg, conn) -> {}); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + assertThat(peer.isDisconnected()).isTrue(); + } + } + + @Test + public void disconnectOnWrongChainId() { + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final MessageData messageData = + BlockHeadersMessage.create(Collections.singletonList(blockchain.getBlockHeader(1).get())); + final MockPeerConnection peer = + setupPeerWithoutStatusExchange(ethManager, (cap, msg, conn) -> {}); + + // Send status message with wrong chain + final StatusMessage statusMessage = + StatusMessage.create( + EthVersion.V63, + 2222, + blockchain.getChainHead().getTotalDifficulty(), + blockchain.getChainHeadHash(), + blockchain.getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get().getHash()); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, statusMessage)); + + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + assertThat(peer.isDisconnected()).isTrue(); + } + } + + @Test + public void disconnectOnWrongGenesisHash() { + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final MessageData messageData = + BlockHeadersMessage.create(Collections.singletonList(blockchain.getBlockHeader(1).get())); + final MockPeerConnection peer = + setupPeerWithoutStatusExchange(ethManager, (cap, msg, conn) -> {}); + + // Send status message with wrong chain + final StatusMessage statusMessage = + StatusMessage.create( + EthVersion.V63, + 1, + blockchain.getChainHead().getTotalDifficulty(), + gen.hash(), + blockchain.getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get().getHash()); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, statusMessage)); + + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + assertThat(peer.isDisconnected()).isTrue(); + } + } + + @Test(expected = ConditionTimeoutException.class) + public void doNotDisconnectOnValidMessage() { + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final MessageData messageData = + GetBlockBodiesMessage.create(Collections.singletonList(gen.hash())); + final MockPeerConnection peer = setupPeer(ethManager, (cap, msg, conn) -> {}); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + Awaitility.await() + .catchUncaughtExceptions() + .atMost(200, TimeUnit.MILLISECONDS) + .until(peer::isDisconnected); + } + } + + @Test + public void respondToGetHeaders() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final long startBlock = 5L; + final int blockCount = 5; + final MessageData messageData = + GetBlockHeadersMessage.create(startBlock, blockCount, false, 0); + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(blockCount); + for (int i = 0; i < blockCount; i++) { + assertThat(headers.get(i).getNumber()).isEqualTo(startBlock + i); + } + message.release(); + done.complete(null); + }; + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetHeadersWithinLimits() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + final int limit = 5; + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1, limit)) { + final long startBlock = 5L; + final int blockCount = 10; + final MessageData messageData = + GetBlockHeadersMessage.create(startBlock, blockCount, false, 0); + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(limit); + for (int i = 0; i < limit; i++) { + assertThat(headers.get(i).getNumber()).isEqualTo(startBlock + i); + } + message.release(); + done.complete(null); + }; + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetHeadersReversed() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final long endBlock = 10L; + final int blockCount = 5; + final MessageData messageData = GetBlockHeadersMessage.create(endBlock, blockCount, true, 0); + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(blockCount); + for (int i = 0; i < blockCount; i++) { + assertThat(headers.get(i).getNumber()).isEqualTo(endBlock - i); + } + message.release(); + done.complete(null); + }; + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetHeadersWithSkip() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final long startBlock = 5L; + final int blockCount = 5; + final int skip = 1; + final MessageData messageData = + GetBlockHeadersMessage.create(startBlock, blockCount, false, 1); + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(blockCount); + for (int i = 0; i < blockCount; i++) { + assertThat(headers.get(i).getNumber()).isEqualTo(startBlock + i * (skip + 1)); + } + message.release(); + done.complete(null); + }; + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetHeadersReversedWithSkip() + throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final long endBlock = 10L; + final int blockCount = 5; + final int skip = 1; + final MessageData messageData = + GetBlockHeadersMessage.create(endBlock, blockCount, true, skip); + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(blockCount); + for (int i = 0; i < blockCount; i++) { + assertThat(headers.get(i).getNumber()).isEqualTo(endBlock - i * (skip + 1)); + } + message.release(); + done.complete(null); + }; + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + private MockPeerConnection setupPeer( + final EthProtocolManager ethManager, final PeerSendHandler onSend) { + final MockPeerConnection peer = setupPeerWithoutStatusExchange(ethManager, onSend); + final StatusMessage statusMessage = + StatusMessage.create( + EthVersion.V63, + 1, + blockchain.getChainHead().getTotalDifficulty(), + blockchain.getChainHeadHash(), + blockchain.getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get().getHash()); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, statusMessage)); + return peer; + } + + private MockPeerConnection setupPeerWithoutStatusExchange( + final EthProtocolManager ethManager, final PeerSendHandler onSend) { + final Set caps = new HashSet<>(Arrays.asList(EthProtocol.ETH63)); + final MockPeerConnection peer = new MockPeerConnection(caps, onSend); + ethManager.handleNewConnection(peer); + return peer; + } + + @Test + public void respondToGetHeadersPartial() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final long startBlock = blockchain.getChainHeadBlockNumber() - 1L; + final int blockCount = 5; + final MessageData messageData = + GetBlockHeadersMessage.create(startBlock, blockCount, false, 0); + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(2); + for (int i = 0; i < 2; i++) { + assertThat(headers.get(i).getNumber()).isEqualTo(startBlock + i); + } + message.release(); + done.complete(null); + }; + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetHeadersEmpty() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final long startBlock = blockchain.getChainHeadBlockNumber() + 1; + final int blockCount = 5; + final MessageData messageData = + GetBlockHeadersMessage.create(startBlock, blockCount, false, 0); + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(0); + message.release(); + done.complete(null); + }; + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetBodies() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + // Setup blocks query + final long startBlock = blockchain.getChainHeadBlockNumber() - 5; + final int blockCount = 2; + final Block[] expectedBlocks = new Block[blockCount]; + for (int i = 0; i < blockCount; i++) { + final BlockHeader header = blockchain.getBlockHeader(startBlock + i).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + expectedBlocks[i] = new Block(header, body); + } + final List hashes = + Arrays.stream(expectedBlocks).map(Block::getHash).collect(Collectors.toList()); + final MessageData messageData = GetBlockBodiesMessage.create(hashes); + + // Define handler to validate response + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_BODIES); + final BlockBodiesMessage blocksMessage = BlockBodiesMessage.readFrom(message); + final List bodies = + Lists.newArrayList(blocksMessage.bodies(protocolSchedule)); + assertThat(bodies.size()).isEqualTo(blockCount); + for (int i = 0; i < blockCount; i++) { + assertThat(expectedBlocks[i].getBody()).isEqualTo(bodies.get(i)); + } + message.release(); + done.complete(null); + }; + + // Run test + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetBodiesWithinLimits() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + final int limit = 5; + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1, limit)) { + // Setup blocks query + final int blockCount = 10; + final long startBlock = blockchain.getChainHeadBlockNumber() - blockCount; + final Block[] expectedBlocks = new Block[blockCount]; + for (int i = 0; i < blockCount; i++) { + final BlockHeader header = blockchain.getBlockHeader(startBlock + i).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + expectedBlocks[i] = new Block(header, body); + } + final List hashes = + Arrays.stream(expectedBlocks).map(Block::getHash).collect(Collectors.toList()); + final MessageData messageData = GetBlockBodiesMessage.create(hashes); + + // Define handler to validate response + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_BODIES); + final BlockBodiesMessage blocksMessage = BlockBodiesMessage.readFrom(message); + final List bodies = + Lists.newArrayList(blocksMessage.bodies(protocolSchedule)); + assertThat(bodies.size()).isEqualTo(limit); + for (int i = 0; i < limit; i++) { + assertThat(expectedBlocks[i].getBody()).isEqualTo(bodies.get(i)); + } + message.release(); + done.complete(null); + }; + + // Run test + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetBodiesPartial() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + // Setup blocks query + final long expectedBlockNumber = blockchain.getChainHeadBlockNumber() - 1; + final BlockHeader header = blockchain.getBlockHeader(expectedBlockNumber).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + final Block expectedBlock = new Block(header, body); + + final List hashes = Arrays.asList(gen.hash(), expectedBlock.getHash(), gen.hash()); + final MessageData messageData = GetBlockBodiesMessage.create(hashes); + + // Define handler to validate response + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_BODIES); + final BlockBodiesMessage blocksMessage = BlockBodiesMessage.readFrom(message); + final List bodies = + Lists.newArrayList(blocksMessage.bodies(protocolSchedule)); + assertThat(bodies.size()).isEqualTo(1); + assertThat(expectedBlock.getBody()).isEqualTo(bodies.get(0)); + message.release(); + done.complete(null); + }; + + // Run test + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetReceipts() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + // Setup blocks query + final long startBlock = blockchain.getChainHeadBlockNumber() - 5; + final int blockCount = 2; + final List> expectedReceipts = new ArrayList<>(blockCount); + final List blockHashes = new ArrayList<>(blockCount); + for (int i = 0; i < blockCount; i++) { + final BlockHeader header = blockchain.getBlockHeader(startBlock + i).get(); + expectedReceipts.add(blockchain.getTxReceipts(header.getHash()).get()); + blockHashes.add(header.getHash()); + } + final MessageData messageData = GetReceiptsMessage.create(blockHashes); + + // Define handler to validate response + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV63.RECEIPTS); + final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(message); + final List> receipts = + Lists.newArrayList(receiptsMessage.receipts()); + assertThat(receipts.size()).isEqualTo(blockCount); + for (int i = 0; i < blockCount; i++) { + assertThat(expectedReceipts.get(i)).isEqualTo(receipts.get(i)); + } + message.release(); + done.complete(null); + }; + + // Run test + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetReceiptsWithinLimits() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + final int limit = 5; + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1, limit)) { + // Setup blocks query + final int blockCount = 10; + final long startBlock = blockchain.getChainHeadBlockNumber() - blockCount; + final List> expectedReceipts = new ArrayList<>(blockCount); + final List blockHashes = new ArrayList<>(blockCount); + for (int i = 0; i < blockCount; i++) { + final BlockHeader header = blockchain.getBlockHeader(startBlock + i).get(); + expectedReceipts.add(blockchain.getTxReceipts(header.getHash()).get()); + blockHashes.add(header.getHash()); + } + final MessageData messageData = GetReceiptsMessage.create(blockHashes); + + // Define handler to validate response + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV63.RECEIPTS); + final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(message); + final List> receipts = + Lists.newArrayList(receiptsMessage.receipts()); + assertThat(receipts.size()).isEqualTo(limit); + for (int i = 0; i < limit; i++) { + assertThat(expectedReceipts.get(i)).isEqualTo(receipts.get(i)); + } + message.release(); + done.complete(null); + }; + + // Run test + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void respondToGetReceiptsPartial() throws ExecutionException, InterruptedException { + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + // Setup blocks query + final long blockNumber = blockchain.getChainHeadBlockNumber() - 5; + final int blockCount = 2; + final BlockHeader header = blockchain.getBlockHeader(blockNumber).get(); + final List expectedReceipts = + blockchain.getTxReceipts(header.getHash()).get(); + final Hash blockHash = header.getHash(); + final MessageData messageData = + GetReceiptsMessage.create(Arrays.asList(gen.hash(), blockHash, gen.hash())); + + // Define handler to validate response + final PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV63.RECEIPTS); + final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(message); + final List> receipts = + Lists.newArrayList(receiptsMessage.receipts()); + assertThat(receipts.size()).isEqualTo(1); + assertThat(expectedReceipts).isEqualTo(receipts.get(0)); + message.release(); + done.complete(null); + }; + + // Run test + final PeerConnection peer = setupPeer(ethManager, onSend); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } + + @Test + public void newBlockMinedSendsNewBlockMessageToAllPeers() { + final EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1); + + // Define handler to validate response + final PeerSendHandler onSend = mock(PeerSendHandler.class); + final List peers = Lists.newArrayList(); + + final int PEER_COUNT = 5; + for (int i = 0; i < PEER_COUNT; i++) { + peers.add(setupPeer(ethManager, onSend)); + } + + final Hash chainHeadHash = blockchain.getChainHeadHash(); + final Block minedBlock = + new Block( + blockchain.getBlockHeader(chainHeadHash).get(), + blockchain.getBlockBody(chainHeadHash).get()); + + final UInt256 expectedTotalDifficulty = blockchain.getChainHead().getTotalDifficulty(); + + reset(onSend); + + ethManager.blockMined(minedBlock); + + final ArgumentCaptor messageSentCaptor = + ArgumentCaptor.forClass(NewBlockMessage.class); + final ArgumentCaptor receivingPeerCaptor = + ArgumentCaptor.forClass(PeerConnection.class); + final ArgumentCaptor capabilityCaptor = ArgumentCaptor.forClass(Capability.class); + + verify(onSend, times(PEER_COUNT)) + .exec( + capabilityCaptor.capture(), messageSentCaptor.capture(), receivingPeerCaptor.capture()); + + // assert that all entries in capability param were Eth63 + assertThat(capabilityCaptor.getAllValues().stream().distinct().collect(Collectors.toList())) + .isEqualTo(Collections.singletonList(EthProtocol.ETH63)); + + // assert that all messages transmitted contain the expected block & total difficulty. + final ProtocolSchedule protocolSchdeule = MainnetProtocolSchedule.create(); + for (final NewBlockMessage msg : messageSentCaptor.getAllValues()) { + assertThat(msg.block(protocolSchdeule)).isEqualTo(minedBlock); + assertThat(msg.totalDifficulty(protocolSchdeule)).isEqualTo(expectedTotalDifficulty); + msg.release(); + } + + assertThat(receivingPeerCaptor.getAllValues().containsAll(peers)).isTrue(); + } + + @Test + public void shouldSuccessfullyRespondToGetHeadersRequestLessThanZero() + throws ExecutionException, InterruptedException { + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final Block genesisBlock = gen.genesisBlock(); + final DefaultMutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisBlock, kvStore, MainnetBlockHashFunction::createHash); + + final BlockDataGenerator.BlockOptions options = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(1L) + .setParentHash(blockchain.getBlockHashByNumber(0L).get()); + final Block block = gen.block(options); + final List receipts = gen.receipts(block); + blockchain.appendBlock(block, receipts); + + final CompletableFuture done = new CompletableFuture<>(); + try (EthProtocolManager ethManager = new EthProtocolManager(blockchain, 1, true, 1)) { + final long startBlock = 1L; + final int requestedBlockCount = 13; + final int receivedBlockCount = 2; + final MessageData messageData = + GetBlockHeadersMessage.create(startBlock, requestedBlockCount, true, 0); + final MockPeerConnection.PeerSendHandler onSend = + (cap, message, conn) -> { + if (message.getCode() == EthPV62.STATUS) { + // Ignore status message + return; + } + assertThat(message.getCode()).isEqualTo(EthPV62.BLOCK_HEADERS); + final BlockHeadersMessage headersMsg = BlockHeadersMessage.readFrom(message); + final List headers = + Lists.newArrayList(headersMsg.getHeaders(protocolSchedule)); + assertThat(headers.size()).isEqualTo(receivedBlockCount); + for (int i = 0; i < receivedBlockCount; i++) { + assertThat(headers.get(i).getNumber()).isEqualTo(receivedBlockCount - 1 - i); + } + message.release(); + done.complete(null); + }; + + final Set caps = new HashSet<>(Arrays.asList(EthProtocol.ETH63)); + final MockPeerConnection peer = new MockPeerConnection(caps, onSend); + ethManager.handleNewConnection(peer); + final StatusMessage statusMessage = + StatusMessage.create( + EthProtocol.EthVersion.V63, + 1, + blockchain.getChainHead().getTotalDifficulty(), + blockchain.getChainHeadHash(), + blockchain.getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get().getHash()); + + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, statusMessage)); + ethManager.processMessage(EthProtocol.ETH63, new DefaultMessage(peer, messageData)); + done.get(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTestUtil.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTestUtil.java new file mode 100755 index 00000000000..1025733fdb7 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthProtocolManagerTestUtil.java @@ -0,0 +1,75 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.ChainHead; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.DeterministicEthScheduler.TimeoutPolicy; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.DefaultMessage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.util.uint.UInt256; + +public class EthProtocolManagerTestUtil { + + public static EthProtocolManager create( + final Blockchain blockchain, final TimeoutPolicy timeoutPolicy) { + final int networkId = 1; + final EthScheduler ethScheduler = new DeterministicEthScheduler(timeoutPolicy); + return new EthProtocolManager( + blockchain, networkId, false, EthProtocolManager.DEFAULT_REQUEST_LIMIT, ethScheduler); + } + + public static EthProtocolManager create(final Blockchain blockchain) { + return create(blockchain, () -> false); + } + + public static EthProtocolManager create() { + final Blockchain blockchain = + new DefaultMutableBlockchain( + GenesisConfig.mainnet().getBlock(), + new InMemoryKeyValueStorage(), + ScheduleBasedBlockHashFunction.create(MainnetProtocolSchedule.create())); + return create(blockchain); + } + + public static void broadcastMessage( + final EthProtocolManager ethProtocolManager, + final RespondingEthPeer peer, + final MessageData message) { + ethProtocolManager.processMessage( + EthProtocol.ETH63, new DefaultMessage(peer.getPeerConnection(), message)); + } + + public static RespondingEthPeer createPeer( + final EthProtocolManager ethProtocolManager, final UInt256 td) { + return RespondingEthPeer.create(ethProtocolManager, td); + } + + public static RespondingEthPeer createPeer( + final EthProtocolManager ethProtocolManager, final UInt256 td, final long estimatedHeight) { + return RespondingEthPeer.create(ethProtocolManager, td, estimatedHeight); + } + + public static RespondingEthPeer createPeer(final EthProtocolManager ethProtocolManager) { + return RespondingEthPeer.create(ethProtocolManager, UInt256.of(1000L)); + } + + public static RespondingEthPeer createPeer( + final EthProtocolManager ethProtocolManager, final long estimatedHeight) { + return RespondingEthPeer.create(ethProtocolManager, UInt256.of(1000L), estimatedHeight); + } + + public static RespondingEthPeer createPeer( + final EthProtocolManager ethProtocolManager, final Blockchain blockchain) { + final ChainHead head = blockchain.getChainHead(); + return RespondingEthPeer.create( + ethProtocolManager, + head.getHash(), + head.getTotalDifficulty(), + blockchain.getChainHeadBlockNumber()); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthSchedulerTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthSchedulerTest.java new file mode 100755 index 00000000000..19dff7a7630 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/EthSchedulerTest.java @@ -0,0 +1,195 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Before; +import org.junit.Test; + +public class EthSchedulerTest { + + private DeterministicEthScheduler ethScheduler; + private MockExecutorService workerExecutor; + private MockScheduledExecutor scheduledExecutor; + private AtomicBoolean shouldTimeout; + + @Before + public void setup() { + shouldTimeout = new AtomicBoolean(false); + ethScheduler = new DeterministicEthScheduler(shouldTimeout::get); + workerExecutor = ethScheduler.mockWorkerExecutor(); + scheduledExecutor = ethScheduler.mockScheduledExecutor(); + } + + @Test + public void scheduleWorkerTask_completesWhenScheduledTaskCompletes() { + final CompletableFuture future = new CompletableFuture<>(); + final CompletableFuture result = ethScheduler.scheduleWorkerTask(() -> future); + + assertThat(result.isDone()).isFalse(); + future.complete("bla"); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isFalse(); + assertThat(result.isCancelled()).isFalse(); + } + + @Test + public void scheduleWorkerTask_completesWhenScheduledTaskFails() { + final CompletableFuture future = new CompletableFuture<>(); + final CompletableFuture result = ethScheduler.scheduleWorkerTask(() -> future); + + assertThat(result.isDone()).isFalse(); + future.completeExceptionally(new RuntimeException("whoops")); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isTrue(); + assertThat(result.isCancelled()).isFalse(); + } + + @Test + public void scheduleWorkerTask_completesWhenScheduledTaskIsCancelled() { + final CompletableFuture future = new CompletableFuture<>(); + final CompletableFuture result = ethScheduler.scheduleWorkerTask(() -> future); + + assertThat(result.isDone()).isFalse(); + future.cancel(false); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isTrue(); + assertThat(result.isCancelled()).isTrue(); + } + + @Test + public void scheduleWorkerTask_cancelsScheduledFutureWhenResultIsCancelled() { + final CompletableFuture result = + ethScheduler.scheduleWorkerTask(() -> new CompletableFuture<>()); + + assertThat(workerExecutor.getScheduledFutures().size()).isEqualTo(1); + final Future future = workerExecutor.getScheduledFutures().get(0); + + verify(future, times(0)).cancel(anyBoolean()); + result.cancel(true); + verify(future, times(1)).cancel(eq(false)); + } + + @Test + public void scheduleFutureTask_completesWhenScheduledTaskCompletes() { + final CompletableFuture future = new CompletableFuture<>(); + final CompletableFuture result = + ethScheduler.scheduleFutureTask(() -> future, Duration.ofMillis(100)); + + assertThat(result.isDone()).isFalse(); + future.complete("bla"); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isFalse(); + assertThat(result.isCancelled()).isFalse(); + } + + @Test + public void scheduleFutureTask_completesWhenScheduledTaskFails() { + final CompletableFuture future = new CompletableFuture<>(); + final CompletableFuture result = + ethScheduler.scheduleFutureTask(() -> future, Duration.ofMillis(100)); + + assertThat(result.isDone()).isFalse(); + future.completeExceptionally(new RuntimeException("whoops")); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isTrue(); + assertThat(result.isCancelled()).isFalse(); + } + + @Test + public void scheduleFutureTask_completesWhenScheduledTaskIsCancelled() { + final CompletableFuture future = new CompletableFuture<>(); + final CompletableFuture result = + ethScheduler.scheduleFutureTask(() -> future, Duration.ofMillis(100)); + + assertThat(result.isDone()).isFalse(); + future.cancel(false); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isTrue(); + assertThat(result.isCancelled()).isTrue(); + } + + @Test + public void scheduleFutureTask_cancelsScheduledFutureWhenResultIsCancelled() { + final CompletableFuture result = + ethScheduler.scheduleFutureTask(() -> new CompletableFuture<>(), Duration.ofMillis(100)); + + assertThat(scheduledExecutor.getScheduledFutures().size()).isEqualTo(1); + final Future future = scheduledExecutor.getScheduledFutures().get(0); + + verify(future, times(0)).cancel(anyBoolean()); + result.cancel(true); + verify(future, times(1)).cancel(eq(false)); + } + + @Test + public void timeout_resultCompletesWhenScheduledTaskCompletes() { + final MockEthTask task = new MockEthTask(); + final CompletableFuture result = ethScheduler.timeout(task, Duration.ofSeconds(2)); + + assertThat(task.hasBeenStarted()).isTrue(); + assertThat(task.isDone()).isFalse(); + assertThat(result.isDone()).isFalse(); + + task.complete(); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isFalse(); + assertThat(result.isCancelled()).isFalse(); + } + + @Test + public void timeout_resultCompletesWhenScheduledTaskFails() { + final MockEthTask task = new MockEthTask(); + final CompletableFuture result = ethScheduler.timeout(task, Duration.ofSeconds(2)); + + assertThat(task.hasBeenStarted()).isTrue(); + assertThat(task.isDone()).isFalse(); + assertThat(result.isDone()).isFalse(); + + task.fail(); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isTrue(); + assertThat(result.isCancelled()).isFalse(); + } + + @Test + public void timeout_resultCompletesOnTimeout() { + shouldTimeout.set(true); + final MockEthTask task = new MockEthTask(); + final CompletableFuture result = ethScheduler.timeout(task, Duration.ofSeconds(2)); + + // Timeout fires immediately, so everything should be done + assertThat(task.hasBeenStarted()).isTrue(); + assertThat(task.isDone()).isTrue(); + assertThat(result.isDone()).isTrue(); + assertThat(result.isCompletedExceptionally()).isTrue(); + assertThatThrownBy(result::get).hasCauseInstanceOf(TimeoutException.class); + assertThat(result.isCancelled()).isFalse(); + } + + @Test + public void timeout_cancelsTaskWhenResultIsCancelled() { + final MockEthTask task = new MockEthTask(); + final CompletableFuture result = ethScheduler.timeout(task, Duration.ofSeconds(2)); + + assertThat(task.hasBeenStarted()).isTrue(); + assertThat(task.isDone()).isFalse(); + assertThat(result.isDone()).isFalse(); + + result.cancel(false); + assertThat(task.isDone()).isTrue(); + assertThat(task.isFailed()).isTrue(); + assertThat(task.isSucceeded()).isFalse(); + assertThat(task.isCancelled()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockEthTask.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockEthTask.java new file mode 100755 index 00000000000..afd1989ba99 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockEthTask.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +public class MockEthTask extends AbstractEthTask { + + private boolean executed = false; + + @Override + protected void executeTask() { + executed = true; + } + + public boolean hasBeenStarted() { + return executed; + } + + public void complete() { + result.get().complete(null); + } + + public void fail() { + result.get().completeExceptionally(new RuntimeException("Failure forced for testing")); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockExecutorService.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockExecutorService.java new file mode 100755 index 00000000000..a75132fbcc2 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockExecutorService.java @@ -0,0 +1,120 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.mockito.Mockito.spy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class MockExecutorService implements ExecutorService { + + private final List> scheduledFutures = new ArrayList<>(); + + // Test utility for inspecting scheduled futures + public List> getScheduledFutures() { + return scheduledFutures; + } + + @Override + public void shutdown() {} + + @Override + public List shutdownNow() { + return Collections.emptyList(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(final long timeout, final TimeUnit unit) + throws InterruptedException { + return false; + } + + @Override + public Future submit(final Callable task) { + CompletableFuture future = new CompletableFuture<>(); + try { + final T result = task.call(); + future.complete(result); + } catch (final Exception e) { + future.completeExceptionally(e); + } + future = spy(future); + scheduledFutures.add(future); + return future; + } + + @Override + public Future submit(final Runnable task, final T result) { + CompletableFuture future = new CompletableFuture<>(); + try { + task.run(); + future.complete(result); + } catch (final Exception e) { + future.completeExceptionally(e); + } + future = spy(future); + scheduledFutures.add(future); + return future; + } + + @Override + public Future submit(final Runnable task) { + CompletableFuture future = new CompletableFuture<>(); + try { + task.run(); + future.complete(null); + } catch (final Exception e) { + future.completeExceptionally(e); + } + future = spy(future); + scheduledFutures.add(future); + return future; + } + + @Override + public List> invokeAll(final Collection> tasks) + throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public List> invokeAll( + final Collection> tasks, final long timeout, final TimeUnit unit) + throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public T invokeAny(final Collection> tasks) + throws InterruptedException, ExecutionException { + throw new UnsupportedOperationException(); + } + + @Override + public T invokeAny( + final Collection> tasks, final long timeout, final TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + throw new UnsupportedOperationException(); + } + + @Override + public void execute(final Runnable command) {} +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockPeerConnection.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockPeerConnection.java new file mode 100755 index 00000000000..e750ca27be8 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockPeerConnection.java @@ -0,0 +1,89 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import com.google.common.base.Strings; + +class MockPeerConnection implements PeerConnection { + + private static final PeerSendHandler NOOP_ON_SEND = (cap, msg, conn) -> {}; + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + private final PeerSendHandler onSend; + private final Set caps; + private volatile boolean disconnected = false; + private final Bytes32 nodeId; + + public MockPeerConnection(final Set caps, final PeerSendHandler onSend) { + this.caps = caps; + this.onSend = onSend; + this.nodeId = generateUsefulNodeId(); + } + + private Bytes32 generateUsefulNodeId() { + // EthPeer only shows the first 20 characters of the node ID so add some padding. + return Bytes32.fromHexStringLenient( + "0x" + ID_GENERATOR.incrementAndGet() + Strings.repeat("0", 46)); + } + + public MockPeerConnection(final Set caps) { + this(caps, NOOP_ON_SEND); + } + + @Override + public void send(final Capability capability, final MessageData message) throws PeerNotConnected { + if (disconnected) { + message.release(); + throw new PeerNotConnected("MockPeerConnection disconnected"); + } + onSend.exec(capability, message, this); + } + + @Override + public Set getAgreedCapabilities() { + return caps; + } + + @Override + public PeerInfo getPeer() { + return new PeerInfo(5, "Mock", new ArrayList<>(caps), 0, nodeId); + } + + @Override + public void terminateConnection(final DisconnectReason reason, final boolean peerInitiated) { + disconnect(reason); + } + + @Override + public void disconnect(final DisconnectReason reason) { + disconnected = true; + } + + @Override + public SocketAddress getLocalAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public SocketAddress getRemoteAddress() { + throw new UnsupportedOperationException(); + } + + public boolean isDisconnected() { + return disconnected; + } + + @FunctionalInterface + public interface PeerSendHandler { + void exec(Capability cap, MessageData msg, PeerConnection connection); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockScheduledExecutor.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockScheduledExecutor.java new file mode 100755 index 00000000000..b1680fdc41c --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/MockScheduledExecutor.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import java.util.concurrent.Callable; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class MockScheduledExecutor extends MockExecutorService implements ScheduledExecutorService { + + @Override + public ScheduledFuture schedule( + final Runnable command, final long delay, final TimeUnit unit) { + final Future future = this.submit(command); + return new MockScheduledFuture<>(future); + } + + @Override + public ScheduledFuture schedule( + final Callable callable, final long delay, final TimeUnit unit) { + final Future future = this.submit(callable); + return new MockScheduledFuture<>(future); + } + + @Override + public ScheduledFuture scheduleAtFixedRate( + final Runnable command, final long initialDelay, final long period, final TimeUnit unit) { + final Future future = this.submit(command); + return new MockScheduledFuture<>(future); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay( + final Runnable command, final long initialDelay, final long delay, final TimeUnit unit) { + final Future future = this.submit(command); + return new MockScheduledFuture<>(future); + } + + private static class MockScheduledFuture implements ScheduledFuture { + + private final Future future; + + public MockScheduledFuture(final Future future) { + this.future = future; + } + + @Override + public long getDelay(final TimeUnit unit) { + return 0; + } + + @Override + public int compareTo(final Delayed o) { + return 0; + } + + @Override + public boolean cancel(final boolean mayInterruptIfRunning) { + return future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + return future.get(); + } + + @Override + public T get(final long timeout, final TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return future.get(timeout, unit); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputationTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputationTest.java new file mode 100755 index 00000000000..decc1e9c18f --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/PeerReputationTest.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static net.consensys.pantheon.ethereum.eth.manager.PeerReputation.USELESS_RESPONSE_WINDOW_IN_MILLIS; +import static net.consensys.pantheon.ethereum.eth.messages.EthPV62.GET_BLOCK_BODIES; +import static net.consensys.pantheon.ethereum.eth.messages.EthPV62.GET_BLOCK_HEADERS; +import static net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason.TIMEOUT; +import static net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason.USELESS_PEER; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class PeerReputationTest { + + private final PeerReputation reputation = new PeerReputation(); + + @Test + public void shouldOnlyDisconnectWhenTimeoutLimitReached() { + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).contains(TIMEOUT); + } + + @Test + public void shouldTrackTimeoutsSeparatelyForDifferentRequestTypes() { + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_BODIES)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_BODIES)).isEmpty(); + + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).contains(TIMEOUT); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_BODIES)).contains(TIMEOUT); + } + + @Test + public void shouldResetTimeoutCountForRequestType() { + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).isEmpty(); + + assertThat(reputation.recordRequestTimeout(GET_BLOCK_BODIES)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_BODIES)).isEmpty(); + + reputation.resetTimeoutCount(GET_BLOCK_HEADERS); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_HEADERS)).isEmpty(); + assertThat(reputation.recordRequestTimeout(GET_BLOCK_BODIES)).contains(TIMEOUT); + } + + @Test + public void shouldOnlyDisconnectWhenEmptyResponseThresholdReached() { + assertThat(reputation.recordUselessResponse(1001)).isEmpty(); + assertThat(reputation.recordUselessResponse(1002)).isEmpty(); + assertThat(reputation.recordUselessResponse(1003)).isEmpty(); + assertThat(reputation.recordUselessResponse(1004)).isEmpty(); + assertThat(reputation.recordUselessResponse(1005)).contains(USELESS_PEER); + } + + @Test + public void shouldDiscardEmptyResponseRecordsAfterTimeWindowElapses() { + // Bring it to the brink of disconnection. + assertThat(reputation.recordUselessResponse(1001)).isEmpty(); + assertThat(reputation.recordUselessResponse(1002)).isEmpty(); + assertThat(reputation.recordUselessResponse(1003)).isEmpty(); + assertThat(reputation.recordUselessResponse(1004)).isEmpty(); + + // But then the next empty response doesn't come in until after the window expires on the first + assertThat(reputation.recordUselessResponse(1001 + USELESS_RESPONSE_WINDOW_IN_MILLIS + 1)) + .isEmpty(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RequestManagerTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RequestManagerTest.java new file mode 100755 index 00000000000..ca2b254b137 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RequestManagerTest.java @@ -0,0 +1,212 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.RequestSender; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseCallback; +import net.consensys.pantheon.ethereum.eth.manager.RequestManager.ResponseStream; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import io.netty.buffer.Unpooled; +import org.junit.Test; + +public class RequestManagerTest { + + @Test + public void dispatchesMessagesReceivedAfterRegisteringCallback() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer); + + final AtomicInteger sendCount = new AtomicInteger(0); + final RequestSender sender = sendCount::incrementAndGet; + final List receivedMessages = new ArrayList<>(); + final AtomicInteger closedCount = new AtomicInteger(0); + final ResponseCallback responseHandler = + (closed, msg, p) -> { + if (closed) { + closedCount.incrementAndGet(); + } else { + receivedMessages.add(msg); + } + }; + + // Send request + final ResponseStream stream = requestManager.dispatchRequest(sender); + assertThat(sendCount.get()).isEqualTo(1); + stream.then(responseHandler); + + // Dispatch message + final EthMessage mockMessage = mockMessage(peer); + requestManager.dispatchResponse(mockMessage); + + // Response handler should get message + assertThat(receivedMessages.size()).isEqualTo(1); + assertThat(receivedMessages.get(0)).isEqualTo(mockMessage.getData()); + assertThat(closedCount.get()).isEqualTo(1); + } + + @Test + public void dispatchesMessagesReceivedBeforeRegisteringCallback() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer); + + final AtomicInteger sendCount = new AtomicInteger(0); + final RequestSender sender = sendCount::incrementAndGet; + final List receivedMessages = new ArrayList<>(); + final AtomicInteger closedCount = new AtomicInteger(0); + final ResponseCallback responseHandler = + (closed, msg, p) -> { + if (closed) { + closedCount.incrementAndGet(); + } else { + receivedMessages.add(msg); + } + }; + + // Send request + final ResponseStream stream = requestManager.dispatchRequest(sender); + assertThat(sendCount.get()).isEqualTo(1); + + // Dispatch message + final EthMessage mockMessage = mockMessage(peer); + requestManager.dispatchResponse(mockMessage); + + // Response handler should get message + stream.then(responseHandler); + assertThat(receivedMessages.size()).isEqualTo(1); + assertThat(receivedMessages.get(0)).isEqualTo(mockMessage.getData()); + assertThat(closedCount.get()).isEqualTo(1); + } + + @Test + public void dispatchesMessagesReceivedBeforeAndAfterRegisteringCallback() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer); + + final AtomicInteger sendCount = new AtomicInteger(0); + final RequestSender sender = sendCount::incrementAndGet; + final List receivedMessages = new ArrayList<>(); + final AtomicInteger closedCount = new AtomicInteger(0); + final ResponseCallback responseHandler = + (closed, msg, p) -> { + if (closed) { + closedCount.incrementAndGet(); + } else { + receivedMessages.add(msg); + } + }; + + // Send 2 requests so we can receive 2 messages before closing + final ResponseStream stream = requestManager.dispatchRequest(sender); + assertThat(sendCount.get()).isEqualTo(1); + requestManager.dispatchRequest(sender); + assertThat(sendCount.get()).isEqualTo(2); + + // Dispatch first message + EthMessage mockMessage = mockMessage(peer); + requestManager.dispatchResponse(mockMessage); + + // Response handler should get messages sent before it is registered + stream.then(responseHandler); + assertThat(receivedMessages.size()).isEqualTo(1); + assertThat(receivedMessages.get(0)).isEqualTo(mockMessage.getData()); + assertThat(closedCount.get()).isEqualTo(0); + + // Dispatch second message + mockMessage = mockMessage(peer); + requestManager.dispatchResponse(mockMessage); + + // Response handler should get messages sent after it is registered + assertThat(receivedMessages.size()).isEqualTo(2); + assertThat(receivedMessages.get(1)).isEqualTo(mockMessage.getData()); + assertThat(closedCount.get()).isEqualTo(1); + } + + @Test + public void dispatchesMessagesToMultipleStreams() throws Exception { + final EthPeer peer = createPeer(); + final RequestManager requestManager = new RequestManager(peer); + + final AtomicInteger sendCount = new AtomicInteger(0); + final RequestSender sender = sendCount::incrementAndGet; + + final List receivedMessagesA = new ArrayList<>(); + final AtomicInteger closedCountA = new AtomicInteger(0); + final ResponseCallback responseHandlerA = + (closed, msg, p) -> { + if (closed) { + closedCountA.incrementAndGet(); + } else { + receivedMessagesA.add(msg); + } + }; + final List receivedMessagesB = new ArrayList<>(); + final AtomicInteger closedCountB = new AtomicInteger(0); + final ResponseCallback responseHandlerB = + (closed, msg, p) -> { + if (closed) { + closedCountB.incrementAndGet(); + } else { + receivedMessagesB.add(msg); + } + }; + + // Send request + final ResponseStream streamA = requestManager.dispatchRequest(sender); + final ResponseStream streamB = requestManager.dispatchRequest(sender); + assertThat(sendCount.get()).isEqualTo(2); + streamA.then(responseHandlerA); + + // Dispatch message + EthMessage mockMessage = mockMessage(peer); + requestManager.dispatchResponse(mockMessage); + + // Response handler A should get message + assertThat(receivedMessagesA.size()).isEqualTo(1); + assertThat(receivedMessagesA.get(0)).isEqualTo(mockMessage.getData()); + assertThat(closedCountA.get()).isEqualTo(0); + + streamB.then(responseHandlerB); + + // Response handler B should get message + assertThat(receivedMessagesB.size()).isEqualTo(1); + assertThat(receivedMessagesB.get(0)).isEqualTo(mockMessage.getData()); + assertThat(closedCountB.get()).isEqualTo(0); + + // Dispatch second message + mockMessage = mockMessage(peer); + requestManager.dispatchResponse(mockMessage); + + // Response handler A should get message + assertThat(receivedMessagesA.size()).isEqualTo(2); + assertThat(receivedMessagesA.get(1)).isEqualTo(mockMessage.getData()); + assertThat(closedCountA.get()).isEqualTo(1); + // Response handler B should get message + assertThat(receivedMessagesB.size()).isEqualTo(2); + assertThat(receivedMessagesB.get(1)).isEqualTo(mockMessage.getData()); + assertThat(closedCountB.get()).isEqualTo(1); + } + + private EthMessage mockMessage(final EthPeer peer) { + return new EthMessage(peer, new RawMessage(1, Unpooled.buffer())); + } + + private EthPeer createPeer() { + final Set caps = new HashSet<>(Collections.singletonList(EthProtocol.ETH63)); + final PeerConnection peerConnection = new MockPeerConnection(caps); + final Consumer onPeerReady = (peer) -> {}; + return new EthPeer(peerConnection, EthProtocol.NAME, onPeerReady); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RespondingEthPeer.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RespondingEthPeer.java new file mode 100755 index 00000000000..95b0da41be7 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/RespondingEthPeer.java @@ -0,0 +1,310 @@ +package net.consensys.pantheon.ethereum.eth.manager; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.messages.BlockBodiesMessage; +import net.consensys.pantheon.ethereum.eth.messages.BlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.EthPV63; +import net.consensys.pantheon.ethereum.eth.messages.NodeDataMessage; +import net.consensys.pantheon.ethereum.eth.messages.ReceiptsMessage; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.DefaultMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Stream; + +import com.google.common.collect.Lists; + +public class RespondingEthPeer { + private static final BlockDataGenerator gen = new BlockDataGenerator(); + private static final int DEFAULT_ESTIMATED_HEIGHT = 1000; + private final EthPeer ethPeer; + private final Queue outgoingMessages; + private final EthProtocolManager ethProtocolManager; + private final MockPeerConnection peerConnection; + + private RespondingEthPeer( + final EthProtocolManager ethProtocolManager, + final MockPeerConnection peerConnection, + final EthPeer ethPeer, + final Queue outgoingMessages) { + this.ethProtocolManager = ethProtocolManager; + this.peerConnection = peerConnection; + this.ethPeer = ethPeer; + this.outgoingMessages = outgoingMessages; + } + + public static void respondOnce(final Responder responder, final List peers) { + for (final RespondingEthPeer peer : peers) { + if (peer.respond(responder)) { + break; + } + } + } + + public static void respondOnce(final Responder responder, final RespondingEthPeer... peers) { + respondOnce(responder, Arrays.asList(peers)); + } + + public MockPeerConnection getPeerConnection() { + return peerConnection; + } + + public static RespondingEthPeer create( + final EthProtocolManager ethProtocolManager, final UInt256 totalDifficulty) { + return create(ethProtocolManager, totalDifficulty, DEFAULT_ESTIMATED_HEIGHT); + } + + public static RespondingEthPeer create( + final EthProtocolManager ethProtocolManager, + final UInt256 totalDifficulty, + final long estimatedHeight) { + final Hash chainHeadHash = gen.hash(); + return create(ethProtocolManager, chainHeadHash, totalDifficulty, estimatedHeight); + } + + public static RespondingEthPeer create( + final EthProtocolManager ethProtocolManager, + final Hash chainHeadHash, + final UInt256 totalDifficulty) { + return create(ethProtocolManager, chainHeadHash, totalDifficulty, DEFAULT_ESTIMATED_HEIGHT); + } + + public static RespondingEthPeer create( + final EthProtocolManager ethProtocolManager, + final Hash chainHeadHash, + final UInt256 totalDifficulty, + final long estimatedHeight) { + final EthPeers ethPeers = ethProtocolManager.ethContext().getEthPeers(); + + final Set caps = new HashSet<>(Collections.singletonList(EthProtocol.ETH63)); + final Queue outgoingMessages = new ArrayDeque<>(); + final MockPeerConnection peerConnection = + new MockPeerConnection( + caps, (cap, msg, conn) -> outgoingMessages.add(new OutgoingMessage(cap, msg))); + ethPeers.registerConnection(peerConnection); + final EthPeer peer = ethPeers.peer(peerConnection); + peer.registerStatusReceived(chainHeadHash, totalDifficulty); + peer.chainState().update(chainHeadHash, estimatedHeight); + peer.registerStatusSent(); + + return new RespondingEthPeer(ethProtocolManager, peerConnection, peer, outgoingMessages); + } + + public EthPeer getEthPeer() { + return ethPeer; + } + + public void respondWhile(final Responder responder, final RespondWhileCondition condition) { + while (condition.shouldRespond()) { + respond(responder); + } + } + + public void respondTimes(final Responder responder, final int maxCycles) { + // Respond repeatedly, as each round may produce new outgoing messages + int count = 0; + while (!outgoingMessages.isEmpty()) { + count++; + respond(responder); + if (count >= maxCycles) { + break; + } + } + } + + /** + * @param responder + * @return True if any requests were processed + */ + public boolean respond(final Responder responder) { + // Respond to queued messages + final List currentMessages = new ArrayList<>(outgoingMessages); + outgoingMessages.clear(); + for (final OutgoingMessage msg : currentMessages) { + try { + final Optional maybeResponse = + responder.respond(msg.capability, msg.messageData); + maybeResponse.ifPresent( + (response) -> { + try { + ethProtocolManager.processMessage( + msg.capability, new DefaultMessage(peerConnection, response)); + } finally { + response.release(); + } + }); + } finally { + msg.messageData.release(); + } + } + return currentMessages.size() > 0; + } + + public Optional peekNextOutgoingRequest() { + if (outgoingMessages.isEmpty()) { + return Optional.empty(); + } + return Optional.of(outgoingMessages.peek().messageData); + } + + public Stream pendingOutgoingRequests() { + return outgoingMessages.stream().map(OutgoingMessage::messageData); + } + + public boolean hasOutstandingRequests() { + return !outgoingMessages.isEmpty(); + } + + public static Responder blockchainResponder(final Blockchain blockchain) { + return (cap, msg) -> { + MessageData response = null; + switch (msg.getCode()) { + case EthPV62.GET_BLOCK_HEADERS: + response = EthServer.constructGetHeadersResponse(blockchain, msg, 200); + break; + case EthPV62.GET_BLOCK_BODIES: + response = EthServer.constructGetBodiesResponse(blockchain, msg, 200); + break; + case EthPV63.GET_RECEIPTS: + response = EthServer.constructGetReceiptsResponse(blockchain, msg, 200); + break; + case EthPV63.GET_NODE_DATA: + response = EthServer.constructGetNodeDataResponse(msg, 200); + break; + } + return Optional.ofNullable(response); + }; + } + + public static Responder partialResponder( + final Blockchain blockchain, final ProtocolSchedule protocolSchedule) { + final Responder fullResponder = blockchainResponder(blockchain); + return (cap, msg) -> { + final Optional maybeResponse = fullResponder.respond(cap, msg); + if (!maybeResponse.isPresent()) { + return maybeResponse; + } + // Rewrite response with a subset of data + final MessageData originalResponse = maybeResponse.get(); + MessageData partialResponse = originalResponse; + switch (msg.getCode()) { + case EthPV62.GET_BLOCK_HEADERS: + final BlockHeadersMessage headersMessage = BlockHeadersMessage.readFrom(originalResponse); + try { + final List originalHeaders = + Lists.newArrayList(headersMessage.getHeaders(protocolSchedule)); + final List partialHeaders = + originalHeaders.subList(0, originalHeaders.size() / 2); + partialResponse = BlockHeadersMessage.create(partialHeaders); + } finally { + headersMessage.release(); + } + break; + case EthPV62.GET_BLOCK_BODIES: + final BlockBodiesMessage bodiesMessage = BlockBodiesMessage.readFrom(originalResponse); + try { + final List originalBodies = + Lists.newArrayList(bodiesMessage.bodies(protocolSchedule)); + final List partialBodies = + originalBodies.subList(0, originalBodies.size() / 2); + partialResponse = BlockBodiesMessage.create(partialBodies); + } finally { + bodiesMessage.release(); + } + break; + case EthPV63.GET_RECEIPTS: + final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(originalResponse); + try { + final List> originalReceipts = + Lists.newArrayList(receiptsMessage.receipts()); + final List> partialReceipts = + originalReceipts.subList(0, originalReceipts.size() / 2); + partialResponse = ReceiptsMessage.create(partialReceipts); + } finally { + receiptsMessage.release(); + } + break; + case EthPV63.GET_NODE_DATA: + final NodeDataMessage nodeDataMessage = NodeDataMessage.readFrom(originalResponse); + try { + final List originalNodeData = + Lists.newArrayList(nodeDataMessage.nodeData()); + final List partialNodeData = + originalNodeData.subList(0, originalNodeData.size() / 2); + partialResponse = NodeDataMessage.create(partialNodeData); + } finally { + nodeDataMessage.release(); + } + break; + } + return Optional.of(partialResponse); + }; + } + + public static Responder emptyResponder() { + return (cap, msg) -> { + MessageData response = null; + switch (msg.getCode()) { + case EthPV62.GET_BLOCK_HEADERS: + response = BlockHeadersMessage.create(Collections.emptyList()); + break; + case EthPV62.GET_BLOCK_BODIES: + response = BlockBodiesMessage.create(Collections.emptyList()); + break; + case EthPV63.GET_RECEIPTS: + response = ReceiptsMessage.create(Collections.emptyList()); + break; + case EthPV63.GET_NODE_DATA: + response = NodeDataMessage.create(Collections.emptyList()); + break; + } + return Optional.ofNullable(response); + }; + } + + static class OutgoingMessage { + private final Capability capability; + private final MessageData messageData; + + OutgoingMessage(final Capability capability, final MessageData messageData) { + this.capability = capability; + this.messageData = messageData; + } + + public Capability capability() { + return capability; + } + + public MessageData messageData() { + return messageData; + } + } + + @FunctionalInterface + public interface Responder { + Optional respond(Capability cap, MessageData msg); + } + + public interface RespondWhileCondition { + boolean shouldRespond(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/AbstractMessageTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/AbstractMessageTaskTest.java new file mode 100755 index 00000000000..f1295c727c3 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/AbstractMessageTaskTest.java @@ -0,0 +1,126 @@ +package net.consensys.pantheon.ethereum.eth.manager.ethtaskutils; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public abstract class AbstractMessageTaskTest { + protected static Blockchain blockchain; + protected static ProtocolSchedule protocolSchedule; + protected static ProtocolContext protocolContext; + protected EthProtocolManager ethProtocolManager; + protected EthContext ethContext; + protected AtomicBoolean peersDoTimeout; + protected AtomicInteger peerCountToTimeout; + + @BeforeClass + public static void setup() { + final BlockchainSetupUtil blockchainSetupUtil = BlockchainSetupUtil.forTesting(); + blockchainSetupUtil.importAllBlocks(); + blockchain = blockchainSetupUtil.getBlockchain(); + protocolSchedule = blockchainSetupUtil.getProtocolSchedule(); + protocolContext = blockchainSetupUtil.getProtocolContext(); + assert (blockchainSetupUtil.getMaxBlockNumber() >= 20L); + } + + @Before + public void setupTest() { + peersDoTimeout = new AtomicBoolean(false); + peerCountToTimeout = new AtomicInteger(0); + ethProtocolManager = + EthProtocolManagerTestUtil.create( + blockchain, () -> peerCountToTimeout.getAndDecrement() > 0 || peersDoTimeout.get()); + ethContext = ethProtocolManager.ethContext(); + } + + protected abstract T generateDataToBeRequested(); + + protected abstract EthTask createTask(T requestedData); + + protected abstract void assertResultMatchesExpectation( + T requestedData, R response, EthPeer respondingPeer); + + @Test + public void completesWhenPeersAreResponsive() throws ExecutionException, InterruptedException { + // Setup a responsive peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); + + // Setup data to be requested and expected response + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicReference actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + respondingPeer.respondWhile(responder, () -> !future.isDone()); + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertResultMatchesExpectation(requestedData, actualResult.get(), respondingPeer.getEthPeer()); + } + + @Test + public void doesNotCompleteWhenPeersDoNotRespond() + throws ExecutionException, InterruptedException { + // Setup a unresponsive peer + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); + + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + future.whenComplete( + (response, error) -> { + done.compareAndSet(false, true); + }); + assertThat(done).isFalse(); + } + + @Test + public void cancel() throws ExecutionException, InterruptedException { + // Setup a unresponsive peer + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); + + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + + assertThat(future.isDone()).isFalse(); + task.cancel(); + assertThat(future.isDone()).isTrue(); + assertThat(future.isCancelled()).isTrue(); + assertThat(task.run().isCancelled()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java new file mode 100755 index 00000000000..0708f1134fa --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/BlockchainSetupUtil.java @@ -0,0 +1,194 @@ +package net.consensys.pantheon.ethereum.eth.manager.ethtaskutils; + +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.util.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.util.RawBlockIterator; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.rules.TemporaryFolder; + +public class BlockchainSetupUtil { + private final GenesisConfig genesisConfig; + private final KeyValueStorage kvStore; + private final MutableBlockchain blockchain; + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final WorldStateArchive worldArchive; + private final List blocks; + private long maxBlockNumber; + + public BlockchainSetupUtil( + final GenesisConfig genesisConfig, + final KeyValueStorage kvStore, + final MutableBlockchain blockchain, + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule, + final WorldStateArchive worldArchive, + final List blocks) { + this.genesisConfig = genesisConfig; + this.kvStore = kvStore; + this.blockchain = blockchain; + this.protocolContext = protocolContext; + this.protocolSchedule = protocolSchedule; + this.worldArchive = worldArchive; + this.blocks = blocks; + } + + public Blockchain importAllBlocks() { + importBlocks(blocks); + return blockchain; + } + + public void importFirstBlocks(final int count) { + importBlocks(blocks.subList(0, count)); + } + + public void importBlockAtIndex(final int index) { + importBlocks(Collections.singletonList(blocks.get(index))); + } + + public Block getBlock(final int index) { + checkArgument(index < blocks.size(), "Invalid block index"); + return blocks.get(index); + } + + public int blockCount() { + return blocks.size(); + } + + public static BlockchainSetupUtil forTesting() { + final ProtocolSchedule protocolSchedule = MainnetProtocolSchedule.create(); + final TemporaryFolder temp = new TemporaryFolder(); + try { + temp.create(); + final URL genesisFileUrl = getResourceUrl(temp, "testGenesis.json"); + final GenesisConfig genesisConfig = + GenesisConfig.fromJson( + Resources.toString(genesisFileUrl, Charsets.UTF_8), protocolSchedule); + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final MutableBlockchain blockchain = + new DefaultMutableBlockchain( + genesisConfig.getBlock(), kvStore, MainnetBlockHashFunction::createHash); + final WorldStateArchive worldArchive = createInMemoryWorldStateArchive(); + + genesisConfig.writeStateTo(worldArchive.getMutable()); + final ProtocolContext protocolContext = + new ProtocolContext<>(blockchain, worldArchive, null); + + final Path blocksPath = getResourcePath(temp, "testBlockchain.blocks"); + final List blocks = new ArrayList<>(); + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + try (final RawBlockIterator iterator = + new RawBlockIterator(blocksPath, rlp -> BlockHeader.readFrom(rlp, blockHashFunction))) { + while (iterator.hasNext()) { + blocks.add(iterator.next()); + } + } + return new BlockchainSetupUtil<>( + genesisConfig, + kvStore, + blockchain, + protocolContext, + protocolSchedule, + worldArchive, + blocks); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } finally { + temp.delete(); + } + } + + private static Path getResourcePath(final TemporaryFolder temp, final String resource) + throws IOException { + final URL url = Resources.getResource(resource); + final Path path = + Files.write( + temp.newFile().toPath(), + Resources.toByteArray(url), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + return path; + } + + private static URL getResourceUrl(final TemporaryFolder temp, final String resource) + throws IOException { + final Path path = getResourcePath(temp, resource); + return path.toUri().toURL(); + } + + public long getMaxBlockNumber() { + return maxBlockNumber; + } + + public GenesisConfig getGenesisConfig() { + return genesisConfig; + } + + public KeyValueStorage getKvStore() { + return kvStore; + } + + public MutableBlockchain getBlockchain() { + return blockchain; + } + + public ProtocolContext getProtocolContext() { + return protocolContext; + } + + public ProtocolSchedule getProtocolSchedule() { + return protocolSchedule; + } + + public WorldStateArchive getWorldArchive() { + return worldArchive; + } + + private void importBlocks(final List blocks) { + for (final Block block : blocks) { + if (block.getHeader().getNumber() == BlockHeader.GENESIS_BLOCK_NUMBER) { + continue; + } + final ProtocolSpec protocolSpec = + protocolSchedule.getByBlockNumber(block.getHeader().getNumber()); + final BlockImporter blockImporter = protocolSpec.getBlockImporter(); + final boolean result = + blockImporter.importBlock(protocolContext, block, HeaderValidationMode.FULL); + if (!result) { + throw new IllegalStateException("Unable to import block " + block.getHeader().getNumber()); + } + } + this.maxBlockNumber = blockchain.getChainHeadBlockNumber(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/PeerMessageTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/PeerMessageTaskTest.java new file mode 100755 index 00000000000..59a448b9152 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/PeerMessageTaskTest.java @@ -0,0 +1,141 @@ +package net.consensys.pantheon.ethereum.eth.manager.ethtaskutils; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.EthTaskException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.EthTaskException.FailureReason; +import net.consensys.pantheon.util.ExceptionUtils; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +/** + * Tests ethTasks that interact with a single peer to retrieve data from the network. + * + * @param The type of data being retrieved. + */ +public abstract class PeerMessageTaskTest extends AbstractMessageTaskTest> { + @Test + public void completesWhenPeerReturnsPartialResult() + throws ExecutionException, InterruptedException { + // Setup a partially responsive peer + final Responder responder = RespondingEthPeer.partialResponder(blockchain, protocolSchedule); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); + + // Execute task and wait for response + final AtomicReference actualResult = new AtomicReference<>(); + final AtomicReference actualPeer = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + final T requestedData = generateDataToBeRequested(); + final EthTask> task = createTask(requestedData); + final CompletableFuture> future = task.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + future.whenComplete( + (response, error) -> { + actualResult.set(response.getResult()); + actualPeer.set(response.getPeer()); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertPartialResultMatchesExpectation(requestedData, actualResult.get()); + assertThat(actualPeer.get()).isEqualTo(respondingEthPeer.getEthPeer()); + } + + @Test + public void failsWhenNoPeersAreAvailable() throws ExecutionException, InterruptedException { + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task + final EthTask> task = createTask(requestedData); + final CompletableFuture> future = task.run(); + final AtomicReference failure = new AtomicReference<>(); + future.whenComplete( + (r, t) -> { + failure.set(t); + }); + + assertThat(future.isCompletedExceptionally()).isTrue(); + assertThat(failure.get()).isNotNull(); + // Check wrapped failure + final Throwable error = ExceptionUtils.rootCause(failure.get()); + assertThat(error).isInstanceOf(EthTaskException.class); + final EthTaskException ethException = (EthTaskException) error; + assertThat(ethException.reason()).isEqualTo(FailureReason.NO_AVAILABLE_PEERS); + + assertThat(task.run().isCompletedExceptionally()).isTrue(); + task.cancel(); + assertThat(task.run().isCompletedExceptionally()).isTrue(); + } + + @Test + public void completesWhenPeersSendEmptyResponses() { + // Setup a unresponsive peer + final Responder responder = RespondingEthPeer.emptyResponder(); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); + + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final EthTask> task = createTask(requestedData); + final CompletableFuture> future = task.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + future.whenComplete( + (response, error) -> { + done.compareAndSet(false, true); + }); + assertThat(future.isDone()).isTrue(); + assertThat(future.isCompletedExceptionally()).isFalse(); + } + + @Test + public void recordsTimeoutAgainstPeerWhenTaskTimesOut() { + peersDoTimeout.set(true); + // Setup a unresponsive peer + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); + + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final EthTask> task = createTask(requestedData); + final CompletableFuture> future = task.run(); + + assertThat(future.isCompletedExceptionally()).isTrue(); + assertThat( + respondingEthPeer + .getEthPeer() + .timeoutCounts() + .values() + .stream() + .mapToInt(AtomicInteger::get) + .sum()) + .isEqualTo(1); + } + + @Override + protected void assertResultMatchesExpectation( + final T requestedData, final PeerTaskResult response, final EthPeer respondingPeer) { + assertThat(response.getResult()).isEqualTo(requestedData); + assertThat(response.getPeer()).isEqualTo(respondingPeer); + } + + protected abstract void assertPartialResultMatchesExpectation(T requestedData, T partialResponse); +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskTest.java new file mode 100755 index 00000000000..f260ba44a17 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskTest.java @@ -0,0 +1,143 @@ +package net.consensys.pantheon.ethereum.eth.manager.ethtaskutils; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +/** + * Tests ethTasks that request data from the network, and retry until all of the data is received. + * + * @param + */ +public abstract class RetryingMessageTaskTest extends AbstractMessageTaskTest { + + @Test + public void doesNotCompleteWhenPeerReturnsPartialResult() + throws ExecutionException, InterruptedException { + // Setup data to be requested and expected response + + // Setup a partially responsive peer + final Responder responder = RespondingEthPeer.partialResponder(blockchain, protocolSchedule); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final T requestedData = generateDataToBeRequested(); + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + respondingPeer.respondTimes(responder, 20); + future.whenComplete( + (result, error) -> { + done.compareAndSet(false, true); + }); + + assertThat(done).isFalse(); + } + + @Test + public void doesNotCompleteWhenPeersAreUnavailable() + throws ExecutionException, InterruptedException { + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + done.compareAndSet(false, true); + }); + + assertThat(done).isFalse(); + } + + @Test + public void completesWhenPeersAreTemporarilyUnavailable() + throws ExecutionException, InterruptedException, TimeoutException { + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final AtomicReference actualResult = new AtomicReference<>(); + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isFalse(); + + // Setup a peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + respondingPeer.respondWhile(responder, () -> !future.isDone()); + + assertResultMatchesExpectation(requestedData, actualResult.get(), respondingPeer.getEthPeer()); + } + + @Test + public void completeWhenPeersTimeoutTemporarily() + throws ExecutionException, InterruptedException, TimeoutException { + peerCountToTimeout.set(1); + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final AtomicReference actualResult = new AtomicReference<>(); + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isFalse(); + respondingPeer.respondWhile(responder, () -> !future.isDone()); + + assertResultMatchesExpectation(requestedData, actualResult.get(), respondingPeer.getEthPeer()); + } + + @Test + public void doesNotCompleteWhenPeersSendEmptyResponses() + throws ExecutionException, InterruptedException { + // Setup a unresponsive peer + final Responder responder = RespondingEthPeer.emptyResponder(); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Setup data to be requested + final T requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final EthTask task = createTask(requestedData); + final CompletableFuture future = task.run(); + respondingPeer.respondTimes(responder, 20); + future.whenComplete( + (response, error) -> { + done.compareAndSet(false, true); + }); + assertThat(future.isDone()).isFalse(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskWithResultsTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskWithResultsTest.java new file mode 100755 index 00000000000..e4cc18e80ad --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/manager/ethtaskutils/RetryingMessageTaskWithResultsTest.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.ethereum.eth.manager.ethtaskutils; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; + +public abstract class RetryingMessageTaskWithResultsTest extends RetryingMessageTaskTest { + + @Override + protected void assertResultMatchesExpectation( + final T requestedData, final T response, final EthPeer respondingPeer) { + assertThat(response).isEqualTo(requestedData); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessageTest.java new file mode 100755 index 00000000000..b5ea299a374 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockBodiesMessageTest.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.development.DevelopmentProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.common.io.Resources; +import io.netty.buffer.ByteBuf; +import io.vertx.core.json.JsonObject; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +/** Tests for {@link BlockBodiesMessage}. */ +public final class BlockBodiesMessageTest { + + @Test + public void blockBodiesRoundTrip() throws IOException { + final List bodies = new ArrayList<>(); + final ByteBuffer buffer = + ByteBuffer.wrap(Resources.toByteArray(Resources.getResource("50.blocks"))); + for (int i = 0; i < 50; ++i) { + final byte[] block = new byte[RlpUtils.decodeLength(buffer, 0)]; + buffer.get(block); + buffer.compact().position(0); + final RLPInput oneBlock = new BytesValueRLPInput(BytesValue.wrap(block), false); + oneBlock.enterList(); + // We don't care about the header, just the body + oneBlock.skipNext(); + bodies.add( + // We know the test data to only contain Frontier blocks + new BlockBody( + oneBlock.readList(Transaction::readFrom), + oneBlock.readList( + rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash)))); + } + final MessageData initialMessage = BlockBodiesMessage.create(bodies); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV62.BLOCK_BODIES, rawBuffer); + final BlockBodiesMessage message = BlockBodiesMessage.readFrom(raw); + try { + final Iterator readBodies = + message.bodies(DevelopmentProtocolSchedule.create(new JsonObject())).iterator(); + for (int i = 0; i < 50; ++i) { + Assertions.assertThat(readBodies.next()).isEqualTo(bodies.get(i)); + } + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessageTest.java new file mode 100755 index 00000000000..1f19cc35a8a --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/BlockHeadersMessageTest.java @@ -0,0 +1,62 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.development.DevelopmentProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.common.io.Resources; +import io.netty.buffer.ByteBuf; +import io.vertx.core.json.JsonObject; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +/** Tests for {@link BlockHeadersMessage}. */ +public final class BlockHeadersMessageTest { + + @Test + public void blockHeadersRoundTrip() throws IOException { + final List headers = new ArrayList<>(); + final ByteBuffer buffer = + ByteBuffer.wrap(Resources.toByteArray(Resources.getResource("50.blocks"))); + for (int i = 0; i < 50; ++i) { + final byte[] block = new byte[RlpUtils.decodeLength(buffer, 0)]; + buffer.get(block); + buffer.compact().position(0); + final RLPInput oneBlock = new BytesValueRLPInput(BytesValue.wrap(block), false); + oneBlock.enterList(); + headers.add(BlockHeader.readFrom(oneBlock, MainnetBlockHashFunction::createHash)); + // We don't care about the bodies, just the headers + oneBlock.skipNext(); + oneBlock.skipNext(); + } + final MessageData initialMessage = BlockHeadersMessage.create(headers); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV62.BLOCK_HEADERS, rawBuffer); + final BlockHeadersMessage message = BlockHeadersMessage.readFrom(raw); + try { + final Iterator readHeaders = + message.getHeaders(DevelopmentProtocolSchedule.create(new JsonObject())); + for (int i = 0; i < 50; ++i) { + Assertions.assertThat(readHeaders.next()).isEqualTo(headers.get(i)); + } + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessageTest.java new file mode 100755 index 00000000000..e9e2e904b6d --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockBodiesMessageTest.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.common.io.Resources; +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +/** Tests for {@link GetBlockBodiesMessage}. */ +public final class GetBlockBodiesMessageTest { + + @Test + public void getBlockBodiesRoundTrip() throws IOException { + final List hashes = new ArrayList<>(); + final ByteBuffer buffer = + ByteBuffer.wrap(Resources.toByteArray(Resources.getResource("50.blocks"))); + for (int i = 0; i < 50; ++i) { + final byte[] block = new byte[RlpUtils.decodeLength(buffer, 0)]; + buffer.get(block); + buffer.compact().position(0); + final RLPInput oneBlock = new BytesValueRLPInput(BytesValue.wrap(block), false); + oneBlock.enterList(); + hashes.add(BlockHeader.readFrom(oneBlock, MainnetBlockHashFunction::createHash).getHash()); + // We don't care about the bodies, just the headers + oneBlock.skipNext(); + oneBlock.skipNext(); + } + final MessageData initialMessage = GetBlockBodiesMessage.create(hashes); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV62.GET_BLOCK_BODIES, rawBuffer); + final GetBlockBodiesMessage message = GetBlockBodiesMessage.readFrom(raw); + try { + final Iterator readHeaders = message.hashes().iterator(); + for (int i = 0; i < 50; ++i) { + Assertions.assertThat(readHeaders.next()).isEqualTo(hashes.get(i)); + } + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessageTest.java new file mode 100755 index 00000000000..27d3286cfa7 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetBlockHeadersMessageTest.java @@ -0,0 +1,67 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; + +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public final class GetBlockHeadersMessageTest { + + @Test + public void roundTripWithHash() { + for (final boolean reverse : Arrays.asList(true, false)) { + final Hash hash = Hash.hash(BytesValue.wrap(new byte[10])); + final int skip = 10; + final int maxHeaders = 128; + final GetBlockHeadersMessage initialMessage = + GetBlockHeadersMessage.create(hash, maxHeaders, reverse, skip); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV62.GET_BLOCK_HEADERS, rawBuffer); + final GetBlockHeadersMessage message = GetBlockHeadersMessage.readFrom(raw); + try { + Assertions.assertThat(message.blockNumber()).isEmpty(); + Assertions.assertThat(message.hash().get()).isEqualTo(hash); + Assertions.assertThat(message.reverse()).isEqualTo(reverse); + Assertions.assertThat(message.skip()).isEqualTo(skip); + Assertions.assertThat(message.maxHeaders()).isEqualTo(maxHeaders); + } finally { + initialMessage.release(); + raw.release(); + message.release(); + } + } + } + + @Test + public void roundTripBlockNum() { + for (final boolean reverse : Arrays.asList(true, false)) { + final long blockNum = 1000L; + final int skip = 10; + final int maxHeaders = 128; + final GetBlockHeadersMessage initialMessage = + GetBlockHeadersMessage.create(blockNum, maxHeaders, reverse, skip); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + final MessageData raw = new RawMessage(EthPV62.GET_BLOCK_HEADERS, rawBuffer); + final GetBlockHeadersMessage message = GetBlockHeadersMessage.readFrom(raw); + try { + Assertions.assertThat(initialMessage.blockNumber().getAsLong()).isEqualTo(blockNum); + Assertions.assertThat(initialMessage.hash()).isEmpty(); + Assertions.assertThat(initialMessage.reverse()).isEqualTo(reverse); + Assertions.assertThat(initialMessage.skip()).isEqualTo(skip); + Assertions.assertThat(initialMessage.maxHeaders()).isEqualTo(maxHeaders); + } finally { + initialMessage.release(); + raw.release(); + message.release(); + } + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessageTest.java new file mode 100755 index 00000000000..ff6391dc3da --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetNodeDataMessageTest.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public final class GetNodeDataMessageTest { + + @Test + public void roundTripTest() throws IOException { + // Generate some hashes + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List hashes = new ArrayList<>(); + final int hashCount = 20; + for (int i = 0; i < hashCount; ++i) { + hashes.add(gen.hash()); + } + + // Perform round-trip transformation + // Create GetNodeData, copy it to a generic message, then read back into a GetNodeData message + final MessageData initialMessage = GetNodeDataMessage.create(hashes); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV63.GET_NODE_DATA, rawBuffer); + final GetNodeDataMessage message = GetNodeDataMessage.readFrom(raw); + + // Read hashes back out after round trip and check they match originals. + try { + final Iterator readData = message.hashes().iterator(); + for (int i = 0; i < hashCount; ++i) { + Assertions.assertThat(readData.next()).isEqualTo(hashes.get(i)); + } + Assertions.assertThat(readData.hasNext()).isFalse(); + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessageTest.java new file mode 100755 index 00000000000..fa991ed0d39 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/GetReceiptsMessageTest.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public final class GetReceiptsMessageTest { + + @Test + public void roundTripTest() throws IOException { + // Generate some hashes + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List hashes = new ArrayList<>(); + final int hashCount = 20; + for (int i = 0; i < hashCount; ++i) { + hashes.add(gen.hash()); + } + + // Perform round-trip transformation + // Create GetReceipts message, copy it to a generic message, then read back into a GetReceipts + // message + final MessageData initialMessage = GetReceiptsMessage.create(hashes); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV63.GET_RECEIPTS, rawBuffer); + final GetReceiptsMessage message = GetReceiptsMessage.readFrom(raw); + + // Read hashes back out after round trip and check they match originals. + try { + final Iterator readData = message.hashes().iterator(); + for (int i = 0; i < hashCount; ++i) { + Assertions.assertThat(readData.next()).isEqualTo(hashes.get(i)); + } + Assertions.assertThat(readData.hasNext()).isFalse(); + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessageTest.java new file mode 100755 index 00000000000..4457add3534 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockHashesMessageTest.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.common.io.Resources; +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +/** Tests for {@link NewBlockHashesMessage}. */ +public final class NewBlockHashesMessageTest { + + @Test + public void blockHeadersRoundTrip() throws IOException { + final List hashes = new ArrayList<>(); + final ByteBuffer buffer = + ByteBuffer.wrap(Resources.toByteArray(Resources.getResource("50.blocks"))); + for (int i = 0; i < 50; ++i) { + final byte[] block = new byte[RlpUtils.decodeLength(buffer, 0)]; + buffer.get(block); + buffer.compact().position(0); + final RLPInput oneBlock = new BytesValueRLPInput(BytesValue.wrap(block), false); + oneBlock.enterList(); + final BlockHeader header = + BlockHeader.readFrom(oneBlock, MainnetBlockHashFunction::createHash); + hashes.add(new NewBlockHashesMessage.NewBlockHash(header.getHash(), header.getNumber())); + // We don't care about the bodies, just the header hashes + oneBlock.skipNext(); + oneBlock.skipNext(); + } + final MessageData initialMessage = NewBlockHashesMessage.create(hashes); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV62.NEW_BLOCK_HASHES, rawBuffer); + final NewBlockHashesMessage message = NewBlockHashesMessage.readFrom(raw); + try { + final Iterator readHeaders = message.getNewHashes(); + for (int i = 0; i < 50; ++i) { + Assertions.assertThat(readHeaders.next()).isEqualTo(hashes.get(i)); + } + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessageTest.java new file mode 100755 index 00000000000..10491d2197a --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NewBlockMessageTest.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import io.netty.buffer.Unpooled; +import org.junit.Test; + +public class NewBlockMessageTest { + private static final ProtocolSchedule protocolSchedule = MainnetProtocolSchedule.create(); + + @Test + public void roundTripNewBlockMessage() { + final UInt256 totalDifficulty = UInt256.of(98765); + final BlockDataGenerator blockGenerator = new BlockDataGenerator(); + final Block blockForInsertion = blockGenerator.block(); + + final NewBlockMessage msg = NewBlockMessage.create(blockForInsertion, totalDifficulty); + assertThat(msg.getCode()).isEqualTo(EthPV62.NEW_BLOCK); + assertThat(msg.totalDifficulty(protocolSchedule)).isEqualTo(totalDifficulty); + final Block extractedBlock = msg.block(protocolSchedule); + assertThat(extractedBlock).isEqualTo(blockForInsertion); + } + + @Test + public void rawMessageUpCastsToANewBlockMessage() { + final UInt256 totalDifficulty = UInt256.of(12345); + final BlockDataGenerator blockGenerator = new BlockDataGenerator(); + final Block blockForInsertion = blockGenerator.block(); + + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + blockForInsertion.writeTo(tmp); + tmp.writeUInt256Scalar(totalDifficulty); + tmp.endList(); + + final BytesValue msgPayload = tmp.encoded(); + + final RawMessage rawMsg = + new RawMessage(EthPV62.NEW_BLOCK, Unpooled.wrappedBuffer(tmp.encoded().extractArray())); + + final NewBlockMessage newBlockMsg = NewBlockMessage.readFrom(rawMsg); + + assertThat(newBlockMsg.getCode()).isEqualTo(EthPV62.NEW_BLOCK); + assertThat(newBlockMsg.totalDifficulty(protocolSchedule)).isEqualTo(totalDifficulty); + final Block extractedBlock = newBlockMsg.block(protocolSchedule); + assertThat(extractedBlock).isEqualTo(blockForInsertion); + } + + @Test + public void readFromMessageWithWrongCodeThrows() { + final ProtocolSchedule protSchedule = MainnetProtocolSchedule.create(); + final RawMessage rawMsg = new RawMessage(EthPV62.BLOCK_HEADERS, NetworkMemoryPool.allocate(1)); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> NewBlockMessage.readFrom(rawMsg)); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessageTest.java new file mode 100755 index 00000000000..bbb7807a111 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/NodeDataMessageTest.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public final class NodeDataMessageTest { + + @Test + public void roundTripTest() throws IOException { + // Generate some data + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List nodeData = new ArrayList<>(); + final int nodeCount = 20; + for (int i = 0; i < nodeCount; ++i) { + nodeData.add(gen.bytesValue()); + } + + // Perform round-trip transformation + // Create specific message, copy it to a generic message, then read back into a specific format + final MessageData initialMessage = NodeDataMessage.create(nodeData); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV63.NODE_DATA, rawBuffer); + final NodeDataMessage message = NodeDataMessage.readFrom(raw); + + // Read data back out after round trip and check they match originals. + try { + final Iterator readData = message.nodeData().iterator(); + for (int i = 0; i < nodeCount; ++i) { + Assertions.assertThat(readData.next()).isEqualTo(nodeData.get(i)); + } + Assertions.assertThat(readData.hasNext()).isFalse(); + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessageTest.java new file mode 100755 index 00000000000..b16715dd58c --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/ReceiptsMessageTest.java @@ -0,0 +1,56 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public final class ReceiptsMessageTest { + + @Test + public void roundTripTest() throws IOException { + // Generate some data + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List> receipts = new ArrayList<>(); + final int dataCount = 20; + final int receiptsPerSet = 3; + for (int i = 0; i < dataCount; ++i) { + final List receiptSet = new ArrayList<>(); + for (int j = 0; j < receiptsPerSet; j++) { + receiptSet.add(gen.receipt()); + } + receipts.add(receiptSet); + } + + // Perform round-trip transformation + // Create specific message, copy it to a generic message, then read back into a specific format + final MessageData initialMessage = ReceiptsMessage.create(receipts); + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV63.RECEIPTS, rawBuffer); + final ReceiptsMessage message = ReceiptsMessage.readFrom(raw); + + // Read data back out after round trip and check they match originals. + try { + final Iterator> readData = message.receipts().iterator(); + for (int i = 0; i < dataCount; ++i) { + Assertions.assertThat(readData.next()).isEqualTo(receipts.get(i)); + } + Assertions.assertThat(readData.hasNext()).isFalse(); + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessageTest.java new file mode 100755 index 00000000000..838643b3e48 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/StatusMessageTest.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.EthProtocol.EthVersion; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Random; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Test; + +public class StatusMessageTest { + + @Test + public void getters() { + final int version = EthVersion.V62; + final int networkId = 1; + final UInt256 td = UInt256.of(1000L); + final Hash bestHash = randHash(1L); + final Hash genesisHash = randHash(2L); + + final StatusMessage msg = StatusMessage.create(version, networkId, td, bestHash, genesisHash); + + assertThat(msg.protocolVersion()).isEqualTo(version); + assertThat(msg.networkId()).isEqualTo(networkId); + assertThat(msg.totalDifficulty()).isEqualTo(td); + assertThat(msg.bestHash()).isEqualTo(bestHash); + assertThat(msg.genesisHash()).isEqualTo(genesisHash); + } + + @Test + public void serializeDeserialize() { + final int version = EthVersion.V62; + final int networkId = 1; + final UInt256 td = UInt256.of(1000L); + final Hash bestHash = randHash(1L); + final Hash genesisHash = randHash(2L); + + final MessageData msg = StatusMessage.create(version, networkId, td, bestHash, genesisHash); + + // Make a message copy from serialized data and check deserialized results + final ByteBuf buffer = Unpooled.buffer(msg.getSize(), msg.getSize()); + msg.writeTo(buffer); + final StatusMessage copy = new StatusMessage(buffer); + + assertThat(copy.protocolVersion()).isEqualTo(version); + assertThat(copy.networkId()).isEqualTo(networkId); + assertThat(copy.totalDifficulty()).isEqualTo(td); + assertThat(copy.bestHash()).isEqualTo(bestHash); + assertThat(copy.genesisHash()).isEqualTo(genesisHash); + } + + private Hash randHash(final long seed) { + final Random random = new Random(seed); + final byte[] bytes = new byte[32]; + random.nextBytes(bytes); + return Hash.wrap(Bytes32.wrap(bytes)); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessageTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessageTest.java new file mode 100755 index 00000000000..c052e104f68 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/messages/TransactionsMessageTest.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.eth.messages; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class TransactionsMessageTest { + + @Test + public void transactionRoundTrip() throws IOException { + // Setup list of transactions + final int txCount = 20; + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List transactions = new ArrayList<>(); + for (int i = 0; i < txCount; ++i) { + transactions.add(gen.transaction()); + } + + // Create TransactionsMessage + final MessageData initialMessage = TransactionsMessage.create(transactions); + // Read message into a generic RawMessage + final ByteBuf rawBuffer = NetworkMemoryPool.allocate(initialMessage.getSize()); + initialMessage.writeTo(rawBuffer); + final MessageData raw = new RawMessage(EthPV62.TRANSACTIONS, rawBuffer); + // Transform back to a TransactionsMessage from RawMessage + final TransactionsMessage message = TransactionsMessage.readFrom(raw); + + // Check that transactions match original inputs after transformations + try { + final Iterator readTransactions = message.transactions(Transaction::readFrom); + for (int i = 0; i < txCount; ++i) { + Assertions.assertThat(readTransactions.next()).isEqualTo(transactions.get(i)); + } + Assertions.assertThat(readTransactions.hasNext()).isFalse(); + } finally { + message.release(); + initialMessage.release(); + raw.release(); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManagerTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManagerTest.java new file mode 100755 index 00000000000..2dd98c15128 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/BlockPropagationManagerTest.java @@ -0,0 +1,511 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.BlockchainSetupUtil; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockHashesMessage; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockHashesMessage.NewBlockHash; +import net.consensys.pantheon.ethereum.eth.messages.NewBlockMessage; +import net.consensys.pantheon.ethereum.eth.sync.state.PendingBlocks; +import net.consensys.pantheon.ethereum.eth.sync.state.SyncState; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator.BlockOptions; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class BlockPropagationManagerTest { + + private static Blockchain fullBlockchain; + + private BlockchainSetupUtil blockchainUtil; + private ProtocolSchedule protocolSchedule; + private ProtocolContext protocolContext; + private MutableBlockchain blockchain; + private EthProtocolManager ethProtocolManager; + private BlockPropagationManager blockPropagationManager; + private SynchronizerConfiguration syncConfig; + private SyncState syncState; + + @BeforeClass + public static void setupSuite() { + fullBlockchain = BlockchainSetupUtil.forTesting().importAllBlocks(); + } + + @Before + public void setup() { + blockchainUtil = BlockchainSetupUtil.forTesting(); + blockchain = spy(blockchainUtil.getBlockchain()); + protocolSchedule = blockchainUtil.getProtocolSchedule(); + final ProtocolContext tempProtocolContext = blockchainUtil.getProtocolContext(); + protocolContext = + new ProtocolContext<>( + blockchain, + tempProtocolContext.getWorldStateArchive(), + tempProtocolContext.getConsensusState()); + ethProtocolManager = EthProtocolManagerTestUtil.create(blockchain); + syncConfig = + SynchronizerConfiguration.builder() + .blockPropagationRange(-3, 5) + .build() + .validated(blockchain); + syncState = new SyncState(blockchain, ethProtocolManager.ethContext(), new PendingBlocks()); + blockPropagationManager = + new BlockPropagationManager<>( + syncConfig, + protocolSchedule, + protocolContext, + ethProtocolManager.ethContext(), + syncState); + } + + @Test + public void importsAnnouncedBlocks_aheadOfChainInOrder() { + blockchainUtil.importFirstBlocks(2); + final Block nextBlock = blockchainUtil.getBlock(2); + final Block nextNextBlock = blockchainUtil.getBlock(3); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockHashesMessage nextAnnouncement = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(nextBlock.getHash(), nextBlock.getHeader().getNumber()))); + final NewBlockHashesMessage nextNextAnnouncement = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(nextNextBlock.getHash(), nextNextBlock.getHeader().getNumber()))); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + + // Broadcast first message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + // Broadcast second message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextNextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isTrue(); + } + + @Test + public void importsAnnouncedBlocks_aheadOfChainOutOfOrder() { + blockchainUtil.importFirstBlocks(2); + final Block nextBlock = blockchainUtil.getBlock(2); + final Block nextNextBlock = blockchainUtil.getBlock(3); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockHashesMessage nextAnnouncement = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(nextBlock.getHash(), nextBlock.getHeader().getNumber()))); + final NewBlockHashesMessage nextNextAnnouncement = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(nextNextBlock.getHash(), nextNextBlock.getHeader().getNumber()))); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + + // Broadcast second message first + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextNextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + // Broadcast first message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isTrue(); + } + + @Test + public void importsAnnouncedNewBlocks_aheadOfChainInOrder() { + blockchainUtil.importFirstBlocks(2); + final Block nextBlock = blockchainUtil.getBlock(2); + final Block nextNextBlock = blockchainUtil.getBlock(3); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockMessage nextAnnouncement = + NewBlockMessage.create( + nextBlock, fullBlockchain.getTotalDifficultyByHash(nextBlock.getHash()).get()); + final NewBlockMessage nextNextAnnouncement = + NewBlockMessage.create( + nextNextBlock, fullBlockchain.getTotalDifficultyByHash(nextNextBlock.getHash()).get()); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + + // Broadcast first message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + // Broadcast second message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextNextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isTrue(); + } + + @Test + public void importsAnnouncedNewBlocks_aheadOfChainOutOfOrder() { + blockchainUtil.importFirstBlocks(2); + final Block nextBlock = blockchainUtil.getBlock(2); + final Block nextNextBlock = blockchainUtil.getBlock(3); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockMessage nextAnnouncement = + NewBlockMessage.create( + nextBlock, fullBlockchain.getTotalDifficultyByHash(nextBlock.getHash()).get()); + final NewBlockMessage nextNextAnnouncement = + NewBlockMessage.create( + nextNextBlock, fullBlockchain.getTotalDifficultyByHash(nextNextBlock.getHash()).get()); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + + // Broadcast second message first + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextNextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + // Broadcast first message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextAnnouncement); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + assertThat(blockchain.contains(nextNextBlock.getHash())).isTrue(); + } + + @Test + public void importsMixedOutOfOrderMessages() { + blockchainUtil.importFirstBlocks(2); + final Block block1 = blockchainUtil.getBlock(2); + final Block block2 = blockchainUtil.getBlock(3); + final Block block3 = blockchainUtil.getBlock(4); + final Block block4 = blockchainUtil.getBlock(5); + + // Sanity check + assertThat(blockchain.contains(block1.getHash())).isFalse(); + assertThat(blockchain.contains(block2.getHash())).isFalse(); + assertThat(blockchain.contains(block3.getHash())).isFalse(); + assertThat(blockchain.contains(block4.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockHashesMessage block1Msg = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(block1.getHash(), block1.getHeader().getNumber()))); + final NewBlockMessage block2Msg = + NewBlockMessage.create( + block2, fullBlockchain.getTotalDifficultyByHash(block2.getHash()).get()); + final NewBlockHashesMessage block3Msg = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(block3.getHash(), block3.getHeader().getNumber()))); + final NewBlockMessage block4Msg = + NewBlockMessage.create( + block4, fullBlockchain.getTotalDifficultyByHash(block4.getHash()).get()); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + + // Broadcast older blocks + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, block3Msg); + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, block4Msg); + peer.respondWhile(responder, peer::hasOutstandingRequests); + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, block2Msg); + peer.respondWhile(responder, peer::hasOutstandingRequests); + // Broadcast first block + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, block1Msg); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(block1.getHash())).isTrue(); + assertThat(blockchain.contains(block2.getHash())).isTrue(); + assertThat(blockchain.contains(block3.getHash())).isTrue(); + assertThat(blockchain.contains(block4.getHash())).isTrue(); + } + + @Test + public void handlesDuplicateAnnouncements() { + blockchainUtil.importFirstBlocks(2); + final Block nextBlock = blockchainUtil.getBlock(2); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockHashesMessage newBlockHash = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(nextBlock.getHash(), nextBlock.getHeader().getNumber()))); + final NewBlockMessage newBlock = + NewBlockMessage.create( + nextBlock, fullBlockchain.getTotalDifficultyByHash(nextBlock.getHash()).get()); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + + // Broadcast first message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, newBlock); + peer.respondWhile(responder, peer::hasOutstandingRequests); + // Broadcast duplicate + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, newBlockHash); + peer.respondWhile(responder, peer::hasOutstandingRequests); + // Broadcast duplicate + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, newBlock); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + verify(blockchain, times(1)).appendBlock(any(), any()); + } + + @Test + public void handlesPendingDuplicateAnnouncements() { + blockchainUtil.importFirstBlocks(2); + final Block nextBlock = blockchainUtil.getBlock(2); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockHashesMessage newBlockHash = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(nextBlock.getHash(), nextBlock.getHeader().getNumber()))); + final NewBlockMessage newBlock = + NewBlockMessage.create( + nextBlock, fullBlockchain.getTotalDifficultyByHash(nextBlock.getHash()).get()); + + // Broadcast messages + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, newBlock); + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, newBlockHash); + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, newBlock); + // Respond + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + verify(blockchain, times(1)).appendBlock(any(), any()); + } + + @Test + public void ignoresFutureNewBlockHashAnnouncement() { + blockchainUtil.importFirstBlocks(2); + final Block futureBlock = blockchainUtil.getBlock(11); + + // Sanity check + assertThat(blockchain.contains(futureBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockHashesMessage futureAnnouncement = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(futureBlock.getHash(), futureBlock.getHeader().getNumber()))); + + // Broadcast + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, futureAnnouncement); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(futureBlock.getHash())).isFalse(); + } + + @Test + public void ignoresFutureNewBlockAnnouncement() { + blockchainUtil.importFirstBlocks(2); + final Block futureBlock = blockchainUtil.getBlock(11); + + // Sanity check + assertThat(blockchain.contains(futureBlock.getHash())).isFalse(); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockMessage futureAnnouncement = + NewBlockMessage.create( + futureBlock, fullBlockchain.getTotalDifficultyByHash(futureBlock.getHash()).get()); + + // Broadcast + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, futureAnnouncement); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(blockchain.contains(futureBlock.getHash())).isFalse(); + } + + @Test + public void ignoresOldNewBlockHashAnnouncement() { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(10); + final Block blockOne = blockchainUtil.getBlock(1); + final Block oldBlock = gen.nextBlock(blockOne); + + // Sanity check + assertThat(blockchain.contains(oldBlock.getHash())).isFalse(); + + final BlockPropagationManager propManager = spy(blockPropagationManager); + propManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockHashesMessage oldAnnouncement = + NewBlockHashesMessage.create( + Collections.singletonList( + new NewBlockHash(oldBlock.getHash(), oldBlock.getHeader().getNumber()))); + + // Broadcast + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, oldAnnouncement); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + verify(propManager, times(0)).importOrSavePendingBlock(any()); + assertThat(blockchain.contains(oldBlock.getHash())).isFalse(); + } + + @Test + public void ignoresOldNewBlockAnnouncement() { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(10); + final Block blockOne = blockchainUtil.getBlock(1); + final Block oldBlock = gen.nextBlock(blockOne); + + // Sanity check + assertThat(blockchain.contains(oldBlock.getHash())).isFalse(); + + final BlockPropagationManager propManager = spy(blockPropagationManager); + propManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockMessage oldAnnouncement = NewBlockMessage.create(oldBlock, UInt256.ZERO); + + // Broadcast + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, oldAnnouncement); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + verify(propManager, times(0)).importOrSavePendingBlock(any()); + assertThat(blockchain.contains(oldBlock.getHash())).isFalse(); + } + + @Test + public void purgesOldBlocks() { + final int oldBlocksToImport = 3; + syncConfig = + SynchronizerConfiguration.builder() + .blockPropagationRange(-oldBlocksToImport, 5) + .build() + .validated(blockchain); + final BlockPropagationManager blockPropagationManager = + new BlockPropagationManager<>( + syncConfig, + protocolSchedule, + protocolContext, + ethProtocolManager.ethContext(), + syncState); + + final BlockDataGenerator gen = new BlockDataGenerator(); + // Import some blocks + blockchainUtil.importFirstBlocks(5); + // Set up test block next to head, that should eventually be purged + final Block blockToPurge = + gen.block(BlockOptions.create().setBlockNumber(blockchain.getChainHeadBlockNumber())); + + blockPropagationManager.start(); + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final NewBlockMessage blockAnnouncementMsg = NewBlockMessage.create(blockToPurge, UInt256.ZERO); + + // Broadcast + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, blockAnnouncementMsg); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + // Check that we pushed our block into the pending collection + assertThat(blockchain.contains(blockToPurge.getHash())).isFalse(); + assertThat(syncState.pendingBlocks().contains(blockToPurge.getHash())).isTrue(); + + // Import blocks until we bury the target block far enough to be cleaned up + for (int i = 0; i < oldBlocksToImport; i++) { + blockchainUtil.importBlockAtIndex((int) blockchain.getChainHeadBlockNumber() + 1); + + assertThat(blockchain.contains(blockToPurge.getHash())).isFalse(); + assertThat(syncState.pendingBlocks().contains(blockToPurge.getHash())).isTrue(); + } + + // Import again to trigger cleanup + blockchainUtil.importBlockAtIndex((int) blockchain.getChainHeadBlockNumber() + 1); + assertThat(blockchain.contains(blockToPurge.getHash())).isFalse(); + assertThat(syncState.pendingBlocks().contains(blockToPurge.getHash())).isFalse(); + } + + @Test + public void updatesChainHeadWhenNewBlockMessageReceived() { + blockchainUtil.importFirstBlocks(2); + final Block nextBlock = blockchainUtil.getBlock(2); + + blockPropagationManager.start(); + + // Setup peer and messages + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 0); + final UInt256 totalDifficulty = + fullBlockchain.getTotalDifficultyByHash(nextBlock.getHash()).get(); + final NewBlockMessage nextAnnouncement = NewBlockMessage.create(nextBlock, totalDifficulty); + + // Broadcast message + EthProtocolManagerTestUtil.broadcastMessage(ethProtocolManager, peer, nextAnnouncement); + final Responder responder = RespondingEthPeer.blockchainResponder(fullBlockchain); + peer.respondWhile(responder, peer::hasOutstandingRequests); + + assertThat(peer.getEthPeer().chainState().getBestBlock().getHash()) + .isEqualTo(nextBlock.getHash()); + assertThat(peer.getEthPeer().chainState().getEstimatedHeight()) + .isEqualTo(nextBlock.getHeader().getNumber()); + assertThat(peer.getEthPeer().chainState().getBestBlock().getTotalDifficulty()) + .isEqualTo(totalDifficulty); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTrackerTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTrackerTest.java new file mode 100755 index 00000000000..c84da731ff6 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/ChainHeadTrackerTest.java @@ -0,0 +1,83 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.ChainState; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.BlockchainSetupUtil; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.util.uint.UInt256; + +import org.junit.Test; + +public class ChainHeadTrackerTest { + + private final BlockchainSetupUtil blockchainSetupUtil = BlockchainSetupUtil.forTesting(); + private final MutableBlockchain blockchain = blockchainSetupUtil.getBlockchain(); + private final EthProtocolManager ethProtocolManager = + EthProtocolManagerTestUtil.create(blockchain); + private final RespondingEthPeer respondingPeer = + RespondingEthPeer.create( + ethProtocolManager, + blockchain.getChainHeadHash(), + blockchain.getChainHead().getTotalDifficulty(), + 0); + private final ProtocolSchedule protocolSchedule = + GenesisConfig.development().getProtocolSchedule(); + private final TrailingPeerLimiter trailingPeerLimiter = mock(TrailingPeerLimiter.class); + private final ChainHeadTracker chainHeadTracker = + new ChainHeadTracker(ethProtocolManager.ethContext(), protocolSchedule, trailingPeerLimiter); + + @Test + public void shouldRequestHeaderChainHeadWhenNewPeerConnects() { + final Responder responder = + RespondingEthPeer.blockchainResponder(blockchainSetupUtil.getBlockchain()); + chainHeadTracker.onPeerConnected(respondingPeer.getEthPeer()); + + assertThat(chainHeadState().getEstimatedHeight()).isZero(); + + respondingPeer.respond(responder); + + assertThat(chainHeadState().getEstimatedHeight()) + .isEqualTo(blockchain.getChainHeadBlockNumber()); + } + + @Test + public void shouldIgnoreHeadersIfChainHeadHasAlreadyBeenUpdatedWhileWaiting() { + final Responder responder = + RespondingEthPeer.blockchainResponder(blockchainSetupUtil.getBlockchain()); + chainHeadTracker.onPeerConnected(respondingPeer.getEthPeer()); + + // Change the hash of the current known head + respondingPeer.getEthPeer().chainState().statusReceived(Hash.EMPTY_TRIE_HASH, UInt256.ONE); + + respondingPeer.respond(responder); + + assertThat(chainHeadState().getEstimatedHeight()).isZero(); + } + + @Test + public void shouldCheckTrialingPeerLimits() { + final Responder responder = + RespondingEthPeer.blockchainResponder(blockchainSetupUtil.getBlockchain()); + chainHeadTracker.onPeerConnected(respondingPeer.getEthPeer()); + + assertThat(chainHeadState().getEstimatedHeight()).isZero(); + + respondingPeer.respond(responder); + + assertThat(chainHeadState().getEstimatedHeight()) + .isEqualTo(blockchain.getChainHeadBlockNumber()); + } + + private ChainState chainHeadState() { + return respondingPeer.getEthPeer().chainState(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/DownloaderTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/DownloaderTest.java new file mode 100755 index 00000000000..501d153b120 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/DownloaderTest.java @@ -0,0 +1,627 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.BlockchainSetupUtil; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.GetBlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.sync.state.PendingBlocks; +import net.consensys.pantheon.ethereum.eth.sync.state.SyncState; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import org.junit.Before; +import org.junit.Test; + +public class DownloaderTest { + + protected ProtocolSchedule protocolSchedule; + protected EthProtocolManager ethProtocolManager; + protected EthContext ethContext; + protected ProtocolContext protocolContext; + private SyncState syncState; + + private BlockDataGenerator gen; + private BlockchainSetupUtil localBlockchainSetup; + protected MutableBlockchain localBlockchain; + private BlockchainSetupUtil otherBlockchainSetup; + protected Blockchain otherBlockchain; + + @Before + public void setupTest() { + gen = new BlockDataGenerator(); + localBlockchainSetup = BlockchainSetupUtil.forTesting(); + localBlockchain = spy(localBlockchainSetup.getBlockchain()); + otherBlockchainSetup = BlockchainSetupUtil.forTesting(); + otherBlockchain = otherBlockchainSetup.getBlockchain(); + + protocolSchedule = localBlockchainSetup.getProtocolSchedule(); + protocolContext = localBlockchainSetup.getProtocolContext(); + ethProtocolManager = EthProtocolManagerTestUtil.create(localBlockchain); + ethContext = ethProtocolManager.ethContext(); + syncState = new SyncState(protocolContext.getBlockchain(), ethContext, new PendingBlocks()); + } + + private Downloader downloader(final SynchronizerConfiguration syncConfig) { + return new Downloader<>(syncConfig, protocolSchedule, protocolContext, ethContext, syncState); + } + + private Downloader downloader() { + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder().build().validated(localBlockchain); + return downloader(syncConfig); + } + + @Test + public void syncsToBetterChain_multipleSegments() { + otherBlockchainSetup.importFirstBlocks(15); + final long targetBlock = otherBlockchain.getChainHeadBlockNumber(); + // Sanity check + assertThat(targetBlock).isGreaterThan(localBlockchain.getChainHeadBlockNumber()); + + final RespondingEthPeer peer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(10) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + while (!syncState.syncTarget().isPresent()) { + peer.respond(responder); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peer.getEthPeer()); + + while (localBlockchain.getChainHeadBlockNumber() < targetBlock) { + peer.respond(responder); + } + + assertThat(localBlockchain.getChainHeadBlockNumber()).isEqualTo(targetBlock); + } + + @Test + public void syncsToBetterChain_singleSegment() { + otherBlockchainSetup.importFirstBlocks(5); + final long targetBlock = otherBlockchain.getChainHeadBlockNumber(); + // Sanity check + assertThat(targetBlock).isGreaterThan(localBlockchain.getChainHeadBlockNumber()); + + final RespondingEthPeer peer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(10) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + while (!syncState.syncTarget().isPresent()) { + peer.respond(responder); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peer.getEthPeer()); + + while (localBlockchain.getChainHeadBlockNumber() < targetBlock) { + peer.respond(responder); + } + + assertThat(localBlockchain.getChainHeadBlockNumber()).isEqualTo(targetBlock); + } + + @Test + public void syncsToBetterChain_singleSegmentOnBoundary() { + otherBlockchainSetup.importFirstBlocks(5); + final long targetBlock = otherBlockchain.getChainHeadBlockNumber(); + // Sanity check + assertThat(targetBlock).isGreaterThan(localBlockchain.getChainHeadBlockNumber()); + + final RespondingEthPeer peer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(4) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + while (!syncState.syncTarget().isPresent()) { + peer.respond(responder); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peer.getEthPeer()); + + while (localBlockchain.getChainHeadBlockNumber() < targetBlock) { + peer.respond(responder); + } + + assertThat(localBlockchain.getChainHeadBlockNumber()).isEqualTo(targetBlock); + } + + @Test + public void doesNotSyncToWorseChain() { + localBlockchainSetup.importFirstBlocks(15); + // Sanity check + assertThat(localBlockchain.getChainHeadBlockNumber()) + .isGreaterThan(BlockHeader.GENESIS_BLOCK_NUMBER); + + final RespondingEthPeer peer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + final Downloader downloader = downloader(); + downloader.start(); + + peer.respond(responder); + assertThat(syncState.syncTarget()).isNotPresent(); + + while (peer.hasOutstandingRequests()) { + peer.respond(responder); + } + + assertThat(syncState.syncTarget()).isNotPresent(); + verify(localBlockchain, times(0)).appendBlock(any(), any()); + } + + @Test + public void syncsToBetterChain_fromFork() { + otherBlockchainSetup.importFirstBlocks(15); + final long targetBlock = otherBlockchain.getChainHeadBlockNumber(); + + // Add divergent blocks to local chain + localBlockchainSetup.importFirstBlocks(3); + gen = new BlockDataGenerator(); + final Block chainHead = localBlockchain.getChainHeadBlock(); + final Block forkBlock = + gen.block(gen.nextBlockOptions(chainHead).setDifficulty(UInt256.of(0L))); + localBlockchain.appendBlock(forkBlock, gen.receipts(forkBlock)); + + // Sanity check + assertThat(targetBlock).isGreaterThan(localBlockchain.getChainHeadBlockNumber()); + assertThat(otherBlockchain.contains(localBlockchain.getChainHead().getHash())).isFalse(); + + final RespondingEthPeer peer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(10) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + while (localBlockchain.getChainHeadBlockNumber() < targetBlock) { + peer.respond(responder); + } + + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peer.getEthPeer()); + assertThat(localBlockchain.getChainHeadBlockNumber()).isEqualTo(targetBlock); + } + + @Test + public void choosesBestPeerAsSyncTarget_byTd() { + final UInt256 localTd = localBlockchain.getChainHead().getTotalDifficulty(); + + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + final RespondingEthPeer peerA = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(100)); + final RespondingEthPeer peerB = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(200)); + + final Downloader downloader = downloader(); + downloader.start(); + + // Process until the sync target is selected + while (!syncState.syncTarget().isPresent()) { + RespondingEthPeer.respondOnce(responder, peerA, peerB); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerB.getEthPeer()); + } + + @Test + public void choosesBestPeerAsSyncTarget_byTdAndHeight() { + final UInt256 localTd = localBlockchain.getChainHead().getTotalDifficulty(); + + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + final RespondingEthPeer peerA = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(100), 0); + peerA.getEthPeer().chainState().update(gen.hash(), 100); + final RespondingEthPeer peerB = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(200), 0); + peerA.getEthPeer().chainState().update(gen.hash(), 50); + + final Downloader downloader = downloader(); + downloader.start(); + + // Process until the sync target is selected + while (!syncState.syncTarget().isPresent()) { + RespondingEthPeer.respondOnce(responder, peerA, peerB); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerA.getEthPeer()); + } + + @Test + public void switchesSyncTarget_betterHeight() { + final UInt256 localTd = localBlockchain.getChainHead().getTotalDifficulty(); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + // Peer A is initially better + final RespondingEthPeer peerA = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(200), 50); + final RespondingEthPeer peerB = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(100), 50); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(5) + .downloaderChangeTargetThresholdByHeight(10) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + // Process until the sync target is selected + while (!syncState.syncTarget().isPresent()) { + peerA.respond(responder); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerA.getEthPeer()); + + // Update Peer B so that its a better target and send some responses to push logic forward + peerB.getEthPeer().chainState().update(gen.hash(), 100); + + // Process through first task cycle + final CompletableFuture firstTask = downloader.currentTask; + while (downloader.currentTask == firstTask) { + RespondingEthPeer.respondOnce(responder, peerA, peerB); + } + + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerB.getEthPeer()); + } + + @Test + public void doesNotSwitchSyncTarget_betterHeightUnderThreshold() { + otherBlockchainSetup.importFirstBlocks(8); + final UInt256 localTd = localBlockchain.getChainHead().getTotalDifficulty(); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + final RespondingEthPeer bestPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(200)); + final RespondingEthPeer otherPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(100)); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(5) + .downloaderChangeTargetThresholdByHeight(1000) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + // Process until the sync target is selected + while (!syncState.syncTarget().isPresent()) { + bestPeer.respond(responder); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(bestPeer.getEthPeer()); + + // Update otherPeer so that its a better target, but under the threshold to switch + otherPeer.getEthPeer().chainState().update(gen.hash(), 100); + + // Process through first task cycle + final CompletableFuture firstTask = downloader.currentTask; + while (downloader.currentTask == firstTask) { + RespondingEthPeer.respondOnce(responder, bestPeer, otherPeer); + } + + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(bestPeer.getEthPeer()); + } + + @Test + public void switchesSyncTarget_betterTd() { + final UInt256 localTd = localBlockchain.getChainHead().getTotalDifficulty(); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + // Peer A is initially better + final RespondingEthPeer peerA = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(200)); + final RespondingEthPeer peerB = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(100)); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(5) + .downloaderChangeTargetThresholdByTd(UInt256.of(10)) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + // Process until the sync target is selected + while (!syncState.syncTarget().isPresent()) { + peerA.respond(responder); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerA.getEthPeer()); + + // Update Peer B so that its a better target and send some responses to push logic forward + peerB + .getEthPeer() + .chainState() + .update(gen.header(), syncState.chainHeadTotalDifficulty().plus(300)); + + // Process through first task cycle + final CompletableFuture firstTask = downloader.currentTask; + while (downloader.currentTask == firstTask) { + RespondingEthPeer.respondOnce(responder, peerA, peerB); + } + + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(peerB.getEthPeer()); + } + + @Test + public void doesNotSwitchSyncTarget_betterTdUnderThreshold() { + final long localChainHeadAtStart = localBlockchain.getChainHeadBlockNumber(); + final UInt256 localTd = localBlockchain.getChainHead().getTotalDifficulty(); + otherBlockchainSetup.importFirstBlocks(8); + final Responder responder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + // Sanity check + assertThat(localChainHeadAtStart).isLessThan(otherBlockchain.getChainHeadBlockNumber()); + + final RespondingEthPeer bestPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(200)); + final RespondingEthPeer otherPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, localTd.plus(100)); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(5) + .downloaderChangeTargetThresholdByTd(UInt256.of(100_000_000L)) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + downloader.start(); + + // Process until the sync target is selected + while (!syncState.syncTarget().isPresent()) { + bestPeer.respond(responder); + } + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(bestPeer.getEthPeer()); + + // Update otherPeer so that its a better target and send some responses to push logic forward + bestPeer + .getEthPeer() + .chainState() + .update(gen.header(1000), syncState.chainHeadTotalDifficulty().plus(201)); + otherPeer + .getEthPeer() + .chainState() + .update(gen.header(1000), syncState.chainHeadTotalDifficulty().plus(300)); + + // Process through first task cycle + final CompletableFuture firstTask = downloader.currentTask; + while (downloader.currentTask == firstTask) { + RespondingEthPeer.respondOnce(responder, bestPeer, otherPeer); + } + + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(bestPeer.getEthPeer()); + } + + @Test + public void recoversFromSyncTargetDisconnect() { + localBlockchainSetup.importFirstBlocks(2); + final long localChainHeadAtStart = localBlockchain.getChainHeadBlockNumber(); + otherBlockchainSetup.importAllBlocks(); + final long targetBlock = otherBlockchain.getChainHeadBlockNumber(); + // Sanity check + assertThat(targetBlock).isGreaterThan(localBlockchain.getChainHeadBlockNumber()); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(5) + .downloaderHeadersRequestSize(3) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + + final long bestPeerChainHead = otherBlockchain.getChainHeadBlockNumber(); + final RespondingEthPeer bestPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); + final long secondBestPeerChainHead = bestPeerChainHead - 3; + final Blockchain shorterChain = createShortChain(otherBlockchain, secondBestPeerChainHead); + final RespondingEthPeer secondBestPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, shorterChain); + final Responder bestResponder = RespondingEthPeer.blockchainResponder(otherBlockchain); + final Responder secondBestResponder = RespondingEthPeer.blockchainResponder(shorterChain); + downloader.start(); + + // Process through sync target selection + await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + bestPeer.respond(bestResponder); + assertThat(syncState.syncTarget()).isNotEmpty(); + }); + + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(bestPeer.getEthPeer()); + + // The next message should be for checkpoint headers from the sync target + final Optional maybeNextMessage = bestPeer.peekNextOutgoingRequest(); + assertThat(maybeNextMessage).isPresent(); + final MessageData nextMessage = maybeNextMessage.get(); + assertThat(nextMessage.getCode()).isEqualTo(EthPV62.GET_BLOCK_HEADERS); + final GetBlockHeadersMessage headersMessage = GetBlockHeadersMessage.readFrom(nextMessage); + assertThat(headersMessage.skip()).isGreaterThan(0); + + // Process through the first import + await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + if (!bestPeer.respond(bestResponder)) { + secondBestPeer.respond(secondBestResponder); + } + assertThat(localBlockchain.getChainHeadBlockNumber()) + .isNotEqualTo(localChainHeadAtStart); + }); + + // Disconnect peer + ethProtocolManager.handleDisconnect( + bestPeer.getPeerConnection(), DisconnectReason.TOO_MANY_PEERS, true); + + // Downloader should recover and sync to next best peer + await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + secondBestPeer.respond(secondBestResponder); + assertThat(localBlockchain.getChainHeadBlockNumber()) + .isEqualTo(secondBestPeerChainHead); + }); + } + + @Test + public void requestsCheckpointsFromSyncTarget() { + localBlockchainSetup.importFirstBlocks(2); + otherBlockchainSetup.importAllBlocks(); + final long targetBlock = otherBlockchain.getChainHeadBlockNumber(); + // Sanity check + assertThat(targetBlock).isGreaterThan(localBlockchain.getChainHeadBlockNumber()); + + final SynchronizerConfiguration syncConfig = + SynchronizerConfiguration.builder() + .downloaderChainSegmentSize(5) + .downloaderHeadersRequestSize(3) + .build() + .validated(localBlockchain); + final Downloader downloader = downloader(syncConfig); + + // Setup the best peer we should use as our sync target + final long bestPeerChainHead = otherBlockchain.getChainHeadBlockNumber(); + final RespondingEthPeer bestPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, otherBlockchain); + final Responder bestResponder = RespondingEthPeer.blockchainResponder(otherBlockchain); + + // Create some other peers that are available to sync from + final int otherPeersCount = 5; + final List otherPeers = new ArrayList<>(otherPeersCount); + final long otherChainhead = bestPeerChainHead - 3; + final Blockchain shorterChain = createShortChain(otherBlockchain, otherChainhead); + final Responder otherResponder = RespondingEthPeer.blockchainResponder(shorterChain); + for (int i = 0; i < otherPeersCount; i++) { + final RespondingEthPeer otherPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, shorterChain); + otherPeers.add(otherPeer); + } + + downloader.start(); + + // Process through sync target selection + await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + bestPeer.respond(bestResponder); + assertThat(syncState.syncTarget()).isNotEmpty(); + }); + + assertThat(syncState.syncTarget()).isPresent(); + assertThat(syncState.syncTarget().get().peer()).isEqualTo(bestPeer.getEthPeer()); + + while (localBlockchain.getChainHeadBlockNumber() < bestPeerChainHead) { + // Check that any requests for checkpoint headers are only sent to the best peer + final long checkpointRequestsToOtherPeers = + otherPeers + .stream() + .map(RespondingEthPeer::pendingOutgoingRequests) + .flatMap(Function.identity()) + .filter(m -> m.getCode() == EthPV62.GET_BLOCK_HEADERS) + .map(GetBlockHeadersMessage::readFrom) + .filter(m -> m.skip() > 0) + .count(); + assertThat(checkpointRequestsToOtherPeers).isEqualTo(0L); + + bestPeer.respond(bestResponder); + for (final RespondingEthPeer otherPeer : otherPeers) { + otherPeer.respond(otherResponder); + } + } + } + + private MutableBlockchain createShortChain( + final Blockchain blockchain, final long truncateAtBlockNumber) { + final BlockHeader genesisHeader = + blockchain.getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get(); + final BlockBody genesisBody = blockchain.getBlockBody(genesisHeader.getHash()).get(); + final Block genesisBlock = new Block(genesisHeader, genesisBody); + final MutableBlockchain shortChain = + new DefaultMutableBlockchain( + genesisBlock, + new InMemoryKeyValueStorage(), + ScheduleBasedBlockHashFunction.create(protocolSchedule)); + long nextBlock = genesisHeader.getNumber() + 1; + while (nextBlock <= truncateAtBlockNumber) { + final BlockHeader header = blockchain.getBlockHeader(nextBlock).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + final List receipts = blockchain.getTxReceipts(header.getHash()).get(); + final Block block = new Block(header, body); + shortChain.appendBlock(block, receipts); + nextBlock++; + } + return shortChain; + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiterTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiterTest.java new file mode 100755 index 00000000000..04ca856fc22 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/TrailingPeerLimiterTest.java @@ -0,0 +1,145 @@ +package net.consensys.pantheon.ethereum.eth.sync; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.eth.manager.ChainState; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthPeers; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +public class TrailingPeerLimiterTest { + + private static final long CHAIN_HEAD = 10_000L; + private static final int MAX_TRAILING_PEERS = 2; + private static final int TRAILING_PEER_BLOCKS_BEHIND_THRESHOLD = 10; + private final EthPeers ethPeers = mock(EthPeers.class); + private final Blockchain blockchain = mock(Blockchain.class); + private final List peers = new ArrayList<>(); + private final TrailingPeerLimiter trailingPeerLimiter = + new TrailingPeerLimiter( + ethPeers, blockchain, TRAILING_PEER_BLOCKS_BEHIND_THRESHOLD, MAX_TRAILING_PEERS); + + @Before + public void setUp() { + when(ethPeers.availablePeers()).then(invocation -> peers.stream()); + when(blockchain.getChainHeadBlockNumber()).thenReturn(CHAIN_HEAD); + } + + @Test + public void shouldDisconnectFurthestBehindPeerWhenTrailingPeerLimitExceeded() { + final EthPeer ethPeer1 = addPeerWithEstimatedHeight(1); + addPeerWithEstimatedHeight(3); + addPeerWithEstimatedHeight(2); + + trailingPeerLimiter.enforceTrailingPeerLimit(); + + assertDisconnections(ethPeer1); + } + + @Test + public void shouldDisconnectMultiplePeersWhenTrailingPeerLimitExceeded() { + final EthPeer ethPeer1 = addPeerWithEstimatedHeight(1); + final EthPeer ethPeer2 = addPeerWithEstimatedHeight(2); + addPeerWithEstimatedHeight(3); + addPeerWithEstimatedHeight(4); + + trailingPeerLimiter.enforceTrailingPeerLimit(); + + assertDisconnections(ethPeer1, ethPeer2); + } + + @Test + public void shouldNotDisconnectPeersWhenLimitNotReached() { + addPeerWithEstimatedHeight(1); + addPeerWithEstimatedHeight(2); + + trailingPeerLimiter.enforceTrailingPeerLimit(); + + assertDisconnections(); + } + + @Test + public void shouldNotDisconnectPeersWithinToleranceOfChainHead() { + addPeerWithEstimatedHeight(CHAIN_HEAD); + addPeerWithEstimatedHeight(CHAIN_HEAD); + addPeerWithEstimatedHeight(CHAIN_HEAD); + addPeerWithEstimatedHeight(CHAIN_HEAD - TRAILING_PEER_BLOCKS_BEHIND_THRESHOLD); + addPeerWithEstimatedHeight(CHAIN_HEAD - TRAILING_PEER_BLOCKS_BEHIND_THRESHOLD); + + trailingPeerLimiter.enforceTrailingPeerLimit(); + + assertDisconnections(); + } + + @Test + public void shouldRecheckTrailingPeersWhenBlockAddedThatIsMultipleOf100() { + final EthPeer ethPeer1 = addPeerWithEstimatedHeight(1); + addPeerWithEstimatedHeight(3); + addPeerWithEstimatedHeight(2); + + final BlockAddedEvent blockAddedEvent = + BlockAddedEvent.createForHeadAdvancement( + new Block( + new BlockHeaderTestFixture().number(500).buildHeader(), + new BlockBody(emptyList(), emptyList()))); + trailingPeerLimiter.onBlockAdded(blockAddedEvent, blockchain); + + assertDisconnections(ethPeer1); + } + + @Test + public void shouldNotRecheckTrailingPeersWhenBlockAddedIsNotAMultipleOf100() { + addPeerWithEstimatedHeight(1); + addPeerWithEstimatedHeight(3); + addPeerWithEstimatedHeight(2); + + final BlockAddedEvent blockAddedEvent = + BlockAddedEvent.createForHeadAdvancement( + new Block( + new BlockHeaderTestFixture().number(599).buildHeader(), + new BlockBody(emptyList(), emptyList()))); + trailingPeerLimiter.onBlockAdded(blockAddedEvent, blockchain); + + assertDisconnections(); + } + + private void assertDisconnections(final EthPeer... disconnectedPeers) { + final List disconnected = asList(disconnectedPeers); + for (final EthPeer peer : peers) { + if (disconnected.contains(peer)) { + verify(peer).disconnect(DisconnectReason.TOO_MANY_PEERS); + } else { + verify(peer, never()).disconnect(any(DisconnectReason.class)); + } + } + } + + private EthPeer addPeerWithEstimatedHeight(final long height) { + final EthPeer peer = mock(EthPeer.class); + final ChainState chainState = new ChainState(); + chainState.statusReceived(Hash.EMPTY, UInt256.ONE); + chainState.update(Hash.EMPTY, height); + when(peer.chainState()).thenReturn(chainState); + peers.add(peer); + return peer; + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocksTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocksTest.java new file mode 100755 index 00000000000..829ff342656 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/state/PendingBlocksTest.java @@ -0,0 +1,113 @@ +package net.consensys.pantheon.ethereum.eth.sync.state; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +public class PendingBlocksTest { + + private PendingBlocks pendingBlocks; + private BlockDataGenerator gen; + + @Before + public void setup() { + pendingBlocks = new PendingBlocks(); + gen = new BlockDataGenerator(); + } + + @Test + public void registerPendingBlock() { + final Block block = gen.block(); + + // Sanity check + assertThat(pendingBlocks.contains(block.getHash())).isFalse(); + + pendingBlocks.registerPendingBlock(block); + + assertThat(pendingBlocks.contains(block.getHash())).isTrue(); + final List pendingBlocksForParent = + pendingBlocks.childrenOf(block.getHeader().getParentHash()); + assertThat(pendingBlocksForParent).isEqualTo(Collections.singletonList(block)); + } + + @Test + public void deregisterPendingBlock() { + final Block block = gen.block(); + pendingBlocks.registerPendingBlock(block); + pendingBlocks.deregisterPendingBlock(block); + + assertThat(pendingBlocks.contains(block.getHash())).isFalse(); + final List pendingBlocksForParent = + pendingBlocks.childrenOf(block.getHeader().getParentHash()); + assertThat(pendingBlocksForParent).isEqualTo(Collections.emptyList()); + } + + @Test + public void registerSiblingBlocks() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block parentBlock = gen.block(); + final Block childBlock = gen.nextBlock(parentBlock); + final Block childBlock2 = gen.nextBlock(parentBlock); + final List children = Arrays.asList(childBlock, childBlock2); + + pendingBlocks.registerPendingBlock(childBlock); + pendingBlocks.registerPendingBlock(childBlock2); + + assertThat(pendingBlocks.contains(childBlock.getHash())).isTrue(); + assertThat(pendingBlocks.contains(childBlock2.getHash())).isTrue(); + + final List pendingBlocksForParent = pendingBlocks.childrenOf(parentBlock.getHash()); + assertThat(pendingBlocksForParent.size()).isEqualTo(2); + assertThat(new HashSet<>(pendingBlocksForParent)).isEqualTo(new HashSet<>(children)); + } + + @Test + public void deregisterSubsetOfSiblingBlocks() { + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block parentBlock = gen.block(); + final Block childBlock = gen.nextBlock(parentBlock); + final Block childBlock2 = gen.nextBlock(parentBlock); + + pendingBlocks.registerPendingBlock(childBlock); + pendingBlocks.registerPendingBlock(childBlock2); + pendingBlocks.deregisterPendingBlock(childBlock); + + assertThat(pendingBlocks.contains(childBlock.getHash())).isFalse(); + assertThat(pendingBlocks.contains(childBlock2.getHash())).isTrue(); + + final List pendingBlocksForParent = pendingBlocks.childrenOf(parentBlock.getHash()); + assertThat(pendingBlocksForParent).isEqualTo(Collections.singletonList(childBlock2)); + } + + @Test + public void purgeBlocks() { + final List blocks = gen.blockSequence(10); + + for (final Block block : blocks) { + pendingBlocks.registerPendingBlock(block); + assertThat(pendingBlocks.contains(block.getHash())).isTrue(); + } + + final List blocksToPurge = blocks.subList(0, 5); + final List blocksToKeep = blocks.subList(5, blocks.size()); + pendingBlocks.purgeBlocksOlderThan(blocksToKeep.get(0).getHeader().getNumber()); + + for (final Block block : blocksToPurge) { + assertThat(pendingBlocks.contains(block.getHash())).isFalse(); + assertThat(pendingBlocks.childrenOf(block.getHeader().getParentHash()).size()).isEqualTo(0); + } + for (final Block block : blocksToKeep) { + assertThat(pendingBlocks.contains(block.getHash())).isTrue(); + assertThat(pendingBlocks.childrenOf(block.getHeader().getParentHash()).size()).isEqualTo(1); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java new file mode 100755 index 00000000000..2603442b112 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.RetryingMessageTaskWithResultsTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CompleteBlocksTaskTest extends RetryingMessageTaskWithResultsTest> { + + @Override + protected List generateDataToBeRequested() { + // Setup data to be requested and expected response + final List blocks = new ArrayList<>(); + for (long i = 0; i < 3; i++) { + final BlockHeader header = blockchain.getBlockHeader(10 + i).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + blocks.add(new Block(header, body)); + } + return blocks; + } + + @Override + protected EthTask> createTask(final List requestedData) { + final List headersToComplete = + requestedData.stream().map(Block::getHeader).collect(Collectors.toList()); + return CompleteBlocksTask.forHeaders(protocolSchedule, ethContext, headersToComplete); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java new file mode 100755 index 00000000000..ec3309c3913 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskParameterizedTest.java @@ -0,0 +1,169 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.uint.UInt256; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class DetermineCommonAncestorTaskParameterizedTest { + private final ProtocolSchedule protocolSchedule = MainnetProtocolSchedule.create(); + private static final BlockDataGenerator blockDataGenerator = new BlockDataGenerator(); + + private static Block genesisBlock; + private static KeyValueStorage localKvStore; + private static DefaultMutableBlockchain localBlockchain; + private static final int chainHeight = 50; + private final int headerRequestSize; + private final int commonAncestorHeight; + + private KeyValueStorage remoteKvStore; + private DefaultMutableBlockchain remoteBlockchain; + + public DetermineCommonAncestorTaskParameterizedTest( + final int headerRequestSize, final int commonAncestorHeight) { + this.headerRequestSize = headerRequestSize; + this.commonAncestorHeight = commonAncestorHeight; + } + + @BeforeClass + public static void setupClass() { + genesisBlock = blockDataGenerator.genesisBlock(); + localKvStore = new InMemoryKeyValueStorage(); + localBlockchain = + new DefaultMutableBlockchain( + genesisBlock, localKvStore, MainnetBlockHashFunction::createHash); + + // Setup local chain + for (int i = 1; i <= chainHeight; i++) { + final BlockDataGenerator.BlockOptions options = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block = blockDataGenerator.block(options); + final List receipts = blockDataGenerator.receipts(block); + localBlockchain.appendBlock(block, receipts); + } + } + + @Before + public void setup() { + remoteKvStore = new InMemoryKeyValueStorage(); + remoteBlockchain = + new DefaultMutableBlockchain( + genesisBlock, remoteKvStore, MainnetBlockHashFunction::createHash); + } + + @Parameters(name = "requestSize={0}, commonAncestor={1}") + public static Collection parameters() throws IOException { + final int[] requestSizes = {5, 12, chainHeight, chainHeight * 2}; + final List params = new ArrayList<>(); + for (final int requestSize : requestSizes) { + for (int i = 0; i <= chainHeight; i++) { + params.add(new Object[] {requestSize, i}); + } + } + return params; + } + + @Test + public void searchesAgainstNetwork() { + BlockHeader commonHeader = genesisBlock.getHeader(); + for (long i = 1; i <= commonAncestorHeight; i++) { + commonHeader = localBlockchain.getBlockHeader(i).get(); + final List receipts = + localBlockchain.getTxReceipts(commonHeader.getHash()).get(); + final BlockBody commonBody = localBlockchain.getBlockBody(commonHeader.getHash()).get(); + remoteBlockchain.appendBlock(new Block(commonHeader, commonBody), receipts); + } + + // Remaining blocks are disparate... + for (long i = commonAncestorHeight + 1L; i <= chainHeight; i++) { + final BlockDataGenerator.BlockOptions localOptions = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + final Block localBlock = blockDataGenerator.block(localOptions); + final List localReceipts = blockDataGenerator.receipts(localBlock); + localBlockchain.appendBlock(localBlock, localReceipts); + + final BlockDataGenerator.BlockOptions remoteOptions = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ONE) // differentiator + .setBlockNumber(i) + .setParentHash(remoteBlockchain.getBlockHashByNumber(i - 1).get()); + final Block remoteBlock = blockDataGenerator.block(remoteOptions); + final List remoteReceipts = blockDataGenerator.receipts(remoteBlock); + remoteBlockchain.appendBlock(remoteBlock, remoteReceipts); + } + + final EthProtocolManager ethProtocolManager = + EthProtocolManagerTestUtil.create(localBlockchain); + + final RespondingEthPeer.Responder responder = + RespondingEthPeer.blockchainResponder(remoteBlockchain); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Execute task and wait for response + final AtomicReference actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + + final EthContext ethContext = ethProtocolManager.ethContext(); + final ProtocolContext protocolContext = + new ProtocolContext<>(localBlockchain, createInMemoryWorldStateArchive(), null); + + final EthTask task = + DetermineCommonAncestorTask.create( + protocolSchedule, + protocolContext, + ethContext, + respondingEthPeer.getEthPeer(), + headerRequestSize); + + final CompletableFuture future = task.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + + future.whenComplete( + (response, error) -> { + actualResult.set(response); + done.compareAndSet(false, true); + }); + + assertThat(actualResult.get()).isNotNull(); + assertThat(actualResult.get().getHash()) + .isEqualTo(MainnetBlockHashFunction.createHash(commonHeader)); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskTest.java new file mode 100755 index 00000000000..2adfb524185 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DetermineCommonAncestorTaskTest.java @@ -0,0 +1,386 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.EthTaskException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.EthTaskException.FailureReason; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.ExceptionUtils; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; + +public class DetermineCommonAncestorTaskTest { + + private final ProtocolSchedule protocolSchedule = MainnetProtocolSchedule.create(); + private final BlockDataGenerator blockDataGenerator = new BlockDataGenerator(); + private KeyValueStorage localKvStore; + private DefaultMutableBlockchain localBlockchain; + private final int defaultHeaderRequestSize = 10; + Block genesisBlock; + private EthProtocolManager ethProtocolManager; + private EthContext ethContext; + private ProtocolContext protocolContext; + + @Before + public void setup() { + genesisBlock = blockDataGenerator.genesisBlock(); + localKvStore = new InMemoryKeyValueStorage(); + localBlockchain = + new DefaultMutableBlockchain( + genesisBlock, localKvStore, MainnetBlockHashFunction::createHash); + ethProtocolManager = EthProtocolManagerTestUtil.create(localBlockchain); + ethContext = ethProtocolManager.ethContext(); + protocolContext = + new ProtocolContext<>(localBlockchain, createInMemoryWorldStateArchive(), null); + } + + @Test + public void shouldThrowExceptionNoCommonBlock() { + // Populate local chain + for (long i = 1; i <= 9; i++) { + final BlockDataGenerator.BlockOptions options00 = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block00 = blockDataGenerator.block(options00); + final List receipts00 = blockDataGenerator.receipts(block00); + localBlockchain.appendBlock(block00, receipts00); + } + + // Populate remote chain + final Block remoteGenesisBlock = blockDataGenerator.genesisBlock(); + final DefaultMutableBlockchain remoteBlockchain = + new DefaultMutableBlockchain( + remoteGenesisBlock, + new InMemoryKeyValueStorage(), + MainnetBlockHashFunction::createHash); + for (long i = 1; i <= 9; i++) { + final BlockDataGenerator.BlockOptions options01 = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ONE) + .setBlockNumber(i) + .setParentHash(remoteBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block01 = blockDataGenerator.block(options01); + final List receipts01 = blockDataGenerator.receipts(block01); + remoteBlockchain.appendBlock(block01, receipts01); + } + + final RespondingEthPeer.Responder responder = + RespondingEthPeer.blockchainResponder(remoteBlockchain); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + final EthTask task = + DetermineCommonAncestorTask.create( + protocolSchedule, + protocolContext, + ethContext, + respondingEthPeer.getEthPeer(), + defaultHeaderRequestSize); + + final CompletableFuture future = task.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + final AtomicReference failure = new AtomicReference<>(); + future.whenComplete( + (response, error) -> { + failure.set(error); + }); + + assertThat(failure.get()).isNotNull(); + final Throwable error = ExceptionUtils.rootCause(failure.get()); + assertThat(error) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("No common ancestor."); + } + + @Test + public void shouldFailIfPeerDisconnects() { + final Block block = blockDataGenerator.nextBlock(localBlockchain.getChainHeadBlock()); + localBlockchain.appendBlock(block, blockDataGenerator.receipts(block)); + + final RespondingEthPeer.Responder responder = + RespondingEthPeer.blockchainResponder(localBlockchain); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Disconnect the target peer + respondingEthPeer.getEthPeer().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + + final EthTask task = + DetermineCommonAncestorTask.create( + protocolSchedule, + protocolContext, + ethContext, + respondingEthPeer.getEthPeer(), + defaultHeaderRequestSize); + + // Execute task and wait for response + final AtomicReference failure = new AtomicReference<>(); + final CompletableFuture future = task.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + future.whenComplete( + (response, error) -> { + failure.set(error); + }); + + assertThat(failure.get()).isNotNull(); + final Throwable error = ExceptionUtils.rootCause(failure.get()); + assertThat(error).isInstanceOf(EthTaskException.class); + assertThat(((EthTaskException) error).reason()).isEqualTo(FailureReason.PEER_DISCONNECTED); + } + + @Test + public void shouldCorrectlyCalculateSkipIntervalAndCount() { + final long maximumPossibleCommonAncestorNumber = 100; + final long minimumPossibleCommonAncestorNumber = 0; + final int headerRequestSize = 10; + + final long range = maximumPossibleCommonAncestorNumber - minimumPossibleCommonAncestorNumber; + final int skipInterval = + DetermineCommonAncestorTask.calculateSkipInterval(range, headerRequestSize); + final int count = DetermineCommonAncestorTask.calculateCount(range, skipInterval); + + assertThat(count).isEqualTo(11); + assertThat(skipInterval).isEqualTo(9); + } + + @Test + public void shouldGracefullyHandleExecutionsForNoCommonAncestor() { + // Populate local chain + for (long i = 1; i <= 99; i++) { + final BlockDataGenerator.BlockOptions options00 = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block00 = blockDataGenerator.block(options00); + final List receipts00 = blockDataGenerator.receipts(block00); + localBlockchain.appendBlock(block00, receipts00); + } + + // Populate remote chain + final Block remoteGenesisBlock = blockDataGenerator.genesisBlock(); + final DefaultMutableBlockchain remoteBlockchain = + new DefaultMutableBlockchain( + remoteGenesisBlock, + new InMemoryKeyValueStorage(), + MainnetBlockHashFunction::createHash); + for (long i = 1; i <= 99; i++) { + final BlockDataGenerator.BlockOptions options01 = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ONE) + .setBlockNumber(i) + .setParentHash(remoteBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block01 = blockDataGenerator.block(options01); + final List receipts01 = blockDataGenerator.receipts(block01); + remoteBlockchain.appendBlock(block01, receipts01); + } + + final RespondingEthPeer.Responder responder = + RespondingEthPeer.blockchainResponder(remoteBlockchain); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + final DetermineCommonAncestorTask task = + DetermineCommonAncestorTask.create( + protocolSchedule, + protocolContext, + ethContext, + respondingEthPeer.getEthPeer(), + defaultHeaderRequestSize); + final DetermineCommonAncestorTask spy = spy(task); + + // Execute task + final CompletableFuture future = spy.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + + final AtomicReference result = new AtomicReference<>(); + future.whenComplete( + (response, error) -> { + result.set(response); + }); + + Assertions.assertThat(result.get().getHash()) + .isEqualTo(MainnetBlockHashFunction.createHash(genesisBlock.getHeader())); + + verify(spy, times(2)).requestHeaders(); + } + + @Test + public void shouldIssueConsistentNumberOfRequestsToPeer() { + // Populate local chain + for (long i = 1; i <= 100; i++) { + final BlockDataGenerator.BlockOptions options00 = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block00 = blockDataGenerator.block(options00); + final List receipts00 = blockDataGenerator.receipts(block00); + localBlockchain.appendBlock(block00, receipts00); + } + + // Populate remote chain + final DefaultMutableBlockchain remoteBlockchain = + new DefaultMutableBlockchain( + genesisBlock, new InMemoryKeyValueStorage(), MainnetBlockHashFunction::createHash); + for (long i = 1; i <= 100; i++) { + final BlockDataGenerator.BlockOptions options01 = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ONE) + .setBlockNumber(i) + .setParentHash(remoteBlockchain.getBlockHashByNumber(i - 1).get()); + final Block block01 = blockDataGenerator.block(options01); + final List receipts01 = blockDataGenerator.receipts(block01); + remoteBlockchain.appendBlock(block01, receipts01); + } + + final RespondingEthPeer.Responder responder = + RespondingEthPeer.blockchainResponder(remoteBlockchain); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + final DetermineCommonAncestorTask task = + DetermineCommonAncestorTask.create( + protocolSchedule, + protocolContext, + ethContext, + respondingEthPeer.getEthPeer(), + defaultHeaderRequestSize); + final DetermineCommonAncestorTask spy = spy(task); + + // Execute task + final CompletableFuture future = spy.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + + final AtomicReference result = new AtomicReference<>(); + future.whenComplete( + (response, error) -> { + result.set(response); + }); + + Assertions.assertThat(result.get().getHash()) + .isEqualTo(MainnetBlockHashFunction.createHash(genesisBlock.getHeader())); + + verify(spy, times(3)).requestHeaders(); + } + + @Test + public void shouldShortCircuitOnHeaderInInitialRequest() { + DefaultMutableBlockchain remoteBlockchain = + new DefaultMutableBlockchain( + genesisBlock, new InMemoryKeyValueStorage(), MainnetBlockHashFunction::createHash); + + Block commonBlock = null; + + // Populate common chain + for (long i = 1; i <= 95; i++) { + BlockDataGenerator.BlockOptions options = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + commonBlock = blockDataGenerator.block(options); + List receipts = blockDataGenerator.receipts(commonBlock); + localBlockchain.appendBlock(commonBlock, receipts); + remoteBlockchain.appendBlock(commonBlock, receipts); + } + + // Populate local chain + for (long i = 96; i <= 99; i++) { + BlockDataGenerator.BlockOptions options00 = + new BlockDataGenerator.BlockOptions() + .setBlockNumber(i) + .setParentHash(localBlockchain.getBlockHashByNumber(i - 1).get()); + Block block00 = blockDataGenerator.block(options00); + List receipts00 = blockDataGenerator.receipts(block00); + localBlockchain.appendBlock(block00, receipts00); + } + + // Populate remote chain + for (long i = 96; i <= 99; i++) { + BlockDataGenerator.BlockOptions options01 = + new BlockDataGenerator.BlockOptions() + .setDifficulty(UInt256.ONE) + .setBlockNumber(i) + .setParentHash(remoteBlockchain.getBlockHashByNumber(i - 1).get()); + Block block01 = blockDataGenerator.block(options01); + List receipts01 = blockDataGenerator.receipts(block01); + remoteBlockchain.appendBlock(block01, receipts01); + } + + RespondingEthPeer.Responder responder = RespondingEthPeer.blockchainResponder(remoteBlockchain); + RespondingEthPeer respondingEthPeer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + DetermineCommonAncestorTask task = + DetermineCommonAncestorTask.create( + protocolSchedule, + protocolContext, + ethContext, + respondingEthPeer.getEthPeer(), + defaultHeaderRequestSize); + DetermineCommonAncestorTask spy = spy(task); + + // Execute task + CompletableFuture future = spy.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + + AtomicReference result = new AtomicReference<>(); + future.whenComplete( + (response, error) -> { + result.set(response); + }); + + Assertions.assertThat(result.get().getHash()) + .isEqualTo(MainnetBlockHashFunction.createHash(commonBlock.getHeader())); + + verify(spy, times(1)).requestHeaders(); + } + + @Test + public void returnsImmediatelyWhenThereIsNoWorkToDo() throws Exception { + final RespondingEthPeer respondingEthPeer = + spy(EthProtocolManagerTestUtil.createPeer(ethProtocolManager)); + final EthPeer peer = spy(respondingEthPeer.getEthPeer()); + + final EthTask task = + DetermineCommonAncestorTask.create( + protocolSchedule, protocolContext, ethContext, peer, defaultHeaderRequestSize); + + final CompletableFuture result = task.run(); + assertThat(result).isCompletedWithValue(genesisBlock.getHeader()); + + // Make sure we didn't ask for any headers + verify(peer, times(0)).getHeadersByHash(any(), anyInt(), anyBoolean(), anyInt()); + verify(peer, times(0)).getHeadersByNumber(anyLong(), anyInt(), anyBoolean(), anyInt()); + verify(peer, times(0)).send(any()); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTaskTest.java new file mode 100755 index 00000000000..60ca53e60f9 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/DownloadHeaderSequenceTaskTest.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.RetryingMessageTaskWithResultsTest; + +import java.util.ArrayList; +import java.util.List; + +public class DownloadHeaderSequenceTaskTest + extends RetryingMessageTaskWithResultsTest> { + + @Override + protected List generateDataToBeRequested() { + final List requestedHeaders = new ArrayList<>(); + for (long i = 0; i < 3; i++) { + final long blockNumber = 10 + i; + final BlockHeader header = blockchain.getBlockHeader(blockNumber).get(); + requestedHeaders.add(header); + } + return requestedHeaders; + } + + @Override + protected EthTask> createTask(final List requestedData) { + final BlockHeader lastHeader = requestedData.get(requestedData.size() - 1); + final BlockHeader referenceHeader = blockchain.getBlockHeader(lastHeader.getNumber() + 1).get(); + return DownloadHeaderSequenceTask.endingAtHeader( + protocolSchedule, protocolContext, ethContext, referenceHeader, requestedData.size()); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTaskTest.java new file mode 100755 index 00000000000..a810c43de8b --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBlockFromPeerTaskTest.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.AbstractMessageTaskTest; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.EthTaskException; +import net.consensys.pantheon.ethereum.eth.manager.exceptions.EthTaskException.FailureReason; +import net.consensys.pantheon.util.ExceptionUtils; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +public class GetBlockFromPeerTaskTest + extends AbstractMessageTaskTest> { + + @Override + protected Block generateDataToBeRequested() { + final BlockHeader header = blockchain.getBlockHeader(5).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + return new Block(header, body); + } + + @Override + protected EthTask> createTask(final Block requestedData) { + return GetBlockFromPeerTask.create(protocolSchedule, ethContext, requestedData.getHash()); + } + + @Override + protected void assertResultMatchesExpectation( + final Block requestedData, + final PeerTaskResult response, + final EthPeer respondingPeer) { + assertThat(response.getResult()).isEqualTo(requestedData); + assertThat(response.getPeer()).isEqualTo(respondingPeer); + } + + @Test + public void failsWhenNoPeersAreAvailable() throws ExecutionException, InterruptedException { + // Setup data to be requested + final Block requestedData = generateDataToBeRequested(); + + // Execute task + final EthTask> task = createTask(requestedData); + final CompletableFuture> future = task.run(); + final AtomicReference failure = new AtomicReference<>(); + future.whenComplete( + (r, t) -> { + failure.set(t); + }); + + assertThat(future.isCompletedExceptionally()).isTrue(); + assertThat(failure.get()).isNotNull(); + // Check wrapped failure + final Throwable error = ExceptionUtils.rootCause(failure.get()); + assertThat(error).isInstanceOf(EthTaskException.class); + final EthTaskException ethException = (EthTaskException) error; + assertThat(ethException.reason()).isEqualTo(FailureReason.NO_AVAILABLE_PEERS); + + assertThat(task.run().isCompletedExceptionally()).isTrue(); + task.cancel(); + assertThat(task.run().isCompletedExceptionally()).isTrue(); + } + + @Test + public void failsWhenPeersSendEmptyResponses() throws ExecutionException, InterruptedException { + // Setup a unresponsive peer + final Responder responder = RespondingEthPeer.emptyResponder(); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Setup data to be requested + final Block requestedData = generateDataToBeRequested(); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final EthTask> task = createTask(requestedData); + final CompletableFuture> future = task.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + final AtomicReference failure = new AtomicReference<>(); + future.whenComplete( + (response, error) -> { + failure.set(error); + }); + + assertThat(future.isCompletedExceptionally()).isTrue(); + assertThat(failure.get()).isNotNull(); + // Check wrapped failure + final Throwable error = ExceptionUtils.rootCause(failure.get()); + assertThat(error).isInstanceOf(EthTaskException.class); + assertThat(((EthTaskException) error).reason()).isEqualTo(FailureReason.INCOMPLETE_RESULTS); + + assertThat(task.run().isCompletedExceptionally()).isTrue(); + task.cancel(); + assertThat(task.run().isCompletedExceptionally()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTaskTest.java new file mode 100755 index 00000000000..0ee6247c02b --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetBodiesFromPeerTaskTest.java @@ -0,0 +1,45 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.PeerMessageTaskTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class GetBodiesFromPeerTaskTest extends PeerMessageTaskTest> { + + @Override + protected List generateDataToBeRequested() { + final List requestedBlocks = new ArrayList<>(); + for (long i = 0; i < 3; i++) { + final BlockHeader header = blockchain.getBlockHeader(10 + i).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + requestedBlocks.add(new Block(header, body)); + } + return requestedBlocks; + } + + @Override + protected EthTask>> createTask(final List requestedData) { + final List headersToComplete = + requestedData.stream().map(Block::getHeader).collect(Collectors.toList()); + return GetBodiesFromPeerTask.forHeaders(protocolSchedule, ethContext, headersToComplete); + } + + @Override + protected void assertPartialResultMatchesExpectation( + final List requestedData, final List partialResponse) { + assertThat(partialResponse.size()).isLessThanOrEqualTo(requestedData.size()); + assertThat(partialResponse.size()).isGreaterThan(0); + for (final Block block : partialResponse) { + assertThat(requestedData).contains(block); + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTaskTest.java new file mode 100755 index 00000000000..d54445630da --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByHashTaskTest.java @@ -0,0 +1,114 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.PeerMessageTaskTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +public class GetHeadersFromPeerByHashTaskTest extends PeerMessageTaskTest> { + + @Override + protected void assertPartialResultMatchesExpectation( + final List requestedData, final List partialResponse) { + assertThat(partialResponse.size()).isLessThanOrEqualTo(requestedData.size()); + assertThat(partialResponse.size()).isGreaterThan(0); + for (final BlockHeader header : partialResponse) { + assertThat(requestedData).contains(header); + } + } + + @Override + protected List generateDataToBeRequested() { + final int count = 3; + final List requestedHeaders = new ArrayList<>(count); + for (long i = 0; i < count; i++) { + requestedHeaders.add(blockchain.getBlockHeader(5 + i).get()); + } + return requestedHeaders; + } + + @Override + protected EthTask>> createTask( + final List requestedData) { + final BlockHeader firstHeader = requestedData.get(0); + return GetHeadersFromPeerByHashTask.startingAtHash( + protocolSchedule, + ethContext, + firstHeader.getHash(), + firstHeader.getNumber(), + requestedData.size()); + } + + @Test + public void getHeadersFromHashNoSkip() { + getHeadersFromHash(0, false); + } + + @Test + public void getHeadersFromHashNoSkipReversed() { + getHeadersFromHash(0, true); + } + + @Test + public void getHeadersFromHashWithSkip() { + getHeadersFromHash(2, false); + } + + @Test + public void getHeadersFromHashWithSkipReversed() { + getHeadersFromHash(2, true); + } + + private void getHeadersFromHash(final int skip, final boolean reverse) { + // Setup a responsive peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Set up parameters and calculated expected response + final long startNumber = reverse ? blockchain.getChainHeadBlockNumber() - 2 : 2; + final int delta = (skip + 1) * (reverse ? -1 : 1); + final int count = 4; + final List expectedHeaders = new ArrayList<>(count); + for (long i = 0; i < count; i++) { + expectedHeaders.add(blockchain.getBlockHeader(startNumber + delta * i).get()); + } + + // Execute task and wait for response + final AbstractGetHeadersFromPeerTask task = + new GetHeadersFromPeerByHashTask( + protocolSchedule, + ethContext, + blockchain.getBlockHashByNumber(startNumber).get(), + startNumber, + count, + skip, + reverse); + final AtomicReference>> actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + final CompletableFuture>> future = task.run(); + respondingPeer.respondWhile(responder, () -> !future.isDone()); + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertThat(actualResult.get().getPeer()).isEqualTo(respondingPeer.getEthPeer()); + assertThat(actualResult.get().getResult()).isEqualTo(expectedHeaders); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTaskTest.java new file mode 100755 index 00000000000..9d26bce2199 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/GetHeadersFromPeerByNumberTaskTest.java @@ -0,0 +1,104 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.PeerMessageTaskTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +public class GetHeadersFromPeerByNumberTaskTest extends PeerMessageTaskTest> { + + @Override + protected void assertPartialResultMatchesExpectation( + final List requestedData, final List partialResponse) { + assertThat(partialResponse.size()).isLessThanOrEqualTo(requestedData.size()); + assertThat(partialResponse.size()).isGreaterThan(0); + for (final BlockHeader header : partialResponse) { + assertThat(requestedData).contains(header); + } + } + + @Override + protected List generateDataToBeRequested() { + final int count = 3; + final List requestedHeaders = new ArrayList<>(count); + for (long i = 0; i < count; i++) { + requestedHeaders.add(blockchain.getBlockHeader(5 + i).get()); + } + return requestedHeaders; + } + + @Override + protected EthTask>> createTask( + final List requestedData) { + final BlockHeader firstHeader = requestedData.get(0); + return GetHeadersFromPeerByNumberTask.startingAtNumber( + protocolSchedule, ethContext, firstHeader.getNumber(), requestedData.size()); + } + + @Test + public void getHeadersFromHashNoSkip() { + getHeadersFromHash(0, false); + } + + @Test + public void getHeadersFromHashNoSkipReversed() { + getHeadersFromHash(0, true); + } + + @Test + public void getHeadersFromHashWithSkip() { + getHeadersFromHash(2, false); + } + + @Test + public void getHeadersFromHashWithSkipReversed() { + getHeadersFromHash(2, true); + } + + private void getHeadersFromHash(final int skip, final boolean reverse) { + // Setup a responsive peer + final RespondingEthPeer.Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Set up parameters and calculated expected response + final long startNumber = reverse ? blockchain.getChainHeadBlockNumber() - 2 : 2; + final int delta = (skip + 1) * (reverse ? -1 : 1); + final int count = 4; + final List expectedHeaders = new ArrayList<>(count); + for (long i = 0; i < count; i++) { + expectedHeaders.add(blockchain.getBlockHeader(startNumber + delta * i).get()); + } + + // Execute task and wait for response + final AbstractGetHeadersFromPeerTask task = + new GetHeadersFromPeerByNumberTask( + protocolSchedule, ethContext, startNumber, count, skip, reverse); + final AtomicReference>> actualResult = + new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + final CompletableFuture>> future = task.run(); + respondingPeer.respondWhile(responder, () -> !future.isDone()); + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertThat(actualResult.get().getPeer()).isEqualTo(respondingPeer.getEthPeer()); + assertThat(actualResult.get().getResult()).isEqualTo(expectedHeaders); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTaskTest.java new file mode 100755 index 00000000000..2d0f4cb31be --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/ImportBlocksTaskTest.java @@ -0,0 +1,174 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.eth.manager.AbstractPeerTask.PeerTaskResult; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.AbstractMessageTaskTest; +import net.consensys.pantheon.ethereum.eth.messages.BlockHeadersMessage; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class ImportBlocksTaskTest + extends AbstractMessageTaskTest, PeerTaskResult>> { + + @Override + protected List generateDataToBeRequested() { + final long chainHead = blockchain.getChainHeadBlockNumber(); + final long importSize = 5; + final long startNumber = chainHead - importSize + 1; + final List blocksToImport = new ArrayList<>(); + for (long i = 0; i < importSize; i++) { + final BlockHeader header = blockchain.getBlockHeader(startNumber + i).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + blocksToImport.add(new Block(header, body)); + } + return blocksToImport; + } + + @Override + protected EthTask>> createTask(final List requestedData) { + final Block firstBlock = requestedData.get(0); + final MutableBlockchain shortBlockchain = + createShortChain(firstBlock.getHeader().getNumber() - 1); + final ProtocolContext modifiedContext = + new ProtocolContext<>( + shortBlockchain, + protocolContext.getWorldStateArchive(), + protocolContext.getConsensusState()); + return ImportBlocksTask.fromHeader( + protocolSchedule, + modifiedContext, + ethContext, + firstBlock.getHeader(), + requestedData.size()); + } + + @Override + protected void assertResultMatchesExpectation( + final List requestedData, + final PeerTaskResult> response, + final EthPeer respondingPeer) { + assertThat(response.getResult()).isEqualTo(requestedData); + assertThat(response.getPeer()).isEqualTo(respondingPeer); + } + + @Test + public void completesWhenPeerReturnsPartialResult() + throws ExecutionException, InterruptedException { + + // Respond with some headers and all corresponding bodies + final Responder fullResponder = RespondingEthPeer.blockchainResponder(blockchain); + final Responder partialResponder = + (final Capability cap, final MessageData msg) -> { + final Optional fullReponse = fullResponder.respond(cap, msg); + if (msg.getCode() == EthPV62.GET_BLOCK_HEADERS) { + // Return a partial headers response + final BlockHeadersMessage headersMessage = + BlockHeadersMessage.readFrom(fullReponse.get()); + final List originalHeaders = + Lists.newArrayList(headersMessage.getHeaders(protocolSchedule)); + final List partialHeaders = + originalHeaders.subList(0, originalHeaders.size() / 2); + return Optional.of(BlockHeadersMessage.create(partialHeaders)); + } + return fullReponse; + }; + + final RespondingEthPeer peer = EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Execute task + final AtomicReference> actualResult = new AtomicReference<>(); + final AtomicReference actualPeer = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + final List requestedData = generateDataToBeRequested(); + final EthTask>> task = createTask(requestedData); + final CompletableFuture>> future = task.run(); + future.whenComplete( + (response, error) -> { + actualResult.set(response.getResult()); + actualPeer.set(response.getPeer()); + done.compareAndSet(false, true); + }); + + // Send partial responses + peer.respondWhile(partialResponder, () -> !future.isDone()); + + assertThat(done).isTrue(); + assertThat(actualPeer.get()).isEqualTo(peer.getEthPeer()); + assertThat(actualResult.get().size()).isLessThan(requestedData.size()); + for (final Block block : actualResult.get()) { + assertThat(requestedData).contains(block); + assertThat(blockchain.contains(block.getHash())).isTrue(); + } + } + + @Test + public void completesWhenPeersSendEmptyResponses() + throws ExecutionException, InterruptedException { + // Setup a unresponsive peer + final Responder responder = RespondingEthPeer.emptyResponder(); + final RespondingEthPeer respondingEthPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Execute task and wait for response + final AtomicBoolean done = new AtomicBoolean(false); + final List requestedData = generateDataToBeRequested(); + final EthTask>> task = createTask(requestedData); + final CompletableFuture>> future = task.run(); + respondingEthPeer.respondWhile(responder, () -> !future.isDone()); + future.whenComplete( + (response, error) -> { + done.compareAndSet(false, true); + }); + assertThat(future.isDone()).isTrue(); + assertThat(future.isCompletedExceptionally()).isFalse(); + } + + private MutableBlockchain createShortChain(final long truncateAtBlockNumber) { + final BlockHeader genesisHeader = + blockchain.getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get(); + final BlockBody genesisBody = blockchain.getBlockBody(genesisHeader.getHash()).get(); + final Block genesisBlock = new Block(genesisHeader, genesisBody); + final MutableBlockchain shortChain = + new DefaultMutableBlockchain( + genesisBlock, + new InMemoryKeyValueStorage(), + ScheduleBasedBlockHashFunction.create(protocolSchedule)); + long nextBlock = genesisHeader.getNumber() + 1; + while (nextBlock <= truncateAtBlockNumber) { + final BlockHeader header = blockchain.getBlockHeader(nextBlock).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + final List receipts = blockchain.getTxReceipts(header.getHash()).get(); + final Block block = new Block(header, body); + shortChain.appendBlock(block, receipts); + nextBlock++; + } + return shortChain; + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTaskTest.java new file mode 100755 index 00000000000..ad8668aa6b6 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PersistBlockTaskTest.java @@ -0,0 +1,307 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.BlockchainSetupUtil; +import net.consensys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.awaitility.Awaitility; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class PersistBlockTaskTest { + + private BlockchainSetupUtil blockchainUtil; + private ProtocolSchedule protocolSchedule; + private ProtocolContext protocolContext; + private MutableBlockchain blockchain; + + @Before + public void setup() { + blockchainUtil = BlockchainSetupUtil.forTesting(); + protocolSchedule = blockchainUtil.getProtocolSchedule(); + protocolContext = blockchainUtil.getProtocolContext(); + blockchain = blockchainUtil.getBlockchain(); + } + + @Test + public void importsValidBlock() throws Exception { + blockchainUtil.importFirstBlocks(3); + final Block nextBlock = blockchainUtil.getBlock(3); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + + // Create task + final PersistBlockTask task = + PersistBlockTask.create( + protocolSchedule, protocolContext, nextBlock, HeaderValidationMode.FULL); + final CompletableFuture result = task.run(); + + Awaitility.await().atMost(30, SECONDS).until(result::isDone); + + assertThat(result.isCompletedExceptionally()).isFalse(); + assertThat(result.get()).isEqualTo(nextBlock); + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + } + + @Test + public void failsToImportInvalidBlock() { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(3); + final Block nextBlock = gen.block(); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + + // Create task + final PersistBlockTask task = + PersistBlockTask.create( + protocolSchedule, protocolContext, nextBlock, HeaderValidationMode.FULL); + final CompletableFuture result = task.run(); + + Awaitility.await().atMost(30, SECONDS).until(result::isDone); + + assertThat(result.isCompletedExceptionally()).isTrue(); + assertThatThrownBy(result::get).hasCauseInstanceOf(InvalidBlockException.class); + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + @Test + public void importsValidBlockSequence() throws Exception { + blockchainUtil.importFirstBlocks(3); + final List nextBlocks = + Arrays.asList(blockchainUtil.getBlock(3), blockchainUtil.getBlock(4)); + + // Sanity check + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + // Create task + final CompletableFuture> task = + PersistBlockTask.forSequentialBlocks( + protocolSchedule, protocolContext, nextBlocks, HeaderValidationMode.FULL) + .get(); + + Awaitility.await().atMost(30, SECONDS).until(task::isDone); + + assertThat(task.isCompletedExceptionally()).isFalse(); + assertThat(task.get()).isEqualTo(nextBlocks); + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + } + } + + @Test + public void failsToImportInvalidBlockSequenceWhereSecondBlockFails() throws Exception { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(3); + final List nextBlocks = Arrays.asList(blockchainUtil.getBlock(3), gen.block()); + + // Sanity check + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + // Create task + final CompletableFuture> task = + PersistBlockTask.forSequentialBlocks( + protocolSchedule, protocolContext, nextBlocks, HeaderValidationMode.FULL) + .get(); + + Awaitility.await().atMost(30, SECONDS).until(task::isDone); + + assertThat(task.isCompletedExceptionally()).isTrue(); + assertThatThrownBy(task::get).hasCauseInstanceOf(InvalidBlockException.class); + assertThat(blockchain.contains(nextBlocks.get(0).getHash())).isTrue(); + assertThat(blockchain.contains(nextBlocks.get(1).getHash())).isFalse(); + } + + @Test + public void failsToImportInvalidBlockSequenceWhereFirstBlockFails() throws Exception { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(3); + final List nextBlocks = Arrays.asList(gen.block(), blockchainUtil.getBlock(3)); + + // Sanity check + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + // Create task + final CompletableFuture> task = + PersistBlockTask.forSequentialBlocks( + protocolSchedule, protocolContext, nextBlocks, HeaderValidationMode.FULL) + .get(); + + Awaitility.await().atMost(30, SECONDS).until(task::isDone); + + assertThat(task.isCompletedExceptionally()).isTrue(); + assertThatThrownBy(task::get).hasCauseInstanceOf(InvalidBlockException.class); + assertThat(blockchain.contains(nextBlocks.get(0).getHash())).isFalse(); + assertThat(blockchain.contains(nextBlocks.get(1).getHash())).isFalse(); + } + + @Test + public void importsValidUnorderedBlocks() throws Exception { + blockchainUtil.importFirstBlocks(3); + final Block valid = blockchainUtil.getBlock(3); + final List nextBlocks = Collections.singletonList(valid); + + // Sanity check + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + // Create task + final CompletableFuture> task = + PersistBlockTask.forUnorderedBlocks( + protocolSchedule, protocolContext, nextBlocks, HeaderValidationMode.FULL) + .get(); + + Awaitility.await().atMost(30, SECONDS).until(task::isDone); + + assertThat(task.isCompletedExceptionally()).isFalse(); + assertThat(task.get().size()).isEqualTo(1); + assertThat(task.get().contains(valid)).isTrue(); + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isTrue(); + } + } + + @Test + public void importsInvalidUnorderedBlock() throws Exception { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(3); + final Block invalid = gen.block(); + final List nextBlocks = Collections.singletonList(invalid); + + // Sanity check + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + // Create task + final CompletableFuture> task = + PersistBlockTask.forUnorderedBlocks( + protocolSchedule, protocolContext, nextBlocks, HeaderValidationMode.FULL) + .get(); + + Awaitility.await().atMost(30, SECONDS).until(task::isDone); + + assertThat(task.isCompletedExceptionally()).isTrue(); + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + } + + @Test + public void importsInvalidUnorderedBlocks() throws Exception { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(3); + final List nextBlocks = Arrays.asList(gen.block(), gen.block()); + + // Sanity check + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + // Create task + final CompletableFuture> task = + PersistBlockTask.forUnorderedBlocks( + protocolSchedule, protocolContext, nextBlocks, HeaderValidationMode.FULL) + .get(); + + Awaitility.await().atMost(30, SECONDS).until(task::isDone); + + assertThat(task.isCompletedExceptionally()).isTrue(); + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + } + + @Test + public void importsUnorderedBlocksWithMixOfValidAndInvalidBlocks() throws Exception { + final BlockDataGenerator gen = new BlockDataGenerator(); + blockchainUtil.importFirstBlocks(3); + final Block valid = blockchainUtil.getBlock(3); + final Block invalid = gen.block(); + final List nextBlocks = Arrays.asList(invalid, valid); + + // Sanity check + for (final Block nextBlock : nextBlocks) { + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + // Create task + final CompletableFuture> task = + PersistBlockTask.forUnorderedBlocks( + protocolSchedule, protocolContext, nextBlocks, HeaderValidationMode.FULL) + .get(); + + Awaitility.await().atMost(30, SECONDS).until(task::isDone); + + assertThat(task.isCompletedExceptionally()).isFalse(); + assertThat(task.get().size()).isEqualTo(1); + assertThat(task.get().contains(valid)).isTrue(); + assertThat(blockchain.contains(valid.getHash())).isTrue(); + assertThat(blockchain.contains(invalid.getHash())).isFalse(); + } + + @Test + public void cancelBeforeRunning() throws Exception { + blockchainUtil.importFirstBlocks(3); + final Block nextBlock = blockchainUtil.getBlock(3); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + + // Create task + final PersistBlockTask task = + PersistBlockTask.create( + protocolSchedule, protocolContext, nextBlock, HeaderValidationMode.FULL); + + task.cancel(); + final CompletableFuture result = task.run(); + + assertThat(result.isCancelled()).isTrue(); + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } + + @Test + public void cancelAfterRunning() throws Exception { + blockchainUtil.importFirstBlocks(3); + final Block nextBlock = blockchainUtil.getBlock(3); + + // Sanity check + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + + // Create task + final PersistBlockTask task = + PersistBlockTask.create( + protocolSchedule, protocolContext, nextBlock, HeaderValidationMode.FULL); + final PersistBlockTask taskSpy = Mockito.spy(task); + Mockito.doNothing().when(taskSpy).executeTask(); + + final CompletableFuture result = taskSpy.run(); + taskSpy.cancel(); + + assertThat(result.isCancelled()).isTrue(); + assertThat(blockchain.contains(nextBlock.getHash())).isFalse(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTaskTest.java new file mode 100755 index 00000000000..0314bbc6425 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/PipelinedImportChainSegmentTaskTest.java @@ -0,0 +1,433 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer; +import net.consensys.pantheon.ethereum.eth.manager.RespondingEthPeer.Responder; +import net.consensys.pantheon.ethereum.eth.manager.ethtaskutils.RetryingMessageTaskTest; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.EthPV63; +import net.consensys.pantheon.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator.BlockOptions; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import org.junit.Test; + +public class PipelinedImportChainSegmentTaskTest + extends RetryingMessageTaskTest, List> { + + @Override + protected List generateDataToBeRequested() { + final long chainHead = blockchain.getChainHeadBlockNumber(); + final long importSize = 5; + final long startNumber = chainHead - importSize + 1; + final List blocksToImport = new ArrayList<>(); + for (long i = 0; i < importSize; i++) { + blocksToImport.add(getBlockAtNumber(startNumber + i)); + } + return blocksToImport; + } + + private Block getBlockAtNumber(final long number) { + final BlockHeader header = blockchain.getBlockHeader(number).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + return new Block(header, body); + } + + @Override + protected EthTask> createTask(final List requestedData) { + final Block firstBlock = requestedData.get(0); + final Block lastBlock = requestedData.get(requestedData.size() - 1); + final Block previousBlock = getBlockAtNumber(firstBlock.getHeader().getNumber() - 1); + final MutableBlockchain shortBlockchain = createShortChain(firstBlock.getHeader().getNumber()); + final ProtocolContext modifiedContext = + new ProtocolContext<>( + shortBlockchain, + protocolContext.getWorldStateArchive(), + protocolContext.getConsensusState()); + return PipelinedImportChainSegmentTask.forCheckpoints( + protocolSchedule, + modifiedContext, + ethContext, + 1, + new BlockHeader[] {previousBlock.getHeader(), lastBlock.getHeader()}); + } + + @Override + protected void assertResultMatchesExpectation( + final List requestedData, final List response, final EthPeer respondingPeer) { + assertThat(response).isEqualTo(requestedData); + } + + @Test + public void betweenContiguousHeadersSucceeds() { + // Setup a responsive peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Setup task and expectations + final Block firstBlock = getBlockAtNumber(5L); + final Block secondBlock = getBlockAtNumber(6L); + final List expectedResult = Collections.singletonList(secondBlock); + final MutableBlockchain shortBlockchain = createShortChain(firstBlock.getHeader().getNumber()); + final ProtocolContext modifiedContext = + new ProtocolContext<>( + shortBlockchain, + protocolContext.getWorldStateArchive(), + protocolContext.getConsensusState()); + final EthTask> task = + PipelinedImportChainSegmentTask.forCheckpoints( + protocolSchedule, + modifiedContext, + ethContext, + 1, + firstBlock.getHeader(), + secondBlock.getHeader()); + + // Sanity check + assertThat(shortBlockchain.contains(secondBlock.getHash())).isFalse(); + + // Execute task and wait for response + final AtomicReference> actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + + final CompletableFuture> future = task.run(); + respondingPeer.respond(responder); + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertResultMatchesExpectation(expectedResult, actualResult.get(), respondingPeer.getEthPeer()); + } + + @Test + public void betweenUnconnectedHeadersFails() { + final BlockDataGenerator gen = new BlockDataGenerator(); + // Setup a responsive peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Setup data + final Block fakeFirstBlock = gen.block(BlockOptions.create().setBlockNumber(5L)); + final Block firstBlock = getBlockAtNumber(5L); + final Block secondBlock = getBlockAtNumber(6L); + final Block thirdBlock = getBlockAtNumber(7L); + + // Setup task + final MutableBlockchain shortBlockchain = createShortChain(firstBlock.getHeader().getNumber()); + final ProtocolContext modifiedContext = + new ProtocolContext<>( + shortBlockchain, + protocolContext.getWorldStateArchive(), + protocolContext.getConsensusState()); + final EthTask> task = + PipelinedImportChainSegmentTask.forCheckpoints( + protocolSchedule, + modifiedContext, + ethContext, + 1, + fakeFirstBlock.getHeader(), + thirdBlock.getHeader()); + + // Sanity check + assertThat(shortBlockchain.contains(secondBlock.getHash())).isFalse(); + + // Execute task and wait for response + final AtomicReference actualError = new AtomicReference<>(); + final AtomicReference> actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + + final CompletableFuture> future = task.run(); + respondingPeer.respond(responder); + future.whenComplete( + (result, error) -> { + actualResult.set(result); + actualError.set(error); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertThat(actualResult.get()).isNull(); + assertThat(actualError.get()).hasCauseInstanceOf(InvalidBlockException.class); + } + + @Test + public void shouldSyncInSequencesOfChunksSequentially() { + // Setup a responsive peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Setup task for three chunks + final List checkpointHeaders = + LongStream.range(0, 13) + .filter(n -> n % 4 == 0) + .mapToObj(this::getBlockAtNumber) + .map(Block::getHeader) + .collect(Collectors.toList()); + final List expectedResult = + LongStream.range(1, 13).mapToObj(this::getBlockAtNumber).collect(Collectors.toList()); + final MutableBlockchain shortBlockchain = createShortChain(0); + final ProtocolContext modifiedContext = + new ProtocolContext<>( + shortBlockchain, + protocolContext.getWorldStateArchive(), + protocolContext.getConsensusState()); + final EthTask> task = + PipelinedImportChainSegmentTask.forCheckpoints( + protocolSchedule, modifiedContext, ethContext, 1, checkpointHeaders); + + // Execute task and wait for response + final AtomicReference> actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + + final CompletableFuture> future = task.run(); + final CountingResponder countingResponder = CountingResponder.wrap(responder); + + // Import first segment's headers and bodies + respondingPeer.respondTimes(countingResponder, 2); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(1); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(1); + // Import second segment's headers and bodies + respondingPeer.respondTimes(countingResponder, 2); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(2); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(2); + // Import third segment's headers and bodies + respondingPeer.respondTimes(countingResponder, 2); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(3); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(3); + + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertResultMatchesExpectation(expectedResult, actualResult.get(), respondingPeer.getEthPeer()); + } + + @Test + public void shouldPipelineChainSegmentImportsUpToMaxActiveChunks() { + // Setup a responsive peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Setup task and expectations + final List checkpointHeaders = + LongStream.range(0, 13) + .filter(n -> n % 4 == 0) + .mapToObj(this::getBlockAtNumber) + .map(Block::getHeader) + .collect(Collectors.toList()); + final List expectedResult = + LongStream.range(1, 13).mapToObj(this::getBlockAtNumber).collect(Collectors.toList()); + final MutableBlockchain shortBlockchain = createShortChain(0); + final ProtocolContext modifiedContext = + new ProtocolContext<>( + shortBlockchain, + protocolContext.getWorldStateArchive(), + protocolContext.getConsensusState()); + final EthTask> task = + PipelinedImportChainSegmentTask.forCheckpoints( + protocolSchedule, modifiedContext, ethContext, 2, checkpointHeaders); + + // Execute task and wait for response + final AtomicReference> actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + + final CompletableFuture> future = task.run(); + final CountingResponder countingResponder = CountingResponder.wrap(responder); + + // Import first segment's header + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(1); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(0); + // Import first segment's body and second segment's header + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(2); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(1); + // Import second segment's body and third segment's header + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(3); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(2); + // Import third segment's body + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(3); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(3); + + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertResultMatchesExpectation(expectedResult, actualResult.get(), respondingPeer.getEthPeer()); + } + + @Test + public void shouldPipelineChainSegmentImportsWithinMaxActiveChunks() { + // Setup a responsive peer + final Responder responder = RespondingEthPeer.blockchainResponder(blockchain); + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + // Setup task and expectations + final List checkpointHeaders = + LongStream.range(0, 13) + .filter(n -> n % 4 == 0) + .mapToObj(this::getBlockAtNumber) + .map(Block::getHeader) + .collect(Collectors.toList()); + final List expectedResult = + LongStream.range(1, 13).mapToObj(this::getBlockAtNumber).collect(Collectors.toList()); + final MutableBlockchain shortBlockchain = createShortChain(0); + final ProtocolContext modifiedContext = + new ProtocolContext<>( + shortBlockchain, + protocolContext.getWorldStateArchive(), + protocolContext.getConsensusState()); + final EthTask> task = + PipelinedImportChainSegmentTask.forCheckpoints( + protocolSchedule, modifiedContext, ethContext, 3, checkpointHeaders); + + // Execute task and wait for response + final AtomicReference> actualResult = new AtomicReference<>(); + final AtomicBoolean done = new AtomicBoolean(false); + + final CompletableFuture> future = task.run(); + final CountingResponder countingResponder = CountingResponder.wrap(responder); + + // Import first segment's header + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(1); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(0); + // Import first segment's body and second segment's header + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(2); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(1); + // Import second segment's body and third segment's header + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(3); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(2); + // Import third segment's body + respondingPeer.respond(countingResponder); + assertThat(countingResponder.getBlockHeaderMessages()).isEqualTo(3); + assertThat(countingResponder.getBlockBodiesMessages()).isEqualTo(3); + + future.whenComplete( + (result, error) -> { + actualResult.set(result); + done.compareAndSet(false, true); + }); + + assertThat(done).isTrue(); + assertResultMatchesExpectation(expectedResult, actualResult.get(), respondingPeer.getEthPeer()); + } + + private MutableBlockchain createShortChain(final long lastBlockToInclude) { + final BlockHeader genesisHeader = + blockchain.getBlockHeader(BlockHeader.GENESIS_BLOCK_NUMBER).get(); + final BlockBody genesisBody = blockchain.getBlockBody(genesisHeader.getHash()).get(); + final Block genesisBlock = new Block(genesisHeader, genesisBody); + final MutableBlockchain shortChain = + new DefaultMutableBlockchain( + genesisBlock, + new InMemoryKeyValueStorage(), + ScheduleBasedBlockHashFunction.create(protocolSchedule)); + long nextBlock = genesisHeader.getNumber() + 1; + while (nextBlock <= lastBlockToInclude) { + final BlockHeader header = blockchain.getBlockHeader(nextBlock).get(); + final BlockBody body = blockchain.getBlockBody(header.getHash()).get(); + final List receipts = blockchain.getTxReceipts(header.getHash()).get(); + final Block block = new Block(header, body); + shortChain.appendBlock(block, receipts); + nextBlock++; + } + return shortChain; + } + + private static class CountingResponder implements Responder { + + private final Responder delegate; + private int getBlockHeaderMessages = 0; + private int getBlockBodiesMessages = 0; + private int getReceiptsMessages = 0; + private int getNodeDataMessages = 0; + + private static CountingResponder wrap(final Responder delegate) { + return new CountingResponder(delegate); + } + + private CountingResponder(final Responder delegate) { + this.delegate = delegate; + } + + @Override + public Optional respond(final Capability cap, final MessageData msg) { + final MessageData response = null; + switch (msg.getCode()) { + case EthPV62.GET_BLOCK_HEADERS: + getBlockHeaderMessages++; + break; + case EthPV62.GET_BLOCK_BODIES: + getBlockBodiesMessages++; + break; + case EthPV63.GET_RECEIPTS: + getReceiptsMessages++; + break; + case EthPV63.GET_NODE_DATA: + getNodeDataMessages++; + break; + } + return delegate.respond(cap, msg); + } + + public int getBlockHeaderMessages() { + return getBlockHeaderMessages; + } + + public int getBlockBodiesMessages() { + return getBlockBodiesMessages; + } + + public int getReceiptsMessages() { + return getReceiptsMessages; + } + + public int getNodeDataMessages() { + return getNodeDataMessages; + } + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTaskTest.java new file mode 100755 index 00000000000..e6cd68b104f --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeerTaskTest.java @@ -0,0 +1,70 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Before; +import org.junit.Test; + +public class WaitForPeerTaskTest { + private EthProtocolManager ethProtocolManager; + private EthContext ethContext; + + @Before + public void setupTest() { + ethProtocolManager = EthProtocolManagerTestUtil.create(); + ethContext = ethProtocolManager.ethContext(); + } + + @Test + public void completesWhenPeerConnects() throws ExecutionException, InterruptedException { + // Execute task and wait for response + final AtomicBoolean successful = new AtomicBoolean(false); + final EthTask task = WaitForPeerTask.create(ethContext); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + if (error == null) { + successful.compareAndSet(false, true); + } + }); + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + assertThat(successful).isTrue(); + } + + @Test + public void doesNotCompleteWhenNoPeerConnects() throws ExecutionException, InterruptedException { + final AtomicBoolean successful = new AtomicBoolean(false); + final EthTask task = WaitForPeerTask.create(ethContext); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + if (error == null) { + successful.compareAndSet(false, true); + } + }); + + assertThat(successful).isFalse(); + } + + @Test + public void cancel() throws ExecutionException, InterruptedException { + // Execute task + final EthTask task = WaitForPeerTask.create(ethContext); + final CompletableFuture future = task.run(); + + assertThat(future.isDone()).isFalse(); + task.cancel(); + assertThat(future.isDone()).isTrue(); + assertThat(future.isCancelled()).isTrue(); + assertThat(task.run().isCancelled()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTaskTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTaskTest.java new file mode 100755 index 00000000000..182659aeb49 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/sync/tasks/WaitForPeersTaskTest.java @@ -0,0 +1,88 @@ +package net.consensys.pantheon.ethereum.eth.sync.tasks; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManagerTestUtil; +import net.consensys.pantheon.ethereum.eth.manager.EthTask; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Before; +import org.junit.Test; + +public class WaitForPeersTaskTest { + private EthProtocolManager ethProtocolManager; + private EthContext ethContext; + + @Before + public void setupTest() { + ethProtocolManager = EthProtocolManagerTestUtil.create(); + ethContext = ethProtocolManager.ethContext(); + } + + @Test + public void completesWhenPeersConnects() throws ExecutionException, InterruptedException { + // Execute task and wait for response + final AtomicBoolean successful = new AtomicBoolean(false); + final EthTask task = WaitForPeersTask.create(ethContext, 2); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + if (error == null) { + successful.compareAndSet(false, true); + } + }); + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + assertThat(successful).isTrue(); + } + + @Test + public void doesNotCompleteWhenNoPeerConnects() throws ExecutionException, InterruptedException { + final AtomicBoolean successful = new AtomicBoolean(false); + final EthTask task = WaitForPeersTask.create(ethContext, 2); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + if (error == null) { + successful.compareAndSet(false, true); + } + }); + + assertThat(successful).isFalse(); + } + + @Test + public void doesNotCompleteWhenSomePeersConnects() + throws ExecutionException, InterruptedException { + final AtomicBoolean successful = new AtomicBoolean(false); + final EthTask task = WaitForPeersTask.create(ethContext, 2); + final CompletableFuture future = task.run(); + future.whenComplete( + (result, error) -> { + if (error == null) { + successful.compareAndSet(false, true); + } + }); + EthProtocolManagerTestUtil.createPeer(ethProtocolManager); + + assertThat(successful).isFalse(); + } + + @Test + public void cancel() throws ExecutionException, InterruptedException { + // Execute task + final EthTask task = WaitForPeersTask.create(ethContext, 2); + final CompletableFuture future = task.run(); + + assertThat(future.isDone()).isFalse(); + task.cancel(); + assertThat(future.isDone()).isTrue(); + assertThat(future.isCancelled()).isTrue(); + assertThat(task.run().isCancelled()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTrackerTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTrackerTest.java new file mode 100755 index 00000000000..06519ed9f2f --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/PeerTransactionTrackerTest.java @@ -0,0 +1,79 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; + +public class PeerTransactionTrackerTest { + + private final EthPeer ethPeer1 = mock(EthPeer.class); + private final EthPeer ethPeer2 = mock(EthPeer.class); + private final BlockDataGenerator generator = new BlockDataGenerator(); + private final PeerTransactionTracker tracker = new PeerTransactionTracker(); + private final Transaction transaction1 = generator.transaction(); + private final Transaction transaction2 = generator.transaction(); + private final Transaction transaction3 = generator.transaction(); + + @Test + public void shouldTrackTransactionsToSendToPeer() { + tracker.addToPeerSendQueue(ethPeer1, transaction1); + tracker.addToPeerSendQueue(ethPeer1, transaction2); + tracker.addToPeerSendQueue(ethPeer2, transaction3); + + assertThat(tracker.getEthPeersWithUnsentTransactions()).containsOnly(ethPeer1, ethPeer2); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer1)) + .containsOnly(transaction1, transaction2); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer2)).containsOnly(transaction3); + } + + @Test + public void shouldExcludeAlreadySeenTransactionsFromTransactionsToSend() { + tracker.markTransactionsAsSeen(ethPeer1, ImmutableSet.of(transaction2)); + + tracker.addToPeerSendQueue(ethPeer1, transaction1); + tracker.addToPeerSendQueue(ethPeer1, transaction2); + tracker.addToPeerSendQueue(ethPeer2, transaction3); + + assertThat(tracker.getEthPeersWithUnsentTransactions()).containsOnly(ethPeer1, ethPeer2); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer1)).containsOnly(transaction1); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer2)).containsOnly(transaction3); + } + + @Test + public void shouldExcludeAlreadySeenTransactionsAsACollectionFromTransactionsToSend() { + tracker.markTransactionsAsSeen(ethPeer1, ImmutableSet.of(transaction1, transaction2)); + + tracker.addToPeerSendQueue(ethPeer1, transaction1); + tracker.addToPeerSendQueue(ethPeer1, transaction2); + tracker.addToPeerSendQueue(ethPeer2, transaction3); + + assertThat(tracker.getEthPeersWithUnsentTransactions()).containsOnly(ethPeer2); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer1)).isEmpty(); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer2)).containsOnly(transaction3); + } + + @Test + public void shouldClearDataWhenPeerDisconnects() { + tracker.markTransactionsAsSeen(ethPeer1, ImmutableSet.of(transaction1)); + + tracker.addToPeerSendQueue(ethPeer1, transaction2); + tracker.addToPeerSendQueue(ethPeer2, transaction3); + + tracker.onDisconnect(ethPeer1); + + assertThat(tracker.getEthPeersWithUnsentTransactions()).containsOnly(ethPeer2); + + // Should have cleared data that ethPeer1 has already seen transaction1 + tracker.addToPeerSendQueue(ethPeer1, transaction1); + + assertThat(tracker.getEthPeersWithUnsentTransactions()).containsOnly(ethPeer1, ethPeer2); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer1)).containsOnly(transaction1); + assertThat(tracker.claimTransactionsToSendToPeer(ethPeer2)).containsOnly(transaction3); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNode.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNode.java new file mode 100755 index 00000000000..97a00eced0b --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNode.java @@ -0,0 +1,182 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static java.util.Collections.singletonList; +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.util.Preconditions.checkNotNull; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.EthContext; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.NetworkRunner; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.NetworkingConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.RlpxConfiguration; +import net.consensys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalInt; +import java.util.concurrent.CompletableFuture; + +import io.vertx.core.Vertx; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class TestNode implements Closeable { + + private static final Logger LOG = LogManager.getLogger(); + + protected final Integer port; + protected final SECP256K1.KeyPair kp; + protected final P2PNetwork network; + protected final Peer selfPeer; + protected final Map disconnections = new HashMap<>(); + private final TransactionPool transactionPool; + + public TestNode( + final Vertx vertx, + final Integer port, + final SECP256K1.KeyPair kp, + final DiscoveryConfiguration discoveryCfg) { + checkNotNull(vertx); + checkNotNull(discoveryCfg); + + final int listenPort = port != null ? port : 0; + this.kp = kp != null ? kp : SECP256K1.KeyPair.generate(); + + final NetworkingConfiguration networkingConfiguration = + NetworkingConfiguration.create() + .setDiscovery(discoveryCfg) + .setRlpx(RlpxConfiguration.create().setBindPort(listenPort)) + .setSupportedProtocols(EthProtocol.get()); + + final GenesisConfig genesisConfig = GenesisConfig.development(); + final ProtocolSchedule protocolSchedule = genesisConfig.getProtocolSchedule(); + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + final KeyValueStorage kv = new InMemoryKeyValueStorage(); + final MutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisConfig.getBlock(), kv, blockHashFunction); + final WorldStateArchive worldStateArchive = createInMemoryWorldStateArchive(); + genesisConfig.writeStateTo(worldStateArchive.getMutable()); + final ProtocolContext protocolContext = + new ProtocolContext<>(blockchain, worldStateArchive, null); + final EthProtocolManager ethProtocolManager = new EthProtocolManager(blockchain, 1, false, 1); + + final NetworkRunner networkRunner = + NetworkRunner.builder() + .subProtocols(EthProtocol.get()) + .protocolManagers(singletonList(ethProtocolManager)) + .network( + capabilities -> + new NettyP2PNetwork( + vertx, + this.kp, + networkingConfiguration, + capabilities, + ethProtocolManager, + new PeerBlacklist())) + .build(); + network = networkRunner.getNetwork(); + this.port = network.getSelf().getPort(); + network.subscribeDisconnect( + (connection, reason, initiatedByPeer) -> disconnections.put(connection, reason)); + + final EthContext ethContext = ethProtocolManager.ethContext(); + transactionPool = + TransactionPoolFactory.createTransactionPool(protocolSchedule, protocolContext, ethContext); + networkRunner.start(); + + selfPeer = new DefaultPeer(id(), endpoint()); + } + + public BytesValue id() { + return kp.getPublicKey().getEncodedBytes(); + } + + public static String shortId(final BytesValue id) { + return id.slice(62).toString().substring(2); + } + + public String shortId() { + return shortId(id()); + } + + public Endpoint endpoint() { + checkNotNull( + port, "Must either pass port to ctor, or call createNetwork() first to set the port"); + return new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), port, OptionalInt.of(port)); + } + + public Peer selfPeer() { + return selfPeer; + } + + public CompletableFuture connect(final TestNode remoteNode) { + return network.connect(remoteNode.selfPeer()); + } + + @SuppressWarnings("ConstantConditions") + @Override + public void close() throws IOException { + IOException firstEx = null; + try { + network.close(); + } catch (final IOException e) { + if (firstEx == null) { + firstEx = e; + } + LOG.warn("Error closing. Continuing", e); + } + + if (firstEx != null) { + throw new IOException("Unable to close successfully. Wrapping first exception.", firstEx); + } + } + + @Override + public String toString() { + return shortId() + + "@" + + selfPeer.getEndpoint().getHost() + + ':' + + selfPeer.getEndpoint().getTcpPort(); + } + + public void receiveRemoteTransaction(final Transaction transaction) { + transactionPool.addRemoteTransactions(singletonList(transaction)); + } + + public void receiveLocalTransaction(final Transaction transaction) { + transactionPool.addLocalTransaction(transaction); + } + + public int getPendingTransactionCount() { + return transactionPool.getPendingTransactions().size(); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNodeList.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNodeList.java new file mode 100755 index 00000000000..685c8b08962 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TestNodeList.java @@ -0,0 +1,267 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; +import static net.consensys.pantheon.ethereum.eth.transactions.TestNode.shortId; +import static org.apache.logging.log4j.util.Strings.join; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.vertx.core.Vertx; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.Awaitility; +import org.awaitility.Duration; +import org.awaitility.core.ConditionTimeoutException; + +public class TestNodeList implements Closeable { + private static final Logger LOG = LogManager.getLogger(); + protected final List nodes = new ArrayList<>(); + private final Duration MSG_WAIT = new Duration(2, TimeUnit.SECONDS); + + public TestNode create( + final Vertx vertx, + final Integer port, + final SECP256K1.KeyPair kp, + final DiscoveryConfiguration discoveryCfg) + throws IOException { + final TestNode node = new TestNode(vertx, port, kp, discoveryCfg); + nodes.add(node); + return node; + } + + public void startNetworks() { + for (final TestNode node : nodes) { + node.network.run(); + } + } + + public void connectAndAssertAll() + throws InterruptedException, ExecutionException, TimeoutException { + for (int i = 0; i < nodes.size(); i++) { + final TestNode source = nodes.get(i); + for (int j = i + 1; j < nodes.size(); j++) { + final TestNode destination = nodes.get(j); + try { + LOG.info("Attempting to connect source " + source.shortId() + " to dest " + destination); + assertThat(source.connect(destination).get(30L, TimeUnit.SECONDS).getPeer().getNodeId()) + .isEqualTo(destination.id()); + // Wait for the destination node to finish bonding. + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .until(() -> hasConnection(destination, source)); + LOG.info("Successfully connected " + source.shortId() + " to dest " + destination); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + final String msg = + format( + "Error connecting source node %s to destination node %s in time allotted.", + source.shortId(), destination.shortId()); + LOG.error(msg, e); + throw e; + } + } + } + } + + @SuppressWarnings("StringConcatenationInsideStringBufferAppend") + public void assertPeerCounts() { + int errCnt = 0; + final StringBuilder sb = new StringBuilder(); + + final int expectedCount = nodes.size() - 1; // Connections to every other node, but not itself + for (final TestNode node : nodes) { + final int actualCount = node.network.getPeers().size(); + if (expectedCount != actualCount) { + sb.append( + "Node " + + node.shortId() + + " expected " + + expectedCount + + " peer connections, but actually has " + + actualCount + + " peer connections.\n"); + errCnt++; + } + } + final String header = "" + errCnt + " Nodes have missing peer connections.\n"; + assertThat(errCnt).describedAs(header + sb).isEqualTo(0); + } + + private boolean hasConnection(final TestNode node1, final TestNode node2) { + for (final PeerConnection peer : node1.network.getPeers()) { + if (node2.id().equals(peer.getPeer().getNodeId())) { + return true; + } + } + return false; + } + + private Collection findMissingConnections(final TestNode testNode) { + final Collection missingConnections = new HashSet<>(); + for (final TestNode node : nodes) { + if (testNode == node) continue; // don't expect connections to self + if (!hasConnection(testNode, node)) { + missingConnections.add(node); + } + } + return missingConnections; + } + + private Collection findExtraConnections(final TestNode testNode) { + final Collection extraConnections = new HashSet<>(nodes); + extraConnections.removeIf(next -> hasConnection(testNode, next) || testNode == next); + return extraConnections; + } + + /** Assert that all Nodes have exactly 1 connection to each other node */ + @SuppressWarnings("StringConcatenationInsideStringBufferAppend") + public void assertPeerConnections() { + int errCnt = 0; + final StringBuilder sb = new StringBuilder(); + Collection incorrectConnections; + + for (final TestNode node1 : nodes) { + incorrectConnections = findMissingConnections(node1); + for (final TestNode missingConnection : incorrectConnections) { + sb.append( + "Node " + + node1.shortId() + + " is missing connection to node " + + missingConnection.shortId() + + ".\n"); + errCnt++; + } + + incorrectConnections = findExtraConnections(node1); + for (final TestNode extraConnection : incorrectConnections) { + sb.append( + "Node " + + node1.shortId() + + " has unexpected connection to node " + + extraConnection.shortId() + + ".\n"); + errCnt++; + } + } + final String header = "There are " + errCnt + " incorrect peer connections.\n"; + assertThat(errCnt).describedAs(header + sb).isEqualTo(0); + } + + public void assertPendingTransactionCounts(final int... expected) { + checkNotNull(expected); + checkArgument( + expected.length == nodes.size(), + "Expected values for sd nodes, but got %s.", + expected.length, + nodes.size()); + int errCnt = 0; + final StringBuilder sb = new StringBuilder(); + int i = 0; + for (final TestNode node : nodes) { + final int expectedCnt = expected[i]; + try { + Awaitility.await() + .atMost(MSG_WAIT) + .until(() -> node.getPendingTransactionCount() == expectedCnt); + } catch (final ConditionTimeoutException e) { + /* Ignore ConditionTimeoutException. We just want a fancy wait here. The real check will happen + below and has a proper exception message + */ + } + + final int actual = node.getPendingTransactionCount(); + if (actual != expected[i]) { + errCnt++; + final String msg = + format( + "Node %s expected %d pending txs, but has %d.\n", + node.shortId(), expected[i], actual); + sb.append(msg); + } + i++; + } + final String header = "Nodes have " + errCnt + " incorrect Pending Tx pool sizes.\n"; + assertThat(errCnt).describedAs(header + sb).isEqualTo(0); + } + + /** Assert that there were no node disconnections reported from the P2P network */ + public void assertNoNetworkDisconnections() { + int errCnt = 0; + final StringBuilder sb = new StringBuilder(); + for (final TestNode node : nodes) { + for (final Map.Entry entry : + node.disconnections.entrySet()) { + final PeerConnection peer = entry.getKey(); + final String peerString = peer.getPeer().getNodeId() + "@" + peer.getRemoteAddress(); + final String unsentTxMsg = + "Node " + + node.shortId() + + " has received a disconnection from " + + peerString + + " for " + + entry.getValue() + + "\n"; + sb.append(unsentTxMsg); + errCnt++; + } + } + final String header = "Nodes have received " + errCnt + " disconnections.\n"; + assertThat(errCnt).describedAs(header + sb).isEqualTo(0); + } + + /** Logs the Peer connections for each node. */ + public void logPeerConnections() { + final List connStr = new ArrayList<>(); + for (final TestNode node : nodes) { + for (final PeerConnection peer : node.network.getPeers()) { + final String localString = node.shortId() + "@" + peer.getLocalAddress(); + final String peerString = + shortId(peer.getPeer().getNodeId()) + "@" + peer.getRemoteAddress(); + connStr.add("Connection: " + localString + " to " + peerString); + } + } + LOG.info("TestNodeList Connections:\n" + join(connStr, '\n')); + } + + @Override + public void close() throws IOException { + IOException firstEx = null; + + for (final Iterator it = nodes.iterator(); it.hasNext(); ) + try { + final TestNode node = it.next(); + node.close(); + it.remove(); + } catch (final IOException e) { + if (firstEx == null) firstEx = e; + LOG.warn("Error closing. Continuing", e); + } + if (firstEx != null) + throw new IOException("Unable to close node resources. Wrapping first exception.", firstEx); + } + + public TestNode get(final int index) { + return nodes.get(index); + } + + public void add(final int index, final TestNode element) { + nodes.add(index, element); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolPropagationTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolPropagationTest.java new file mode 100755 index 00000000000..05a0a4c1f17 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionPoolPropagationTest.java @@ -0,0 +1,121 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.PrivateKey; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionTestFixture; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import org.awaitility.Awaitility; +import org.junit.After; +import org.junit.Before; +import org.junit.ComparisonFailure; +import org.junit.Test; + +public class TransactionPoolPropagationTest { + + final DiscoveryConfiguration noDiscovery = DiscoveryConfiguration.create().setActive(false); + + private Vertx vertx; + + @Before + public void setUp() { + vertx = Vertx.vertx(); + } + + @After + public void tearDown() { + vertx.close(); + } + + /** Helper to do common setup tasks. */ + private void initTest(final TestNodeList txNodes) throws Exception { + txNodes.startNetworks(); + txNodes.connectAndAssertAll(); + txNodes.logPeerConnections(); + txNodes.assertPeerCounts(); + txNodes.assertPeerConnections(); + } + + /** Helper to do common wrapup tasks. */ + private void wrapup(final TestNodeList txNodes) { + txNodes.assertNoNetworkDisconnections(); + txNodes.assertPeerCounts(); + txNodes.assertPeerConnections(); + } + + /** + * 2nd order test to verify the framework correctly fails if a disconnect occurs It could have a + * more detailed exception check - more than just the class. + */ + @Test(expected = ComparisonFailure.class) + public void disconnectShouldThrow() throws Exception { + + try (final TestNodeList txNodes = new TestNodeList()) { + // Create & Start Nodes + final TestNode node1 = txNodes.create(vertx, null, null, noDiscovery); + txNodes.create(vertx, null, null, noDiscovery); + txNodes.create(vertx, null, null, noDiscovery); + + initTest(txNodes); + + node1.network.getPeers().iterator().next().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + + wrapup(txNodes); + } + } + + /** + * Simulate a 4-node cluster. Send at least 1 Tx to each node, and multiple Tx to at least one + * node. Verify that all nodes get the correct number of pending transactions. + */ + @Test + public void shouldPropagateLocalAndRemoteTransactions() throws Exception { + try (final TestNodeList nodes = new TestNodeList()) { + // Create & Start Nodes + final TestNode node1 = nodes.create(vertx, null, null, noDiscovery); + final TestNode node2 = nodes.create(vertx, null, null, noDiscovery); + final TestNode node3 = nodes.create(vertx, null, null, noDiscovery); + final TestNode node4 = nodes.create(vertx, null, null, noDiscovery); + final KeyPair keyPair = + KeyPair.create( + PrivateKey.create( + Bytes32.fromHexString( + "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63"))); + final TransactionTestFixture transactionBuilder = new TransactionTestFixture(); + transactionBuilder.gasLimit(1_000_000); + final Transaction transaction1 = transactionBuilder.nonce(0).createTransaction(keyPair); + final Transaction transaction2 = transactionBuilder.nonce(1).createTransaction(keyPair); + final Transaction transaction3 = transactionBuilder.nonce(2).createTransaction(keyPair); + final Transaction transaction4 = transactionBuilder.nonce(3).createTransaction(keyPair); + final Transaction transaction5 = transactionBuilder.nonce(4).createTransaction(keyPair); + initTest(nodes); + node1.receiveRemoteTransaction(transaction1); + waitForPendingTransactionCounts(nodes, 1); + + node2.receiveRemoteTransaction(transaction2); + waitForPendingTransactionCounts(nodes, 2); + + node3.receiveRemoteTransaction(transaction3); + waitForPendingTransactionCounts(nodes, 3); + + node4.receiveRemoteTransaction(transaction4); + waitForPendingTransactionCounts(nodes, 4); + + node3.receiveLocalTransaction(transaction5); + waitForPendingTransactionCounts(nodes, 5); + } + } + + private void waitForPendingTransactionCounts(final TestNodeList nodes, final int expected) { + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .untilAsserted( + () -> nodes.assertPendingTransactionCounts(expected, expected, expected, expected)); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessorTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessorTest.java new file mode 100755 index 00000000000..0c379901590 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageProcessorTest.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static java.util.Arrays.asList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.messages.TransactionsMessage; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; + +public class TransactionsMessageProcessorTest { + + private final TransactionPool transactionPool = mock(TransactionPool.class); + private final PeerTransactionTracker transactionTracker = mock(PeerTransactionTracker.class); + private final EthPeer peer1 = mock(EthPeer.class); + + private final BlockDataGenerator generator = new BlockDataGenerator(); + private final Transaction transaction1 = generator.transaction(); + private final Transaction transaction2 = generator.transaction(); + private final Transaction transaction3 = generator.transaction(); + + private final TransactionsMessageProcessor messageHandler = + new TransactionsMessageProcessor(transactionTracker, transactionPool); + + @Test + public void shouldMarkAllReceivedTransactionsAsSeen() { + messageHandler.processTransactionsMessage( + peer1, TransactionsMessage.create(asList(transaction1, transaction2, transaction3))); + + verify(transactionTracker) + .markTransactionsAsSeen(peer1, ImmutableSet.of(transaction1, transaction2, transaction3)); + } + + @Test + public void shouldAddReceivedTransactionsToTransactionPool() { + messageHandler.processTransactionsMessage( + peer1, TransactionsMessage.create(asList(transaction1, transaction2, transaction3))); + + verify(transactionPool) + .addRemoteTransactions(ImmutableSet.of(transaction1, transaction2, transaction3)); + } +} diff --git a/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSenderTest.java b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSenderTest.java new file mode 100755 index 00000000000..0fa051ba2e8 --- /dev/null +++ b/ethereum/eth/src/test/java/net/consensys/pantheon/ethereum/eth/transactions/TransactionsMessageSenderTest.java @@ -0,0 +1,93 @@ +package net.consensys.pantheon.ethereum.eth.transactions; + +import static com.google.common.collect.Sets.newHashSet; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.eth.manager.EthPeer; +import net.consensys.pantheon.ethereum.eth.messages.EthPV62; +import net.consensys.pantheon.ethereum.eth.messages.TransactionsMessage; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.util.List; +import java.util.Set; +import java.util.stream.IntStream; + +import com.google.common.collect.Sets; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class TransactionsMessageSenderTest { + + private final EthPeer peer1 = mock(EthPeer.class); + private final EthPeer peer2 = mock(EthPeer.class); + private final BlockDataGenerator generator = new BlockDataGenerator(); + private final Transaction transaction1 = generator.transaction(); + private final Transaction transaction2 = generator.transaction(); + private final Transaction transaction3 = generator.transaction(); + + private final PeerTransactionTracker transactionTracker = new PeerTransactionTracker(); + private final TransactionsMessageSender messageSender = + new TransactionsMessageSender(transactionTracker); + + @Test + public void shouldSendTransactionsToEachPeer() throws Exception { + transactionTracker.addToPeerSendQueue(peer1, transaction1); + transactionTracker.addToPeerSendQueue(peer1, transaction2); + transactionTracker.addToPeerSendQueue(peer2, transaction3); + + messageSender.sendTransactionsToPeers(); + + verify(peer1).send(transactionsMessageContaining(transaction1, transaction2)); + verify(peer2).send(transactionsMessageContaining(transaction3)); + verifyNoMoreInteractions(peer1, peer2); + } + + @Test + public void shouldSendTransactionsInBatches() throws Exception { + final Set fifteenTransactions = + IntStream.range(0, 15).mapToObj(number -> generator.transaction()).collect(toSet()); + fifteenTransactions.forEach( + transaction -> transactionTracker.addToPeerSendQueue(peer1, transaction)); + + messageSender.sendTransactionsToPeers(); + + final ArgumentCaptor messageDataArgumentCaptor = + ArgumentCaptor.forClass(MessageData.class); + verify(peer1, times(2)).send(messageDataArgumentCaptor.capture()); + + final List sentMessages = messageDataArgumentCaptor.getAllValues(); + + assertThat(sentMessages).hasSize(2); + assertThat(sentMessages).allMatch(message -> message.getCode() == EthPV62.TRANSACTIONS); + final Set firstBatch = getTransactionsFromMessage(sentMessages.get(0)); + final Set secondBatch = getTransactionsFromMessage(sentMessages.get(1)); + + assertThat(firstBatch).hasSize(10); + assertThat(secondBatch).hasSize(5); + + assertThat(Sets.union(firstBatch, secondBatch)).isEqualTo(fifteenTransactions); + } + + private MessageData transactionsMessageContaining(final Transaction... transactions) { + return argThat( + message -> { + final Set actualSentTransactions = getTransactionsFromMessage(message); + final Set expectedTransactions = newHashSet(transactions); + return message.getCode() == EthPV62.TRANSACTIONS + && actualSentTransactions.equals(expectedTransactions); + }); + } + + private Set getTransactionsFromMessage(final MessageData message) { + final TransactionsMessage transactionsMessage = TransactionsMessage.readFrom(message); + return newHashSet(transactionsMessage.transactions(Transaction::readFrom)); + } +} diff --git a/ethereum/eth/src/test/resources/50.blocks b/ethereum/eth/src/test/resources/50.blocks new file mode 100755 index 0000000000000000000000000000000000000000..0802ad0a6035b543b71e0d4c435e95b0db0cac6d GIT binary patch literal 30779 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 genesisConfig; + + private final ProtocolSchedule protocolSchedule; + + private final List blocks; + + private final Block genesisBlock; + + public BlockchainImporter(final URL blocksUrl, final String genesisJson) throws Exception { + protocolSchedule = MainnetProtocolSchedule.fromConfig(new JsonObject(genesisJson)); + + blocks = new ArrayList<>(); + try (final RawBlockIterator iterator = + new RawBlockIterator( + Paths.get(blocksUrl.toURI()), + rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash))) { + while (iterator.hasNext()) { + blocks.add(iterator.next()); + } + } + + genesisBlock = blocks.get(0); + genesisConfig = GenesisConfig.fromJson(genesisJson, protocolSchedule); + } + + public GenesisConfig getGenesisConfig() { + return genesisConfig; + } + + public ProtocolSchedule getProtocolSchedule() { + return protocolSchedule; + } + + public List getBlocks() { + return blocks; + } + + public Block getGenesisBlock() { + return genesisBlock; + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseKey.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseKey.java new file mode 100755 index 00000000000..3258cf33062 --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseKey.java @@ -0,0 +1,21 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +public enum JsonRpcResponseKey { + COINBASE, + DIFFICULTY, + EXTRA_DATA, + GAS_LIMIT, + GAS_USED, + LOGS_BLOOM, + MIX_HASH, + NONCE, + NUMBER, + OMMERS_HASH, + PARENT_HASH, + RECEIPTS_ROOT, + SIZE, + STATE_ROOT, + TIMESTAMP, + TOTAL_DIFFICULTY, + TRANSACTION_ROOT +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseUtils.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseUtils.java new file mode 100755 index 00000000000..4d52350cdcd --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcResponseUtils.java @@ -0,0 +1,205 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.COINBASE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.EXTRA_DATA; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_LIMIT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_USED; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.LOGS_BLOOM; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.MIX_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NONCE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NUMBER; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.OMMERS_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.PARENT_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.RECEIPTS_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.SIZE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.STATE_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TIMESTAMP; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TOTAL_DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TRANSACTION_ROOT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionCompleteResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionHashResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionResult; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; + +public class JsonRpcResponseUtils { + + /** Hex is base 16 */ + private static final int HEX_RADIX = 16; + + /** @param values hex encoded values. */ + public JsonRpcResponse response(final Map values) { + return response(values, new ArrayList<>()); + } + + /** @param values hex encoded values. */ + public JsonRpcResponse response( + final Map values, final List transactions) { + + final Hash mixHash = hash(values.get(MIX_HASH)); + final Hash parentHash = hash(values.get(PARENT_HASH)); + final Hash ommersHash = hash(values.get(OMMERS_HASH)); + final Address coinbase = address(values.get(COINBASE)); + final Hash stateRoot = hash(values.get(STATE_ROOT)); + final Hash transactionsRoot = hash(values.get(TRANSACTION_ROOT)); + final Hash receiptsRoot = hash(values.get(RECEIPTS_ROOT)); + final LogsBloomFilter logsBloom = logsBloom(values.get(LOGS_BLOOM)); + final UInt256 difficulty = unsignedInt256(values.get(DIFFICULTY)); + final BytesValue extraData = bytes(values.get(EXTRA_DATA)); + final BlockHashFunction hashFunction = MainnetBlockHashFunction::createHash; + final long number = unsignedLong(values.get(NUMBER)); + final long gasLimit = unsignedLong(values.get(GAS_LIMIT)); + final long gasUsed = unsignedLong(values.get(GAS_USED)); + final long timestamp = unsignedLong(values.get(TIMESTAMP)); + final long nonce = unsignedLong(values.get(NONCE)); + final UInt256 totalDifficulty = unsignedInt256(values.get(TOTAL_DIFFICULTY)); + final int size = unsignedInt(values.get(SIZE)); + + final List ommers = new ArrayList<>(); + + final BlockHeader header = + new BlockHeader( + parentHash, + ommersHash, + coinbase, + stateRoot, + transactionsRoot, + receiptsRoot, + logsBloom, + difficulty, + number, + gasLimit, + gasUsed, + timestamp, + extraData, + mixHash, + nonce, + hashFunction); + + return new JsonRpcSuccessResponse( + null, new BlockResult(header, transactions, ommers, totalDifficulty, size)); + } + + public List transactions(final String... values) { + final List nodes = new ArrayList<>(values.length); + + for (int i = 0; i < values.length; i++) { + nodes.add(new TransactionHashResult(values[i])); + } + + return nodes; + } + + public List transactions(final TransactionResult... transactions) { + final List list = new ArrayList<>(transactions.length); + + for (final TransactionResult transaction : transactions) { + list.add(transaction); + } + + return list; + } + + public TransactionResult transaction( + final String blockHash, + final String blockNumber, + final String fromAddress, + final String gas, + final String gasPrice, + final String hash, + final String input, + final String nonce, + final String toAddress, + final String transactionIndex, + final String value, + final String v, + final String r, + final String s) { + + final Transaction transaction = mock(Transaction.class); + when(transaction.getGasPrice()).thenReturn(Wei.fromHexString(gasPrice)); + when(transaction.getNonce()).thenReturn(unsignedLong(nonce)); + when(transaction.getV()).thenReturn(bigInteger(v).intValue()); + when(transaction.getR()).thenReturn(bigInteger(r)); + when(transaction.getS()).thenReturn(bigInteger(s)); + when(transaction.hash()).thenReturn(hash(hash)); + when(transaction.getTo()).thenReturn(Optional.ofNullable(address(toAddress))); + when(transaction.getSender()).thenReturn(address(fromAddress)); + when(transaction.getPayload()).thenReturn(bytes(input)); + when(transaction.getValue()).thenReturn(wei(value)); + when(transaction.getGasLimit()).thenReturn(unsignedLong(gas)); + + return new TransactionCompleteResult( + new TransactionWithMetadata( + transaction, + unsignedLong(blockNumber), + Hash.fromHexString(blockHash), + unsignedInt(transactionIndex))); + } + + private int unsignedInt(final String value) { + final String hex = removeHexPrefix(value); + return new BigInteger(hex, HEX_RADIX).intValue(); + } + + private long unsignedLong(final String value) { + final String hex = removeHexPrefix(value); + return new BigInteger(hex, HEX_RADIX).longValue(); + } + + private Hash hash(final String hex) { + return Hash.fromHexString(hex); + } + + private String removeHexPrefix(final String prefixedHex) { + return prefixedHex.startsWith("0x") ? prefixedHex.substring(2) : prefixedHex; + } + + private BigInteger bigInteger(final String hex) { + return new BigInteger(removeHexPrefix(hex), HEX_RADIX); + } + + private Wei wei(final String hex) { + return Wei.fromHexString(hex); + } + + private Address address(final String hex) { + return Address.fromHexString(hex); + } + + private LogsBloomFilter logsBloom(final String hex) { + return LogsBloomFilter.fromHexString(hex); + } + + private UInt256 unsignedInt256(final String hex) { + return UInt256.fromHexString(hex); + } + + private BytesValue bytes(final String hex) { + return BytesValue.fromHexString(hex); + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java new file mode 100755 index 00000000000..c8aed9700b4 --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestMethodsFactory.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterIdGenerator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; + +import java.util.HashSet; +import java.util.Map; + +/** Provides a facade to construct the JSON-RPC component. */ +public class JsonRpcTestMethodsFactory { + + private static final String CLIENT_VERSION = "TestClientVersion/0.1.0"; + + private final BlockchainImporter importer; + + public JsonRpcTestMethodsFactory(final BlockchainImporter importer) { + this.importer = importer; + } + + public Map methods(final String chainId) { + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + final WorldStateArchive stateArchive = + new WorldStateArchive(new KeyValueStorageWorldStateStorage(keyValueStorage)); + + importer.getGenesisConfig().writeStateTo(stateArchive.getMutable(Hash.EMPTY_TRIE_HASH)); + + final MutableBlockchain blockchain = + new DefaultMutableBlockchain( + importer.getGenesisBlock(), keyValueStorage, MainnetBlockHashFunction::createHash); + final ProtocolContext context = new ProtocolContext<>(blockchain, stateArchive, null); + + for (final Block block : importer.getBlocks()) { + final ProtocolSchedule protocolSchedule = importer.getProtocolSchedule(); + final ProtocolSpec protocolSpec = + protocolSchedule.getByBlockNumber(block.getHeader().getNumber()); + final BlockImporter blockImporter = protocolSpec.getBlockImporter(); + blockImporter.importBlock(context, block, HeaderValidationMode.FULL); + } + + final BlockchainQueries blockchainQueries = new BlockchainQueries(blockchain, stateArchive); + + final Synchronizer synchronizer = mock(Synchronizer.class); + final P2PNetwork peerDiscovery = mock(P2PNetwork.class); + final TransactionPool transactionPool = mock(TransactionPool.class); + final FilterManager filterManager = + new FilterManager(blockchainQueries, transactionPool, new FilterIdGenerator()); + final MiningCoordinator miningCoordinator = mock(MiningCoordinator.class); + + return new JsonRpcMethodsFactory() + .methods( + CLIENT_VERSION, + chainId, + peerDiscovery, + blockchainQueries, + synchronizer, + MainnetProtocolSchedule.create(), + filterManager, + transactionPool, + miningCoordinator, + new HashSet<>(), + JsonRpcConfiguration.DEFAULT_JSON_RPC_APIS); + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthCallIntegrationTest.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthCallIntegrationTest.java new file mode 100755 index 00000000000..da3e266e06c --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthCallIntegrationTest.java @@ -0,0 +1,168 @@ +package net.consensys.pantheon.ethereum.jsonrpc.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import net.consensys.pantheon.ethereum.jsonrpc.BlockchainImporter; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcTestMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.net.URL; +import java.util.Map; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EthCallIntegrationTest { + + private static final String CHAIN_ID = "6986785976597"; + private static JsonRpcTestMethodsFactory BLOCKCHAIN; + + private JsonRpcMethod method; + + @BeforeClass + public static void setUpOnce() throws Exception { + final URL blocksUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + final String gensisjson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + BLOCKCHAIN = new JsonRpcTestMethodsFactory(new BlockchainImporter(blocksUrl, gensisjson)); + } + + @Before + public void setUp() { + final Map methods = BLOCKCHAIN.methods(CHAIN_ID); + method = methods.get("eth_call"); + } + + @Test + public void shouldReturnExpectedResultForCallAtLatestBlock() { + final CallParameter callParameter = + new CallParameter( + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + null, + null, + null, + "0x12a7b914"); + final JsonRpcRequest request = requestWithParams(callParameter, "latest"); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse( + null, "0x0000000000000000000000000000000000000000000000000000000000000001"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnExpectedResultForCallAtSpecificBlock() { + final CallParameter callParameter = + new CallParameter( + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + null, + null, + null, + "0x12a7b914"); + final JsonRpcRequest request = requestWithParams(callParameter, "0x8"); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse( + null, "0x0000000000000000000000000000000000000000000000000000000000000000"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnInvalidRequestWhenMissingToField() { + final CallParameter callParameter = + new CallParameter( + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, null, null, null, "0x12a7b914"); + final JsonRpcRequest request = requestWithParams(callParameter, "latest"); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasNoCause() + .hasMessage("Missing \"to\" field in call arguments"); + } + + @Test + public void shouldReturnErrorWithGasLimitTooLow() { + final CallParameter callParameter = + new CallParameter( + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x0", + null, + null, + "0x12a7b914"); + final JsonRpcRequest request = requestWithParams(callParameter, "latest"); + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.INTRINSIC_GAS_EXCEEDS_LIMIT); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnErrorWithGasPriceTooHigh() { + final CallParameter callParameter = + new CallParameter( + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + null, + "0x10000000000000", + null, + "0x12a7b914"); + final JsonRpcRequest request = requestWithParams(callParameter, "latest"); + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnEmptyHashResultForCallWithOnlyToField() { + final CallParameter callParameter = + new CallParameter( + null, "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", null, null, null, null); + final JsonRpcRequest request = requestWithParams(callParameter, "latest"); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest("2.0", "eth_call", params); + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthEstimateGasIntegrationTest.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthEstimateGasIntegrationTest.java new file mode 100755 index 00000000000..10f4e56ed54 --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthEstimateGasIntegrationTest.java @@ -0,0 +1,134 @@ +package net.consensys.pantheon.ethereum.jsonrpc.methods; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.BlockchainImporter; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcTestMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.net.URL; +import java.util.Map; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EthEstimateGasIntegrationTest { + + private static final String CHAIN_ID = "6986785976597"; + private static JsonRpcTestMethodsFactory BLOCKCHAIN; + + private JsonRpcMethod method; + + @BeforeClass + public static void setUpOnce() throws Exception { + final URL blocksUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + final String genesisJson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + BLOCKCHAIN = new JsonRpcTestMethodsFactory(new BlockchainImporter(blocksUrl, genesisJson)); + } + + @Before + public void setUp() { + final Map methods = BLOCKCHAIN.methods(CHAIN_ID); + method = methods.get("eth_estimateGas"); + } + + @Test + public void shouldReturnExpectedValueForEmptyCallParameter() { + final CallParameter callParameter = new CallParameter(null, null, null, null, null, null); + final JsonRpcRequest request = requestWithParams(callParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x5208"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnExpectedValueForTransfer() { + final CallParameter callParameter = + new CallParameter( + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x8888f1f195afa192cfee860698584c030f4c9db1", + null, + null, + "0x1", + null); + final JsonRpcRequest request = requestWithParams(callParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x5208"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnExpectedValueForContractDeploy() { + final CallParameter callParameter = + new CallParameter( + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + null, + null, + null, + null, + "0x608060405234801561001057600080fd5b50610157806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bdab8bf146100515780639ae97baa14610068575b600080fd5b34801561005d57600080fd5b5061006661007f565b005b34801561007457600080fd5b5061007d6100b9565b005b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60016040518082815260200191505060405180910390a1565b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60026040518082815260200191505060405180910390a17fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60036040518082815260200191505060405180910390a15600a165627a7a7230582010ddaa52e73a98c06dbcd22b234b97206c1d7ed64a7c048e10c2043a3d2309cb0029"); + final JsonRpcRequest request = requestWithParams(callParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x1b551"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldIgnoreGasLimitAndGasPriceAndReturnExpectedValue() { + final CallParameter callParameter = + new CallParameter( + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + null, + "0x1", + "0x9999999999", + null, + "0x608060405234801561001057600080fd5b50610157806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bdab8bf146100515780639ae97baa14610068575b600080fd5b34801561005d57600080fd5b5061006661007f565b005b34801561007457600080fd5b5061007d6100b9565b005b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60016040518082815260200191505060405180910390a1565b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60026040518082815260200191505060405180910390a17fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60036040518082815260200191505060405180910390a15600a165627a7a7230582010ddaa52e73a98c06dbcd22b234b97206c1d7ed64a7c048e10c2043a3d2309cb0029"); + final JsonRpcRequest request = requestWithParams(callParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x1b551"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnExpectedValueForInsufficientGas() { + final CallParameter callParameter = new CallParameter(null, null, "0x1", null, null, null); + final JsonRpcRequest request = requestWithParams(callParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, "0x5208"); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest("2.0", "eth_estimateGas", params); + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByHashIntegrationTest.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByHashIntegrationTest.java new file mode 100755 index 00000000000..905c597222c --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByHashIntegrationTest.java @@ -0,0 +1,200 @@ +package net.consensys.pantheon.ethereum.jsonrpc.methods; + +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.COINBASE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.EXTRA_DATA; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_LIMIT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_USED; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.LOGS_BLOOM; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.MIX_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NONCE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NUMBER; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.OMMERS_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.PARENT_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.RECEIPTS_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.SIZE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.STATE_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TIMESTAMP; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TOTAL_DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TRANSACTION_ROOT; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.BlockchainImporter; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseUtils; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcTestMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionResult; + +import java.net.URL; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EthGetBlockByHashIntegrationTest { + + private Map methods; + private static JsonRpcTestMethodsFactory BLOCKCHAIN; + private final JsonRpcResponseUtils responseUtils = new JsonRpcResponseUtils(); + private static final String CHAIN_ID = "6986785976597"; + private final String ETH_METHOD = "eth_getBlockByHash"; + private final String JSON_RPC_VERSION = "2.0"; + private final String ZERO_HASH = String.valueOf(Hash.ZERO); + + @BeforeClass + public static void setUpOnce() throws Exception { + final URL blocksUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + final String gensisjson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + BLOCKCHAIN = new JsonRpcTestMethodsFactory(new BlockchainImporter(blocksUrl, gensisjson)); + } + + @Before + public void setUp() { + methods = BLOCKCHAIN.methods(CHAIN_ID); + } + + @Test + public void returnCorrectEthMethodName() { + assertThat(ethGetBlockByHash().getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void returnEmptyResponseIfBlockNotFound() { + final JsonRpcResponse expected = new JsonRpcSuccessResponse(null, null); + + final JsonRpcMethod method = ethGetBlockByHash(); + final JsonRpcResponse actual = method.response(requestWithParams(ZERO_HASH, true)); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + @Test + public void returnFullTransactionIfBlockFound() { + final Map expectedResult = new EnumMap<>(JsonRpcResponseKey.class); + expectedResult.put(NUMBER, "0x1"); + expectedResult.put( + MIX_HASH, "0x552dd5ae3ccb7a278c6cedda27e48963101be56526a8ee8d70e8227f2c08edf0"); + expectedResult.put( + PARENT_HASH, "0xf8f01382f5636d02edac7fff679a6feb7a572d37a395daaab77938feb6fe217f"); + expectedResult.put(NONCE, "0xbd2b1f0ebba7b989"); + expectedResult.put( + OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + expectedResult.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + expectedResult.put( + TRANSACTION_ROOT, "0x3ccbb984a0a736604acae327d9b643f8e75c7931cb2c6ac10dab4226e2e4c5a3"); + expectedResult.put( + STATE_ROOT, "0xee57559895449b8dbd0a096b2999cf97b517b645ec8db33c7f5934778672263e"); + expectedResult.put( + RECEIPTS_ROOT, "0xa2bd925fcbb8b1ec39612553b17c9265ab198f5af25cc564655114bf5a28c75d"); + expectedResult.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + expectedResult.put(DIFFICULTY, "0x20000"); + expectedResult.put(TOTAL_DIFFICULTY, "0x40000"); + expectedResult.put(EXTRA_DATA, "0x"); + expectedResult.put(SIZE, "0x96a"); + expectedResult.put(GAS_LIMIT, "0x2fefd8"); + expectedResult.put(GAS_USED, "0x78674"); + expectedResult.put(TIMESTAMP, "0x561bc2e0"); + final List transactions = + responseUtils.transactions( + responseUtils.transaction( + "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", + "0x1", + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x2fefd8", + "0x1", + "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed", + "0x5b5b610705806100106000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063102accc11461012c57806312a7b9141461013a5780631774e6461461014c5780631e26fd331461015d5780631f9030371461016e578063343a875d1461018057806338cc4831146101955780634e7ad367146101bd57806357cb2fc4146101cb57806365538c73146101e057806368895979146101ee57806376bc21d9146102005780639a19a9531461020e5780639dc2c8f51461021f578063a53b1c1e1461022d578063a67808571461023e578063b61c05031461024c578063c2b12a731461025a578063d2282dc51461026b578063e30081a01461027c578063e8beef5b1461028d578063f38b06001461029b578063f5b53e17146102a9578063fd408767146102bb57005b6101346104d6565b60006000f35b61014261039b565b8060005260206000f35b610157600435610326565b60006000f35b6101686004356102c9565b60006000f35b610176610442565b8060005260206000f35b6101886103d3565b8060ff1660005260206000f35b61019d610413565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b6101c56104c5565b60006000f35b6101d36103b7565b8060000b60005260206000f35b6101e8610454565b60006000f35b6101f6610401565b8060005260206000f35b61020861051f565b60006000f35b6102196004356102e5565b60006000f35b610227610693565b60006000f35b610238600435610342565b60006000f35b610246610484565b60006000f35b610254610493565b60006000f35b61026560043561038d565b60006000f35b610276600435610350565b60006000f35b61028760043561035e565b60006000f35b6102956105b4565b60006000f35b6102a3610547565b60006000f35b6102b16103ef565b8060005260206000f35b6102c3610600565b60006000f35b80600060006101000a81548160ff021916908302179055505b50565b80600060016101000a81548160ff02191690837f01000000000000000000000000000000000000000000000000000000000000009081020402179055505b50565b80600060026101000a81548160ff021916908302179055505b50565b806001600050819055505b50565b806002600050819055505b50565b80600360006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908302179055505b50565b806004600050819055505b50565b6000600060009054906101000a900460ff1690506103b4565b90565b6000600060019054906101000a900460000b90506103d0565b90565b6000600060029054906101000a900460ff1690506103ec565b90565b600060016000505490506103fe565b90565b60006002600050549050610410565b90565b6000600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905061043f565b90565b60006004600050549050610451565b90565b7f65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be5806000602a81526020016000a15b565b6000602a81526020016000a05b565b60017f81933b308056e7e85668661dcd102b1f22795b4431f9cf4625794f381c271c6b6000602a81526020016000a25b565b60016000602a81526020016000a15b565b3373ffffffffffffffffffffffffffffffffffffffff1660017f0e216b62efbb97e751a2ce09f607048751720397ecfb9eef1e48a6644948985b6000602a81526020016000a35b565b3373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a25b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017f317b31292193c2a4f561cc40a95ea0d97a2733f14af6d6d59522473e1f3ae65f6000602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a35b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017fd5f0a30e4be0c6be577a71eceb7464245a796a7e6a55c0d971837b250de05f4e60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff16600160007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a35b56", + "0x0", + null, + "0x0", + "0xa", + "0x1c", + "0xe439aa8812c1c0a751b0931ea20c5a30cd54fe15cae883c59fd8107e04557679", + "0x58d025af99b538b778a47da8115c43d5cee564c3cc8d58eb972aaf80ea2c406e")); + + final JsonRpcResponse expected = responseUtils.response(expectedResult, transactions); + final JsonRpcResponse actual = + ethGetBlockByHash() + .response( + requestWithParams( + "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", true)); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void returnTransactionHashOnlyIfBlockFound() { + final Map expectedResult = new EnumMap<>(JsonRpcResponseKey.class); + expectedResult.put(NUMBER, "0x1"); + expectedResult.put( + MIX_HASH, "0x552dd5ae3ccb7a278c6cedda27e48963101be56526a8ee8d70e8227f2c08edf0"); + expectedResult.put( + PARENT_HASH, "0xf8f01382f5636d02edac7fff679a6feb7a572d37a395daaab77938feb6fe217f"); + expectedResult.put(NONCE, "0xbd2b1f0ebba7b989"); + expectedResult.put( + OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + expectedResult.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + expectedResult.put( + TRANSACTION_ROOT, "0x3ccbb984a0a736604acae327d9b643f8e75c7931cb2c6ac10dab4226e2e4c5a3"); + expectedResult.put( + STATE_ROOT, "0xee57559895449b8dbd0a096b2999cf97b517b645ec8db33c7f5934778672263e"); + expectedResult.put( + RECEIPTS_ROOT, "0xa2bd925fcbb8b1ec39612553b17c9265ab198f5af25cc564655114bf5a28c75d"); + expectedResult.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + expectedResult.put(DIFFICULTY, "0x20000"); + expectedResult.put(TOTAL_DIFFICULTY, "0x40000"); + expectedResult.put(EXTRA_DATA, "0x"); + expectedResult.put(SIZE, "0x96a"); + expectedResult.put(GAS_LIMIT, "0x2fefd8"); + expectedResult.put(GAS_USED, "0x78674"); + expectedResult.put(TIMESTAMP, "0x561bc2e0"); + final List transactions = + responseUtils.transactions( + "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed"); + + final JsonRpcResponse expected = responseUtils.response(expectedResult, transactions); + final JsonRpcRequest request = + requestWithParams( + "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", false); + final JsonRpcResponse actual = ethGetBlockByHash().response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } + + private JsonRpcMethod ethGetBlockByHash() { + final JsonRpcMethod method = methods.get(ETH_METHOD); + assertThat(method).isNotNull(); + return method; + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByNumberIntegrationTest.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByNumberIntegrationTest.java new file mode 100755 index 00000000000..be7aaf8f8be --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetBlockByNumberIntegrationTest.java @@ -0,0 +1,395 @@ +package net.consensys.pantheon.ethereum.jsonrpc.methods; + +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.COINBASE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.EXTRA_DATA; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_LIMIT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_USED; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.LOGS_BLOOM; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.MIX_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NONCE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NUMBER; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.OMMERS_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.PARENT_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.RECEIPTS_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.SIZE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.STATE_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TIMESTAMP; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TOTAL_DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TRANSACTION_ROOT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import net.consensys.pantheon.ethereum.jsonrpc.BlockchainImporter; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseUtils; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcTestMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionResult; + +import java.net.URL; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetBlockByNumberIntegrationTest { + + private static final String CHAIN_ID = "6986785976597"; + private static final String ETH_METHOD = "eth_getBlockByNumber"; + private static final String JSON_RPC_VERSION = "2.0"; + private static JsonRpcTestMethodsFactory BLOCKCHAIN; + + private final JsonRpcResponseUtils responseUtils = new JsonRpcResponseUtils(); + private Map methods; + + @BeforeClass + public static void setUpOnce() throws Exception { + final URL blocksUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + final String gensisjson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + BLOCKCHAIN = new JsonRpcTestMethodsFactory(new BlockchainImporter(blocksUrl, gensisjson)); + } + + @Before + public void setUp() { + methods = BLOCKCHAIN.methods(CHAIN_ID); + } + + @Test + public void ethMethodName() { + final String ethName = ethGetBlockNumber().getName(); + + assertThat(ethName).matches(ETH_METHOD); + } + + @Test + public void earliestBlockHashes() { + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + out.put(DIFFICULTY, "0x20000"); + out.put(EXTRA_DATA, "0x42"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x0"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + out.put(MIX_HASH, "0x2c85bcbce56429100b2108254bb56906257582aeafcbd682bc9af67a9f5aee46"); + out.put(NONCE, "0x78cc16f7b4f65485"); + out.put(NUMBER, "0x0"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x0000000000000000000000000000000000000000000000000000000000000000"); + out.put(RECEIPTS_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + out.put(STATE_ROOT, "0x7dba07d6b448a186e9612e5f737d1c909dce473e53199901a302c00646d523c1"); + out.put(SIZE, "0x1ff"); + out.put(TIMESTAMP, "0x54c98c81"); + out.put(TOTAL_DIFFICULTY, "0x20000"); + out.put(TRANSACTION_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + final JsonRpcResponse expected = responseUtils.response(out); + final JsonRpcRequest request = requestWithParams("earliest", false); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void earliestBlockTransactions() { + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + out.put(DIFFICULTY, "0x20000"); + out.put(EXTRA_DATA, "0x42"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x0"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + out.put(MIX_HASH, "0x2c85bcbce56429100b2108254bb56906257582aeafcbd682bc9af67a9f5aee46"); + out.put(NONCE, "0x78cc16f7b4f65485"); + out.put(NUMBER, "0x0"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x0000000000000000000000000000000000000000000000000000000000000000"); + out.put(RECEIPTS_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + out.put(STATE_ROOT, "0x7dba07d6b448a186e9612e5f737d1c909dce473e53199901a302c00646d523c1"); + out.put(SIZE, "0x1ff"); + out.put(TIMESTAMP, "0x54c98c81"); + out.put(TOTAL_DIFFICULTY, "0x20000"); + out.put(TRANSACTION_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + final JsonRpcResponse expected = responseUtils.response(out); + final JsonRpcRequest request = requestWithParams("earliest", true); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void latestBlockHashes() { + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + out.put(DIFFICULTY, "0x207c0"); + out.put(EXTRA_DATA, "0x"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x5c99"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000080000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000400000000000000000200000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000800000000040000000000000000000000000000000000000000010000000000000000000000000"); + out.put(MIX_HASH, "0x4edd77bfff565659bb0ae09421918e4def65d938a900eb94230eb01f5ce80c99"); + out.put(NONCE, "0xdb063000b00e8026"); + out.put(NUMBER, "0x20"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x0f765087745aa259d9e5ac39c367c57432a16ed98e3b0d81c5b51d10f301dc49"); + out.put(RECEIPTS_ROOT, "0xa50a7e67e833f4502524371ee462ccbcc6c6cabd2aeb1555c56150007a53183c"); + out.put(STATE_ROOT, "0xf65f3dd13f72f5fa5607a5224691419969b4f4bae7a00a6cdb853f2ca9eeb1be"); + out.put(SIZE, "0x268"); + out.put(TIMESTAMP, "0x561bc33d"); + out.put(TOTAL_DIFFICULTY, "0x427c00"); + out.put(TRANSACTION_ROOT, "0x6075dd391cf791c74f9e01855d9e5061d009c0903dc102e8b00bcafde8f92839"); + final List transactions = + responseUtils.transactions( + "0xcef53f2311d7c80e9086d661e69ac11a5f3d081e28e02a9ba9b66749407ac310"); + final JsonRpcResponse expected = responseUtils.response(out, transactions); + final JsonRpcRequest request = requestWithParams("latest", false); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void latestBlockTransactions() { + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + out.put(DIFFICULTY, "0x207c0"); + out.put(EXTRA_DATA, "0x"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x5c99"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000080000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000400000000000000000200000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000800000000040000000000000000000000000000000000000000010000000000000000000000000"); + out.put(MIX_HASH, "0x4edd77bfff565659bb0ae09421918e4def65d938a900eb94230eb01f5ce80c99"); + out.put(NONCE, "0xdb063000b00e8026"); + out.put(NUMBER, "0x20"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x0f765087745aa259d9e5ac39c367c57432a16ed98e3b0d81c5b51d10f301dc49"); + out.put(RECEIPTS_ROOT, "0xa50a7e67e833f4502524371ee462ccbcc6c6cabd2aeb1555c56150007a53183c"); + out.put(STATE_ROOT, "0xf65f3dd13f72f5fa5607a5224691419969b4f4bae7a00a6cdb853f2ca9eeb1be"); + out.put(SIZE, "0x268"); + out.put(TIMESTAMP, "0x561bc33d"); + out.put(TOTAL_DIFFICULTY, "0x427c00"); + out.put(TRANSACTION_ROOT, "0x6075dd391cf791c74f9e01855d9e5061d009c0903dc102e8b00bcafde8f92839"); + final List transactions = + responseUtils.transactions( + responseUtils.transaction( + "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "0x20", + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x4cb2f", + "0x1", + "0xcef53f2311d7c80e9086d661e69ac11a5f3d081e28e02a9ba9b66749407ac310", + "0x9dc2c8f5", + "0x1f", + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x0", + "0xa", + "0x1b", + "0x705b002a7df60707d33812e0298411721be20ea5a2f533707295140d89263b79", + "0x78024390784f24160739533b3ceea2698289a02afd9cc768581b4aa3d5f4b105")); + final JsonRpcResponse expected = responseUtils.response(out, transactions); + final JsonRpcRequest request = requestWithParams("latest", true); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void pendingBlockHashes() { + final JsonRpcResponse expected = new JsonRpcSuccessResponse(null, null); + final JsonRpcRequest request = requestWithParams("pending", false); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + @Test + public void pendingBlockTransactions() { + final JsonRpcResponse expected = new JsonRpcSuccessResponse(null, null); + final JsonRpcRequest request = requestWithParams("pending", true); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + @Test + public void blockSixHashes() { + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + out.put(DIFFICULTY, "0x20100"); + out.put(EXTRA_DATA, "0x"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x559f"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + out.put(MIX_HASH, "0x1657e6f42fc186c23d921ba9bcf93f287db353762682f675fa3969757e410e00"); + out.put(NONCE, "0xb65c663250417c60"); + out.put(NUMBER, "0x5"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x4e9a67b663f9abe03e7e9fd5452c9497998337077122f44ee78a466f6a7358de"); + out.put(RECEIPTS_ROOT, "0x01bf16fce84572feb648e5f3487eb3b6648a49639888a90eb552aa661f38f8bd"); + out.put(STATE_ROOT, "0x0c7a49b1ae3138ae33d88b21d5543b8d2c8e2377bd2b58e73db8ea8924395ff4"); + out.put(SIZE, "0x268"); + out.put(TIMESTAMP, "0x561bc2ec"); + out.put(TOTAL_DIFFICULTY, "0xc0280"); + out.put(TRANSACTION_ROOT, "0xd8672f45d109c2e0b27acf68fd67b9eae14957fd2bf2444210ee0d7e97bc68a6"); + final List transactions = + responseUtils.transactions( + "0xec7e53d1b99ef586b3e43c1c7068311f6861d51ac3d6fbf257ac0b54ba3f2032"); + final JsonRpcResponse expected = responseUtils.response(out, transactions); + final JsonRpcRequest request = requestWithParams("0x5", false); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void blockSixTransactions() { + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0x8888f1f195afa192cfee860698584c030f4c9db1"); + out.put(DIFFICULTY, "0x20100"); + out.put(EXTRA_DATA, "0x"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x559f"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + out.put(MIX_HASH, "0x1657e6f42fc186c23d921ba9bcf93f287db353762682f675fa3969757e410e00"); + out.put(NONCE, "0xb65c663250417c60"); + out.put(NUMBER, "0x5"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x4e9a67b663f9abe03e7e9fd5452c9497998337077122f44ee78a466f6a7358de"); + out.put(RECEIPTS_ROOT, "0x01bf16fce84572feb648e5f3487eb3b6648a49639888a90eb552aa661f38f8bd"); + out.put(STATE_ROOT, "0x0c7a49b1ae3138ae33d88b21d5543b8d2c8e2377bd2b58e73db8ea8924395ff4"); + out.put(SIZE, "0x268"); + out.put(TIMESTAMP, "0x561bc2ec"); + out.put(TOTAL_DIFFICULTY, "0xc0280"); + out.put(TRANSACTION_ROOT, "0xd8672f45d109c2e0b27acf68fd67b9eae14957fd2bf2444210ee0d7e97bc68a6"); + final List transactions = + responseUtils.transactions( + responseUtils.transaction( + "0x609427ccfeae6d2a930927c9a29a0a3077cac7e4b5826159586b10e25770eef9", + "0x5", + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x4cb2f", + "0x1", + "0xec7e53d1b99ef586b3e43c1c7068311f6861d51ac3d6fbf257ac0b54ba3f2032", + "0xf5b53e17", + "0x4", + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x0", + "0xa", + "0x1c", + "0x1c07bd41fc821f95b9f543b080c520654727f9cf829800f789c3b03b8de8b326", + "0x259c8aceea2d462192d95f9d6b7cb9e0bf2a6d549c3a4111194fdd22105728f5")); + final JsonRpcResponse expected = responseUtils.response(out, transactions); + final JsonRpcRequest request = requestWithParams("0x5", true); + + final JsonRpcResponse actual = ethGetBlockNumber().response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + /** The Tag | Quantity is the first parameter, either a String or a number */ + @Test + public void missingTagParameterBlockHashes() { + final JsonRpcRequest request = requestWithParams(false); + + final Throwable thrown = catchThrowable(() -> ethGetBlockNumber().response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Invalid json rpc parameter at index 0"); + } + + /** The Tag | Quantity is the first parameter, either a String or a number */ + @Test + public void missingTagParameterBlockTransactions() { + final JsonRpcRequest request = requestWithParams(true); + + final Throwable thrown = catchThrowable(() -> ethGetBlockNumber().response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Invalid json rpc parameter at index 0"); + } + + /** + * The Boolean type second parameter, denotes whether to retrieve the complete transaction or just + * the transaction hash. + */ + @Test + public void missingHashesOrTransactionParameter() { + final JsonRpcRequest request = requestWithParams("earliest"); + + final Throwable thrown = catchThrowable(() -> ethGetBlockNumber().response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasNoCause() + .hasMessage("Missing required json rpc parameter at index 1"); + } + + /** + * The Boolean type second parameter, denotes whether to retrieve the complete transaction or just + * the transaction hash. + */ + @Test + public void missingAllParameters() { + final JsonRpcRequest request = requestWithParams(); + + final Throwable thrown = catchThrowable(() -> ethGetBlockNumber().response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasNoCause() + .hasMessage("Missing required json rpc parameter at index 0"); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } + + private JsonRpcMethod ethGetBlockNumber() { + final JsonRpcMethod method = methods.get(ETH_METHOD); + assertThat(method).isNotNull(); + return method; + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetFilterChangesIntegrationTest.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetFilterChangesIntegrationTest.java new file mode 100755 index 00000000000..1fe7aac6b2f --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetFilterChangesIntegrationTest.java @@ -0,0 +1,266 @@ +package net.consensys.pantheon.ethereum.jsonrpc.methods; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.core.TransactionPool.TransactionBatchAddedListener; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterIdGenerator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetFilterChanges; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; + +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetFilterChangesIntegrationTest { + + @Mock private TransactionBatchAddedListener batchAddedListener; + private MutableBlockchain blockchain; + private final String ETH_METHOD = "eth_getFilterChanges"; + private final String JSON_RPC_VERSION = "2.0"; + private TransactionPool transactionPool; + private final PendingTransactions transactions = new PendingTransactions(MAX_TRANSACTIONS); + private static final int MAX_TRANSACTIONS = 5; + private static final KeyPair keyPair = KeyPair.generate(); + private final Transaction transaction = createTransaction(1); + private final JsonRpcParameter parameters = new JsonRpcParameter(); + private FilterManager filterManager; + private EthGetFilterChanges method; + + @Before + public void setUp() { + final GenesisConfig genesisConfig = GenesisConfig.mainnet(); + final Block genesisBlock = genesisConfig.getBlock(); + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + blockchain = + new DefaultMutableBlockchain( + genesisBlock, keyValueStorage, MainnetBlockHashFunction::createHash); + final WorldStateArchive worldStateArchive = + new WorldStateArchive(new KeyValueStorageWorldStateStorage(keyValueStorage)); + final ProtocolContext protocolContext = + new ProtocolContext<>(blockchain, worldStateArchive, null); + transactionPool = + new TransactionPool( + transactions, genesisConfig.getProtocolSchedule(), protocolContext, batchAddedListener); + final BlockchainQueries blockchainQueries = + new BlockchainQueries(blockchain, worldStateArchive); + filterManager = new FilterManager(blockchainQueries, transactionPool, new FilterIdGenerator()); + method = new EthGetFilterChanges(filterManager, parameters); + } + + @Test + public void shouldReturnErrorResponseIfFilterNotFound() { + final JsonRpcRequest request = requestWithParams("0"); + + final JsonRpcResponse expected = new JsonRpcErrorResponse(null, JsonRpcError.FILTER_NOT_FOUND); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByField(expected); + } + + @Test + public void shouldReturnEmptyArrayIfNoNewBlocks() { + final String filterId = filterManager.installBlockFilter(); + + assertThatFilterExists(filterId); + + final JsonRpcRequest request = requestWithParams(String.valueOf(filterId)); + final JsonRpcSuccessResponse expected = new JsonRpcSuccessResponse(null, Lists.emptyList()); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByField(expected); + + filterManager.uninstallFilter(filterId); + + assertThatFilterDoesNotExist(filterId); + } + + @Test + public void shouldReturnEmptyArrayIfNoAddedPendingTransactions() { + final String filterId = filterManager.installPendingTransactionFilter(); + + assertThatFilterExists(filterId); + + final JsonRpcRequest request = requestWithParams(String.valueOf(filterId)); + + // We haven't added any transactions, so the list of pending transactions should be empty. + final JsonRpcSuccessResponse expected = new JsonRpcSuccessResponse(null, Lists.emptyList()); + final JsonRpcResponse actual = method.response(request); + assertThat(actual).isEqualToComparingFieldByField(expected); + + filterManager.uninstallFilter(filterId); + + assertThatFilterDoesNotExist(filterId); + } + + @Test + public void shouldReturnHashesIfNewBlocks() { + final String filterId = filterManager.installBlockFilter(); + + assertThatFilterExists(filterId); + + final JsonRpcRequest request = requestWithParams(String.valueOf(filterId)); + + // We haven't added any blocks, so the list of new blocks should be empty. + JsonRpcSuccessResponse expected = new JsonRpcSuccessResponse(null, Lists.emptyList()); + JsonRpcResponse actual = method.response(request); + assertThat(actual).isEqualToComparingFieldByField(expected); + + final Block block = appendBlock(transaction); + + // We've added one block, so there should be one new hash. + expected = new JsonRpcSuccessResponse(null, Lists.newArrayList(block.getHash().toString())); + actual = method.response(request); + assertThat(actual).isEqualToComparingFieldByField(expected); + + // The queue should be flushed and return no results. + expected = new JsonRpcSuccessResponse(null, Lists.emptyList()); + actual = method.response(request); + assertThat(actual).isEqualToComparingFieldByField(expected); + + filterManager.uninstallFilter(filterId); + + assertThatFilterDoesNotExist(filterId); + } + + @Test + public void shouldReturnHashesIfNewPendingTransactions() { + final String filterId = filterManager.installPendingTransactionFilter(); + + assertThatFilterExists(filterId); + + final JsonRpcRequest request = requestWithParams(String.valueOf(filterId)); + + // We haven't added any transactions, so the list of pending transactions should be empty. + JsonRpcSuccessResponse expected = new JsonRpcSuccessResponse(null, Lists.emptyList()); + JsonRpcResponse actual = method.response(request); + assertThat(actual).isEqualToComparingFieldByField(expected); + + transactions.addRemoteTransaction(transaction); + + // We've added one transaction, so there should be one new hash. + expected = + new JsonRpcSuccessResponse(null, Lists.newArrayList(String.valueOf(transaction.hash()))); + actual = method.response(request); + assertThat(actual).isEqualToComparingFieldByField(expected); + + // The queue should be flushed and return no results. + expected = new JsonRpcSuccessResponse(null, Lists.emptyList()); + actual = method.response(request); + assertThat(actual).isEqualToComparingFieldByField(expected); + + filterManager.uninstallFilter(filterId); + + assertThatFilterDoesNotExist(filterId); + } + + private void assertThatFilterExists(final String filterId) { + assertThat(filterExists(filterId)).isTrue(); + } + + private void assertThatFilterDoesNotExist(final String filterId) { + assertThat(filterExists(filterId)).isFalse(); + } + + /** + * Determines whether a specified filter exists. + * + * @param filterId The filter ID to check. + * @return A boolean - true if the filter exists, false if not. + */ + private boolean filterExists(final String filterId) { + final JsonRpcResponse response = method.response(requestWithParams(String.valueOf(filterId))); + if (response instanceof JsonRpcSuccessResponse) { + return true; + } else { + assertThat(response).isInstanceOf(JsonRpcErrorResponse.class); + assertThat(((JsonRpcErrorResponse) response).getError()) + .isEqualTo(JsonRpcError.FILTER_NOT_FOUND); + return false; + } + } + + private Block appendBlock(final Transaction... transactionsToAdd) { + return appendBlock(UInt256.ONE, getHeaderForCurrentChainHead(), transactionsToAdd); + } + + private BlockHeader getHeaderForCurrentChainHead() { + return blockchain.getBlockHeader(blockchain.getChainHeadHash()).get(); + } + + private Block appendBlock( + final UInt256 difficulty, + final BlockHeader parentBlock, + final Transaction... transactionsToAdd) { + final List transactionList = asList(transactionsToAdd); + final Block block = + new Block( + new BlockHeaderTestFixture() + .difficulty(difficulty) + .parentHash(parentBlock.getHash()) + .number(parentBlock.getNumber() + 1) + .buildHeader(), + new BlockBody(transactionList, emptyList())); + final List transactionReceipts = + transactionList + .stream() + .map(transaction -> new TransactionReceipt(1, 1, emptyList())) + .collect(toList()); + blockchain.appendBlock(block, transactionReceipts); + return block; + } + + private Transaction createTransaction(final int transactionNumber) { + return Transaction.builder() + .gasLimit(100) + .gasPrice(Wei.ZERO) + .nonce(1) + .payload(BytesValue.EMPTY) + .to(Address.ID) + .value(Wei.of(transactionNumber)) + .sender(Address.ID) + .chainId(1) + .signAndBuild(keyPair); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockHashAndIndexIntegrationTest.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockHashAndIndexIntegrationTest.java new file mode 100755 index 00000000000..68065e1b137 --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockHashAndIndexIntegrationTest.java @@ -0,0 +1,111 @@ +package net.consensys.pantheon.ethereum.jsonrpc.methods; + +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.COINBASE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.EXTRA_DATA; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_LIMIT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_USED; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.LOGS_BLOOM; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.MIX_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NONCE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NUMBER; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.OMMERS_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.PARENT_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.RECEIPTS_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.SIZE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.STATE_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TIMESTAMP; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TOTAL_DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TRANSACTION_ROOT; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.BlockchainImporter; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseUtils; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcTestMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.net.URL; +import java.util.EnumMap; +import java.util.Map; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EthGetUncleByBlockHashAndIndexIntegrationTest { + + private static final String CHAIN_ID = "6986785976597"; + private static JsonRpcTestMethodsFactory BLOCKCHAIN; + + private final JsonRpcResponseUtils responseUtils = new JsonRpcResponseUtils(); + private JsonRpcMethod method; + + @BeforeClass + public static void setUpOnce() throws Exception { + final URL blocksUrl = + EthGetUncleByBlockHashAndIndexIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthGetUncleByBlockHashAndIndexIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + final String gensisjson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + BLOCKCHAIN = new JsonRpcTestMethodsFactory(new BlockchainImporter(blocksUrl, gensisjson)); + } + + @Before + public void setUp() { + method = BLOCKCHAIN.methods(CHAIN_ID).get("eth_getUncleByBlockHashAndIndex"); + } + + @Test + public void shouldGetExpectedBlockResult() { + final JsonRpcRequest request = + new JsonRpcRequest( + "2.0", + "eth_getUncleByBlockHashAndIndex", + new Object[] { + "0x4e9a67b663f9abe03e7e9fd5452c9497998337077122f44ee78a466f6a7358de", "0x0" + }); + + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + out.put(DIFFICULTY, "0x20040"); + out.put(EXTRA_DATA, "0x"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x0"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + out.put(MIX_HASH, "0xe970d9815a634e25a778a765764d91ecc80d667a85721dcd4297d00be8d2af29"); + out.put(NONCE, "0x64050e6ee4c2f3c7"); + out.put(NUMBER, "0x2"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9"); + out.put(RECEIPTS_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + out.put(STATE_ROOT, "0xee57559895449b8dbd0a096b2999cf97b517b645ec8db33c7f5934778672263e"); + out.put(SIZE, "0x1ff"); + out.put(TIMESTAMP, "0x561bc2e7"); + out.put(TOTAL_DIFFICULTY, "0x0"); + out.put(TRANSACTION_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + + final JsonRpcResponse expected = responseUtils.response(out); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isInstanceOf(JsonRpcSuccessResponse.class); + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } +} diff --git a/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockNumberAndIndexIntegrationTest.java b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockNumberAndIndexIntegrationTest.java new file mode 100755 index 00000000000..f63aa2e23ef --- /dev/null +++ b/ethereum/jsonrpc/src/integration-test/java/net/consensys/pantheon/ethereum/jsonrpc/methods/EthGetUncleByBlockNumberAndIndexIntegrationTest.java @@ -0,0 +1,108 @@ +package net.consensys.pantheon.ethereum.jsonrpc.methods; + +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.COINBASE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.EXTRA_DATA; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_LIMIT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.GAS_USED; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.LOGS_BLOOM; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.MIX_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NONCE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.NUMBER; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.OMMERS_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.PARENT_HASH; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.RECEIPTS_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.SIZE; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.STATE_ROOT; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TIMESTAMP; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TOTAL_DIFFICULTY; +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey.TRANSACTION_ROOT; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.BlockchainImporter; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseKey; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcResponseUtils; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcTestMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.net.URL; +import java.util.EnumMap; +import java.util.Map; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EthGetUncleByBlockNumberAndIndexIntegrationTest { + + private static final String CHAIN_ID = "6986785976597"; + private static JsonRpcTestMethodsFactory BLOCKCHAIN; + + private final JsonRpcResponseUtils responseUtils = new JsonRpcResponseUtils(); + private JsonRpcMethod method; + + @BeforeClass + public static void setUpOnce() throws Exception { + final URL blocksUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthGetBlockByNumberIntegrationTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + final String gensisjson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + BLOCKCHAIN = new JsonRpcTestMethodsFactory(new BlockchainImporter(blocksUrl, gensisjson)); + } + + @Before + public void setUp() { + method = BLOCKCHAIN.methods(CHAIN_ID).get("eth_getUncleByBlockNumberAndIndex"); + } + + @Test + public void shouldGetExpectedBlockResult() { + final Map out = new EnumMap<>(JsonRpcResponseKey.class); + out.put(COINBASE, "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + out.put(DIFFICULTY, "0x20040"); + out.put(EXTRA_DATA, "0x"); + out.put(GAS_LIMIT, "0x2fefd8"); + out.put(GAS_USED, "0x0"); + out.put( + LOGS_BLOOM, + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + out.put(MIX_HASH, "0xe970d9815a634e25a778a765764d91ecc80d667a85721dcd4297d00be8d2af29"); + out.put(NONCE, "0x64050e6ee4c2f3c7"); + out.put(NUMBER, "0x2"); + out.put(OMMERS_HASH, "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"); + out.put(PARENT_HASH, "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9"); + out.put(RECEIPTS_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + out.put(STATE_ROOT, "0xee57559895449b8dbd0a096b2999cf97b517b645ec8db33c7f5934778672263e"); + out.put(SIZE, "0x1ff"); + out.put(TIMESTAMP, "0x561bc2e7"); + out.put(TOTAL_DIFFICULTY, "0x0"); + out.put(TRANSACTION_ROOT, "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + final JsonRpcResponse expected = responseUtils.response(out); + final JsonRpcRequest request = getUncleByBlockNumberAndIndex(); + + final JsonRpcSuccessResponse actual = (JsonRpcSuccessResponse) method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + private JsonRpcRequest getUncleByBlockNumberAndIndex() { + return new JsonRpcRequest( + "2.0", "eth_getUncleByBlockNumberAndIndex", new Object[] {"0x4", "0x0"}); + } +} diff --git a/ethereum/jsonrpc/src/integration-test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks b/ethereum/jsonrpc/src/integration-test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks new file mode 100755 index 0000000000000000000000000000000000000000..d29453d3e53b4382cc3ff4254e1931182a6c630d GIT binary patch literal 23287 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 DEFAULT_JSON_RPC_APIS = + Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + + private boolean enabled; + private int port; + private String host; + private Collection corsAllowedDomains = Collections.emptyList(); + private Collection rpcApis; + + public enum RpcApis { + DEBUG, + ETH, + MINER, + NET, + WEB3; + + public String getValue() { + return this.name().toLowerCase(); + } + } + + public static JsonRpcConfiguration createDefault() { + final JsonRpcConfiguration config = new JsonRpcConfiguration(); + config.setEnabled(false); + config.setPort(DEFAULT_JSON_RPC_PORT); + config.setHost(DEFAULT_JSON_RPC_HOST); + config.rpcApis = DEFAULT_JSON_RPC_APIS; + return config; + } + + private JsonRpcConfiguration() {} + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + public int getPort() { + return port; + } + + public void setPort(final int port) { + this.port = port; + } + + public String getHost() { + return host; + } + + public void setHost(final String host) { + this.host = host; + } + + public Collection getCorsAllowedDomains() { + return corsAllowedDomains; + } + + public void setCorsAllowedDomains(final Collection corsAllowedDomains) { + if (corsAllowedDomains != null) { + this.corsAllowedDomains = corsAllowedDomains; + } + } + + public Collection getRpcApis() { + return rpcApis; + } + + public void setRpcApis(final Collection rpcApis) { + this.rpcApis = rpcApis; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("enabled", enabled) + .add("port", port) + .add("host", host) + .add("corsAllowedDomains", corsAllowedDomains) + .add("rpcApis", rpcApis) + .toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final JsonRpcConfiguration that = (JsonRpcConfiguration) o; + return enabled == that.enabled + && port == that.port + && Objects.equal(host, that.host) + && Objects.equal(corsAllowedDomains, that.corsAllowedDomains) + && Objects.equal(rpcApis, that.rpcApis); + } + + @Override + public int hashCode() { + return Objects.hashCode(enabled, port, host, corsAllowedDomains, rpcApis); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java new file mode 100755 index 00000000000..3ad02858772 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcErrorConverter.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; + +public class JsonRpcErrorConverter { + + public static JsonRpcError convertTransactionInvalidReason( + final TransactionInvalidReason reason) { + switch (reason) { + case NONCE_TOO_LOW: + return JsonRpcError.NONCE_TOO_LOW; + case INCORRECT_NONCE: + return JsonRpcError.INCORRECT_NONCE; + case INVALID_SIGNATURE: + return JsonRpcError.INVALID_TRANSACTION_SIGNATURE; + case INTRINSIC_GAS_EXCEEDS_GAS_LIMIT: + return JsonRpcError.INTRINSIC_GAS_EXCEEDS_LIMIT; + case UPFRONT_COST_EXCEEDS_BALANCE: + return JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE; + case EXCEEDS_BLOCK_GAS_LIMIT: + return JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT; + + default: + return JsonRpcError.INVALID_PARAMS; + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java new file mode 100755 index 00000000000..902d442f0ff --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java @@ -0,0 +1,341 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.stream.Collectors.toList; +import static net.consensys.pantheon.util.NetworkUtility.urlForSocketAddress; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequestId; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcNoResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponseType; +import net.consensys.pantheon.util.NetworkUtility; + +import java.net.BindException; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.concurrent.CompletableFuture; + +import com.google.common.annotations.VisibleForTesting; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.CorsHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class JsonRpcHttpService { + + private static final Logger LOGGER = LogManager.getLogger(JsonRpcHttpService.class); + + private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0); + private static final String APPLICATION_JSON = "application/json"; + private static final JsonRpcResponse NO_RESPONSE = new JsonRpcNoResponse(); + private static final String EMPTY_RESPONSE = ""; + + private final Vertx vertx; + private final JsonRpcConfiguration config; + private final Map jsonRpcMethods; + + private HttpServer httpServer; + + public JsonRpcHttpService( + final Vertx vertx, + final JsonRpcConfiguration config, + final Map methods) { + validateConfig(config); + this.config = config; + this.vertx = vertx; + this.jsonRpcMethods = methods; + } + + private void validateConfig(final JsonRpcConfiguration config) { + checkArgument( + config.getPort() == 0 || NetworkUtility.isValidPort(config.getPort()), + "Invalid port configuration."); + checkArgument(config.getHost() != null, "Required host is not configured."); + } + + public CompletableFuture start() { + LOGGER.info("Starting JsonRPC service on {}:{}", config.getHost(), config.getPort()); + // Create the HTTP server and a router object. + httpServer = + vertx.createHttpServer( + new HttpServerOptions().setHost(config.getHost()).setPort(config.getPort())); + + // Handle json rpc requests + final Router router = Router.router(vertx); + router + .route() + .handler( + CorsHandler.create(buildCorsRegexFromConfig()) + .allowedHeader("*") + .allowedHeader("content-type")); + router.route().handler(BodyHandler.create()); + router.route("/").method(HttpMethod.GET).handler(this::handleEmptyRequest); + router + .route("/") + .method(HttpMethod.POST) + .produces(APPLICATION_JSON) + .handler(this::handleJsonRPCRequest); + + final CompletableFuture resultFuture = new CompletableFuture<>(); + httpServer + .requestHandler(router::accept) + .listen( + res -> { + if (!res.failed()) { + resultFuture.complete(null); + LOGGER.info( + "JsonRPC service started and listening on {}:{}", + config.getHost(), + httpServer.actualPort()); + return; + } + httpServer = null; + final Throwable cause = res.cause(); + if (cause instanceof BindException || cause instanceof SocketException) { + resultFuture.completeExceptionally( + new JsonRpcServiceException( + String.format( + "Failed to bind Ethereum JSON RPC listener to %s:%s: %s", + config.getHost(), config.getPort(), cause.getMessage()))); + return; + } + resultFuture.completeExceptionally(cause); + }); + + return resultFuture; + } + + public CompletableFuture stop() { + if (httpServer == null) { + return CompletableFuture.completedFuture(null); + } + + final CompletableFuture resultFuture = new CompletableFuture<>(); + httpServer.close( + res -> { + if (res.failed()) { + resultFuture.completeExceptionally(res.cause()); + } else { + httpServer = null; + resultFuture.complete(null); + } + }); + return resultFuture; + } + + public InetSocketAddress socketAddress() { + if (httpServer == null) { + return EMPTY_SOCKET_ADDRESS; + } + return new InetSocketAddress(config.getHost(), httpServer.actualPort()); + } + + @VisibleForTesting + public String url() { + if (httpServer == null) { + return ""; + } + return urlForSocketAddress("http", socketAddress()); + } + + private void handleJsonRPCRequest(final RoutingContext routingContext) { + // Parse json + try { + final String json = routingContext.getBodyAsString().trim(); + if (!json.isEmpty() && json.charAt(0) == '{') { + handleJsonSingleRequest(routingContext, new JsonObject(json)); + } else { + final JsonArray array = new JsonArray(json); + if (array.size() < 1) { + handleJsonRpcError(routingContext, null, JsonRpcError.INVALID_REQUEST); + return; + } + handleJsonBatchRequest(routingContext, array); + } + } catch (final DecodeException ex) { + handleJsonRpcError(routingContext, null, JsonRpcError.PARSE_ERROR); + } + } + + // Facilitate remote health-checks in AWS, inter alia. + private void handleEmptyRequest(final RoutingContext routingContext) { + routingContext.response().setStatusCode(201).end(); + } + + private void handleJsonSingleRequest( + final RoutingContext routingContext, final JsonObject request) { + final HttpServerResponse response = routingContext.response(); + vertx.executeBlocking( + future -> { + final JsonRpcResponse jsonRpcResponse = process(request); + future.complete(jsonRpcResponse); + }, + false, + (res) -> { + if (res.failed()) { + response.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()).end(); + return; + } + + final JsonRpcResponse jsonRpcResponse = (JsonRpcResponse) res.result(); + response.setStatusCode(status(jsonRpcResponse).code()); + response.putHeader("Content-Type", APPLICATION_JSON); + response.end(serialise(jsonRpcResponse)); + }); + } + + private HttpResponseStatus status(final JsonRpcResponse response) { + + switch (response.getType()) { + case ERROR: + return HttpResponseStatus.BAD_REQUEST; + case SUCCESS: + case NONE: + default: + return HttpResponseStatus.OK; + } + } + + private String serialise(final JsonRpcResponse response) { + + if (response.getType() == JsonRpcResponseType.NONE) { + return EMPTY_RESPONSE; + } + + return Json.encodePrettily(response); + } + + @SuppressWarnings("rawtypes") + private void handleJsonBatchRequest( + final RoutingContext routingContext, final JsonArray jsonArray) { + // Interpret json as rpc request + final List responses = + jsonArray + .stream() + .map( + obj -> { + if (!(obj instanceof JsonObject)) { + return Future.succeededFuture( + errorResponse(null, JsonRpcError.INVALID_REQUEST)); + } + + final JsonObject req = (JsonObject) obj; + final Future fut = Future.future(); + vertx.executeBlocking( + future -> future.complete(process(req)), + false, + ar -> { + if (ar.failed()) { + fut.fail(ar.cause()); + } else { + fut.complete((JsonRpcResponse) ar.result()); + } + }); + return fut; + }) + .collect(toList()); + + CompositeFuture.all(responses) + .setHandler( + (res) -> { + if (res.failed()) { + routingContext + .response() + .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .end(); + return; + } + final JsonRpcResponse[] completed = + res.result() + .list() + .stream() + .map(JsonRpcResponse.class::cast) + .filter(r -> isNonEmptyResponses(r)) + .toArray(JsonRpcResponse[]::new); + + routingContext.response().end(Json.encode(completed)); + }); + } + + private boolean isNonEmptyResponses(final JsonRpcResponse result) { + return result.getType() != JsonRpcResponseType.NONE; + } + + private JsonRpcResponse process(final JsonObject requestJson) { + final JsonRpcRequest request; + Object id = null; + try { + id = new JsonRpcRequestId(requestJson.getValue("id")).getValue(); + request = requestJson.mapTo(JsonRpcRequest.class); + } catch (final IllegalArgumentException exception) { + return errorResponse(id, JsonRpcError.INVALID_REQUEST); + } + // Handle notifications + if (request.isNotification()) { + // Notifications aren't handled so create empty result for now. + return NO_RESPONSE; + } + + LOGGER.info("JSON-RPC request -> {}", request.getMethod()); + // Find method handler + final JsonRpcMethod method = jsonRpcMethods.get(request.getMethod()); + if (method == null) { + return errorResponse(id, JsonRpcError.METHOD_NOT_FOUND); + } + + // Generate response + try { + return method.response(request); + } catch (final InvalidJsonRpcParameters e) { + LOGGER.debug(e); + return errorResponse(id, JsonRpcError.INVALID_PARAMS); + } + } + + private void handleJsonRpcError( + final RoutingContext routingContext, final Object id, final JsonRpcError error) { + routingContext + .response() + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(Json.encode(new JsonRpcErrorResponse(id, error))); + } + + private JsonRpcResponse errorResponse(final Object id, final JsonRpcError error) { + return new JsonRpcErrorResponse(id, error); + } + + private String buildCorsRegexFromConfig() { + if (config.getCorsAllowedDomains().isEmpty()) { + return ""; + } + if (config.getCorsAllowedDomains().contains("*")) { + return "*"; + } else { + final StringJoiner stringJoiner = new StringJoiner("|"); + config.getCorsAllowedDomains().stream().filter(s -> !s.isEmpty()).forEach(stringJoiner::add); + return stringJoiner.toString(); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java new file mode 100755 index 00000000000..29b4befb008 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java @@ -0,0 +1,214 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterIdGenerator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.AdminPeers; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.DebugStorageRangeAt; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.DebugTraceTransaction; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthAccounts; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthBlockNumber; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthCall; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthCoinbase; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthEstimateGas; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGasPrice; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBalance; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBlockByHash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBlockByNumber; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBlockTransactionCountByHash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBlockTransactionCountByNumber; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetCode; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetFilterChanges; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetFilterLogs; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetLogs; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetStorageAt; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockHashAndIndex; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockNumberAndIndex; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByHash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionCount; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionReceipt; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleByBlockHashAndIndex; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleByBlockNumberAndIndex; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleCountByBlockHash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetUncleCountByBlockNumber; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthMining; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthNewBlockFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthNewFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthNewPendingTransactionFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthProtocolVersion; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthSendRawTransaction; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthSyncing; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthUninstallFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.NetListening; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.NetPeerCount; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.NetVersion; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.Web3ClientVersion; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.Web3Sha3; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerSetCoinbase; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerSetEtherbase; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerStart; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner.MinerStop; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.BlockReplay; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransactionTracer; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessor; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactory; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class JsonRpcMethodsFactory { + + private final BlockResultFactory blockResult = new BlockResultFactory(); + private final JsonRpcParameter parameter = new JsonRpcParameter(); + + public Map methods( + final String clientVersion, + final String chainId, + final P2PNetwork peerNetworkingService, + final Blockchain blockchain, + final WorldStateArchive worldStateArchive, + final Synchronizer synchronizer, + final TransactionPool transactionPool, + final ProtocolSchedule protocolSchedule, + final MiningCoordinator miningCoordinator, + final Set supportedCapabilities, + final Collection rpcApis) { + final BlockchainQueries blockchainQueries = + new BlockchainQueries(blockchain, worldStateArchive); + final FilterManager filterManager = + new FilterManager(blockchainQueries, transactionPool, new FilterIdGenerator()); + return methods( + clientVersion, + chainId, + peerNetworkingService, + blockchainQueries, + synchronizer, + protocolSchedule, + filterManager, + transactionPool, + miningCoordinator, + supportedCapabilities, + rpcApis); + } + + public Map methods( + final String clientVersion, + final String chainId, + final P2PNetwork p2pNetwork, + final BlockchainQueries blockchainQueries, + final Synchronizer synchronizer, + final ProtocolSchedule protocolSchedule, + final FilterManager filterManager, + final TransactionPool transactionPool, + final MiningCoordinator miningCoordinator, + final Set supportedCapabilities, + final Collection rpcApis) { + final Map enabledMethods = new HashMap<>(); + // @formatter:off + if (rpcApis.contains(RpcApis.ETH)) { + addMethods( + enabledMethods, + new EthAccounts(), + new EthBlockNumber(blockchainQueries), + new EthGetBalance(blockchainQueries, parameter), + new EthGetBlockByHash(blockchainQueries, blockResult, parameter), + new EthGetBlockByNumber(blockchainQueries, blockResult, parameter), + new EthGetBlockTransactionCountByNumber(blockchainQueries, parameter), + new EthGetBlockTransactionCountByHash(blockchainQueries, parameter), + new EthCall( + blockchainQueries, + new TransientTransactionProcessor( + blockchainQueries.getBlockchain(), + blockchainQueries.getWorldStateArchive(), + protocolSchedule), + parameter), + new EthGetCode(blockchainQueries, parameter), + new EthGetLogs(blockchainQueries, parameter), + new EthGetUncleCountByBlockHash(blockchainQueries, parameter), + new EthGetUncleCountByBlockNumber(blockchainQueries, parameter), + new EthGetUncleByBlockNumberAndIndex(blockchainQueries, parameter), + new EthGetUncleByBlockHashAndIndex(blockchainQueries, parameter), + new EthNewBlockFilter(filterManager), + new EthNewPendingTransactionFilter(filterManager), + new EthNewFilter(filterManager, parameter), + new EthGetTransactionByHash( + blockchainQueries, transactionPool.getPendingTransactions(), parameter), + new EthGetTransactionByBlockHashAndIndex(blockchainQueries, parameter), + new EthGetTransactionByBlockNumberAndIndex(blockchainQueries, parameter), + new EthGetTransactionCount( + blockchainQueries, transactionPool.getPendingTransactions(), parameter), + new EthGetTransactionReceipt(blockchainQueries, parameter), + new EthUninstallFilter(filterManager, parameter), + new EthGetFilterChanges(filterManager, parameter), + new EthGetFilterLogs(filterManager, parameter), + new EthSyncing(synchronizer), + new EthGetStorageAt(blockchainQueries, parameter), + new EthSendRawTransaction(transactionPool, parameter), + new EthEstimateGas( + blockchainQueries, + new TransientTransactionProcessor( + blockchainQueries.getBlockchain(), + blockchainQueries.getWorldStateArchive(), + protocolSchedule), + parameter), + new EthMining(miningCoordinator), + new EthCoinbase(miningCoordinator), + new EthProtocolVersion(supportedCapabilities), + new EthGasPrice(miningCoordinator)); + } + if (rpcApis.contains(RpcApis.DEBUG)) { + final BlockReplay blockReplay = + new BlockReplay( + protocolSchedule, + blockchainQueries.getBlockchain(), + blockchainQueries.getWorldStateArchive()); + addMethods( + enabledMethods, + new DebugTraceTransaction( + blockchainQueries, new TransactionTracer(blockReplay), parameter), + new DebugStorageRangeAt(parameter, blockchainQueries, blockReplay)); + } + if (rpcApis.contains(RpcApis.NET)) { + addMethods( + enabledMethods, + new NetVersion(chainId), + new NetListening(p2pNetwork), + new NetPeerCount(p2pNetwork), + new AdminPeers(p2pNetwork)); + } + if (rpcApis.contains(RpcApis.WEB3)) { + addMethods(enabledMethods, new Web3ClientVersion(clientVersion), new Web3Sha3()); + } + if (rpcApis.contains(RpcApis.MINER)) { + final MinerSetCoinbase minerSetCoinbase = new MinerSetCoinbase(miningCoordinator, parameter); + addMethods( + enabledMethods, + new MinerStart(miningCoordinator), + new MinerStop(miningCoordinator), + minerSetCoinbase, + new MinerSetEtherbase(minerSetCoinbase)); + } + // @formatter:off + return enabledMethods; + } + + private void addMethods( + final Map methods, final JsonRpcMethod... rpcMethods) { + for (final JsonRpcMethod rpcMethod : rpcMethods) { + methods.put(rpcMethod.getName(), rpcMethod); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcServiceException.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcServiceException.java new file mode 100755 index 00000000000..960db63839b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcServiceException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +public class JsonRpcServiceException extends RuntimeException { + + public JsonRpcServiceException(final String message) { + super(message); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequest.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequest.java new file mode 100755 index 00000000000..4d13f29fa1d --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequest.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcRequestException; + +import java.util.Arrays; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.google.common.base.Objects; + +public class JsonRpcRequest { + + private JsonRpcRequestId id; + private final String method; + private final Object[] params; + private final String version; + private boolean isNotification = true; + + @JsonCreator + public JsonRpcRequest( + @JsonProperty("jsonrpc") final String version, + @JsonProperty("method") final String method, + @JsonProperty("params") final Object[] params) { + this.version = version; + this.method = method; + this.params = params; + if (method == null) { + throw new InvalidJsonRpcRequestException("Field 'method' is required"); + } + } + + @JsonGetter("id") + public Object getId() { + return id == null ? null : id.getValue(); + } + + @JsonGetter("method") + public String getMethod() { + return method; + } + + @JsonGetter("jsonrpc") + public String getVersion() { + return version; + } + + @JsonInclude(Include.NON_NULL) + @JsonGetter("params") + public Object[] getParams() { + return params; + } + + @JsonIgnore + public boolean isNotification() { + return isNotification; + } + + @JsonIgnore + public int getParamLength() { + return params.length; + } + + @JsonSetter("id") + protected void setId(final JsonRpcRequestId id) { + // If an id is explicitly set, its not a notification + isNotification = false; + this.id = id; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final JsonRpcRequest that = (JsonRpcRequest) o; + return isNotification == that.isNotification + && Objects.equal(id, that.id) + && Objects.equal(method, that.method) + && Arrays.equals(params, that.params) + && Objects.equal(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hashCode(id, method, Arrays.hashCode(params), version, isNotification); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequestId.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequestId.java new file mode 100755 index 00000000000..3430d301133 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/JsonRpcRequestId.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcRequestException; + +import java.math.BigInteger; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Objects; + +public class JsonRpcRequestId { + + private static final Class[] VALID_ID_TYPES = + new Class[] { + String.class, Integer.class, Long.class, Float.class, Double.class, BigInteger.class + }; + + private final Object id; + + @JsonCreator + public JsonRpcRequestId(final Object id) { + if (isRequestTypeInvalid(id)) { + throw new InvalidJsonRpcRequestException("Invalid id"); + } + this.id = id; + } + + @JsonValue + public Object getValue() { + return id; + } + + private boolean isRequestTypeInvalid(final Object id) { + return isNotNull(id) && isTypeInvalid(id); + } + + /** + * The JSON spec says "The use of Null as a value for the id member in a Request object is + * discouraged" Both geth and parity accept null values, so we decided to support them as well. + */ + private boolean isNotNull(final Object id) { + return id != null; + } + + private boolean isTypeInvalid(final Object id) { + for (final Class validType : VALID_ID_TYPES) { + if (validType.isInstance(id)) { + return false; + } + } + + return true; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final JsonRpcRequestId that = (JsonRpcRequestId) o; + return Objects.equal(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcParameters.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcParameters.java new file mode 100755 index 00000000000..869eb8edf6d --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcParameters.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.exception; + +public class InvalidJsonRpcParameters extends InvalidJsonRpcRequestException { + + public InvalidJsonRpcParameters(final String s) { + super(s); + } + + public InvalidJsonRpcParameters(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcRequestException.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcRequestException.java new file mode 100755 index 00000000000..705beb040a9 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/exception/InvalidJsonRpcRequestException.java @@ -0,0 +1,11 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.exception; + +public class InvalidJsonRpcRequestException extends IllegalArgumentException { + public InvalidJsonRpcRequestException(final String message) { + super(message); + } + + public InvalidJsonRpcRequestException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGenerator.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGenerator.java new file mode 100755 index 00000000000..fcd0acb9452 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGenerator.java @@ -0,0 +1,10 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import java.util.UUID; + +public class FilterIdGenerator { + + public String nextId() { + return "0x" + UUID.randomUUID().toString().replaceAll("-", ""); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManager.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManager.java new file mode 100755 index 00000000000..233bb7372aa --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManager.java @@ -0,0 +1,296 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.google.common.annotations.VisibleForTesting; + +/** Manages JSON-RPC filter events. */ +public class FilterManager { + + private final Map blockFilters = new ConcurrentHashMap<>(); + + private final Map pendingTransactionFilters = + new ConcurrentHashMap<>(); + + private final Map logFilters = new ConcurrentHashMap<>(); + + private final FilterIdGenerator filterIdGenerator; + + /** Tracks new blocks being added to the blockchain. */ + private static class BlockFilter { + + private final List blockHashes = new ArrayList<>(); + + BlockFilter() {} + + void addBlockHash(final Hash hash) { + blockHashes.add(hash); + } + + List blockHashes() { + return blockHashes; + } + + void clearBlockHashes() { + blockHashes.clear(); + } + } + + /** Tracks new pending transactions that have arrived in the transaction pool */ + private static class PendingTransactionFilter { + + private final List transactionHashes = new ArrayList<>(); + + PendingTransactionFilter() {} + + void addTransactionHash(final Hash hash) { + transactionHashes.add(hash); + } + + List transactionHashes() { + return transactionHashes; + } + + void clearTransactionHashes() { + transactionHashes.clear(); + } + } + + /** Tracks new log events. */ + private static class LogFilter { + + private final BlockParameter fromBlock; + private final BlockParameter toBlock; + private final LogsQuery logsQuery; + + private final List logs = new ArrayList<>(); + + LogFilter( + final BlockParameter fromBlock, final BlockParameter toBlock, final LogsQuery logsQuery) { + this.fromBlock = fromBlock; + this.toBlock = toBlock; + this.logsQuery = logsQuery; + } + + public BlockParameter getFromBlock() { + return fromBlock; + } + + public BlockParameter getToBlock() { + return toBlock; + } + + public LogsQuery getLogsQuery() { + return logsQuery; + } + + void addLog(final List logs) { + this.logs.addAll(logs); + } + + List logs() { + return logs; + } + + void clearLogs() { + logs.clear(); + } + } + + private final BlockchainQueries blockchainQueries; + + public FilterManager( + final BlockchainQueries blockchainQueries, + final TransactionPool transactionPool, + final FilterIdGenerator filterIdGenerator) { + this.filterIdGenerator = filterIdGenerator; + checkNotNull(blockchainQueries.getBlockchain()); + blockchainQueries.getBlockchain().observeBlockAdded(this::recordBlockEvent); + transactionPool.addTransactionListener(this::recordPendingTransactionEvent); + this.blockchainQueries = blockchainQueries; + } + + /** + * Installs a new block filter + * + * @return the block filter id + */ + public String installBlockFilter() { + final String filterId = filterIdGenerator.nextId(); + blockFilters.put(filterId, new BlockFilter()); + return filterId; + } + + /** + * Installs a pending transaction filter + * + * @return the transaction filter id + */ + public String installPendingTransactionFilter() { + final String filterId = filterIdGenerator.nextId(); + pendingTransactionFilters.put(filterId, new PendingTransactionFilter()); + return filterId; + } + + /** + * Installs a new log filter + * + * @param fromBlock {@link BlockParameter} Integer block number, or latest/pending/earliest. + * @param toBlock {@link BlockParameter} Integer block number, or latest/pending/earliest. + * @param logsQuery {@link LogsQuery} Addresses and/or topics to filter by + * @return the log filter id + */ + public String installLogFilter( + final BlockParameter fromBlock, final BlockParameter toBlock, final LogsQuery logsQuery) { + final String filterId = filterIdGenerator.nextId(); + logFilters.put(filterId, new LogFilter(fromBlock, toBlock, logsQuery)); + return filterId; + } + + /** + * Uninstalls the specified filter. + * + * @param filterId the id of the filter to remove + * @return {@code true} if the filter was successfully removed; otherwise {@code false} + */ + public boolean uninstallFilter(final String filterId) { + return blockFilters.remove(filterId) != null + || pendingTransactionFilters.remove(filterId) != null + || logFilters.remove(filterId) != null; + } + + public void recordBlockEvent(final BlockAddedEvent event, final Blockchain blockchain) { + final Hash blockHash = event.getBlock().getHash(); + blockFilters.forEach( + (filterId, filter) -> { + synchronized (filter) { + filter.addBlockHash(blockHash); + } + }); + + checkBlockchainForMatchingLogsForFilters(); + } + + private void checkBlockchainForMatchingLogsForFilters() { + logFilters.forEach( + (filterId, filter) -> { + final long headBlockNumber = blockchainQueries.headBlockNumber(); + final long toBlockNumber = + filter.getToBlock().getNumber().orElse(blockchainQueries.headBlockNumber()); + final List logs = + blockchainQueries.matchingLogs(headBlockNumber, toBlockNumber, filter.getLogsQuery()); + filter.addLog(logs); + }); + } + + @VisibleForTesting + void recordPendingTransactionEvent(final Transaction transaction) { + if (pendingTransactionFilters.isEmpty()) { + return; + } + + pendingTransactionFilters.forEach( + (filterId, filter) -> { + synchronized (filter) { + filter.addTransactionHash(transaction.hash()); + } + }); + } + + /** + * Gets the new block hashes that have occurred since the filter was last checked. + * + * @param filterId the id of the filter to get the new blocks for + * @return the new block hashes that have occurred since the filter was last checked + */ + public List blockChanges(final String filterId) { + final BlockFilter filter = blockFilters.get(filterId); + if (filter == null) { + return null; + } + + final List hashes; + synchronized (filter) { + hashes = new ArrayList<>(filter.blockHashes()); + filter.clearBlockHashes(); + } + return hashes; + } + + /** + * Gets the pending transactions that have occurred since the filter was last checked. + * + * @param filterId the id of the filter to get the pending transactions for + * @return the new pending transaction hashes that have occurred since the filter was last checked + */ + public List pendingTransactionChanges(final String filterId) { + final PendingTransactionFilter filter = pendingTransactionFilters.get(filterId); + if (filter == null) { + return null; + } + + final List hashes; + synchronized (filter) { + hashes = new ArrayList<>(filter.transactionHashes()); + filter.clearTransactionHashes(); + } + return hashes; + } + + public List logsChanges(final String filterId) { + final LogFilter filter = logFilters.get(filterId); + if (filter == null) { + return null; + } + + List logs; + synchronized (filter) { + logs = new ArrayList<>(filter.logs()); + filter.clearLogs(); + } + return logs; + } + + public List logs(final String filterId) { + final LogFilter filter = logFilters.get(filterId); + if (filter == null) { + return null; + } + + final long fromBlockNumber = + filter.getFromBlock().getNumber().orElse(blockchainQueries.headBlockNumber()); + final long toBlockNumber = + filter.getToBlock().getNumber().orElse(blockchainQueries.headBlockNumber()); + + return blockchainQueries.matchingLogs(fromBlockNumber, toBlockNumber, filter.getLogsQuery()); + } + + @VisibleForTesting + int blockFilterCount() { + return blockFilters.size(); + } + + @VisibleForTesting + int pendingTransactionFilterCount() { + return pendingTransactionFilters.size(); + } + + @VisibleForTesting + int logFilterCount() { + return logFilters.size(); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQuery.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQuery.java new file mode 100755 index 00000000000..6b2e07f928f --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQuery.java @@ -0,0 +1,101 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.LogTopic; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.TopicsParameter; + +import java.util.Arrays; +import java.util.List; + +import com.google.common.collect.Lists; + +public class LogsQuery { + + private final List
queryAddresses; + private final List> queryTopics; + + private LogsQuery(final List
addresses, final List> topics) { + this.queryAddresses = addresses; + this.queryTopics = topics; + } + + public boolean matches(final Log log) { + return matchesAddresses(log.getLogger()) && matchesTopics(log.getTopics()); + } + + private boolean matchesAddresses(final Address address) { + return queryAddresses.isEmpty() || queryAddresses.contains(address); + } + + private boolean matchesTopics(final List topics) { + if (queryTopics.isEmpty()) { + return true; + } + if (topics.size() < queryTopics.size()) { + return false; + } + for (int i = 0; i < queryTopics.size(); ++i) { + if (!matchesTopic(topics.get(i), queryTopics.get(i))) { + return false; + } + } + return true; + } + + private boolean matchesTopic(final LogTopic topic, final List matchCriteria) { + for (final LogTopic candidate : matchCriteria) { + if (candidate == null) { + return true; + } + if (candidate.equals(topic)) { + return true; + } + } + return false; + } + + public static class Builder { + private final List
queryAddresses = Lists.newArrayList(); + private final List> queryTopics = Lists.newArrayList(); + + public Builder address(final Address address) { + if (address != null) { + queryAddresses.add(address); + } + return this; + } + + public Builder addresses(final Address... addresses) { + if (addresses != null && addresses.length > 0) { + queryAddresses.addAll(Arrays.asList(addresses)); + } + return this; + } + + public Builder addresses(final List
addresses) { + if (addresses != null && !addresses.isEmpty()) { + queryAddresses.addAll(addresses); + } + return this; + } + + public Builder topics(final List> topics) { + if (topics != null && !topics.isEmpty()) { + queryTopics.addAll(topics); + } + return this; + } + + public Builder topics(final TopicsParameter topicsParameter) { + if (topicsParameter != null) { + topics(topicsParameter.getTopics()); + } + return this; + } + + public LogsQuery build() { + return new LogsQuery(queryAddresses, queryTopics); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AbstractBlockParameterMethod.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AbstractBlockParameterMethod.java new file mode 100755 index 00000000000..a1d5377836b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AbstractBlockParameterMethod.java @@ -0,0 +1,66 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.OptionalLong; + +abstract class AbstractBlockParameterMethod implements JsonRpcMethod { + + private final BlockchainQueries blockchainQueries; + private final JsonRpcParameter parameters; + + protected AbstractBlockParameterMethod( + final BlockchainQueries blockchainQueries, final JsonRpcParameter parameters) { + this.blockchainQueries = blockchainQueries; + this.parameters = parameters; + } + + protected abstract BlockParameter blockParameter(JsonRpcRequest request); + + protected abstract Object resultByBlockNumber(JsonRpcRequest request, long blockNumber); + + protected BlockchainQueries blockchainQueries() { + return blockchainQueries; + } + + protected JsonRpcParameter parameters() { + return parameters; + } + + protected Object pendingResult(final JsonRpcRequest request) { + // TODO: Update once we mine and better understand pending semantics. + // This may also be worth always returning null for. + return null; + } + + protected Object latestResult(final JsonRpcRequest request) { + return resultByBlockNumber(request, blockchainQueries.headBlockNumber()); + } + + protected Object findResultByParamType(final JsonRpcRequest request) { + final BlockParameter blockParam = blockParameter(request); + + final Object result; + final OptionalLong blockNumber = blockParam.getNumber(); + if (blockNumber.isPresent()) { + result = resultByBlockNumber(request, blockNumber.getAsLong()); + } else if (blockParam.isLatest()) { + result = latestResult(request); + } else { + // If block parameter is not numeric or latest, it is pending. + result = pendingResult(request); + } + + return result; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + return new JsonRpcSuccessResponse(request.getId(), findResultByParamType(request)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeers.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeers.java new file mode 100755 index 00000000000..00173c6b2dc --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeers.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.PeerResult; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class AdminPeers implements JsonRpcMethod { + private static final Logger LOG = LogManager.getLogger(); + private final P2PNetwork peerDiscoveryAgent; + + public AdminPeers(final P2PNetwork peerDiscoveryAgent) { + this.peerDiscoveryAgent = peerDiscoveryAgent; + } + + @Override + public String getName() { + return "admin_peers"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + + try { + final List peers = + peerDiscoveryAgent.getPeers().stream().map(PeerResult::new).collect(Collectors.toList()); + final JsonRpcResponse result = new JsonRpcSuccessResponse(req.getId(), peers); + return result; + } catch (final Exception e) { + LOG.error("Error processing request: " + req, e); + throw e; + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAt.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAt.java new file mode 100755 index 00000000000..e5fa6684ff5 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAt.java @@ -0,0 +1,77 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.BlockReplay; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.DebugStorageRangeAtResult; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.NavigableMap; + +public class DebugStorageRangeAt implements JsonRpcMethod { + + private final JsonRpcParameter parameters; + private final BlockchainQueries blockchainQueries; + private final BlockReplay blockReplay; + + public DebugStorageRangeAt( + final JsonRpcParameter parameters, + final BlockchainQueries blockchainQueries, + final BlockReplay blockReplay) { + this.parameters = parameters; + this.blockchainQueries = blockchainQueries; + this.blockReplay = blockReplay; + } + + @Override + public String getName() { + return "debug_storageRangeAt"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Hash blockHash = parameters.required(request.getParams(), 0, Hash.class); + final int transactionIndex = parameters.required(request.getParams(), 1, Integer.class); + final Address accountAddress = parameters.required(request.getParams(), 2, Address.class); + final Hash startKey = parameters.required(request.getParams(), 3, Hash.class); + final int limit = parameters.required(request.getParams(), 4, Integer.class); + + final TransactionWithMetadata transactionWithMetadata = + blockchainQueries.transactionByBlockHashAndIndex(blockHash, transactionIndex); + + return blockReplay + .afterTransactionInBlock( + blockHash, + transactionWithMetadata.getTransaction().hash(), + (transaction, blockHeader, blockchain, worldState, transactionProcessor) -> + extractStorageAt(request, accountAddress, startKey, limit, worldState)) + .orElseGet(() -> new JsonRpcSuccessResponse(request.getId(), null)); + } + + private JsonRpcSuccessResponse extractStorageAt( + final JsonRpcRequest request, + final Address accountAddress, + final Hash startKey, + final int limit, + final MutableWorldState worldState) { + final Account account = worldState.get(accountAddress); + final NavigableMap entries = account.storageEntriesFrom(startKey, limit + 1); + + Bytes32 nextKey = null; + if (entries.size() == limit + 1) { + nextKey = entries.lastKey(); + entries.remove(nextKey); + } + return new JsonRpcSuccessResponse( + request.getId(), new DebugStorageRangeAtResult(entries, nextKey)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransaction.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransaction.java new file mode 100755 index 00000000000..6929c705336 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransaction.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.debug.TraceOptions; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransactionTraceParams; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransactionTracer; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.DebugTraceTransactionResult; +import net.consensys.pantheon.ethereum.vm.DebugOperationTracer; + +import java.util.Optional; + +public class DebugTraceTransaction implements JsonRpcMethod { + + private final JsonRpcParameter parameters; + private final TransactionTracer transactionTracer; + private final BlockchainQueries blockchain; + + public DebugTraceTransaction( + final BlockchainQueries blockchain, + final TransactionTracer transactionTracer, + final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.transactionTracer = transactionTracer; + this.parameters = parameters; + } + + @Override + public String getName() { + return "debug_traceTransaction"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + final Optional transactionTraceParams = + parameters.optional(request.getParams(), 1, TransactionTraceParams.class); + final Optional transactionWithMetadata = + blockchain.transactionByHash(hash); + final Hash blockHash = transactionWithMetadata.get().getBlockHash(); + final TraceOptions traceOptions = + transactionTraceParams + .map(TransactionTraceParams::traceOptions) + .orElse(TraceOptions.DEFAULT); + + final DebugOperationTracer execTracer = new DebugOperationTracer(traceOptions); + + final DebugTraceTransactionResult result = + transactionTracer + .traceTransaction(blockHash, hash, execTracer) + .map(DebugTraceTransactionResult::new) + .orElse(null); + return new JsonRpcSuccessResponse(request.getId(), result); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthAccounts.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthAccounts.java new file mode 100755 index 00000000000..bc38fc282c0 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthAccounts.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class EthAccounts implements JsonRpcMethod { + + @Override + public String getName() { + return "eth_accounts"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + // For now, just return an empty list. + return new JsonRpcSuccessResponse(req.getId(), new Object[] {}); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthBlockNumber.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthBlockNumber.java new file mode 100755 index 00000000000..48d373aa8e0 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthBlockNumber.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +public class EthBlockNumber implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + + public EthBlockNumber(final BlockchainQueries blockchain) { + this.blockchain = blockchain; + } + + @Override + public String getName() { + return "eth_blockNumber"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), Quantity.create(blockchain.headBlockNumber())); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCall.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCall.java new file mode 100755 index 00000000000..53bcd19cfb7 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCall.java @@ -0,0 +1,75 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcErrorConverter.convertTransactionInvalidReason; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessor; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class EthCall extends AbstractBlockParameterMethod { + + private final TransientTransactionProcessor transientTransactionProcessor; + + public EthCall( + final BlockchainQueries blockchainQueries, + final TransientTransactionProcessor transientTransactionProcessor, + final JsonRpcParameter parameters) { + super(blockchainQueries, parameters); + this.transientTransactionProcessor = transientTransactionProcessor; + } + + @Override + public String getName() { + return "eth_call"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 1, BlockParameter.class); + } + + @Override + protected Object resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + final CallParameter callParams = validateAndGetCallParams(request); + + return transientTransactionProcessor + .process(callParams, blockNumber) + .map( + result -> + result + .getValidationResult() + .either( + (() -> + new JsonRpcSuccessResponse( + request.getId(), result.getOutput().toString())), + reason -> + new JsonRpcErrorResponse( + request.getId(), convertTransactionInvalidReason(reason)))) + .orElse(validRequestBlockNotFound(request)); + } + + private JsonRpcSuccessResponse validRequestBlockNotFound(final JsonRpcRequest request) { + return new JsonRpcSuccessResponse(request.getId(), null); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + return (JsonRpcResponse) findResultByParamType(request); + } + + private CallParameter validateAndGetCallParams(final JsonRpcRequest request) { + final CallParameter callParams = + parameters().required(request.getParams(), 0, CallParameter.class); + if (callParams.getTo() == null) { + throw new InvalidJsonRpcParameters("Missing \"to\" field in call arguments"); + } + return callParams; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbase.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbase.java new file mode 100755 index 00000000000..3981ac336fb --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbase.java @@ -0,0 +1,34 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Optional; + +public class EthCoinbase implements JsonRpcMethod { + + private final MiningCoordinator miningCoordinator; + + public EthCoinbase(final MiningCoordinator miningCoordinator) { + this.miningCoordinator = miningCoordinator; + } + + @Override + public String getName() { + return "eth_coinbase"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + final Optional
coinbase = miningCoordinator.getCoinbase(); + if (coinbase.isPresent()) { + return new JsonRpcSuccessResponse(req.getId(), coinbase.get().toString()); + } + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.COINBASE_NOT_SPECIFIED); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGas.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGas.java new file mode 100755 index 00000000000..a0bff4a24ca --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGas.java @@ -0,0 +1,82 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessingResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessor; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +import java.util.function.Function; + +public class EthEstimateGas implements JsonRpcMethod { + + private final BlockchainQueries blockchainQueries; + private final TransientTransactionProcessor transientTransactionProcessor; + private final JsonRpcParameter parameters; + + public EthEstimateGas( + final BlockchainQueries blockchainQueries, + final TransientTransactionProcessor transientTransactionProcessor, + final JsonRpcParameter parameters) { + this.blockchainQueries = blockchainQueries; + this.transientTransactionProcessor = transientTransactionProcessor; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_estimateGas"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final CallParameter callParams = + parameters.required(request.getParams(), 0, CallParameter.class); + + final BlockHeader blockHeader = blockHeader(); + if (blockHeader == null) { + return errorResponse(request); + } + + final CallParameter modifiedCallParams = + overrideGasLimitAndPrice(callParams, blockHeader.getGasLimit()); + + return transientTransactionProcessor + .process(modifiedCallParams, blockHeader.getNumber()) + .map(gasEstimateResponse(request)) + .orElse(errorResponse(request)); + } + + private BlockHeader blockHeader() { + final long headBlockNumber = blockchainQueries.headBlockNumber(); + return blockchainQueries.getBlockchain().getBlockHeader(headBlockNumber).orElse(null); + } + + private CallParameter overrideGasLimitAndPrice( + final CallParameter callParams, final long gasLimit) { + return new CallParameter( + callParams.getFrom() != null ? callParams.getFrom().toString() : null, + callParams.getTo() != null ? callParams.getTo().toString() : null, + Quantity.create(gasLimit), + Quantity.create(0L), + callParams.getValue() != null ? Quantity.create(callParams.getValue()) : null, + callParams.getPayload() != null ? callParams.getPayload().toString() : null); + } + + private Function gasEstimateResponse( + final JsonRpcRequest request) { + return result -> + new JsonRpcSuccessResponse(request.getId(), Quantity.create(result.getGasEstimate())); + } + + private JsonRpcErrorResponse errorResponse(final JsonRpcRequest request) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPrice.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPrice.java new file mode 100755 index 00000000000..be485ccac0c --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPrice.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +public class EthGasPrice implements JsonRpcMethod { + + private final MiningCoordinator miningCoordinator; + + public EthGasPrice(final MiningCoordinator miningCoordinator) { + this.miningCoordinator = miningCoordinator; + } + + @Override + public String getName() { + return "eth_gasPrice"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + Wei gasPrice; + Object result = null; + gasPrice = miningCoordinator.getMinTransactionGasPrice(); + if (gasPrice != null) { + result = Quantity.create(gasPrice.toLong()); + } + return new JsonRpcSuccessResponse(req.getId(), result); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBalance.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBalance.java new file mode 100755 index 00000000000..98dcbb09f8d --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBalance.java @@ -0,0 +1,34 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +public class EthGetBalance extends AbstractBlockParameterMethod { + + public EthGetBalance(final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + super(blockchain, parameters); + } + + @Override + public String getName() { + return "eth_getBalance"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 1, BlockParameter.class); + } + + @Override + protected String resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + final Address address = parameters().required(request.getParams(), 0, Address.class); + return blockchainQueries() + .accountBalance(address, blockNumber) + .map(Quantity::create) + .orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHash.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHash.java new file mode 100755 index 00000000000..29a326c2f1b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHash.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactory; + +public class EthGetBlockByHash implements JsonRpcMethod { + + private final BlockResultFactory blockResult; + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetBlockByHash( + final BlockchainQueries blockchain, + final BlockResultFactory blockResult, + final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.blockResult = blockResult; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getBlockByHash"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + return new JsonRpcSuccessResponse(request.getId(), blockResult(request)); + } + + private BlockResult blockResult(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + + if (isCompleteTransactions(request)) { + return transactionComplete(hash); + } + + return transactionHash(hash); + } + + private BlockResult transactionComplete(final Hash hash) { + return blockchain.blockByHash(hash).map(tx -> blockResult.transactionComplete(tx)).orElse(null); + } + + private BlockResult transactionHash(final Hash hash) { + return blockchain + .blockByHashWithTxHashes(hash) + .map(tx -> blockResult.transactionHash(tx)) + .orElse(null); + } + + private boolean isCompleteTransactions(final JsonRpcRequest request) { + return parameters.required(request.getParams(), 1, Boolean.class); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByNumber.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByNumber.java new file mode 100755 index 00000000000..0d15fb7a8ee --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByNumber.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactory; + +public class EthGetBlockByNumber extends AbstractBlockParameterMethod { + + private final BlockResultFactory blockResult; + + public EthGetBlockByNumber( + final BlockchainQueries blockchain, + final BlockResultFactory blockResult, + final JsonRpcParameter parameters) { + super(blockchain, parameters); + this.blockResult = blockResult; + } + + @Override + public String getName() { + return "eth_getBlockByNumber"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 0, BlockParameter.class); + } + + @Override + protected Object resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + if (isCompleteTransactions(request)) { + return transactionComplete(blockNumber); + } + + return transactionHash(blockNumber); + } + + private BlockResult transactionComplete(final long blockNumber) { + return blockchainQueries() + .blockByNumber(blockNumber) + .map(tx -> blockResult.transactionComplete(tx)) + .orElse(null); + } + + private BlockResult transactionHash(final long blockNumber) { + return blockchainQueries() + .blockByNumberWithTxHashes(blockNumber) + .map(tx -> blockResult.transactionHash(tx)) + .orElse(null); + } + + private boolean isCompleteTransactions(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 1, Boolean.class); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByHash.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByHash.java new file mode 100755 index 00000000000..4fbdfde8e77 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByHash.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +public class EthGetBlockTransactionCountByHash implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetBlockTransactionCountByHash( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getBlockTransactionCountByHash"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + final Integer count = blockchain.getTransactionCount(hash); + + if (count == -1) { + return new JsonRpcSuccessResponse(request.getId(), null); + } + return new JsonRpcSuccessResponse(request.getId(), Quantity.create(count)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByNumber.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByNumber.java new file mode 100755 index 00000000000..2e847fbff05 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockTransactionCountByNumber.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +public class EthGetBlockTransactionCountByNumber extends AbstractBlockParameterMethod { + + public EthGetBlockTransactionCountByNumber( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + super(blockchain, parameters); + } + + @Override + public String getName() { + return "eth_getBlockTransactionCountByNumber"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 0, BlockParameter.class); + } + + @Override + protected String resultByBlockNumber(final JsonRpcRequest req, final long blockNumber) { + return blockchainQueries().getTransactionCount(blockNumber).map(Quantity::create).orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetCode.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetCode.java new file mode 100755 index 00000000000..0aaf7ce1f32 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetCode.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class EthGetCode extends AbstractBlockParameterMethod { + + public EthGetCode(final BlockchainQueries blockchainQueries, final JsonRpcParameter parameters) { + super(blockchainQueries, parameters); + } + + @Override + public String getName() { + return "eth_getCode"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 1, BlockParameter.class); + } + + @Override + protected String resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + final Address address = parameters().required(request.getParams(), 0, Address.class); + return blockchainQueries().getCode(address, blockNumber).map(BytesValue::toString).orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChanges.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChanges.java new file mode 100755 index 00000000000..83f4de2db1e --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChanges.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.LogsResult; + +import java.util.List; +import java.util.stream.Collectors; + +public class EthGetFilterChanges implements JsonRpcMethod { + + private final FilterManager filterManager; + private final JsonRpcParameter parameters; + + public EthGetFilterChanges(final FilterManager filterManager, final JsonRpcParameter parameters) { + this.filterManager = filterManager; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getFilterChanges"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final String filterId = parameters.required(request.getParams(), 0, String.class); + + final List blockHashes = filterManager.blockChanges(filterId); + if (blockHashes != null) { + return new JsonRpcSuccessResponse( + request.getId(), + blockHashes.stream().map(h -> h.toString()).collect(Collectors.toList())); + } + + final List transactionHashes = filterManager.pendingTransactionChanges(filterId); + if (transactionHashes != null) { + return new JsonRpcSuccessResponse( + request.getId(), + transactionHashes.stream().map(h -> h.toString()).collect(Collectors.toList())); + } + + final List logs = filterManager.logsChanges(filterId); + if (logs != null) { + return new JsonRpcSuccessResponse(request.getId(), new LogsResult(logs)); + } + + // Filter was not found. + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.FILTER_NOT_FOUND); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogs.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogs.java new file mode 100755 index 00000000000..0b68878f7ad --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogs.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.LogsResult; + +import java.util.List; + +public class EthGetFilterLogs implements JsonRpcMethod { + + private final FilterManager filterManager; + private final JsonRpcParameter parameters; + + public EthGetFilterLogs(final FilterManager filterManager, final JsonRpcParameter parameters) { + this.filterManager = filterManager; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getFilterLogs"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final String filterId = parameters.required(request.getParams(), 0, String.class); + + final List logs = filterManager.logs(filterId); + if (logs != null) { + return new JsonRpcSuccessResponse(request.getId(), new LogsResult(logs)); + } + + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.FILTER_NOT_FOUND); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetLogs.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetLogs.java new file mode 100755 index 00000000000..8183894789f --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetLogs.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.LogsQuery; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.LogsResult; + +public class EthGetLogs implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetLogs(final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getLogs"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final FilterParameter filter = + parameters.required(request.getParams(), 0, FilterParameter.class); + final LogsQuery query = + new LogsQuery.Builder() + .addresses(filter.getAddresses()) + .topics(filter.getTopics().getTopics()) + .build(); + + if (isValid(filter)) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + } + if (filter.getBlockhash() != null) { + return new JsonRpcSuccessResponse( + request.getId(), new LogsResult(blockchain.matchingLogs(filter.getBlockhash(), query))); + } + + final long fromBlockNumber = filter.getFromBlock().getNumber().orElse(0); + final long toBlockNumber = filter.getToBlock().getNumber().orElse(blockchain.headBlockNumber()); + + return new JsonRpcSuccessResponse( + request.getId(), + new LogsResult(blockchain.matchingLogs(fromBlockNumber, toBlockNumber, query))); + } + + private boolean isValid(final FilterParameter filter) { + return !filter.getFromBlock().isLatest() + && !filter.getToBlock().isLatest() + && filter.getBlockhash() != null; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetStorageAt.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetStorageAt.java new file mode 100755 index 00000000000..8e38f5254df --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetStorageAt.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.UInt256Parameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.util.uint.UInt256; + +public class EthGetStorageAt extends AbstractBlockParameterMethod { + + public EthGetStorageAt(final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + super(blockchain, parameters); + } + + @Override + public String getName() { + return "eth_getStorageAt"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 2, BlockParameter.class); + } + + @Override + protected String resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + final Address address = parameters().required(request.getParams(), 0, Address.class); + final UInt256 position = + parameters().required(request.getParams(), 1, UInt256Parameter.class).getValue(); + return blockchainQueries() + .storageAt(address, position, blockNumber) + .map(UInt256::toHexString) + .orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockHashAndIndex.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockHashAndIndex.java new file mode 100755 index 00000000000..7efaf276a98 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockHashAndIndex.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.UnsignedIntParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionCompleteResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionResult; + +public class EthGetTransactionByBlockHashAndIndex implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetTransactionByBlockHashAndIndex( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getTransactionByBlockHashAndIndex"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + final int index = + parameters.required(request.getParams(), 1, UnsignedIntParameter.class).getValue(); + final TransactionWithMetadata transactionWithMetadata = + blockchain.transactionByBlockHashAndIndex(hash, index); + final TransactionResult result = + transactionWithMetadata == null + ? null + : new TransactionCompleteResult(transactionWithMetadata); + + return new JsonRpcSuccessResponse(request.getId(), result); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockNumberAndIndex.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockNumberAndIndex.java new file mode 100755 index 00000000000..1813eb45245 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByBlockNumberAndIndex.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.UnsignedIntParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionCompleteResult; + +public class EthGetTransactionByBlockNumberAndIndex extends AbstractBlockParameterMethod { + + public EthGetTransactionByBlockNumberAndIndex( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + super(blockchain, parameters); + } + + @Override + public String getName() { + return "eth_getTransactionByBlockNumberAndIndex"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 0, BlockParameter.class); + } + + @Override + protected Object resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + final int index = + parameters().required(request.getParams(), 1, UnsignedIntParameter.class).getValue(); + final TransactionWithMetadata transactionWithMetadata = + blockchainQueries().transactionByBlockNumberAndIndex(blockNumber, index); + return transactionWithMetadata == null + ? null + : new TransactionCompleteResult(transactionWithMetadata); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByHash.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByHash.java new file mode 100755 index 00000000000..37df8dae0e9 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionByHash.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionCompleteResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionPendingResult; + +import java.util.Optional; + +public class EthGetTransactionByHash implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + private final PendingTransactions pendingTransactions; + private final JsonRpcParameter parameters; + + public EthGetTransactionByHash( + final BlockchainQueries blockchain, + final PendingTransactions pendingTransactions, + final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.pendingTransactions = pendingTransactions; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getTransactionByHash"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + if (request.getParamLength() != 1) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + } + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + final JsonRpcSuccessResponse jsonRpcSuccessResponse = + new JsonRpcSuccessResponse(request.getId(), getResult(hash)); + return jsonRpcSuccessResponse; + } + + private Object getResult(final Hash hash) { + final Optional transactionCompleteResult = + blockchain.transactionByHash(hash).map(TransactionCompleteResult::new); + return transactionCompleteResult.orElseGet( + () -> + pendingTransactions + .getTransactionByHash(hash) + .map(TransactionPendingResult::new) + .orElse(null)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCount.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCount.java new file mode 100755 index 00000000000..afce1983d51 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCount.java @@ -0,0 +1,59 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +import java.util.Optional; +import java.util.OptionalLong; +import java.util.concurrent.atomic.AtomicReference; + +public class EthGetTransactionCount extends AbstractBlockParameterMethod { + + private final PendingTransactions pendingTransactions; + + public EthGetTransactionCount( + final BlockchainQueries blockchain, + final PendingTransactions pendingTransactions, + final JsonRpcParameter parameters) { + super(blockchain, parameters); + this.pendingTransactions = pendingTransactions; + } + + @Override + public String getName() { + return "eth_getTransactionCount"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 1, BlockParameter.class); + } + + @Override + protected Object pendingResult(final JsonRpcRequest request) { + final Address address = parameters().required(request.getParams(), 0, Address.class); + final AtomicReference> pendingTransaction = + new AtomicReference<>(Optional.empty()); + final OptionalLong pendingNonce = pendingTransactions.getNextNonceForSender(address); + if (pendingNonce.isPresent()) { + return Quantity.create(pendingNonce.getAsLong()); + } else { + return latestResult(request); + } + } + + @Override + protected String resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + final Address address = parameters().required(request.getParams(), 0, Address.class); + if (blockNumber > blockchainQueries().headBlockNumber()) { + return null; + } + return Quantity.create(blockchainQueries().getTransactionCount(address, blockNumber)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceipt.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceipt.java new file mode 100755 index 00000000000..a589be6ddbf --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceipt.java @@ -0,0 +1,49 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionReceiptWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionReceiptResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionReceiptRootResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionReceiptStatusResult; +import net.consensys.pantheon.ethereum.mainnet.TransactionReceiptType; + +public class EthGetTransactionReceipt implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetTransactionReceipt( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getTransactionReceipt"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + final TransactionReceiptResult result = + blockchain + .transactionReceiptByTransactionHash(hash) + .map(receipt -> getResult(receipt)) + .orElse(null); + return new JsonRpcSuccessResponse(request.getId(), result); + } + + private TransactionReceiptResult getResult(final TransactionReceiptWithMetadata receipt) { + if (receipt.getReceipt().getTransactionReceiptType() == TransactionReceiptType.ROOT) { + return new TransactionReceiptRootResult(receipt); + } else { + return new TransactionReceiptStatusResult(receipt); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndex.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndex.java new file mode 100755 index 00000000000..3a4204a73f4 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndex.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.UnsignedIntParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.UncleBlockResult; + +public class EthGetUncleByBlockHashAndIndex implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetUncleByBlockHashAndIndex( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getUncleByBlockHashAndIndex"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + return new JsonRpcSuccessResponse(request.getId(), blockResult(request)); + } + + private BlockResult blockResult(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + final int index = + parameters.required(request.getParams(), 1, UnsignedIntParameter.class).getValue(); + + return blockchain.getOmmer(hash, index).map(UncleBlockResult::build).orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndex.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndex.java new file mode 100755 index 00000000000..7620163ab55 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndex.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.UnsignedIntParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.UncleBlockResult; + +public class EthGetUncleByBlockNumberAndIndex extends AbstractBlockParameterMethod { + + public EthGetUncleByBlockNumberAndIndex( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + super(blockchain, parameters); + } + + @Override + public String getName() { + return "eth_getUncleByBlockNumberAndIndex"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 0, BlockParameter.class); + } + + @Override + protected BlockResult resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + final int index = + parameters().required(request.getParams(), 1, UnsignedIntParameter.class).getValue(); + return blockchainQueries() + .getOmmer(blockNumber, index) + .map(UncleBlockResult::build) + .orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockHash.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockHash.java new file mode 100755 index 00000000000..57e06a5ef91 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockHash.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +public class EthGetUncleCountByBlockHash implements JsonRpcMethod { + + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetUncleCountByBlockHash( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + this.blockchain = blockchain; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_getUncleCountByBlockHash"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final Hash hash = parameters.required(request.getParams(), 0, Hash.class); + final String result = blockchain.getOmmerCount(hash).map(Quantity::create).orElse(null); + return new JsonRpcSuccessResponse(request.getId(), result); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockNumber.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockNumber.java new file mode 100755 index 00000000000..9ac56e57de2 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleCountByBlockNumber.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +public class EthGetUncleCountByBlockNumber extends AbstractBlockParameterMethod { + + public EthGetUncleCountByBlockNumber( + final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + super(blockchain, parameters); + } + + @Override + public String getName() { + return "eth_getUncleCountByBlockNumber"; + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters().required(request.getParams(), 0, BlockParameter.class); + } + + @Override + protected String resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + return blockchainQueries().getOmmerCount(blockNumber).map(Quantity::create).orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMining.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMining.java new file mode 100755 index 00000000000..6af6f34b692 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMining.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class EthMining implements JsonRpcMethod { + + private final MiningCoordinator miningCoordinator; + + public EthMining(final MiningCoordinator miningCoordinator) { + this.miningCoordinator = miningCoordinator; + } + + @Override + public String getName() { + return "eth_mining"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + + return new JsonRpcSuccessResponse(req.getId(), miningCoordinator.isRunning()); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilter.java new file mode 100755 index 00000000000..c52923adb68 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilter.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class EthNewBlockFilter implements JsonRpcMethod { + + private final FilterManager filterManager; + + public EthNewBlockFilter(final FilterManager filterManager) { + this.filterManager = filterManager; + } + + @Override + public String getName() { + return "eth_newBlockFilter"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), filterManager.installBlockFilter()); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilter.java new file mode 100755 index 00000000000..209f69d0e9b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilter.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.LogsQuery; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class EthNewFilter implements JsonRpcMethod { + + private final FilterManager filterManager; + private final JsonRpcParameter parameters; + + public EthNewFilter(final FilterManager filterManager, final JsonRpcParameter parameters) { + this.filterManager = filterManager; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_newFilter"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final FilterParameter filter = + parameters.required(request.getParams(), 0, FilterParameter.class); + final LogsQuery query = + new LogsQuery.Builder().addresses(filter.getAddresses()).topics(filter.getTopics()).build(); + + final String logFilterId = + filterManager.installLogFilter(filter.getFromBlock(), filter.getToBlock(), query); + + return new JsonRpcSuccessResponse(request.getId(), logFilterId); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewPendingTransactionFilter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewPendingTransactionFilter.java new file mode 100755 index 00000000000..ffeff5f7f37 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewPendingTransactionFilter.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class EthNewPendingTransactionFilter implements JsonRpcMethod { + + private final FilterManager filterManager; + + public EthNewPendingTransactionFilter(final FilterManager filterManager) { + this.filterManager = filterManager; + } + + @Override + public String getName() { + return "eth_newPendingTransactionFilter"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), filterManager.installPendingTransactionFilter()); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersion.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersion.java new file mode 100755 index 00000000000..f2077b944d0 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersion.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.OptionalInt; +import java.util.Set; + +public class EthProtocolVersion implements JsonRpcMethod { + + private final Integer highestEthVersion; + + public EthProtocolVersion(final Set supportedCapabilities) { + final OptionalInt version = + supportedCapabilities + .stream() + .filter(cap -> EthProtocol.NAME.equals(cap.getName())) + .mapToInt(Capability::getVersion) + .max(); + highestEthVersion = version.isPresent() ? version.getAsInt() : null; + } + + @Override + public String getName() { + return "eth_protocolVersion"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), highestEthVersion); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransaction.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransaction.java new file mode 100755 index 00000000000..66a2bd43540 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransaction.java @@ -0,0 +1,73 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static net.consensys.pantheon.ethereum.jsonrpc.JsonRpcErrorConverter.convertTransactionInvalidReason; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcRequestException; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class EthSendRawTransaction implements JsonRpcMethod { + + private static final Logger LOGGER = LogManager.getLogger(EthSendRawTransaction.class); + + private final TransactionPool transactionPool; + private final JsonRpcParameter parameters; + + public EthSendRawTransaction( + final TransactionPool transactionPool, final JsonRpcParameter parameters) { + this.transactionPool = transactionPool; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_sendRawTransaction"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + if (request.getParamLength() != 1) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + } + final String rawTransaction = parameters.required(request.getParams(), 0, String.class); + + Transaction transaction; + try { + transaction = decodeRawTransaction(rawTransaction); + } catch (final InvalidJsonRpcRequestException e) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + } + + final ValidationResult validationResult = + transactionPool.addLocalTransaction(transaction); + return validationResult.either( + () -> new JsonRpcSuccessResponse(request.getId(), transaction.hash().toString()), + errorReason -> + new JsonRpcErrorResponse( + request.getId(), convertTransactionInvalidReason(errorReason))); + } + + private Transaction decodeRawTransaction(final String hash) + throws InvalidJsonRpcRequestException { + try { + return Transaction.readFrom(RLP.input(BytesValue.fromHexString(hash))); + } catch (IllegalArgumentException | RLPException e) { + LOGGER.debug(e); + throw new InvalidJsonRpcRequestException("Invalid raw transaction hex", e); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncing.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncing.java new file mode 100755 index 00000000000..497835a3f71 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncing.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.SyncingResult; + +/* + * SyncProgress retrieves the current progress of the syncing algorithm. If there's no sync + * currently running, it returns false. + */ +public class EthSyncing implements JsonRpcMethod { + + private final Synchronizer synchronizer; + + public EthSyncing(final Synchronizer synchronizer) { + this.synchronizer = synchronizer; + } + + @Override + public String getName() { + return "eth_syncing"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + // Returns false when not synchronizing. + final Object result = + synchronizer.getSyncStatus().map(s -> (Object) new SyncingResult(s)).orElse(false); + return new JsonRpcSuccessResponse(req.getId(), result); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthUninstallFilter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthUninstallFilter.java new file mode 100755 index 00000000000..1cd7ff1a752 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthUninstallFilter.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class EthUninstallFilter implements JsonRpcMethod { + + private final FilterManager filterManager; + private final JsonRpcParameter parameters; + + public EthUninstallFilter(final FilterManager filterManager, final JsonRpcParameter parameters) { + this.filterManager = filterManager; + this.parameters = parameters; + } + + @Override + public String getName() { + return "eth_uninstallFilter"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + final String filterId = parameters.required(request.getParams(), 0, String.class); + + return new JsonRpcSuccessResponse(request.getId(), filterManager.uninstallFilter(filterId)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/JsonRpcMethod.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/JsonRpcMethod.java new file mode 100755 index 00000000000..2719a853064 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/JsonRpcMethod.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; + +public interface JsonRpcMethod { + + /** + * Standardised JSON-RPC method name. + * + * @return identification of the JSON-RPC method. + */ + String getName(); + + /** + * Applies the method to given request. + * + * @param request input data for the JSON-RPC method. + * @return output from applying the JSON-RPC method to the input. + */ + JsonRpcResponse response(JsonRpcRequest request); +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListening.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListening.java new file mode 100755 index 00000000000..0cc7049acce --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListening.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; + +public class NetListening implements JsonRpcMethod { + + private final P2PNetwork p2pNetwork; + + public NetListening(final P2PNetwork p2pNetwork) { + this.p2pNetwork = p2pNetwork; + } + + @Override + public String getName() { + return "net_listening"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), p2pNetwork.isListening()); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetPeerCount.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetPeerCount.java new file mode 100755 index 00000000000..0da6b2f1af1 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetPeerCount.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; + +public class NetPeerCount implements JsonRpcMethod { + private final P2PNetwork p2pNetwork; + + public NetPeerCount(final P2PNetwork p2pNetwork) { + this.p2pNetwork = p2pNetwork; + } + + @Override + public String getName() { + return "net_peerCount"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), Quantity.create(p2pNetwork.getPeers().size())); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetVersion.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetVersion.java new file mode 100755 index 00000000000..d0826cec7dd --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetVersion.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +/** + * In Consensys' client, net_version maps to the network id, as specified in * + * https://github.com/ethereum/wiki/wiki/JSON-RPC#net_version + * + *

This method can be deprecated in the future, @see https://github.com/ethereum/EIPs/issues/611 + */ +public class NetVersion implements JsonRpcMethod { + private final String chainId; + + public NetVersion(final String chainId) { + this.chainId = chainId; + } + + @Override + public String getName() { + return "net_version"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), chainId); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3ClientVersion.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3ClientVersion.java new file mode 100755 index 00000000000..0521f6d5f14 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3ClientVersion.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class Web3ClientVersion implements JsonRpcMethod { + + private final String clientVersion; + + public Web3ClientVersion(final String clientVersion) { + this.clientVersion = clientVersion; + } + + @Override + public String getName() { + return "web3_clientVersion"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return new JsonRpcSuccessResponse(req.getId(), clientVersion); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3.java new file mode 100755 index 00000000000..1a87861b0b8 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class Web3Sha3 implements JsonRpcMethod { + + public Web3Sha3() {} + + @Override + public String getName() { + return "web3_sha3"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + if (req.getParamLength() != 1) { + // Do we want custom messages for each different type of invalid params? + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.INVALID_PARAMS); + } + + final String data = req.getParams()[0].toString(); + + if (!data.isEmpty() && !data.startsWith("0x")) { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.INVALID_PARAMS); + } + + try { + final BytesValue byteData = BytesValue.fromHexString(data); + return new JsonRpcSuccessResponse(req.getId(), Hash.keccak256(byteData).toString()); + } catch (final IllegalArgumentException err) { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.INVALID_PARAMS); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbase.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbase.java new file mode 100755 index 00000000000..e3796797ec7 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbase.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class MinerSetCoinbase implements JsonRpcMethod { + + private final MiningCoordinator miningCoordinator; + private final JsonRpcParameter parameters; + + public MinerSetCoinbase( + final MiningCoordinator miningCoordinator, final JsonRpcParameter parameters) { + this.miningCoordinator = miningCoordinator; + this.parameters = parameters; + } + + @Override + public String getName() { + return "miner_setCoinbase"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + final Address coinbase = parameters.required(req.getParams(), 0, Address.class); + + miningCoordinator.setCoinbase(coinbase); + + return new JsonRpcSuccessResponse(req.getId(), true); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbase.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbase.java new file mode 100755 index 00000000000..30d64a0f582 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbase.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; + +public class MinerSetEtherbase implements JsonRpcMethod { + + private final MinerSetCoinbase minerSetCoinbaseMethod; + + public MinerSetEtherbase(final MinerSetCoinbase minerSetCoinbaseMethod) { + + this.minerSetCoinbaseMethod = minerSetCoinbaseMethod; + } + + @Override + public String getName() { + return "miner_setEtherbase"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + return minerSetCoinbaseMethod.response(req); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStart.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStart.java new file mode 100755 index 00000000000..5f2d34904e4 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStart.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import net.consensys.pantheon.ethereum.blockcreation.CoinbaseNotSetException; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class MinerStart implements JsonRpcMethod { + + private final MiningCoordinator miningCoordinator; + + public MinerStart(final MiningCoordinator miningCoordinator) { + this.miningCoordinator = miningCoordinator; + } + + @Override + public String getName() { + return "miner_start"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + try { + miningCoordinator.enable(); + } catch (final CoinbaseNotSetException e) { + return new JsonRpcErrorResponse(req.getId(), JsonRpcError.COINBASE_NOT_SET); + } + + return new JsonRpcSuccessResponse(req.getId(), true); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStop.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStop.java new file mode 100755 index 00000000000..1d5e6785065 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStop.java @@ -0,0 +1,30 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +public class MinerStop implements JsonRpcMethod { + + private final MiningCoordinator miningCoordinator; + + public MinerStop(final MiningCoordinator miningCoordinator) { + this.miningCoordinator = miningCoordinator; + } + + @Override + public String getName() { + return "miner_stop"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest req) { + if (miningCoordinator != null) { + miningCoordinator.disable(); + } + + return new JsonRpcSuccessResponse(req.getId(), true); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/BlockParameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/BlockParameter.java new file mode 100755 index 00000000000..bc7a67f7fd9 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/BlockParameter.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import net.consensys.pantheon.ethereum.core.BlockHeader; + +import java.util.Objects; +import java.util.OptionalLong; + +import com.fasterxml.jackson.annotation.JsonCreator; + +// Represents a block parameter that can be a special value ("pending", "earliest", "latest") or +// a number formatted as a hex string. +// See: https://github.com/ethereum/wiki/wiki/JSON-RPC#the-default-block-parameter +public class BlockParameter { + + private final BlockParameterType type; + private final OptionalLong number; + + @JsonCreator + public BlockParameter(final String value) { + final String normalizedValue = value.toLowerCase(); + + if (Objects.equals(normalizedValue, "earliest")) { + type = BlockParameterType.EARLIEST; + number = OptionalLong.of(BlockHeader.GENESIS_BLOCK_NUMBER); + } else if (Objects.equals(normalizedValue, "latest")) { + type = BlockParameterType.LATEST; + number = OptionalLong.empty(); + } else if (Objects.equals(normalizedValue, "pending")) { + type = BlockParameterType.PENDING; + number = OptionalLong.empty(); + } else { + type = BlockParameterType.NUMERIC; + number = OptionalLong.of(Long.decode(value)); + } + } + + public OptionalLong getNumber() { + return number; + } + + public boolean isPending() { + return this.type == BlockParameterType.PENDING; + } + + public boolean isLatest() { + return this.type == BlockParameterType.LATEST; + } + + public boolean isEarliest() { + return this.type == BlockParameterType.EARLIEST; + } + + public boolean isNumeric() { + return this.type == BlockParameterType.NUMERIC; + } + + private enum BlockParameterType { + EARLIEST, + LATEST, + PENDING, + NUMERIC; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/CallParameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/CallParameter.java new file mode 100755 index 00000000000..9cbd46dd8ca --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/CallParameter.java @@ -0,0 +1,87 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Objects; + +// Represents parameters for a eth_call or eth_estimateGas JSON-RPC methods. +public class CallParameter { + + private final Address from; + + private final Address to; + + private final long gasLimit; + + private final Wei gasPrice; + + private final Wei value; + + private final BytesValue payload; + + @JsonCreator + public CallParameter( + @JsonProperty("from") final String from, + @JsonProperty("to") final String to, + @JsonProperty("gas") final String gasLimit, + @JsonProperty("gasPrice") final String gasPrice, + @JsonProperty("value") final String value, + @JsonProperty("data") final String payload) { + this.from = from != null ? Address.fromHexString(from) : null; + this.to = to != null ? Address.fromHexString(to) : null; + this.gasLimit = gasLimit != null ? Long.decode(gasLimit) : -1; + this.gasPrice = gasPrice != null ? Wei.fromHexString(gasPrice) : null; + this.value = value != null ? Wei.fromHexString(value) : null; + this.payload = payload != null ? BytesValue.fromHexString(payload) : null; + } + + public Address getFrom() { + return from; + } + + public Address getTo() { + return to; + } + + public long getGasLimit() { + return gasLimit; + } + + public Wei getGasPrice() { + return gasPrice; + } + + public Wei getValue() { + return value; + } + + public BytesValue getPayload() { + return payload; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CallParameter that = (CallParameter) o; + return gasLimit == that.gasLimit + && Objects.equal(from, that.from) + && Objects.equal(to, that.to) + && Objects.equal(gasPrice, that.gasPrice) + && Objects.equal(value, that.value) + && Objects.equal(payload, that.payload); + } + + @Override + public int hashCode() { + return Objects.hashCode(from, to, gasLimit, gasPrice, value, payload); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameter.java new file mode 100755 index 00000000000..95150b3e969 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameter.java @@ -0,0 +1,78 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +public class FilterParameter { + + private final BlockParameter fromBlock; + private final BlockParameter toBlock; + private final List

addresses; + private final TopicsParameter topics; + private final Hash blockhash; + + @JsonCreator + public FilterParameter( + @JsonProperty("fromBlock") final String fromBlock, + @JsonProperty("toBlock") final String toBlock, + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) @JsonProperty("address") + final List address, + @JsonProperty("topics") final List> topics, + @JsonProperty("blockhash") final String blockhash) { + this.fromBlock = + fromBlock != null ? new BlockParameter(fromBlock) : new BlockParameter("latest"); + this.toBlock = toBlock != null ? new BlockParameter(toBlock) : new BlockParameter("latest"); + this.addresses = address != null ? renderAddress(address) : Collections.emptyList(); + this.topics = + topics != null ? new TopicsParameter(topics) : new TopicsParameter(Collections.emptyList()); + this.blockhash = blockhash != null ? Hash.fromHexString(blockhash) : null; + } + + private List
renderAddress(final List inputAddresses) { + final List
addresses = new ArrayList<>(); + for (final String value : inputAddresses) { + addresses.add(Address.fromHexString(value)); + } + return addresses; + } + + public BlockParameter getFromBlock() { + return fromBlock; + } + + public BlockParameter getToBlock() { + return toBlock; + } + + public List
getAddresses() { + return addresses; + } + + public TopicsParameter getTopics() { + return topics; + } + + public Hash getBlockhash() { + return blockhash; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("fromBlock", fromBlock) + .add("toBlock", toBlock) + .add("addresses", addresses) + .add("topics", topics) + .add("blockhash", blockhash) + .toString(); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/JsonRpcParameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/JsonRpcParameter.java new file mode 100755 index 00000000000..20de07aca66 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/JsonRpcParameter.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonRpcParameter { + + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Retrieves a required parameter at the given index interpreted as the given class. Throws + * InvalidJsonRpcParameters if parameter is missing or of the wrong type. + * + * @param params the list of objects from which to extract a typed object. + * @param index Which index of the params array to access. + * @param paramClass What type is expected at this index. + * @param The type of parameter. + * @return Returns the parameter cast as T if available, otherwise throws exception. + */ + public T required(final Object[] params, final int index, final Class paramClass) { + final Optional optionalParam = optional(params, index, paramClass); + if (!optionalParam.isPresent()) { + throw new InvalidJsonRpcParameters("Missing required json rpc parameter at index " + index); + } + + return optionalParam.get(); + } + + /** + * Retrieves an optional parameter at the given index interpreted as the given class. Throws + * InvalidJsonRpcParameters if parameter is of the wrong type. + * + * @param params the list of objects from which to extract a typed object. + * @param index Which index of the params array to access. + * @param paramClass What type is expected at this index. + * @param The type of parameter. + * @return Returns the parameter cast as T if available. + */ + @SuppressWarnings("unchecked") + public Optional optional( + final Object[] params, final int index, final Class paramClass) { + if (params == null || params.length <= index) { + return Optional.empty(); + } + + final T param; + final Object rawParam = params[index]; + if (paramClass.isAssignableFrom(rawParam.getClass())) { + // If we're dealing with a simple type, just cast the value + param = (T) rawParam; + } else { + // Otherwise, serialize param back to json and then deserialize to the paramClass type + try { + final String json = mapper.writeValueAsString(rawParam); + param = mapper.readValue(json, paramClass); + } catch (final JsonProcessingException e) { + throw new InvalidJsonRpcParameters("Invalid json rpc parameter at index " + index, e); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + return Optional.of(param); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/TopicsParameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/TopicsParameter.java new file mode 100755 index 00000000000..a3c1198f63d --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/TopicsParameter.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import net.consensys.pantheon.ethereum.core.LogTopic; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public class TopicsParameter { + + private final List> queryTopics = new ArrayList<>(); + + @JsonCreator + public TopicsParameter(final List> topics) { + if (topics != null) { + for (final List list : topics) { + final List inputTopics = new ArrayList<>(); + if (list != null) { + for (final String input : list) { + final LogTopic topic = + input != null ? LogTopic.create(BytesValue.fromHexString(input)) : null; + inputTopics.add(topic); + } + } + queryTopics.add(inputTopics); + } + } + } + + public List> getTopics() { + return queryTopics; + } + + @Override + public String toString() { + return "TopicsParameter{" + "queryTopics=" + queryTopics + '}'; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UInt256Parameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UInt256Parameter.java new file mode 100755 index 00000000000..92389fb5982 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UInt256Parameter.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import net.consensys.pantheon.util.uint.UInt256; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public class UInt256Parameter { + + private final UInt256 value; + + @JsonCreator + public UInt256Parameter(final String value) { + this.value = UInt256.fromHexString(value); + } + + public UInt256 getValue() { + return value; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedIntParameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedIntParameter.java new file mode 100755 index 00000000000..a384b034f61 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedIntParameter.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public class UnsignedIntParameter { + + private final int value; + + @JsonCreator + public UnsignedIntParameter(final String value) { + this.value = Integer.decode(value); + checkArgument(this.value >= 0); + } + + public int getValue() { + return value; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedLongParameter.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedLongParameter.java new file mode 100755 index 00000000000..2a0c0c94a25 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/UnsignedLongParameter.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public class UnsignedLongParameter { + + private final long value; + + @JsonCreator + public UnsignedLongParameter(final String value) { + this.value = Long.decode(value); + checkArgument(this.value >= 0); + } + + public long getValue() { + return value; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/BlockReplay.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/BlockReplay.java new file mode 100755 index 00000000000..24002a6b590 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/BlockReplay.java @@ -0,0 +1,94 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; + +import java.util.Optional; + +public class BlockReplay { + + private final ProtocolSchedule protocolSchedule; + private final Blockchain blockchain; + private final WorldStateArchive worldStateArchive; + + public BlockReplay( + final ProtocolSchedule protocolSchedule, + final Blockchain blockchain, + final WorldStateArchive worldStateArchive) { + this.protocolSchedule = protocolSchedule; + this.blockchain = blockchain; + this.worldStateArchive = worldStateArchive; + } + + public Optional beforeTransactionInBlock( + final Hash blockHash, final Hash transactionHash, final Action action) { + final BlockHeader header = blockchain.getBlockHeader(blockHash).orElse(null); + if (header == null) { + return Optional.empty(); + } + final BlockBody body = blockchain.getBlockBody(header.getHash()).orElse(null); + if (body == null) { + return Optional.empty(); + } + final ProtocolSpec protocolSpec = protocolSchedule.getByBlockNumber(header.getNumber()); + final TransactionProcessor transactionProcessor = protocolSpec.getTransactionProcessor(); + final BlockHeader previous = blockchain.getBlockHeader(header.getParentHash()).orElse(null); + if (previous == null) { + return Optional.empty(); + } + final MutableWorldState mutableWorldState = + worldStateArchive.getMutable(previous.getStateRoot()); + for (final Transaction transaction : body.getTransactions()) { + if (transaction.hash().equals(transactionHash)) { + return Optional.of( + action.performAction( + transaction, header, blockchain, mutableWorldState, transactionProcessor)); + } else { + final ProtocolSpec spec = protocolSchedule.getByBlockNumber(header.getNumber()); + transactionProcessor.processTransaction( + blockchain, + mutableWorldState.updater(), + header, + transaction, + spec.getMiningBeneficiaryCalculator().calculateBeneficiary(header)); + } + } + return Optional.empty(); + } + + public Optional afterTransactionInBlock( + final Hash blockHash, final Hash transactionHash, final Action action) { + return beforeTransactionInBlock( + blockHash, + transactionHash, + (transaction, blockHeader, blockchain, worldState, transactionProcessor) -> { + final ProtocolSpec spec = protocolSchedule.getByBlockNumber(blockHeader.getNumber()); + transactionProcessor.processTransaction( + blockchain, + worldState.updater(), + blockHeader, + transaction, + spec.getMiningBeneficiaryCalculator().calculateBeneficiary(blockHeader)); + return action.performAction( + transaction, blockHeader, blockchain, worldState, transactionProcessor); + }); + } + + public interface Action { + + T performAction( + Transaction transaction, + BlockHeader blockHeader, + Blockchain blockchain, + MutableWorldState worldState, + TransactionProcessor transactionProcessor); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTrace.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTrace.java new file mode 100755 index 00000000000..f61f801d722 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTrace.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; + +import java.util.List; + +public class TransactionTrace { + + private final Transaction transaction; + private final Result result; + private final List traceFrames; + + public TransactionTrace( + final Transaction transaction, final Result result, final List traceFrames) { + this.transaction = transaction; + this.result = result; + this.traceFrames = traceFrames; + } + + public Transaction getTransaction() { + return transaction; + } + + public long getGas() { + return transaction.getGasLimit() - result.getGasRemaining(); + } + + public Result getResult() { + return result; + } + + public List getTraceFrames() { + return traceFrames; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTraceParams.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTraceParams.java new file mode 100755 index 00000000000..0920b106664 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTraceParams.java @@ -0,0 +1,29 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import net.consensys.pantheon.ethereum.debug.TraceOptions; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransactionTraceParams { + + private final boolean disableStorage; + private final boolean disableMemory; + private final boolean disableStack; + + @JsonCreator() + public TransactionTraceParams( + @JsonProperty("disableStorage") final boolean disableStorage, + @JsonProperty("disableMemory") final boolean disableMemory, + @JsonProperty("disableStack") final boolean disableStack) { + this.disableStorage = disableStorage; + this.disableMemory = disableMemory; + this.disableStack = disableStack; + } + + public TraceOptions traceOptions() { + return new TraceOptions(!disableStorage, !disableMemory, !disableStack); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracer.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracer.java new file mode 100755 index 00000000000..ded75d6677a --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracer.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; +import net.consensys.pantheon.ethereum.vm.DebugOperationTracer; + +import java.util.Optional; + +/** Used to produce debug traces of transactions */ +public class TransactionTracer { + + private final BlockReplay blockReplay; + + public TransactionTracer(final BlockReplay blockReplay) { + this.blockReplay = blockReplay; + } + + public Optional traceTransaction( + final Hash blockHash, final Hash transactionHash, final DebugOperationTracer tracer) { + return blockReplay.beforeTransactionInBlock( + blockHash, + transactionHash, + (transaction, header, blockchain, mutableWorldState, transactionProcessor) -> { + final Result result = + transactionProcessor.processTransaction( + blockchain, + mutableWorldState.updater(), + header, + transaction, + header.getCoinbase(), + tracer); + return new TransactionTrace(transaction, result, tracer.getTraceFrames()); + }); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResult.java new file mode 100755 index 00000000000..7d9006d63ab --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResult.java @@ -0,0 +1,57 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.google.common.base.Objects; + +public class TransientTransactionProcessingResult { + + private final Transaction transaction; + private final Result result; + + TransientTransactionProcessingResult(final Transaction transaction, final Result result) { + this.transaction = transaction; + this.result = result; + } + + public boolean isSuccessful() { + return result.isSuccessful(); + } + + public long getGasEstimate() { + return transaction.getGasLimit() - result.getGasRemaining(); + } + + public BytesValue getOutput() { + return result.getOutput(); + } + + public ValidationResult getValidationResult() { + return result.getValidationResult(); + } + + public Result getResult() { + return result; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final TransientTransactionProcessingResult that = (TransientTransactionProcessingResult) o; + return Objects.equal(transaction, that.transaction) && Objects.equal(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hashCode(transaction, result); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessor.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessor.java new file mode 100755 index 00000000000..04c8a29be7c --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessor.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +/* + * Used to process transactions for eth_call and eth_estimateGas. + * + * The processing won't affect the world state, it is used to execute read operations on the + * blockchain or to estimate the transaction gas cost. + */ +public class TransientTransactionProcessor { + + // Dummy signature for transactions to not fail being processed. + private static final SECP256K1.Signature FAKE_SIGNATURE = + SECP256K1.Signature.create(SECP256K1.HALF_CURVE_ORDER, SECP256K1.HALF_CURVE_ORDER, (byte) 0); + + // TODO: Identify a better default from account to use, such as the registered + // coinbase or an account currently unlocked by the client. + private static final Address DEFAULT_FROM = + Address.fromHexString("0x0000000000000000000000000000000000000000"); + + private final Blockchain blockchain; + private final WorldStateArchive worldStateArchive; + private final ProtocolSchedule protocolSchedule; + + public TransientTransactionProcessor( + final Blockchain blockchain, + final WorldStateArchive worldStateArchive, + final ProtocolSchedule protocolSchedule) { + this.blockchain = blockchain; + this.worldStateArchive = worldStateArchive; + this.protocolSchedule = protocolSchedule; + } + + public Optional process( + final CallParameter callParams, final long blockNumber) { + final BlockHeader header = blockchain.getBlockHeader(blockNumber).orElse(null); + if (header == null) { + return Optional.empty(); + } + final MutableWorldState worldState = worldStateArchive.getMutable(header.getStateRoot()); + + final Address senderAddress = + callParams.getFrom() != null ? callParams.getFrom() : DEFAULT_FROM; + final Account sender = worldState.get(senderAddress); + final long nonce = sender != null ? sender.getNonce() : 0L; + final long gasLimit = + callParams.getGasLimit() >= 0 ? callParams.getGasLimit() : header.getGasLimit(); + final Wei gasPrice = callParams.getGasPrice() != null ? callParams.getGasPrice() : Wei.ZERO; + final Wei value = callParams.getValue() != null ? callParams.getValue() : Wei.ZERO; + final BytesValue payload = + callParams.getPayload() != null ? callParams.getPayload() : BytesValue.EMPTY; + + final Transaction transaction = + Transaction.builder() + .nonce(nonce) + .gasPrice(gasPrice) + .gasLimit(gasLimit) + .to(callParams.getTo()) + .sender(senderAddress) + .value(value) + .payload(payload) + .signature(FAKE_SIGNATURE) + .build(); + + final ProtocolSpec protocolSpec = protocolSchedule.getByBlockNumber(header.getNumber()); + + final TransactionProcessor transactionProcessor = + protocolSchedule.getByBlockNumber(header.getNumber()).getTransactionProcessor(); + final TransactionProcessor.Result result = + transactionProcessor.processTransaction( + blockchain, + worldState.updater(), + header, + transaction, + protocolSpec.getMiningBeneficiaryCalculator().calculateBeneficiary(header)); + + return Optional.of(new TransientTransactionProcessingResult(transaction, result)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockWithMetadata.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockWithMetadata.java new file mode 100755 index 00000000000..68f0f3204d1 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockWithMetadata.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.queries; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; + +public class BlockWithMetadata { + + private final BlockHeader header; + private final List transactions; + private final List ommers; + private final UInt256 totalDifficulty; + private final int size; + + /** + * @param header The block header + * @param transactions Block transactions in generic format + * @param ommers Block ommers in generic format + * @param totalDifficulty The cumulative difficulty up to and including this block + * @param size The size of the rlp-encoded block (header + body). + */ + public BlockWithMetadata( + final BlockHeader header, + final List transactions, + final List ommers, + final UInt256 totalDifficulty, + final int size) { + this.header = header; + this.transactions = transactions; + this.ommers = ommers; + this.totalDifficulty = totalDifficulty; + this.size = size; + } + + public BlockHeader getHeader() { + return header; + } + + public List getOmmers() { + return ommers; + } + + public List getTransactions() { + return transactions; + } + + public UInt256 getTotalDifficulty() { + return totalDifficulty; + } + + public int getSize() { + return size; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueries.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueries.java new file mode 100755 index 00000000000..4ef03fab003 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueries.java @@ -0,0 +1,612 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.queries; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.chain.TransactionLocation; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.LogsQuery; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; + +public class BlockchainQueries { + + private final WorldStateArchive worldStateArchive; + private final Blockchain blockchain; + + public BlockchainQueries(final Blockchain blockchain, final WorldStateArchive worldStateArchive) { + this.blockchain = blockchain; + this.worldStateArchive = worldStateArchive; + } + + public Blockchain getBlockchain() { + return blockchain; + } + + public WorldStateArchive getWorldStateArchive() { + return worldStateArchive; + } + + /** + * Retrieves the header hash of the block at the given height in the canonical chain. + * + * @param number The height of the block whose hash should be retrieved. + * @return The hash of the block at the given height. + */ + public Optional getBlockHashByNumber(final long number) { + return blockchain.getBlockHashByNumber(number); + } + + /** + * Return the block number of the head of the chain. + * + * @return The block number of the head of the chain. + */ + public long headBlockNumber() { + return blockchain.getChainHeadBlockNumber(); + } + + /** + * Determines the block header for the address associated with this storage index. + * + * @param address The address of the account that owns the storage being queried. + * @param storageIndex The storage index whose value is being retrieved. + * @param blockNumber The blockNumber that is being queried. + * @return The value at the storage index being queried. + */ + public Optional storageAt( + final Address address, final UInt256 storageIndex, final long blockNumber) { + if (!withinValidRange(blockNumber)) { + return Optional.empty(); + } + return Optional.of( + blockchain + .getBlockHeader(blockNumber) + .map(header -> worldStateArchive.get(header.getStateRoot())) + .map(worldState -> worldState.get(address)) + .map(account -> account.getStorageValue(storageIndex)) + .orElse(UInt256.ZERO)); + } + + /** + * Returns the nonce of the given account at a specific block number. + * + * @param address The address of the account being queried. + * @param blockNumber The block number being queried. + * @return The nonce of the account. + */ + public UInt256 getAccountNonce(final Address address, final long blockNumber) { + throw new UnsupportedOperationException(); + } + + /** + * Returns the balance of the given account at a specific block number. + * + * @param address The address of the account being queried. + * @param blockNumber The block number being queried. + * @return The balance of the account in Wei. + */ + public Optional accountBalance(final Address address, final long blockNumber) { + if (!withinValidRange(blockNumber)) { + return Optional.empty(); + } + return Optional.of( + blockchain + .getBlockHeader(blockNumber) + .map(header -> worldStateArchive.get(header.getStateRoot())) + .map(worldState -> worldState.get(address)) + .map(Account::getBalance) + .orElse(Wei.ZERO)); + } + + /** + * Retrieves the code associated with the given account at a particular block number. + * + * @param address The account address being queried. + * @param blockNumber The height of the block to be checked. + * @return The code associated with this address. + */ + public Optional getCode(final Address address, final long blockNumber) { + if (!withinValidRange(blockNumber)) { + return Optional.empty(); + } + return Optional.of( + blockchain + .getBlockHeader(blockNumber) + .map(bh -> worldStateArchive.get(bh.getStateRoot())) + .map(ws -> ws.get(address)) + .map(Account::getCode) + .orElse(BytesValue.EMPTY)); + } + + /** + * Returns the number of transactions in the block at the given height. + * + * @param blockNumber The height of the block being queried. + * @return The number of transactions contained in the referenced block. + */ + public Optional getTransactionCount(final long blockNumber) { + if (!withinValidRange(blockNumber)) { + return Optional.empty(); + } + return Optional.of( + blockchain + .getBlockHashByNumber(blockNumber) + .flatMap(this::blockByHashWithTxHashes) + .map(BlockWithMetadata::getTransactions) + .map(List::size) + .orElse(-1)); + } + + /** + * Returns the number of transactions in the block with the given hash. + * + * @param blockHeaderHash The hash of the block being queried. + * @return The number of transactions contained in the referenced block. + */ + public Integer getTransactionCount(final Hash blockHeaderHash) { + return blockchain + .getBlockBody(blockHeaderHash) + .map(body -> body.getTransactions().size()) + .orElse(-1); + } + + /** + * Returns the number of transactions in the latest block. + * + * @return The number of transactions contained in the latest block. + */ + public Optional getTransactionCount() { + throw new UnsupportedOperationException(); + } + + /** + * Returns the number of transactions sent from the given address in the block at the given + * height. + * + * @param address The address whose sent transactions we want to count. + * @param blockNumber The height of the block being queried. + * @return The number of transactions sent from the given address. + */ + public long getTransactionCount(final Address address, final long blockNumber) { + return blockchain + .getBlockHeader(blockNumber) + .map(header -> worldStateArchive.get(header.getStateRoot())) + .map(worldState -> worldState.get(address)) + .map(Account::getNonce) + .orElse(0L); + } + + /** + * Returns the number of transactions sent from the given address in the latest block. + * + * @param address The address whose sent transactions we want to count. + * @return The number of transactions sent from the given address. + */ + public long getTransactionCount(final Address address) { + return getTransactionCount(address, headBlockNumber()); + } + + /** + * Returns the number of ommers in the block at the given height. + * + * @param blockNumber The height of the block being queried. + * @return The number of ommers in the referenced block. + */ + public Optional getOmmerCount(final long blockNumber) { + return blockchain.getBlockHashByNumber(blockNumber).flatMap(this::getOmmerCount); + } + + /** + * Returns the number of ommers in the block at the given height. + * + * @param blockHeaderHash The hash of the block being queried. + * @return The number of ommers in the referenced block. + */ + public Optional getOmmerCount(final Hash blockHeaderHash) { + return blockchain.getBlockBody(blockHeaderHash).map(b -> b.getOmmers().size()); + } + + /** + * Returns the number of ommers in the latest block. + * + * @return The number of ommers in the latest block. + */ + public Optional getOmmerCount() { + return getOmmerCount(blockchain.getChainHeadHash()); + } + + /** + * Returns the ommer at the given index for the referenced block. + * + * @param blockHeaderHash The hash of the block to be queried. + * @param index The index of the ommer in the blocks ommers list. + * @return The ommer at the given index belonging to the referenced block. + */ + public Optional getOmmer(final Hash blockHeaderHash, final int index) { + return blockchain.getBlockBody(blockHeaderHash).map(blockBody -> getOmmer(blockBody, index)); + } + + private BlockHeader getOmmer(final BlockBody blockBody, final int index) { + final List ommers = blockBody.getOmmers(); + if (ommers.size() > index) { + return ommers.get(index); + } else { + return null; + } + } + + /** + * Returns the ommer at the given index for the referenced block. + * + * @param blockNumber The block number identifying the block to be queried. + * @param index The index of the ommer in the blocks ommers list. + * @return The ommer at the given index belonging to the referenced block. + */ + public Optional getOmmer(final long blockNumber, final int index) { + return blockchain.getBlockHashByNumber(blockNumber).flatMap(hash -> getOmmer(hash, index)); + } + + /** + * Returns the ommer at the given index for the latest block. + * + * @param index The index of the ommer in the blocks ommers list. + * @return The ommer at the given index belonging to the latest block. + */ + public Optional getOmmer(final int index) { + return blockchain + .getBlockHashByNumber(blockchain.getChainHeadBlockNumber()) + .flatMap(hash -> getOmmer(hash, index)); + } + + /** + * Given a block hash, returns the associated block augmented with metadata. + * + * @param blockHeaderHash The hash of the target block's header. + * @return The referenced block. + */ + public Optional> blockByHash( + final Hash blockHeaderHash) { + return blockchain + .getBlockHeader(blockHeaderHash) + .flatMap( + header -> + blockchain + .getBlockBody(blockHeaderHash) + .flatMap( + body -> + blockchain + .getTotalDifficultyByHash(blockHeaderHash) + .map( + (td) -> { + final List txs = body.getTransactions(); + final List formattedTxs = + formatTransactions( + txs, header.getNumber(), blockHeaderHash); + final List ommers = + body.getOmmers() + .stream() + .map(BlockHeader::getHash) + .collect(Collectors.toList()); + final int size = new Block(header, body).calculateSize(); + return new BlockWithMetadata<>( + header, formattedTxs, ommers, td, size); + }))); + } + + /** + * Given a block number, returns the associated block augmented with metadata. + * + * @param number The height of the target block. + * @return The referenced block. + */ + public Optional> blockByNumber( + final long number) { + return blockchain.getBlockHashByNumber(number).flatMap(this::blockByHash); + } + + /** + * Returns the latest block augmented with metadata. + * + * @return The latest block. + */ + public Optional> latestBlock() { + return this.blockByHash(blockchain.getChainHeadHash()); + } + + /** + * Given a block hash, returns the associated block with metadata and a list of transaction hashes + * rather than full transactions. + * + * @param blockHeaderHash The hash of the target block's header. + * @return The referenced block. + */ + public Optional> blockByHashWithTxHashes( + final Hash blockHeaderHash) { + return blockchain + .getBlockHeader(blockHeaderHash) + .flatMap( + header -> + blockchain + .getBlockBody(blockHeaderHash) + .flatMap( + body -> + blockchain + .getTotalDifficultyByHash(blockHeaderHash) + .map( + (td) -> { + final List txs = + body.getTransactions() + .stream() + .map(Transaction::hash) + .collect(Collectors.toList()); + final List ommers = + body.getOmmers() + .stream() + .map(BlockHeader::getHash) + .collect(Collectors.toList()); + final int size = new Block(header, body).calculateSize(); + return new BlockWithMetadata<>(header, txs, ommers, td, size); + }))); + } + + /** + * Given a block number, returns the associated block with metadata and a list of transaction + * hashes rather than full transactions. + * + * @param blockNumber The height of the target block's header. + * @return The referenced block. + */ + public Optional> blockByNumberWithTxHashes(final long blockNumber) { + return blockchain.getBlockHashByNumber(blockNumber).flatMap(this::blockByHashWithTxHashes); + } + + /** + * Returns the latest block with metadata and a list of transaction hashes rather than full + * transactions. + * + * @return The latest block. + */ + public Optional> latestBlockWithTxHashes() { + return this.blockByHashWithTxHashes(blockchain.getChainHeadHash()); + } + + /** + * Given a transaction hash, returns the associated transaction. + * + * @param transactionHash The hash of the target transaction. + * @return The transaction associated with the given hash. + */ + public Optional transactionByHash(final Hash transactionHash) { + final Optional maybeLocation = + blockchain.getTransactionLocation(transactionHash); + if (!maybeLocation.isPresent()) { + return Optional.empty(); + } + final TransactionLocation loc = maybeLocation.get(); + final Hash blockHash = loc.getBlockHash(); + final BlockHeader header = blockchain.getBlockHeader(blockHash).get(); + final Transaction transaction = blockchain.getTransactionByHash(transactionHash).get(); + return Optional.of( + new TransactionWithMetadata( + transaction, header.getNumber(), blockHash, loc.getTransactionIndex())); + } + + /** + * Returns the transaction at the given index for the specified block. + * + * @param blockNumber The number of the block being queried. + * @param txIndex The index of the transaction to return. + * @return The transaction at the specified location. + */ + public TransactionWithMetadata transactionByBlockNumberAndIndex( + final long blockNumber, final int txIndex) { + checkArgument(txIndex >= 0); + final BlockHeader header = blockchain.getBlockHeader(blockNumber).get(); + return transactionByHeaderAndIndex(header, txIndex); + } + + /** + * Returns the transaction at the given index for the specified block. + * + * @param blockHeaderHash The hash of the block being queried. + * @param txIndex The index of the transaction to return. + * @return The transaction at the specified location. + */ + public TransactionWithMetadata transactionByBlockHashAndIndex( + final Hash blockHeaderHash, final int txIndex) { + checkArgument(txIndex >= 0); + final BlockHeader header = blockchain.getBlockHeader(blockHeaderHash).get(); + return transactionByHeaderAndIndex(header, txIndex); + } + + /** + * Helper method to return the transaction at the given index for the specified header, used by + * getTransactionByBlock*AndIndex methods. + * + * @param header The block header. + * @param txIndex The index of the transaction to return. + * @return The transaction at the specified location. + */ + private TransactionWithMetadata transactionByHeaderAndIndex( + final BlockHeader header, final int txIndex) { + final Hash blockHeaderHash = header.getHash(); + final BlockBody blockBody = blockchain.getBlockBody(blockHeaderHash).get(); + final List txs = blockBody.getTransactions(); + if (txIndex >= txs.size()) { + return null; + } + return new TransactionWithMetadata( + txs.get(txIndex), header.getNumber(), blockHeaderHash, txIndex); + } + + /** + * Returns the transaction at the given index for the latest block. + * + * @param txIndex The index of the transaction to return. + * @return The transaction at the specified location. + */ + public Optional transactionByIndex(final int txIndex) { + throw new UnsupportedOperationException(); + } + + /** + * Returns the transaction receipt associated with the given transaction hash. + * + * @param transactionHash The hash of the transaction that corresponds to the receipt to retrieve. + * @return The transaction receipt associated with the referenced transaction. + */ + public Optional transactionReceiptByTransactionHash( + final Hash transactionHash) { + final Optional maybeLocation = + blockchain.getTransactionLocation(transactionHash); + if (!maybeLocation.isPresent()) { + return Optional.empty(); + } + final TransactionLocation location = maybeLocation.get(); + final BlockBody blockBody = blockchain.getBlockBody(location.getBlockHash()).get(); + final Transaction transaction = blockBody.getTransactions().get(location.getTransactionIndex()); + + final Hash blockhash = location.getBlockHash(); + final BlockHeader header = blockchain.getBlockHeader(blockhash).get(); + final List transactionReceipts = blockchain.getTxReceipts(blockhash).get(); + final TransactionReceipt transactionReceipt = + transactionReceipts.get(location.getTransactionIndex()); + + long gasUsed = transactionReceipt.getCumulativeGasUsed(); + if (location.getTransactionIndex() > 0) { + gasUsed = + gasUsed + - transactionReceipts.get(location.getTransactionIndex() - 1).getCumulativeGasUsed(); + } + + return Optional.of( + TransactionReceiptWithMetadata.create( + transactionReceipt, + transaction, + transactionHash, + location.getTransactionIndex(), + gasUsed, + blockhash, + header.getNumber())); + } + + /** + * Retrieve logs from the range of blocks with optional filtering based on logger address and log + * topics. + * + * @param fromBlockNumber The block number defining the first block in the search range + * (inclusive). + * @param toBlockNumber The block number defining the last block in the search range (inclusive). + * @param query Constraints on required topics by topic index. For a given index if the set of + * topics is non-empty, the topic at this index must match one of the values in the set. + * @return The set of logs matching the given constraints. + */ + public List matchingLogs( + final long fromBlockNumber, final long toBlockNumber, final LogsQuery query) { + if (fromBlockNumber > toBlockNumber || toBlockNumber > headBlockNumber()) { + return Lists.newArrayList(); + } + List matchingLogs = Lists.newArrayList(); + for (long blockNumber = fromBlockNumber; blockNumber <= toBlockNumber; blockNumber++) { + final Hash blockhash = blockchain.getBlockHashByNumber(blockNumber).get(); + final boolean logHasBeenRemoved = !blockchain.blockIsOnCanonicalChain(blockhash); + final List receipts = blockchain.getTxReceipts(blockhash).get(); + final List transaction = + blockchain.getBlockBody(blockhash).get().getTransactions(); + matchingLogs = + generateLogWithMetadata( + receipts, + blockNumber, + query, + blockhash, + matchingLogs, + transaction, + logHasBeenRemoved); + } + return matchingLogs; + } + + public List matchingLogs(final Hash blockhash, final LogsQuery query) { + final List matchingLogs = Lists.newArrayList(); + final List receipts = blockchain.getTxReceipts(blockhash).get(); + final List transaction = + blockchain.getBlockBody(blockhash).get().getTransactions(); + final long number = blockchain.getBlockHeader(blockhash).get().getNumber(); + final boolean logHasBeenRemoved = !blockchain.blockIsOnCanonicalChain(blockhash); + return generateLogWithMetadata( + receipts, number, query, blockhash, matchingLogs, transaction, logHasBeenRemoved); + } + + private List generateLogWithMetadata( + final List receipts, + final long number, + final LogsQuery query, + final Hash blockhash, + final List matchingLogs, + final List transaction, + final boolean removed) { + for (int transactionIndex = 0; transactionIndex < receipts.size(); ++transactionIndex) { + final TransactionReceipt receipt = receipts.get(transactionIndex); + for (int logIndex = 0; logIndex < receipt.getLogs().size(); ++logIndex) { + if (query.matches(receipt.getLogs().get(logIndex))) { + final LogWithMetadata logWithMetaData = + LogWithMetadata.create( + logIndex, + number, + blockhash, + transaction.get(transactionIndex).hash(), + transactionIndex, + receipts.get(transactionIndex).getLogs().get(logIndex).getLogger(), + receipts.get(transactionIndex).getLogs().get(logIndex).getData(), + receipts.get(transactionIndex).getLogs().get(logIndex).getTopics(), + removed); + matchingLogs.add(logWithMetaData); + } + } + } + return matchingLogs; + } + + /** + * Returns the world state for the corresponding block number + * + * @param blockNumber the block number + * @return the world state at the block number + */ + public Optional worldState(final long blockNumber) { + final Optional header = blockchain.getBlockHeader(blockNumber); + return header.map(BlockHeader::getStateRoot).map(worldStateArchive::getMutable); + } + + private List formatTransactions( + final List txs, final long blockNumber, final Hash blockHash) { + final int count = txs.size(); + final List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(new TransactionWithMetadata(txs.get(i), blockNumber, blockHash, i)); + } + return result; + } + + private boolean withinValidRange(final long blockNumber) { + return blockNumber <= headBlockNumber() && blockNumber >= BlockHeader.GENESIS_BLOCK_NUMBER; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/LogWithMetadata.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/LogWithMetadata.java new file mode 100755 index 00000000000..cfa7296ceef --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/LogWithMetadata.java @@ -0,0 +1,119 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.queries; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogTopic; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import com.google.common.base.MoreObjects; + +public class LogWithMetadata { + + private final int logIndex; + private final long blockNumber; + private final Hash blockHash; + private final Hash transactionHash; + private final int transactionIndex; + private final Address address; + private final BytesValue data; + private final List topics; + private final boolean removed; + + private LogWithMetadata( + final int logIndex, + final long blockNumber, + final Hash blockHash, + final Hash transactionHash, + final int transactionIndex, + final Address address, + final BytesValue data, + final List topics, + final boolean removed) { + this.logIndex = logIndex; + this.blockNumber = blockNumber; + this.blockHash = blockHash; + this.transactionHash = transactionHash; + this.transactionIndex = transactionIndex; + this.address = address; + this.data = data; + this.topics = topics; + this.removed = removed; + } + + public static LogWithMetadata create( + final int logIndex, + final long blockNumber, + final Hash blockHash, + final Hash transactionHash, + final int transactionIndex, + final Address address, + final BytesValue data, + final List topics, + final boolean removed) { + return new LogWithMetadata( + logIndex, + blockNumber, + blockHash, + transactionHash, + transactionIndex, + address, + data, + topics, + removed); + } + + // The index of this log within the entire ordered list of logs associated with the block this log + // belongs to. + public int getLogIndex() { + return logIndex; + } + + public long getBlockNumber() { + return blockNumber; + } + + public Hash getBlockHash() { + return blockHash; + } + + public Hash getTransactionHash() { + return transactionHash; + } + + public int getTransactionIndex() { + return transactionIndex; + } + + public Address getAddress() { + return address; + } + + public BytesValue getData() { + return data; + } + + public List getTopics() { + return topics; + } + + public boolean isRemoved() { + return removed; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("logIndex", logIndex) + .add("blockNumber", blockNumber) + .add("blockHash", blockHash) + .add("transactionHash", transactionHash) + .add("transactionIndex", transactionIndex) + .add("address", address) + .add("data", data) + .add("topics", topics) + .add("removed", removed) + .toString(); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionReceiptWithMetadata.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionReceiptWithMetadata.java new file mode 100755 index 00000000000..c3bbde9bd9b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionReceiptWithMetadata.java @@ -0,0 +1,74 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.queries; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; + +public class TransactionReceiptWithMetadata { + private final TransactionReceipt receipt; + private final Hash transactionHash; + private final int transactionIndex; + private final long gasUsed; + private final long blockNumber; + private final Hash blockHash; + private final Transaction transaction; + + private TransactionReceiptWithMetadata( + final TransactionReceipt receipt, + final Transaction transaction, + final Hash transactionHash, + final int transactionIndex, + final long gasUsed, + final Hash blockHash, + final long blockNumber) { + this.receipt = receipt; + this.transactionHash = transactionHash; + this.transactionIndex = transactionIndex; + this.gasUsed = gasUsed; + this.blockHash = blockHash; + this.blockNumber = blockNumber; + this.transaction = transaction; + } + + public static TransactionReceiptWithMetadata create( + final TransactionReceipt receipt, + final Transaction transaction, + final Hash transactionHash, + final int transactionIndex, + final long gasUsed, + final Hash blockHash, + final long blockNumber) { + return new TransactionReceiptWithMetadata( + receipt, transaction, transactionHash, transactionIndex, gasUsed, blockHash, blockNumber); + } + + public TransactionReceipt getReceipt() { + return receipt; + } + + public Hash getTransactionHash() { + return transactionHash; + } + + public Transaction getTransaction() { + return transaction; + } + + public int getTransactionIndex() { + return transactionIndex; + } + + public Hash getBlockHash() { + return blockHash; + } + + public long getBlockNumber() { + return blockNumber; + } + + // The gas used for this particular transaction (as opposed to cumulativeGas which is included in + // the receipt itself) + public long getGasUsed() { + return gasUsed; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionWithMetadata.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionWithMetadata.java new file mode 100755 index 00000000000..d95d35cbbfc --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/TransactionWithMetadata.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.queries; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; + +public class TransactionWithMetadata { + + private final Transaction transaction; + private final long blockNumber; + private final Hash blockHash; + private final int transactionIndex; + + public TransactionWithMetadata( + final Transaction transaction, + final long blockNumber, + final Hash blockHash, + final int transactionIndex) { + this.transaction = transaction; + this.blockNumber = blockNumber; + this.blockHash = blockHash; + this.transactionIndex = transactionIndex; + } + + public Transaction getTransaction() { + return transaction; + } + + public long getBlockNumber() { + return blockNumber; + } + + public Hash getBlockHash() { + return blockHash; + } + + public int getTransactionIndex() { + return transactionIndex; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java new file mode 100755 index 00000000000..9fc8675254b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +public enum JsonRpcError { + // Standard errors + PARSE_ERROR(-32700, "Parse error"), + INVALID_REQUEST(-32600, "Invalid Request"), + METHOD_NOT_FOUND(-32601, "Method not found"), + INVALID_PARAMS(-32602, "Invalid params"), + INTERNAL_ERROR(-32603, "Internal error"), + + // Filter & Subscription Errors + FILTER_NOT_FOUND(-32000, "Filter not found"), + SUBSCRIPTION_NOT_FOUND(-32000, "Subscription not found"), + + // Transaction validation failures + NONCE_TOO_LOW(-32001, "Nonce too low"), + INVALID_TRANSACTION_SIGNATURE(-32002, "Invalid signature"), + INTRINSIC_GAS_EXCEEDS_LIMIT(-32003, "Intrinsic gas exceeds gas limit"), + TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE(-32004, "Upfront cost exceeds account balance"), + EXCEEDS_BLOCK_GAS_LIMIT(-32005, "Transaction gas limit exceeds block gas limit"), + INCORRECT_NONCE(-32006, "Incorrect nonce"), + + // Miner failures + COINBASE_NOT_SET(-32010, "Coinbase not set. Unable to start mining without a coinbase."), + + // Wallet errors + COINBASE_NOT_SPECIFIED(-32000, "Coinbase must be explicitly specified"); + + private final int code; + private final String message; + + JsonRpcError(final int code, final String message) { + this.code = code; + this.message = message; + } + + @JsonGetter("code") + public int getCode() { + return code; + } + + @JsonGetter("message") + public String getMessage() { + return message; + } + + @JsonCreator + public static JsonRpcError fromJson( + @JsonProperty("code") final int code, @JsonProperty("message") final String message) { + for (final JsonRpcError error : JsonRpcError.values()) { + if (error.getCode() == code) { + return error; + } + } + return null; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcErrorResponse.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcErrorResponse.java new file mode 100755 index 00000000000..96422caa1db --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcErrorResponse.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.google.common.base.Objects; + +@JsonPropertyOrder({"jsonrpc", "id", "error"}) +public class JsonRpcErrorResponse implements JsonRpcResponse { + + private final Object id; + private final JsonRpcError error; + + public JsonRpcErrorResponse(final Object id, final JsonRpcError error) { + this.id = id; + this.error = error; + } + + @JsonGetter("id") + public Object getId() { + return id; + } + + @JsonGetter("error") + public JsonRpcError getError() { + return error; + } + + @Override + @JsonIgnore + public JsonRpcResponseType getType() { + return JsonRpcResponseType.ERROR; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final JsonRpcErrorResponse that = (JsonRpcErrorResponse) o; + return Objects.equal(id, that.id) && error == that.error; + } + + @Override + public int hashCode() { + return Objects.hashCode(id, error); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcNoResponse.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcNoResponse.java new file mode 100755 index 00000000000..c90e67d8e60 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcNoResponse.java @@ -0,0 +1,9 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.response; + +public class JsonRpcNoResponse implements JsonRpcResponse { + + @Override + public JsonRpcResponseType getType() { + return JsonRpcResponseType.NONE; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponse.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponse.java new file mode 100755 index 00000000000..5fc0b7ec0b3 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponse.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public interface JsonRpcResponse { + + @JsonGetter("jsonrpc") + default String getVersion() { + return "2.0"; + } + + JsonRpcResponseType getType(); +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponseType.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponseType.java new file mode 100755 index 00000000000..b96095e3f2f --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcResponseType.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.response; + +/** Various types of responses that the JSON-RPC component may produce. */ +public enum JsonRpcResponseType { + NONE, + SUCCESS, + ERROR +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcSuccessResponse.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcSuccessResponse.java new file mode 100755 index 00000000000..6ba174a3509 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcSuccessResponse.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.google.common.base.Objects; + +@JsonPropertyOrder({"jsonrpc", "id", "result"}) +public class JsonRpcSuccessResponse implements JsonRpcResponse { + + private final Object id; + private final Object result; + + public JsonRpcSuccessResponse(final Object id, final Object result) { + this.id = id; + this.result = result; + } + + @JsonGetter("id") + public Object getId() { + return id; + } + + @JsonGetter("result") + public Object getResult() { + return result; + } + + @Override + @JsonIgnore + public JsonRpcResponseType getType() { + return JsonRpcResponseType.SUCCESS; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final JsonRpcSuccessResponse that = (JsonRpcSuccessResponse) o; + return Objects.equal(id, that.id) && Objects.equal(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hashCode(id, result); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResult.java new file mode 100755 index 00000000000..4ba0f32aca8 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResult.java @@ -0,0 +1,176 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; + +@JsonPropertyOrder({ + "number", + "hash", + "parentHash", + "nonce", + "sha3Uncles", + "logsBloom", + "transactionsRoot", + "stateRoot", + "receiptsRoot", + "miner", + "difficulty", + "totalDifficulty", + "extraData", + "size", + "gasLimit", + "gasUsed", + "timestamp", + "uncles", + "transactions" +}) +public class BlockResult implements JsonRpcResult { + + private final String number; + private final String hash; + private final String parentHash; + private final String nonce; + private final String sha3Uncles; + private final String logsBloom; + private final String transactionsRoot; + private final String stateRoot; + private final String receiptsRoot; + private final String miner; + private final String difficulty; + private final String totalDifficulty; + private final String extraData; + private final String size; + private final String gasLimit; + private final String gasUsed; + private final String timestamp; + private final List transactions; + private final List ommers; + + public BlockResult( + final BlockHeader header, + final List transactions, + final List ommers, + final UInt256 totalDifficulty, + final int size) { + this.number = Quantity.create(header.getNumber()); + this.hash = header.getHash().toString(); + this.parentHash = header.getParentHash().toString(); + this.nonce = Quantity.longToPaddedHex(header.getNonce(), 8); + this.sha3Uncles = header.getOmmersHash().toString(); + this.logsBloom = header.getLogsBloom().getBytes().toString(); + this.transactionsRoot = header.getTransactionsRoot().toString(); + this.stateRoot = header.getStateRoot().toString(); + this.receiptsRoot = header.getReceiptsRoot().toString(); + this.miner = header.getCoinbase().toString(); + this.difficulty = Quantity.create(header.getDifficulty()); + this.totalDifficulty = Quantity.create(totalDifficulty); + this.extraData = header.getExtraData().toString(); + this.size = Quantity.create(size); + this.gasLimit = Quantity.create(header.getGasLimit()); + this.gasUsed = Quantity.create(header.getGasUsed()); + this.timestamp = Quantity.create(header.getTimestamp()); + this.ommers = ommers; + this.transactions = transactions; + } + + @JsonGetter(value = "number") + public String getNumber() { + return number; + } + + @JsonGetter(value = "hash") + public String getHash() { + return hash; + } + + @JsonGetter(value = "parentHash") + public String getParentHash() { + return parentHash; + } + + @JsonGetter(value = "nonce") + public String getNonce() { + return nonce; + } + + @JsonGetter(value = "sha3Uncles") + public String getSha3Uncles() { + return sha3Uncles; + } + + @JsonGetter(value = "logsBloom") + public String getLogsBloom() { + return logsBloom; + } + + @JsonGetter(value = "transactionsRoot") + public String getTransactionsRoot() { + return transactionsRoot; + } + + @JsonGetter(value = "stateRoot") + public String getStateRoot() { + return stateRoot; + } + + @JsonGetter(value = "receiptsRoot") + public String getReceiptsRoot() { + return receiptsRoot; + } + + @JsonGetter(value = "miner") + public String getMiner() { + return miner; + } + + @JsonGetter(value = "difficulty") + public String getDifficulty() { + return difficulty; + } + + @JsonGetter(value = "totalDifficulty") + public String getTotalDifficulty() { + return totalDifficulty; + } + + @JsonGetter(value = "extraData") + public String getExtraData() { + return extraData; + } + + @JsonGetter(value = "size") + public String getSize() { + return size; + } + + @JsonGetter(value = "gasLimit") + public String getGasLimit() { + return gasLimit; + } + + @JsonGetter(value = "gasUsed") + public String getGasUsed() { + return gasUsed; + } + + @JsonGetter(value = "timestamp") + public String getTimestamp() { + return timestamp; + } + + @JsonGetter(value = "uncles") + public List getOmmers() { + return ommers; + } + + @JsonGetter(value = "transactions") + public List getTransactions() { + return transactions; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResultFactory.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResultFactory.java new file mode 100755 index 00000000000..18868689e4b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/BlockResultFactory.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; + +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; + +public class BlockResultFactory { + + public BlockResult transactionComplete( + final BlockWithMetadata blockWithMetadata) { + final List txs = + blockWithMetadata + .getTransactions() + .stream() + .map(TransactionCompleteResult::new) + .collect(Collectors.toList()); + final List ommers = + blockWithMetadata + .getOmmers() + .stream() + .map(Hash::toString) + .map(TextNode::new) + .collect(Collectors.toList()); + return new BlockResult( + blockWithMetadata.getHeader(), + txs, + ommers, + blockWithMetadata.getTotalDifficulty(), + blockWithMetadata.getSize()); + } + + public BlockResult transactionHash(final BlockWithMetadata blockWithMetadata) { + final List txs = + blockWithMetadata + .getTransactions() + .stream() + .map(Hash::toString) + .map(TransactionHashResult::new) + .collect(Collectors.toList()); + final List ommers = + blockWithMetadata + .getOmmers() + .stream() + .map(Hash::toString) + .map(TextNode::new) + .collect(Collectors.toList()); + return new BlockResult( + blockWithMetadata.getHeader(), + txs, + ommers, + blockWithMetadata.getTotalDifficulty(), + blockWithMetadata.getSize()); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugStorageRangeAtResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugStorageRangeAtResult.java new file mode 100755 index 00000000000..b7f588cf1a0 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugStorageRangeAtResult.java @@ -0,0 +1,74 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.google.common.base.MoreObjects; + +public class DebugStorageRangeAtResult implements JsonRpcResult { + + private final Map storage = new HashMap<>(); + private final String nextKey; + + public DebugStorageRangeAtResult(final Map entries, final Bytes32 nextKey) { + entries.forEach((keyHash, value) -> storage.put(keyHash.toString(), new StorageEntry(value))); + this.nextKey = nextKey != null ? nextKey.toString() : null; + } + + @JsonGetter(value = "storage") + public Map getStorage() { + return storage; + } + + @JsonGetter(value = "nextKey") + public String getNextKey() { + return nextKey; + } + + @JsonPropertyOrder(value = {"key", "value"}) + public static class StorageEntry { + private final String value; + + public StorageEntry(final UInt256 value) { + this.value = value.toHexString(); + } + + @JsonGetter(value = "key") + public String getKey() { + return null; + } + + @JsonGetter(value = "value") + public String getValue() { + return value; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("value", value).toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final StorageEntry that = (StorageEntry) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugTraceTransactionResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugTraceTransactionResult.java new file mode 100755 index 00000000000..be72d0d9917 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/DebugTraceTransactionResult.java @@ -0,0 +1,57 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransactionTrace; + +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({"gas", "failed", "returnValue", "structLogs"}) +public class DebugTraceTransactionResult { + + private final List structLogs; + private final String returnValue; + private final long gas; + private final boolean failed; + + public DebugTraceTransactionResult(final TransactionTrace transactionTrace) { + gas = transactionTrace.getGas(); + returnValue = transactionTrace.getResult().getOutput().toString().substring(2); + structLogs = + transactionTrace + .getTraceFrames() + .stream() + .map(DebugTraceTransactionResult::createStructLog) + .collect(Collectors.toList()); + failed = !transactionTrace.getResult().isSuccessful(); + } + + private static StructLog createStructLog(final TraceFrame frame) { + return frame.getExceptionalHaltReasons().isEmpty() + ? new StructLog(frame) + : new StructLogWithError(frame); + } + + @JsonGetter(value = "structLogs") + public List getStructLogs() { + return structLogs; + } + + @JsonGetter(value = "returnValue") + public String getReturnValue() { + return returnValue; + } + + @JsonGetter(value = "gas") + public long getGas() { + return gas; + } + + @JsonGetter(value = "failed") + public boolean failed() { + return failed; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/JsonRpcResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/JsonRpcResult.java new file mode 100755 index 00000000000..a6c2feaba69 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/JsonRpcResult.java @@ -0,0 +1,7 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +@JsonInclude(Include.NON_ABSENT) +public interface JsonRpcResult {} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogResult.java new file mode 100755 index 00000000000..5b473b4fbe8 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogResult.java @@ -0,0 +1,96 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.LogTopic; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** A single log result. */ +@JsonPropertyOrder({ + "logIndex", + "removed", + "blockNumber", + "blockHash", + "transactionHash", + "transactionIndex", + "address", + "data", + "topics" +}) +public class LogResult implements JsonRpcResult { + + private final String logIndex; + private final String blockNumber; + private final String blockHash; + private final String transactionHash; + private final String transactionIndex; + private final String address; + private final String data; + private final List topics; + private final boolean removed; + + public LogResult(final LogWithMetadata logWithMetadata) { + this.logIndex = Quantity.create(logWithMetadata.getLogIndex()); + this.blockNumber = Quantity.create(logWithMetadata.getBlockNumber()); + this.blockHash = logWithMetadata.getBlockHash().toString(); + this.transactionHash = logWithMetadata.getTransactionHash().toString(); + this.transactionIndex = Quantity.create(logWithMetadata.getTransactionIndex()); + this.address = logWithMetadata.getAddress().toString(); + this.data = logWithMetadata.getData().toString(); + this.topics = new ArrayList<>(logWithMetadata.getTopics().size()); + this.removed = logWithMetadata.isRemoved(); + + for (final LogTopic topic : logWithMetadata.getTopics()) { + topics.add(topic.toString()); + } + } + + @JsonGetter(value = "logIndex") + public String getLogIndex() { + return logIndex; + } + + @JsonGetter(value = "blockNumber") + public String getBlockNumber() { + return blockNumber; + } + + @JsonGetter(value = "blockHash") + public String getBlockHash() { + return blockHash; + } + + @JsonGetter(value = "transactionHash") + public String getTransactionHash() { + return transactionHash; + } + + @JsonGetter(value = "transactionIndex") + public String getTransactionIndex() { + return transactionIndex; + } + + @JsonGetter(value = "address") + public String getAddress() { + return address; + } + + @JsonGetter(value = "data") + public String getData() { + return data; + } + + @JsonGetter(value = "topics") + public List getTopics() { + return topics; + } + + @JsonGetter(value = "removed") + public boolean isRemoved() { + return removed; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogsResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogsResult.java new file mode 100755 index 00000000000..344983341de --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/LogsResult.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** The result set from querying the logs from one or more blocks. */ +public class LogsResult { + + private final List results; + + public LogsResult(final List logs) { + results = new ArrayList<>(logs.size()); + + for (final LogWithMetadata log : logs) { + results.add(new LogResult(log)); + } + } + + @JsonValue + public List getResults() { + return results; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResult.java new file mode 100755 index 00000000000..096cdaba911 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResult.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import java.net.SocketAddress; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({"localAddress", "remoteAddress"}) +public class NetworkResult { + + private final String localAddress; + private final String remoteAddress; + + public NetworkResult(final SocketAddress localAddress, final SocketAddress remoteAddress) { + this.localAddress = removeTrailingSlash(localAddress.toString()); + this.remoteAddress = removeTrailingSlash(remoteAddress.toString()); + } + + @JsonGetter(value = "localAddress") + public String getLocalAddress() { + return localAddress; + } + + @JsonGetter(value = "remoteAddress") + public String getRemoteAddress() { + return remoteAddress; + } + + private String removeTrailingSlash(final String address) { + if (address != null && address.startsWith("/")) { + return address.substring(1); + } else { + return address; + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/PeerResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/PeerResult.java new file mode 100755 index 00000000000..86494631982 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/PeerResult.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; + +@JsonPropertyOrder({"version", "name", "caps", "network", "port", "id"}) +public class PeerResult { + + private final String version; + private final String name; + private final List caps; + private final NetworkResult network; + private final String port; + private final String id; + + public PeerResult(final PeerConnection peer) { + this.version = Quantity.create(peer.getPeer().getVersion()); + this.name = peer.getPeer().getClientId(); + this.caps = + peer.getPeer() + .getCapabilities() + .stream() + .map(Capability::toString) + .map(TextNode::new) + .collect(Collectors.toList()); + this.network = new NetworkResult(peer.getLocalAddress(), peer.getRemoteAddress()); + this.port = Quantity.create(peer.getPeer().getPort()); + this.id = peer.getPeer().getNodeId().toString(); + } + + @JsonGetter(value = "version") + public String getVersion() { + return version; + } + + @JsonGetter(value = "name") + public String getName() { + return name; + } + + @JsonGetter(value = "caps") + public List getCaps() { + return caps; + } + + @JsonGetter(value = "network") + public NetworkResult getNetwork() { + return network; + } + + @JsonGetter(value = "port") + public String getPort() { + return port; + } + + @JsonGetter(value = "id") + public String getId() { + return id; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/Quantity.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/Quantity.java new file mode 100755 index 00000000000..6cf55c83467 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/Quantity.java @@ -0,0 +1,77 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Value; + +import java.math.BigInteger; +import java.util.Objects; + +import com.google.common.base.Strings; + +/** + * Utility for formatting "quantity" fields and results to be returned. Quantity fields are + * represented as minimal length hex strings with no zero-padding. There is one exception to this + * rule: quantities equal to zero are represented by the hex string "0x0". + */ +public class Quantity { + + private static final String HEX_PREFIX = "0x"; + private static final String HEX_ZERO = "0x0"; + + private Quantity() {} + + public static String create(final UInt256Value value) { + return uint256ToHex(value.asUInt256()); + } + + public static String create(final UInt256 value) { + return uint256ToHex(value); + } + + public static String create(final int value) { + return uint256ToHex(UInt256.of(value)); + } + + public static String create(final long value) { + return uint256ToHex(UInt256.of(value)); + } + + public static String format(final BigInteger input) { + return formatMinimalValue(input.toString(16)); + } + + public static String format(final byte value) { + return formatMinimalValue(Integer.toHexString(value)); + } + + public static String format(final int value) { + return formatMinimalValue(Integer.toHexString(value)); + } + + /** + * Fixed-length bytes sequences and should be returned as hex strings zero-padded to the expected + * length. + * + * @param val the value to encode in the string + * @param byteLength the number of bytes to be represented in the output string. + * @return A zero-padded string containing byteLength * 2 characters, not including the 0x prefix + */ + public static String longToPaddedHex(final long val, final int byteLength) { + final String formatted = Long.toHexString(val); + final String zeroPadding = Strings.repeat("0", byteLength * 2 - formatted.length()); + return String.format("%s%s%s", HEX_PREFIX, zeroPadding, formatted); + } + + private static String uint256ToHex(final UInt256 value) { + return value == null ? null : formatMinimalValue(value.toShortHexString()); + } + + private static String formatMinimalValue(final String hexValue) { + final String prefixedHexString = prefixHexNotation(hexValue); + return Objects.equals(prefixedHexString, HEX_PREFIX) ? HEX_ZERO : prefixedHexString; + } + + private static String prefixHexNotation(final String hexValue) { + return hexValue.startsWith(HEX_PREFIX) ? hexValue : HEX_PREFIX + hexValue; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLog.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLog.java new file mode 100755 index 00000000000..c2ee0d22fcf --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLog.java @@ -0,0 +1,121 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.util.bytes.Bytes32s; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({"pc", "op", "gas", "gasCost", "depth", "stack", "memory", "storage"}) +public class StructLog { + + private final int depth; + private final long gas; + private final long gasCost; + private final String[] memory; + private final String op; + private final int pc; + private final String[] stack; + private final Object storage; + + public StructLog(final TraceFrame traceFrame) { + depth = traceFrame.getDepth() + 1; + gas = traceFrame.getGasRemaining().toLong(); + gasCost = traceFrame.getGasCost().map(Gas::toLong).orElse(0L); + memory = + traceFrame + .getMemory() + .map(a -> Arrays.stream(a).map(Bytes32s::unprefixedHexString).toArray(String[]::new)) + .orElse(null); + op = traceFrame.getOpcode(); + pc = traceFrame.getPc(); + stack = + traceFrame + .getStack() + .map(a -> Arrays.stream(a).map(Bytes32s::unprefixedHexString).toArray(String[]::new)) + .orElse(null); + storage = traceFrame.getStorage().map(StructLog::formatStorage).orElse(null); + } + + private static Map formatStorage(final Map storage) { + final Map formattedStorage = new TreeMap<>(); + storage.forEach( + (key, value) -> + formattedStorage.put(key.toUnprefixedHexString(), value.toUnprefixedHexString())); + return formattedStorage; + } + + @JsonGetter("depth") + public int depth() { + return depth; + } + + @JsonGetter("gas") + public long gas() { + return gas; + } + + @JsonGetter("gasCost") + public long gasCost() { + return gasCost; + } + + @JsonGetter("memory") + public String[] memory() { + return memory; + } + + @JsonGetter("op") + public String op() { + return op; + } + + @JsonGetter("pc") + public int pc() { + return pc; + } + + @JsonGetter("stack") + public String[] stack() { + return stack; + } + + @JsonGetter("storage") + public Object storage() { + return storage; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final StructLog structLog = (StructLog) o; + return depth == structLog.depth + && gas == structLog.gas + && gasCost == structLog.gasCost + && pc == structLog.pc + && Arrays.equals(memory, structLog.memory) + && Objects.equals(op, structLog.op) + && Arrays.equals(stack, structLog.stack) + && Objects.equals(storage, structLog.storage); + } + + @Override + public int hashCode() { + int result = Objects.hash(depth, gas, gasCost, op, pc, storage); + result = 31 * result + Arrays.hashCode(memory); + result = 31 * result + Arrays.hashCode(stack); + return result; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLogWithError.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLogWithError.java new file mode 100755 index 00000000000..4cdde8f9afe --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/StructLogWithError.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class StructLogWithError extends StructLog { + + private final String[] error; + + public StructLogWithError(final TraceFrame traceFrame) { + super(traceFrame); + error = + traceFrame.getExceptionalHaltReasons().isEmpty() + ? null + : traceFrame + .getExceptionalHaltReasons() + .stream() + .map(ExceptionalHaltReason::name) + .toArray(String[]::new); + } + + @JsonGetter("error") + public String[] getError() { + return error; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/SyncingResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/SyncingResult.java new file mode 100755 index 00000000000..35475f8ca2c --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/SyncingResult.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.SyncStatus; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({"startingBlock", "currentBlock", "highestBlock"}) +public class SyncingResult implements JsonRpcResult { + + private final String startingBlock; + private final String currentBlock; + private final String highestBlock; + + public SyncingResult(final SyncStatus syncStatus) { + + this.startingBlock = Quantity.create(syncStatus.getStartingBlock()); + this.currentBlock = Quantity.create(syncStatus.getCurrentBlock()); + this.highestBlock = Quantity.create(syncStatus.getHighestBlock()); + } + + @JsonGetter(value = "startingBlock") + public String getStartingBlock() { + return startingBlock; + } + + @JsonGetter(value = "currentBlock") + public String getCurrentBlock() { + return currentBlock; + } + + @JsonGetter(value = "highestBlock") + public String getHighestBlock() { + return highestBlock; + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof SyncingResult)) { + return false; + } + final SyncingResult that = (SyncingResult) other; + return this.startingBlock.equals(that.startingBlock) + && this.currentBlock.equals(that.currentBlock) + && this.highestBlock.equals(that.highestBlock); + } + + @Override + public int hashCode() { + return Objects.hash(startingBlock, currentBlock, highestBlock); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionCompleteResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionCompleteResult.java new file mode 100755 index 00000000000..59844c7cea7 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionCompleteResult.java @@ -0,0 +1,130 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ + "blockHash", + "blockNumber", + "from", + "gas", + "gasPrice", + "hash", + "input", + "nonce", + "to", + "transactionIndex", + "value", + "v", + "r", + "s" +}) +public class TransactionCompleteResult implements TransactionResult { + + private final String blockHash; + private final String blockNumber; + private final String from; + private final String gas; + private final String gasPrice; + private final String hash; + private final String input; + private final String nonce; + private final String to; + private final String transactionIndex; + private final String value; + private final String v; + private final String r; + private final String s; + + public TransactionCompleteResult(final TransactionWithMetadata tx) { + final Transaction transaction = tx.getTransaction(); + this.blockHash = tx.getBlockHash().toString(); + this.blockNumber = Quantity.create(tx.getBlockNumber()); + this.from = transaction.getSender().toString(); + this.gas = Quantity.create(transaction.getGasLimit()); + this.gasPrice = Quantity.create(transaction.getGasPrice()); + this.hash = transaction.hash().toString(); + this.input = transaction.getPayload().toString(); + this.nonce = Quantity.create(transaction.getNonce()); + this.to = transaction.getTo().map(BytesValue::toString).orElse(null); + this.transactionIndex = Quantity.create(tx.getTransactionIndex()); + this.value = Quantity.create(transaction.getValue()); + this.v = Quantity.format(transaction.getV()); + this.r = Quantity.format(transaction.getR()); + this.s = Quantity.format(transaction.getS()); + } + + @JsonGetter(value = "blockHash") + public String getBlockHash() { + return blockHash; + } + + @JsonGetter(value = "blockNumber") + public String getBlockNumber() { + return blockNumber; + } + + @JsonGetter(value = "from") + public String getFrom() { + return from; + } + + @JsonGetter(value = "gas") + public String getGas() { + return gas; + } + + @JsonGetter(value = "gasPrice") + public String getGasPrice() { + return gasPrice; + } + + @JsonGetter(value = "hash") + public String getHash() { + return hash; + } + + @JsonGetter(value = "input") + public String getInput() { + return input; + } + + @JsonGetter(value = "nonce") + public String getNonce() { + return nonce; + } + + @JsonGetter(value = "to") + public String getTo() { + return to; + } + + @JsonGetter(value = "transactionIndex") + public String getTransactionIndex() { + return transactionIndex; + } + + @JsonGetter(value = "value") + public String getValue() { + return value; + } + + @JsonGetter(value = "v") + public String getV() { + return v; + } + + @JsonGetter(value = "r") + public String getR() { + return r; + } + + @JsonGetter(value = "s") + public String getS() { + return s; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionHashResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionHashResult.java new file mode 100755 index 00000000000..6fc1a7de97a --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionHashResult.java @@ -0,0 +1,17 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import com.fasterxml.jackson.annotation.JsonValue; + +public class TransactionHashResult implements TransactionResult { + + private final String hash; + + public TransactionHashResult(final String hash) { + this.hash = hash; + } + + @JsonValue + public String getHash() { + return hash; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionPendingResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionPendingResult.java new file mode 100755 index 00000000000..66963ae0e54 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionPendingResult.java @@ -0,0 +1,107 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ + "blockHash", + "blockNumber", + "from", + "gas", + "gasPrice", + "hash", + "input", + "nonce", + "to", + "transactionIndex", + "value", + "v", + "r", + "s" +}) +public class TransactionPendingResult implements TransactionResult { + + private final String from; + private final String gas; + private final String gasPrice; + private final String hash; + private final String input; + private final String nonce; + private final String to; + private final String value; + private final String v; + private final String r; + private final String s; + + public TransactionPendingResult(final Transaction transaction) { + this.from = transaction.getSender().toString(); + this.gas = Quantity.create(transaction.getGasLimit()); + this.gasPrice = Quantity.create(transaction.getGasPrice()); + this.hash = transaction.hash().toString(); + this.input = transaction.getPayload().toString(); + this.nonce = Quantity.create(transaction.getNonce()); + this.to = transaction.getTo().map(BytesValue::toString).orElse(null); + this.value = Quantity.create(transaction.getValue()); + this.v = Quantity.format(transaction.getV()); + this.r = Quantity.format(transaction.getR()); + this.s = Quantity.format(transaction.getS()); + } + + @JsonGetter(value = "from") + public String getFrom() { + return from; + } + + @JsonGetter(value = "gas") + public String getGas() { + return gas; + } + + @JsonGetter(value = "gasPrice") + public String getGasPrice() { + return gasPrice; + } + + @JsonGetter(value = "hash") + public String getHash() { + return hash; + } + + @JsonGetter(value = "input") + public String getInput() { + return input; + } + + @JsonGetter(value = "nonce") + public String getNonce() { + return nonce; + } + + @JsonGetter(value = "to") + public String getTo() { + return to; + } + + @JsonGetter(value = "value") + public String getValue() { + return value; + } + + @JsonGetter(value = "v") + public String getV() { + return v; + } + + @JsonGetter(value = "r") + public String getR() { + return r; + } + + @JsonGetter(value = "s") + public String getS() { + return s; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptLogResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptLogResult.java new file mode 100755 index 00000000000..7fd55a11850 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptLogResult.java @@ -0,0 +1,105 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.LogTopic; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ + "address", + "topics", + "data", + "blockNumber", + "transactionHash", + "transactionIndex", + "blockHash", + "logIndex", + "removed" +}) +public class TransactionReceiptLogResult { + + private final String address; + private final List topics; + private final String data; + private final String blockNumber; + private final String transactionHash; + private final String transactionIndex; + private final String blockHash; + private final String logIndex; + private final boolean removed; + + public TransactionReceiptLogResult( + final Log log, + final long blockNumber, + final Hash transactionHash, + final Hash blockHash, + final int transactionIndex, + final int logIndex) { + this.address = log.getLogger().toString(); + this.topics = new ArrayList<>(log.getTopics().size()); + + for (final LogTopic topic : log.getTopics()) { + topics.add(topic.toString()); + } + + this.data = log.getData().toString(); + this.blockNumber = Quantity.create(blockNumber); + this.transactionHash = transactionHash.toString(); + this.transactionIndex = Quantity.create(transactionIndex); + this.blockHash = blockHash.toString(); + this.logIndex = Quantity.create(logIndex); + + // TODO: Handle chain reorgs, i.e. return `true` if log is removed + this.removed = false; + } + + @JsonGetter(value = "address") + public String getAddress() { + return address; + } + + @JsonGetter(value = "topics") + public List getTopics() { + return topics; + } + + @JsonGetter(value = "data") + public String getData() { + return data; + } + + @JsonGetter(value = "blockNumber") + public String getBlockNumber() { + return blockNumber; + } + + @JsonGetter(value = "transactionHash") + public String getTransactionHash() { + return transactionHash; + } + + @JsonGetter(value = "transactionIndex") + public String getTransactionIndex() { + return transactionIndex; + } + + @JsonGetter(value = "blockHash") + public String getBlockHash() { + return blockHash; + } + + @JsonGetter(value = "logIndex") + public String getLogIndex() { + return logIndex; + } + + @JsonGetter(value = "removed") + public boolean isRemoved() { + return removed; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptResult.java new file mode 100755 index 00000000000..eb766fa0161 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptResult.java @@ -0,0 +1,143 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionReceiptWithMetadata; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ + "blockHash", + "blockNumber", + "contractAddress", + "cumulativeGasUsed", + "from", + "gasUsed", + "logs", + "logsBloom", + "root", + "status", + "to", + "transactionHash", + "transactionIndex" +}) +public abstract class TransactionReceiptResult { + + private final String blockHash; + private final String blockNumber; + private final String contractAddress; + private final String cumulativeGasUsed; + private final String from; + private final String gasUsed; + private final List logs; + private final String logsBloom; + private final String to; + private final String transactionHash; + private final String transactionIndex; + + protected final TransactionReceipt receipt; + + public TransactionReceiptResult(final TransactionReceiptWithMetadata receiptWithMetadata) { + + receipt = receiptWithMetadata.getReceipt(); + + this.blockHash = receiptWithMetadata.getBlockHash().toString(); + this.blockNumber = Quantity.create(receiptWithMetadata.getBlockNumber()); + this.contractAddress = + receiptWithMetadata.getTransaction().contractAddress().map(Address::toString).orElse(null); + this.cumulativeGasUsed = Quantity.create(receipt.getCumulativeGasUsed()); + this.from = receiptWithMetadata.getTransaction().getSender().toString(); + this.gasUsed = Quantity.create(receiptWithMetadata.getGasUsed()); + this.logs = + logReceipts( + receipt.getLogs(), + receiptWithMetadata.getBlockNumber(), + receiptWithMetadata.getTransaction().hash(), + receiptWithMetadata.getBlockHash(), + receiptWithMetadata.getTransactionIndex()); + this.logsBloom = receipt.getBloomFilter().toString(); + this.to = receiptWithMetadata.getTransaction().getTo().map(BytesValue::toString).orElse(null); + this.transactionHash = receiptWithMetadata.getTransaction().hash().toString(); + this.transactionIndex = Quantity.create(receiptWithMetadata.getTransactionIndex()); + } + + @JsonGetter(value = "blockHash") + public String getBlockHash() { + return blockHash; + } + + @JsonGetter(value = "blockNumber") + public String getBlockNumber() { + return blockNumber; + } + + @JsonGetter(value = "contractAddress") + public String getContractAddress() { + return contractAddress; + } + + @JsonGetter(value = "cumulativeGasUsed") + public String getCumulativeGasUsed() { + return cumulativeGasUsed; + } + + @JsonGetter(value = "from") + public String getFrom() { + return from; + } + + @JsonGetter(value = "gasUsed") + public String getGasUsed() { + return gasUsed; + } + + @JsonGetter(value = "logs") + public List getLogs() { + return logs; + } + + @JsonGetter(value = "logsBloom") + public String getLogsBloom() { + return logsBloom; + } + + @JsonGetter(value = "to") + public String getTo() { + return to; + } + + @JsonGetter(value = "transactionHash") + public String getTransactionHash() { + return transactionHash; + } + + @JsonGetter(value = "transactionIndex") + public String getTransactionIndex() { + return transactionIndex; + } + + private List logReceipts( + final List logs, + final long blockNumber, + final Hash transactionHash, + final Hash blockHash, + final int transactionIndex) { + final List logResults = new ArrayList<>(logs.size()); + + for (int i = 0; i < logs.size(); i++) { + final Log log = logs.get(i); + logResults.add( + new TransactionReceiptLogResult( + log, blockNumber, transactionHash, blockHash, transactionIndex, i)); + } + + return logResults; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptRootResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptRootResult.java new file mode 100755 index 00000000000..5114513356e --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptRootResult.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionReceiptWithMetadata; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class TransactionReceiptRootResult extends TransactionReceiptResult { + + private final String root; + + public TransactionReceiptRootResult(final TransactionReceiptWithMetadata receiptWithMetadata) { + super(receiptWithMetadata); + root = receipt.getStateRoot().toString(); + } + + @JsonGetter(value = "root") + public String getRoot() { + return root; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptStatusResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptStatusResult.java new file mode 100755 index 00000000000..98322b8676b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionReceiptStatusResult.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionReceiptWithMetadata; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class TransactionReceiptStatusResult extends TransactionReceiptResult { + + private final String status; + + public TransactionReceiptStatusResult(final TransactionReceiptWithMetadata receiptWithMetadata) { + super(receiptWithMetadata); + status = Quantity.create(receipt.getStatus()); + } + + @JsonGetter(value = "status") + public String getStatus() { + return status; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionResult.java new file mode 100755 index 00000000000..9d6061395d9 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/TransactionResult.java @@ -0,0 +1,3 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +public interface TransactionResult {} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/UncleBlockResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/UncleBlockResult.java new file mode 100755 index 00000000000..118fcbc9d64 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/UncleBlockResult.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Collections; + +public class UncleBlockResult { + + /** + * Returns an uncle block, which doesn't include transactions or ommers. + * + * @param header The uncle block header. + * @return A BlockResult, generated from the header and empty body. + */ + public static BlockResult build(final BlockHeader header) { + final BlockBody body = new BlockBody(Collections.emptyList(), Collections.emptyList()); + final int size = new Block(header, body).calculateSize(); + return new BlockResult( + header, Collections.emptyList(), Collections.emptyList(), UInt256.ZERO, size); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java new file mode 100755 index 00000000000..c557b28b090 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfiguration.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket; + +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; + +import java.util.Arrays; +import java.util.Collection; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +public class WebSocketConfiguration { + + public static final String DEFAULT_WEBSOCKET_HOST = "127.0.0.1"; + public static final int DEFAULT_WEBSOCKET_PORT = 8546; + public static final Collection DEFAULT_WEBSOCKET_APIS = + Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + + private boolean enabled; + private int port; + private String host; + private Collection rpcApis; + + public static WebSocketConfiguration createDefault() { + final WebSocketConfiguration config = new WebSocketConfiguration(); + config.setEnabled(false); + config.setHost(DEFAULT_WEBSOCKET_HOST); + config.setPort(DEFAULT_WEBSOCKET_PORT); + config.setRpcApis(DEFAULT_WEBSOCKET_APIS); + return config; + } + + private WebSocketConfiguration() {} + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + public void setHost(final String host) { + this.host = host; + } + + public String getHost() { + return host; + } + + public void setPort(final int port) { + this.port = port; + } + + public int getPort() { + return port; + } + + public Collection getRpcApis() { + return rpcApis; + } + + public void setRpcApis(final Collection rpcApis) { + this.rpcApis = rpcApis; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("enabled", enabled) + .add("port", port) + .add("host", host) + .add("rpcApis", rpcApis) + .toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final WebSocketConfiguration that = (WebSocketConfiguration) o; + return enabled == that.enabled + && port == that.port + && Objects.equal(host, that.host) + && Objects.equal(rpcApis, that.rpcApis); + } + + @Override + public int hashCode() { + return Objects.hashCode(enabled, port, host, rpcApis); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandler.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandler.java new file mode 100755 index 00000000000..9b8038c1212 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandler.java @@ -0,0 +1,67 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketRpcRequest; + +import java.util.Map; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class WebSocketRequestHandler { + + private static final Logger LOGGER = LogManager.getLogger(WebSocketRequestHandler.class); + + private final Vertx vertx; + private final Map methods; + + public WebSocketRequestHandler(final Vertx vertx, final Map methods) { + this.vertx = vertx; + this.methods = methods; + } + + public void handle(final String id, final Buffer buffer) { + vertx.executeBlocking( + future -> { + WebSocketRpcRequest request; + try { + request = buffer.toJsonObject().mapTo(WebSocketRpcRequest.class); + } catch (IllegalArgumentException | DecodeException e) { + LOGGER.debug("Error mapping json to WebSocketRpcRequest", e); + future.complete(JsonRpcError.INVALID_REQUEST); + return; + } + + if (!methods.containsKey(request.getMethod())) { + future.complete(JsonRpcError.METHOD_NOT_FOUND); + LOGGER.debug("Can't find method {}", request.getMethod()); + return; + } + final JsonRpcMethod method = methods.get(request.getMethod()); + try { + LOGGER.info("WS-RPC request -> {}", request.getMethod()); + request.setConnectionId(id); + future.complete(method.response(request)); + } catch (final Exception e) { + LOGGER.error(JsonRpcError.INTERNAL_ERROR.getMessage(), e); + future.complete(JsonRpcError.INTERNAL_ERROR); + } + }, + result -> { + if (result.succeeded()) { + replyToClient(id, Json.encodeToBuffer(result.result())); + } else { + replyToClient(id, Json.encodeToBuffer(JsonRpcError.INTERNAL_ERROR)); + } + }); + } + + private void replyToClient(final String id, final Buffer request) { + vertx.eventBus().send(id, request.toString()); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java new file mode 100755 index 00000000000..fcf2c883b0c --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketService.java @@ -0,0 +1,140 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket; + +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.ServerWebSocket; +import io.vertx.core.net.SocketAddress; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class WebSocketService { + + private static final Logger LOGGER = LogManager.getLogger(WebSocketService.class); + + private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0); + + private final Vertx vertx; + private final WebSocketConfiguration configuration; + private final WebSocketRequestHandler websocketRequestHandler; + + private HttpServer httpServer; + + public WebSocketService( + final Vertx vertx, + final WebSocketConfiguration configuration, + final WebSocketRequestHandler websocketRequestHandler) { + this.vertx = vertx; + this.configuration = configuration; + this.websocketRequestHandler = websocketRequestHandler; + } + + public CompletableFuture start() { + LOGGER.info( + "Starting Websocket service on {}:{}", configuration.getHost(), configuration.getPort()); + + final CompletableFuture resultFuture = new CompletableFuture<>(); + + httpServer = + vertx + .createHttpServer( + new HttpServerOptions() + .setHost(configuration.getHost()) + .setPort(configuration.getPort()) + .setWebsocketSubProtocols("undefined")) + .websocketHandler(websocketHandler()) + .listen(startHandler(resultFuture)); + + return resultFuture; + } + + private Handler websocketHandler() { + return websocket -> { + final SocketAddress socketAddress = websocket.remoteAddress(); + final String connectionId = websocket.textHandlerID(); + + LOGGER.debug("Websocket Connected ({})", socketAddressAsString(socketAddress)); + + websocket.handler( + buffer -> { + LOGGER.debug( + "Received Websocket request {} ({})", + buffer.toString(), + socketAddressAsString(socketAddress)); + + websocketRequestHandler.handle(connectionId, buffer); + }); + + websocket.closeHandler( + v -> { + LOGGER.debug("Websocket Disconnected ({})", socketAddressAsString(socketAddress)); + vertx + .eventBus() + .publish(SubscriptionManager.EVENTBUS_REMOVE_SUBSCRIPTIONS_ADDRESS, connectionId); + }); + + websocket.exceptionHandler( + t -> { + LOGGER.debug( + "Unrecoverable error on Websocket: {} ({})", + t.getMessage(), + socketAddressAsString(socketAddress)); + websocket.close(); + }); + }; + } + + private Handler> startHandler(final CompletableFuture resultFuture) { + return res -> { + if (res.succeeded()) { + + LOGGER.info( + "Websocket service started and listening on {}:{}", + configuration.getHost(), + httpServer.actualPort()); + + resultFuture.complete(null); + } else { + resultFuture.completeExceptionally(res.cause()); + } + }; + } + + public CompletableFuture stop() { + if (httpServer == null) { + return CompletableFuture.completedFuture(null); + } + + final CompletableFuture resultFuture = new CompletableFuture<>(); + + httpServer.close( + res -> { + if (res.succeeded()) { + httpServer = null; + resultFuture.complete(null); + } else { + resultFuture.completeExceptionally(res.cause()); + } + }); + + return resultFuture; + } + + public InetSocketAddress socketAddress() { + if (httpServer == null) { + return EMPTY_SOCKET_ADDRESS; + } + return new InetSocketAddress(configuration.getHost(), httpServer.actualPort()); + } + + private String socketAddressAsString(final SocketAddress socketAddress) { + return String.format("host=%s, port=%d", socketAddress.host(), socketAddress.port()); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/AbstractSubscriptionMethod.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/AbstractSubscriptionMethod.java new file mode 100755 index 00000000000..d9c0d98d124 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/AbstractSubscriptionMethod.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; + +abstract class AbstractSubscriptionMethod implements JsonRpcMethod { + + private final SubscriptionManager subscriptionManager; + private final SubscriptionRequestMapper mapper; + + AbstractSubscriptionMethod( + final SubscriptionManager subscriptionManager, final SubscriptionRequestMapper mapper) { + this.subscriptionManager = subscriptionManager; + this.mapper = mapper; + } + + SubscriptionManager subscriptionManager() { + return subscriptionManager; + } + + public SubscriptionRequestMapper getMapper() { + return mapper; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribe.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribe.java new file mode 100755 index 00000000000..271e62aec4b --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribe.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; + +public class EthSubscribe extends AbstractSubscriptionMethod { + + EthSubscribe( + final SubscriptionManager subscriptionManager, final SubscriptionRequestMapper mapper) { + super(subscriptionManager, mapper); + } + + @Override + public String getName() { + return "eth_subscribe"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + try { + final SubscribeRequest subscribeRequest = getMapper().mapSubscribeRequest(request); + final Long subscriptionId = subscriptionManager().subscribe(subscribeRequest); + + return new JsonRpcSuccessResponse(request.getId(), Quantity.create(subscriptionId)); + } catch (final InvalidSubscriptionRequestException isEx) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_REQUEST); + } catch (final Exception e) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribe.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribe.java new file mode 100755 index 00000000000..70be489ce99 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribe.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionNotFoundException; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.UnsubscribeRequest; + +public class EthUnsubscribe extends AbstractSubscriptionMethod { + + EthUnsubscribe( + final SubscriptionManager subscriptionManager, final SubscriptionRequestMapper mapper) { + super(subscriptionManager, mapper); + } + + @Override + public String getName() { + return "eth_unsubscribe"; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + try { + final UnsubscribeRequest unsubscribeRequest = getMapper().mapUnsubscribeRequest(request); + final boolean unsubscribed = subscriptionManager().unsubscribe(unsubscribeRequest); + + return new JsonRpcSuccessResponse(request.getId(), unsubscribed); + } catch (final InvalidSubscriptionRequestException isEx) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_REQUEST); + } catch (final SubscriptionNotFoundException snfEx) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.SUBSCRIPTION_NOT_FOUND); + } catch (final Exception e) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactory.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactory.java new file mode 100755 index 00000000000..20e95737de5 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactory.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; + +import java.util.HashMap; +import java.util.Map; + +public class WebSocketMethodsFactory { + + private final SubscriptionManager subscriptionManager; + private final Map jsonRpcMethods; + private final JsonRpcParameter parameter = new JsonRpcParameter(); + + public WebSocketMethodsFactory( + final SubscriptionManager subscriptionManager, + final Map jsonRpcMethods) { + this.subscriptionManager = subscriptionManager; + this.jsonRpcMethods = jsonRpcMethods; + } + + public Map methods() { + final Map websocketMethods = new HashMap<>(); + websocketMethods.putAll(jsonRpcMethods); + addMethods( + websocketMethods, + new EthSubscribe(subscriptionManager, new SubscriptionRequestMapper(parameter)), + new EthUnsubscribe(subscriptionManager, new SubscriptionRequestMapper(parameter))); + return websocketMethods; + } + + public Map addMethods( + final Map methods, final JsonRpcMethod... rpcMethods) { + for (final JsonRpcMethod rpcMethod : rpcMethods) { + methods.put(rpcMethod.getName(), rpcMethod); + } + return methods; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketRpcRequest.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketRpcRequest.java new file mode 100755 index 00000000000..9b8eec38743 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketRpcRequest.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; + +public class WebSocketRpcRequest extends JsonRpcRequest { + + private String connectionId; + + @JsonCreator + public WebSocketRpcRequest( + @JsonProperty("jsonrpc") final String version, + @JsonProperty("method") final String method, + @JsonProperty("params") final Object[] params, + @JsonProperty("connectionId") final String connectionId) { + super(version, method, params); + this.connectionId = connectionId; + } + + @JsonSetter("connectionId") + public void setConnectionId(final String connectionId) { + this.connectionId = connectionId; + } + + @JsonGetter("connectionId") + public String getConnectionId() { + return this.connectionId; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/Subscription.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/Subscription.java new file mode 100755 index 00000000000..6c97e824e8d --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/Subscription.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription; + +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +public class Subscription { + + private final Long id; + private final SubscriptionType subscriptionType; + + public Subscription(final Long id, final SubscriptionType subscriptionType) { + this.id = id; + this.subscriptionType = subscriptionType; + } + + public SubscriptionType getSubscriptionType() { + return subscriptionType; + } + + public Long getId() { + return id; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("subscriptionType", subscriptionType) + .toString(); + } + + public boolean isType(final SubscriptionType type) { + return this.subscriptionType == type; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Subscription that = (Subscription) o; + return Objects.equal(id, that.id) && subscriptionType == that.subscriptionType; + } + + @Override + public int hashCode() { + return Objects.hashCode(id, subscriptionType); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilder.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilder.java new file mode 100755 index 00000000000..041347a28a1 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilder.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription; + +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.blockheaders.NewBlockHeadersSubscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.logs.LogsSubscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing.SyncingSubscription; + +import java.util.Optional; +import java.util.function.Function; + +public class SubscriptionBuilder { + + public Subscription build(final long id, final SubscribeRequest request) { + final SubscriptionType subscriptionType = request.getSubscriptionType(); + switch (subscriptionType) { + case NEW_BLOCK_HEADERS: + { + return new NewBlockHeadersSubscription(id, request.getIncludeTransaction()); + } + case LOGS: + { + return new LogsSubscription( + id, + Optional.ofNullable(request.getFilterParameter()) + .orElseThrow(IllegalArgumentException::new)); + } + case SYNCING: + { + return new SyncingSubscription(id, subscriptionType); + } + case NEW_PENDING_TRANSACTIONS: + default: + return new Subscription(id, subscriptionType); + } + } + + @SuppressWarnings("unchecked") + public Function mapToSubscriptionClass(final Class clazz) { + return subscription -> { + if (clazz.isAssignableFrom(subscription.getClass())) { + return (T) subscription; + } else { + final String msg = + String.format( + "%s instance can't be mapped to type %s", + subscription.getClass().getSimpleName(), clazz.getSimpleName()); + throw new IllegalArgumentException(msg); + } + }; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManager.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManager.java new file mode 100755 index 00000000000..886aa532777 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManager.java @@ -0,0 +1,147 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.UnsubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.response.SubscriptionResponse; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.eventbus.Message; +import io.vertx.core.json.Json; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * The SubscriptionManager is responsible for managing subscriptions and sending messages to the + * clients that have an active subscription subscription. + * + *

TODO: The logic to send a notification to a client that has an active subscription TODO: + * handle connection close (remove subscriptions) + */ +public class SubscriptionManager extends AbstractVerticle { + + private static final Logger LOGGER = LogManager.getLogger(SubscriptionManager.class); + + public static final String EVENTBUS_REMOVE_SUBSCRIPTIONS_ADDRESS = + "SubscriptionManager::removeSubscriptions"; + + private final AtomicLong subscriptionCounter = new AtomicLong(0); + private final Map subscriptions = new HashMap<>(); + private final Map> connectionSubscriptionsMap = new HashMap<>(); + private final SubscriptionBuilder subscriptionBuilder = new SubscriptionBuilder(); + + @Override + public void start() { + vertx.eventBus().consumer(EVENTBUS_REMOVE_SUBSCRIPTIONS_ADDRESS, this::removeSubscriptions); + } + + public Long subscribe(final SubscribeRequest request) { + LOGGER.info("Subscribe request {}", request); + + final long subscriptionId = subscriptionCounter.incrementAndGet(); + final Subscription subscription = subscriptionBuilder.build(subscriptionId, request); + addSubscription(subscription, request.getConnectionId()); + + return subscription.getId(); + } + + private void addSubscription(final Subscription subscription, final String connectionId) { + subscriptions.put(subscription.getId(), subscription); + mapSubscriptionToConnection(connectionId, subscription.getId()); + } + + private void mapSubscriptionToConnection(final String connectionId, final Long subscriptionId) { + if (connectionSubscriptionsMap.containsKey(connectionId)) { + connectionSubscriptionsMap.get(connectionId).add(subscriptionId); + } else { + connectionSubscriptionsMap.put(connectionId, Lists.newArrayList(subscriptionId)); + } + } + + public boolean unsubscribe(final UnsubscribeRequest request) { + LOGGER.debug("Unsubscribe request subscriptionId = {}", request.getSubscriptionId()); + + if (!subscriptions.containsKey(request.getSubscriptionId())) { + throw new SubscriptionNotFoundException(request.getSubscriptionId()); + } + + destroySubscription(request.getSubscriptionId(), request.getConnectionId()); + + return true; + } + + private void destroySubscription(final long subscriptionId, final String connectionId) { + subscriptions.remove(subscriptionId); + + if (connectionSubscriptionsMap.containsKey(connectionId)) { + removeSubscriptionToConnectionMapping(connectionId, subscriptionId); + } + } + + private void removeSubscriptionToConnectionMapping( + final String connectionId, final Long subscriptionId) { + if (connectionSubscriptionsMap.get(connectionId).size() > 1) { + connectionSubscriptionsMap.get(connectionId).remove(subscriptionId); + } else { + connectionSubscriptionsMap.remove(connectionId); + } + } + + @VisibleForTesting + void removeSubscriptions(final Message message) { + final String connectionId = message.body(); + if (connectionId == null || "".equals(connectionId)) { + LOGGER.warn("Received invalid connectionId ({}). No subscriptions removed."); + } + + LOGGER.debug("Removing subscription for connectionId = {}", connectionId); + + final List subscriptionIds = + Lists.newArrayList( + connectionSubscriptionsMap.getOrDefault(connectionId, Lists.newArrayList())); + subscriptionIds.forEach(subscriptionId -> destroySubscription(subscriptionId, connectionId)); + } + + @VisibleForTesting + Map subscriptions() { + return Maps.newHashMap(subscriptions); + } + + @VisibleForTesting + public Map> getConnectionSubscriptionsMap() { + return Maps.newHashMap(connectionSubscriptionsMap); + } + + public List subscriptionsOfType(final SubscriptionType type, final Class clazz) { + return subscriptions + .entrySet() + .stream() + .map(Entry::getValue) + .filter(subscription -> subscription.isType(type)) + .map(subscriptionBuilder.mapToSubscriptionClass(clazz)) + .collect(Collectors.toList()); + } + + public void sendMessage(final Long subscriptionId, final JsonRpcResult msg) { + final SubscriptionResponse response = new SubscriptionResponse(subscriptionId, msg); + + connectionSubscriptionsMap + .entrySet() + .stream() + .filter(e -> e.getValue().contains(subscriptionId)) + .map(Entry::getKey) + .findFirst() + .ifPresent(connectionId -> vertx.eventBus().send(connectionId, Json.encode(response))); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionNotFoundException.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionNotFoundException.java new file mode 100755 index 00000000000..a91343cdff4 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionNotFoundException.java @@ -0,0 +1,15 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription; + +public class SubscriptionNotFoundException extends RuntimeException { + + private final Long subscriptionId; + + public SubscriptionNotFoundException(final Long subscriptionId) { + super(String.format("Subscription not found (id=%s)", subscriptionId)); + this.subscriptionId = subscriptionId; + } + + public Long getSubscriptionId() { + return subscriptionId; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscription.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscription.java new file mode 100755 index 00000000000..aa6be15e2be --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscription.java @@ -0,0 +1,18 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.blockheaders; + +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.Subscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +public class NewBlockHeadersSubscription extends Subscription { + + private final boolean includeTransactions; + + public NewBlockHeadersSubscription(final Long subscriptionId, final boolean includeTransactions) { + super(subscriptionId, SubscriptionType.NEW_BLOCK_HEADERS); + this.includeTransactions = includeTransactions; + } + + public boolean getIncludeTransactions() { + return includeTransactions; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionService.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionService.java new file mode 100755 index 00000000000..b38a0ac8759 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionService.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.blockheaders; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.BlockAddedObserver; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactory; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import java.util.List; + +public class NewBlockHeadersSubscriptionService implements BlockAddedObserver { + + private final SubscriptionManager subscriptionManager; + private final BlockchainQueries blockchainQueries; + private final BlockResultFactory blockResult = new BlockResultFactory(); + + public NewBlockHeadersSubscriptionService( + final SubscriptionManager subscriptionManager, final BlockchainQueries blockchainQueries) { + this.subscriptionManager = subscriptionManager; + this.blockchainQueries = blockchainQueries; + } + + @Override + public void onBlockAdded(final BlockAddedEvent event, final Blockchain blockchain) { + final List subscribers = + subscriptionManager.subscriptionsOfType( + SubscriptionType.NEW_BLOCK_HEADERS, NewBlockHeadersSubscription.class); + + final Hash newBlockHash = event.getBlock().getHash(); + + for (final NewBlockHeadersSubscription subscription : subscribers) { + final BlockResult newBlock = + subscription.getIncludeTransactions() + ? blockWithCompleteTransaction(newBlockHash) + : blockWithTransactionHash(newBlockHash); + + subscriptionManager.sendMessage(subscription.getId(), newBlock); + } + } + + private BlockResult blockWithCompleteTransaction(final Hash hash) { + return blockchainQueries.blockByHash(hash).map(blockResult::transactionComplete).orElse(null); + } + + private BlockResult blockWithTransactionHash(final Hash hash) { + return blockchainQueries + .blockByHashWithTxHashes(hash) + .map(blockResult::transactionHash) + .orElse(null); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscription.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscription.java new file mode 100755 index 00000000000..17a23ebda82 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscription.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.logs; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.LogsQuery; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.Subscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +public class LogsSubscription extends Subscription { + + private final FilterParameter filterParameter; + + public LogsSubscription(final Long subscriptionId, final FilterParameter filterParameter) { + super(subscriptionId, SubscriptionType.LOGS); + this.filterParameter = filterParameter; + } + + public LogsQuery getLogsQuery() { + return new LogsQuery.Builder() + .addresses(filterParameter.getAddresses()) + .topics(filterParameter.getTopics()) + .build(); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java new file mode 100755 index 00000000000..94a60a71c32 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionService.java @@ -0,0 +1,102 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.logs; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.BlockAddedObserver; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionReceiptWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.LogResult; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import java.util.List; +import java.util.Optional; + +public class LogsSubscriptionService implements BlockAddedObserver { + + private final SubscriptionManager subscriptionManager; + private final BlockchainQueries blockchainQueries; + + public LogsSubscriptionService( + final SubscriptionManager subscriptionManager, final BlockchainQueries blockchainQueries) { + this.subscriptionManager = subscriptionManager; + this.blockchainQueries = blockchainQueries; + } + + @Override + public void onBlockAdded(final BlockAddedEvent event, final Blockchain blockchain) { + final List logsSubscriptions = + subscriptionManager.subscriptionsOfType(SubscriptionType.LOGS, LogsSubscription.class); + + if (logsSubscriptions.isEmpty()) { + return; + } + + event + .getAddedTransactions() + .stream() + .map(tx -> blockchainQueries.transactionReceiptByTransactionHash(tx.hash())) + .filter(Optional::isPresent) + .map(Optional::get) + .forEachOrdered( + receiptWithMetadata -> { + final List logs = receiptWithMetadata.getReceipt().getLogs(); + sendLogsToMatchingSubscriptions(logs, logsSubscriptions, receiptWithMetadata, false); + }); + + event + .getRemovedTransactions() + .stream() + .map(tx -> blockchainQueries.transactionReceiptByTransactionHash(tx.hash())) + .filter(Optional::isPresent) + .map(Optional::get) + .forEachOrdered( + receiptWithMetadata -> { + final List logs = receiptWithMetadata.getReceipt().getLogs(); + sendLogsToMatchingSubscriptions(logs, logsSubscriptions, receiptWithMetadata, true); + }); + } + + private void sendLogsToMatchingSubscriptions( + final List logs, + final List logsSubscriptions, + final TransactionReceiptWithMetadata receiptWithMetadata, + final boolean removed) { + for (int logIndex = 0; logIndex < logs.size(); logIndex++) { + for (final LogsSubscription subscription : logsSubscriptions) { + if (subscription.getLogsQuery().matches(logs.get(logIndex))) { + sendLogToSubscription(receiptWithMetadata, removed, logIndex, subscription); + } + } + } + } + + private void sendLogToSubscription( + final TransactionReceiptWithMetadata receiptWithMetadata, + final boolean removed, + final int logIndex, + final LogsSubscription subscription) { + final LogWithMetadata logWithMetaData = logWithMetadata(logIndex, receiptWithMetadata, removed); + subscriptionManager.sendMessage(subscription.getId(), new LogResult(logWithMetaData)); + } + + // @formatter:off + private LogWithMetadata logWithMetadata( + final int logIndex, + final TransactionReceiptWithMetadata transactionReceiptWithMetadata, + final boolean removed) { + return LogWithMetadata.create( + logIndex, + transactionReceiptWithMetadata.getBlockNumber(), + transactionReceiptWithMetadata.getBlockHash(), + transactionReceiptWithMetadata.getTransactionHash(), + transactionReceiptWithMetadata.getTransactionIndex(), + transactionReceiptWithMetadata.getReceipt().getLogs().get(logIndex).getLogger(), + transactionReceiptWithMetadata.getReceipt().getLogs().get(logIndex).getData(), + transactionReceiptWithMetadata.getReceipt().getLogs().get(logIndex).getTopics(), + removed); + } + // @formatter:on +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionResult.java new file mode 100755 index 00000000000..ae2cf2f4372 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionResult.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.pending; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; + +import com.fasterxml.jackson.annotation.JsonValue; + +public class PendingTransactionResult implements JsonRpcResult { + + private final String hash; + + public PendingTransactionResult(final Hash hash) { + this.hash = hash.toString(); + } + + @JsonValue + public String getHash() { + return hash; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionService.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionService.java new file mode 100755 index 00000000000..38faff5e500 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionService.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.pending; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.PendingTransactionListener; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.Subscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import java.util.List; + +public class PendingTransactionSubscriptionService implements PendingTransactionListener { + + private final SubscriptionManager subscriptionManager; + + public PendingTransactionSubscriptionService(final SubscriptionManager subscriptionManager) { + this.subscriptionManager = subscriptionManager; + } + + @Override + public void onTransactionAdded(final Transaction pendingTransaction) { + notifySubscribers(pendingTransaction.hash()); + } + + private void notifySubscribers(final Hash pendingTransaction) { + final List subscriptions = pendingTransactionSubscriptions(); + + for (final Subscription subscription : subscriptions) { + subscriptionManager.sendMessage( + subscription.getId(), new PendingTransactionResult(pendingTransaction)); + } + } + + private List pendingTransactionSubscriptions() { + return subscriptionManager.subscriptionsOfType( + SubscriptionType.NEW_PENDING_TRANSACTIONS, Subscription.class); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidRequestException.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidRequestException.java new file mode 100755 index 00000000000..c49d53579fe --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidRequestException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +public class InvalidRequestException extends RuntimeException { + + public InvalidRequestException(final String message) { + super(message); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java new file mode 100755 index 00000000000..08435195ec1 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/InvalidSubscriptionRequestException.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +public class InvalidSubscriptionRequestException extends RuntimeException { + + public InvalidSubscriptionRequestException() { + super(); + } + + public InvalidSubscriptionRequestException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/LogsSubscriptionParam.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/LogsSubscriptionParam.java new file mode 100755 index 00000000000..22057e3bc2c --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/LogsSubscriptionParam.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +class LogsSubscriptionParam { + + private final String address; + private final List topics; + + @JsonCreator + LogsSubscriptionParam( + @JsonProperty("address") final String address, + @JsonProperty("topics") final List topics) { + this.address = address; + this.topics = topics; + } + + String address() { + return address; + } + + List topics() { + return topics; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/NewBlockHeadersSubscriptionParam.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/NewBlockHeadersSubscriptionParam.java new file mode 100755 index 00000000000..381fb419a56 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/NewBlockHeadersSubscriptionParam.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +class NewBlockHeadersSubscriptionParam { + + private final boolean includeTransaction; + + @JsonCreator + NewBlockHeadersSubscriptionParam( + @JsonProperty("includeTransactions") final boolean includeTransaction) { + this.includeTransaction = includeTransaction; + } + + boolean includeTransaction() { + return includeTransaction; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscribeRequest.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscribeRequest.java new file mode 100755 index 00000000000..2eaf4b186e5 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscribeRequest.java @@ -0,0 +1,74 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; + +import com.google.common.base.Objects; + +public class SubscribeRequest { + + private final SubscriptionType subscriptionType; + private final Boolean includeTransaction; + private final FilterParameter filterParameter; + private final String connectionId; + + public SubscribeRequest( + final SubscriptionType subscriptionType, + final FilterParameter filterParameter, + final Boolean includeTransaction, + final String connectionId) { + this.subscriptionType = subscriptionType; + this.includeTransaction = includeTransaction; + this.filterParameter = filterParameter; + this.connectionId = connectionId; + } + + public SubscriptionType getSubscriptionType() { + return subscriptionType; + } + + public FilterParameter getFilterParameter() { + return filterParameter; + } + + public Boolean getIncludeTransaction() { + return includeTransaction; + } + + public String getConnectionId() { + return this.connectionId; + } + + @Override + public String toString() { + return "SubscribeRequest{" + + "subscriptionType=" + + subscriptionType + + ", includeTransaction=" + + includeTransaction + + ", filterParameter=" + + filterParameter + + ", connectionId=" + + connectionId + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SubscribeRequest that = (SubscribeRequest) o; + return subscriptionType == that.subscriptionType + && Objects.equal(includeTransaction, that.includeTransaction) + && Objects.equal(filterParameter, that.filterParameter) + && Objects.equal(connectionId, that.connectionId); + } + + @Override + public int hashCode() { + return Objects.hashCode(subscriptionType, includeTransaction, filterParameter, connectionId); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java new file mode 100755 index 00000000000..7aa13ff3531 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapper.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.UnsignedLongParameter; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketRpcRequest; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class SubscriptionRequestMapper { + + private final JsonRpcParameter parameter; + + public SubscriptionRequestMapper(final JsonRpcParameter parameter) { + this.parameter = parameter; + } + + public SubscribeRequest mapSubscribeRequest(final JsonRpcRequest jsonRpcRequest) + throws InvalidSubscriptionRequestException { + try { + final WebSocketRpcRequest webSocketRpcRequest = validateRequest(jsonRpcRequest); + + final SubscriptionType subscriptionType = + parameter.required(webSocketRpcRequest.getParams(), 0, SubscriptionType.class); + + switch (subscriptionType) { + case NEW_BLOCK_HEADERS: + { + return parseNewBlockHeadersRequest(webSocketRpcRequest); + } + case LOGS: + { + return parseLogsRequest(webSocketRpcRequest, parameter); + } + case NEW_PENDING_TRANSACTIONS: + case SYNCING: + default: + return new SubscribeRequest( + subscriptionType, null, null, webSocketRpcRequest.getConnectionId()); + } + } catch (final Exception e) { + throw new InvalidSubscriptionRequestException("Error parsing subscribe request", e); + } + } + + private SubscribeRequest parseNewBlockHeadersRequest(final WebSocketRpcRequest request) { + final Optional params = + parameter.optional(request.getParams(), 1, NewBlockHeadersSubscriptionParam.class); + final boolean includeTransactions = params.isPresent() && params.get().includeTransaction(); + return new SubscribeRequest( + SubscriptionType.NEW_BLOCK_HEADERS, null, includeTransactions, request.getConnectionId()); + } + + private SubscribeRequest parseLogsRequest( + final WebSocketRpcRequest request, final JsonRpcParameter parameter) { + final LogsSubscriptionParam logFilterParams = + parameter.required(request.getParams(), 1, LogsSubscriptionParam.class); + return new SubscribeRequest( + SubscriptionType.LOGS, + createFilterParameter(logFilterParams), + null, + request.getConnectionId()); + } + + private FilterParameter createFilterParameter(final LogsSubscriptionParam logFilterParams) { + final List addresses = hasAddresses(logFilterParams); + final List> topics = hasTopics(logFilterParams); + return new FilterParameter(null, null, addresses, topics, null); + } + + private List hasAddresses(final LogsSubscriptionParam logFilterParams) { + return logFilterParams.address() != null && !logFilterParams.address().isEmpty() + ? Arrays.asList(logFilterParams.address()) + : Collections.emptyList(); + } + + private List> hasTopics(final LogsSubscriptionParam logFilterParams) { + return logFilterParams.topics() != null && !logFilterParams.topics().isEmpty() + ? Arrays.asList(logFilterParams.topics()) + : Collections.emptyList(); + } + + public UnsubscribeRequest mapUnsubscribeRequest(final JsonRpcRequest jsonRpcRequest) + throws InvalidSubscriptionRequestException { + try { + final WebSocketRpcRequest webSocketRpcRequest = validateRequest(jsonRpcRequest); + + final long subscriptionId = + parameter + .required(webSocketRpcRequest.getParams(), 0, UnsignedLongParameter.class) + .getValue(); + return new UnsubscribeRequest(subscriptionId, webSocketRpcRequest.getConnectionId()); + } catch (final Exception e) { + throw new InvalidSubscriptionRequestException("Error parsing subscribe request", e); + } + } + + private WebSocketRpcRequest validateRequest(final JsonRpcRequest jsonRpcRequest) { + if (jsonRpcRequest instanceof WebSocketRpcRequest) { + return (WebSocketRpcRequest) jsonRpcRequest; + } else { + throw new InvalidRequestException("Invalid request received."); + } + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionType.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionType.java new file mode 100755 index 00000000000..b174d1033ae --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionType.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum SubscriptionType { + @JsonProperty("newHeads") + NEW_BLOCK_HEADERS("newHeads"), + + @JsonProperty("logs") + LOGS("logs"), + + @JsonProperty("newPendingTransactions") + NEW_PENDING_TRANSACTIONS("newPendingTransactions"), + + @JsonProperty("syncing") + SYNCING("syncing"); + + private final String code; + + SubscriptionType(final String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + public static SubscriptionType fromCode(final String code) { + for (final SubscriptionType subscriptionType : SubscriptionType.values()) { + if (code.equals(subscriptionType.getCode())) { + return subscriptionType; + } + } + + throw new IllegalArgumentException(String.format("Invalid subscription type '%s'", code)); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/UnsubscribeRequest.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/UnsubscribeRequest.java new file mode 100755 index 00000000000..fb55eee51f5 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/UnsubscribeRequest.java @@ -0,0 +1,50 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +import com.google.common.base.Objects; + +public class UnsubscribeRequest { + + private final Long subscriptionId; + private final String connectionId; + + public UnsubscribeRequest(final Long subscriptionId, final String connectionId) { + this.subscriptionId = subscriptionId; + this.connectionId = connectionId; + } + + public Long getSubscriptionId() { + return subscriptionId; + } + + public String getConnectionId() { + return connectionId; + } + + @Override + public String toString() { + return "UnsubscribeRequest{" + + "subscriptionId='" + + subscriptionId + + ", connectionId=" + + connectionId + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final UnsubscribeRequest that = (UnsubscribeRequest) o; + return Objects.equal(subscriptionId, that.subscriptionId) + && Objects.equal(connectionId, that.connectionId); + } + + @Override + public int hashCode() { + return Objects.hashCode(subscriptionId, connectionId); + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponse.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponse.java new file mode 100755 index 00000000000..404ae42ccf6 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponse.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.response; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({"jsonrpc", "method", "params"}) +public class SubscriptionResponse { + + private static final String JSON_RPC_VERSION = "2.0"; + private static final String METHOD_NAME = "eth_subscription"; + + private final SubscriptionResponseResult params; + + public SubscriptionResponse(final long subscriptionId, final JsonRpcResult result) { + this.params = new SubscriptionResponseResult(Quantity.create(subscriptionId), result); + } + + @JsonGetter("jsonrpc") + public String getJsonrpc() { + return JSON_RPC_VERSION; + } + + @JsonGetter("method") + public String getMethod() { + return METHOD_NAME; + } + + @JsonGetter("params") + public SubscriptionResponseResult getParams() { + return params; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java new file mode 100755 index 00000000000..e77c67f8883 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/response/SubscriptionResponseResult.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.response; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({"subscription", "result"}) +public class SubscriptionResponseResult { + + private final String subscription; + private final JsonRpcResult result; + + SubscriptionResponseResult(final String subscription, final JsonRpcResult result) { + this.subscription = subscription; + this.result = result; + } + + @JsonGetter("subscription") + public String getSubscription() { + return subscription; + } + + @JsonGetter("result") + public JsonRpcResult getResult() { + return result; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/NotSynchronisingResult.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/NotSynchronisingResult.java new file mode 100755 index 00000000000..d9d5f13a98c --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/NotSynchronisingResult.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; + +import com.fasterxml.jackson.annotation.JsonValue; + +public class NotSynchronisingResult implements JsonRpcResult { + + @JsonValue + public boolean getResult() { + return false; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscription.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscription.java new file mode 100755 index 00000000000..8aa98d36dba --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscription.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing; + +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.Subscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +public class SyncingSubscription extends Subscription { + private boolean firstMessageHasBeenSent = false; + + public SyncingSubscription(final Long id, final SubscriptionType subscriptionType) { + super(id, subscriptionType); + } + + public void setFirstMessageHasBeenSent(final boolean firstMessageHasBeenSent) { + this.firstMessageHasBeenSent = firstMessageHasBeenSent; + } + + public boolean isFirstMessageHasBeenSent() { + return firstMessageHasBeenSent; + } +} diff --git a/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionService.java b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionService.java new file mode 100755 index 00000000000..46839f1cd82 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionService.java @@ -0,0 +1,74 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing; + +import net.consensys.pantheon.ethereum.core.SyncStatus; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.SyncingResult; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.Subscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import java.util.List; +import java.util.Optional; + +public class SyncingSubscriptionService { + + private final SubscriptionManager subscriptionManager; + private final Synchronizer synchronizer; + private final long currentRefreshDelay = 5000; + + private Optional previousSyncStatus; + private long timerId; + + public SyncingSubscriptionService( + final SubscriptionManager subscriptionManager, final Synchronizer synchronizer) { + this.subscriptionManager = subscriptionManager; + this.synchronizer = synchronizer; + previousSyncStatus = synchronizer.getSyncStatus(); + engageNextTimerTick(); + } + + public void sendSyncingToMatchingSubscriptions() { + final List syncingSubscriptions = + subscriptionManager.subscriptionsOfType(SubscriptionType.SYNCING, Subscription.class); + final Optional syncStatus = synchronizer.getSyncStatus(); + + final boolean syncStatusChange = !syncStatus.equals(previousSyncStatus); + final JsonRpcResult result; + + if (syncStatus.isPresent()) { + result = new SyncingResult(syncStatus.get()); + } else { + result = new NotSynchronisingResult(); + } + + for (final Subscription subscription : syncingSubscriptions) { + sendSyncingResultToSubscription((SyncingSubscription) subscription, result, syncStatusChange); + } + previousSyncStatus = syncStatus; + } + + private void sendSyncingResultToSubscription( + final SyncingSubscription subscription, + final JsonRpcResult result, + final boolean syncStatusChange) { + if (syncStatusChange || !subscription.isFirstMessageHasBeenSent()) { + subscriptionManager.sendMessage(subscription.getId(), result); + subscription.setFirstMessageHasBeenSent(true); + } + } + + public void engageNextTimerTick() { + if (subscriptionManager.getVertx() != null) { + this.timerId = + subscriptionManager + .getVertx() + .setTimer( + currentRefreshDelay, + (id) -> { + sendSyncingToMatchingSubscriptions(); + engageNextTimerTick(); + }); + } + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java new file mode 100755 index 00000000000..ed989d7c088 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AbstractEthJsonRpcHttpServiceTest.java @@ -0,0 +1,194 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterIdGenerator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.util.RawBlockIterator; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; + +import java.net.URL; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import io.vertx.core.Vertx; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; + +public abstract class AbstractEthJsonRpcHttpServiceTest { + + protected static ProtocolSchedule PROTOCOL_SCHEDULE; + + protected static List BLOCKS; + + protected static Block GENESIS_BLOCK; + + protected static GenesisConfig GENESIS_CONFIG; + + protected final Vertx vertx = Vertx.vertx(); + + protected JsonRpcHttpService service; + + protected OkHttpClient client; + + protected String baseUrl; + + protected final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + protected final String CLIENT_VERSION = "TestClientVersion/0.1.0"; + + protected final String NET_VERSION = "6986785976597"; + + protected static final Collection JSON_RPC_APIS = + Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + + protected MutableBlockchain blockchain; + + protected WorldStateArchive stateArchive; + + protected FilterManager filterManager; + + protected ProtocolContext context; + + @BeforeClass + public static void setupConstants() throws Exception { + PROTOCOL_SCHEDULE = MainnetProtocolSchedule.create(); + + final URL blocksUrl = + EthJsonRpcHttpBySpecTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthJsonRpcHttpBySpecTest.class + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + BLOCKS = new ArrayList<>(); + try (final RawBlockIterator iterator = + new RawBlockIterator( + Paths.get(blocksUrl.toURI()), + rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash))) { + while (iterator.hasNext()) { + BLOCKS.add(iterator.next()); + } + } + + final String gensisjson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + GENESIS_BLOCK = BLOCKS.get(0); + GENESIS_CONFIG = GenesisConfig.fromJson(gensisjson, PROTOCOL_SCHEDULE); + } + + @Before + public void setupTest() { + final Synchronizer synchronizerMock = mock(Synchronizer.class); + final P2PNetwork peerDiscoveryMock = mock(P2PNetwork.class); + final TransactionPool transactionPoolMock = mock(TransactionPool.class); + final MiningCoordinator miningCoordinatorMock = mock(MiningCoordinator.class); + when(transactionPoolMock.addLocalTransaction(any(Transaction.class))) + .thenReturn(ValidationResult.valid()); + final PendingTransactions pendingTransactionsMock = mock(PendingTransactions.class); + when(transactionPoolMock.getPendingTransactions()).thenReturn(pendingTransactionsMock); + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + stateArchive = createInMemoryWorldStateArchive(); + GENESIS_CONFIG.writeStateTo(stateArchive.getMutable(Hash.EMPTY_TRIE_HASH)); + + blockchain = + new DefaultMutableBlockchain( + GENESIS_BLOCK, keyValueStorage, MainnetBlockHashFunction::createHash); + context = new ProtocolContext<>(blockchain, stateArchive, null); + + final BlockchainQueries blockchainQueries = new BlockchainQueries(blockchain, stateArchive); + final FilterIdGenerator filterIdGenerator = mock(FilterIdGenerator.class); + when(filterIdGenerator.nextId()).thenReturn("0x1"); + filterManager = new FilterManager(blockchainQueries, transactionPoolMock, filterIdGenerator); + + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + + final Map methods = + new JsonRpcMethodsFactory() + .methods( + CLIENT_VERSION, + NET_VERSION, + peerDiscoveryMock, + blockchainQueries, + synchronizerMock, + MainnetProtocolSchedule.create(), + filterManager, + transactionPoolMock, + miningCoordinatorMock, + supportedCapabilities, + JSON_RPC_APIS); + final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); + config.setPort(0); + service = new JsonRpcHttpService(vertx, config, methods); + service.start().join(); + + client = new OkHttpClient(); + baseUrl = service.url(); + } + + @After + public void shutdownServer() { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + service.stop().join(); + vertx.close(); + } + + protected void importBlock(final int n) { + final Block block = BLOCKS.get(n); + final ProtocolSpec protocolSpec = + PROTOCOL_SCHEDULE.getByBlockNumber(block.getHeader().getNumber()); + final BlockImporter blockImporter = protocolSpec.getBlockImporter(); + blockImporter.importBlock(context, block, HeaderValidationMode.FULL); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AdminJsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AdminJsonRpcHttpServiceTest.java new file mode 100755 index 00000000000..78ffc81631d --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/AdminJsonRpcHttpServiceTest.java @@ -0,0 +1,101 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.StringTokenizer; + +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Test; + +public class AdminJsonRpcHttpServiceTest extends JsonRpcHttpServiceTest { + private static final Logger LOG = LogManager.getLogger(); + + @Test + public void getPeers() throws Exception { + final List caps = new ArrayList<>(); + caps.add(Capability.create("eth", 61)); + caps.add(Capability.create("eth", 62)); + final List peerList = new ArrayList<>(); + final PeerInfo info1 = + new PeerInfo(4, CLIENT_VERSION, caps, 30302, BytesValue.fromHexString("0001")); + final PeerInfo info2 = + new PeerInfo(4, CLIENT_VERSION, caps, 60302, BytesValue.fromHexString("0002")); + final PeerInfo info3 = + new PeerInfo(4, CLIENT_VERSION, caps, 60303, BytesValue.fromHexString("0003")); + final InetSocketAddress addr30301 = new InetSocketAddress("localhost", 30301); + final InetSocketAddress addr30302 = new InetSocketAddress("localhost", 30302); + final InetSocketAddress addr30303 = new InetSocketAddress("localhost", 30303); + final InetSocketAddress addr60301 = new InetSocketAddress("localhost", 60301); + final InetSocketAddress addr60302 = new InetSocketAddress("localhost", 60302); + final InetSocketAddress addr60303 = new InetSocketAddress("localhost", 60303); + + peerList.add(new MockPeerConnection(info1, addr60301, addr30302)); + peerList.add(new MockPeerConnection(info2, addr30301, addr60302)); + peerList.add(new MockPeerConnection(info3, addr30301, addr60303)); + + when(peerDiscoveryMock.getPeers()).thenReturn(peerList); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"admin_peers\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + LOG.info("Request: " + request); + try (Response resp = client.newCall(request).execute()) { + LOG.info("Response: " + resp); + + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + LOG.info("Response Body: " + json.encodePrettily()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonArray result = json.getJsonArray("result"); + + assertPeerResultMatchesPeer(result, peerList); + } + } + + protected void assertPeerResultMatchesPeer( + final JsonArray result, final Collection peerList) { + int i = -1; + for (final PeerConnection peerConn : peerList) { + final JsonObject peerJson = result.getJsonObject(++i); + final int jsonVersion = Integer.decode(peerJson.getString("version")); + final String jsonClient = peerJson.getString("name"); + final List caps = getCapabilities(peerJson.getJsonArray("caps")); + final int jsonPort = Integer.decode(peerJson.getString("port")); + final BytesValue jsonNodeId = BytesValue.fromHexString(peerJson.getString("id")); + + final PeerInfo jsonPeer = new PeerInfo(jsonVersion, jsonClient, caps, jsonPort, jsonNodeId); + assertThat(peerConn.getPeer()).isEqualTo(jsonPeer); + } + } + + protected List getCapabilities(final JsonArray jsonCaps) { + final List caps = new ArrayList<>(); + for (final Object jsonCap : jsonCaps) { + final StringTokenizer st = new StringTokenizer(jsonCap.toString(), "/"); + caps.add(Capability.create(st.nextToken(), Integer.valueOf(st.nextToken()))); + } + return caps; + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/EthJsonRpcHttpBySpecTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/EthJsonRpcHttpBySpecTest.java new file mode 100755 index 00000000000..476702aaa76 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/EthJsonRpcHttpBySpecTest.java @@ -0,0 +1,260 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthBlockNumber; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthCall; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthEstimateGas; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBalance; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBlockTransactionCountByHash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetBlockTransactionCountByNumber; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetCode; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetFilterChanges; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetLogs; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetStorageAt; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockHashAndIndex; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockNumberAndIndex; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByHash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionCount; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionReceipt; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthNewBlockFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthNewFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthNewPendingTransactionFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthSendRawTransaction; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.EthUninstallFilter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; + +import java.io.IOException; +import java.util.Collection; + +import com.google.common.base.Charsets; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.Resources; +import io.vertx.core.json.JsonObject; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class EthJsonRpcHttpBySpecTest extends AbstractEthJsonRpcHttpServiceTest { + + private final String specFileName; + + public EthJsonRpcHttpBySpecTest(final String specFileName) { + this.specFileName = specFileName; + } + + /* + Mapping between Json-RPC method class and its spec files + + Formatter will be turned on to make this easier to read (one spec per line) + @formatter:off + */ + @Parameters(name = "{index}: {0}") + public static Collection specs() { + final Multimap, String> specs = ArrayListMultimap.create(); + + specs.put(EthGetTransactionByHash.class, "eth_getTransactionByHash_addressReceiver"); + specs.put(EthGetTransactionByHash.class, "eth_getTransactionByHash_contractCreation"); + specs.put(EthGetTransactionByHash.class, "eth_getTransactionByHash_null"); + specs.put(EthGetTransactionByHash.class, "eth_getTransactionByHash_invalidParams"); + specs.put(EthGetTransactionByHash.class, "eth_getTransactionByHash_typeMismatch"); + specs.put(EthGetTransactionByHash.class, "eth_getTransactionByHash_invalidHashAndIndex"); + + specs.put(EthGetBalance.class, "eth_getBalance_latest"); + specs.put(EthGetBalance.class, "eth_getBalance_illegalRangeGreaterThan"); + specs.put(EthGetBalance.class, "eth_getBalance_illegalRangeLessThan"); + specs.put(EthGetBalance.class, "eth_getBalance_invalidParams"); + + specs.put(EthGetStorageAt.class, "eth_getStorageAt_latest"); + specs.put(EthGetStorageAt.class, "eth_getStorageAt_invalidParams"); + specs.put(EthGetStorageAt.class, "eth_getStorageAt_illegalRangeGreaterThan"); + specs.put(EthGetStorageAt.class, "eth_getStorageAt_illegalRangeLessThan"); + + specs.put(EthGetTransactionReceipt.class, "eth_getTransactionReceipt_contractAddress"); + specs.put(EthGetTransactionReceipt.class, "eth_getTransactionReceipt_nullContractAddress"); + specs.put(EthGetTransactionReceipt.class, "eth_getTransactionReceipt_logs"); + + specs.put(EthGetLogs.class, "eth_getLogs_invalidInput"); + specs.put(EthGetLogs.class, "eth_getLogs_blockhash"); + specs.put(EthGetLogs.class, "eth_getLogs_toBlockOutOfRange"); + specs.put(EthGetLogs.class, "eth_getLogs_fromBlockExceedToBlock"); + specs.put(EthGetLogs.class, "eth_getLogs_nullParam"); + specs.put(EthGetLogs.class, "eth_getLogs_matchTopic"); + specs.put(EthGetLogs.class, "eth_getLogs_failTopicPosition"); + + specs.put(EthNewFilter.class, "eth_getNewFilter_validFilterLatestBlock"); + specs.put(EthNewFilter.class, "eth_getNewFilter_validFilterWithBlockNumber"); + specs.put(EthNewFilter.class, "eth_getNewFilter_invalidFilter"); + specs.put(EthNewFilter.class, "eth_getNewFilter_emptyFilter"); + specs.put(EthNewFilter.class, "eth_getNewFilter_addressOnly"); + specs.put(EthNewFilter.class, "eth_getNewFilter_topicOnly"); + + specs.put( + EthGetTransactionByBlockHashAndIndex.class, "eth_getTransactionByBlockHashAndIndex_null"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, + "eth_getTransactionByBlockHashAndIndex_intOverflow"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, + "eth_getTransactionByBlockHashAndIndex_wrongParamType"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, + "eth_getTransactionByBlockHashAndIndex_missingParams"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, + "eth_getTransactionByBlockHashAndIndex_missingParam_00"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, + "eth_getTransactionByBlockHashAndIndex_missingParam_01"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, "eth_getTransactionByBlockHashAndIndex_00"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, "eth_getTransactionByBlockHashAndIndex_01"); + specs.put( + EthGetTransactionByBlockHashAndIndex.class, "eth_getTransactionByBlockHashAndIndex_02"); + + specs.put( + EthGetTransactionByBlockNumberAndIndex.class, + "eth_getTransactionByBlockNumberAndIndex_null"); + specs.put( + EthGetTransactionByBlockNumberAndIndex.class, + "eth_getTransactionByBlockNumberAndIndex_latest"); + specs.put( + EthGetTransactionByBlockNumberAndIndex.class, + "eth_getTransactionByBlockNumberAndIndex_earliestNull"); + specs.put( + EthGetTransactionByBlockNumberAndIndex.class, + "eth_getTransactionByBlockNumberAndIndex_pendingNull"); + specs.put( + EthGetTransactionByBlockNumberAndIndex.class, + "eth_getTransactionByBlockNumberAndIndex_invalidParams"); + specs.put( + EthGetTransactionByBlockNumberAndIndex.class, "eth_getTransactionByBlockNumberAndIndex_00"); + specs.put( + EthGetTransactionByBlockNumberAndIndex.class, "eth_getTransactionByBlockNumberAndIndex_01"); + + specs.put( + EthGetBlockTransactionCountByNumber.class, + "eth_getBlockTransactionCountByNumber_invalidParams"); + specs.put( + EthGetBlockTransactionCountByNumber.class, "eth_getBlockTransactionCountByNumber_null"); + specs.put( + EthGetBlockTransactionCountByNumber.class, "eth_getBlockTransactionCountByNumber_earliest"); + specs.put( + EthGetBlockTransactionCountByNumber.class, "eth_getBlockTransactionCountByNumber_latest"); + specs.put(EthGetBlockTransactionCountByNumber.class, "eth_getBlockTransactionCountByNumber_00"); + specs.put( + EthGetBlockTransactionCountByNumber.class, + "eth_getBlockTransactionCountByNumber_illegalRangeGreaterThan"); + specs.put( + EthGetBlockTransactionCountByNumber.class, + "eth_getBlockTransactionCountByNumber_illegalRangeLessThan"); + + specs.put( + EthGetBlockTransactionCountByHash.class, + "eth_getBlockTransactionCountByHash_invalidParams"); + specs.put( + EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_noResult"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_00"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_01"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_02"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_03"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_04"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_05"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_06"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_07"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_08"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_09"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_10"); + specs.put(EthGetBlockTransactionCountByHash.class, "eth_getBlockTransactionCountByHash_11"); + + specs.put(EthGetTransactionCount.class, "eth_getTransactionCount_illegalRange"); + specs.put(EthGetTransactionCount.class, "eth_getTransactionCount_latest"); + specs.put(EthGetTransactionCount.class, "eth_getTransactionCount_earliest"); + specs.put(EthGetTransactionCount.class, "eth_getTransactionCount_blockNumber"); + specs.put(EthGetTransactionCount.class, "eth_getTransactionCount_missingArgument"); + + specs.put(EthGetCode.class, "eth_getCode_illegalRangeLessThan"); + specs.put(EthGetCode.class, "eth_getCode_illegalRangeGreaterThan"); + specs.put(EthGetCode.class, "eth_getCode_success"); + specs.put(EthGetCode.class, "eth_getCode_noCodeNumber"); + specs.put(EthGetCode.class, "eth_getCode_noCodeLatest"); + specs.put(EthGetCode.class, "eth_getCode_invalidParams"); + + specs.put(EthBlockNumber.class, "eth_blockNumber"); + + specs.put(EthCall.class, "eth_call_earliestBlock"); + specs.put(EthCall.class, "eth_call_block_8"); + specs.put(EthCall.class, "eth_call_gasLimitTooLow_block_8"); + specs.put(EthCall.class, "eth_call_gasPriceTooHigh_block_8"); + specs.put(EthCall.class, "eth_call_valueTooHigh_block_8"); + specs.put(EthCall.class, "eth_call_callParamsMissing_block_8"); + specs.put(EthCall.class, "eth_call_toMissing_block_8"); + specs.put(EthCall.class, "eth_call_latestBlock"); + + specs.put(EthNewBlockFilter.class, "eth_newBlockFilter"); + + specs.put(EthNewPendingTransactionFilter.class, "eth_newPendingTransactionFilter"); + + specs.put(EthUninstallFilter.class, "eth_uninstallFilter_NonexistentFilter"); + specs.put(EthUninstallFilter.class, "eth_uninstallFilter_FilterIdTooLong"); + specs.put(EthUninstallFilter.class, "eth_uninstallFilter_FilterIdNegative"); + + specs.put(EthGetFilterChanges.class, "eth_getFilterChanges_NonexistentFilter"); + specs.put(EthGetFilterChanges.class, "eth_getFilterChanges_FilterIdTooLong"); + specs.put(EthGetFilterChanges.class, "eth_getFilterChanges_FilterIdNegative"); + + specs.put(EthSendRawTransaction.class, "eth_sendRawTransaction_transferEther"); + specs.put(EthSendRawTransaction.class, "eth_sendRawTransaction_contractCreation"); + specs.put(EthSendRawTransaction.class, "eth_sendRawTransaction_messageCall"); + specs.put(EthSendRawTransaction.class, "eth_sendRawTransaction_invalidByteValueHex"); + specs.put(EthSendRawTransaction.class, "eth_sendRawTransaction_invalidRawTransaction"); + specs.put(EthSendRawTransaction.class, "eth_sendRawTransaction_unsignedTransaction"); + + specs.put(EthEstimateGas.class, "eth_estimateGas_contractDeploy"); + specs.put(EthEstimateGas.class, "eth_estimateGas_transfer"); + specs.put(EthEstimateGas.class, "eth_estimateGas_noParams"); + specs.put(EthEstimateGas.class, "eth_estimateGas_insufficientGas"); + + return specs.values(); + } + // @formatter:on + + @Test + public void jsonRPCCallWithSpecFile() throws Exception { + jsonRPCCall(specFileName); + } + + private void jsonRPCCall(final String name) throws IOException { + final String testSpecFile = name + ".json"; + final String json = + Resources.toString( + EthJsonRpcHttpBySpecTest.class.getResource(testSpecFile), Charsets.UTF_8); + final JsonObject spec = new JsonObject(json); + + final String rawRequestBody = spec.getJsonObject("request").toString(); + final RequestBody requestBody = RequestBody.create(JSON, rawRequestBody); + final Request request = new Request.Builder().post(requestBody).url(baseUrl).build(); + + importBlocks(1, BLOCKS.size()); + try (Response resp = client.newCall(request).execute()) { + final int expectedStatusCode = spec.getInteger("statusCode"); + assertThat(resp.code()).isEqualTo(expectedStatusCode); + + final String expectedRespBody = spec.getJsonObject("response").encodePrettily(); + assertThat(resp.body().string()).isEqualTo(expectedRespBody); + } + } + + private void importBlocks(final int from, final int to) { + for (int i = from; i < to; ++i) { + importBlock(i); + } + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcConfigurationTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcConfigurationTest.java new file mode 100755 index 00000000000..47318c5ba11 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcConfigurationTest.java @@ -0,0 +1,57 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class JsonRpcConfigurationTest { + + @Test + public void defaultConfiguration() { + final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault(); + + assertThat(configuration.isEnabled()).isFalse(); + assertThat(configuration.getHost()).isEqualTo("127.0.0.1"); + assertThat(configuration.getPort()).isEqualTo(8545); + assertThat(configuration.getCorsAllowedDomains()).isEmpty(); + assertThat(configuration.getRpcApis()) + .containsExactlyInAnyOrder(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + } + + @Test + public void corsAllowedOriginsDefaultShouldBeEmptyList() { + final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault(); + assertThat(configuration.getCorsAllowedDomains()).isEmpty(); + } + + @Test + public void rpcApiDefaultShouldBePredefinedList() { + final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault(); + assertThat(configuration.getRpcApis()).containsExactly(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + } + + @Test + public void settingCorsAllowedOriginsShouldOverridePreviousValues() { + final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault(); + + configuration.setCorsAllowedDomains(Lists.newArrayList("foo", "bar")); + assertThat(configuration.getCorsAllowedDomains()).containsExactly("foo", "bar"); + + configuration.setCorsAllowedDomains(Lists.newArrayList("zap")); + assertThat(configuration.getCorsAllowedDomains()).containsExactly("zap"); + } + + @Test + public void settingRpcApisShouldOverridePreviousValues() { + final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault(); + + configuration.setRpcApis(Lists.newArrayList(RpcApis.ETH, RpcApis.MINER)); + assertThat(configuration.getRpcApis()).containsExactly(RpcApis.ETH, RpcApis.MINER); + + configuration.setRpcApis(Lists.newArrayList(RpcApis.DEBUG)); + assertThat(configuration.getRpcApis()).containsExactly(RpcApis.DEBUG); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java new file mode 100755 index 00000000000..87ddf578c2b --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceCorsTest.java @@ -0,0 +1,161 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; + +import com.google.common.collect.Lists; +import io.vertx.core.Vertx; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Request.Builder; +import okhttp3.Response; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class JsonRpcHttpServiceCorsTest { + + private final Vertx vertx = Vertx.vertx(); + private final OkHttpClient client = new OkHttpClient(); + private JsonRpcHttpService jsonRpcHttpService; + + @Before + public void before() { + final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault(); + configuration.setPort(0); + } + + @After + public void after() { + jsonRpcHttpService.stop().join(); + } + + @Test + public void requestWithNonAcceptedOriginShouldFail() throws Exception { + jsonRpcHttpService = createJsonRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = + new Builder().url(jsonRpcHttpService.url()).header("Origin", "http://bar.me").build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isFalse(); + } + } + + @Test + public void requestWithAcceptedOriginShouldSucceed() throws Exception { + jsonRpcHttpService = createJsonRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = + new Builder().url(jsonRpcHttpService.url()).header("Origin", "http://foo.io").build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithOneOfMultipleAcceptedOriginsShouldSucceed() throws Exception { + jsonRpcHttpService = + createJsonRpcHttpServiceWithAllowedDomains("http://foo.io", "http://bar.me"); + + final Request request = + new Builder().url(jsonRpcHttpService.url()).header("Origin", "http://bar.me").build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithNoneOfMultipleAcceptedOriginsShouldFail() throws Exception { + jsonRpcHttpService = + createJsonRpcHttpServiceWithAllowedDomains("http://foo.io", "http://bar.me"); + + final Request request = + new Builder().url(jsonRpcHttpService.url()).header("Origin", "http://hel.lo").build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isFalse(); + } + } + + @Test + public void requestWithNoOriginShouldSucceedWhenNoCorsConfigSet() throws Exception { + jsonRpcHttpService = createJsonRpcHttpServiceWithAllowedDomains(); + + final Request request = new Builder().url(jsonRpcHttpService.url()).build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithNoOriginShouldSucceedWhenCorsIsSet() throws Exception { + jsonRpcHttpService = createJsonRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = new Builder().url(jsonRpcHttpService.url()).build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithAnyOriginShouldNotSucceedWhenCorsIsEmpty() throws Exception { + jsonRpcHttpService = createJsonRpcHttpServiceWithAllowedDomains(""); + + final Request request = + new Builder().url(jsonRpcHttpService.url()).header("Origin", "http://bar.me").build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isFalse(); + } + } + + @Test + public void requestWithAnyOriginShouldSucceedWhenCorsIsStart() throws Exception { + jsonRpcHttpService = createJsonRpcHttpServiceWithAllowedDomains("*"); + + final Request request = + new Builder().url(jsonRpcHttpService.url()).header("Origin", "http://bar.me").build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithAccessControlRequestMethodShouldReturnAllowedHeaders() throws Exception { + jsonRpcHttpService = createJsonRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = + new Builder() + .url(jsonRpcHttpService.url()) + .method("OPTIONS", null) + .header("Access-Control-Request-Method", "OPTIONS") + .header("Origin", "http://foo.io") + .build(); + + try (Response response = client.newCall(request).execute()) { + assertThat(response.header("Access-Control-Allow-Headers")).contains("*", "content-type"); + } + } + + private JsonRpcHttpService createJsonRpcHttpServiceWithAllowedDomains( + final String... corsAllowedDomains) { + final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); + config.setPort(0); + if (corsAllowedDomains != null) { + config.setCorsAllowedDomains(Lists.newArrayList(corsAllowedDomains)); + } + + final JsonRpcHttpService jsonRpcHttpService = + new JsonRpcHttpService(vertx, config, new HashMap<>()); + jsonRpcHttpService.start().join(); + + return jsonRpcHttpService; + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java new file mode 100755 index 00000000000..d0d09dd5301 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceRpcApisTest.java @@ -0,0 +1,174 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Lists; +import io.vertx.core.Vertx; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class JsonRpcHttpServiceRpcApisTest { + + private final Vertx vertx = Vertx.vertx(); + private final OkHttpClient client = new OkHttpClient(); + private JsonRpcHttpService service; + private static String baseUrl; + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private static final String CLIENT_VERSION = "TestClientVersion/0.1.0"; + private static final String NET_VERSION = "6986785976597"; + private JsonRpcConfiguration configuration; + + @Mock protected static BlockchainQueries blockchainQueries; + + private final JsonRpcTestHelper testHelper = new JsonRpcTestHelper(); + + @Before + public void before() { + configuration = JsonRpcConfiguration.createDefault(); + configuration.setPort(0); + } + + @After + public void after() { + service.stop().join(); + } + + @Test + public void requestWithNetMethodShouldSucceedWhenDefaultApisEnabled() throws Exception { + service = createJsonRpcHttpServiceWithRpcApis(configuration); + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + } + } + + @Test + public void requestWithNetMethodShouldSucceedWhenNetApiIsEnabled() throws Exception { + service = createJsonRpcHttpServiceWithRpcApis(RpcApis.NET); + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + } + } + + @Test + public void requestWithNetMethodShouldFailWhenNetApiIsNotEnabled() throws Exception { + service = createJsonRpcHttpServiceWithRpcApis(RpcApis.WEB3); + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.METHOD_NOT_FOUND; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void requestWithNetMethodShouldSucceedWhenNetApiAndOtherIsEnabled() throws Exception { + service = createJsonRpcHttpServiceWithRpcApis(RpcApis.NET, RpcApis.WEB3); + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + } + } + + private JsonRpcConfiguration createJsonRpcConfigurationWithRpcApis(final RpcApis... rpcApis) { + final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); + config.setCorsAllowedDomains(singletonList("*")); + config.setPort(0); + if (rpcApis != null) { + config.setRpcApis(Lists.newArrayList(rpcApis)); + } + return config; + } + + private JsonRpcHttpService createJsonRpcHttpServiceWithRpcApis(final RpcApis... rpcApis) { + return createJsonRpcHttpServiceWithRpcApis(createJsonRpcConfigurationWithRpcApis(rpcApis)); + } + + private JsonRpcHttpService createJsonRpcHttpServiceWithRpcApis( + final JsonRpcConfiguration config) { + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + + final Map rpcMethods = + spy( + new JsonRpcMethodsFactory() + .methods( + CLIENT_VERSION, + NET_VERSION, + mock(P2PNetwork.class), + blockchainQueries, + mock(Synchronizer.class), + MainnetProtocolSchedule.create(), + mock(FilterManager.class), + mock(TransactionPool.class), + mock(MiningCoordinator.class), + supportedCapabilities, + config.getRpcApis())); + final JsonRpcHttpService jsonRpcHttpService = new JsonRpcHttpService(vertx, config, rpcMethods); + jsonRpcHttpService.start().join(); + + baseUrl = jsonRpcHttpService.url(); + return jsonRpcHttpService; + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java new file mode 100755 index 00000000000..c7f34d2f84a --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcHttpServiceTest.java @@ -0,0 +1,2037 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.LogsBloomFilter; +import net.consensys.pantheon.ethereum.core.SyncStatus; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.uint.UInt256; + +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import io.vertx.core.Vertx; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +public class JsonRpcHttpServiceTest { + + private static final Vertx vertx = Vertx.vertx(); + + protected static Map rpcMethods; + protected static JsonRpcHttpService service; + protected static OkHttpClient client; + protected static String baseUrl; + protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + protected static final String CLIENT_VERSION = "TestClientVersion/0.1.0"; + protected static final String NET_VERSION = "6986785976597"; + protected static P2PNetwork peerDiscoveryMock; + protected static BlockchainQueries blockchainQueries; + protected static Synchronizer synchronizer; + protected static final Collection JSON_RPC_APIS = + Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + protected final JsonRpcTestHelper testHelper = new JsonRpcTestHelper(); + + @BeforeClass + public static void initServerAndClient() { + peerDiscoveryMock = mock(P2PNetwork.class); + blockchainQueries = mock(BlockchainQueries.class); + synchronizer = mock(Synchronizer.class); + + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + + rpcMethods = + spy( + new JsonRpcMethodsFactory() + .methods( + CLIENT_VERSION, + NET_VERSION, + peerDiscoveryMock, + blockchainQueries, + synchronizer, + MainnetProtocolSchedule.create(), + mock(FilterManager.class), + mock(TransactionPool.class), + mock(MiningCoordinator.class), + supportedCapabilities, + JSON_RPC_APIS)); + service = createJsonRpcHttpService(); + service.start().join(); + + // Build an OkHttp client. + client = new OkHttpClient(); + baseUrl = service.url(); + } + + protected static JsonRpcHttpService createJsonRpcHttpService(final JsonRpcConfiguration config) { + return new JsonRpcHttpService(vertx, config, rpcMethods); + } + + protected static JsonRpcHttpService createJsonRpcHttpService() { + return new JsonRpcHttpService(vertx, createJsonRpcConfig(), rpcMethods); + } + + protected static JsonRpcConfiguration createJsonRpcConfig() { + final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault(); + config.setPort(0); + return config; + } + + /** Tears down the HTTP server. */ + @AfterClass + public static void shutdownServer() { + service.stop().join(); + } + + @Test + public void invalidCallToStart() { + service + .start() + .whenComplete( + (unused, exception) -> { + assertThat(exception).isInstanceOf(IllegalStateException.class); + }); + } + + @Test + public void http404() throws Exception { + final Request request = new Request.Builder().get().url(baseUrl + "/foo").build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(404); + } + } + + @Test + public void handleEmptyRequest() throws Exception { + final Request request = new Request.Builder().get().url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(201); + } + } + + @Test + public void getSocketAddressWhenActive() { + final InetSocketAddress socketAddress = service.socketAddress(); + assertThat("127.0.0.1").isEqualTo(socketAddress.getAddress().getHostAddress()); + assertThat(socketAddress.getPort() > 0).isTrue(); + } + + @Test + public void getSocketAddressWhenStoppedIsEmpty() { + final JsonRpcHttpService service = createJsonRpcHttpService(); + + final InetSocketAddress socketAddress = service.socketAddress(); + assertThat("0.0.0.0").isEqualTo(socketAddress.getAddress().getHostAddress()); + assertThat(0).isEqualTo(socketAddress.getPort()); + assertThat("").isEqualTo(service.url()); + } + + @Test + public void getSocketAddressWhenBindingToAllInterfaces() { + final JsonRpcConfiguration config = createJsonRpcConfig(); + config.setHost("0.0.0.0"); + final JsonRpcHttpService service = createJsonRpcHttpService(config); + service.start().join(); + + try { + final InetSocketAddress socketAddress = service.socketAddress(); + assertThat("0.0.0.0").isEqualTo(socketAddress.getAddress().getHostAddress()); + assertThat(socketAddress.getPort() > 0).isTrue(); + assertThat(!service.url().contains("0.0.0.0")).isTrue(); + } finally { + service.stop().join(); + } + } + + @Test + public void responseContainsJsonContentTypeHeader() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.header("Content-Type")).isEqualTo("application/json"); + } + } + + @Test + public void web3ClientVersionSuccessful() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void netVersionSuccessful() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_version\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(NET_VERSION); + } + } + + @Test + public void ethAccountsSuccessful() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"eth_accounts\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonArray result = json.getJsonArray("result"); + assertThat(result.size()).isEqualTo(0); + } + } + + @Test + public void netPeerCountSuccessful() throws Exception { + when(peerDiscoveryMock.getPeers()).thenReturn(Arrays.asList(null, null, null)); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_peerCount\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String expectedResult = "0x3"; + assertThat(json.getString("result")).isEqualTo(expectedResult); + } + } + + @Test + public void ethGetUncleCountByBlockHash() throws Exception { + final int uncleCount = 2; + final String number = "0x567"; + final Hash blockHash = Hash.hash(BytesValue.of(1)); + when(blockchainQueries.getOmmerCount(eq(blockHash))).thenReturn(Optional.of(uncleCount)); + + final String id = "123"; + final String params = "\"params\": [\"" + blockHash + "\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockHash\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String jsonStr = resp.body().string(); + final JsonObject json = new JsonObject(jsonStr); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String expectedResult = "0x2"; + assertThat(json.getString("result")).isEqualTo(expectedResult); + } + } + + @Test + public void ethGetUncleCountByBlockHashNoData() throws Exception { + final String number = "0x567"; + final Hash blockHash = Hash.hash(BytesValue.of(1)); + when(blockchainQueries.getOmmerCount(eq(blockHash))).thenReturn(Optional.empty()); + + final String id = "123"; + final String params = "\"params\": [\"" + blockHash + "\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockHash\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String jsonStr = resp.body().string(); + final JsonObject json = new JsonObject(jsonStr); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + assertThat(json.getString("result")).isNull(); + } + } + + @Test + public void ethGetUncleCountByBlockNumber() throws Exception { + final int uncleCount = 2; + final String number = "0x567"; + final long blockNumber = Long.decode(number); + when(blockchainQueries.getOmmerCount(eq(blockNumber))).thenReturn(Optional.of(uncleCount)); + + final String id = "123"; + final String params = "\"params\": [\"" + number + "\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockNumber\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String expectedResult = "0x2"; + assertThat(json.getString("result")).isEqualTo(expectedResult); + } + } + + @Test + public void ethGetUncleCountByBlockNumberNoData() throws Exception { + final String number = "0x567"; + final long blockNumber = Long.decode(number); + when(blockchainQueries.getOmmerCount(eq(blockNumber))).thenReturn(Optional.empty()); + + final String id = "123"; + final String params = "\"params\": [\"" + number + "\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockNumber\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + assertThat(json.getString("result")).isNull(); + } + } + + @Test + public void ethGetUncleCountByBlockNumberEarliest() throws Exception { + final int uncleCount = 2; + when(blockchainQueries.getOmmerCount(eq(BlockHeader.GENESIS_BLOCK_NUMBER))) + .thenReturn(Optional.of(uncleCount)); + + final String id = "123"; + final String params = "\"params\": [\"earliest\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockNumber\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String expectedResult = "0x2"; + assertThat(json.getString("result")).isEqualTo(expectedResult); + } + } + + @Test + public void ethGetUncleCountByBlockNumberLatest() throws Exception { + final int uncleCount = 0; + when(blockchainQueries.headBlockNumber()).thenReturn(0L); + when(blockchainQueries.getOmmerCount(eq(0L))).thenReturn(Optional.of(uncleCount)); + + final String id = "123"; + final String params = "\"params\": [\"latest\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockNumber\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String expectedResult = "0x0"; + assertThat(json.getString("result")).isEqualTo(expectedResult); + } + } + + @Test + public void ethGetUncleCountByBlockNumberPending() throws Exception { + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block pending = gen.block(); + + final String id = "123"; + final String params = "\"params\": [\"pending\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockNumber\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + assertThat(json.getString("result")).isNull(); + } + } + + @Test + public void ethGetUncleCountByBlockNumberPendingNoData() throws Exception { + final String id = "123"; + final String params = "\"params\": [\"pending\"]"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + "," + + params + + ",\"method\":\"eth_getUncleCountByBlockNumber\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + assertThat(json.getString("result")).isNull(); + } + } + + @Test + public void netPeerCountOfZero() throws Exception { + when(peerDiscoveryMock.getPeers()).thenReturn(Collections.emptyList()); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"net_peerCount\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String expectedResult = "0x0"; + assertThat(json.getString("result")).isEqualTo(expectedResult); + } + } + + @Test + public void getBalanceForLatest() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final String mockBalance = "0x35"; + when(blockchainQueries.headBlockNumber()).thenReturn(0L); + when(blockchainQueries.accountBalance(eq(address), eq(0L))) + .thenReturn(Optional.of(Wei.fromHexString(mockBalance))); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBalance\", \"params\": [\"" + + address + + "\",\"latest\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(mockBalance).isEqualTo(result); + } + } + + @Test + public void getBalanceForLatestWithZeroBalance() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final Wei mockBalance = Wei.of(0); + when(blockchainQueries.headBlockNumber()).thenReturn(0L); + when(blockchainQueries.accountBalance(eq(address), eq(0L))) + .thenReturn(Optional.of(mockBalance)); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBalance\", \"params\": [\"" + + address + + "\",\"latest\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat("0x0").isEqualTo(result); + } + } + + @Test + public void getBalanceForEarliest() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final String mockBalance = "0x33"; + when(blockchainQueries.accountBalance(eq(address), eq(0L))) + .thenReturn(Optional.of(Wei.fromHexString(mockBalance))); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBalance\", \"params\": [\"" + + address + + "\",\"earliest\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(mockBalance).isEqualTo(result); + } + } + + @Test + public void getBalanceByBlockNumber() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final String mockBalance = "0x32"; + final long blockNumber = 13L; + when(blockchainQueries.accountBalance(eq(address), eq(blockNumber))) + .thenReturn(Optional.of(Wei.fromHexString(mockBalance))); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBalance\", \"params\": [\"" + + address + + "\",\"0x" + + Long.toString(blockNumber, 16) + + "\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(mockBalance).isEqualTo(result); + } + } + + @Test + public void getBlockByHashForUnknownBlock() throws Exception { + final String id = "123"; + final String blockHashString = + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273321"; + final Hash blockHash = Hash.fromHexString(blockHashString); + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [\"" + + blockHashString + + "\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + // Setup mocks + when(blockchainQueries.blockByHash(eq(blockHash))).thenReturn(Optional.empty()); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final Object result = json.getValue("result"); + // For now, no block will be returned so we should get null + assertThat(result).isNull(); + } + } + + @Test + public void getBlockByHashWithTransactions() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block block = gen.block(); + final BlockWithMetadata blockWMetadata = + blockWithMetadata(block); + final Hash blockHash = block.getHeader().getHash(); + when(blockchainQueries.blockByHash(eq(blockHash))).thenReturn(Optional.of(blockWMetadata)); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [\"" + + blockHash + + "\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + verifyBlockResult(block, blockWMetadata.getTotalDifficulty(), result, false); + } + } + + @Test + public void getBlockByHashWithTransactionHashes() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block block = gen.block(); + final BlockWithMetadata blockWMetadata = blockWithMetadataAndTxHashes(block); + final Hash blockHash = block.getHeader().getHash(); + when(blockchainQueries.blockByHashWithTxHashes(eq(blockHash))) + .thenReturn(Optional.of(blockWMetadata)); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [\"" + + blockHash + + "\",false]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + verifyBlockResult(block, blockWMetadata.getTotalDifficulty(), result, true); + } + } + + @Test + public void getBlockByHashWithMissingHashParameter() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + // Setup mocks + when(blockchainQueries.blockByHash(ArgumentMatchers.isA(Hash.class))) + .thenReturn(Optional.empty()); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByHashWithMissingBooleanParameter() throws Exception { + final String id = "123"; + final String blockHashString = + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273321"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [\"" + + blockHashString + + "\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByHashWithInvalidHashParameterWithOddLength() throws Exception { + final String id = "123"; + final String blockHashString = "0xe"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [\"" + + blockHashString + + "\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByHashWithInvalidHashParameterThatIsTooShort() throws Exception { + final String id = "123"; + final String blockHashString = "0xe670"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [\"" + + blockHashString + + "\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByHashWithInvalidBooleanParameter() throws Exception { + final String id = "123"; + final String blockHashString = + "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273321"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": [\"" + + blockHashString + + "\",{}]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByHashWithAllParametersMissing() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\", \"params\": []}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByHashWithNoParameters() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByHash\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByNumberWithTransactions() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block block = gen.block(); + final BlockWithMetadata blockWithMetadata = + blockWithMetadata(block); + final long number = block.getHeader().getNumber(); + when(blockchainQueries.blockByNumber(eq(number))).thenReturn(Optional.of(blockWithMetadata)); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"0x" + + Long.toString(number, 16) + + "\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + verifyBlockResult(block, blockWithMetadata.getTotalDifficulty(), result, false); + } + } + + @Test + public void getBlockByNumberWithTransactionHashes() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block block = gen.block(); + final BlockWithMetadata blockWithMetadata = blockWithMetadataAndTxHashes(block); + final long number = block.getHeader().getNumber(); + when(blockchainQueries.blockByNumberWithTxHashes(eq(number))) + .thenReturn(Optional.of(blockWithMetadata)); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"0x" + + Long.toString(number, 16) + + "\",false]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + verifyBlockResult(block, blockWithMetadata.getTotalDifficulty(), result, true); + } + } + + @Test + public void getBlockByNumberForInvalidBlockParameter() throws Exception { + final String id = "123"; + + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"bla\",false]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void getBlockByNumberForEarliest() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block block = gen.genesisBlock(); + final BlockWithMetadata blockWithMetadata = + blockWithMetadata(block); + when(blockchainQueries.blockByNumber(eq(BlockHeader.GENESIS_BLOCK_NUMBER))) + .thenReturn(Optional.of(blockWithMetadata)); + + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"earliest\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + verifyBlockResult(block, blockWithMetadata.getTotalDifficulty(), result, false); + } + } + + @Test + public void getBlockByNumberForBlockNumberZero() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"0x0\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block block = gen.genesisBlock(); + final BlockWithMetadata blockWithMetadata = + blockWithMetadata(block); + when(blockchainQueries.blockByNumber(eq(0L))).thenReturn(Optional.of(blockWithMetadata)); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + verifyBlockResult(block, blockWithMetadata.getTotalDifficulty(), result, false); + } + } + + @Test + public void getBlockByNumberForLatest() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"latest\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Block block = gen.genesisBlock(); + final BlockWithMetadata blockWithMetadata = + blockWithMetadata(block); + when(blockchainQueries.headBlockNumber()).thenReturn(0L); + when(blockchainQueries.blockByNumber(eq(0L))).thenReturn(Optional.of(blockWithMetadata)); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + verifyBlockResult(block, blockWithMetadata.getTotalDifficulty(), result, false); + } + } + + @Test + public void getBlockByNumberForPending() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getBlockByNumber\", \"params\": [\"pending\",true]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final JsonObject result = json.getJsonObject("result"); + assertThat(result).isNull(); + } + } + + @Test + public void extraneousParameters() throws Exception { + final String id = "123"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\", \"params\": [1,2,3]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void requestMissingVersionFieldShouldSucceed() throws Exception { + final String id = "456"; + final RequestBody body = + RequestBody.create( + JSON, "{\"id\":" + Json.encode(id) + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void notification() throws Exception { + // No id field is present - marking this as a notification + final RequestBody body = + RequestBody.create(JSON, "{\"jsonrpc\":\"2.0\",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + // Notifications return an empty response + assertThat(resp.code()).isEqualTo(200); + final String resBody = resp.body().string(); + assertThat(resBody).isEqualTo(""); + } + } + + @Test + public void nullId() throws Exception { + // Be lenient - allow explicit null id fields + final RequestBody body = + RequestBody.create( + JSON, "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, null); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void emptyStringIdField() throws Exception { + final String id = ""; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + // An empty string is still a string, so should be a valid id + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void negativeNumericId() throws Exception { + final int id = -1; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void largeNumericId() throws Exception { + final BigInteger id = + new BigInteger( + "234567899875432345679098765323457892345678998754323456790987653234578923456789987543234567909876532345789"); + + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void largeStringId() throws Exception { + final StringBuilder idBuilder = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + idBuilder.append(i); + } + final String id = idBuilder.toString(); + + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void fractionalNumericId() throws Exception { + final double id = 1.5; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void objectId() throws Exception { + final RequestBody body = + RequestBody.create( + JSON, "{\"jsonrpc\":\"2.0\",\"id\":{},\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_REQUEST; + testHelper.assertValidJsonRpcError( + json, null, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void arrayId() throws Exception { + final RequestBody body = + RequestBody.create( + JSON, "{\"jsonrpc\":\"2.0\",\"id\":[],\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_REQUEST; + testHelper.assertValidJsonRpcError( + json, null, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void missingMethodField() throws Exception { + final Integer id = 2; + final RequestBody body = + RequestBody.create(JSON, "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + "}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_REQUEST; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void invalidJson() throws Exception { + final RequestBody body = RequestBody.create(JSON, "{bla"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.PARSE_ERROR; + testHelper.assertValidJsonRpcError( + json, null, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void wrongJsonType() throws Exception { + final RequestBody body = RequestBody.create(JSON, "\"a string\""); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.PARSE_ERROR; + testHelper.assertValidJsonRpcError( + json, null, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void requestWithWrongVersionShouldSucceed() throws Exception { + final String id = "234"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"1.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"web3_clientVersion\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + final String result = json.getString("result"); + assertThat(result).isEqualTo(CLIENT_VERSION); + } + } + + @Test + public void unknownMethod() throws Exception { + final String id = "234"; + final RequestBody body = + RequestBody.create( + JSON, "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"bla\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.METHOD_NOT_FOUND; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void exceptionallyHandleJsonSingleRequest() throws Exception { + final JsonRpcMethod jsonRpcMethod = mock(JsonRpcMethod.class); + when(jsonRpcMethod.getName()).thenReturn("foo"); + when(jsonRpcMethod.response(ArgumentMatchers.any())) + .thenThrow(new RuntimeException("test exception")); + + doReturn(Optional.of(jsonRpcMethod)).when(rpcMethods).get("foo"); + + final RequestBody body = + RequestBody.create(JSON, "{\"jsonrpc\":\"2.0\",\"id\":\"666\",\"method\":\"foo\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(500); + } + } + + @Test + public void exceptionallyHandleJsonBatchRequest() throws Exception { + final JsonRpcMethod jsonRpcMethod = mock(JsonRpcMethod.class); + when(jsonRpcMethod.getName()).thenReturn("foo"); + when(jsonRpcMethod.response(ArgumentMatchers.any())) + .thenThrow(new RuntimeException("test exception")); + doReturn(Optional.of(jsonRpcMethod)).when(rpcMethods).get("foo"); + + final RequestBody body = + RequestBody.create( + JSON, + "[{\"jsonrpc\":\"2.0\",\"id\":\"000\",\"method\":\"web3_clientVersion\"}," + + "{\"jsonrpc\":\"2.0\",\"id\":\"111\",\"method\":\"foo\"}," + + "{\"jsonrpc\":\"2.0\",\"id\":\"222\",\"method\":\"net_version\"}]"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(500); + } + } + + @Test + public void batchRequest() throws Exception { + final int clientVersionRequestId = 2; + final int brokenRequestId = 3; + final int netVersionRequestId = 4; + final RequestBody body = + RequestBody.create( + JSON, + "[{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(clientVersionRequestId) + + ",\"method\":\"web3_clientVersion\"}," + + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(brokenRequestId) + + ",\"method\":\"bla\"}," + + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(netVersionRequestId) + + ",\"method\":\"net_version\"}]"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonArray json = new JsonArray(resp.body().string()); + final int requestCount = 3; + assertThat(json.size()).isEqualTo(requestCount); + final Map responses = new HashMap<>(); + for (int i = 0; i < requestCount; ++i) { + final JsonObject response = json.getJsonObject(i); + responses.put(response.getInteger("id"), response); + } + + // Check result web3_clientVersion + final JsonObject jsonClientVersion = responses.get(clientVersionRequestId); + testHelper.assertValidJsonRpcResult(jsonClientVersion, clientVersionRequestId); + assertThat(jsonClientVersion.getString("result")).isEqualTo(CLIENT_VERSION); + + // Check result unknown method + final JsonObject jsonError = responses.get(brokenRequestId); + final JsonRpcError expectedError = JsonRpcError.METHOD_NOT_FOUND; + testHelper.assertValidJsonRpcError( + jsonError, brokenRequestId, expectedError.getCode(), expectedError.getMessage()); + + // Check result net_version + final JsonObject jsonNetVersion = responses.get(netVersionRequestId); + testHelper.assertValidJsonRpcResult(jsonNetVersion, netVersionRequestId); + assertThat(jsonNetVersion.getString("result")).isEqualTo(NET_VERSION); + } + } + + @Test + public void batchRequestContainingInvalidRequest() throws Exception { + final int clientVersionRequestId = 2; + final int invalidId = 3; + final int netVersionRequestId = 4; + final String[] reqs = new String[3]; + reqs[0] = + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(clientVersionRequestId) + + ",\"method\":\"web3_clientVersion\"}"; + reqs[1] = "5"; + reqs[2] = + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(netVersionRequestId) + + ",\"method\":\"net_version\"}"; + final String batchRequest = "[" + String.join(", ", reqs) + "]"; + final RequestBody body = RequestBody.create(JSON, batchRequest); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String jsonStr = resp.body().string(); + final JsonArray json = new JsonArray(jsonStr); + final int requestCount = 3; + assertThat(json.size()).isEqualTo(requestCount); + + // Organize results for inspection + final Map responses = new HashMap<>(); + for (int i = 0; i < requestCount; ++i) { + final JsonObject response = json.getJsonObject(i); + Integer identifier = response.getInteger("id"); + if (identifier == null) { + identifier = invalidId; + } + responses.put(identifier, response); + } + + // Check result web3_clientVersion + final JsonObject jsonClientVersion = responses.get(clientVersionRequestId); + testHelper.assertValidJsonRpcResult(jsonClientVersion, clientVersionRequestId); + assertThat(jsonClientVersion.getString("result")).isEqualTo(CLIENT_VERSION); + + // Check invalid request + final JsonObject jsonError = responses.get(invalidId); + final JsonRpcError expectedError = JsonRpcError.INVALID_REQUEST; + testHelper.assertValidJsonRpcError( + jsonError, null, expectedError.getCode(), expectedError.getMessage()); + + // Check result net_version + final JsonObject jsonNetVersion = responses.get(netVersionRequestId); + testHelper.assertValidJsonRpcResult(jsonNetVersion, netVersionRequestId); + assertThat(jsonNetVersion.getString("result")).isEqualTo(NET_VERSION); + } + } + + @Test + public void batchRequestParseError() throws Exception { + final String req = + "[\n" + + " {\"jsonrpc\": \"2.0\", \"method\": \"net_version\", \"id\": \"1\"},\n" + + " {\"jsonrpc\": \"2.0\", \"method\"\n" + + "]"; + + final RequestBody body = RequestBody.create(JSON, req); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.PARSE_ERROR; + testHelper.assertValidJsonRpcError( + json, null, expectedError.getCode(), expectedError.getMessage()); + } + } + + @Test + public void batchRequestWithNotifications() throws Exception { + final int clientVersionRequestId = 2; + final int netVersionRequestId = 3; + final RequestBody body = + RequestBody.create( + JSON, + "[{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(clientVersionRequestId) + + ",\"method\":\"web3_clientVersion\"}," + + "{\"jsonrpc\":\"2.0\", \"method\":\"web3_clientVersion\"}," + + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(netVersionRequestId) + + ",\"method\":\"net_version\"}]"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final JsonArray json = new JsonArray(resp.body().string()); + // 2 Responses since the notification is ignored + final int responseCount = 2; + assertThat(json.size()).isEqualTo(responseCount); + final Map responses = new HashMap<>(); + for (int i = 0; i < responseCount; ++i) { + final JsonObject response = json.getJsonObject(i); + responses.put(response.getInteger("id"), response); + } + + // Check result web3_clientVersion + final JsonObject jsonClientVersion = responses.get(clientVersionRequestId); + testHelper.assertValidJsonRpcResult(jsonClientVersion, clientVersionRequestId); + assertThat(jsonClientVersion.getString("result")).isEqualTo(CLIENT_VERSION); + + // Check result net_version + final JsonObject jsonNetVersion = responses.get(netVersionRequestId); + testHelper.assertValidJsonRpcResult(jsonNetVersion, netVersionRequestId); + assertThat(jsonNetVersion.getString("result")).isEqualTo(NET_VERSION); + } + } + + /** + * Tests that empty batch requests are treated as invalid requests as per + * http://www.jsonrpc.org/specification#batch. + */ + @Test + public void emptyBatchRequest() throws Exception { + final RequestBody body = RequestBody.create(JSON, "[]"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_REQUEST; + testHelper.assertValidJsonRpcError( + json, null, expectedError.getCode(), expectedError.getMessage()); + } + } + + private void verifyBlockResult( + final Block block, + final UInt256 td, + final JsonObject result, + final boolean shouldTransactionsBeHashed) { + assertBlockResultMatchesBlock(result, block); + + if (td == null) { + assertThat(result.getJsonObject("totalDifficulty")).isNull(); + } else { + assertThat(UInt256.fromHexString(result.getString("totalDifficulty"))).isEqualTo(td); + } + + // Check ommers + final JsonArray ommersResult = result.getJsonArray("uncles"); + assertThat(ommersResult.size()).isEqualTo(block.getBody().getOmmers().size()); + for (int i = 0; i < block.getBody().getOmmers().size(); i++) { + final BlockHeader ommer = block.getBody().getOmmers().get(i); + final Hash ommerHash = ommer.getHash(); + assertThat(Hash.fromHexString(ommersResult.getString(i))).isEqualTo(ommerHash); + } + + // Check transactions + final JsonArray transactionsResult = result.getJsonArray("transactions"); + assertThat(transactionsResult.size()).isEqualTo(block.getBody().getTransactions().size()); + for (int i = 0; i < block.getBody().getTransactions().size(); i++) { + final Transaction transaction = block.getBody().getTransactions().get(i); + if (shouldTransactionsBeHashed) { + assertThat(Hash.fromHexString(transactionsResult.getString(i))) + .isEqualTo(transaction.hash()); + } else { + final JsonObject transactionResult = transactionsResult.getJsonObject(i); + final Integer expectedIndex = i; + final Hash expectedBlockHash = block.getHeader().getHash(); + final long expectedBlockNumber = block.getHeader().getNumber(); + assertTransactionResultMatchesTransaction( + transactionResult, transaction, expectedIndex, expectedBlockHash, expectedBlockNumber); + } + } + } + + private void assertTransactionResultMatchesTransaction( + final JsonObject result, + final Transaction transaction, + final Integer index, + final Hash blockHash, + final Long blockNumber) { + assertThat(Hash.fromHexString(result.getString("hash"))).isEqualTo(transaction.hash()); + assertThat(Long.decode(result.getString("nonce"))).isEqualByComparingTo(transaction.getNonce()); + if (blockHash != null) { + assertThat(Hash.fromHexString(result.getString("blockHash"))).isEqualTo(blockHash); + } else { + assertThat(result.getValue("blockHash")).isNull(); + } + if (blockNumber != null) { + assertThat(Long.decode(result.getString("blockNumber"))).isEqualTo(blockNumber); + } else { + assertThat(result.getValue("blockNumber")).isNull(); + } + if (index != null) { + assertThat(UInt256.fromHexString(result.getString("transactionIndex")).toInt()) + .isEqualTo(index); + } else { + assertThat(result.getValue("transactionIndex")).isNull(); + } + assertThat(Address.fromHexString(result.getString("from"))).isEqualTo(transaction.getSender()); + if (transaction.getTo().isPresent()) { + assertThat(Address.fromHexString(result.getString("to"))) + .isEqualTo(transaction.getTo().get()); + } else { + assertThat(result.getValue("to")).isNull(); + } + assertThat(Wei.fromHexString(result.getString("value"))).isEqualTo(transaction.getValue()); + assertThat(Wei.fromHexString(result.getString("gasPrice"))) + .isEqualTo(transaction.getGasPrice()); + assertThat(Long.decode(result.getString("gas"))).isEqualTo(transaction.getGasLimit()); + assertThat(BytesValue.fromHexString(result.getString("input"))) + .isEqualTo(transaction.getPayload()); + } + + private void assertBlockResultMatchesBlock(final JsonObject result, final Block block) { + final BlockHeader header = block.getHeader(); + assertThat(Hash.fromHexString(result.getString("parentHash"))) + .isEqualTo(header.getParentHash()); + assertThat(Hash.fromHexString(result.getString("sha3Uncles"))) + .isEqualTo(header.getOmmersHash()); + assertThat(Hash.fromHexString(result.getString("transactionsRoot"))) + .isEqualTo(header.getTransactionsRoot()); + assertThat(Hash.fromHexString(result.getString("stateRoot"))).isEqualTo(header.getStateRoot()); + assertThat(Hash.fromHexString(result.getString("receiptsRoot"))) + .isEqualTo(header.getReceiptsRoot()); + assertThat(Address.fromHexString(result.getString("miner"))).isEqualTo(header.getCoinbase()); + assertThat(UInt256.fromHexString(result.getString("difficulty"))) + .isEqualTo(header.getDifficulty()); + assertThat(BytesValue.fromHexString(result.getString("extraData"))) + .isEqualTo(header.getExtraData()); + assertThat(hexStringToInt(result.getString("size"))).isEqualTo(block.calculateSize()); + assertThat(Long.decode(result.getString("gasLimit"))).isEqualTo(header.getGasLimit()); + assertThat(Long.decode(result.getString("gasUsed"))).isEqualTo(header.getGasUsed()); + assertThat(Long.decode(result.getString("timestamp"))).isEqualTo(header.getTimestamp()); + assertThat(Long.decode(result.getString("number"))).isEqualTo(header.getNumber()); + // Nonce is a data field and should represent 8 bytes exactly + final String nonceResult = result.getString("nonce").toLowerCase(); + assertThat(nonceResult.length() == 18 && nonceResult.startsWith("0x")).isTrue(); + assertThat(Long.parseUnsignedLong(nonceResult.substring(2), 16)).isEqualTo(header.getNonce()); + assertThat(Hash.fromHexString(result.getString("hash"))).isEqualTo(header.getHash()); + assertThat(LogsBloomFilter.fromHexString(result.getString("logsBloom"))) + .isEqualTo(header.getLogsBloom()); + } + + private int hexStringToInt(final String hexString) { + return BytesValues.extractInt(BytesValue.fromHexStringLenient(hexString)); + } + + @Test + public void ethSyncingFalse() throws Exception { + final String id = "007"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"eth_syncing\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + when(synchronizer.getSyncStatus()).thenReturn(Optional.empty()); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Verify general result format. + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidJsonRpcResult(json, id); + // Evaluate result. + assertThat(json.getBoolean("result")).isFalse(); + } + } + + @Test + public void ethSyncingResultIsPresent() throws Exception { + final SyncStatus testResult = new SyncStatus(1L, 8L, 7L); + when(synchronizer.getSyncStatus()).thenReturn(Optional.of(testResult)); + final String id = "999"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + Json.encode(id) + ",\"method\":\"eth_syncing\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + final JsonObject result = json.getJsonObject("result"); + final long startingBlock = Long.decode(result.getString("startingBlock")); + assertThat(startingBlock).isEqualTo(1L); + final long currentBlock = Long.decode(result.getString("currentBlock")); + assertThat(currentBlock).isEqualTo(8L); + final long highestBlock = Long.decode(result.getString("highestBlock")); + assertThat(highestBlock).isEqualTo(7L); + } + } + + public BlockWithMetadata blockWithMetadata(final Block block) { + final UInt256 td = block.getHeader().getDifficulty().plus(10L); + final int size = block.calculateSize(); + + final List txs = block.getBody().getTransactions(); + final List formattedTxs = new ArrayList<>(txs.size()); + for (int i = 0; i < txs.size(); i++) { + formattedTxs.add( + new TransactionWithMetadata( + txs.get(i), block.getHeader().getNumber(), block.getHash(), i)); + } + final List ommers = + block.getBody().getOmmers().stream().map(BlockHeader::getHash).collect(Collectors.toList()); + return new BlockWithMetadata<>(block.getHeader(), formattedTxs, ommers, td, size); + } + + public BlockWithMetadata blockWithMetadataAndTxHashes(final Block block) { + final UInt256 td = block.getHeader().getDifficulty().plus(10L); + final int size = block.calculateSize(); + + final List txs = + block + .getBody() + .getTransactions() + .stream() + .map(Transaction::hash) + .collect(Collectors.toList()); + final List ommers = + block.getBody().getOmmers().stream().map(BlockHeader::getHash).collect(Collectors.toList()); + return new BlockWithMetadata<>(block.getHeader(), txs, ommers, td, size); + } + + @Test + public void ethGetStorageLatestAtIndexZero() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final String mockStorage = "0x0000000000000000000000000000000000000000000000000000000000000001"; + when(blockchainQueries.headBlockNumber()).thenReturn(0L); + when(blockchainQueries.storageAt(eq(address), eq(UInt256.ZERO), eq(0L))) + .thenReturn(Optional.of(UInt256.fromHexString(mockStorage))); + + final String id = "88"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getStorageAt\", \"params\": [\"" + + address + + "\",\"" + + UInt256.ZERO + + "\",\"latest\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat("0x0000000000000000000000000000000000000000000000000000000000000001") + .isEqualTo(result); + } + } + + @Test + public void ethGetStorageLatestAtIndexOne() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final String mockStorage = "0x0000000000000000000000000000000000000000000000000000000000000006"; + when(blockchainQueries.headBlockNumber()).thenReturn(0L); + when(blockchainQueries.storageAt(eq(address), eq(UInt256.ONE), eq(0L))) + .thenReturn(Optional.of(UInt256.fromHexString(mockStorage))); + + final String id = "88"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getStorageAt\", \"params\": [\"" + + address + + "\",\"" + + UInt256.ONE + + "\",\"latest\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat("0x0000000000000000000000000000000000000000000000000000000000000006") + .isEqualTo(result); + } + } + + @Test + public void ethGetStorageAtEarliest() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final String mockStorage = "0x0000000000000000000000000000000000000000000000000000000000000006"; + when(blockchainQueries.storageAt(address, UInt256.ONE, 0L)) + .thenReturn(Optional.of(UInt256.fromHexString(mockStorage))); + + final String id = "88"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getStorageAt\", \"params\": [\"" + + address + + "\",\"" + + UInt256.ONE + + "\",\"earliest\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + // Check result + final String result = json.getString("result"); + assertThat("0x0000000000000000000000000000000000000000000000000000000000000006") + .isEqualTo(result); + } + } + + @Test + public void ethGetStorageAtBlockNumber() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + final String mockStorage = "0x0000000000000000000000000000000000000000000000000000000000000002"; + when(blockchainQueries.storageAt(address, UInt256.ZERO, 0L)) + .thenReturn(Optional.of(UInt256.fromHexString(mockStorage))); + + final String id = "999"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getStorageAt\", \"params\": [\"" + + address + + "\",\"" + + UInt256.ZERO + + "\",\"" + + 0L + + "\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(200); + // Check general format of result + final String respBody = resp.body().string(); + final JsonObject json = new JsonObject(respBody); + testHelper.assertValidJsonRpcResult(json, id); + + // Check result + final Object result = json.getString("result"); + assertThat("0x0000000000000000000000000000000000000000000000000000000000000002") + .isEqualTo(result); + } + } + + @Test + public void ethGetStorageAtInvalidParameterStorageIndex() throws Exception { + // Setup mocks to return a block + final BlockDataGenerator gen = new BlockDataGenerator(); + final Address address = gen.address(); + + final String id = "88"; + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"method\":\"eth_getStorageAt\", \"params\": [\"" + + address + + "\",\"" + + "blah" + + "\",\"latest\"]}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + + try (Response resp = client.newCall(request).execute()) { + assertThat(resp.code()).isEqualTo(400); + // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + final JsonRpcError expectedError = JsonRpcError.INVALID_PARAMS; + testHelper.assertValidJsonRpcError( + json, id, expectedError.getCode(), expectedError.getMessage()); + } + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestHelper.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestHelper.java new file mode 100755 index 00000000000..5fc46e41460 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/JsonRpcTestHelper.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import java.util.Set; + +import io.vertx.core.json.JsonObject; + +public class JsonRpcTestHelper { + + protected void assertValidJsonRpcResult(final JsonObject json, final Object id) { + // Check all expected fieldnames are set + final Set fieldNames = json.fieldNames(); + assertThat(fieldNames.size()).isEqualTo(3); + assertThat(fieldNames.contains("id")).isTrue(); + assertThat(fieldNames.contains("jsonrpc")).isTrue(); + assertThat(fieldNames.contains("result")).isTrue(); + + // Check standard field values + assertIdMatches(json, id); + assertThat(json.getString("jsonrpc")).isEqualTo("2.0"); + } + + protected void assertValidJsonRpcError( + final JsonObject json, final Object id, final int errorCode, final String errorMessage) + throws Exception { + // Check all expected fieldnames are set + final Set fieldNames = json.fieldNames(); + assertThat(fieldNames.size()).isEqualTo(3); + assertThat(fieldNames.contains("id")).isTrue(); + assertThat(fieldNames.contains("jsonrpc")).isTrue(); + assertThat(fieldNames.contains("error")).isTrue(); + + // Check standard field values + assertIdMatches(json, id); + assertThat(json.getString("jsonrpc")).isEqualTo("2.0"); + + // Check error format + final JsonObject error = json.getJsonObject("error"); + final Set errorFieldNames = error.fieldNames(); + assertThat(errorFieldNames.size()).isEqualTo(2); + assertThat(errorFieldNames.contains("code")).isTrue(); + assertThat(errorFieldNames.contains("message")).isTrue(); + + // Check error field values + assertThat(error.getInteger("code")).isEqualTo(errorCode); + assertThat(error.getString("message")).isEqualTo(errorMessage); + } + + protected void assertIdMatches(final JsonObject json, final Object expectedId) { + final Object actualId = json.getValue("id"); + if (expectedId == null) { + assertThat(actualId).isNull(); + return; + } + + assertThat(expectedId) + .isInstanceOfAny( + String.class, Integer.class, Long.class, Float.class, Double.class, BigInteger.class); + assertThat(actualId).isInstanceOf(expectedId.getClass()); + assertThat(actualId.toString()).isEqualTo(expectedId.toString()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/MockPeerConnection.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/MockPeerConnection.java new file mode 100755 index 00000000000..ba94479f0fb --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/MockPeerConnection.java @@ -0,0 +1,66 @@ +package net.consensys.pantheon.ethereum.jsonrpc; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Set; + +public class MockPeerConnection implements PeerConnection { + PeerInfo peerInfo; + InetSocketAddress localAddress; + InetSocketAddress remoteAddress; + + public MockPeerConnection( + final PeerInfo peerInfo, + final InetSocketAddress localAddress, + final InetSocketAddress remoteAddress) { + this.peerInfo = peerInfo; + this.localAddress = localAddress; + this.remoteAddress = remoteAddress; + } + + @Override + public void send(final Capability capability, final MessageData message) { + throw new UnsupportedOperationException(); + } + + @Override + public Set getAgreedCapabilities() { + throw new UnsupportedOperationException(); + } + + @Override + public Capability capability(final String protocol) { + throw new UnsupportedOperationException(); + } + + @Override + public PeerInfo getPeer() { + return peerInfo; + } + + @Override + public void terminateConnection(final DisconnectReason reason, final boolean peerInitiated) { + throw new UnsupportedOperationException(); + } + + @Override + public void disconnect(final DisconnectReason reason) { + throw new UnsupportedOperationException(); + } + + @Override + public SocketAddress getLocalAddress() { + return localAddress; + } + + @Override + public SocketAddress getRemoteAddress() { + return remoteAddress; + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/EthJsonRpcHttpServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/EthJsonRpcHttpServiceTest.java new file mode 100755 index 00000000000..23b7193bb8b --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/EthJsonRpcHttpServiceTest.java @@ -0,0 +1,133 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.jsonrpc.AbstractEthJsonRpcHttpServiceTest; + +import java.io.IOException; + +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Test; + +public class EthJsonRpcHttpServiceTest extends AbstractEthJsonRpcHttpServiceTest { + + private Hash recordPendingTransaction(final int blockNumber, final int transactionIndex) { + final Block block = BLOCKS.get(1); + final Transaction transaction = block.getBody().getTransactions().get(0); + filterManager.recordPendingTransactionEvent(transaction); + return transaction.hash(); + } + + @Test + public void getFilterChanges_noBlocks() throws Exception { + final String expectedRespBody = + String.format("{%n \"jsonrpc\" : \"2.0\",%n \"id\" : 2,%n \"result\" : [ ]%n}"); + final ResponseBody body = ethNewBlockFilter(1).body(); + final String result = getResult(body); + body.close(); + final Response resp = ethGetFilterChanges(2, result); + assertThat(resp.code()).isEqualTo(200); + assertThat(resp.body().string()).isEqualTo(expectedRespBody); + } + + @Test + public void getFilterChanges_oneBlock() throws Exception { + final String expectedRespBody = + String.format( + "{%n \"jsonrpc\" : \"2.0\",%n \"id\" : 2,%n \"result\" : [ \"0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9\" ]%n}"); + final ResponseBody body = ethNewBlockFilter(1).body(); + final String result = getResult(body); + body.close(); + + importBlock(1); + final Response resp = ethGetFilterChanges(2, result); + assertThat(resp.code()).isEqualTo(200); + assertThat(resp.body().string()).isEqualTo(expectedRespBody); + } + + @Test + public void getFilterChanges_noTransactions() throws Exception { + final String expectedRespBody = + String.format("{%n \"jsonrpc\" : \"2.0\",%n \"id\" : 2,%n \"result\" : [ ]%n}"); + final ResponseBody body = ethNewPendingTransactionFilter(1).body(); + final String result = getResult(body); + body.close(); + final Response resp = ethGetFilterChanges(2, result); + assertThat(resp.code()).isEqualTo(200); + assertThat(resp.body().string()).isEqualTo(expectedRespBody); + } + + @Test + public void getFilterChanges_oneTransaction() throws Exception { + final ResponseBody body = ethNewPendingTransactionFilter(1).body(); + final String result = getResult(body); + body.close(); + final Hash transactionHash = recordPendingTransaction(1, 1); + + final Response resp = ethGetFilterChanges(2, result); + assertThat(resp.code()).isEqualTo(200); + final String expectedRespBody = + String.format( + "{%n \"jsonrpc\" : \"2.0\",%n \"id\" : 2,%n \"result\" : [ \"" + + transactionHash + + "\" ]%n}"); + assertThat(resp.body().string()).isEqualTo(expectedRespBody); + } + + @Test + public void uninstallFilter() throws Exception { + final String expectedRespBody = + String.format("{%n \"jsonrpc\" : \"2.0\",%n \"id\" : 2,%n \"result\" : true%n}"); + final ResponseBody body = ethNewBlockFilter(1).body(); + final String result = getResult(body); + body.close(); + final Response resp = ethUninstallFilter(2, result); + assertThat(resp.code()).isEqualTo(200); + assertThat(resp.body().string()).isEqualTo(expectedRespBody); + } + + private String getResult(final ResponseBody body) throws IOException { + final JsonObject json = new JsonObject(body.string()); + return json.getString("result"); + } + + private Response jsonRpcRequest(final int id, final String method, final String params) + throws Exception { + final RequestBody body = + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(id) + + ",\"params\": " + + params + + ",\"method\":\"" + + method + + "\"}"); + final Request request = new Request.Builder().post(body).url(baseUrl).build(); + return client.newCall(request).execute(); + } + + private Response ethNewBlockFilter(final int id) throws Exception { + return jsonRpcRequest(id, "eth_newBlockFilter", "[]"); + } + + private Response ethNewPendingTransactionFilter(final int id) throws Exception { + return jsonRpcRequest(id, "eth_newPendingTransactionFilter", "[]"); + } + + private Response ethGetFilterChanges(final int id, final String filterId) throws Exception { + return jsonRpcRequest(id, "eth_getFilterChanges", "[\"" + filterId + "\"]"); + } + + private Response ethUninstallFilter(final int id, final String filterId) throws Exception { + return jsonRpcRequest(id, "eth_uninstallFilter", "[\"" + filterId + "\"]"); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGeneratorTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGeneratorTest.java new file mode 100755 index 00000000000..6cee361fa7b --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterIdGeneratorTest.java @@ -0,0 +1,18 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class FilterIdGeneratorTest { + + @Test + public void idIsAHexString() { + final FilterIdGenerator generator = new FilterIdGenerator(); + final String s = generator.nextId(); + final BytesValue bytesValue = BytesValue.fromHexString(s); + assertEquals(s, bytesValue.toString()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerLogFilterTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerLogFilterTest.java new file mode 100755 index 00000000000..456c1f7eddc --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerLogFilterTest.java @@ -0,0 +1,176 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class FilterManagerLogFilterTest { + + private FilterManager filterManager; + + @Mock private Blockchain blockchain; + @Mock private BlockchainQueries blockchainQueries; + @Mock private TransactionPool transactionPool; + + @Before + public void setupTest() { + when(blockchainQueries.getBlockchain()).thenReturn(blockchain); + this.filterManager = + new FilterManager(blockchainQueries, transactionPool, new FilterIdGenerator()); + } + + @Test + public void installUninstallNewLogFilter() { + assertThat(filterManager.logFilterCount()).isEqualTo(0); + + final String filterId = filterManager.installLogFilter(latest(), latest(), logsQuery()); + assertThat(filterManager.logFilterCount()).isEqualTo(1); + + assertThat(filterManager.uninstallFilter(filterId)).isTrue(); + assertThat(filterManager.logFilterCount()).isEqualTo(0); + + assertThat(filterManager.blockChanges(filterId)).isNull(); + } + + @Test + public void shouldCheckMatchingLogsWhenRecordedNewBlockEvent() { + when(blockchainQueries.headBlockNumber()).thenReturn(100L); + + filterManager.installLogFilter(latest(), latest(), logsQuery()); + recordNewBlockEvent(); + + verify(blockchainQueries).matchingLogs(eq(100L), eq(100L), refEq(logsQuery())); + } + + @Test + public void shouldUseHeadBlockAsFromBlockNumberWhenCheckingLogsForChanges() { + when(blockchainQueries.headBlockNumber()).thenReturn(3L); + + filterManager.installLogFilter(blockNum(1L), blockNum(10L), logsQuery()); + recordNewBlockEvent(); + + verify(blockchainQueries).matchingLogs(eq(3L), eq(10L), refEq(logsQuery())); + } + + @Test + public void shouldReturnLogWhenLogFilterMatches() { + final LogWithMetadata log = logWithMetadata(); + when(blockchainQueries.headBlockNumber()).thenReturn(100L); + when(blockchainQueries.matchingLogs(eq(100L), eq(100L), refEq(logsQuery()))) + .thenReturn(Lists.newArrayList(log)); + + final String filterId = filterManager.installLogFilter(latest(), latest(), logsQuery()); + recordNewBlockEvent(); + + final List retrievedLogs = filterManager.logsChanges(filterId); + + assertThat(retrievedLogs).isEqualToComparingFieldByFieldRecursively(Lists.newArrayList(log)); + } + + @Test + public void shouldCheckLogsForEveryLogFilter() { + filterManager.installLogFilter(latest(), latest(), logsQuery()); + filterManager.installLogFilter(latest(), latest(), logsQuery()); + filterManager.installLogFilter(latest(), latest(), logsQuery()); + recordNewBlockEvent(); + + verify(blockchainQueries, times(3)).matchingLogs(anyLong(), anyLong(), any()); + } + + @Test + public void shouldReturnNullWhenForAbsentLogFilter() { + final List logs = filterManager.logsChanges("NOT THERE"); + + assertThat(logs).isNull(); + verify(blockchainQueries, times(0)).matchingLogs(anyLong(), anyLong(), any()); + } + + @Test + public void shouldClearLogsAfterGettingLogChanges() { + when(blockchainQueries.matchingLogs(anyLong(), anyLong(), any())) + .thenReturn(Lists.newArrayList(logWithMetadata())); + + final String filterId = filterManager.installLogFilter(latest(), latest(), logsQuery()); + recordNewBlockEvent(); + recordNewBlockEvent(); + + assertThat(filterManager.logsChanges(filterId).size()).isEqualTo(2); + assertThat(filterManager.logsChanges(filterId).size()).isEqualTo(0); + } + + private void recordNewBlockEvent() { + filterManager.recordBlockEvent( + BlockAddedEvent.createForHeadAdvancement(new BlockDataGenerator().block()), + blockchainQueries.getBlockchain()); + } + + @Test + public void getLogsForAbsentFilterReturnsNull() { + assertThat(filterManager.logs("NOTTHERE")).isNull(); + } + + @Test + public void getLogsForExistingFilterReturnsResults() { + final LogWithMetadata log = logWithMetadata(); + when(blockchainQueries.headBlockNumber()).thenReturn(100L); + when(blockchainQueries.matchingLogs(eq(100L), eq(100L), refEq(logsQuery()))) + .thenReturn(Lists.newArrayList(log)); + + final String filterId = filterManager.installLogFilter(latest(), latest(), logsQuery()); + final List retrievedLogs = filterManager.logs(filterId); + + assertThat(retrievedLogs).isEqualToComparingFieldByFieldRecursively(Lists.newArrayList(log)); + } + + private LogWithMetadata logWithMetadata() { + return LogWithMetadata.create( + 0, + 100L, + Hash.ZERO, + Hash.ZERO, + 0, + Address.fromHexString("0x0"), + BytesValue.EMPTY, + Lists.newArrayList(), + false); + } + + private LogsQuery logsQuery() { + return new LogsQuery.Builder().build(); + } + + private BlockParameter latest() { + return new BlockParameter("latest"); + } + + private BlockParameter blockNum(final long blockNum) { + return new BlockParameter(Quantity.create(blockNum)); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerTest.java new file mode 100755 index 00000000000..15cc5cda54d --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/FilterManagerTest.java @@ -0,0 +1,203 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class FilterManagerTest { + + private BlockDataGenerator blockGenerator; + private Block currentBlock; + private FilterManager filterManager; + + @Mock private Blockchain blockchain; + @Mock private BlockchainQueries blockchainQueries; + @Mock private TransactionPool transactionPool; + + @Before + public void setupTest() { + when(blockchainQueries.getBlockchain()).thenReturn(blockchain); + this.blockGenerator = new BlockDataGenerator(); + this.currentBlock = blockGenerator.genesisBlock(); + this.filterManager = + new FilterManager(blockchainQueries, transactionPool, new FilterIdGenerator()); + } + + @Test + public void uninstallNonexistentFilter() { + assertThat(filterManager.uninstallFilter("1")).isFalse(); + } + + @Test + public void installUninstallNewBlockFilter() { + assertThat(filterManager.blockFilterCount()).isEqualTo(0); + + final String filterId = filterManager.installBlockFilter(); + assertThat(filterManager.blockFilterCount()).isEqualTo(1); + + assertThat(filterManager.uninstallFilter(filterId)).isTrue(); + assertThat(filterManager.blockFilterCount()).isEqualTo(0); + + assertThat(filterManager.blockChanges(filterId)).isNull(); + } + + @Test + public void newBlockChanges_SingleFilter() { + final String filterId = filterManager.installBlockFilter(); + assertThat(filterManager.blockChanges(filterId).size()).isEqualTo(0); + + final Hash blockHash1 = appendBlockToBlockchain(); + final List expectedHashes = Lists.newArrayList(blockHash1); + assertThat(filterManager.blockChanges(filterId)).isEqualTo(expectedHashes); + + // Check that changes have been flushed. + expectedHashes.clear(); + assertThat(filterManager.blockChanges(filterId)).isEqualTo(expectedHashes); + + final Hash blockHash2 = appendBlockToBlockchain(); + expectedHashes.add(blockHash2); + final Hash blockHash3 = appendBlockToBlockchain(); + expectedHashes.add(blockHash3); + assertThat(filterManager.blockChanges(filterId)).isEqualTo(expectedHashes); + } + + @Test + public void newBlockChanges_MultipleFilters() { + final String filterId1 = filterManager.installBlockFilter(); + assertThat(filterManager.blockChanges(filterId1).size()).isEqualTo(0); + + final Hash blockHash1 = appendBlockToBlockchain(); + final List expectedHashes1 = Lists.newArrayList(blockHash1); + + final String filterId2 = filterManager.installBlockFilter(); + final Hash blockHash2 = appendBlockToBlockchain(); + expectedHashes1.add(blockHash2); + final List expectedHashes2 = Lists.newArrayList(blockHash2); + assertThat(filterManager.blockChanges(filterId1)).isEqualTo(expectedHashes1); + assertThat(filterManager.blockChanges(filterId2)).isEqualTo(expectedHashes2); + expectedHashes1.clear(); + expectedHashes2.clear(); + + // Both filters have been flushed. + assertThat(filterManager.blockChanges(filterId1)).isEqualTo(expectedHashes1); + assertThat(filterManager.blockChanges(filterId2)).isEqualTo(expectedHashes2); + + final Hash blockHash3 = appendBlockToBlockchain(); + expectedHashes1.add(blockHash3); + expectedHashes2.add(blockHash3); + + // Flush the first filter. + assertThat(filterManager.blockChanges(filterId1)).isEqualTo(expectedHashes1); + expectedHashes1.clear(); + + final Hash blockHash4 = appendBlockToBlockchain(); + expectedHashes1.add(blockHash4); + expectedHashes2.add(blockHash4); + assertThat(filterManager.blockChanges(filterId1)).isEqualTo(expectedHashes1); + assertThat(filterManager.blockChanges(filterId2)).isEqualTo(expectedHashes2); + } + + @Test + public void installUninstallPendingTransactionFilter() { + assertThat(filterManager.pendingTransactionFilterCount()).isEqualTo(0); + + final String filterId = filterManager.installPendingTransactionFilter(); + assertThat(filterManager.pendingTransactionFilterCount()).isEqualTo(1); + + assertThat(filterManager.uninstallFilter(filterId)).isTrue(); + assertThat(filterManager.pendingTransactionFilterCount()).isEqualTo(0); + + assertThat(filterManager.pendingTransactionChanges(filterId)).isNull(); + } + + @Test + public void getTransactionChanges_SingleFilter() { + final String filterId = filterManager.installPendingTransactionFilter(); + assertThat(filterManager.pendingTransactionChanges(filterId).size()).isEqualTo(0); + + final Hash transactionHash1 = receivePendingTransaction(); + final List expectedHashes = Lists.newArrayList(transactionHash1); + assertThat(filterManager.pendingTransactionChanges(filterId)).isEqualTo(expectedHashes); + + // Check that changes have been flushed. + expectedHashes.clear(); + assertThat(filterManager.pendingTransactionChanges(filterId)).isEqualTo(expectedHashes); + + final Hash transactionHash2 = receivePendingTransaction(); + expectedHashes.add(transactionHash2); + final Hash transactionHash3 = receivePendingTransaction(); + expectedHashes.add(transactionHash3); + assertThat(filterManager.pendingTransactionChanges(filterId)).isEqualTo(expectedHashes); + } + + @Test + public void getPendingTransactionChanges_MultipleFilters() { + final String filterId1 = filterManager.installPendingTransactionFilter(); + assertThat(filterManager.pendingTransactionChanges(filterId1).size()).isEqualTo(0); + + final Hash transactionHash1 = receivePendingTransaction(); + final List expectedHashes1 = Lists.newArrayList(transactionHash1); + + final String filterId2 = filterManager.installPendingTransactionFilter(); + final Hash transactionHash2 = receivePendingTransaction(); + expectedHashes1.add(transactionHash2); + final List expectedHashes2 = Lists.newArrayList(transactionHash2); + assertThat(filterManager.pendingTransactionChanges(filterId1)).isEqualTo(expectedHashes1); + assertThat(filterManager.pendingTransactionChanges(filterId2)).isEqualTo(expectedHashes2); + expectedHashes1.clear(); + expectedHashes2.clear(); + + // Both filters have been flushed. + assertThat(filterManager.pendingTransactionChanges(filterId1)).isEqualTo(expectedHashes1); + assertThat(filterManager.pendingTransactionChanges(filterId2)).isEqualTo(expectedHashes2); + + final Hash transactionHash3 = receivePendingTransaction(); + expectedHashes1.add(transactionHash3); + expectedHashes2.add(transactionHash3); + + // Flush the first filter. + assertThat(filterManager.pendingTransactionChanges(filterId1)).isEqualTo(expectedHashes1); + expectedHashes1.clear(); + + final Hash transactionHash4 = receivePendingTransaction(); + expectedHashes1.add(transactionHash4); + expectedHashes2.add(transactionHash4); + assertThat(filterManager.pendingTransactionChanges(filterId1)).isEqualTo(expectedHashes1); + assertThat(filterManager.pendingTransactionChanges(filterId2)).isEqualTo(expectedHashes2); + } + + private Hash appendBlockToBlockchain() { + final long blockNumber = currentBlock.getHeader().getNumber() + 1; + final Hash parentHash = currentBlock.getHash(); + final BlockDataGenerator.BlockOptions options = + new BlockDataGenerator.BlockOptions().setBlockNumber(blockNumber).setParentHash(parentHash); + currentBlock = blockGenerator.block(options); + filterManager.recordBlockEvent( + BlockAddedEvent.createForHeadAdvancement(currentBlock), blockchainQueries.getBlockchain()); + return currentBlock.getHash(); + } + + private Hash receivePendingTransaction() { + final Transaction transaction = blockGenerator.transaction(); + filterManager.recordPendingTransactionEvent(transaction); + return transaction.hash(); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQueryTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQueryTest.java new file mode 100755 index 00000000000..d6bb23834ba --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/filter/LogsQueryTest.java @@ -0,0 +1,405 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.LogTopic; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class LogsQueryTest { + + @Test + public void wildcardQueryAddressTopicReturnTrue() { + final LogsQuery query = new LogsQuery.Builder().build(); + + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final BytesValue data = BytesValue.fromHexString("0x0102"); + final List topics = new ArrayList<>(); + final Log log = new Log(address, data, topics); + + assertThat(query.matches(log)).isTrue(); + } + + @Test + public void univariateAddressMatchReturnsTrue() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final LogsQuery query = new LogsQuery.Builder().address(address).build(); + + final List topics = new ArrayList<>(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), topics); + + assertThat(query.matches(log)).isTrue(); + } + + @Test + public void univariateAddressMismatchReturnsFalse() { + final Address address1 = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final LogsQuery query = new LogsQuery.Builder().address(address1).build(); + + final Address address2 = Address.fromHexString("0x2222222222222222222222222222222222222222"); + final BytesValue data = BytesValue.fromHexString("0x0102"); + final List topics = new ArrayList<>(); + final Log log = new Log(address2, data, topics); + + assertThat(query.matches(log)).isFalse(); + } + + @Test + public void multivariateAddressQueryMatchReturnsTrue() { + final Address address1 = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final Address address2 = Address.fromHexString("0x2222222222222222222222222222222222222222"); + final LogsQuery query = new LogsQuery.Builder().addresses(address1, address2).build(); + + final List topics = new ArrayList<>(); + final Log log = new Log(address1, BytesValue.fromHexString("0x0102"), topics); + + assertThat(query.matches(log)).isTrue(); + } + + @Test + public void multivariateAddressMismatchReturnsFalse() { + final Address address1 = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final Address address2 = Address.fromHexString("0x2222222222222222222222222222222222222222"); + final LogsQuery query = new LogsQuery.Builder().addresses(address1, address2).build(); + + final Address address3 = Address.fromHexString("0x3333333333333333333333333333333333333333"); + final BytesValue data = BytesValue.fromHexString("0x0102"); + final List topics = new ArrayList<>(); + final Log log = new Log(address3, data, topics); + + assertThat(query.matches(log)).isFalse(); + } + + @Test + public void univariateTopicQueryLogWithoutTopicReturnFalse() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final LogTopic topic = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + final List topics = new ArrayList<>(); + topics.add(topic); + + final List> topicsQuery = new ArrayList<>(); + topicsQuery.add(topics); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(topicsQuery).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), Lists.newArrayList()); + + assertThat(query.matches(log)).isFalse(); + } + + @Test + public void univariateTopicQueryMatchReturnTrue() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final LogTopic topic = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + + final List topics = new ArrayList<>(); + topics.add(topic); + + final List> topicsQuery = new ArrayList<>(); + topicsQuery.add(topics); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(topicsQuery).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), Lists.newArrayList(topic)); + + assertThat(query.matches(log)).isTrue(); + } + + @Test + public void univariateTopicQueryMismatchReturnFalse() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + final LogTopic topic = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final List topics = Lists.newArrayList(topic); + final List> topicsQuery = new ArrayList<>(); + topicsQuery.add(topics); + final LogsQuery query = new LogsQuery.Builder().address(address).topics(topicsQuery).build(); + + final BytesValue data = BytesValue.fromHexString("0x0102"); + topics.add( + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + final Log log = new Log(address, data, Lists.newArrayList()); + + assertThat(query.matches(log)).isFalse(); + } + + @Test + public void multivariateTopicQueryMismatchReturnFalse() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + final LogTopic topic3 = + LogTopic.fromHexString( + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + + final List logTopics = new ArrayList<>(); + logTopics.add(topic1); + logTopics.add(topic2); + + final List queryTopics = new ArrayList<>(); + queryTopics.add(topic3); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(queryParameter).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isFalse(); + } + + /** [null, B] "anything in first position AND B in second position (and anything after)" */ + @Test + public void multivariateSurplusTopicMatchMultivariateNullQueryReturnTrue() { + final Address address1 = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + final LogTopic topic3 = + LogTopic.fromHexString( + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + final LogTopic topic4 = null; + + final List logTopics = new ArrayList<>(); + logTopics.add(topic1); + logTopics.add(topic2); + logTopics.add(topic3); + + final List queryTopics = new ArrayList<>(); + queryTopics.add(topic4); + queryTopics.add(topic2); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics); + + final LogsQuery query = + new LogsQuery.Builder().address(address1).topics(queryParameter).build(); + final Log log = new Log(address1, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isTrue(); + } + + /** [A, B] "A in first position AND B in second position (and anything after)" */ + @Test + public void multivariateSurplusTopicMatchMultivariateQueryReturnTrue_00() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + final List logTopics = new ArrayList<>(); + logTopics.add(topic1); + logTopics.add(topic2); + + final List queryTopics = new ArrayList<>(); + queryTopics.add(topic1); + queryTopics.add(topic2); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(queryParameter).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isTrue(); + } + + /** [A, B] "A in first position AND B in second position (and anything after)" */ + @Test + public void multivariateSurplusTopicMatchMultivariateQueryReturnTrue_01() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + final LogTopic topic3 = + LogTopic.fromHexString( + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + + final List logTopics = new ArrayList<>(); + logTopics.add(topic1); + logTopics.add(topic2); + + final List queryTopics = new ArrayList<>(); + queryTopics.add(topic1); + queryTopics.add(topic3); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(queryParameter).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isTrue(); + } + + /** [A, B] "A in first position AND B in second position (and anything after)" */ + @Test + public void multivariateSurplusTopicMatchMultivariateQueryReturnTrue_02() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + final LogTopic topic3 = + LogTopic.fromHexString( + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + + final List logTopics = new ArrayList<>(); + logTopics.add(topic1); + logTopics.add(topic2); + logTopics.add(topic3); + + final List queryTopics = new ArrayList<>(); + queryTopics.add(topic1); + queryTopics.add(topic2); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(queryParameter).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isTrue(); + } + + /** + * [[A, B], [A, B]] "(A OR B) in first position AND (A OR B) in second position (and anything + * after)" + */ + @Test + public void redundantUnivariateTopicMatchMultivariateQueryReturnTrue() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + final List logTopics = new ArrayList<>(); + logTopics.add(topic2); + logTopics.add(topic2); + + final List queryTopics1 = new ArrayList<>(); + queryTopics1.add(topic1); + queryTopics1.add(topic2); + final List queryTopics2 = new ArrayList<>(); + queryTopics2.add(topic1); + queryTopics2.add(topic2); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics1); + queryParameter.add(queryTopics2); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(queryParameter).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isTrue(); + } + + @Test + public void multivariateTopicMatchRedundantMultivariateQueryReturnTrue() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + final LogTopic topic3 = + LogTopic.fromHexString( + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + + final List logTopics = new ArrayList<>(); + logTopics.add(topic1); + logTopics.add(topic3); + + final List queryTopics1 = new ArrayList<>(); + queryTopics1.add(topic1); + queryTopics1.add(topic2); + final List queryTopics2 = new ArrayList<>(); + queryTopics2.add(topic1); + queryTopics2.add(topic2); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics1); + queryParameter.add(queryTopics2); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(queryParameter).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isFalse(); + } + + @Test + public void multivariateTopicMatchMultivariateQueryReturnTrue() { + final Address address = Address.fromHexString("0x1111111111111111111111111111111111111111"); + + final LogTopic topic1 = + LogTopic.fromHexString( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + final LogTopic topic2 = + LogTopic.fromHexString( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + final LogTopic topic3 = + LogTopic.fromHexString( + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + final LogTopic topic4 = + LogTopic.fromHexString( + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"); + + final List logTopics = new ArrayList<>(); + logTopics.add(topic1); + logTopics.add(topic3); + + final List queryTopics1 = new ArrayList<>(); + queryTopics1.add(topic1); + queryTopics1.add(topic2); + final List queryTopics2 = new ArrayList<>(); + queryTopics2.add(topic3); + queryTopics2.add(topic4); + + final List> queryParameter = new ArrayList<>(); + queryParameter.add(queryTopics1); + queryParameter.add(queryTopics2); + + final LogsQuery query = new LogsQuery.Builder().address(address).topics(queryParameter).build(); + final Log log = new Log(address, BytesValue.fromHexString("0x0102"), logTopics); + + assertThat(query.matches(log)).isTrue(); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeersTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeersTest.java new file mode 100755 index 00000000000..593385f710a --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/AdminPeersTest.java @@ -0,0 +1,88 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.MockPeerConnection; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.PeerResult; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class AdminPeersTest { + + private AdminPeers adminPeers; + + @Mock private P2PNetwork p2pNetwork; + + @Before + public void before() { + adminPeers = new AdminPeers(p2pNetwork); + } + + @Test + public void shouldReturnExpectedMethodName() { + assertThat(adminPeers.getName()).isEqualTo("admin_peers"); + } + + @Test + public void shouldReturnEmptyPeersListWhenP2PNetworkDoesNotHavePeers() { + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, Collections.emptyList()); + final JsonRpcRequest request = adminPeers(); + when(p2pNetwork.getPeers()).thenReturn(Collections.emptyList()); + + final JsonRpcResponse response = adminPeers.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnExpectedPeerListWhenP2PNetworkHavePeers() { + final Collection peerList = peerList(); + final List expectedPeerResults = + peerList.stream().map(PeerResult::new).collect(Collectors.toList()); + + final JsonRpcRequest request = adminPeers(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, expectedPeerResults); + + when(p2pNetwork.getPeers()).thenReturn(peerList); + + final JsonRpcResponse response = adminPeers.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + private Collection peerList() { + final PeerInfo peerInfo = + new PeerInfo(5, "0x0", Collections.emptyList(), 30303, BytesValue.EMPTY); + final PeerConnection p = + new MockPeerConnection( + peerInfo, + InetSocketAddress.createUnresolved("1.2.3.4", 9876), + InetSocketAddress.createUnresolved("4.3.2.1", 6789)); + return Lists.newArrayList(p); + } + + private JsonRpcRequest adminPeers() { + return new JsonRpcRequest("2.0", "admin_peers", new Object[] {}); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAtTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAtTest.java new file mode 100755 index 00000000000..8319a4d9f8e --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugStorageRangeAtTest.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.BlockReplay; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.BlockReplay.Action; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.DebugStorageRangeAtResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.DebugStorageRangeAtResult.StorageEntry; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.NavigableMap; +import java.util.Optional; +import java.util.TreeMap; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; + +public class DebugStorageRangeAtTest { + + private static final int TRANSACTION_INDEX = 2; + private static final Bytes32 START_KEY_HASH = Bytes32.fromHexString("0x22"); + private final JsonRpcParameter parameters = new JsonRpcParameter(); + private final Blockchain blockchain = mock(Blockchain.class); + private final BlockchainQueries blockchainQueries = mock(BlockchainQueries.class); + private final BlockReplay blockReplay = mock(BlockReplay.class); + private final DebugStorageRangeAt debugStorageRangeAt = + new DebugStorageRangeAt(parameters, blockchainQueries, blockReplay); + private final MutableWorldState worldState = mock(MutableWorldState.class); + private final Account account = mock(Account.class); + private final TransactionProcessor transactionProcessor = mock(TransactionProcessor.class); + private final Transaction transaction = mock(Transaction.class); + + private final BlockHeader blockHeader = mock(BlockHeader.class); + private final Hash blockHash = + Hash.fromHexString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + private final Hash transactionHash = + Hash.fromHexString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + private final Address accountAddress = Address.MODEXP; + + @Before + public void setUp() { + when(transaction.hash()).thenReturn(transactionHash); + } + + @Test + public void nameShouldBeDebugStorageRangeAt() { + assertEquals("debug_storageRangeAt", debugStorageRangeAt.getName()); + } + + @Test + public void shouldRetrieveStorageRange() { + final TransactionWithMetadata transactionWithMetadata = + new TransactionWithMetadata(transaction, 12L, blockHash, TRANSACTION_INDEX); + final JsonRpcRequest request = + new JsonRpcRequest( + "2.0", + "debug_storageRangeAt", + new Object[] { + blockHash.toString(), TRANSACTION_INDEX, accountAddress, START_KEY_HASH.toString(), 10 + }); + + when(blockchainQueries.transactionByBlockHashAndIndex(blockHash, TRANSACTION_INDEX)) + .thenReturn(transactionWithMetadata); + when(worldState.get(accountAddress)).thenReturn(account); + when(blockReplay.afterTransactionInBlock(eq(blockHash), eq(transactionHash), any())) + .thenAnswer(this::callAction); + final NavigableMap rawEntries = new TreeMap<>(); + rawEntries.put(Bytes32.fromHexString("0x33"), UInt256.of(6)); + rawEntries.put(Bytes32.fromHexString("0x44"), UInt256.of(7)); + when(account.storageEntriesFrom(START_KEY_HASH, 11)).thenReturn(rawEntries); + final JsonRpcSuccessResponse response = + (JsonRpcSuccessResponse) debugStorageRangeAt.response(request); + final DebugStorageRangeAtResult result = (DebugStorageRangeAtResult) response.getResult(); + + assertThat(result).isNotNull(); + assertThat(result.getNextKey()).isNull(); + assertThat(result.getStorage()) + .containsExactly( + entry(Bytes32.fromHexString("0x33").toString(), new StorageEntry(UInt256.of(6))), + entry(Bytes32.fromHexString("0x44").toString(), new StorageEntry(UInt256.of(7)))); + } + + private Object callAction(final InvocationOnMock invocation) { + return Optional.of( + ((Action) invocation.getArgument(2)) + .performAction(transaction, blockHeader, blockchain, worldState, transactionProcessor)); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransactionTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransactionTest.java new file mode 100755 index 00000000000..66b7e6e2c14 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/DebugTraceTransactionTest.java @@ -0,0 +1,99 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Gas; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransactionTrace; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransactionTracer; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.DebugTraceTransactionResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.StructLog; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; +import net.consensys.pantheon.ethereum.vm.ExceptionalHaltReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.junit.Test; + +public class DebugTraceTransactionTest { + + private final JsonRpcParameter parameters = new JsonRpcParameter(); + private final BlockchainQueries blockchain = mock(BlockchainQueries.class); + private final TransactionTracer transactionTracer = mock(TransactionTracer.class); + private final DebugTraceTransaction debugTraceTransaction = + new DebugTraceTransaction(blockchain, transactionTracer, parameters); + private final Transaction transaction = mock(Transaction.class); + + private final BlockHeader blockHeader = mock(BlockHeader.class); + private final Hash blockHash = + Hash.fromHexString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + private final Hash transactionHash = + Hash.fromHexString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + @Test + public void nameShouldBeDebugTraceTransaction() { + assertEquals("debug_traceTransaction", debugTraceTransaction.getName()); + } + + @Test + public void shouldTraceTheTransactionUsingTheTransactionTracer() { + final TransactionWithMetadata transactionWithMetadata = + new TransactionWithMetadata(transaction, 12L, blockHash, 2); + final Map map = new HashMap<>(); + map.put("disableStorage", true); + final Object[] params = new Object[] {transactionHash, map}; + final JsonRpcRequest request = new JsonRpcRequest("2.0", "debug_traceTransaction", params); + final Result result = mock(Result.class); + + final TraceFrame traceFrame = + new TraceFrame( + 12, + "NONE", + Gas.of(45), + Optional.of(Gas.of(56)), + 2, + EnumSet.noneOf(ExceptionalHaltReason.class), + Optional.empty(), + Optional.empty(), + Optional.empty()); + final List traceFrames = Collections.singletonList(traceFrame); + final TransactionTrace transactionTrace = + new TransactionTrace(transaction, result, traceFrames); + when(transaction.getGasLimit()).thenReturn(100L); + when(result.getGasRemaining()).thenReturn(27L); + when(result.getOutput()).thenReturn(BytesValue.fromHexString("1234")); + when(blockHeader.getNumber()).thenReturn(12L); + when(blockchain.headBlockNumber()).thenReturn(12L); + when(blockchain.transactionByHash(transactionHash)) + .thenReturn(Optional.of(transactionWithMetadata)); + when(transactionTracer.traceTransaction(eq(blockHash), eq(transactionHash), any())) + .thenReturn(Optional.of(transactionTrace)); + final JsonRpcSuccessResponse response = + (JsonRpcSuccessResponse) debugTraceTransaction.response(request); + final DebugTraceTransactionResult transactionResult = + (DebugTraceTransactionResult) response.getResult(); + + assertEquals(73, transactionResult.getGas()); + assertEquals("1234", transactionResult.getReturnValue()); + final List expectedStructLogs = Collections.singletonList(new StructLog(traceFrame)); + assertEquals(expectedStructLogs, transactionResult.getStructLogs()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCallTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCallTest.java new file mode 100755 index 00000000000..ba01455eb3a --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCallTest.java @@ -0,0 +1,152 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessingResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessor; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthCallTest { + + private EthCall method; + + @Mock private BlockchainQueries blockchainQueries; + @Mock private TransientTransactionProcessor transientTransactionProcessor; + + @Before + public void setUp() { + method = new EthCall(blockchainQueries, transientTransactionProcessor, new JsonRpcParameter()); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("eth_call"); + } + + @Test + public void shouldThrowInvalidJsonRpcParametersExceptionWhenMissingToField() { + final CallParameter callParameter = new CallParameter("0x0", null, "0x0", "0x0", "0x0", ""); + final JsonRpcRequest request = ethCallRequest(callParameter, "latest"); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasNoCause() + .hasMessage("Missing \"to\" field in call arguments"); + } + + @Test + public void shouldReturnNullWhenProcessorReturnsEmpty() { + final JsonRpcRequest request = ethCallRequest(callParameter(), "latest"); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, null); + + when(transientTransactionProcessor.process(any(), anyLong())).thenReturn(Optional.empty()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + verify(transientTransactionProcessor).process(any(), anyLong()); + } + + @Test + public void shouldAcceptRequestWhenMissingOptionalFields() { + final CallParameter callParameter = new CallParameter(null, "0x0", null, null, null, null); + final JsonRpcRequest request = ethCallRequest(callParameter, "latest"); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, BytesValue.of().toString()); + + mockTransactionProcessorSuccessResult(BytesValue.of()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + verify(transientTransactionProcessor).process(eq(callParameter), anyLong()); + } + + @Test + public void shouldReturnExecutionResultWhenExecutionIsSuccessful() { + final JsonRpcRequest request = ethCallRequest(callParameter(), "latest"); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, BytesValue.of(1).toString()); + mockTransactionProcessorSuccessResult(BytesValue.of(1)); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + verify(transientTransactionProcessor).process(eq(callParameter()), anyLong()); + } + + @Test + public void shouldUseCorrectBlockNumberWhenLatest() { + final JsonRpcRequest request = ethCallRequest(callParameter(), "latest"); + when(blockchainQueries.headBlockNumber()).thenReturn(11L); + when(transientTransactionProcessor.process(any(), anyLong())).thenReturn(Optional.empty()); + + method.response(request); + + verify(transientTransactionProcessor).process(any(), eq(11L)); + } + + @Test + public void shouldUseCorrectBlockNumberWhenEarliest() { + final JsonRpcRequest request = ethCallRequest(callParameter(), "earliest"); + when(transientTransactionProcessor.process(any(), anyLong())).thenReturn(Optional.empty()); + method.response(request); + + verify(transientTransactionProcessor).process(any(), eq(0L)); + } + + @Test + public void shouldUseCorrectBlockNumberWhenSpecified() { + final JsonRpcRequest request = ethCallRequest(callParameter(), Quantity.create(13L)); + when(transientTransactionProcessor.process(any(), anyLong())).thenReturn(Optional.empty()); + + method.response(request); + + verify(transientTransactionProcessor).process(any(), eq(13L)); + } + + private CallParameter callParameter() { + return new CallParameter("0x0", "0x0", "0x0", "0x0", "0x0", ""); + } + + private JsonRpcRequest ethCallRequest( + final CallParameter callParameter, final String blockNumberInHex) { + return new JsonRpcRequest("2.0", "eth_call", new Object[] {callParameter, blockNumberInHex}); + } + + private void mockTransactionProcessorSuccessResult(final BytesValue output) { + final TransientTransactionProcessingResult result = + mock(TransientTransactionProcessingResult.class); + + when(result.getValidationResult()).thenReturn(ValidationResult.valid()); + when(result.getOutput()).thenReturn(output); + when(transientTransactionProcessor.process(any(), anyLong())).thenReturn(Optional.of(result)); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbaseTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbaseTest.java new file mode 100755 index 00000000000..2a82a60a8d7 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthCoinbaseTest.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthCoinbaseTest { + + @Mock private MiningCoordinator miningCoordinator; + private EthCoinbase method; + private final String JSON_RPC_VERSION = "2.0"; + private final String ETH_METHOD = "eth_coinbase"; + + @Before + public void setUp() { + method = new EthCoinbase(miningCoordinator); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void shouldReturnExpectedValueWhenMiningCoordinatorExists() { + final JsonRpcRequest request = requestWithParams(); + final String expectedAddressString = "fe3b557e8fb62b89f4916b721be55ceb828dbd73"; + final Address expectedAddress = Address.fromHexString(expectedAddressString); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(request.getId(), "0x" + expectedAddressString); + when(miningCoordinator.getCoinbase()).thenReturn(Optional.of(expectedAddress)); + + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(miningCoordinator).getCoinbase(); + verifyNoMoreInteractions(miningCoordinator); + } + + @Test + public void shouldReturnErrorWhenCoinbaseNotSpecified() { + final JsonRpcRequest request = requestWithParams(); + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.COINBASE_NOT_SPECIFIED); + when(miningCoordinator.getCoinbase()).thenReturn(Optional.empty()); + + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGasTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGasTest.java new file mode 100755 index 00000000000..a8808debcfc --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthEstimateGasTest.java @@ -0,0 +1,99 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessingResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessor; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthEstimateGasTest { + + private EthEstimateGas method; + + @Mock private BlockHeader blockHeader; + @Mock private Blockchain blockchain; + @Mock private BlockchainQueries blockchainQueries; + @Mock private TransientTransactionProcessor transientTransactionProcessor; + + @Before + public void setUp() { + when(blockchainQueries.headBlockNumber()).thenReturn(1L); + when(blockchainQueries.getBlockchain()).thenReturn(blockchain); + when(blockchain.getBlockHeader(eq(1L))).thenReturn(Optional.of(blockHeader)); + when(blockHeader.getGasLimit()).thenReturn(Long.MAX_VALUE); + when(blockHeader.getNumber()).thenReturn(1L); + + method = + new EthEstimateGas( + blockchainQueries, transientTransactionProcessor, new JsonRpcParameter()); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("eth_estimateGas"); + } + + @Test + public void shouldReturnErrorWhenTransientTransactionProcessorReturnsEmpty() { + final JsonRpcRequest request = ethEstimateGasRequest(callParameter()); + when(transientTransactionProcessor.process(eq(modifiedCallParameter()), eq(1L))) + .thenReturn(Optional.empty()); + + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.INTERNAL_ERROR); + + assertThat(method.response(request)).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnGasEstimateWhenTransientTransactionProcessorReturnsResult() { + final JsonRpcRequest request = ethEstimateGasRequest(callParameter()); + mockTransientProcessorResultGasEstimate(1L); + + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, Quantity.create(1L)); + + assertThat(method.response(request)).isEqualToComparingFieldByField(expectedResponse); + } + + private void mockTransientProcessorResultGasEstimate(final long gasEstimate) { + final TransientTransactionProcessingResult result = + mock(TransientTransactionProcessingResult.class); + when(result.getGasEstimate()).thenReturn(gasEstimate); + when(transientTransactionProcessor.process(eq(modifiedCallParameter()), eq(1L))) + .thenReturn(Optional.of(result)); + } + + private CallParameter callParameter() { + return new CallParameter("0x0", "0x0", "0x0", "0x0", "0x0", ""); + } + + private CallParameter modifiedCallParameter() { + return new CallParameter("0x0", "0x0", Quantity.create(Long.MAX_VALUE), "0x0", "0x0", ""); + } + + private JsonRpcRequest ethEstimateGasRequest(final CallParameter callParameter) { + return new JsonRpcRequest("2.0", "eth_estimateGas", new Object[] {callParameter}); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPriceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPriceTest.java new file mode 100755 index 00000000000..121c9147025 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGasPriceTest.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGasPriceTest { + + @Mock private MiningCoordinator miningCoordinator; + private EthGasPrice method; + private final String JSON_RPC_VERSION = "2.0"; + private final String ETH_METHOD = "eth_gasPrice"; + + @Before + public void setUp() { + method = new EthGasPrice(miningCoordinator); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void shouldReturnExpectedValueWhenMiningCoordinatorExists() { + final JsonRpcRequest request = requestWithParams(); + final String expectedWei = "0x4d2"; + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(request.getId(), expectedWei); + when(miningCoordinator.getMinTransactionGasPrice()).thenReturn(Wei.of(1234)); + + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(miningCoordinator).getMinTransactionGasPrice(); + verifyNoMoreInteractions(miningCoordinator); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHashTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHashTest.java new file mode 100755 index 00000000000..fa0ac4718f7 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetBlockByHashTest.java @@ -0,0 +1,107 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactory; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetBlockByHashTest { + + @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Mock private BlockchainQueries blockChainQueries; + private final BlockResultFactory blockResult = new BlockResultFactory(); + private final JsonRpcParameter parameters = new JsonRpcParameter(); + private EthGetBlockByHash method; + private final String JSON_RPC_VERSION = "2.0"; + private final String ETH_METHOD = "eth_getBlockByHash"; + private final String ZERO_HASH = String.valueOf(Hash.ZERO); + + @Before + public void setUp() { + method = new EthGetBlockByHash(blockChainQueries, blockResult, parameters); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void exceptionWhenNoParamsSupplied() { + final JsonRpcRequest request = requestWithParams(); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Missing required json rpc parameter at index 0"); + + method.response(request); + + verifyNoMoreInteractions(blockChainQueries); + } + + @Test + public void exceptionWhenNoHashSupplied() { + final JsonRpcRequest request = requestWithParams("false"); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Invalid json rpc parameter at index 0"); + + method.response(request); + + verifyNoMoreInteractions(blockChainQueries); + } + + @Test + public void exceptionWhenNoBoolSupplied() { + final JsonRpcRequest request = requestWithParams(ZERO_HASH); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Missing required json rpc parameter at index 1"); + + method.response(request); + + verifyNoMoreInteractions(blockChainQueries); + } + + @Test + public void exceptionWhenHashParamInvalid() { + final JsonRpcRequest request = requestWithParams("hash", "true"); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Invalid json rpc parameter at index 0"); + + method.response(request); + + verifyNoMoreInteractions(blockChainQueries); + } + + @Test + public void exceptionWhenBoolParamInvalid() { + final JsonRpcRequest request = requestWithParams(ZERO_HASH, "maybe"); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Invalid json rpc parameter at index 1"); + + method.response(request); + + verifyNoMoreInteractions(blockChainQueries); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChangesTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChangesTest.java new file mode 100755 index 00000000000..9565aece1b6 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterChangesTest.java @@ -0,0 +1,195 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.LogsResult; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetFilterChangesTest { + + private EthGetFilterChanges method; + + @Mock FilterManager filterManager; + + @Before + public void setUp() { + method = new EthGetFilterChanges(filterManager, new JsonRpcParameter()); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("eth_getFilterChanges"); + } + + @Test + public void shouldThrowExceptionWhenNoParamsSupplied() { + final JsonRpcRequest request = requestWithParams(); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + assertThat(thrown) + .hasNoCause() + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + + verifyZeroInteractions(filterManager); + } + + @Test + public void shouldThrowExceptionWhenInvalidParamsSupplied() { + final JsonRpcRequest request = requestWithParams(); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + + verifyZeroInteractions(filterManager); + } + + @Test + public void shouldReturnErrorResponseWhenFilterManagerDoesNotFindAnyFilters() { + final JsonRpcRequest request = requestWithParams("0x1"); + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.FILTER_NOT_FOUND); + + when(filterManager.blockChanges(anyString())).thenReturn(null); + when(filterManager.pendingTransactionChanges(anyString())).thenReturn(null); + when(filterManager.logsChanges(anyString())).thenReturn(null); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + verify(filterManager).blockChanges(eq("0x1")); + verify(filterManager).pendingTransactionChanges(eq("0x1")); + verify(filterManager).logsChanges(eq("0x1")); + } + + @Test + public void shouldReturnHashesWhenFilterManagerFindsBlockFilterWithHashes() { + final JsonRpcRequest request = requestWithParams("0x1"); + when(filterManager.blockChanges("0x1")).thenReturn(Lists.newArrayList(Hash.ZERO)); + + final List expectedHashes = + Lists.newArrayList(Hash.ZERO).stream().map(Hash::toString).collect(Collectors.toList()); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, expectedHashes); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnEmptyHashesWhenFilterManagerFindsBlockFilterWithNoChanges() { + final JsonRpcRequest request = requestWithParams("0x1"); + when(filterManager.blockChanges("0x1")).thenReturn(Lists.newArrayList()); + + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, Lists.newArrayList()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnHashesWhenFilterManagerFindsPendingTransactionFilterWithHashes() { + final JsonRpcRequest request = requestWithParams("0x1"); + when(filterManager.blockChanges(anyString())).thenReturn(null); + when(filterManager.pendingTransactionChanges("0x1")).thenReturn(Lists.newArrayList(Hash.ZERO)); + + final List expectedHashes = + Lists.newArrayList(Hash.ZERO).stream().map(Hash::toString).collect(Collectors.toList()); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, expectedHashes); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnEmptyHashesWhenFilterManagerFindsPendingTransactionFilterWithNoChanges() { + final JsonRpcRequest request = requestWithParams("0x1"); + when(filterManager.blockChanges(anyString())).thenReturn(null); + when(filterManager.pendingTransactionChanges("0x1")).thenReturn(Lists.newArrayList()); + + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, Lists.newArrayList()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnLogsWhenFilterManagerFindsLogFilterWithLogs() { + final JsonRpcRequest request = requestWithParams("0x1"); + when(filterManager.blockChanges(anyString())).thenReturn(null); + when(filterManager.pendingTransactionChanges(anyString())).thenReturn(null); + when(filterManager.logsChanges("0x1")).thenReturn(Lists.newArrayList(logWithMetadata())); + + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, new LogsResult(Lists.newArrayList(logWithMetadata()))); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + @Test + public void shouldReturnEmptyLogsWhenFilterManagerFindsLogFilterWithNoChanges() { + final JsonRpcRequest request = requestWithParams("0x1"); + when(filterManager.blockChanges(anyString())).thenReturn(null); + when(filterManager.pendingTransactionChanges(anyString())).thenReturn(null); + when(filterManager.logsChanges("0x1")).thenReturn(Lists.newArrayList()); + + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, new LogsResult(Lists.newArrayList())); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest("2.0", "eth_getFilterChanges", params); + } + + private LogWithMetadata logWithMetadata() { + return LogWithMetadata.create( + 0, + 100L, + Hash.ZERO, + Hash.ZERO, + 0, + Address.fromHexString("0x0"), + BytesValue.EMPTY, + Lists.newArrayList(), + false); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogsTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogsTest.java new file mode 100755 index 00000000000..c542ec080fd --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetFilterLogsTest.java @@ -0,0 +1,128 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.LogsResult; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetFilterLogsTest { + + private EthGetFilterLogs method; + + @Mock FilterManager filterManager; + + @Before + public void setUp() { + method = new EthGetFilterLogs(filterManager, new JsonRpcParameter()); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("eth_getFilterLogs"); + } + + @Test + public void shouldReturnErrorWhenMissingParams() { + final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_getFilterLogs", new Object[] {}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + + verifyZeroInteractions(filterManager); + } + + @Test + public void shouldReturnErrorWhenMissingFilterId() { + final JsonRpcRequest request = requestWithFilterId(); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + + verifyZeroInteractions(filterManager); + } + + @Test + public void shouldReturnFilterNotFoundWhenFilterManagerReturnsNull() { + final JsonRpcRequest request = requestWithFilterId("NOT FOUND"); + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.FILTER_NOT_FOUND); + when(filterManager.logs(eq("NOT FOUND"))).thenReturn(null); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnEmptyListWhenFilterManagerReturnsEmpty() { + final JsonRpcRequest request = requestWithFilterId("0x1"); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, new LogsResult(new ArrayList<>())); + when(filterManager.logs(eq("0x1"))).thenReturn(new ArrayList<>()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + @Test + public void shouldReturnExpectedLogsWhenFilterManagerReturnsLogs() { + final JsonRpcRequest request = requestWithFilterId("0x1"); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(null, new LogsResult(logs())); + when(filterManager.logs(eq("0x1"))).thenReturn(logs()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + private JsonRpcRequest requestWithFilterId(final Object... filterId) { + return new JsonRpcRequest("2.0", "eth_getFilterLogs", filterId); + } + + private List logs() { + return Arrays.asList( + LogWithMetadata.create( + 0, + 100L, + Hash.ZERO, + Hash.ZERO, + 0, + Address.fromHexString("0x0"), + BytesValue.EMPTY, + Lists.newArrayList(), + false)); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCountTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCountTest.java new file mode 100755 index 00000000000..b8f52c319d8 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionCountTest.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.PendingTransactions; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.OptionalLong; + +import org.junit.Test; + +public class EthGetTransactionCountTest { + + private final JsonRpcParameter parameters = new JsonRpcParameter(); + private final BlockchainQueries blockchain = mock(BlockchainQueries.class); + private final PendingTransactions pendingTransactions = mock(PendingTransactions.class); + + private final EthGetTransactionCount ethGetTransactionCount = + new EthGetTransactionCount(blockchain, pendingTransactions, parameters); + private final String pendingTransactionString = "0x00000000000000000000000000000000000000AA"; + private final Object[] pendingParams = new Object[] {pendingTransactionString, "pending"}; + + @Test + public void shouldUsePendingTransactionsWhenToldTo() { + when(pendingTransactions.getNextNonceForSender(Address.fromHexString(pendingTransactionString))) + .thenReturn(OptionalLong.of(12)); + final JsonRpcRequest request = + new JsonRpcRequest("1", "eth_getTransactionCount", pendingParams); + final JsonRpcSuccessResponse response = + (JsonRpcSuccessResponse) ethGetTransactionCount.response(request); + assertEquals("0xc", response.getResult()); + } + + @Test + public void shouldUseLatestTransactionsWhenNoPendingTransactions() { + final Address address = Address.fromHexString(pendingTransactionString); + when(pendingTransactions.getNextNonceForSender(address)).thenReturn(OptionalLong.empty()); + when(blockchain.headBlockNumber()).thenReturn(1L); + when(blockchain.getTransactionCount(address, 1L)).thenReturn(7L); + final JsonRpcRequest request = + new JsonRpcRequest("1", "eth_getTransactionCount", pendingParams); + final JsonRpcSuccessResponse response = + (JsonRpcSuccessResponse) ethGetTransactionCount.response(request); + assertEquals("0x7", response.getResult()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java new file mode 100755 index 00000000000..4e9d3b7b28f --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetTransactionReceiptTest.java @@ -0,0 +1,142 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionReceiptWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionReceiptRootResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.TransactionReceiptStatusResult; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.TransactionReceiptType; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.Optional; + +import org.junit.Test; + +public class EthGetTransactionReceiptTest { + + private final TransactionReceipt stateReceipt = + new TransactionReceipt(1, 12, Collections.emptyList()); + private final Hash stateRoot = + Hash.fromHexString("0000000000000000000000000000000000000000000000000000000000000000"); + private final TransactionReceipt rootReceipt = + new TransactionReceipt(stateRoot, 12, Collections.emptyList()); + + private final Signature signature = Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 1); + private final Address sender = + Address.fromHexString("0x0000000000000000000000000000000000000003"); + private final Transaction transaction = + Transaction.builder() + .nonce(1) + .gasPrice(Wei.of(12)) + .gasLimit(43) + .payload(BytesValue.EMPTY) + .value(Wei.ZERO) + .signature(signature) + .sender(sender) + .build(); + + private final Hash hash = + Hash.fromHexString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + private final Hash blockHash = + Hash.fromHexString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + private final TransactionReceiptWithMetadata stateReceiptWithMetaData = + TransactionReceiptWithMetadata.create(stateReceipt, transaction, hash, 1, 2, blockHash, 4); + private final TransactionReceiptWithMetadata rootReceiptWithMetaData = + TransactionReceiptWithMetadata.create(rootReceipt, transaction, hash, 1, 2, blockHash, 4); + + private final ProtocolSpec rootTransactionTypeSpec = + new ProtocolSpec<>( + "root", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + TransactionReceiptType.ROOT, + BlockHeader::getCoinbase); + private final ProtocolSpec statusTransactionTypeSpec = + new ProtocolSpec<>( + "status", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + TransactionReceiptType.STATUS, + BlockHeader::getCoinbase); + + private final JsonRpcParameter parameters = new JsonRpcParameter(); + + @SuppressWarnings("unchecked") + private final ProtocolSchedule protocolSchedule = mock(ProtocolSchedule.class); + + private final BlockchainQueries blockchain = mock(BlockchainQueries.class); + private final EthGetTransactionReceipt ethGetTransactionReceipt = + new EthGetTransactionReceipt(blockchain, parameters); + private final String receiptString = + "0xcbef69eaf44af151aa66677ae4b8d8c343a09f667c873a3a6f4558fa4051fa5f"; + private final Hash receiptHash = + Hash.fromHexString("cbef69eaf44af151aa66677ae4b8d8c343a09f667c873a3a6f4558fa4051fa5f"); + Object[] params = new Object[] {receiptString}; + private final JsonRpcRequest request = + new JsonRpcRequest("1", "eth_getTransactionReceipt", params);; + + @Test + public void shouldCreateAStatusTransactionReceiptWhenStatusTypeProtocol() { + when(blockchain.headBlockNumber()).thenReturn(1L); + when(blockchain.transactionReceiptByTransactionHash(receiptHash)) + .thenReturn(Optional.of(stateReceiptWithMetaData)); + when(protocolSchedule.getByBlockNumber(1)).thenReturn(statusTransactionTypeSpec); + + final JsonRpcSuccessResponse response = + (JsonRpcSuccessResponse) ethGetTransactionReceipt.response(request); + final TransactionReceiptStatusResult result = + (TransactionReceiptStatusResult) response.getResult(); + + assertEquals("0x1", result.getStatus()); + } + + @Test + public void shouldCreateARootTransactionReceiptWhenRootTypeProtocol() { + when(blockchain.headBlockNumber()).thenReturn(1L); + when(blockchain.transactionReceiptByTransactionHash(receiptHash)) + .thenReturn(Optional.of(rootReceiptWithMetaData)); + when(protocolSchedule.getByBlockNumber(1)).thenReturn(rootTransactionTypeSpec); + + final JsonRpcSuccessResponse response = + (JsonRpcSuccessResponse) ethGetTransactionReceipt.response(request); + final TransactionReceiptRootResult result = (TransactionReceiptRootResult) response.getResult(); + + assertEquals(stateRoot.toString(), result.getRoot()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndexTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndexTest.java new file mode 100755 index 00000000000..c13739a57a0 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockHashAndIndexTest.java @@ -0,0 +1,160 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionTestFixture; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetUncleByBlockHashAndIndexTest { + + private final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture(); + private final TransactionTestFixture transactionTestFixture = new TransactionTestFixture(); + + private EthGetUncleByBlockHashAndIndex method; + private final Hash zeroHash = Hash.ZERO; + + @Mock private BlockchainQueries blockchainQueries; + + @Before + public void before() { + this.method = new EthGetUncleByBlockHashAndIndex(blockchainQueries, new JsonRpcParameter()); + } + + @Test + public void methodShouldReturnExpectedName() { + assertThat(method.getName()).isEqualTo("eth_getUncleByBlockHashAndIndex"); + } + + @Test + public void shouldReturnErrorWhenMissingBlockHashParam() { + final JsonRpcRequest request = getUncleByBlockHashAndIndex(new Object[] {}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + } + + @Test + public void shouldReturnErrorWhenMissingIndexParam() { + final JsonRpcRequest request = getUncleByBlockHashAndIndex(new Object[] {zeroHash}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 1"); + } + + @Test + public void shouldReturnErrorWhenInvalidBlockHashParam() { + final JsonRpcRequest request = getUncleByBlockHashAndIndex(new Object[] {"not-a-hash"}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Invalid json rpc parameter at index 0"); + } + + @Test + public void shouldReturnErrorWhenInvalidIndexParam() { + final JsonRpcRequest request = + getUncleByBlockHashAndIndex(new Object[] {zeroHash, "not-an-index"}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Invalid json rpc parameter at index 1"); + } + + @Test + public void shouldReturnNullResultWhenBlockDoesNotHaveOmmer() { + final JsonRpcRequest request = getUncleByBlockHashAndIndex(new Object[] {zeroHash, "0x0"}); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, null); + + when(blockchainQueries.getOmmer(eq(zeroHash), eq(0))).thenReturn(Optional.empty()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + @Test + public void shouldReturnExpectedBlockResult() { + final JsonRpcRequest request = getUncleByBlockHashAndIndex(new Object[] {zeroHash, "0x0"}); + final BlockHeader header = blockHeaderTestFixture.buildHeader(); + final BlockResult expectedBlockResult = blockResult(header); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, expectedBlockResult); + + when(blockchainQueries.getOmmer(eq(zeroHash), eq(0))).thenReturn(Optional.of(header)); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + private BlockResult blockResult(final BlockHeader header) { + final Block block = + new Block(header, new BlockBody(Collections.emptyList(), Collections.emptyList())); + return new BlockResult( + header, + Collections.emptyList(), + Collections.emptyList(), + UInt256.ZERO, + block.calculateSize()); + } + + private JsonRpcRequest getUncleByBlockHashAndIndex(final Object[] params) { + return new JsonRpcRequest("2.0", "eth_getUncleByBlockHashAndIndex", params); + } + + public BlockWithMetadata blockWithMetadata( + final BlockHeader header) { + final KeyPair keyPair = KeyPair.generate(); + final List transactions = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + final Transaction transaction = transactionTestFixture.createTransaction(keyPair); + transactions.add( + new TransactionWithMetadata(transaction, header.getNumber(), header.getHash(), 0)); + } + + final List ommers = new ArrayList<>(); + ommers.add(Hash.ZERO); + + return new BlockWithMetadata<>(header, transactions, ommers, header.getDifficulty(), 0); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndexTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndexTest.java new file mode 100755 index 00000000000..8e81c3177ed --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthGetUncleByBlockNumberAndIndexTest.java @@ -0,0 +1,136 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionTestFixture; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetUncleByBlockNumberAndIndexTest { + + private final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture(); + private final TransactionTestFixture transactionTestFixture = new TransactionTestFixture(); + + private EthGetUncleByBlockNumberAndIndex method; + + @Mock private BlockchainQueries blockchainQueries; + + @Before + public void before() { + this.method = new EthGetUncleByBlockNumberAndIndex(blockchainQueries, new JsonRpcParameter()); + } + + @Test + public void methodShouldReturnExpectedName() { + assertThat(method.getName()).isEqualTo("eth_getUncleByBlockNumberAndIndex"); + } + + @Test + public void shouldReturnErrorWhenMissingBlockNumberParam() { + final JsonRpcRequest request = getUncleByBlockNumberAndIndex(new Object[] {}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + } + + @Test + public void shouldReturnErrorWhenMissingIndexParam() { + final JsonRpcRequest request = getUncleByBlockNumberAndIndex(new Object[] {"0x1"}); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 1"); + } + + @Test + public void shouldReturnNullResultWhenBlockDoesNotHaveOmmer() { + final JsonRpcRequest request = getUncleByBlockNumberAndIndex(new Object[] {"0x1", "0x0"}); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, null); + + when(blockchainQueries.getOmmer(eq(1L), eq(0))).thenReturn(Optional.empty()); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + @Test + public void shouldReturnExpectedBlockResult() { + final JsonRpcRequest request = getUncleByBlockNumberAndIndex(new Object[] {"0x1", "0x0"}); + final BlockHeader header = blockHeaderTestFixture.buildHeader(); + final BlockResult expectedBlockResult = blockResult(header); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, expectedBlockResult); + + when(blockchainQueries.getOmmer(eq(1L), eq(0))).thenReturn(Optional.of(header)); + + final JsonRpcResponse response = method.response(request); + + assertThat(response).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + private BlockResult blockResult(final BlockHeader header) { + final Block block = + new Block(header, new BlockBody(Collections.emptyList(), Collections.emptyList())); + return new BlockResult( + header, + Collections.emptyList(), + Collections.emptyList(), + UInt256.ZERO, + block.calculateSize()); + } + + private JsonRpcRequest getUncleByBlockNumberAndIndex(final Object[] params) { + return new JsonRpcRequest("2.0", "eth_getUncleByBlockNumberAndIndex", params); + } + + public BlockWithMetadata blockWithMetadata( + final BlockHeader header) { + final KeyPair keyPair = KeyPair.generate(); + final List transactions = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + final Transaction transaction = transactionTestFixture.createTransaction(keyPair); + transactions.add( + new TransactionWithMetadata(transaction, header.getNumber(), header.getHash(), 0)); + } + + final List ommers = new ArrayList<>(); + ommers.add(Hash.ZERO); + + return new BlockWithMetadata<>(header, transactions, ommers, header.getDifficulty(), 0); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMiningTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMiningTest.java new file mode 100755 index 00000000000..04de4a9b48e --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthMiningTest.java @@ -0,0 +1,64 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthMiningTest { + + @Mock private MiningCoordinator miningCoordinator; + private EthMining method; + private final String JSON_RPC_VERSION = "2.0"; + private final String ETH_METHOD = "eth_mining"; + + @Before + public void setUp() { + method = new EthMining(miningCoordinator); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void shouldReturnTrueWhenMiningCoordinatorExistsAndRunning() { + final JsonRpcRequest request = requestWithParams(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), true); + when(miningCoordinator.isRunning()).thenReturn(true); + + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(miningCoordinator).isRunning(); + verifyNoMoreInteractions(miningCoordinator); + } + + @Test + public void shouldReturnFalseWhenMiningCoordinatorExistsAndDisabled() { + final JsonRpcRequest request = requestWithParams(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), false); + when(miningCoordinator.isRunning()).thenReturn(false); + + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(miningCoordinator).isRunning(); + verifyNoMoreInteractions(miningCoordinator); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilterTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilterTest.java new file mode 100755 index 00000000000..05b69eee06b --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewBlockFilterTest.java @@ -0,0 +1,46 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthNewBlockFilterTest { + + @Mock private FilterManager filterManager; + private EthNewBlockFilter method; + private final String ETH_METHOD = "eth_newBlockFilter"; + + @Before + public void setUp() { + method = new EthNewBlockFilter(filterManager); + } + + @Test + public void getMethodReturnsExpectedName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void getResponse() { + when(filterManager.installBlockFilter()).thenReturn("0x0"); + final JsonRpcRequest request = new JsonRpcRequest("2.0", ETH_METHOD, new String[] {}); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), "0x0"); + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(filterManager).installBlockFilter(); + verifyNoMoreInteractions(filterManager); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilterTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilterTest.java new file mode 100755 index 00000000000..c729b4ef40d --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthNewFilterTest.java @@ -0,0 +1,159 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.LogsQuery; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.TopicsParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthNewFilterTest { + + @Mock private FilterManager filterManager; + private EthNewFilter method; + private final String ETH_METHOD = "eth_newFilter"; + + @Before + public void setUp() { + method = new EthNewFilter(filterManager, new JsonRpcParameter()); + } + + @Test + public void methodReturnsExpectedMethodName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void newFilterWithoutFromBlockParamUsesLatestAsDefault() { + final FilterParameter filterParameter = new FilterParameter(null, null, null, null, null); + final JsonRpcRequest request = ethNewFilter(filterParameter); + + method.response(request); + + verify(filterManager).installLogFilter(refEq(blockParamLatest()), any(), any()); + } + + @Test + public void newFilterWithoutToBlockParamUsesLatestAsDefault() { + final FilterParameter filterParameter = new FilterParameter(null, null, null, null, null); + final JsonRpcRequest request = ethNewFilter(filterParameter); + + method.response(request); + + verify(filterManager).installLogFilter(any(), refEq(blockParamLatest()), any()); + } + + @Test + public void newFilterWithoutAddressAndTopicsParamsInstallsEmptyLogFilter() { + final FilterParameter filterParameter = + new FilterParameter("latest", "latest", null, null, null); + final JsonRpcRequest request = ethNewFilter(filterParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), "0x1"); + + final LogsQuery expectedLogsQuery = new LogsQuery.Builder().build(); + when(filterManager.installLogFilter(any(), any(), refEq(expectedLogsQuery))).thenReturn("0x1"); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(filterManager) + .installLogFilter( + refEq(blockParamLatest()), refEq(blockParamLatest()), refEq(expectedLogsQuery)); + } + + @Test + public void newFilterWithTopicsOnlyParamInstallsExpectedLogFilter() { + final List> topics = topics(); + final FilterParameter filterParameter = filterParamWithAddressAndTopics(null, topics); + final JsonRpcRequest request = ethNewFilter(filterParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), "0x1"); + + final LogsQuery expectedLogsQuery = + new LogsQuery.Builder().topics(new TopicsParameter(topics)).build(); + when(filterManager.installLogFilter(any(), any(), refEq(expectedLogsQuery))).thenReturn("0x1"); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(filterManager) + .installLogFilter( + refEq(blockParamLatest()), refEq(blockParamLatest()), refEq(expectedLogsQuery)); + } + + @Test + public void newFilterWithAddressOnlyParamInstallsExpectedLogFilter() { + final Address address = Address.fromHexString("0x0"); + final FilterParameter filterParameter = filterParamWithAddressAndTopics(address, null); + final JsonRpcRequest request = ethNewFilter(filterParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), "0x1"); + + final LogsQuery expectedLogsQuery = new LogsQuery.Builder().address(address).build(); + when(filterManager.installLogFilter(any(), any(), refEq(expectedLogsQuery))).thenReturn("0x1"); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(filterManager) + .installLogFilter( + refEq(blockParamLatest()), refEq(blockParamLatest()), refEq(expectedLogsQuery)); + } + + @Test + public void newFilterWithAddressAndTopicsParamInstallsExpectedLogFilter() { + final Address address = Address.fromHexString("0x0"); + final List> topics = topics(); + final FilterParameter filterParameter = filterParamWithAddressAndTopics(address, topics); + final JsonRpcRequest request = ethNewFilter(filterParameter); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), "0x1"); + + final LogsQuery expectedLogsQuery = + new LogsQuery.Builder().address(address).topics(new TopicsParameter(topics)).build(); + when(filterManager.installLogFilter(any(), any(), refEq(expectedLogsQuery))).thenReturn("0x1"); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(filterManager) + .installLogFilter( + refEq(blockParamLatest()), refEq(blockParamLatest()), refEq(expectedLogsQuery)); + } + + private List> topics() { + return Arrays.asList( + Arrays.asList("0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b")); + } + + private FilterParameter filterParamWithAddressAndTopics( + final Address address, final List> topics) { + final List addresses = address != null ? Arrays.asList(address.toString()) : null; + return new FilterParameter("latest", "latest", addresses, topics, null); + } + + private JsonRpcRequest ethNewFilter(final FilterParameter filterParameter) { + return new JsonRpcRequest("2.0", ETH_METHOD, new Object[] {filterParameter}); + } + + private BlockParameter blockParamLatest() { + return new BlockParameter("latest"); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersionTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersionTest.java new file mode 100755 index 00000000000..5b9f242c189 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthProtocolVersionTest.java @@ -0,0 +1,75 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +public class EthProtocolVersionTest { + private EthProtocolVersion method; + private final String JSON_RPC_VERSION = "2.0"; + private final String ETH_METHOD = "eth_protocolVersion"; + private Set supportedCapabilities; + + @Test + public void returnsCorrectMethodName() { + setupSupportedEthProtocols(); + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void shouldReturn63WhenMaxProtocolIsETH63() { + + setupSupportedEthProtocols(); + + final JsonRpcRequest request = requestWithParams(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), 63); + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnNullNoEthProtocolsSupported() { + + supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(Capability.create("istanbul", 64)); + method = new EthProtocolVersion(supportedCapabilities); + + final JsonRpcRequest request = requestWithParams(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), null); + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturn63WhenMixedProtocolsSupported() { + + setupSupportedEthProtocols(); + supportedCapabilities.add(Capability.create("istanbul", 64)); + method = new EthProtocolVersion(supportedCapabilities); + + final JsonRpcRequest request = requestWithParams(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), 63); + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } + + private void setupSupportedEthProtocols() { + supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + method = new EthProtocolVersion(supportedCapabilities); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java new file mode 100755 index 00000000000..47071f7d376 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSendRawTransactionTest.java @@ -0,0 +1,150 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason; +import net.consensys.pantheon.ethereum.mainnet.ValidationResult; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthSendRawTransactionTest { + + private static final String VALID_TRANSACTION = + "0xf86d0485174876e800830222e0945aae326516b4f8fe08074b7e972e40a713048d62880de0b6b3a7640000801ba05d4e7998757264daab67df2ce6f7e7a0ae36910778a406ca73898c9899a32b9ea0674700d5c3d1d27f2e6b4469957dfd1a1c49bf92383d80717afc84eb05695d5b"; + @Mock private TransactionPool transactionPool; + + @Mock private JsonRpcParameter parameter; + + private EthSendRawTransaction method; + + @Before + public void before() { + method = new EthSendRawTransaction(transactionPool, parameter); + } + + @Test + public void requestIsMissingParameter() { + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "eth_sendRawTransaction", new String[] {}); + + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void invalidTransactionRlpDecoding() { + final String rawTransaction = "0x00"; + when(parameter.required(any(Object[].class), anyInt(), any())).thenReturn(rawTransaction); + + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "eth_sendRawTransaction", new String[] {rawTransaction}); + + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void validTransactionIsSentToTransactionPool() { + when(parameter.required(any(Object[].class), anyInt(), any())).thenReturn(VALID_TRANSACTION); + when(transactionPool.addLocalTransaction(any(Transaction.class))) + .thenReturn(ValidationResult.valid()); + + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "eth_sendRawTransaction", new String[] {VALID_TRANSACTION}); + + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse( + request.getId(), "0xbaabcc1bd699e7378451e4ce5969edb9bdcae76cb79bdacae793525c31e423c7"); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(transactionPool).addLocalTransaction(any(Transaction.class)); + } + + @Test + public void transactionWithNonceBelowAccountNonceIsRejected() { + verifyErrorForInvalidTransaction( + TransactionInvalidReason.NONCE_TOO_LOW, JsonRpcError.NONCE_TOO_LOW); + } + + @Test + public void transactionWithNonceAboveAccountNonceIsRejected() { + verifyErrorForInvalidTransaction( + TransactionInvalidReason.INCORRECT_NONCE, JsonRpcError.INCORRECT_NONCE); + } + + @Test + public void transactionWithInvalidSignatureIsRejected() { + verifyErrorForInvalidTransaction( + TransactionInvalidReason.INVALID_SIGNATURE, JsonRpcError.INVALID_TRANSACTION_SIGNATURE); + } + + @Test + public void transactionWithIntrinsicGasExceedingGasLimitIsRejected() { + verifyErrorForInvalidTransaction( + TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT, + JsonRpcError.INTRINSIC_GAS_EXCEEDS_LIMIT); + } + + @Test + public void transactionWithUpfrontGasExceedingAccountBalanceIsRejected() { + verifyErrorForInvalidTransaction( + TransactionInvalidReason.UPFRONT_COST_EXCEEDS_BALANCE, + JsonRpcError.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE); + } + + @Test + public void transactionWithGasLimitExceedingBlockGasLimitIsRejected() { + verifyErrorForInvalidTransaction( + TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT, JsonRpcError.EXCEEDS_BLOCK_GAS_LIMIT); + } + + private void verifyErrorForInvalidTransaction( + final TransactionInvalidReason transactionInvalidReason, final JsonRpcError expectedError) { + when(parameter.required(any(Object[].class), anyInt(), any())).thenReturn(VALID_TRANSACTION); + when(transactionPool.addLocalTransaction(any(Transaction.class))) + .thenReturn(ValidationResult.invalid(transactionInvalidReason)); + + final JsonRpcRequest request = + new JsonRpcRequest("2.0", "eth_sendRawTransaction", new String[] {VALID_TRANSACTION}); + + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), expectedError); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(transactionPool).addLocalTransaction(any(Transaction.class)); + } + + @Test + public void getMethodReturnsExpectedName() { + assertThat(method.getName()).matches("eth_sendRawTransaction"); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncingTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncingTest.java new file mode 100755 index 00000000000..4e9554d9229 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/EthSyncingTest.java @@ -0,0 +1,72 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.SyncStatus; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.SyncingResult; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthSyncingTest { + + @Mock private Synchronizer synchronizer; + private EthSyncing method; + private final String JSON_RPC_VERSION = "2.0"; + private final String ETH_METHOD = "eth_syncing"; + + @Before + public void setUp() { + method = new EthSyncing(synchronizer); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void shouldReturnFalseWhenSyncStatusIsEmpty() { + final JsonRpcRequest request = requestWithParams(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(request.getId(), false); + final Optional optionalSyncStatus = Optional.empty(); + when(synchronizer.getSyncStatus()).thenReturn(optionalSyncStatus); + + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(synchronizer).getSyncStatus(); + verifyNoMoreInteractions(synchronizer); + } + + @Test + public void shouldReturnExpectedValueWhenSyncStatusIsNotEmpty() { + final JsonRpcRequest request = requestWithParams(); + final SyncStatus expectedSyncStatus = new SyncStatus(0, 1, 2); + final JsonRpcResponse expectedResponse = + new JsonRpcSuccessResponse(request.getId(), new SyncingResult(expectedSyncStatus)); + final Optional optionalSyncStatus = Optional.of(expectedSyncStatus); + when(synchronizer.getSyncStatus()).thenReturn(optionalSyncStatus); + + final JsonRpcResponse actualResponse = method.response(request); + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + verify(synchronizer).getSyncStatus(); + verifyNoMoreInteractions(synchronizer); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListeningTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListeningTest.java new file mode 100755 index 00000000000..5f04be3429c --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/NetListeningTest.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NetListeningTest { + + private NetListening method; + + @Mock private P2PNetwork p2PNetwork; + + @Before + public void before() { + this.method = new NetListening(p2PNetwork); + } + + @Test + public void shouldReturnTrueWhenNetworkIsListening() { + when(p2PNetwork.isListening()).thenReturn(true); + + final JsonRpcRequest request = netListeningRequest(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, true); + + assertThat(method.response(request)).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnFalseWhenNetworkIsNotListening() { + when(p2PNetwork.isListening()).thenReturn(false); + + final JsonRpcRequest request = netListeningRequest(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, false); + + assertThat(method.response(request)).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest netListeningRequest() { + return new JsonRpcRequest("2.0", "net_listening", new Object[] {}); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/TransientTransactionProcessorTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/TransientTransactionProcessorTest.java new file mode 100755 index 00000000000..3ca9eb72ac7 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/TransientTransactionProcessorTest.java @@ -0,0 +1,230 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.CallParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessingResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.processor.TransientTransactionProcessor; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result.Status; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +@SuppressWarnings({"rawtypes", "unchecked"}) +public class TransientTransactionProcessorTest { + + private static final SECP256K1.Signature FAKE_SIGNATURE = + SECP256K1.Signature.create(SECP256K1.HALF_CURVE_ORDER, SECP256K1.HALF_CURVE_ORDER, (byte) 0); + + private static final Address DEFAULT_FROM = + Address.fromHexString("0x0000000000000000000000000000000000000000"); + + private TransientTransactionProcessor transientTransactionProcessor; + + @Mock private Blockchain blockchain; + @Mock private WorldStateArchive worldStateArchive; + @Mock private MutableWorldState worldState; + @Mock private ProtocolSchedule protocolSchedule; + @Mock private ProtocolSpec protocolSpec; + @Mock private TransactionProcessor transactionProcessor; + + @Before + public void setUp() { + this.transientTransactionProcessor = + new TransientTransactionProcessor(blockchain, worldStateArchive, protocolSchedule); + } + + @Test + public void shouldReturnEmptyWhenBlockDoesNotExist() { + when(blockchain.getBlockHeader(eq(1L))).thenReturn(Optional.empty()); + + final Optional result = + transientTransactionProcessor.process(callParameter(), 1L); + + assertThat(result.isPresent()).isFalse(); + } + + @Test + public void shouldReturnSuccessfulResultWhenProcessingIsSuccessful() { + final CallParameter callParameter = callParameter(); + + mockBlockchainForBlockHeader(Hash.ZERO, 1L); + mockWorldStateForAccount(Hash.ZERO, callParameter.getFrom(), 1L); + + final Transaction expectedTransaction = + Transaction.builder() + .nonce(1L) + .gasPrice(callParameter.getGasPrice()) + .gasLimit(callParameter.getGasLimit()) + .to(callParameter.getTo()) + .sender(callParameter.getFrom()) + .value(callParameter.getValue()) + .payload(callParameter.getPayload()) + .signature(FAKE_SIGNATURE) + .build(); + mockProcessorStatusForTransaction( + 1L, expectedTransaction, Status.SUCCESSFUL, callParameter.getPayload()); + + final Optional result = + transientTransactionProcessor.process(callParameter, 1L); + + assertThat(result.get().isSuccessful()).isTrue(); + verifyTransactionWasProcessed(expectedTransaction); + } + + @Test + public void shouldUseDefaultValuesWhenMissingOptionalFields() { + final CallParameter callParameter = callParameter(); + + mockBlockchainForBlockHeader(Hash.ZERO, 1L); + mockWorldStateForAccount(Hash.ZERO, Address.fromHexString("0x0"), 1L); + + final Transaction expectedTransaction = + Transaction.builder() + .nonce(1L) + .gasPrice(Wei.ZERO) + .gasLimit(0L) + .to(DEFAULT_FROM) + .sender(Address.fromHexString("0x0")) + .value(Wei.ZERO) + .payload(BytesValue.EMPTY) + .signature(FAKE_SIGNATURE) + .build(); + mockProcessorStatusForTransaction(1L, expectedTransaction, Status.SUCCESSFUL, BytesValue.of()); + + transientTransactionProcessor.process(callParameter, 1L); + + verifyTransactionWasProcessed(expectedTransaction); + } + + @Test + public void shouldUseZeroNonceWhenAccountDoesNotExist() { + final CallParameter callParameter = callParameter(); + + mockBlockchainForBlockHeader(Hash.ZERO, 1L); + mockWorldStateForAbsentAccount(Hash.ZERO); + + final Transaction expectedTransaction = + Transaction.builder() + .nonce(0L) + .gasPrice(Wei.ZERO) + .gasLimit(0L) + .to(DEFAULT_FROM) + .sender(Address.fromHexString("0x0")) + .value(Wei.ZERO) + .payload(BytesValue.EMPTY) + .signature(FAKE_SIGNATURE) + .build(); + mockProcessorStatusForTransaction(1L, expectedTransaction, Status.SUCCESSFUL, BytesValue.of()); + + transientTransactionProcessor.process(callParameter, 1L); + + verifyTransactionWasProcessed(expectedTransaction); + } + + @Test + public void shouldReturnFailureResultWhenProcessingFails() { + final CallParameter callParameter = callParameter(); + + mockBlockchainForBlockHeader(Hash.ZERO, 1L); + mockWorldStateForAccount(Hash.ZERO, Address.fromHexString("0x0"), 1L); + + final Transaction expectedTransaction = + Transaction.builder() + .nonce(1L) + .gasPrice(callParameter.getGasPrice()) + .gasLimit(callParameter.getGasLimit()) + .to(callParameter.getTo()) + .sender(callParameter.getFrom()) + .value(callParameter.getValue()) + .payload(callParameter.getPayload()) + .signature(FAKE_SIGNATURE) + .build(); + mockProcessorStatusForTransaction(1L, expectedTransaction, Status.FAILED, null); + + final Optional result = + transientTransactionProcessor.process(callParameter, 1L); + + assertThat(result.get().isSuccessful()).isFalse(); + verifyTransactionWasProcessed(expectedTransaction); + } + + private void mockWorldStateForAccount( + final Hash stateRoot, final Address address, final long nonce) { + final Account account = mock(Account.class); + when(account.getNonce()).thenReturn(nonce); + when(worldStateArchive.getMutable(eq(stateRoot))).thenReturn(worldState); + when(worldState.get(eq(address))).thenReturn(account); + } + + private void mockWorldStateForAbsentAccount(final Hash stateRoot) { + when(worldStateArchive.getMutable(eq(stateRoot))).thenReturn(worldState); + when(worldState.get(any())).thenReturn(null); + } + + private void mockBlockchainForBlockHeader(final Hash stateRoot, final long blockNumber) { + final BlockHeader blockHeader = mock(BlockHeader.class); + when(blockHeader.getStateRoot()).thenReturn(stateRoot); + when(blockHeader.getNumber()).thenReturn(blockNumber); + when(blockchain.getBlockHeader(blockNumber)).thenReturn(Optional.of(blockHeader)); + } + + private void mockProcessorStatusForTransaction( + final long blockNumber, + final Transaction transaction, + final Result.Status status, + final BytesValue output) { + when(protocolSchedule.getByBlockNumber(eq(blockNumber))).thenReturn(protocolSpec); + when(protocolSpec.getTransactionProcessor()).thenReturn(transactionProcessor); + when(protocolSpec.getMiningBeneficiaryCalculator()).thenReturn(BlockHeader::getCoinbase); + + final Result result = mock(Result.class); + switch (status) { + case SUCCESSFUL: + when(result.isSuccessful()).thenReturn(true); + break; + case INVALID: + case FAILED: + when(result.isSuccessful()).thenReturn(false); + break; + } + + when(transactionProcessor.processTransaction(any(), any(), any(), eq(transaction), any())) + .thenReturn(result); + } + + private void verifyTransactionWasProcessed(final Transaction expectedTransaction) { + verify(transactionProcessor) + .processTransaction(any(), any(), any(), eq(expectedTransaction), any()); + } + + private CallParameter callParameter() { + return new CallParameter("0x0", "0x0", "0x0", "0x0", "0x0", ""); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3Test.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3Test.java new file mode 100755 index 00000000000..62700a0e84b --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/Web3Sha3Test.java @@ -0,0 +1,118 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Test; + +public class Web3Sha3Test { + + private final Web3Sha3 method = new Web3Sha3(); + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("web3_sha3"); + } + + @Test + public void shouldReturnCorrectResult() { + final JsonRpcRequest request = + new JsonRpcRequest("2", "web3_sha3", new Object[] {"0x68656c6c6f20776f726c64"}); + + final JsonRpcResponse expected = + new JsonRpcSuccessResponse( + request.getId(), "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldReturnEmptyStringResult() { + final JsonRpcRequest request = new JsonRpcRequest("2", "web3_sha3", new Object[] {""}); + + final JsonRpcResponse expected = + new JsonRpcSuccessResponse( + request.getId(), "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldReturnErrorOnOddLengthParam() { + final JsonRpcRequest request = + new JsonRpcRequest("2", "web3_sha3", new Object[] {"0x68656c6c6f20776f726c6"}); + + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldReturnErrorOnNonHexParam() { + final JsonRpcRequest request = + new JsonRpcRequest("2", "web3_sha3", new Object[] {"0x68656c6c6fThisIsNotHex"}); + + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldReturnErrorOnNoPrefixParam() { + final JsonRpcRequest request = + new JsonRpcRequest("2", "web3_sha3", new Object[] {"68656c6c6f20776f726c64"}); + + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldReturnErrorOnNoPrefixNonHexParam() { + final JsonRpcRequest request = + new JsonRpcRequest("2", "web3_sha3", new Object[] {"68656c6c6fThisIsNotHex"}); + + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldReturnErrorOnExtraParam() { + final JsonRpcRequest request = + new JsonRpcRequest( + "2", "web3_sha3", new Object[] {"0x68656c6c6f20776f726c64", "{encode:'hex'}"}); + + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } + + @Test + public void shouldReturnErrorOnNoParam() { + final JsonRpcRequest request = new JsonRpcRequest("2", "web3_sha3", new Object[] {}); + + final JsonRpcResponse expected = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_PARAMS); + final JsonRpcResponse actual = method.response(request); + + assertThat(actual).isEqualToComparingFieldByFieldRecursively(expected); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbaseTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbaseTest.java new file mode 100755 index 00000000000..e724c4b12f0 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetCoinbaseTest.java @@ -0,0 +1,77 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MinerSetCoinbaseTest { + + private MinerSetCoinbase method; + + @Mock private MiningCoordinator miningCoordinator; + + @Before + public void before() { + this.method = new MinerSetCoinbase(miningCoordinator, new JsonRpcParameter()); + } + + @Test + public void shouldReturnExpectedMethodName() { + assertThat(method.getName()).isEqualTo("miner_setCoinbase"); + } + + @Test + public void shouldFailWhenMissingAddress() { + final JsonRpcRequest request = minerSetCoinbaseRequest(null); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown) + .isInstanceOf(InvalidJsonRpcParameters.class) + .hasMessage("Missing required json rpc parameter at index 0"); + } + + @Test + public void shouldFailWhenAddressIsInvalid() { + final JsonRpcRequest request = minerSetCoinbaseRequest("foo"); + + final Throwable thrown = catchThrowable(() -> method.response(request)); + + assertThat(thrown).isInstanceOf(InvalidJsonRpcParameters.class); + } + + @Test + public void shouldSetCoinbaseWhenRequestHasAddress() { + final JsonRpcRequest request = minerSetCoinbaseRequest("0x0"); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, true); + + final JsonRpcResponse response = method.response(request); + + verify(miningCoordinator).setCoinbase(eq(Address.fromHexString("0x0"))); + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest minerSetCoinbaseRequest(final String hexString) { + if (hexString != null) { + return new JsonRpcRequest("2.0", "miner_setCoinbase", new Object[] {hexString}); + } else { + return new JsonRpcRequest("2.0", "miner_setCoinbase", new Object[] {}); + } + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbaseTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbaseTest.java new file mode 100755 index 00000000000..61aad7e1807 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerSetEtherbaseTest.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MinerSetEtherbaseTest { + + private MinerSetEtherbase method; + + @Mock private MinerSetCoinbase minerSetCoinbase; + + @Before + public void before() { + this.method = new MinerSetEtherbase(minerSetCoinbase); + } + + @Test + public void shouldReturnExpectedMethodName() { + assertThat(method.getName()).isEqualTo("miner_setEtherbase"); + } + + @Test + public void shouldDelegateToMinerSetCoinbase() { + final JsonRpcRequest request = + new JsonRpcRequest(null, "miner_setEtherbase", new Object[] {"0x0"}); + + final ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(JsonRpcRequest.class); + when(minerSetCoinbase.response(requestCaptor.capture())) + .thenReturn(new JsonRpcSuccessResponse(null, true)); + + method.response(request); + + final JsonRpcRequest delegatedRequest = requestCaptor.getValue(); + assertThat(delegatedRequest).isEqualToComparingFieldByField(request); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStartTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStartTest.java new file mode 100755 index 00000000000..f05d6706ca6 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStartTest.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; + +import net.consensys.pantheon.ethereum.blockcreation.CoinbaseNotSetException; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MinerStartTest { + + private MinerStart method; + + @Mock private MiningCoordinator miningCoordinator; + + @Before + public void before() { + method = new MinerStart(miningCoordinator); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("miner_start"); + } + + @Test + public void shouldReturnTrueWhenMiningStartsSuccessfully() { + final JsonRpcRequest request = minerStart(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, true); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void shouldReturnCoinbaseNotSetErrorWhenCoinbaseHasNotBeenSet() { + final JsonRpcRequest request = minerStart(); + final JsonRpcResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.COINBASE_NOT_SET); + + doThrow(new CoinbaseNotSetException("")).when(miningCoordinator).enable(); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest minerStart() { + return new JsonRpcRequest("2.0", "miner_start", null); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStopTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStopTest.java new file mode 100755 index 00000000000..9f1ff14fc04 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/methods/miner/MinerStopTest.java @@ -0,0 +1,46 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.methods.miner; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MinerStopTest { + + private MinerStop method; + + @Mock private MiningCoordinator miningCoordinator; + + @Before + public void before() { + method = new MinerStop(miningCoordinator); + } + + @Test + public void shouldReturnCorrectMethodName() { + assertThat(method.getName()).isEqualTo("miner_stop"); + } + + @Test + public void shouldReturnTrueWhenMiningStopsSuccessfully() { + final JsonRpcRequest request = minerStop(); + final JsonRpcResponse expectedResponse = new JsonRpcSuccessResponse(null, true); + + final JsonRpcResponse actualResponse = method.response(request); + + assertThat(actualResponse).isEqualToComparingFieldByField(expectedResponse); + } + + private JsonRpcRequest minerStop() { + return new JsonRpcRequest("2.0", "miner_stop", null); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameterTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameterTest.java new file mode 100755 index 00000000000..4fd44272364 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/parameters/FilterParameterTest.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.parameters; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; + +import java.util.Arrays; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +public class FilterParameterTest { + + private final JsonRpcParameter parameters = new JsonRpcParameter(); + + @Test + public void jsonWithArrayOfAddressesShouldSerializeSuccessfully() throws Exception { + final String jsonWithAddressArray = + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getLogs\",\"params\":[{\"address\":[\"0x0\",\"0x1\"]}],\"id\":1}"; + final JsonRpcRequest request = readJsonAsJsonRpcRequest(jsonWithAddressArray); + final FilterParameter expectedFilterParameter = filterParameterWithAddresses("0x0", "0x1"); + + final FilterParameter parsedFilterParameter = + parameters.required(request.getParams(), 0, FilterParameter.class); + + assertThat(parsedFilterParameter) + .isEqualToComparingFieldByFieldRecursively(expectedFilterParameter); + } + + @Test + public void jsonWithSingleAddressShouldSerializeSuccessfully() throws Exception { + final String jsonWithSingleAddress = + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getLogs\",\"params\":[{\"address\":\"0x0\"}],\"id\":1}"; + final JsonRpcRequest request = readJsonAsJsonRpcRequest(jsonWithSingleAddress); + final FilterParameter expectedFilterParameter = filterParameterWithAddresses("0x0"); + + final FilterParameter parsedFilterParameter = + parameters.required(request.getParams(), 0, FilterParameter.class); + + assertThat(parsedFilterParameter) + .isEqualToComparingFieldByFieldRecursively(expectedFilterParameter); + } + + private FilterParameter filterParameterWithAddresses(final String... addresses) { + return new FilterParameter("latest", "latest", Arrays.asList(addresses), null, null); + } + + private JsonRpcRequest readJsonAsJsonRpcRequest(final String jsonWithSingleAddress) + throws java.io.IOException { + return new ObjectMapper().readValue(jsonWithSingleAddress, JsonRpcRequest.class); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracerTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracerTest.java new file mode 100755 index 00000000000..0fff93d3619 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransactionTracerTest.java @@ -0,0 +1,179 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.debug.TraceFrame; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; +import net.consensys.pantheon.ethereum.vm.DebugOperationTracer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionTracerTest { + + @Mock private ProtocolSchedule protocolSchedule; + @Mock private Blockchain blockchain; + + @Mock private WorldStateArchive worldStateArchive; + + @Mock private BlockHeader blockHeader; + + @Mock private BlockBody blockBody; + + @Mock private BlockHeader previousBlockHeader; + + @Mock private Transaction transaction; + + @Mock private Transaction otherTransaction; + + @Mock private DebugOperationTracer tracer; + + @Mock private ProtocolSpec protocolSpec; + + @Mock private MutableWorldState mutableWorldState; + + @Mock private TransactionProcessor transactionProcessor; + + private TransactionTracer transactionTracer; + + private final Hash transactionHash = + Hash.fromHexString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + private final Hash otherTransactionHash = + Hash.fromHexString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + private final Hash blockHash = + Hash.fromHexString("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); + + private final Hash previousBlockHash = + Hash.fromHexString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + private final Hash invalidBlockHash = + Hash.fromHexString("1111111111111111111111111111111111111111111111111111111111111111"); + + @Before + public void setUp() throws Exception { + transactionTracer = + new TransactionTracer(new BlockReplay(protocolSchedule, blockchain, worldStateArchive)); + when(transaction.hash()).thenReturn(transactionHash); + when(otherTransaction.hash()).thenReturn(otherTransactionHash); + when(blockHeader.getNumber()).thenReturn(12L); + when(blockHeader.getHash()).thenReturn(blockHash); + when(blockHeader.getParentHash()).thenReturn(previousBlockHash); + when(previousBlockHeader.getStateRoot()).thenReturn(Hash.ZERO); + when(worldStateArchive.getMutable(Hash.ZERO)).thenReturn(mutableWorldState); + when(protocolSchedule.getByBlockNumber(12)).thenReturn(protocolSpec); + when(protocolSpec.getTransactionProcessor()).thenReturn(transactionProcessor); + when(protocolSpec.getMiningBeneficiaryCalculator()).thenReturn(BlockHeader::getCoinbase); + } + + @Test + public void traceTransactionShouldReturnNoneWhenBlockHeaderNotFound() { + final Optional transactionTrace = + transactionTracer.traceTransaction(invalidBlockHash, transactionHash, tracer); + assertEquals(Optional.empty(), transactionTrace); + } + + @Test + public void traceTransactionShouldReturnTraceFramesFromExecutionTracer() { + when(blockchain.getBlockHeader(blockHash)).thenReturn(Optional.of(blockHeader)); + when(blockchain.getBlockHeader(previousBlockHash)).thenReturn(Optional.of(previousBlockHeader)); + when(blockBody.getTransactions()).thenReturn(Collections.singletonList(transaction)); + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.of(blockBody)); + final List traceFrames = Collections.singletonList(mock(TraceFrame.class)); + when(tracer.getTraceFrames()).thenReturn(traceFrames); + + final Optional transactionTrace = + transactionTracer.traceTransaction(blockHash, transactionHash, tracer); + + assertEquals(traceFrames, transactionTrace.get().getTraceFrames()); + } + + @Test + public void + traceTransactionShouldReturnTraceFramesFromExecutionTracerAfterExecutingOtherTransactions() { + when(blockchain.getBlockHeader(blockHash)).thenReturn(Optional.of(blockHeader)); + when(blockchain.getBlockHeader(previousBlockHash)).thenReturn(Optional.of(previousBlockHeader)); + + when(blockBody.getTransactions()).thenReturn(Arrays.asList(otherTransaction, transaction)); + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.of(blockBody)); + final List traceFrames = Collections.singletonList(mock(TraceFrame.class)); + when(tracer.getTraceFrames()).thenReturn(traceFrames); + + final Optional transactionTrace = + transactionTracer.traceTransaction(blockHash, transactionHash, tracer); + + assertEquals(traceFrames, transactionTrace.get().getTraceFrames()); + } + + @Test + public void traceTransactionShouldReturnResultFromProcessTransaction() { + final Result result = mock(Result.class); + + when(blockchain.getBlockHeader(blockHash)).thenReturn(Optional.of(blockHeader)); + when(blockchain.getBlockHeader(previousBlockHash)).thenReturn(Optional.of(previousBlockHeader)); + + when(blockBody.getTransactions()).thenReturn(Collections.singletonList(transaction)); + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.of(blockBody)); + + when(transactionProcessor.processTransaction( + blockchain, + mutableWorldState.updater(), + blockHeader, + transaction, + blockHeader.getCoinbase(), + tracer)) + .thenReturn(result); + + final Optional transactionTrace = + transactionTracer.traceTransaction(blockHash, transactionHash, tracer); + + assertEquals(result, transactionTrace.get().getResult()); + } + + @Test + public void traceTransactionShouldReturnEmptyResultWhenTransactionNotInCurrentBlock() { + + when(blockchain.getBlockHeader(blockHash)).thenReturn(Optional.of(blockHeader)); + when(blockchain.getBlockHeader(previousBlockHash)).thenReturn(Optional.of(previousBlockHeader)); + + when(blockBody.getTransactions()).thenReturn(Collections.singletonList(otherTransaction)); + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.of(blockBody)); + + final Optional transactionTrace = + transactionTracer.traceTransaction(blockHash, transactionHash, tracer); + + assertEquals(Optional.empty(), transactionTrace); + } + + @Test + public void traceTransactionShouldReturnEmptyResultWhenBlockIsNotAvailable() { + + when(blockchain.getBlockHeader(blockHash)).thenReturn(Optional.of(blockHeader)); + when(blockchain.getBlockBody(blockHash)).thenReturn(Optional.empty()); + + final Optional transactionTrace = + transactionTracer.traceTransaction(blockHash, transactionHash, tracer); + + assertEquals(Optional.empty(), transactionTrace); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResultTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResultTest.java new file mode 100755 index 00000000000..9a3240afa86 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/processor/TransientTransactionProcessingResultTest.java @@ -0,0 +1,75 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.processor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.mainnet.TransactionProcessor.Result; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TransientTransactionProcessingResultTest { + + private TransientTransactionProcessingResult transientTransactionProcessingResult; + + @Mock private Transaction transaction; + @Mock private Result result; + + @Before + public void before() { + this.transientTransactionProcessingResult = + new TransientTransactionProcessingResult(transaction, result); + } + + @Test + public void shouldDelegateToTransactionProcessorResultWhenOutputIsCalled() { + transientTransactionProcessingResult.getOutput(); + + verify(result).getOutput(); + } + + @Test + public void shouldDelegateToTransactionProcessorResultWhenIsSuccessfulIsCalled() { + transientTransactionProcessingResult.isSuccessful(); + + verify(result).isSuccessful(); + } + + @Test + public void shouldUseTransactionProcessorResultAndTransactionToCalculateGasEstimate() { + transientTransactionProcessingResult.getGasEstimate(); + + verify(transaction).getGasLimit(); + verify(result).getGasRemaining(); + } + + @Test + public void shouldCalculateCorrectGasEstimateWhenConsumedAllGas() { + when(transaction.getGasLimit()).thenReturn(5L); + when(result.getGasRemaining()).thenReturn(0L); + + assertThat(transientTransactionProcessingResult.getGasEstimate()).isEqualTo(5L); + } + + @Test + public void shouldCalculateCorrectGasEstimateWhenGasWasInsufficient() { + when(transaction.getGasLimit()).thenReturn(1L); + when(result.getGasRemaining()).thenReturn(-5L); + + assertThat(transientTransactionProcessingResult.getGasEstimate()).isEqualTo(6L); + } + + @Test + public void shouldCalculateCorrectGasEstimateWhenGasLimitWasSufficient() { + when(transaction.getGasLimit()).thenReturn(10L); + when(result.getGasRemaining()).thenReturn(3L); + + assertThat(transientTransactionProcessingResult.getGasEstimate()).isEqualTo(7L); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueriesTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueriesTest.java new file mode 100755 index 00000000000..73746989aa6 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/queries/BlockchainQueriesTest.java @@ -0,0 +1,572 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.queries; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.ethereum.core.InMemoryWorldState.createInMemoryWorldStateArchive; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Account; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldState; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.internal.filter.LogsQuery.Builder; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator; +import net.consensys.pantheon.ethereum.testutil.BlockDataGenerator.BlockOptions; +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; + +public class BlockchainQueriesTest { + private BlockDataGenerator gen; + + @Before + public void setup() { + gen = new BlockDataGenerator(); + } + + @Test + public void getBlockByHash() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(1).block; + + final BlockWithMetadata result = + queries.blockByHash(targetBlock.getHash()).get(); + assertBlockMatchesResult(targetBlock, result); + } + + @Test + public void getBlockByHashForInvalidHash() { + final BlockchainWithData data = setupBlockchain(2); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional> result = + queries.blockByHash(gen.hash()); + assertFalse(result.isPresent()); + } + + @Test + public void getBlockByNumber() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(1).block; + + final BlockWithMetadata result = + queries.blockByNumber(targetBlock.getHeader().getNumber()).get(); + assertBlockMatchesResult(targetBlock, result); + } + + @Test + public void getBlockByNumberForInvalidNumber() { + final BlockchainWithData data = setupBlockchain(2); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional> result = + queries.blockByNumber(10L); + assertFalse(result.isPresent()); + } + + @Test + public void getLatestBlock() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(2).block; + + final BlockWithMetadata result = queries.latestBlock().get(); + assertBlockMatchesResult(targetBlock, result); + } + + @Test + public void getBlockByHashWithTxHashes() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(1).block; + + final BlockWithMetadata result = + queries.blockByHashWithTxHashes(targetBlock.getHash()).get(); + assertBlockMatchesResultWithTxHashes(targetBlock, result); + } + + @Test + public void getBlockByHashWithTxHashesForInvalidHash() { + final BlockchainWithData data = setupBlockchain(2); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional> result = + queries.blockByHashWithTxHashes(gen.hash()); + assertFalse(result.isPresent()); + } + + @Test + public void getBlockByNumberWithTxHashes() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(1).block; + + final BlockWithMetadata result = + queries.blockByNumberWithTxHashes(targetBlock.getHeader().getNumber()).get(); + assertBlockMatchesResultWithTxHashes(targetBlock, result); + } + + @Test + public void getBlockByNumberWithTxHashesForInvalidHash() { + final BlockchainWithData data = setupBlockchain(2); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional> result = queries.blockByNumberWithTxHashes(10L); + assertFalse(result.isPresent()); + } + + @Test + public void getLatestBlockWithTxHashes() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(2).block; + + final BlockWithMetadata result = queries.latestBlockWithTxHashes().get(); + assertBlockMatchesResultWithTxHashes(targetBlock, result); + } + + @Test + public void getHeadBlockNumber() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + long result = queries.headBlockNumber(); + assertEquals(2L, result); + + // Increment and test + final Block lastBlock = data.blockData.get(2).block; + final Block nextBlock = gen.nextBlock(lastBlock); + data.blockchain.appendBlock(nextBlock, gen.receipts(nextBlock)); + + // Check that number has incremented + result = queries.headBlockNumber(); + assertEquals(3L, result); + } + + @Test + public void getAccountStorageBlockNumber() { + final List

addresses = Arrays.asList(gen.address(), gen.address(), gen.address()); + final List storageKeys = + Arrays.asList(gen.storageKey(), gen.storageKey(), gen.storageKey()); + final BlockchainWithData data = setupBlockchain(3, addresses, storageKeys); + final BlockchainQueries queries = data.blockchainQueries; + + final Hash latestStateRoot0 = data.blockData.get(2).block.getHeader().getStateRoot(); + final WorldState worldState0 = data.worldStateArchive.get(latestStateRoot0); + addresses.forEach( + address -> + storageKeys.forEach( + storageKey -> { + final Account actualAccount0 = worldState0.get(address); + final UInt256 result = queries.storageAt(address, storageKey, 2L).get(); + assertEquals(actualAccount0.getStorageValue(storageKey), result); + })); + + final Hash latestStateRoot1 = data.blockData.get(1).block.getHeader().getStateRoot(); + final WorldState worldState1 = data.worldStateArchive.get(latestStateRoot1); + addresses.forEach( + address -> + storageKeys.forEach( + storageKey -> { + final Account actualAccount1 = worldState1.get(address); + final UInt256 result = queries.storageAt(address, storageKey, 1L).get(); + assertEquals(actualAccount1.getStorageValue(storageKey), result); + })); + } + + @Test + public void getAccountBalanceAtBlockNumber() { + final List
addresses = Arrays.asList(gen.address(), gen.address(), gen.address()); + final int blockCount = 3; + final BlockchainWithData data = setupBlockchain(blockCount, addresses); + final BlockchainQueries queries = data.blockchainQueries; + + for (int i = 0; i < blockCount; i++) { + final long curBlockNumber = i; + final Hash stateRoot = data.blockData.get(i).block.getHeader().getStateRoot(); + final WorldState worldState = data.worldStateArchive.get(stateRoot); + assertTrue(addresses.size() > 0); + + addresses.forEach( + address -> { + final Account actualAccount = worldState.get(address); + final Wei result = queries.accountBalance(address, curBlockNumber).get(); + + assertEquals(actualAccount.getBalance(), result); + }); + } + } + + @Test + public void getAccountBalanceNonExistentAtBlockNumber() { + final List
addresses = Arrays.asList(gen.address(), gen.address(), gen.address()); + final BlockchainWithData data = setupBlockchain(3, addresses); + final BlockchainQueries queries = data.blockchainQueries; + assertTrue(addresses.size() > 0); + + // Get random non-existent account + final Wei result = queries.accountBalance(gen.address(), 1L).get(); + assertEquals(Wei.ZERO, result); + } + + @Test + public void getOmmerCountByHash() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(1).block; + + final Optional result = queries.getOmmerCount(targetBlock.getHash()); + assertEquals(targetBlock.getBody().getOmmers().size(), (int) result.get()); + } + + @Test + public void getOmmerCountByInvalidHash() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional result = queries.getOmmerCount(gen.hash()); + assertFalse(result.isPresent()); + } + + @Test + public void getOmmerCountByNumber() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(1).block; + + final Optional result = queries.getOmmerCount(targetBlock.getHeader().getNumber()); + assertEquals(targetBlock.getBody().getOmmers().size(), (int) result.get()); + } + + @Test + public void getOmmerCountForInvalidNumber() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final long invalidNumber = data.blockchain.getChainHeadBlockNumber() + 10; + final Optional result = queries.getOmmerCount(invalidNumber); + assertFalse(result.isPresent()); + } + + @Test + public void getOmmerCountForLatestBlock() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Block targetBlock = data.blockData.get(data.blockData.size() - 1).block; + final Optional result = queries.getOmmerCount(); + assertEquals(targetBlock.getBody().getOmmers().size(), (int) result.get()); + } + + @Test + public void logsShouldBeFlaggedAsRemovedWhenBlockIsNotInCanonicalChain() { + // create initial blockchain + final BlockchainWithData data = setupBlockchain(3); + final Block targetBlock = data.blockData.get(data.blockData.size() - 1).block; + final List blocks = + data.blockData.stream().map(b -> b.block).collect(Collectors.toList()); + final List> blockReceipts = + blocks.stream().map(gen::receipts).collect(Collectors.toList()); + + // check that logs have removed = false + List logs = + data.blockchainQueries.matchingLogs(targetBlock.getHash(), new Builder().build()); + assertThat(logs).isNotEmpty(); + assertThat(logs).allMatch(l -> !l.isRemoved()); + + // Create parallel fork of length 1 + final int forkBlock = 2; + final int commonAncestor = 1; + final BlockOptions options = + new BlockOptions() + .setParentHash(data.blockchain.getBlockHashByNumber(commonAncestor).get()) + .setBlockNumber(forkBlock) + .setDifficulty( + data.blockchain.getBlockHeader(forkBlock).get().getDifficulty().plus(10L)); + final Block fork = gen.block(options); + final List forkReceipts = gen.receipts(fork); + + final List reorgedChain = new ArrayList<>(blocks.subList(0, forkBlock)); + reorgedChain.add(fork); + final List> reorgedReceipts = + new ArrayList<>(blockReceipts.subList(0, forkBlock)); + reorgedReceipts.add(forkReceipts); + + // Add fork + data.blockchain.appendBlock(fork, forkReceipts); + + // check that logs have removed = true + logs = data.blockchainQueries.matchingLogs(targetBlock.getHash(), new Builder().build()); + assertThat(logs).isNotEmpty(); + assertThat(logs).allMatch(LogWithMetadata::isRemoved); + } + + @Test + public void getOmmerByBlockHashAndIndexShouldReturnEmptyWhenBlockDoesNotExist() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional ommerOptional = queries.getOmmer(Hash.ZERO, 0); + + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getOmmerByBlockHashAndIndexShouldReturnEmptyWhenBlockDoesNotHaveOmmers() { + final BlockchainWithData data = setupBlockchain(1); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(0).block; + + final Optional ommerOptional = queries.getOmmer(targetBlock.getHash(), 0); + + assertThat(targetBlock.getBody().getOmmers()).hasSize(0); + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getOmmerByBlockHashAndIndexShouldReturnEmptyWhenIndexIsOutOfRange() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(0).block; + final int indexOutOfRange = targetBlock.getBody().getOmmers().size() + 1; + + final Optional ommerOptional = + queries.getOmmer(targetBlock.getHash(), indexOutOfRange); + + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getOmmerByBlockHashAndIndexShouldReturnExpectedOmmerHeader() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(data.blockData.size() - 1).block; + final BlockHeader ommerBlockHeader = targetBlock.getBody().getOmmers().get(0); + + final BlockHeader retrievedOmmerBlockHeader = queries.getOmmer(targetBlock.getHash(), 0).get(); + + assertThat(retrievedOmmerBlockHeader).isEqualTo(ommerBlockHeader); + } + + @Test + public void getOmmerByBlockNumberAndIndexShouldReturnEmptyWhenBlockDoesNotExist() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional ommerOptional = queries.getOmmer(999, 0); + + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getOmmerByBlockNumberAndIndexShouldReturnEmptyWhenBlockDoesNotHaveOmmers() { + final BlockchainWithData data = setupBlockchain(1); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(0).block; + + final Optional ommerOptional = + queries.getOmmer(targetBlock.getHeader().getNumber(), 0); + + assertThat(targetBlock.getBody().getOmmers()).hasSize(0); + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getOmmerByBlockNumberAndIndexShouldReturnEmptyWhenIndexIsOutOfRange() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(0).block; + final int indexOutOfRange = targetBlock.getBody().getOmmers().size() + 1; + + final Optional ommerOptional = + queries.getOmmer(targetBlock.getHeader().getNumber(), indexOutOfRange); + + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getOmmerByBlockNumberAndIndexShouldReturnExpectedOmmerHeader() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(data.blockData.size() - 1).block; + final BlockHeader ommerBlockHeader = targetBlock.getBody().getOmmers().get(1); + + final BlockHeader retrievedOmmerBlockHeader = + queries.getOmmer(targetBlock.getHeader().getNumber(), 1).get(); + + assertThat(retrievedOmmerBlockHeader).isEqualTo(ommerBlockHeader); + } + + @Test + public void getLatestBlockOmmerByIndexShouldReturnEmptyWhenLatestBlockDoesNotHaveOmmers() { + final BlockchainWithData data = setupBlockchain(1); + final BlockchainQueries queries = data.blockchainQueries; + + final Optional ommerOptional = queries.getOmmer(0); + + assertThat(queries.blockByNumber(queries.headBlockNumber()).get().getOmmers()).hasSize(0); + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getLatestBlockOmmerByIndexShouldReturnEmptyWhenIndexIsOutOfRange() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(data.blockData.size() - 1).block; + final int indexOutOfRange = targetBlock.getBody().getOmmers().size() + 1; + + final Optional ommerOptional = queries.getOmmer(indexOutOfRange); + + assertThat(ommerOptional).isEmpty(); + } + + @Test + public void getLatestBlockOmmerByIndexShouldReturnExpectedOmmerHeader() { + final BlockchainWithData data = setupBlockchain(3); + final BlockchainQueries queries = data.blockchainQueries; + final Block targetBlock = data.blockData.get(data.blockData.size() - 1).block; + final BlockHeader ommerBlockHeader = targetBlock.getBody().getOmmers().get(0); + + final BlockHeader retrievedOmmerBlockHeader = queries.getOmmer(0).get(); + + assertThat(retrievedOmmerBlockHeader).isEqualTo(ommerBlockHeader); + } + + private void assertBlockMatchesResult( + final Block targetBlock, final BlockWithMetadata result) { + assertEquals(targetBlock.getHeader(), result.getHeader()); + final List expectedOmmers = + targetBlock + .getBody() + .getOmmers() + .stream() + .map(BlockHeader::getHash) + .collect(Collectors.toList()); + assertEquals(expectedOmmers, result.getOmmers()); + + for (int i = 0; i < result.getTransactions().size(); i++) { + final TransactionWithMetadata txResult = result.getTransactions().get(i); + final Transaction targetTx = targetBlock.getBody().getTransactions().get(i); + assertEquals(targetTx, txResult.getTransaction()); + assertEquals(i, txResult.getTransactionIndex()); + assertEquals(targetBlock.getHash(), txResult.getBlockHash()); + assertEquals(targetBlock.getHeader().getNumber(), txResult.getBlockNumber()); + } + } + + private void assertBlockMatchesResultWithTxHashes( + final Block targetBlock, final BlockWithMetadata result) { + assertEquals(targetBlock.getHeader(), result.getHeader()); + final List expectedOmmers = + targetBlock + .getBody() + .getOmmers() + .stream() + .map(BlockHeader::getHash) + .collect(Collectors.toList()); + assertEquals(expectedOmmers, result.getOmmers()); + + for (int i = 0; i < result.getTransactions().size(); i++) { + final Hash txResult = result.getTransactions().get(i); + final Transaction actualTx = targetBlock.getBody().getTransactions().get(i); + assertEquals(actualTx.hash(), txResult); + } + } + + private BlockchainWithData setupBlockchain(final int blocksToAdd) { + return setupBlockchain(blocksToAdd, Collections.emptyList(), Collections.emptyList()); + } + + private BlockchainWithData setupBlockchain( + final int blocksToAdd, final List
accountsToSetup) { + return setupBlockchain(blocksToAdd, accountsToSetup, Collections.emptyList()); + } + + private BlockchainWithData setupBlockchain( + final int blocksToAdd, final List
accountsToSetup, final List storageKeys) { + checkArgument(blocksToAdd >= 1, "Must add at least one block to the queries"); + + final WorldStateArchive worldStateArchive = createInMemoryWorldStateArchive(); + + // Generate some queries data + final List blockData = new ArrayList<>(blocksToAdd); + final List blocks = + gen.blockSequence(blocksToAdd, worldStateArchive, accountsToSetup, storageKeys); + for (int i = 0; i < blocksToAdd; i++) { + final Block block = blocks.get(i); + final List receipts = gen.receipts(block); + blockData.add(new BlockData(block, receipts)); + } + + // Setup blockchain + final KeyValueStorage kvStore = new InMemoryKeyValueStorage(); + final MutableBlockchain blockchain = + new DefaultMutableBlockchain(blocks.get(0), kvStore, MainnetBlockHashFunction::createHash); + blockData + .subList(1, blockData.size()) + .forEach( + b -> { + blockchain.appendBlock(b.block, b.receipts); + }); + + return new BlockchainWithData(blockchain, blockData, worldStateArchive); + } + + private static class BlockchainWithData { + final MutableBlockchain blockchain; + final List blockData; + final WorldStateArchive worldStateArchive; + final BlockchainQueries blockchainQueries; + + private BlockchainWithData( + final MutableBlockchain blockchain, + final List blockData, + final WorldStateArchive worldStateArchive) { + this.blockchain = blockchain; + this.blockData = blockData; + this.worldStateArchive = worldStateArchive; + this.blockchainQueries = new BlockchainQueries(blockchain, worldStateArchive); + } + } + + private static class BlockData { + final Block block; + final List receipts; + + private BlockData(final Block block, final List receipts) { + this.block = block; + this.receipts = receipts; + } + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResultTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResultTest.java new file mode 100755 index 00000000000..b3dd7da93c8 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/internal/results/NetworkResultTest.java @@ -0,0 +1,20 @@ +package net.consensys.pantheon.ethereum.jsonrpc.internal.results; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import org.junit.Test; + +public class NetworkResultTest { + + @Test + public void localAndRemoteAddressShouldNotStartWithForwardSlash() { + final SocketAddress socketAddress = new InetSocketAddress("1.2.3.4", 7890); + final NetworkResult networkResult = new NetworkResult(socketAddress, socketAddress); + + assertThat(networkResult.getLocalAddress()).isEqualTo("1.2.3.4:7890"); + assertThat(networkResult.getRemoteAddress()).isEqualTo("1.2.3.4:7890"); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfigurationTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfigurationTest.java new file mode 100755 index 00000000000..52d619ac860 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketConfigurationTest.java @@ -0,0 +1,21 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; + +import org.junit.Test; + +public class WebSocketConfigurationTest { + + @Test + public void defaultConfiguration() { + final WebSocketConfiguration configuration = WebSocketConfiguration.createDefault(); + + assertThat(configuration.isEnabled()).isFalse(); + assertThat(configuration.getHost()).isEqualTo("127.0.0.1"); + assertThat(configuration.getPort()).isEqualTo(8546); + assertThat(configuration.getRpcApis()) + .containsExactlyInAnyOrder(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandlerTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandlerTest.java new file mode 100755 index 00000000000..59254321851 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketRequestHandlerTest.java @@ -0,0 +1,166 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketRpcRequest; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +@RunWith(VertxUnitRunner.class) +public class WebSocketRequestHandlerTest { + + private static final int VERTX_AWAIT_TIMEOUT_MILLIS = 10000; + + private Vertx vertx; + private WebSocketRequestHandler handler; + private JsonRpcMethod jsonRpcMethodMock; + private final Map methods = new HashMap<>(); + + @Before + public void before(final TestContext context) { + vertx = Vertx.vertx(); + + jsonRpcMethodMock = mock(JsonRpcMethod.class); + + methods.put("eth_x", jsonRpcMethodMock); + handler = new WebSocketRequestHandler(vertx, methods); + } + + @After + public void after(final TestContext context) { + Mockito.reset(jsonRpcMethodMock); + vertx.close(context.asyncAssertSuccess()); + } + + @Test + public void handlerDeliversResponseSuccessfully(final TestContext context) { + final Async async = context.async(); + + final JsonObject requestJson = new JsonObject().put("id", 1).put("method", "eth_x"); + final JsonRpcRequest expectedRequest = requestJson.mapTo(WebSocketRpcRequest.class); + final JsonRpcSuccessResponse expectedResponse = + new JsonRpcSuccessResponse(expectedRequest.getId(), null); + when(jsonRpcMethodMock.response(eq(expectedRequest))).thenReturn(expectedResponse); + + final String websocketId = UUID.randomUUID().toString(); + + vertx + .eventBus() + .consumer(websocketId) + .handler( + msg -> { + context.assertEquals(Json.encode(expectedResponse), msg.body()); + async.complete(); + }) + .completionHandler(v -> handler.handle(websocketId, Buffer.buffer(requestJson.toString()))); + + async.awaitSuccess(WebSocketRequestHandlerTest.VERTX_AWAIT_TIMEOUT_MILLIS); + } + + @Test + public void jsonDecodeFailureShouldRespondInvalidRequest(final TestContext context) { + final Async async = context.async(); + + final String websocketId = UUID.randomUUID().toString(); + + vertx + .eventBus() + .consumer(websocketId) + .handler( + msg -> { + context.assertEquals(Json.encode(JsonRpcError.INVALID_REQUEST), msg.body()); + verifyZeroInteractions(jsonRpcMethodMock); + async.complete(); + }) + .completionHandler(v -> handler.handle(websocketId, Buffer.buffer(""))); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } + + @Test + public void objectMapperFailureShouldRespondInvalidRequest(final TestContext context) { + final Async async = context.async(); + + final String websocketId = UUID.randomUUID().toString(); + + vertx + .eventBus() + .consumer(websocketId) + .handler( + msg -> { + context.assertEquals(Json.encode(JsonRpcError.INVALID_REQUEST), msg.body()); + verifyZeroInteractions(jsonRpcMethodMock); + async.complete(); + }) + .completionHandler(v -> handler.handle(websocketId, Buffer.buffer("{}"))); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } + + @Test + public void absentMethodShouldRespondMethodNotFound(final TestContext context) { + final Async async = context.async(); + + final JsonObject requestJson = + new JsonObject().put("id", 1).put("method", "eth_nonexistentMethod"); + + final String websocketId = UUID.randomUUID().toString(); + + vertx + .eventBus() + .consumer(websocketId) + .handler( + msg -> { + context.assertEquals(Json.encode(JsonRpcError.METHOD_NOT_FOUND), msg.body()); + async.complete(); + }) + .completionHandler(v -> handler.handle(websocketId, Buffer.buffer(requestJson.toString()))); + + async.awaitSuccess(WebSocketRequestHandlerTest.VERTX_AWAIT_TIMEOUT_MILLIS); + } + + @Test + public void onExceptionProcessingRequestShouldRespondInternalError(final TestContext context) { + final Async async = context.async(); + + final JsonObject requestJson = new JsonObject().put("id", 1).put("method", "eth_x"); + final JsonRpcRequest expectedRequest = requestJson.mapTo(WebSocketRpcRequest.class); + when(jsonRpcMethodMock.response(eq(expectedRequest))).thenThrow(new RuntimeException()); + + final String websocketId = UUID.randomUUID().toString(); + + vertx + .eventBus() + .consumer(websocketId) + .handler( + msg -> { + context.assertEquals(Json.encode(JsonRpcError.INTERNAL_ERROR), msg.body()); + async.complete(); + }) + .completionHandler(v -> handler.handle(websocketId, Buffer.buffer(requestJson.toString()))); + + async.awaitSuccess(WebSocketRequestHandlerTest.VERTX_AWAIT_TIMEOUT_MILLIS); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java new file mode 100755 index 00000000000..2b92bb3e166 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/WebSocketServiceTest.java @@ -0,0 +1,127 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.WebSocketBase; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class WebSocketServiceTest { + + private static final int VERTX_AWAIT_TIMEOUT_MILLIS = 10000; + + private Vertx vertx; + private WebSocketConfiguration websocketConfiguration; + private WebSocketRequestHandler webSocketRequestHandlerSpy; + private WebSocketService websocketService; + + @Before + public void before() { + vertx = Vertx.vertx(); + + websocketConfiguration = WebSocketConfiguration.createDefault(); + final Map websocketMethods = + new WebSocketMethodsFactory(new SubscriptionManager(), new HashMap<>()).methods(); + webSocketRequestHandlerSpy = spy(new WebSocketRequestHandler(vertx, websocketMethods)); + + websocketService = + new WebSocketService(vertx, websocketConfiguration, webSocketRequestHandlerSpy); + websocketService.start().join(); + } + + @After + public void after() { + reset(webSocketRequestHandlerSpy); + websocketService.stop(); + } + + @Test + public void websocketServiceExecutesHandlerOnMessage(final TestContext context) { + final Async async = context.async(); + + final String request = "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"syncing\"]}"; + final String expectedResponse = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x1\"}"; + + vertx + .createHttpClient() + .websocket( + websocketConfiguration.getPort(), + websocketConfiguration.getHost(), + "/", + webSocket -> { + webSocket.write(Buffer.buffer(request)); + + webSocket.handler( + buffer -> { + context.assertEquals(expectedResponse, buffer.toString()); + async.complete(); + }); + }); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } + + @Test + public void websocketServiceRemoveSubscriptionOnConnectionClose(final TestContext context) { + final Async async = context.async(); + + vertx + .eventBus() + .consumer(SubscriptionManager.EVENTBUS_REMOVE_SUBSCRIPTIONS_ADDRESS) + .handler( + m -> { + context.assertNotNull(m.body()); + async.complete(); + }) + .completionHandler( + v -> + vertx + .createHttpClient() + .websocket( + websocketConfiguration.getPort(), + websocketConfiguration.getHost(), + "/", + WebSocketBase::close)); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } + + @Test + public void websocketServiceCloseConnectionOnUnrecoverableError(final TestContext context) { + final Async async = context.async(); + + final byte[] bigMessage = new byte[HttpServerOptions.DEFAULT_MAX_WEBSOCKET_MESSAGE_SIZE + 1]; + Arrays.fill(bigMessage, (byte) 1); + + vertx + .createHttpClient() + .websocket( + websocketConfiguration.getPort(), + websocketConfiguration.getHost(), + "/", + webSocket -> { + webSocket.write(Buffer.buffer(bigMessage)); + webSocket.closeHandler(v -> async.complete()); + }); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeIntegrationTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeIntegrationTest.java new file mode 100755 index 00000000000..6a32b062d39 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeIntegrationTest.java @@ -0,0 +1,126 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketRequestHandler; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.Json; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class EthSubscribeIntegrationTest { + + private Vertx vertx; + private WebSocketRequestHandler webSocketRequestHandler; + private SubscriptionManager subscriptionManager; + private WebSocketMethodsFactory webSocketMethodsFactory; + private final int ASYNC_TIMEOUT = 5000; + private final String CONNECTION_ID_1 = "test-connection-id-1"; + private final String CONNECTION_ID_2 = "test-connection-id-2"; + + @Before + public void before() { + vertx = Vertx.vertx(); + subscriptionManager = new SubscriptionManager(); + webSocketMethodsFactory = new WebSocketMethodsFactory(subscriptionManager, new HashMap<>()); + webSocketRequestHandler = new WebSocketRequestHandler(vertx, webSocketMethodsFactory.methods()); + } + + @Test + public void shouldAddConnectionToMap(final TestContext context) { + final Async async = context.async(); + + final JsonRpcRequest subscribeRequest = createEthSubscribeRequest(CONNECTION_ID_1); + + vertx + .eventBus() + .consumer(CONNECTION_ID_1) + .handler( + msg -> { + final Map> connectionSubscriptionsMap = + subscriptionManager.getConnectionSubscriptionsMap(); + assertThat(connectionSubscriptionsMap.size()).isEqualTo(1); + assertThat(connectionSubscriptionsMap.containsKey(CONNECTION_ID_1)).isTrue(); + async.complete(); + }) + .completionHandler( + v -> + webSocketRequestHandler.handle( + CONNECTION_ID_1, Buffer.buffer(Json.encode(subscribeRequest)))); + + async.awaitSuccess(ASYNC_TIMEOUT); + } + + @Test + public void shouldAddMultipleConnectionsToMap(final TestContext context) { + final Async async = context.async(2); + + final JsonRpcRequest subscribeRequest1 = createEthSubscribeRequest(CONNECTION_ID_1); + final JsonRpcRequest subscribeRequest2 = createEthSubscribeRequest(CONNECTION_ID_2); + + vertx + .eventBus() + .consumer(CONNECTION_ID_1) + .handler( + msg -> { + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat( + subscriptionManager + .getConnectionSubscriptionsMap() + .containsKey(CONNECTION_ID_1)) + .isTrue(); + async.countDown(); + + vertx + .eventBus() + .consumer(CONNECTION_ID_2) + .handler( + msg2 -> { + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()) + .isEqualTo(2); + assertThat( + subscriptionManager + .getConnectionSubscriptionsMap() + .containsKey(CONNECTION_ID_1)) + .isTrue(); + assertThat( + subscriptionManager + .getConnectionSubscriptionsMap() + .containsKey(CONNECTION_ID_2)) + .isTrue(); + async.countDown(); + }) + .completionHandler( + v -> + webSocketRequestHandler.handle( + CONNECTION_ID_2, Buffer.buffer(Json.encode(subscribeRequest2)))); + }) + .completionHandler( + v -> + webSocketRequestHandler.handle( + CONNECTION_ID_1, Buffer.buffer(Json.encode(subscribeRequest1)))); + + async.awaitSuccess(ASYNC_TIMEOUT); + } + + private WebSocketRpcRequest createEthSubscribeRequest(final String connectionId) { + return Json.decodeValue( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"syncing\"], \"connectionId\": \"" + + connectionId + + "\"}", + WebSocketRpcRequest.class); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeTest.java new file mode 100755 index 00000000000..17a45468385 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthSubscribeTest.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.Quantity; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import io.vertx.core.json.Json; +import org.junit.Before; +import org.junit.Test; + +public class EthSubscribeTest { + + private EthSubscribe ethSubscribe; + private SubscriptionManager subscriptionManagerMock; + private SubscriptionRequestMapper mapperMock; + + @Before + public void before() { + subscriptionManagerMock = mock(SubscriptionManager.class); + mapperMock = mock(SubscriptionRequestMapper.class); + ethSubscribe = new EthSubscribe(subscriptionManagerMock, mapperMock); + } + + @Test + public void nameIsEthSubscribe() { + assertThat(ethSubscribe.getName()).isEqualTo("eth_subscribe"); + } + + @Test + public void responseContainsSubscriptionId() { + final WebSocketRpcRequest request = createWebSocketRpcRequest(); + + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, request.getConnectionId()); + + when(mapperMock.mapSubscribeRequest(eq(request))).thenReturn(subscribeRequest); + when(subscriptionManagerMock.subscribe(eq(subscribeRequest))).thenReturn(1L); + + final JsonRpcSuccessResponse expectedResponse = + new JsonRpcSuccessResponse(request.getId(), Quantity.create((1L))); + + assertThat(ethSubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void invalidSubscribeRequestRespondsInvalidRequestResponse() { + final WebSocketRpcRequest request = createWebSocketRpcRequest(); + when(mapperMock.mapSubscribeRequest(any())) + .thenThrow(new InvalidSubscriptionRequestException()); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_REQUEST); + + assertThat(ethSubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void uncaughtErrorOnSubscriptionManagerShouldRespondInternalErrorResponse() { + final WebSocketRpcRequest request = createWebSocketRpcRequest(); + when(mapperMock.mapSubscribeRequest(any())).thenReturn(mock(SubscribeRequest.class)); + when(subscriptionManagerMock.subscribe(any())).thenThrow(new RuntimeException()); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR); + + assertThat(ethSubscribe.response(request)).isEqualTo(expectedResponse); + } + + private WebSocketRpcRequest createWebSocketRpcRequest() { + return Json.decodeValue( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"syncing\"], \"connectionId\": \"1\"}", + WebSocketRpcRequest.class); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeIntegrationTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeIntegrationTest.java new file mode 100755 index 00000000000..29f311b4d88 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeIntegrationTest.java @@ -0,0 +1,132 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketRequestHandler; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import java.util.HashMap; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.Json; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class EthUnsubscribeIntegrationTest { + + private Vertx vertx; + private WebSocketRequestHandler webSocketRequestHandler; + private SubscriptionManager subscriptionManager; + private WebSocketMethodsFactory webSocketMethodsFactory; + private final int ASYNC_TIMEOUT = 5000; + private final String CONNECTION_ID = "test-connection-id-1"; + + @Before + public void before() { + vertx = Vertx.vertx(); + subscriptionManager = new SubscriptionManager(); + webSocketMethodsFactory = new WebSocketMethodsFactory(subscriptionManager, new HashMap<>()); + webSocketRequestHandler = new WebSocketRequestHandler(vertx, webSocketMethodsFactory.methods()); + } + + @Test + public void shouldRemoveConnectionWithSingleSubscriptionFromMap(final TestContext context) { + final Async async = context.async(); + + // Check the connectionMap is empty + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(0); + + // Add the subscription we'd like to remove + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + final Long subscriptionId = subscriptionManager.subscribe(subscribeRequest); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + + final JsonRpcRequest unsubscribeRequest = + createEthUnsubscribeRequest(subscriptionId, CONNECTION_ID); + + vertx + .eventBus() + .consumer(CONNECTION_ID) + .handler( + msg -> { + assertThat(subscriptionManager.getConnectionSubscriptionsMap().isEmpty()).isTrue(); + async.complete(); + }) + .completionHandler( + v -> + webSocketRequestHandler.handle( + CONNECTION_ID, Buffer.buffer(Json.encode(unsubscribeRequest)))); + + async.awaitSuccess(ASYNC_TIMEOUT); + } + + @Test + public void shouldRemoveSubscriptionAndKeepConnection(final TestContext context) { + final Async async = context.async(); + + // Check the connectionMap is empty + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(0); + + // Add the subscriptions we'd like to remove + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + final Long subscriptionId1 = subscriptionManager.subscribe(subscribeRequest); + final Long subscriptionId2 = subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().containsKey(CONNECTION_ID)) + .isTrue(); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(2); + + final JsonRpcRequest unsubscribeRequest = + createEthUnsubscribeRequest(subscriptionId2, CONNECTION_ID); + + vertx + .eventBus() + .consumer(CONNECTION_ID) + .handler( + msg -> { + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat( + subscriptionManager + .getConnectionSubscriptionsMap() + .containsKey(CONNECTION_ID)) + .isTrue(); + assertThat( + subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(1); + assertThat( + subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).get(0)) + .isEqualTo(subscriptionId1); + async.complete(); + }) + .completionHandler( + v -> + webSocketRequestHandler.handle( + CONNECTION_ID, Buffer.buffer(Json.encode(unsubscribeRequest)))); + + async.awaitSuccess(ASYNC_TIMEOUT); + } + + private WebSocketRpcRequest createEthUnsubscribeRequest( + final Long subscriptionId, final String connectionId) { + return Json.decodeValue( + "{\"id\": 1, \"method\": \"eth_unsubscribe\", \"params\": [\"" + + subscriptionId + + "\"], \"connectionId\": \"" + + connectionId + + "\"}", + WebSocketRpcRequest.class); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeTest.java new file mode 100755 index 00000000000..7dca9b7bc70 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/EthUnsubscribeTest.java @@ -0,0 +1,97 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import net.consensys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionNotFoundException; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.InvalidSubscriptionRequestException; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionRequestMapper; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.UnsubscribeRequest; + +import io.vertx.core.json.Json; +import org.junit.Before; +import org.junit.Test; + +public class EthUnsubscribeTest { + + private EthUnsubscribe ethUnsubscribe; + private SubscriptionManager subscriptionManagerMock; + private SubscriptionRequestMapper mapperMock; + private final String CONNECTION_ID = "test-connection-id"; + + @Before + public void before() { + subscriptionManagerMock = mock(SubscriptionManager.class); + mapperMock = mock(SubscriptionRequestMapper.class); + ethUnsubscribe = new EthUnsubscribe(subscriptionManagerMock, mapperMock); + } + + @Test + public void nameIsEthUnsubscribe() { + assertThat(ethUnsubscribe.getName()).isEqualTo("eth_unsubscribe"); + } + + @Test + public void responseContainsUnsubscribeStatus() { + final JsonRpcRequest request = createJsonRpcRequest(); + final UnsubscribeRequest unsubscribeRequest = new UnsubscribeRequest(1L, CONNECTION_ID); + when(mapperMock.mapUnsubscribeRequest(eq(request))).thenReturn(unsubscribeRequest); + when(subscriptionManagerMock.unsubscribe(eq(unsubscribeRequest))).thenReturn(true); + + final JsonRpcSuccessResponse expectedResponse = + new JsonRpcSuccessResponse(request.getId(), true); + + assertThat(ethUnsubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void invalidUnsubscribeRequestReturnsInvalidRequestResponse() { + final JsonRpcRequest request = createJsonRpcRequest(); + when(mapperMock.mapUnsubscribeRequest(any())) + .thenThrow(new InvalidSubscriptionRequestException()); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INVALID_REQUEST); + + assertThat(ethUnsubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void whenSubscriptionNotFoundReturnError() { + final JsonRpcRequest request = createJsonRpcRequest(); + when(mapperMock.mapUnsubscribeRequest(any())).thenReturn(mock(UnsubscribeRequest.class)); + when(subscriptionManagerMock.unsubscribe(any())) + .thenThrow(new SubscriptionNotFoundException(1L)); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.SUBSCRIPTION_NOT_FOUND); + + assertThat(ethUnsubscribe.response(request)).isEqualTo(expectedResponse); + } + + @Test + public void uncaughtErrorOnSubscriptionManagerReturnsInternalErrorResponse() { + final JsonRpcRequest request = createJsonRpcRequest(); + when(mapperMock.mapUnsubscribeRequest(any())).thenReturn(mock(UnsubscribeRequest.class)); + when(subscriptionManagerMock.unsubscribe(any())).thenThrow(new RuntimeException()); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR); + + assertThat(ethUnsubscribe.response(request)).isEqualTo(expectedResponse); + } + + private JsonRpcRequest createJsonRpcRequest() { + return Json.decodeValue( + "{\"id\": 1, \"method\": \"eth_unsubscribe\", \"params\": [\"0x0\"]}", + JsonRpcRequest.class); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactoryTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactoryTest.java new file mode 100755 index 00000000000..0f0df9b8103 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/methods/WebSocketMethodsFactoryTest.java @@ -0,0 +1,73 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class WebSocketMethodsFactoryTest { + + private WebSocketMethodsFactory factory; + + @Mock private SubscriptionManager subscriptionManager; + private final Map jsonRpcMethods = new HashMap<>(); + + @Before + public void before() { + jsonRpcMethods.put("eth_unsubscribe", jsonRpcMethod("eth_unsubscribe")); + factory = new WebSocketMethodsFactory(subscriptionManager, jsonRpcMethods); + } + + @Test + public void websocketsFactoryShouldCreateEthSubscribe() { + final JsonRpcMethod method = factory.methods().get("eth_subscribe"); + + assertThat(method).isNotNull(); + assertThat(method).isInstanceOf(EthSubscribe.class); + } + + @Test + public void websocketsFactoryShouldCreateEthUnsubscribe() { + final JsonRpcMethod method = factory.methods().get("eth_unsubscribe"); + + assertThat(method).isNotNull(); + assertThat(method).isInstanceOf(EthUnsubscribe.class); + } + + @Test + public void factoryCreatesExpectedNumberOfMethods() { + final Map methodsMap = factory.methods(); + assertThat(methodsMap).hasSize(2); + } + + @Test + public void factoryIncludesJsonRpcMethodsWhenCreatingWebsocketMethods() { + final JsonRpcMethod jsonRpcMethod1 = jsonRpcMethod("method1"); + final JsonRpcMethod jsonRpcMethod2 = jsonRpcMethod("method2"); + + final Map jsonRpcMethodsMap = new HashMap<>(); + jsonRpcMethodsMap.put("method1", jsonRpcMethod1); + jsonRpcMethodsMap.put("method2", jsonRpcMethod2); + factory = new WebSocketMethodsFactory(subscriptionManager, jsonRpcMethodsMap); + + final Map methods = factory.methods(); + + assertThat(methods).containsKeys("method1", "method2"); + } + + private JsonRpcMethod jsonRpcMethod(final String name) { + final JsonRpcMethod jsonRpcMethod = mock(JsonRpcMethod.class); + return jsonRpcMethod; + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java new file mode 100755 index 00000000000..fa4acf01430 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionBuilderTest.java @@ -0,0 +1,130 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.blockheaders.NewBlockHeadersSubscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.logs.LogsSubscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing.SyncingSubscription; + +import java.util.function.Function; + +import org.junit.Before; +import org.junit.Test; + +public class SubscriptionBuilderTest { + + private SubscriptionBuilder subscriptionBuilder; + + @Before + public void before() { + subscriptionBuilder = new SubscriptionBuilder(); + } + + @Test + public void shouldBuildLogsSubscriptionWhenSubscribeRequestTypeIsLogs() { + final FilterParameter filterParameter = filterParameter(); + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.LOGS, filterParameter, null, "connectionId"); + final LogsSubscription expectedSubscription = new LogsSubscription(1L, filterParameter); + + final Subscription builtSubscription = subscriptionBuilder.build(1L, subscribeRequest); + + assertThat(builtSubscription).isEqualToComparingFieldByField(expectedSubscription); + } + + @Test + public void shouldBuildNewBlockHeadsSubscriptionWhenSubscribeRequestTypeIsNewBlockHeads() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, true, "connectionId"); + final NewBlockHeadersSubscription expectedSubscription = + new NewBlockHeadersSubscription(1L, true); + + final Subscription builtSubscription = subscriptionBuilder.build(1L, subscribeRequest); + + assertThat(builtSubscription).isEqualToComparingFieldByField(expectedSubscription); + } + + @Test + public void shouldBuildSubscriptionWhenSubscribeRequestTypeIsNewPendingTransactions() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_PENDING_TRANSACTIONS, null, null, "connectionId"); + final Subscription expectedSubscription = + new Subscription(1L, SubscriptionType.NEW_PENDING_TRANSACTIONS); + + final Subscription builtSubscription = subscriptionBuilder.build(1L, subscribeRequest); + + assertThat(builtSubscription).isEqualToComparingFieldByField(expectedSubscription); + } + + @Test + public void shouldBuildSubscriptionWhenSubscribeRequestTypeIsSyncing() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, "connectionId"); + final SyncingSubscription expectedSubscription = + new SyncingSubscription(1L, SubscriptionType.SYNCING); + + final Subscription builtSubscription = subscriptionBuilder.build(1L, subscribeRequest); + + assertThat(builtSubscription).isEqualToComparingFieldByField(expectedSubscription); + } + + @Test + public void shouldReturnLogsSubscriptionWhenMappingLogsSubscription() { + final Function function = + subscriptionBuilder.mapToSubscriptionClass(LogsSubscription.class); + final Subscription subscription = new LogsSubscription(1L, filterParameter()); + + assertThat(function.apply(subscription)).isInstanceOf(LogsSubscription.class); + } + + @Test + public void shouldReturnNewBlockHeadsSubscriptionWhenMappingNewBlockHeadsSubscription() { + final Function function = + subscriptionBuilder.mapToSubscriptionClass(NewBlockHeadersSubscription.class); + final Subscription subscription = new NewBlockHeadersSubscription(1L, true); + + assertThat(function.apply(subscription)).isInstanceOf(NewBlockHeadersSubscription.class); + } + + @Test + public void shouldReturnSubscriptionWhenMappingNewPendingTransactionsSubscription() { + final Function function = + subscriptionBuilder.mapToSubscriptionClass(Subscription.class); + final Subscription logsSubscription = + new Subscription(1L, SubscriptionType.NEW_PENDING_TRANSACTIONS); + + assertThat(function.apply(logsSubscription)).isInstanceOf(Subscription.class); + } + + @Test + public void shouldReturnSubscriptionWhenMappingSyncingSubscription() { + final Function function = + subscriptionBuilder.mapToSubscriptionClass(SyncingSubscription.class); + final Subscription subscription = new SyncingSubscription(1L, SubscriptionType.SYNCING); + + assertThat(function.apply(subscription)).isInstanceOf(SyncingSubscription.class); + } + + @Test + @SuppressWarnings("ReturnValueIgnored") + public void shouldThrownIllegalArgumentExceptionWhenMappingWrongSubscriptionType() { + final Function function = + subscriptionBuilder.mapToSubscriptionClass(LogsSubscription.class); + final NewBlockHeadersSubscription subscription = new NewBlockHeadersSubscription(1L, true); + + final Throwable thrown = catchThrowable(() -> function.apply(subscription)); + assertThat(thrown) + .hasNoCause() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "NewBlockHeadersSubscription instance can't be mapped to type LogsSubscription"); + } + + private FilterParameter filterParameter() { + return new FilterParameter(null, null, null, null, null); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java new file mode 100755 index 00000000000..1d282b3f5e0 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerSendMessageTest.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription; + +import static junit.framework.TestCase.fail; +import static org.mockito.Mockito.mock; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.response.SubscriptionResponse; + +import java.util.UUID; + +import io.vertx.core.Vertx; +import io.vertx.core.json.Json; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(VertxUnitRunner.class) +public class SubscriptionManagerSendMessageTest { + + private static final int VERTX_AWAIT_TIMEOUT_MILLIS = 10000; + + private Vertx vertx; + private SubscriptionManager subscriptionManager; + + @Before + public void before(final TestContext context) { + vertx = Vertx.vertx(); + subscriptionManager = new SubscriptionManager(); + vertx.deployVerticle(subscriptionManager, context.asyncAssertSuccess()); + } + + @Test + public void shouldSendMessageOnTheConnectionIdEventBusAddressForExistingSubscription( + final TestContext context) { + final String connectionId = UUID.randomUUID().toString(); + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, connectionId); + + final JsonRpcResult expectedResult = mock(JsonRpcResult.class); + final SubscriptionResponse expectedResponse = new SubscriptionResponse(1L, expectedResult); + + final Long subscriptionId = subscriptionManager.subscribe(subscribeRequest); + + final Async async = context.async(); + + vertx + .eventBus() + .consumer(connectionId) + .handler( + msg -> { + context.assertEquals(Json.encode(expectedResponse), msg.body()); + async.complete(); + }) + .completionHandler(v -> subscriptionManager.sendMessage(subscriptionId, expectedResult)); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } + + @Test + public void shouldNotSendMessageOnTheConnectionIdEventBusAddressForAbsentSubscription( + final TestContext context) { + final String connectionId = UUID.randomUUID().toString(); + + final Async async = context.async(); + + vertx + .eventBus() + .consumer(connectionId) + .handler( + msg -> { + fail("Shouldn't receive message"); + async.complete(); + }) + .completionHandler(v -> subscriptionManager.sendMessage(1L, mock(JsonRpcResult.class))); + + // if it doesn't receive the message in 5 seconds we assume it won't receive anymore + vertx.setPeriodic(5000, v -> async.complete()); + + async.awaitSuccess(VERTX_AWAIT_TIMEOUT_MILLIS); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerTest.java new file mode 100755 index 00000000000..e4715e49615 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/SubscriptionManagerTest.java @@ -0,0 +1,204 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.both; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; + +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.blockheaders.NewBlockHeadersSubscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.UnsubscribeRequest; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing.SyncingSubscription; + +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class SubscriptionManagerTest { + + @Rule public ExpectedException thrown = ExpectedException.none(); + + private SubscriptionManager subscriptionManager; + private final String CONNECTION_ID = "test-connection-id"; + + @Before + public void before() { + subscriptionManager = new SubscriptionManager(); + } + + @Test + public void subscribeShouldCreateSubscription() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + + final Long subscriptionId = subscriptionManager.subscribe(subscribeRequest); + + final SyncingSubscription expectedSubscription = + new SyncingSubscription(subscriptionId, subscribeRequest.getSubscriptionType()); + final Subscription createdSubscription = + subscriptionManager.subscriptions().get(subscriptionId); + + assertThat(subscriptionId).isEqualTo(1L); + assertThat(createdSubscription).isEqualTo(expectedSubscription); + } + + @Test + public void unsubscribeExistingSubscriptionShouldDestroySubscription() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + final Long subscriptionId = subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.subscriptions().get(subscriptionId)).isNotNull(); + + final UnsubscribeRequest unsubscribeRequest = + new UnsubscribeRequest(subscriptionId, CONNECTION_ID); + final boolean unsubscribed = subscriptionManager.unsubscribe(unsubscribeRequest); + + assertThat(unsubscribed).isTrue(); + assertThat(subscriptionManager.subscriptions().get(subscriptionId)).isNull(); + } + + @Test + public void unsubscribeAbsentSubscriptionShouldThrowSubscriptionNotFoundException() { + final UnsubscribeRequest unsubscribeRequest = new UnsubscribeRequest(1L, CONNECTION_ID); + + thrown.expect( + both(hasMessage(equalTo("Subscription not found (id=1)"))) + .and(instanceOf(SubscriptionNotFoundException.class))); + + subscriptionManager.unsubscribe(unsubscribeRequest); + } + + @Test + public void shouldAddSubscriptionToNewConnection() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + + subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().containsKey(CONNECTION_ID)) + .isTrue(); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(1); + } + + @Test + public void shouldAddSubscriptionToExistingConnection() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + + subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().containsKey(CONNECTION_ID)) + .isTrue(); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(1); + + subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(2); + } + + @Test + public void shouldRemoveSubscriptionFromExistingConnection() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + + final Long subscriptionId1 = subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().containsKey(CONNECTION_ID)) + .isTrue(); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(1); + + final Long subscriptionId2 = subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(2); + + final UnsubscribeRequest unsubscribeRequest = + new UnsubscribeRequest(subscriptionId1, CONNECTION_ID); + subscriptionManager.unsubscribe(unsubscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).get(0)) + .isEqualTo(subscriptionId2); + } + + @Test + public void shouldRemoveConnectionWithSingleSubscriptions() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + + final Long subscriptionId1 = subscriptionManager.subscribe(subscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().size()).isEqualTo(1); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().containsKey(CONNECTION_ID)) + .isTrue(); + assertThat(subscriptionManager.getConnectionSubscriptionsMap().get(CONNECTION_ID).size()) + .isEqualTo(1); + + final UnsubscribeRequest unsubscribeRequest = + new UnsubscribeRequest(subscriptionId1, CONNECTION_ID); + subscriptionManager.unsubscribe(unsubscribeRequest); + + assertThat(subscriptionManager.getConnectionSubscriptionsMap().isEmpty()).isTrue(); + } + + @Test + public void getSubscriptionsOfCorrectTypeReturnExpectedSubscriptions() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, true, CONNECTION_ID); + + subscriptionManager.subscribe(subscribeRequest); + + final List subscriptions = + subscriptionManager.subscriptionsOfType( + SubscriptionType.NEW_BLOCK_HEADERS, NewBlockHeadersSubscription.class); + + assertThat(subscriptions).hasSize(1); + assertThat(subscriptions.get(0)).isInstanceOf(NewBlockHeadersSubscription.class); + } + + @Test + public void getSubscriptionsOfWrongTypeReturnEmptyList() { + final SubscribeRequest subscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, true, CONNECTION_ID); + + subscriptionManager.subscribe(subscribeRequest); + + final List subscriptions = + subscriptionManager.subscriptionsOfType( + SubscriptionType.SYNCING, NewBlockHeadersSubscription.class); + + assertThat(subscriptions).hasSize(0); + } + + @Test + public void unsubscribeWithUnknownConnectionId() { + final SubscribeRequest subscribeRequestOne = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, true, CONNECTION_ID); + final long subscriptionId = subscriptionManager.subscribe(subscribeRequestOne); + + final boolean success = + subscriptionManager.unsubscribe( + new UnsubscribeRequest(subscriptionId, "unknown-connection-id")); + + assertThat(success).isTrue(); + } + + // TODO vertx event bus testing for response +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionServiceTest.java new file mode 100755 index 00000000000..34b5ae15cd3 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/blockheaders/NewBlockHeadersSubscriptionServiceTest.java @@ -0,0 +1,179 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.blockheaders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.TransactionTestFixture; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResult; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.BlockResultFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.JsonRpcResult; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NewBlockHeadersSubscriptionServiceTest { + + private NewBlockHeadersSubscriptionService newBlockHeadersSubscriptionService; + + @Captor ArgumentCaptor subscriptionIdCaptor; + @Captor ArgumentCaptor responseCaptor; + + @Mock private SubscriptionManager subscriptionManager; + @Mock private BlockchainQueries blockchainQueries; + + private final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture(); + private final TransactionTestFixture txTestFixture = new TransactionTestFixture(); + private final BlockHeader blockHeader = blockHeaderTestFixture.buildHeader(); + private final BlockResultFactory blockResultFactory = new BlockResultFactory(); + + @Before + public void before() { + newBlockHeadersSubscriptionService = + new NewBlockHeadersSubscriptionService(subscriptionManager, blockchainQueries); + } + + @Test + public void shouldSendMessageWhenBlockAdded() { + final NewBlockHeadersSubscription subscription = createSubscription(false); + final BlockWithMetadata testBlockWithMetadata = + new BlockWithMetadata<>( + blockHeader, Collections.emptyList(), Collections.emptyList(), UInt256.ONE, 1); + final BlockResult expectedNewBlock = blockResultFactory.transactionHash(testBlockWithMetadata); + + when(blockchainQueries.blockByHashWithTxHashes(testBlockWithMetadata.getHeader().getHash())) + .thenReturn(Optional.of(testBlockWithMetadata)); + + simulateAddingBlock(); + + verify(subscriptionManager) + .sendMessage(subscriptionIdCaptor.capture(), responseCaptor.capture()); + final Long actualSubscriptionId = subscriptionIdCaptor.getValue(); + final Object actualBlock = responseCaptor.getValue(); + + assertThat(actualSubscriptionId).isEqualTo(subscription.getId()); + assertThat(actualBlock).isEqualToComparingFieldByFieldRecursively(expectedNewBlock); + + verify(subscriptionManager, times(1)).sendMessage(any(), any()); + } + + @Test + public void shouldReturnTxHashesWhenIncludeTransactionsFalse() { + final NewBlockHeadersSubscription subscription = createSubscription(false); + final List txHashList = transactionsWithHashOnly(); + final BlockWithMetadata testBlockWithMetadata = + new BlockWithMetadata<>(blockHeader, txHashList, Collections.emptyList(), UInt256.ONE, 1); + final BlockResult expectedNewBlock = blockResultFactory.transactionHash(testBlockWithMetadata); + + when(blockchainQueries.blockByHashWithTxHashes(testBlockWithMetadata.getHeader().getHash())) + .thenReturn(Optional.of(testBlockWithMetadata)); + + simulateAddingBlock(); + + verify(subscriptionManager) + .sendMessage(subscriptionIdCaptor.capture(), responseCaptor.capture()); + final Long actualSubscriptionId = subscriptionIdCaptor.getValue(); + final Object actualBlock = responseCaptor.getValue(); + + assertThat(actualSubscriptionId).isEqualTo(subscription.getId()); + assertThat(actualBlock).isInstanceOf(BlockResult.class); + final BlockResult actualBlockResult = (BlockResult) actualBlock; + assertThat(actualBlockResult.getTransactions()).hasSize(txHashList.size()); + assertThat(actualBlock).isEqualToComparingFieldByFieldRecursively(expectedNewBlock); + + verify(subscriptionManager, times(1)).sendMessage(any(), any()); + verify(blockchainQueries, times(1)).blockByHashWithTxHashes(any()); + verify(blockchainQueries, times(0)).blockByHash(any()); + } + + @Test + public void shouldReturnCompleteTxWhenParameterTrue() { + final NewBlockHeadersSubscription subscription = createSubscription(true); + final List txHashList = transactionsWithMetadata(); + final BlockWithMetadata testBlockWithMetadata = + new BlockWithMetadata<>( + blockHeader, txHashList, Collections.emptyList(), blockHeader.getDifficulty(), 0); + final BlockResult expectedNewBlock = + blockResultFactory.transactionComplete(testBlockWithMetadata); + + when(blockchainQueries.blockByHash(testBlockWithMetadata.getHeader().getHash())) + .thenReturn(Optional.of(testBlockWithMetadata)); + + simulateAddingBlock(); + + verify(subscriptionManager) + .sendMessage(subscriptionIdCaptor.capture(), responseCaptor.capture()); + final Long actualSubscriptionId = subscriptionIdCaptor.getValue(); + final Object actualBlock = responseCaptor.getValue(); + + assertThat(actualSubscriptionId).isEqualTo(subscription.getId()); + assertThat(actualBlock).isInstanceOf(BlockResult.class); + final BlockResult actualBlockResult = (BlockResult) actualBlock; + assertThat(actualBlockResult.getTransactions()).hasSize(txHashList.size()); + assertThat(actualBlock).isEqualToComparingFieldByFieldRecursively(expectedNewBlock); + + verify(subscriptionManager, times(1)).sendMessage(any(), any()); + verify(blockchainQueries, times(0)).blockByHashWithTxHashes(any()); + verify(blockchainQueries, times(1)).blockByHash(any()); + } + + private void simulateAddingBlock() { + final BlockBody blockBody = new BlockBody(Collections.emptyList(), Collections.emptyList()); + final Block testBlock = new Block(blockHeader, blockBody); + newBlockHeadersSubscriptionService.onBlockAdded( + BlockAddedEvent.createForHeadAdvancement(testBlock), blockchainQueries.getBlockchain()); + verify(blockchainQueries, times(1)).getBlockchain(); + } + + private List transactionsWithMetadata() { + final TransactionWithMetadata t1 = + new TransactionWithMetadata( + txTestFixture.createTransaction(KeyPair.generate()), 0L, Hash.ZERO, 0); + final TransactionWithMetadata t2 = + new TransactionWithMetadata( + txTestFixture.createTransaction(KeyPair.generate()), 1L, Hash.ZERO, 1); + return Lists.newArrayList(t1, t2); + } + + private List transactionsWithHashOnly() { + final List hashes = new ArrayList<>(); + for (final TransactionWithMetadata transactionWithMetadata : transactionsWithMetadata()) { + hashes.add(transactionWithMetadata.getTransaction().hash()); + } + return hashes; + } + + private NewBlockHeadersSubscription createSubscription(final boolean includeTransactions) { + final NewBlockHeadersSubscription headerSub = + new NewBlockHeadersSubscription(1L, includeTransactions); + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn(Lists.newArrayList(headerSub)); + return headerSub; + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java new file mode 100755 index 00000000000..e128b204fc5 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/logs/LogsSubscriptionServiceTest.java @@ -0,0 +1,242 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.logs; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.chain.BlockAddedEvent; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.BlockHeaderTestFixture; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Log; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.TransactionTestFixture; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.LogWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.TransactionReceiptWithMetadata; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.LogResult; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class LogsSubscriptionServiceTest { + + private final KeyPair keyPair = KeyPair.generate(); + private final BlockHeaderTestFixture blockHeaderTestFixture = new BlockHeaderTestFixture(); + private final TransactionTestFixture txTestFixture = new TransactionTestFixture(); + + private LogsSubscriptionService logsSubscriptionService; + + @Mock private SubscriptionManager subscriptionManager; + @Mock private BlockchainQueries blockchainQueries; + @Mock private Blockchain blockchain; + + @Before + public void before() { + logsSubscriptionService = new LogsSubscriptionService(subscriptionManager, blockchainQueries); + } + + @Test + public void shouldSendLogMessageWhenBlockAddedEventHasAddedTransactionsMatchingSubscription() { + final Address address = Address.fromHexString("0x0"); + final LogsSubscription subscription = createSubscription(address); + final Transaction transaction = createTransaction(); + final Log log = createLog(address); + final LogResult expectedLogResult = createLogResult(transaction, log, false); + + logsSubscriptionService.onBlockAdded(createBlockAddedEvent(transaction, null), blockchain); + + verify(subscriptionManager).sendMessage(eq(subscription.getId()), refEq(expectedLogResult)); + } + + @Test + public void shouldSendLogMessageWhenBlockAddedEventHasRemovedTransactionsMatchingSubscription() { + final Address address = Address.fromHexString("0x0"); + final LogsSubscription subscription = createSubscription(address); + final Transaction transaction = createTransaction(); + final Log log = createLog(address); + final LogResult expectedLogResult = createLogResult(transaction, log, true); + + logsSubscriptionService.onBlockAdded(createBlockAddedEvent(null, transaction), blockchain); + + verify(subscriptionManager).sendMessage(eq(subscription.getId()), refEq(expectedLogResult)); + } + + @Test + public void shouldSendMessageForAllLogsMatchingSubscription() { + final Address address = Address.fromHexString("0x0"); + final Log log = createLog(address); + final LogsSubscription subscription = createSubscription(address); + final List addedTransactions = createTransactionsWithLog(log); + final List removedTransactions = createTransactionsWithLog(log); + + logsSubscriptionService.onBlockAdded( + createBlockAddedEvent(addedTransactions, removedTransactions), blockchain); + + final int totalOfLogs = addedTransactions.size() + removedTransactions.size(); + + verify(subscriptionManager, times(totalOfLogs)).sendMessage(eq(subscription.getId()), any()); + } + + @Test + public void shouldSendLogMessageToAllMatchingSubscriptions() { + final Address address = Address.fromHexString("0x0"); + final List subscriptions = createSubscriptions(address); + final Transaction transaction = createTransaction(); + final Log log = createLog(address); + final LogResult expectedLogResult = createLogResult(transaction, log, false); + + logsSubscriptionService.onBlockAdded(createBlockAddedEvent(transaction, null), blockchain); + + verify(subscriptionManager, times(subscriptions.size())) + .sendMessage(any(), refEq(expectedLogResult)); + } + + @Test + public void shouldNotSendLogMessageWhenBlockAddedEventHasNoTransactions() { + final Address address = Address.fromHexString("0x0"); + createSubscription(address); + + logsSubscriptionService.onBlockAdded( + createBlockAddedEvent(Collections.emptyList(), Collections.emptyList()), blockchain); + + verify(subscriptionManager).subscriptionsOfType(any(), any()); + verify(subscriptionManager, times(0)).sendMessage(any(), any()); + } + + @Test + public void shouldNotSendLogMessageWhenLogsDoNotMatchAnySubscription() { + createSubscription(Address.fromHexString("0x0")); + final Transaction transaction = createTransaction(); + final Log log = createLog(Address.fromHexString("0x1")); + createLogResult(transaction, log, false); + + logsSubscriptionService.onBlockAdded(createBlockAddedEvent(transaction, null), blockchain); + + verify(subscriptionManager).subscriptionsOfType(any(), any()); + verify(subscriptionManager, times(0)).sendMessage(any(), any()); + } + + private Transaction createTransaction() { + return txTestFixture.createTransaction(keyPair); + } + + private Log createLog(final Address address) { + return new Log(address, BytesValue.EMPTY, Collections.emptyList()); + } + + private LogsSubscription createSubscription(final Address address) { + final FilterParameter filterParameter = + new FilterParameter(null, null, Lists.newArrayList(address.toString()), null, null); + final LogsSubscription logsSubscription = new LogsSubscription(1L, filterParameter); + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn(Lists.newArrayList(logsSubscription)); + return logsSubscription; + } + + private List createSubscriptions(final Address address) { + final List subscriptions = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + final FilterParameter filterParameter = + new FilterParameter(null, null, Lists.newArrayList(address.toString()), null, null); + subscriptions.add(new LogsSubscription((long) i, filterParameter)); + } + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn(Lists.newArrayList(subscriptions)); + return subscriptions; + } + + private LogResult createLogResult( + final Transaction transaction, final Log log, final boolean removed) { + final TransactionReceiptWithMetadata txReceiptWithMetadata = + createTransactionWithLog(transaction, log); + final LogWithMetadata logWithMetadata = createLogWithMetadata(txReceiptWithMetadata, removed); + return new LogResult(logWithMetadata); + } + + private TransactionReceiptWithMetadata createTransactionWithLog( + final Transaction transaction, final Log log) { + final BlockHeader blockHeader = blockHeaderTestFixture.buildHeader(); + final TransactionReceipt transactionReceipt = + new TransactionReceipt(Hash.ZERO, 1L, Lists.newArrayList(log)); + final TransactionReceiptWithMetadata transactionReceiptWithMetadata = + TransactionReceiptWithMetadata.create( + transactionReceipt, + transaction, + transaction.hash(), + 0, + 1L, + blockHeader.getHash(), + blockHeader.getNumber()); + + when(blockchainQueries.transactionReceiptByTransactionHash(eq(transaction.hash()))) + .thenReturn(Optional.of(transactionReceiptWithMetadata)); + + return transactionReceiptWithMetadata; + } + + private BlockAddedEvent createBlockAddedEvent( + final Transaction addedTransaction, final Transaction removedTransaction) { + final Block block = mock(Block.class); + return BlockAddedEvent.createForChainReorg( + block, + addedTransaction != null ? Lists.newArrayList(addedTransaction) : Collections.emptyList(), + removedTransaction != null + ? Lists.newArrayList(removedTransaction) + : Collections.emptyList()); + } + + private BlockAddedEvent createBlockAddedEvent( + final List addedTransactions, final List removedTransactions) { + final Block block = mock(Block.class); + return BlockAddedEvent.createForChainReorg( + block, + addedTransactions != null ? Lists.newArrayList(addedTransactions) : Collections.emptyList(), + removedTransactions != null + ? Lists.newArrayList(removedTransactions) + : Collections.emptyList()); + } + + private List createTransactionsWithLog(final Log log) { + final ArrayList transactions = + Lists.newArrayList(createTransaction(), createTransaction(), createTransaction()); + transactions.forEach(tx -> createTransactionWithLog(tx, log)); + return transactions; + } + + private LogWithMetadata createLogWithMetadata( + final TransactionReceiptWithMetadata transactionReceiptWithMetadata, final boolean removed) { + return LogWithMetadata.create( + 0, + transactionReceiptWithMetadata.getBlockNumber(), + transactionReceiptWithMetadata.getBlockHash(), + transactionReceiptWithMetadata.getTransactionHash(), + transactionReceiptWithMetadata.getTransactionIndex(), + transactionReceiptWithMetadata.getReceipt().getLogs().get(0).getLogger(), + transactionReceiptWithMetadata.getReceipt().getLogs().get(0).getData(), + transactionReceiptWithMetadata.getReceipt().getLogs().get(0).getTopics(), + removed); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionServiceTest.java new file mode 100755 index 00000000000..5968f00fe78 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/pending/PendingTransactionSubscriptionServiceTest.java @@ -0,0 +1,97 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.pending; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.Subscription; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PendingTransactionSubscriptionServiceTest { + + private static final Hash TX_ONE = + Hash.fromHexString("0x15876958423545c3c7b0fcf9be8ffb543305ee1b43db87ed380dcf0cd16589f7"); + + @Mock private SubscriptionManager subscriptionManager; + @Mock private Blockchain blockchain; + @Mock private Block block; + + private PendingTransactionSubscriptionService service; + + @Before + public void setUp() { + service = new PendingTransactionSubscriptionService(subscriptionManager); + } + + @Test + public void onTransactionAddedMustSendMessage() { + final long[] subscriptionIds = new long[] {5, 56, 989}; + setUpSubscriptions(subscriptionIds); + final Transaction pending = transaction(TX_ONE); + + service.onTransactionAdded(pending); + + verifyZeroInteractions(block); + verifyZeroInteractions(blockchain); + verifySubscriptionMangerInteractions(messages(TX_ONE, subscriptionIds)); + } + + private void verifySubscriptionMangerInteractions(final Map expected) { + verify(subscriptionManager) + .subscriptionsOfType(SubscriptionType.NEW_PENDING_TRANSACTIONS, Subscription.class); + + for (final Map.Entry message : expected.entrySet()) { + verify(subscriptionManager) + .sendMessage( + eq(message.getKey()), refEq(new PendingTransactionResult(message.getValue()))); + } + + verifyNoMoreInteractions(subscriptionManager); + } + + private Map messages(final Hash result, final long... subscriptionIds) { + final Map messages = new HashMap<>(); + + for (final long subscriptionId : subscriptionIds) { + messages.put(subscriptionId, result); + } + + return messages; + } + + private Transaction transaction(final Hash hash) { + final Transaction tx = mock(Transaction.class); + when(tx.hash()).thenReturn(hash); + return tx; + } + + private void setUpSubscriptions(final long... subscriptionsIds) { + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn( + Arrays.stream(subscriptionsIds) + .mapToObj(id -> new Subscription(id, SubscriptionType.NEW_PENDING_TRANSACTIONS)) + .collect(Collectors.toList())); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java new file mode 100755 index 00000000000..c38bd06cc4a --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/request/SubscriptionRequestMapperTest.java @@ -0,0 +1,283 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.both; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; + +import net.consensys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import net.consensys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.FilterParameter; +import net.consensys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketRpcRequest; + +import java.util.Arrays; +import java.util.Collections; + +import io.vertx.core.json.Json; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class SubscriptionRequestMapperTest { + + @Rule public ExpectedException thrown = ExpectedException.none(); + + private SubscriptionRequestMapper mapper; + // These tests aren't passing through WebSocketRequestHandler, so connectionId is null. + private final String CONNECTION_ID = null; + + @Before + public void before() { + mapper = new SubscriptionRequestMapper(new JsonRpcParameter()); + } + + @Test + public void mapRequestToUnsubscribeRequest() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_unsubscribe\", \"params\": [\"0x1\"]}"); + final UnsubscribeRequest expectedUnsubscribeRequest = new UnsubscribeRequest(1L, CONNECTION_ID); + + final UnsubscribeRequest unsubscribeRequest = mapper.mapUnsubscribeRequest(jsonRpcRequest); + + assertThat(unsubscribeRequest).isEqualTo(expectedUnsubscribeRequest); + } + + @Test + public void mapRequestToUnsubscribeRequestIgnoresSecondParam() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_unsubscribe\", \"params\": [\"0x1\", {\"foo\": \"bar\"}]}"); + final UnsubscribeRequest expectedUnsubscribeRequest = new UnsubscribeRequest(1L, CONNECTION_ID); + + final UnsubscribeRequest unsubscribeRequest = mapper.mapUnsubscribeRequest(jsonRpcRequest); + + assertThat(unsubscribeRequest).isEqualTo(expectedUnsubscribeRequest); + } + + @Test + public void mapRequestToUnsubscribeRequestMissingSubscriptionIdFails() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest("{\"id\": 1, \"method\": \"eth_unsubscribe\", \"params\": []}"); + + thrown.expect(InvalidSubscriptionRequestException.class); + thrown.expectCause( + both(hasMessage(equalTo("Missing required json rpc parameter at index 0"))) + .and(instanceOf(InvalidJsonRpcParameters.class))); + + mapper.mapUnsubscribeRequest(jsonRpcRequest); + } + + @Test + public void mapRequestToNewHeadsSubscribeIncludeTransactionsTrue() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"newHeads\", {\"includeTransactions\": true}]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, true, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapRequestToNewHeadsSubscribeIncludeTransactionsFalse() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"newHeads\", {\"includeTransactions\": false}]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, false, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapRequestToNewHeadsSubscribeOmittingIncludeTransactions() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"newHeads\"]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, false, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapRequestToNewHeadsWithInvalidSecondParamFails() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"newHeads\", {\"foo\": \"bar\"}]}"); + + thrown.expect(InvalidSubscriptionRequestException.class); + thrown.expectCause( + both(hasMessage(equalTo("Invalid json rpc parameter at index 1"))) + .and(instanceOf(InvalidJsonRpcParameters.class))); + + mapper.mapSubscribeRequest(jsonRpcRequest); + } + + @Test + public void mapRequestToNewHeadsIgnoresThirdParam() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"newHeads\", {\"includeTransactions\": true}, {\"foo\": \"bar\"}]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_BLOCK_HEADERS, null, true, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapRequestToLogs() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"logs\", {\"address\": \"0x8320fe7702b96808f7bbc0d4a888ed1468216cfd\", \"topics\": [\"0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902\"]}]}"); + + final FilterParameter expectedFilterParam = + new FilterParameter( + null, + null, + Arrays.asList("0x8320fe7702b96808f7bbc0d4a888ed1468216cfd"), + Arrays.asList( + Arrays.asList( + "0xd78a0cb8bb633d06981248b816e7bd33c2a35a6089241d099fa519e361cab902")), + null); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.LOGS, expectedFilterParam, null, null); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest) + .isEqualToComparingFieldByFieldRecursively(expectedSubscribeRequest); + } + + @Test + public void mapRequestToLogsWithoutTopics() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"logs\", {\"address\": \"0x8320fe7702b96808f7bbc0d4a888ed1468216cfd\"}]}"); + + final FilterParameter expectedFilterParam = + new FilterParameter( + null, + null, + Arrays.asList("0x8320fe7702b96808f7bbc0d4a888ed1468216cfd"), + Collections.emptyList(), + null); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.LOGS, expectedFilterParam, null, null); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest) + .isEqualToComparingFieldByFieldRecursively(expectedSubscribeRequest); + } + + @Test + public void mapRequestToLogsWithInvalidTopicInFilter() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"logs\", {\"address\": \"0x0\", \"topics\": [\"0x1\"]}]}"); + + thrown.expect(InvalidSubscriptionRequestException.class); + thrown.expectCause( + both(hasMessage(equalTo("Invalid odd-length hex binary representation 0x1"))) + .and(instanceOf(IllegalArgumentException.class))); + + mapper.mapSubscribeRequest(jsonRpcRequest); + } + + @Test + public void mapRequestToLogsWithInvalidSecondParam() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"logs\", {\"foo\": \"bar\"}]}"); + + thrown.expect(InvalidSubscriptionRequestException.class); + thrown.expectCause( + both(hasMessage(equalTo("Invalid json rpc parameter at index 1"))) + .and(instanceOf(InvalidJsonRpcParameters.class))); + + mapper.mapSubscribeRequest(jsonRpcRequest); + } + + @Test + public void mapRequestToNewPendingTransactions() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"newPendingTransactions\"]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_PENDING_TRANSACTIONS, null, null, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapRequestToNewPendingTransactionsIgnoresSecondParam() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"newPendingTransactions\", {\"foo\": \"bar\"}]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.NEW_PENDING_TRANSACTIONS, null, null, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapRequestToSyncingSubscribe() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"syncing\"]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapRequestToSyncingSubscribeIgnoresSecondParam() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"syncing\", {\"foo\": \"bar\"}]}"); + final SubscribeRequest expectedSubscribeRequest = + new SubscribeRequest(SubscriptionType.SYNCING, null, null, CONNECTION_ID); + + final SubscribeRequest subscribeRequest = mapper.mapSubscribeRequest(jsonRpcRequest); + + assertThat(subscribeRequest).isEqualTo(expectedSubscribeRequest); + } + + @Test + public void mapAbsentSubscriptionTypeRequestFails() { + final JsonRpcRequest jsonRpcRequest = + parseWebSocketRpcRequest( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"foo\"]}"); + + thrown.expect(InvalidSubscriptionRequestException.class); + thrown.expectCause( + both(hasMessage(equalTo("Invalid json rpc parameter at index 0"))) + .and(instanceOf(InvalidJsonRpcParameters.class))); + + mapper.mapSubscribeRequest(jsonRpcRequest); + } + + private WebSocketRpcRequest parseWebSocketRpcRequest(final String json) { + return Json.decodeValue(json, WebSocketRpcRequest.class); + } +} diff --git a/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionServiceTest.java b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionServiceTest.java new file mode 100755 index 00000000000..dcb7bd900a9 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/net/consensys/pantheon/ethereum/jsonrpc/websocket/subscription/syncing/SyncingSubscriptionServiceTest.java @@ -0,0 +1,99 @@ +package net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.core.SyncStatus; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.jsonrpc.internal.results.SyncingResult; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.request.SubscriptionType; + +import java.util.Optional; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SyncingSubscriptionServiceTest { + + private SyncingSubscriptionService syncingSubscriptionService; + + @Mock private SubscriptionManager subscriptionManager; + @Mock private Synchronizer synchronizer; + + @Before + public void before() { + syncingSubscriptionService = new SyncingSubscriptionService(subscriptionManager, synchronizer); + } + + @Test + public void shouldSendSyncStatusWhenSyncing() { + final SyncingSubscription subscription = new SyncingSubscription(9L, SubscriptionType.SYNCING); + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn(Lists.newArrayList(subscription)); + final SyncStatus syncStatus = new SyncStatus(0L, 1L, 1L); + final SyncingResult expectedSyncingResult = new SyncingResult(syncStatus); + when(synchronizer.getSyncStatus()).thenReturn(Optional.of(syncStatus)); + + syncingSubscriptionService.sendSyncingToMatchingSubscriptions(); + + verify(subscriptionManager).sendMessage(eq(subscription.getId()), refEq(expectedSyncingResult)); + } + + @Test + public void shouldSendFalseWhenNotSyncing() { + final SyncingSubscription subscription = new SyncingSubscription(9L, SubscriptionType.SYNCING); + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn(Lists.newArrayList(subscription)); + when(synchronizer.getSyncStatus()).thenReturn(Optional.empty()); + + syncingSubscriptionService.sendSyncingToMatchingSubscriptions(); + + verify(subscriptionManager) + .sendMessage(eq(subscription.getId()), refEq(new NotSynchronisingResult())); + } + + @Test + public void shouldSendNoMoreSyncStatusWhenSyncingStatusHasNotChanged() { + final SyncingSubscription subscription = new SyncingSubscription(9L, SubscriptionType.SYNCING); + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn(Lists.newArrayList(subscription)); + final SyncStatus syncStatus = new SyncStatus(0L, 1L, 1L); + final SyncingResult expectedSyncingResult = new SyncingResult(syncStatus); + when(synchronizer.getSyncStatus()).thenReturn(Optional.of(syncStatus)); + + syncingSubscriptionService.sendSyncingToMatchingSubscriptions(); + + verify(subscriptionManager).sendMessage(eq(subscription.getId()), refEq(expectedSyncingResult)); + syncingSubscriptionService.sendSyncingToMatchingSubscriptions(); + } + + @Test + public void shouldSendDifferentSyncStatusWhenSyncingStatusHasChanged() { + final SyncingSubscription subscription = new SyncingSubscription(9L, SubscriptionType.SYNCING); + when(subscriptionManager.subscriptionsOfType(any(), any())) + .thenReturn(Lists.newArrayList(subscription)); + final SyncStatus syncStatus1 = new SyncStatus(0L, 1L, 9L); + final SyncStatus syncStatus2 = new SyncStatus(0L, 5L, 9L); + final SyncingResult expectedSyncingResult1 = new SyncingResult(syncStatus1); + when(synchronizer.getSyncStatus()).thenReturn(Optional.of(syncStatus1)); + + syncingSubscriptionService.sendSyncingToMatchingSubscriptions(); + verify(subscriptionManager) + .sendMessage(eq(subscription.getId()), refEq(expectedSyncingResult1)); + + final SyncingResult expectedSyncingResult2 = new SyncingResult(syncStatus2); + when(synchronizer.getSyncStatus()).thenReturn(Optional.of(syncStatus2)); + syncingSubscriptionService.sendSyncingToMatchingSubscriptions(); + verify(subscriptionManager) + .sendMessage(eq(subscription.getId()), refEq(expectedSyncingResult2)); + } +} diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_blockNumber.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_blockNumber.json new file mode 100755 index 00000000000..fa3de8caae9 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_blockNumber.json @@ -0,0 +1,14 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_blockNumber", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": "0x20" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_block_8.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_block_8.json new file mode 100755 index 00000000000..cc99a8fe5c9 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_block_8.json @@ -0,0 +1,21 @@ +{ + "request": { + "id": 4, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "data": "0x12a7b914" + }, + "0x8" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 4, + "result": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_callParamsMissing_block_8.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_callParamsMissing_block_8.json new file mode 100755 index 00000000000..59472c20002 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_callParamsMissing_block_8.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 4, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + "0x8" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 4, + "error":{ + "code":-32602, + "message":"Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_earliestBlock.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_earliestBlock.json new file mode 100755 index 00000000000..7aff5d3e441 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_earliestBlock.json @@ -0,0 +1,21 @@ +{ + "request": { + "id": 5, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "data": "0x12a7b914" + }, + "earliest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 5, + "result": "0x" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasLimitTooLow_block_8.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasLimitTooLow_block_8.json new file mode 100755 index 00000000000..471f2a57280 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasLimitTooLow_block_8.json @@ -0,0 +1,24 @@ +{ + "request": { + "id": 4, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x0" + }, + "0x8" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 4, + "error" : { + "code" : -32003, + "message" : "Intrinsic gas exceeds gas limit" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasPriceTooHigh_block_8.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasPriceTooHigh_block_8.json new file mode 100755 index 00000000000..9eb9a949b4f --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_gasPriceTooHigh_block_8.json @@ -0,0 +1,24 @@ +{ + "request": { + "id": 4, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gasPrice": "0x10000000000000" + }, + "0x8" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 4, + "error" : { + "code" : -32004, + "message" : "Upfront cost exceeds account balance" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_latestBlock.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_latestBlock.json new file mode 100755 index 00000000000..e83188970d6 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_latestBlock.json @@ -0,0 +1,21 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "data": "0x12a7b914" + }, + "latest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_toMissing_block_8.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_toMissing_block_8.json new file mode 100755 index 00000000000..87db58189c9 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_toMissing_block_8.json @@ -0,0 +1,23 @@ +{ + "request": { + "id": 4, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "data": "0x12a7b914" + }, + "0x8" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 4, + "error":{ + "code":-32602, + "message":"Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_valueTooHigh_block_8.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_valueTooHigh_block_8.json new file mode 100755 index 00000000000..f2c33a254ff --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_call_valueTooHigh_block_8.json @@ -0,0 +1,24 @@ +{ + "request": { + "id": 4, + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "from": "a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "value": "0x340ab63a021fc9aa" + }, + "0x8" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 4, + "error" : { + "code" : -32004, + "message" : "Upfront cost exceeds account balance" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_contractDeploy.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_contractDeploy.json new file mode 100755 index 00000000000..501093d7b18 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_contractDeploy.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_estimateGas", + "params": [ + { + "from": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "data": "0x608060405234801561001057600080fd5b50610157806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bdab8bf146100515780639ae97baa14610068575b600080fd5b34801561005d57600080fd5b5061006661007f565b005b34801561007457600080fd5b5061007d6100b9565b005b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60016040518082815260200191505060405180910390a1565b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60026040518082815260200191505060405180910390a17fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60036040518082815260200191505060405180910390a15600a165627a7a7230582010ddaa52e73a98c06dbcd22b234b97206c1d7ed64a7c048e10c2043a3d2309cb0029" + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": "0x1b551" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_insufficientGas.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_insufficientGas.json new file mode 100755 index 00000000000..add63fd940f --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_insufficientGas.json @@ -0,0 +1,18 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_estimateGas", + "params": [ + { + "gas": "0x1" + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": "0x5208" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_noParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_noParams.json new file mode 100755 index 00000000000..d00ba4dafd2 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_noParams.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_estimateGas", + "params": [ + {} + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": "0x5208" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_transfer.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_transfer.json new file mode 100755 index 00000000000..5ce7101e655 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_estimateGas_transfer.json @@ -0,0 +1,20 @@ +{ + "request": { + "id": 3, + "jsonrpc": "2.0", + "method": "eth_estimateGas", + "params": [ + { + "from": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "to": "0x8888f1f195afa192cfee860698584c030f4c9db1", + "value": "0x1" + } + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 3, + "result": "0x5208" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeGreaterThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeGreaterThan.json new file mode 100755 index 00000000000..c52238a0bc9 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeGreaterThan.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 28, + "jsonrpc": "2.0", + "method": "eth_getBalance", + "params": [ + "0x8888f1f195afa192cfee860698584c030f4c9db1", + "0x21" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 28, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeLessThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeLessThan.json new file mode 100755 index 00000000000..f5eb76565b8 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_illegalRangeLessThan.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 28, + "jsonrpc": "2.0", + "method": "eth_getBalance", + "params": [ + "0x8888f1f195afa192cfee860698584c030f4c9db1", + "-0x10" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 28, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_invalidParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_invalidParams.json new file mode 100755 index 00000000000..6ed4b2ee46b --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_invalidParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 32, + "jsonrpc": "2.0", + "method": "eth_getBalance", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 32, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_latest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_latest.json new file mode 100755 index 00000000000..7a3d92249d1 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBalance_latest.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 27, + "jsonrpc": "2.0", + "method": "eth_getBalance", + "params": [ + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "latest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 27, + "result": "0x140" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_00.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_00.json new file mode 100755 index 00000000000..6f6e172144b --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_00.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 174, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 174, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_01.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_01.json new file mode 100755 index 00000000000..1d2e1478d7e --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_01.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 175, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x0e29f455b8db7b15042efe9eabe0beb0ce2c7901919bba1107b1352191e09942" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 175, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_02.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_02.json new file mode 100755 index 00000000000..b117526b1ab --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_02.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 176, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x3d813a0ffc9cd04436e17e3e9c309f1e80df0407078e50355ce0d570b5424812" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 176, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_03.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_03.json new file mode 100755 index 00000000000..e74b9c2d02d --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_03.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 177, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x4e9a67b663f9abe03e7e9fd5452c9497998337077122f44ee78a466f6a7358de" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 177, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_04.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_04.json new file mode 100755 index 00000000000..3167abf1b3b --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_04.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 178, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x609427ccfeae6d2a930927c9a29a0a3077cac7e4b5826159586b10e25770eef9" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 178, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_05.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_05.json new file mode 100755 index 00000000000..06c7ebc0e6f --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_05.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 179, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x666c5e63d52c4f8fb56830158e84ca903081f45929aedb5045c649645b410751" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 179, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_06.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_06.json new file mode 100755 index 00000000000..30ca50d1e00 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_06.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 180, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x4997e4f37ac249fec5dc4cefce8ceaa2671689c25c5a739f9360f5773ed24e36" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 180, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_07.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_07.json new file mode 100755 index 00000000000..47533500881 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_07.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 181, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x45f6111842923d5154a5aed97318ed56ed2a2c2d93ac9e1cb7383fb3cf937374" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 181, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_08.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_08.json new file mode 100755 index 00000000000..eb5672e4d26 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_08.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 182, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x0362d0ee919714b702cb31d2f4fe6b5c834f36cc19558acb81a4832f86738e39" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 182, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_09.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_09.json new file mode 100755 index 00000000000..9dd797d6502 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_09.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 183, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0xc37f28ec7d58caa76e838629f431821cf69b6bcdf0332df636017d12bfad18c0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 183, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_10.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_10.json new file mode 100755 index 00000000000..f846ef95552 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_10.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 184, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x595973e2cc9451287e31b6a9f5fe4150fd3471f3d2dfc8861fdd4fbfc12b1650" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 184, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_11.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_11.json new file mode 100755 index 00000000000..955206bb721 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_11.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 185, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x2c18d097b30201aa5208badb361bdbcf439c09a126923845c0aa78dbd3ddaa05" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 185, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_invalidParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_invalidParams.json new file mode 100755 index 00000000000..eebd896b8e2 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_invalidParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 209, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 209, + "error": { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_noResult.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_noResult.json new file mode 100755 index 00000000000..13399c6c3fe --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByHash_noResult.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 208, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByHash", + "params": [ + "0x878a132155f53adb7c993ded4cfb687977397d63d873fcdbeb06c18cac907a5c" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 208, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_00.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_00.json new file mode 100755 index 00000000000..28a7d10be9b --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_00.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 210, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByNumber", + "params": [ + "0x1" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 210, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_earliest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_earliest.json new file mode 100755 index 00000000000..6d9b9f93c82 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_earliest.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 246, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByNumber", + "params": [ + "earliest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 246, + "result": "0x0" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeGreaterThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeGreaterThan.json new file mode 100755 index 00000000000..48b8742740f --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeGreaterThan.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 210, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByNumber", + "params": [ + "0x21" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 210, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeLessThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeLessThan.json new file mode 100755 index 00000000000..0ad6c1e5ffe --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_illegalRangeLessThan.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 210, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByNumber", + "params": [ + "-0x10" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 210, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_invalidParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_invalidParams.json new file mode 100755 index 00000000000..13218f3725f --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_invalidParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 248, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByNumber", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 248, + "error": { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_latest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_latest.json new file mode 100755 index 00000000000..08dbfd66e53 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_latest.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 244, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByNumber", + "params": [ + "latest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 244, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_null.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_null.json new file mode 100755 index 00000000000..798d895f665 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getBlockTransactionCountByNumber_null.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 247, + "jsonrpc": "2.0", + "method": "eth_getBlockTransactionCountByNumber", + "params": [ + "0x1869f" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 247, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeGreaterThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeGreaterThan.json new file mode 100755 index 00000000000..7b1bbbf61c6 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeGreaterThan.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 13, + "jsonrpc": "2.0", + "method": "eth_getCode", + "params": [ + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x666" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 13, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeLessThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeLessThan.json new file mode 100755 index 00000000000..680607f3fc8 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_illegalRangeLessThan.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 13, + "jsonrpc": "2.0", + "method": "eth_getCode", + "params": [ + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "-0x10" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 13, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_invalidParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_invalidParams.json new file mode 100755 index 00000000000..ab0b87f1e95 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_invalidParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 255, + "jsonrpc": "2.0", + "method": "eth_getCode", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 255, + "error": { + "code": -32602, + "message": "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeLatest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeLatest.json new file mode 100755 index 00000000000..7fbd8b6ee90 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeLatest.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 250, + "jsonrpc": "2.0", + "method": "eth_getCode", + "params": [ + "0x8888f1f195afa192cfee860698584c030f4c9db1", + "latest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 250, + "result": "0x" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeNumber.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeNumber.json new file mode 100755 index 00000000000..8718925e193 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_noCodeNumber.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 253, + "jsonrpc": "2.0", + "method": "eth_getCode", + "params": [ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 253, + "result": "0x" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_success.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_success.json new file mode 100755 index 00000000000..9d32e7770f6 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getCode_success.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 0, + "jsonrpc": "2.0", + "method": "eth_getCode", + "params": [ + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "latest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 0, + "result": "0x6000357c010000000000000000000000000000000000000000000000000000000090048063102accc11461012c57806312a7b9141461013a5780631774e6461461014c5780631e26fd331461015d5780631f9030371461016e578063343a875d1461018057806338cc4831146101955780634e7ad367146101bd57806357cb2fc4146101cb57806365538c73146101e057806368895979146101ee57806376bc21d9146102005780639a19a9531461020e5780639dc2c8f51461021f578063a53b1c1e1461022d578063a67808571461023e578063b61c05031461024c578063c2b12a731461025a578063d2282dc51461026b578063e30081a01461027c578063e8beef5b1461028d578063f38b06001461029b578063f5b53e17146102a9578063fd408767146102bb57005b6101346104d6565b60006000f35b61014261039b565b8060005260206000f35b610157600435610326565b60006000f35b6101686004356102c9565b60006000f35b610176610442565b8060005260206000f35b6101886103d3565b8060ff1660005260206000f35b61019d610413565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b6101c56104c5565b60006000f35b6101d36103b7565b8060000b60005260206000f35b6101e8610454565b60006000f35b6101f6610401565b8060005260206000f35b61020861051f565b60006000f35b6102196004356102e5565b60006000f35b610227610693565b60006000f35b610238600435610342565b60006000f35b610246610484565b60006000f35b610254610493565b60006000f35b61026560043561038d565b60006000f35b610276600435610350565b60006000f35b61028760043561035e565b60006000f35b6102956105b4565b60006000f35b6102a3610547565b60006000f35b6102b16103ef565b8060005260206000f35b6102c3610600565b60006000f35b80600060006101000a81548160ff021916908302179055505b50565b80600060016101000a81548160ff02191690837f01000000000000000000000000000000000000000000000000000000000000009081020402179055505b50565b80600060026101000a81548160ff021916908302179055505b50565b806001600050819055505b50565b806002600050819055505b50565b80600360006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908302179055505b50565b806004600050819055505b50565b6000600060009054906101000a900460ff1690506103b4565b90565b6000600060019054906101000a900460000b90506103d0565b90565b6000600060029054906101000a900460ff1690506103ec565b90565b600060016000505490506103fe565b90565b60006002600050549050610410565b90565b6000600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905061043f565b90565b60006004600050549050610451565b90565b7f65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be5806000602a81526020016000a15b565b6000602a81526020016000a05b565b60017f81933b308056e7e85668661dcd102b1f22795b4431f9cf4625794f381c271c6b6000602a81526020016000a25b565b60016000602a81526020016000a15b565b3373ffffffffffffffffffffffffffffffffffffffff1660017f0e216b62efbb97e751a2ce09f607048751720397ecfb9eef1e48a6644948985b6000602a81526020016000a35b565b3373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a25b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017f317b31292193c2a4f561cc40a95ea0d97a2733f14af6d6d59522473e1f3ae65f6000602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a35b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017fd5f0a30e4be0c6be577a71eceb7464245a796a7e6a55c0d971837b250de05f4e60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff16600160007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a35b56" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdNegative.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdNegative.json new file mode 100755 index 00000000000..0a9d8966f3c --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdNegative.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_getFilterChanges", + "params": ["0x8000000000000000"] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "error" : { + "code" : -32000, + "message" : "Filter not found" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdTooLong.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdTooLong.json new file mode 100755 index 00000000000..439bd6921de --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_FilterIdTooLong.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_getFilterChanges", + "params": ["0x00FFFFFFFFFFFFFFFF"] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "error" : { + "code" : -32000, + "message" : "Filter not found" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_NonexistentFilter.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_NonexistentFilter.json new file mode 100755 index 00000000000..3f0a6d185e0 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getFilterChanges_NonexistentFilter.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_getFilterChanges", + "params": ["0x61"] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "error": { + "code" : -32000, + "message" : "Filter not found" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_blockhash.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_blockhash.json new file mode 100755 index 00000000000..176a0bb2a0b --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_blockhash.json @@ -0,0 +1,28 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getLogs", + "params": [{ + "address": [], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"]], + "blockhash": "0x3c419f39b340a4c35cc27b8f7880b779dc1abb9814ad13a2a5a55b885cc8ec2d" + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : [{ + "logIndex" : "0x0", + "removed": false, + "blockNumber" : "0x17", + "blockHash" : "0x3c419f39b340a4c35cc27b8f7880b779dc1abb9814ad13a2a5a55b885cc8ec2d", + "transactionHash" : "0x97a385bf570ced7821c6495b3877ddd2afd5c452f350f0d4876e98d9161389c6", + "transactionIndex" : "0x0", + "address" : "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "data" : "0x000000000000000000000000000000000000000000000000000000000000002a", + "topics" : ["0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"] + }] + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_failTopicPosition.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_failTopicPosition.json new file mode 100755 index 00000000000..11dc38ef066 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_failTopicPosition.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getLogs", + "params": [{ + "fromBlock": "0x17", + "toBlock": "0x17", + "address": [], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"], [null]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : [] + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_fromBlockExceedToBlock.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_fromBlockExceedToBlock.json new file mode 100755 index 00000000000..a5ca4cad920 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_fromBlockExceedToBlock.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getLogs", + "params": [{ + "fromBlock": "0x20", + "toBlock": "0x17", + "address": [], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : [] + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_invalidInput.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_invalidInput.json new file mode 100755 index 00000000000..039d4dc6f8e --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_invalidInput.json @@ -0,0 +1,29 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getLogs", + "params": [{ + "fromBlock": "0x17", + "toBlock": "0x17", + "address": [], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"]], + "blockhash": "0x3c419f39b340a4c35cc27b8f7880b779dc1abb9814ad13a2a5a55b885cc8ec2d" + }] + }, + "response": { + "jsonrpc" : "2.0", + "id" : 406, + "error" : { + "code": -32602, + "message": "Invalid params" + } + }, + "statusCode": 400 +} + + + + + + diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_matchTopic.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_matchTopic.json new file mode 100755 index 00000000000..b3b9bc82063 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_matchTopic.json @@ -0,0 +1,29 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getLogs", + "params": [{ + "fromBlock": "0x17", + "toBlock": "0x17", + "address": [], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : [{ + "logIndex" : "0x0", + "removed": false, + "blockNumber" : "0x17", + "blockHash" : "0x3c419f39b340a4c35cc27b8f7880b779dc1abb9814ad13a2a5a55b885cc8ec2d", + "transactionHash" : "0x97a385bf570ced7821c6495b3877ddd2afd5c452f350f0d4876e98d9161389c6", + "transactionIndex" : "0x0", + "address" : "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "data" : "0x000000000000000000000000000000000000000000000000000000000000002a", + "topics" : ["0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"] + }] + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_nullParam.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_nullParam.json new file mode 100755 index 00000000000..6c5e697ad75 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_nullParam.json @@ -0,0 +1,29 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getLogs", + "params": [{ + "fromBlock": "0x17", + "toBlock": "0x17", + "address": [], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : [{ + "logIndex" : "0x0", + "removed": false, + "blockNumber" : "0x17", + "blockHash" : "0x3c419f39b340a4c35cc27b8f7880b779dc1abb9814ad13a2a5a55b885cc8ec2d", + "transactionHash" : "0x97a385bf570ced7821c6495b3877ddd2afd5c452f350f0d4876e98d9161389c6", + "transactionIndex" : "0x0", + "address" : "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "data" : "0x000000000000000000000000000000000000000000000000000000000000002a", + "topics" : ["0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"] + }] + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_toBlockOutOfRange.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_toBlockOutOfRange.json new file mode 100755 index 00000000000..fbbe13dcde1 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getLogs_toBlockOutOfRange.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getLogs", + "params": [{ + "fromBlock": "0x17", + "toBlock": "0x21", + "address": [], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : [] + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_addressOnly.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_addressOnly.json new file mode 100755 index 00000000000..6e200577ae7 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_addressOnly.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_newFilter", + "params": [{ + "address": ["0x6295ee1b4f6dd65047762f924ecd367c17eabf8f"] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_emptyFilter.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_emptyFilter.json new file mode 100755 index 00000000000..2bb91387987 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_emptyFilter.json @@ -0,0 +1,14 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_newFilter", + "params": [{}] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_invalidFilter.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_invalidFilter.json new file mode 100755 index 00000000000..41c9c4d0582 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_invalidFilter.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_newFilter", + "params": [{ + "address": ["foo"] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_topicOnly.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_topicOnly.json new file mode 100755 index 00000000000..e9ce5f2d773 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_topicOnly.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_newFilter", + "params": [{ + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterLatestBlock.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterLatestBlock.json new file mode 100755 index 00000000000..0b5961ceb8d --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterLatestBlock.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_newFilter", + "params": [{ + "fromBlock": "latest", + "toBlock": "latest", + "address": ["0x6295ee1b4f6dd65047762f924ecd367c17eabf8f"], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterWithBlockNumber.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterWithBlockNumber.json new file mode 100755 index 00000000000..480b3dbca39 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getNewFilter_validFilterWithBlockNumber.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_newFilter", + "params": [{ + "fromBlock": "0x17", + "toBlock": "0x17", + "address": ["0x6295ee1b4f6dd65047762f924ecd367c17eabf8f"], + "topics": [["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580"]] + }] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result" : "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeGreaterThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeGreaterThan.json new file mode 100755 index 00000000000..dd366686396 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeGreaterThan.json @@ -0,0 +1,18 @@ +{ + "request": { + "id": 337, + "jsonrpc": "2.0", + "method": "eth_getStorageAt", + "params": [ + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x0", + "0x21" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 337, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeLessThan.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeLessThan.json new file mode 100755 index 00000000000..9061ee83d51 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_illegalRangeLessThan.json @@ -0,0 +1,18 @@ +{ + "request": { + "id": 337, + "jsonrpc": "2.0", + "method": "eth_getStorageAt", + "params": [ + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x0", + "-0x10" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 337, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_invalidParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_invalidParams.json new file mode 100755 index 00000000000..fda23e656a9 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_invalidParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 342, + "jsonrpc": "2.0", + "method": "eth_getStorageAt", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 342, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_latest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_latest.json new file mode 100755 index 00000000000..63cec80ea7c --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getStorageAt_latest.json @@ -0,0 +1,18 @@ +{ + "request": { + "id": 341, + "jsonrpc": "2.0", + "method": "eth_getStorageAt", + "params": [ + "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "0x4", + "latest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 341, + "result": "0xaabbccffffffffffffffffffffffffffffffffffffffffffffffffffffffffee" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_00.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_00.json new file mode 100755 index 00000000000..a4edc86b628 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_00.json @@ -0,0 +1,32 @@ +{ + "request": { + "id": 413, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 413, + "result": { + "blockHash": "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", + "blockNumber": "0x1", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x2fefd8", + "gasPrice": "0x1", + "hash": "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed", + "input": "0x5b5b610705806100106000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063102accc11461012c57806312a7b9141461013a5780631774e6461461014c5780631e26fd331461015d5780631f9030371461016e578063343a875d1461018057806338cc4831146101955780634e7ad367146101bd57806357cb2fc4146101cb57806365538c73146101e057806368895979146101ee57806376bc21d9146102005780639a19a9531461020e5780639dc2c8f51461021f578063a53b1c1e1461022d578063a67808571461023e578063b61c05031461024c578063c2b12a731461025a578063d2282dc51461026b578063e30081a01461027c578063e8beef5b1461028d578063f38b06001461029b578063f5b53e17146102a9578063fd408767146102bb57005b6101346104d6565b60006000f35b61014261039b565b8060005260206000f35b610157600435610326565b60006000f35b6101686004356102c9565b60006000f35b610176610442565b8060005260206000f35b6101886103d3565b8060ff1660005260206000f35b61019d610413565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b6101c56104c5565b60006000f35b6101d36103b7565b8060000b60005260206000f35b6101e8610454565b60006000f35b6101f6610401565b8060005260206000f35b61020861051f565b60006000f35b6102196004356102e5565b60006000f35b610227610693565b60006000f35b610238600435610342565b60006000f35b610246610484565b60006000f35b610254610493565b60006000f35b61026560043561038d565b60006000f35b610276600435610350565b60006000f35b61028760043561035e565b60006000f35b6102956105b4565b60006000f35b6102a3610547565b60006000f35b6102b16103ef565b8060005260206000f35b6102c3610600565b60006000f35b80600060006101000a81548160ff021916908302179055505b50565b80600060016101000a81548160ff02191690837f01000000000000000000000000000000000000000000000000000000000000009081020402179055505b50565b80600060026101000a81548160ff021916908302179055505b50565b806001600050819055505b50565b806002600050819055505b50565b80600360006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908302179055505b50565b806004600050819055505b50565b6000600060009054906101000a900460ff1690506103b4565b90565b6000600060019054906101000a900460000b90506103d0565b90565b6000600060029054906101000a900460ff1690506103ec565b90565b600060016000505490506103fe565b90565b60006002600050549050610410565b90565b6000600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905061043f565b90565b60006004600050549050610451565b90565b7f65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be5806000602a81526020016000a15b565b6000602a81526020016000a05b565b60017f81933b308056e7e85668661dcd102b1f22795b4431f9cf4625794f381c271c6b6000602a81526020016000a25b565b60016000602a81526020016000a15b565b3373ffffffffffffffffffffffffffffffffffffffff1660017f0e216b62efbb97e751a2ce09f607048751720397ecfb9eef1e48a6644948985b6000602a81526020016000a35b565b3373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a25b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017f317b31292193c2a4f561cc40a95ea0d97a2733f14af6d6d59522473e1f3ae65f6000602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a35b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017fd5f0a30e4be0c6be577a71eceb7464245a796a7e6a55c0d971837b250de05f4e60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff16600160007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a35b56", + "nonce": "0x0", + "to": null, + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1c", + "r": "0xe439aa8812c1c0a751b0931ea20c5a30cd54fe15cae883c59fd8107e04557679", + "s": "0x58d025af99b538b778a47da8115c43d5cee564c3cc8d58eb972aaf80ea2c406e" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_01.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_01.json new file mode 100755 index 00000000000..51cfefe27f2 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_01.json @@ -0,0 +1,32 @@ +{ + "request": { + "id": 414, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x0e29f455b8db7b15042efe9eabe0beb0ce2c7901919bba1107b1352191e09942", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 414, + "result": { + "blockHash": "0x0e29f455b8db7b15042efe9eabe0beb0ce2c7901919bba1107b1352191e09942", + "blockNumber": "0x2", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x4cb2f", + "gasPrice": "0x1", + "hash": "0xb1a62356d1433202cdef0ef9030f8abdfbb3aef549fab0867cf0eaee70b09d81", + "input": "0x12a7b914", + "nonce": "0x1", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1b", + "r": "0xed2e0f715eccaab4362c19c1cf35ad8031ab1cabe71ada3fe8b269fe9d726712", + "s": "0x6691074f289f826d23c92808ae363959eb958fb7a91fc721875ece4958114c65" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_02.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_02.json new file mode 100755 index 00000000000..e40ca03f955 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_02.json @@ -0,0 +1,32 @@ +{ + "request": { + "id": 446, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 446, + "result": { + "blockHash": "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "blockNumber": "0x20", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x4cb2f", + "gasPrice": "0x1", + "hash": "0xcef53f2311d7c80e9086d661e69ac11a5f3d081e28e02a9ba9b66749407ac310", + "input": "0x9dc2c8f5", + "nonce": "0x1f", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1b", + "r": "0x705b002a7df60707d33812e0298411721be20ea5a2f533707295140d89263b79", + "s": "0x78024390784f24160739533b3ceea2698289a02afd9cc768581b4aa3d5f4b105" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_intOverflow.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_intOverflow.json new file mode 100755 index 00000000000..30354bb71cb --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_intOverflow.json @@ -0,0 +1,20 @@ +{ + "request": { + "id": 448, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "0x9999999999999999999999999999999999999999999999999999999999999999" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 448, + "error": { + "code": -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_00.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_00.json new file mode 100755 index 00000000000..22371a93792 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_00.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 448, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 448, + "error": { + "code": -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_01.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_01.json new file mode 100755 index 00000000000..626c3a4d746 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParam_01.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 448, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 448, + "error": { + "code": -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParams.json new file mode 100755 index 00000000000..9ea6c609f2a --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_missingParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 448, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 448, + "error": { + "code": -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_null.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_null.json new file mode 100755 index 00000000000..f343142abc7 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_null.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 447, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", + "0xb" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 447, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_wrongParamType.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_wrongParamType.json new file mode 100755 index 00000000000..288a015496c --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockHashAndIndex_wrongParamType.json @@ -0,0 +1,20 @@ +{ + "request": { + "id": 448, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "0x0L" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 448, + "error": { + "code": -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_00.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_00.json new file mode 100755 index 00000000000..16e6bd9a16b --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_00.json @@ -0,0 +1,32 @@ +{ + "request": { + "id": 450, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "0x2", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 450, + "result": { + "blockHash": "0x0e29f455b8db7b15042efe9eabe0beb0ce2c7901919bba1107b1352191e09942", + "blockNumber": "0x2", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x4cb2f", + "gasPrice": "0x1", + "hash": "0xb1a62356d1433202cdef0ef9030f8abdfbb3aef549fab0867cf0eaee70b09d81", + "input": "0x12a7b914", + "nonce": "0x1", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1b", + "r": "0xed2e0f715eccaab4362c19c1cf35ad8031ab1cabe71ada3fe8b269fe9d726712", + "s": "0x6691074f289f826d23c92808ae363959eb958fb7a91fc721875ece4958114c65" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_01.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_01.json new file mode 100755 index 00000000000..1216deeb017 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_01.json @@ -0,0 +1,32 @@ +{ + "request": { + "id": 451, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "0x3", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 451, + "result": { + "blockHash": "0x3d813a0ffc9cd04436e17e3e9c309f1e80df0407078e50355ce0d570b5424812", + "blockNumber": "0x3", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x4cb2f", + "gasPrice": "0x1", + "hash": "0x78e0c0452452a2465744eee11506c40de70f3da1296aa96f08bcf5d326822784", + "input": "0x57cb2fc4", + "nonce": "0x2", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1b", + "r": "0x9dc3bf93e023b46d5d6d3ff2e62b06e10ba3877b8df69a408d8f8ec2ad8ea040", + "s": "0x46c830e900919e5e0e6e48d413ad3f1f7906c6f0fe51c5d38431f3fe64622143" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_earliestNull.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_earliestNull.json new file mode 100755 index 00000000000..3287b586d90 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_earliestNull.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 481, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "earliest", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 481, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_invalidParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_invalidParams.json new file mode 100755 index 00000000000..0a5646a7214 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_invalidParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 486, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 486, + "error": { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_latest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_latest.json new file mode 100755 index 00000000000..ad8cfd98918 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_latest.json @@ -0,0 +1,32 @@ +{ + "request": { + "id": 482, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "latest", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 482, + "result": { + "blockHash": "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "blockNumber": "0x20", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x4cb2f", + "gasPrice": "0x1", + "hash": "0xcef53f2311d7c80e9086d661e69ac11a5f3d081e28e02a9ba9b66749407ac310", + "input": "0x9dc2c8f5", + "nonce": "0x1f", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1b", + "r": "0x705b002a7df60707d33812e0298411721be20ea5a2f533707295140d89263b79", + "s": "0x78024390784f24160739533b3ceea2698289a02afd9cc768581b4aa3d5f4b105" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_null.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_null.json new file mode 100755 index 00000000000..931ee36c3b0 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_null.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 485, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "0x2", + "0xb" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 485, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_pendingNull.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_pendingNull.json new file mode 100755 index 00000000000..7f92502c9de --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByBlockNumberAndIndex_pendingNull.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 484, + "jsonrpc": "2.0", + "method": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "pending", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 484, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_addressReceiver.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_addressReceiver.json new file mode 100755 index 00000000000..6634cfe19cb --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_addressReceiver.json @@ -0,0 +1,31 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [ + "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 406, + "result": { + "blockHash": "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6", + "blockNumber": "0x1e", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x4cb2f", + "gasPrice": "0x1", + "hash": "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4", + "input": "0xe8beef5b", + "nonce": "0x1d", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1c", + "r": "0x11232cac2f935ab8dd5d5972438fde90e05d0dd620860b42886e7d54dc5c4a0c", + "s": "0x3dd467b5faa6e5a0f3c22a5396fefa5b03f07d8114d8434e0e1493736aad8d0e" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_contractCreation.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_contractCreation.json new file mode 100755 index 00000000000..2cea6d0874a --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_contractCreation.json @@ -0,0 +1,31 @@ +{ + "request": { + "id": 344, + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [ + "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 344, + "result": { + "blockHash": "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", + "blockNumber": "0x1", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gas": "0x2fefd8", + "gasPrice": "0x1", + "hash": "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed", + "input": "0x5b5b610705806100106000396000f3006000357c010000000000000000000000000000000000000000000000000000000090048063102accc11461012c57806312a7b9141461013a5780631774e6461461014c5780631e26fd331461015d5780631f9030371461016e578063343a875d1461018057806338cc4831146101955780634e7ad367146101bd57806357cb2fc4146101cb57806365538c73146101e057806368895979146101ee57806376bc21d9146102005780639a19a9531461020e5780639dc2c8f51461021f578063a53b1c1e1461022d578063a67808571461023e578063b61c05031461024c578063c2b12a731461025a578063d2282dc51461026b578063e30081a01461027c578063e8beef5b1461028d578063f38b06001461029b578063f5b53e17146102a9578063fd408767146102bb57005b6101346104d6565b60006000f35b61014261039b565b8060005260206000f35b610157600435610326565b60006000f35b6101686004356102c9565b60006000f35b610176610442565b8060005260206000f35b6101886103d3565b8060ff1660005260206000f35b61019d610413565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b6101c56104c5565b60006000f35b6101d36103b7565b8060000b60005260206000f35b6101e8610454565b60006000f35b6101f6610401565b8060005260206000f35b61020861051f565b60006000f35b6102196004356102e5565b60006000f35b610227610693565b60006000f35b610238600435610342565b60006000f35b610246610484565b60006000f35b610254610493565b60006000f35b61026560043561038d565b60006000f35b610276600435610350565b60006000f35b61028760043561035e565b60006000f35b6102956105b4565b60006000f35b6102a3610547565b60006000f35b6102b16103ef565b8060005260206000f35b6102c3610600565b60006000f35b80600060006101000a81548160ff021916908302179055505b50565b80600060016101000a81548160ff02191690837f01000000000000000000000000000000000000000000000000000000000000009081020402179055505b50565b80600060026101000a81548160ff021916908302179055505b50565b806001600050819055505b50565b806002600050819055505b50565b80600360006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908302179055505b50565b806004600050819055505b50565b6000600060009054906101000a900460ff1690506103b4565b90565b6000600060019054906101000a900460000b90506103d0565b90565b6000600060029054906101000a900460ff1690506103ec565b90565b600060016000505490506103fe565b90565b60006002600050549050610410565b90565b6000600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905061043f565b90565b60006004600050549050610451565b90565b7f65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be5806000602a81526020016000a15b565b6000602a81526020016000a05b565b60017f81933b308056e7e85668661dcd102b1f22795b4431f9cf4625794f381c271c6b6000602a81526020016000a25b565b60016000602a81526020016000a15b565b3373ffffffffffffffffffffffffffffffffffffffff1660017f0e216b62efbb97e751a2ce09f607048751720397ecfb9eef1e48a6644948985b6000602a81526020016000a35b565b3373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a25b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017f317b31292193c2a4f561cc40a95ea0d97a2733f14af6d6d59522473e1f3ae65f6000602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a35b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017fd5f0a30e4be0c6be577a71eceb7464245a796a7e6a55c0d971837b250de05f4e60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff16600160007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a35b56", + "nonce": "0x0", + "to": null, + "transactionIndex": "0x0", + "value": "0xa", + "v": "0x1c", + "r": "0xe439aa8812c1c0a751b0931ea20c5a30cd54fe15cae883c59fd8107e04557679", + "s": "0x58d025af99b538b778a47da8115c43d5cee564c3cc8d58eb972aaf80ea2c406e" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidHashAndIndex.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidHashAndIndex.json new file mode 100755 index 00000000000..b3b3ff27a0b --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidHashAndIndex.json @@ -0,0 +1,20 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [ + "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "0x0" + ] + }, + "response": { + "jsonrpc" : "2.0", + "id" : 406, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidParams.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidParams.json new file mode 100755 index 00000000000..86eb5867ff8 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_invalidParams.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 412, + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [] + }, + "response": { + "jsonrpc" : "2.0", + "id" : 412, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_null.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_null.json new file mode 100755 index 00000000000..d712d2ccbf8 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_null.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 396, + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [ + "0x1044c2ba6827a73c1994a2213d1d76fb147bf6dc2e3aaea6b8423c462f4be897" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 396, + "result": null + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_typeMismatch.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_typeMismatch.json new file mode 100755 index 00000000000..48bb8402d7e --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionByHash_typeMismatch.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 406, + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [ + "0x0" + ] + }, + "response": { + "jsonrpc" : "2.0", + "id" : 406, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_blockNumber.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_blockNumber.json new file mode 100755 index 00000000000..b68795707fa --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_blockNumber.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 487, + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 487, + "result": "0x0" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_earliest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_earliest.json new file mode 100755 index 00000000000..f246e9ea248 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_earliest.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 246, + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "earliest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 246, + "result": "0x0" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_illegalRange.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_illegalRange.json new file mode 100755 index 00000000000..9ceb071d9f5 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_illegalRange.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 487, + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x6667" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 487, + "result": null + }, + "statusCode": 200 +} diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_latest.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_latest.json new file mode 100755 index 00000000000..83ec3c30cbb --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_latest.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 488, + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "latest" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 488, + "result": "0x20" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_missingArgument.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_missingArgument.json new file mode 100755 index 00000000000..873a94bc855 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionCount_missingArgument.json @@ -0,0 +1,17 @@ +{ + "request": { + "id": 489, + "jsonrpc": "2.0", + "method": "eth_getTransactionCount", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 489, + "error": { + "code": -32602, + "message": "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_contractAddress.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_contractAddress.json new file mode 100755 index 00000000000..c64b099a4b1 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_contractAddress.json @@ -0,0 +1,29 @@ +{ + "request": { + "id": 491, + "jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [ + "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 491, + "result": { + "blockHash": "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", + "blockNumber": "0x1", + "contractAddress": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "cumulativeGasUsed": "0x78674", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gasUsed": "0x78674", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "root": "0x6a59608add7cee26032d1c5c3923b91d0b50e6103f61b2137b68229bcdc87395", + "to": null, + "transactionHash": "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed", + "transactionIndex": "0x0" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_logs.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_logs.json new file mode 100755 index 00000000000..7ea85959cbb --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_logs.json @@ -0,0 +1,46 @@ +{ + "request": { + "id": 555, + "jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [ + "0x185a9154a0acc4e0ffc84029aee0f3dbf57ff0b84ec7624cb80e7373a03e8aeb" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 555, + "result": { + "blockHash": "0x0f765087745aa259d9e5ac39c367c57432a16ed98e3b0d81c5b51d10f301dc49", + "blockNumber": "0x1f", + "contractAddress": null, + "cumulativeGasUsed": "0x5eef", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gasUsed": "0x5eef", + "logs": [ + { + "address": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "topics": [ + "0xd5f0a30e4be0c6be577a71eceb7464245a796a7e6a55c0d971837b250de05f4e", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ], + "data": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe9000000000000000000000000000000000000000000000000000000000000002a", + "blockNumber": "0x1f", + "transactionHash": "0x185a9154a0acc4e0ffc84029aee0f3dbf57ff0b84ec7624cb80e7373a03e8aeb", + "transactionIndex": "0x0", + "blockHash": "0x0f765087745aa259d9e5ac39c367c57432a16ed98e3b0d81c5b51d10f301dc49", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000080000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000400000000000000000200000000000000000002000000000000000000000001000000000000000000000000000000000000000000000000000200000000000000000000000000000000000800000000040000000000001000000000000000000000000000010000000000000000000400000", + "root": "0xb55d027526b3f56953584db678b5c3d1a418812c0106b0cfbc3c912c7898dfe5", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionHash": "0x185a9154a0acc4e0ffc84029aee0f3dbf57ff0b84ec7624cb80e7373a03e8aeb", + "transactionIndex": "0x0" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_nullContractAddress.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_nullContractAddress.json new file mode 100755 index 00000000000..031fc30c534 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_getTransactionReceipt_nullContractAddress.json @@ -0,0 +1,29 @@ +{ + "request": { + "id": 493, + "jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [ + "0xb1a62356d1433202cdef0ef9030f8abdfbb3aef549fab0867cf0eaee70b09d81" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 493, + "result": { + "blockHash": "0x0e29f455b8db7b15042efe9eabe0beb0ce2c7901919bba1107b1352191e09942", + "blockNumber": "0x2", + "contractAddress": null, + "cumulativeGasUsed": "0x53f0", + "from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "gasUsed": "0x53f0", + "logs": [], + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "root": "0x947228066df6272aac99931a1a639621d4ac7dc461ce9fd93dfcaad933e299ee", + "to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", + "transactionHash": "0xb1a62356d1433202cdef0ef9030f8abdfbb3aef549fab0867cf0eaee70b09d81", + "transactionIndex": "0x0" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newBlockFilter.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newBlockFilter.json new file mode 100755 index 00000000000..32377ace137 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newBlockFilter.json @@ -0,0 +1,14 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_newBlockFilter", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newPendingTransactionFilter.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newPendingTransactionFilter.json new file mode 100755 index 00000000000..b5bbab57016 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_newPendingTransactionFilter.json @@ -0,0 +1,14 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_newPendingTransactionFilter", + "params": [] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": "0x1" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_contractCreation.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_contractCreation.json new file mode 100755 index 00000000000..93c963c0c91 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_contractCreation.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [ + "0xf901ca0685174876e800830fffff8080b90177608060405234801561001057600080fd5b50610157806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bdab8bf146100515780639ae97baa14610068575b600080fd5b34801561005d57600080fd5b5061006661007f565b005b34801561007457600080fd5b5061007d6100b9565b005b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60016040518082815260200191505060405180910390a1565b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60026040518082815260200191505060405180910390a17fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60036040518082815260200191505060405180910390a15600a165627a7a7230582010ddaa52e73a98c06dbcd22b234b97206c1d7ed64a7c048e10c2043a3d2309cb00291ca00297f7489c9e70447d917f7069a145c9fd0543633bec0a17ac072f1e07ab7f24a0185bd6435c17603b85fd84b8b45605988e855238fe2bbc6ea1f7e9ee6a5fc15f" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0x84df486b376e7eaf35792d710fc38ce110e62ab9cdb73a45d191da74c2190617" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidByteValueHex.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidByteValueHex.json new file mode 100755 index 00000000000..43dc9552481 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidByteValueHex.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [ + "0x0" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidRawTransaction.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidRawTransaction.json new file mode 100755 index 00000000000..0465f81b979 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_invalidRawTransaction.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [ + "0x00" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_messageCall.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_messageCall.json new file mode 100755 index 00000000000..63bf7cbffdb --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_messageCall.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [ + "0xf8690885174876e800830fffff94450b61224a7df4d8a70f3e20d4fd6a6380b920d180843bdab8bf1ba0efcd6b9df2054a4e8599c0967f8e1e45bca79e2998ed7e8bafb4d29aba7dd5c2a01097184ba24f20dc097f1915fbb5f6ac955bbfc014f181df4d80bf04f4a1cfa5" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0xaa6e6646456c576edcd712dbb3f30bf46c3d8310b203960c1e675534553b2daf" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_transferEther.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_transferEther.json new file mode 100755 index 00000000000..6f07ebe64d4 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_transferEther.json @@ -0,0 +1,16 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [ + "0xf86d0485174876e800830222e0945aae326516b4f8fe08074b7e972e40a713048d62880de0b6b3a7640000801ba05d4e7998757264daab67df2ce6f7e7a0ae36910778a406ca73898c9899a32b9ea0674700d5c3d1d27f2e6b4469957dfd1a1c49bf92383d80717afc84eb05695d5b" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "result": "0xbaabcc1bd699e7378451e4ce5969edb9bdcae76cb79bdacae793525c31e423c7" + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_unsignedTransaction.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_unsignedTransaction.json new file mode 100755 index 00000000000..3b717f63b80 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_sendRawTransaction_unsignedTransaction.json @@ -0,0 +1,19 @@ +{ + "request": { + "id": 1, + "jsonrpc": "2.0", + "method": "eth_sendRawTransaction", + "params": [ + "0xed0a85174876e800830222e0945aae326516b4f8fe08074b7e972e40a713048d62880de0b6b3a7640000801c8080" + ] + }, + "response": { + "jsonrpc": "2.0", + "id": 1, + "error" : { + "code" : -32602, + "message" : "Invalid params" + } + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdNegative.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdNegative.json new file mode 100755 index 00000000000..fa78c6e9a2d --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdNegative.json @@ -0,0 +1,14 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_uninstallFilter", + "params": ["0x8000000000000000"] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": false + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdTooLong.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdTooLong.json new file mode 100755 index 00000000000..6f759eb6976 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_FilterIdTooLong.json @@ -0,0 +1,14 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_uninstallFilter", + "params": ["0x00FFFFFFFFFFFFFFFF"] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": false + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_NonexistentFilter.json b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_NonexistentFilter.json new file mode 100755 index 00000000000..265b5853b94 --- /dev/null +++ b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/eth_uninstallFilter_NonexistentFilter.json @@ -0,0 +1,14 @@ +{ + "request": { + "id": 2, + "jsonrpc": "2.0", + "method": "eth_uninstallFilter", + "params": ["0x31"] + }, + "response": { + "jsonrpc": "2.0", + "id": 2, + "result": false + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks b/ethereum/jsonrpc/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestBlockchain.blocks new file mode 100755 index 0000000000000000000000000000000000000000..d29453d3e53b4382cc3ff4254e1931182a6c630d GIT binary patch literal 23287 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 nodes = new HashMap<>(); + private final List capabilities; + + public MockNetwork(final List capabilities) { + this.capabilities = capabilities; + } + + /** + * Get the {@link P2PNetwork} that assumes a given {@link Peer} as the local node. This does not + * connect {@link Peer} to any other peer. Any connections established by {@link #connect(Peer, + * Peer)} require that both participating {@link Peer} have previously been passed to this method. + * + * @param peer Peer to get {@link P2PNetwork} for + * @return P2PNetwork as seen by {@link Peer} + */ + public P2PNetwork setup(final Peer peer) { + synchronized (this) { + return nodes.computeIfAbsent(peer, p -> new MockNetwork.MockP2PNetwork(peer, this)); + } + } + + private PeerConnection connect(final Peer source, final Peer target) { + synchronized (this) { + final MockNetwork.MockPeerConnection establishedConnection = + new MockNetwork.MockPeerConnection(source, target, this); + final MockP2PNetwork sourceNode = nodes.get(source); + final MockP2PNetwork targetNode = nodes.get(target); + sourceNode.connections.put(target, establishedConnection); + final MockNetwork.MockPeerConnection backChannel = + new MockNetwork.MockPeerConnection(target, source, this); + targetNode.connections.put(source, backChannel); + sourceNode.connectCallbacks.forEach(c -> c.accept(establishedConnection)); + targetNode.connectCallbacks.forEach(c -> c.accept(backChannel)); + return establishedConnection; + } + } + + private void disconnect( + final MockNetwork.MockPeerConnection connection, final DisconnectReason reason) { + synchronized (this) { + final MockP2PNetwork sourceNode = nodes.get(connection.from); + final MockP2PNetwork targetNode = nodes.get(connection.to); + if (targetNode.connections.remove(connection.from) == null + || sourceNode.connections.remove(connection.to) == null) { + throw new IllegalStateException( + String.format("No connection between %s and %s", connection.from, connection.to)); + } + targetNode.disconnectCallbacks.forEach(c -> c.onDisconnect(connection, reason, true)); + sourceNode.disconnectCallbacks.forEach( + c -> c.onDisconnect(connection, DisconnectReason.REQUESTED, false)); + } + } + + private final class MockP2PNetwork implements P2PNetwork { + + private final MockNetwork network; + + private final Map connections = new HashMap<>(); + + private final Peer self; + + private final Map>> protocolCallbacks = + new ConcurrentHashMap<>(); + + private final Subscribers> connectCallbacks = new Subscribers<>(); + + private final Subscribers disconnectCallbacks = new Subscribers<>(); + + MockP2PNetwork(final Peer self, final MockNetwork network) { + this.self = self; + this.network = network; + } + + @Override + public Collection getPeers() { + synchronized (network) { + return new ArrayList<>(connections.values()); + } + } + + @Override + public CompletableFuture connect(final Peer peer) { + synchronized (network) { + if (network.nodes.containsKey(peer)) { + final PeerConnection connection = connections.get(peer); + if (connection == null) { + return CompletableFuture.completedFuture(network.connect(self, peer)); + } else { + return CompletableFuture.completedFuture(connection); + } + } else { + return CompletableFuture.supplyAsync( + () -> { + throw new IllegalStateException( + String.format("Tried to connect to unknown peer %s", peer)); + }); + } + } + } + + @Override + public void subscribe(final Capability capability, final Consumer callback) { + protocolCallbacks.computeIfAbsent(capability, key -> new Subscribers<>()).subscribe(callback); + } + + @Override + public void subscribeConnect(final Consumer callback) { + connectCallbacks.subscribe(callback); + } + + @Override + public void subscribeDisconnect(final DisconnectCallback callback) { + disconnectCallbacks.subscribe(callback); + } + + @Override + public void stop() {} + + @Override + public void awaitStop() {} + + @Override + public InetSocketAddress getDiscoverySocketAddress() { + return null; + } + + @Override + public void run() {} + + @Override + public void close() {} + + @Override + public PeerInfo getSelf() { + return new PeerInfo( + 5, self.getId().toString(), new ArrayList<>(capabilities), 0, self.getId()); + } + + @Override + public boolean isListening() { + return true; + } + } + + /** + * A mock connection between two peers that simply invokes the callbacks on the other side's + * {@link MockNetwork.MockP2PNetwork}. + */ + private final class MockPeerConnection implements PeerConnection { + + /** {@link Peer} that this connection originates from. */ + private final Peer from; + + /** + * Peer that this connection targets and that will receive {@link Message}s sent via {@link + * #send(Capability, MessageData)}. + */ + private final Peer to; + + private final MockNetwork network; + + MockPeerConnection(final Peer source, final Peer target, final MockNetwork network) { + from = source; + to = target; + this.network = network; + } + + @Override + public void send(final Capability capability, final MessageData message) + throws PeerNotConnected { + synchronized (network) { + final MockNetwork.MockP2PNetwork target = network.nodes.get(to); + final MockNetwork.MockPeerConnection backChannel = target.connections.get(from); + if (backChannel != null) { + final Message msg = new DefaultMessage(backChannel, message); + final Subscribers> callbacks = target.protocolCallbacks.get(capability); + if (callbacks != null) { + callbacks.forEach(c -> c.accept(msg)); + } + } else { + throw new PeerNotConnected(String.format("%s not connected to %s", to, from)); + } + } + } + + @Override + public Set getAgreedCapabilities() { + return new HashSet<>(capabilities); + } + + @Override + public PeerInfo getPeer() { + return new PeerInfo( + 5, + "mock-network-client", + capabilities, + to.getEndpoint().getTcpPort().getAsInt(), + to.getId()); + } + + @Override + public void terminateConnection(final DisconnectReason reason, final boolean peerInitiated) { + network.disconnect(this, reason); + } + + @Override + public void disconnect(final DisconnectReason reason) { + network.disconnect(this, reason); + } + + @Override + public SocketAddress getLocalAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public SocketAddress getRemoteAddress() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/ethereum/mock-p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/testing/MockNetworkTest.java b/ethereum/mock-p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/testing/MockNetworkTest.java new file mode 100755 index 00000000000..1088edb6a55 --- /dev/null +++ b/ethereum/mock-p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/testing/MockNetworkTest.java @@ -0,0 +1,102 @@ +package net.consensys.pantheon.ethereum.p2p.testing; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.Message; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Predicate; + +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +/** Tests for {@link MockNetwork}. */ +public final class MockNetworkTest { + + @Test + public void exchangeMessages() throws Exception { + final Capability cap = Capability.create("eth", 63); + final MockNetwork network = new MockNetwork(Arrays.asList(cap)); + final Peer one = new DefaultPeer(randomId(), "192.168.1.2", 1234, 4321); + final Peer two = new DefaultPeer(randomId(), "192.168.1.3", 1234, 4321); + try (final P2PNetwork network1 = network.setup(one); + final P2PNetwork network2 = network.setup(two)) { + final CompletableFuture messageFuture = new CompletableFuture<>(); + network1.subscribe(cap, messageFuture::complete); + final Predicate isPeerOne = + peerConnection -> peerConnection.getPeer().getNodeId().equals(one.getId()); + final Predicate isPeerTwo = + peerConnection -> peerConnection.getPeer().getNodeId().equals(two.getId()); + Assertions.assertThat(network1.getPeers().stream().filter(isPeerTwo).findFirst()) + .isNotPresent(); + Assertions.assertThat(network2.getPeers().stream().filter(isPeerOne).findFirst()) + .isNotPresent(); + + // Validate Connect Behaviour + final CompletableFuture peer2Future = new CompletableFuture<>(); + network1.subscribeConnect(peer2Future::complete); + final CompletableFuture peer1Future = new CompletableFuture<>(); + network2.subscribeConnect(peer1Future::complete); + network1.connect(two).get(); + Assertions.assertThat(peer1Future.get().getPeer().getNodeId()).isEqualTo(one.getId()); + Assertions.assertThat(peer2Future.get().getPeer().getNodeId()).isEqualTo(two.getId()); + Assertions.assertThat(network1.getPeers().stream().filter(isPeerTwo).findFirst()).isPresent(); + final Optional optionalConnection = + network2.getPeers().stream().filter(isPeerOne).findFirst(); + Assertions.assertThat(optionalConnection).isPresent(); + + // Validate Message Exchange + final int size = 128; + final ByteBuf dataSent = NetworkMemoryPool.allocate(size); + final byte[] data = new byte[size]; + ThreadLocalRandom.current().nextBytes(data); + dataSent.writeBytes(data); + final int code = 0x74; + final PeerConnection connection = optionalConnection.get(); + connection.send(cap, new RawMessage(code, dataSent)); + final Message receivedMessage = messageFuture.get(); + final MessageData receivedMessageData = receivedMessage.getData(); + final ByteBuf receiveBuffer = NetworkMemoryPool.allocate(size); + receivedMessageData.writeTo(receiveBuffer); + Assertions.assertThat(receiveBuffer.compareTo(dataSent)).isEqualTo(0); + Assertions.assertThat(receivedMessage.getConnection().getPeer().getNodeId()) + .isEqualTo(two.getId()); + Assertions.assertThat(receivedMessageData.getSize()).isEqualTo(size); + Assertions.assertThat(receivedMessageData.getCode()).isEqualTo(code); + + // Validate Disconnect Behaviour + final CompletableFuture peer1DisconnectFuture = new CompletableFuture<>(); + final CompletableFuture peer2DisconnectFuture = new CompletableFuture<>(); + network2.subscribeDisconnect( + (peer, reason, initiatedByPeer) -> peer1DisconnectFuture.complete(reason)); + network1.subscribeDisconnect( + (peer, reason, initiatedByPeer) -> peer2DisconnectFuture.complete(reason)); + connection.disconnect(DisconnectReason.CLIENT_QUITTING); + Assertions.assertThat(peer1DisconnectFuture.get()).isEqualTo(DisconnectReason.REQUESTED); + Assertions.assertThat(peer2DisconnectFuture.get()) + .isEqualTo(DisconnectReason.CLIENT_QUITTING); + Assertions.assertThat(network1.getPeers().stream().filter(isPeerTwo).findFirst()) + .isNotPresent(); + Assertions.assertThat(network2.getPeers().stream().filter(isPeerOne).findFirst()) + .isNotPresent(); + } + } + + private static BytesValue randomId() { + final byte[] raw = new byte[DefaultPeer.PEER_ID_SIZE]; + ThreadLocalRandom.current().nextBytes(raw); + return BytesValue.wrap(raw); + } +} diff --git a/ethereum/p2p/build.gradle b/ethereum/p2p/build.gradle new file mode 100755 index 00000000000..ab6cba78d3f --- /dev/null +++ b/ethereum/p2p/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-p2p' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + implementation project(':crypto') + implementation project(':ethereum:rlp') + implementation project(':ethereum:core') + + implementation 'com.google.guava:guava' + implementation 'io.vertx:vertx-core' + implementation 'org.apache.logging.log4j:log4j-api' + implementation 'org.xerial.snappy:snappy-java' + + runtime 'org.apache.logging.log4j:log4j-core' + + // test dependencies. + testImplementation project (path: ':ethereum:core', configuration: 'testArtifacts') + testImplementation project(':testutil') + + testImplementation 'junit:junit' + testImplementation 'io.vertx:vertx-unit' + testImplementation 'io.vertx:vertx-codegen' + testImplementation "org.mockito:mockito-core" + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.awaitility:awaitility' + + testImplementation('io.pkts:pkts-core') { + exclude group: 'io.pkts', module: 'pkts-sdp' + exclude group: 'io.pkts', module: 'pkts-sip' + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkMemoryPool.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkMemoryPool.java new file mode 100755 index 00000000000..a7d295869e5 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkMemoryPool.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.ethereum.p2p; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.PooledByteBufAllocator; + +public class NetworkMemoryPool { + + private static final ByteBufAllocator ALLOCATOR = new PooledByteBufAllocator(); + + public static ByteBuf allocate(final int size) { + return ALLOCATOR.ioBuffer(0, size); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkRunner.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkRunner.java new file mode 100755 index 00000000000..526be17b9d9 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/NetworkRunner.java @@ -0,0 +1,200 @@ +package net.consensys.pantheon.ethereum.p2p; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class NetworkRunner implements AutoCloseable { + private static final Logger LOGGER = LogManager.getLogger(NetworkRunner.class); + + private final CountDownLatch shutdown = new CountDownLatch(1);; + private final AtomicBoolean started = new AtomicBoolean(false); + private final AtomicBoolean stopped = new AtomicBoolean(false); + + private final ExecutorService networkExecutor = + Executors.newFixedThreadPool( + 1, new ThreadFactoryBuilder().setNameFormat(this.getClass().getSimpleName()).build()); + + private final P2PNetwork network; + private final Map subProtocols; + private final List protocolManagers; + + private NetworkRunner( + final P2PNetwork network, + final Map subProtocols, + final List protocolManagers) { + this.network = network; + this.protocolManagers = protocolManagers; + this.subProtocols = subProtocols; + } + + public P2PNetwork getNetwork() { + return network; + } + + public static Builder builder() { + return new Builder(); + } + + public void start() { + if (started.compareAndSet(false, true)) { + LOGGER.info("Starting Network."); + setupHandlers(); + networkExecutor.submit(network); + } else { + LOGGER.error("Attempted to start already running network."); + } + } + + public void stop() { + if (stopped.compareAndSet(false, true)) { + LOGGER.info("Stopping Network."); + network.stop(); + for (final ProtocolManager protocolManager : protocolManagers) { + protocolManager.stop(); + } + networkExecutor.shutdown(); + shutdown.countDown(); + } else { + LOGGER.error("Attempted to stop already stopped network."); + } + } + + public void awaitStop() throws InterruptedException { + shutdown.await(); + network.awaitStop(); + for (final ProtocolManager protocolManager : protocolManagers) { + protocolManager.awaitStop(); + } + if (!networkExecutor.awaitTermination(2L, TimeUnit.MINUTES)) { + LOGGER.error("Network executor did not shutdown cleanly."); + networkExecutor.shutdownNow(); + networkExecutor.awaitTermination(2L, TimeUnit.MINUTES); + } + LOGGER.info("Network stopped."); + } + + private void setupHandlers() { + // Setup message handlers + for (final ProtocolManager protocolManager : protocolManagers) { + for (final Capability cap : protocolManager.getSupportedCapabilities()) { + final SubProtocol protocol = subProtocols.get(cap.getName()); + network.subscribe( + cap, + message -> { + final MessageData data = message.getData(); + try { + final int code = message.getData().getCode(); + if (!protocol.isValidMessageCode(cap.getVersion(), code)) { + // Handle invalid messsages by disconnecting + LOGGER.info( + "Invalid message code ({}-{}, {}) received from peer, disconnecting from:", + cap.getName(), + cap.getVersion(), + code, + message.getConnection()); + message.getConnection().disconnect(DisconnectReason.BREACH_OF_PROTOCOL); + return; + } + protocolManager.processMessage(cap, message); + } finally { + data.release(); + } + }); + } + } + + // Setup (dis)connect handlers + for (final ProtocolManager protocolManager : protocolManagers) { + network.subscribeConnect( + (connection) -> { + if (Collections.disjoint( + connection.getAgreedCapabilities(), protocolManager.getSupportedCapabilities())) { + return; + } + protocolManager.handleNewConnection(connection); + }); + + network.subscribeDisconnect( + (connection, disconnectReason, initiatedByPeer) -> { + if (Collections.disjoint( + connection.getAgreedCapabilities(), protocolManager.getSupportedCapabilities())) { + return; + } + protocolManager.handleDisconnect(connection, disconnectReason, initiatedByPeer); + }); + } + } + + @Override + public void close() throws Exception { + stop(); + } + + public static class Builder { + private Function, P2PNetwork> networkProvider; + List protocolManagers = new ArrayList<>(); + List subProtocols = new ArrayList<>(); + + public NetworkRunner build() { + final Map subProtocolMap = new HashMap<>(); + for (final SubProtocol subProtocol : subProtocols) { + subProtocolMap.put(subProtocol.getName(), subProtocol); + } + final List caps = + protocolManagers + .stream() + .flatMap(p -> p.getSupportedCapabilities().stream()) + .collect(Collectors.toList()); + for (final Capability cap : caps) { + if (!subProtocolMap.containsKey(cap.getName())) { + throw new IllegalStateException( + "No sub-protocol found corresponding to supported capability: " + cap); + } + } + final P2PNetwork network = networkProvider.apply(caps); + return new NetworkRunner(network, subProtocolMap, protocolManagers); + } + + public Builder protocolManagers(final List protocolManagers) { + this.protocolManagers = protocolManagers; + return this; + } + + public Builder network(final Function, P2PNetwork> networkProvider) { + this.networkProvider = networkProvider; + return this; + } + + public Builder subProtocols(final SubProtocol... subProtocols) { + this.subProtocols.addAll(Arrays.asList(subProtocols)); + return this; + } + + public Builder subProtocols(final List subProtocols) { + this.subProtocols.addAll(subProtocols); + return this; + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/DisconnectCallback.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/DisconnectCallback.java new file mode 100755 index 00000000000..50460ce439a --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/DisconnectCallback.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.p2p.api; + +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +@FunctionalInterface +public interface DisconnectCallback { + void onDisconnect(PeerConnection connection, DisconnectReason reason, boolean initiatedByPeer); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/Message.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/Message.java new file mode 100755 index 00000000000..3ab04465981 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/Message.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.p2p.api; + +/** A P2P network message received from another peer. */ +public interface Message { + + /** + * Returns the {@link MessageData} contained in the message. + * + * @return Data in the message + */ + MessageData getData(); + + /** + * {@link PeerConnection} this message was sent from. + * + * @return PeerConnection this message was sent from. + */ + PeerConnection getConnection(); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/MessageData.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/MessageData.java new file mode 100755 index 00000000000..000916a580a --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/MessageData.java @@ -0,0 +1,34 @@ +package net.consensys.pantheon.ethereum.p2p.api; + +import io.netty.buffer.ByteBuf; + +/** A P2P Network Message's Data. */ +public interface MessageData { + + /** + * Returns the size of the message. + * + * @return Number of bytes {@link #writeTo(ByteBuf)} will write to an output buffer. + */ + int getSize(); + + /** + * Returns the message's code. + * + * @return Message Code + */ + int getCode(); + + /** + * Puts the message's body into the given {@link ByteBuf}. + * + * @param output ByteBuf to write the message to + */ + void writeTo(ByteBuf output); + + /** Releases the memory underlying this message. */ + void release(); + + /** Retains (increments its reference count) the memory underlying this message once. */ + void retain(); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/P2PNetwork.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/P2PNetwork.java new file mode 100755 index 00000000000..6b9529a7a2d --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/P2PNetwork.java @@ -0,0 +1,76 @@ +package net.consensys.pantheon.ethereum.p2p.api; + +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; + +import java.io.Closeable; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** P2P Network Interface. */ +public interface P2PNetwork extends Closeable, Runnable { + + /** + * Returns a snapshot of the currently connected peer connections. + * + * @return Peers currently connected. + */ + Collection getPeers(); + + /** + * Connects to a {@link Peer}. + * + * @param peer Peer to connect to. + * @return Future of the established {@link PeerConnection} + */ + CompletableFuture connect(Peer peer); + + /** + * Subscribe a {@link Consumer} to all incoming {@link Message} of a given sub-protocol. Calling + * {@link #run()} on an implementation without at least having one subscribed {@link Consumer} per + * supported sub-protocol should throw a {@link RuntimeException}. + * + * @param capability Capability (sub-protocol) to subscribe to. + * @param consumer Consumer to subscribe + */ + void subscribe(Capability capability, Consumer consumer); + + /** + * Subscribe a {@link Consumer} to all incoming new Peer connection events. + * + * @param consumer Consumer to subscribe + */ + void subscribeConnect(Consumer consumer); + + /** + * Subscribe a {@link Consumer} to all incoming new Peer disconnect events. + * + * @param consumer Consumer to subscribe + */ + void subscribeDisconnect(DisconnectCallback consumer); + + /** Stops the P2P network layer. */ + void stop(); + + /** Blocks until the P2P network layer has stopped. */ + void awaitStop(); + + InetSocketAddress getDiscoverySocketAddress(); + + /** + * Returns {@link PeerInfo} object for this node + * + * @return the PeerInfo for this node. + */ + PeerInfo getSelf(); + + /** + * Checks if the node is listening for network connections + * + * @return true if the node is listening for network connections, false, otherwise. + */ + boolean isListening(); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/PeerConnection.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/PeerConnection.java new file mode 100755 index 00000000000..0b1e085e970 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/PeerConnection.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.p2p.api; + +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.io.IOException; +import java.net.SocketAddress; +import java.util.Set; + +/** A P2P connection to another node. */ +public interface PeerConnection { + + /** + * Send given data to the connected node. + * + * @param message Data to send + * @param capability Sub-protocol to use + * @throws PeerNotConnected On attempt to send to a disconnected peer + */ + void send(Capability capability, MessageData message) throws PeerNotConnected; + + /** @return a list of shared capabilities between this node and the connected peer */ + Set getAgreedCapabilities(); + + /** + * Returns the agreed capability corresponding to given protocol. + * + * @param protocol the name of the protocol + * @return the agreed capability corresponding to this protocol, returns null if no matching + * capability is supported + */ + default Capability capability(final String protocol) { + for (final Capability cap : getAgreedCapabilities()) { + if (cap.getName().equalsIgnoreCase(protocol)) { + return cap; + } + } + return null; + } + + /** + * Sends a message to the peer for the given subprotocol + * + * @param protocol the subprotocol name + * @param message the message to send + * @throws PeerNotConnected if the peer has disconnected + */ + default void sendForProtocol(final String protocol, final MessageData message) + throws PeerNotConnected { + send(capability(protocol), message); + } + + /** + * Returns the Peer's Description. + * + * @return Peer Description + */ + PeerInfo getPeer(); + + /** + * Immediately terminate the connection without sending a disconnect message. + * + * @param reason the reason for disconnection + * @param peerInitiated true if and only if the remote peer requested disconnection + */ + void terminateConnection(DisconnectReason reason, boolean peerInitiated); + + /** + * Disconnect from this Peer. + * + * @param reason Reason for disconnecting + */ + void disconnect(DisconnectReason reason); + + SocketAddress getLocalAddress(); + + SocketAddress getRemoteAddress(); + + class PeerNotConnected extends IOException { + + public PeerNotConnected(final String message) { + super(message); + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/ProtocolManager.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/ProtocolManager.java new file mode 100755 index 00000000000..380c69127f7 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/api/ProtocolManager.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.ethereum.p2p.api; + +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerRequirement; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.util.List; + +/** Represents an object responsible for managing a wire subprotocol. */ +public interface ProtocolManager extends AutoCloseable, PeerRequirement { + + String getSupportedProtocol(); + + /** + * Defines the list of capabilities supported by this manager. + * + * @return the list of capabilities supported by this manager + */ + List getSupportedCapabilities(); + + /** Stops the protocol manager. */ + void stop(); + + /** + * Blocks until protocol manager has stopped. + * + * @throws InterruptedException if interrupted while waiting + */ + void awaitStop() throws InterruptedException; + + /** + * Processes a message from a peer. + * + * @param cap the capability that corresponds to the message + * @param message the message from the peer + */ + void processMessage(Capability cap, Message message); + + /** + * Handles new peer connections. + * + * @param peerConnection the new peer connection + */ + void handleNewConnection(PeerConnection peerConnection); + + /** + * Handles peer disconnects. + * + * @param peerConnection the connection that is being closed + * @param disconnectReason the reason given for closing the connection + * @param initiatedByPeer true if the peer requested to disconnect, false if this node requested + * the disconnect + */ + void handleDisconnect( + PeerConnection peerConnection, DisconnectReason disconnectReason, boolean initiatedByPeer); + + @Override + default void close() { + stop(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/DiscoveryConfiguration.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/DiscoveryConfiguration.java new file mode 100755 index 00000000000..af78afe433c --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/DiscoveryConfiguration.java @@ -0,0 +1,146 @@ +package net.consensys.pantheon.ethereum.p2p.config; + +import static java.util.stream.Collectors.toList; + +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +public class DiscoveryConfiguration { + public static List MAINNET_BOOTSTRAP_NODES = + Collections.unmodifiableList( + Stream.of( + "enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@52.16.188.185:30303", + "enode://3f1d12044546b76342d59d4a05532c14b85aa669704bfe1f864fe079415aa2c02d743e03218e57a33fb94523adb54032871a6c51b2cc5514cb7c7e35b3ed0a99@13.93.211.84:30303", + "enode://78de8a0916848093c73790ead81d1928bec737d565119932b98c6b100d944b7a95e94f847f689fc723399d2e31129d182f7ef3863f2b4c820abbf3ab2722344d@191.235.84.50:30303", + "enode://158f8aab45f6d19c6cbf4a089c2670541a8da11978a2f90dbf6a502a4a3bab80d288afdbeb7ec0ef6d92de563767f3b1ea9e8e334ca711e9f8e2df5a0385e8e6@13.75.154.138:30303", + "enode://1118980bf48b0a3640bdba04e0fe78b1add18e1cd99bf22d53daac1fd9972ad650df52176e7c7d89d1114cfef2bc23a2959aa54998a46afcf7d91809f0855082@52.74.57.123:30303", + "enode://979b7fa28feeb35a4741660a16076f1943202cb72b6af70d327f053e248bab9ba81760f39d0701ef1d8f89cc1fbd2cacba0710a12cd5314d5e0c9021aa3637f9@5.1.83.226:30303") + .map(DefaultPeer::fromURI) + .collect(toList())); + + private boolean active = true; + private String bindHost = "0.0.0.0"; + private int bindPort = 30303; + private String advertisedHost = "127.0.0.1"; + private int bucketSize = 16; + private List bootstrapPeers = new ArrayList<>(); + + public static DiscoveryConfiguration create() { + return new DiscoveryConfiguration(); + } + + public String getBindHost() { + return bindHost; + } + + public DiscoveryConfiguration setBindHost(final String bindHost) { + this.bindHost = bindHost; + return this; + } + + public int getBindPort() { + return bindPort; + } + + public DiscoveryConfiguration setBindPort(final int bindPort) { + this.bindPort = bindPort; + return this; + } + + public boolean isActive() { + return active; + } + + public DiscoveryConfiguration setActive(final boolean active) { + this.active = active; + return this; + } + + public List getBootstrapPeers() { + return bootstrapPeers; + } + + public DiscoveryConfiguration setBootstrapPeers(final Collection bootstrapPeers) { + if (bootstrapPeers.stream().allMatch(String.class::isInstance)) { + this.bootstrapPeers = + bootstrapPeers + .stream() + .map(String.class::cast) + .filter(string -> !string.isEmpty()) + .map(DefaultPeer::fromURI) + .collect(toList()); + } else if (bootstrapPeers.stream().allMatch(Peer.class::isInstance)) { + this.bootstrapPeers = bootstrapPeers.stream().map(Peer.class::cast).collect(toList()); + } else { + throw new IllegalArgumentException("Expected a list of Peers or a list of enode URIs"); + } + return this; + } + + public String getAdvertisedHost() { + return advertisedHost; + } + + public DiscoveryConfiguration setAdvertisedHost(final String advertisedHost) { + this.advertisedHost = advertisedHost; + return this; + } + + public int getBucketSize() { + return bucketSize; + } + + public DiscoveryConfiguration setBucketSize(final int bucketSize) { + this.bucketSize = bucketSize; + return this; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof DiscoveryConfiguration)) { + return false; + } + final DiscoveryConfiguration that = (DiscoveryConfiguration) o; + return active == that.active + && bindPort == that.bindPort + && bucketSize == that.bucketSize + && Objects.equals(bindHost, that.bindHost) + && Objects.equals(advertisedHost, that.advertisedHost) + && Objects.equals(bootstrapPeers, that.bootstrapPeers); + } + + @Override + public int hashCode() { + return Objects.hash(active, bindHost, bindPort, advertisedHost, bucketSize, bootstrapPeers); + } + + @Override + public String toString() { + return "DiscoveryConfiguration{" + + "active=" + + active + + ", bindHost='" + + bindHost + + '\'' + + ", bindPort=" + + bindPort + + ", advertisedHost='" + + advertisedHost + + '\'' + + ", bucketSize=" + + bucketSize + + ", bootstrapPeers=" + + bootstrapPeers + + '}'; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/NetworkingConfiguration.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/NetworkingConfiguration.java new file mode 100755 index 00000000000..30b48c10116 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/NetworkingConfiguration.java @@ -0,0 +1,82 @@ +package net.consensys.pantheon.ethereum.p2p.config; + +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class NetworkingConfiguration { + private List supportedProtocols = Collections.emptyList(); + private DiscoveryConfiguration discovery = new DiscoveryConfiguration(); + private RlpxConfiguration rlpx = new RlpxConfiguration(); + private String clientId = ""; + + public static NetworkingConfiguration create() { + return new NetworkingConfiguration(); + } + + public List getSupportedProtocols() { + return supportedProtocols; + } + + public NetworkingConfiguration setSupportedProtocols(final List supportedProtocols) { + this.supportedProtocols = supportedProtocols; + return this; + } + + public NetworkingConfiguration setSupportedProtocols(final SubProtocol... supportedProtocols) { + this.supportedProtocols = Arrays.asList(supportedProtocols); + return this; + } + + public DiscoveryConfiguration getDiscovery() { + return discovery; + } + + public NetworkingConfiguration setDiscovery(final DiscoveryConfiguration discovery) { + this.discovery = discovery; + return this; + } + + public RlpxConfiguration getRlpx() { + return rlpx; + } + + public NetworkingConfiguration setRlpx(final RlpxConfiguration rlpx) { + this.rlpx = rlpx; + return this; + } + + public String getClientId() { + return clientId; + } + + public NetworkingConfiguration setClientId(final String clientId) { + this.clientId = clientId; + return this; + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof NetworkingConfiguration)) { + return false; + } + final NetworkingConfiguration that = (NetworkingConfiguration) o; + return Objects.equals(discovery, that.discovery) && Objects.equals(rlpx, that.rlpx); + } + + @Override + public int hashCode() { + return Objects.hash(discovery, rlpx); + } + + @Override + public String toString() { + return "NetworkingConfiguration{" + "discovery=" + discovery + ", rlpx=" + rlpx + '}'; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/RlpxConfiguration.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/RlpxConfiguration.java new file mode 100755 index 00000000000..2212e7edb6c --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/RlpxConfiguration.java @@ -0,0 +1,88 @@ +package net.consensys.pantheon.ethereum.p2p.config; + +import java.util.Objects; + +public class RlpxConfiguration { + private String clientId = "TestClient/1.0.0"; + private String bindHost = "0.0.0.0"; + private int bindPort = 30303; + private int maxPeers = 25; + private WireProtocolConfig wire = new WireProtocolConfig(); + + public static RlpxConfiguration create() { + return new RlpxConfiguration(); + } + + public String getBindHost() { + return bindHost; + } + + public RlpxConfiguration setBindHost(final String bindHost) { + this.bindHost = bindHost; + return this; + } + + public int getBindPort() { + return bindPort; + } + + public RlpxConfiguration setBindPort(final int bindPort) { + this.bindPort = bindPort; + return this; + } + + public WireProtocolConfig getWire() { + return wire; + } + + public RlpxConfiguration setWire(final WireProtocolConfig wire) { + this.wire = wire; + return this; + } + + public RlpxConfiguration setMaxPeers(final int peers) { + maxPeers = peers; + return this; + } + + public int getMaxPeers() { + return maxPeers; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(final String clientId) { + this.clientId = clientId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final RlpxConfiguration that = (RlpxConfiguration) o; + return bindPort == that.bindPort + && Objects.equals(bindHost, that.bindHost) + && Objects.equals(wire, that.wire); + } + + @Override + public int hashCode() { + return Objects.hash(bindHost, bindPort, wire); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("RlpxConfiguration{"); + sb.append("bindHost='").append(bindHost).append('\''); + sb.append(", bindPort=").append(bindPort); + sb.append(", wire=").append(wire); + sb.append('}'); + return sb.toString(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/SubProtocolConfiguration.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/SubProtocolConfiguration.java new file mode 100755 index 00000000000..75c6b78ea84 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/SubProtocolConfiguration.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.p2p.config; + +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.ArrayList; +import java.util.List; + +public class SubProtocolConfiguration { + + private final List subProtocols = new ArrayList<>(); + private final List protocolManagers = new ArrayList<>(); + + public SubProtocolConfiguration withSubProtocol( + final SubProtocol subProtocol, final ProtocolManager protocolManager) { + subProtocols.add(subProtocol); + protocolManagers.add(protocolManager); + return this; + } + + public List getSubProtocols() { + return subProtocols; + } + + public List getProtocolManagers() { + return protocolManagers; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/WireProtocolConfig.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/WireProtocolConfig.java new file mode 100755 index 00000000000..4f0452cfa4c --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/config/WireProtocolConfig.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.ethereum.p2p.config; + +import java.util.Objects; + +public class WireProtocolConfig { + private long keepAlivePeriodMs = 15000; + private long handshakeTimeoutMs = 5000; + private int maxFailedKeepAlives = 3; + + public long getKeepAlivePeriodMs() { + return keepAlivePeriodMs; + } + + public WireProtocolConfig setKeepAlivePeriodMs(final long keepAlivePeriodMs) { + this.keepAlivePeriodMs = keepAlivePeriodMs; + return this; + } + + public long getHandshakeTimeoutMs() { + return handshakeTimeoutMs; + } + + public WireProtocolConfig setHandshakeTimeoutMs(final long handshakeTimeoutMs) { + this.handshakeTimeoutMs = handshakeTimeoutMs; + return this; + } + + public int getMaxFailedKeepAlives() { + return maxFailedKeepAlives; + } + + public WireProtocolConfig setMaxFailedKeepAlives(final int maxFailedKeepAlives) { + this.maxFailedKeepAlives = maxFailedKeepAlives; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final WireProtocolConfig that = (WireProtocolConfig) o; + return keepAlivePeriodMs == that.keepAlivePeriodMs + && handshakeTimeoutMs == that.handshakeTimeoutMs + && maxFailedKeepAlives == that.maxFailedKeepAlives; + } + + @Override + public int hashCode() { + return Objects.hash(keepAlivePeriodMs, handshakeTimeoutMs, maxFailedKeepAlives); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("WireProtocolConfig{"); + sb.append("keepAlivePeriodMs=").append(keepAlivePeriodMs); + sb.append(", handshakeTimeoutMs=").append(handshakeTimeoutMs); + sb.append(", maxFailedKeepAlives=").append(maxFailedKeepAlives); + sb.append('}'); + return sb.toString(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/DiscoveryPeer.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/DiscoveryPeer.java new file mode 100755 index 00000000000..004e3496421 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/DiscoveryPeer.java @@ -0,0 +1,88 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerId; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.OptionalInt; + +/** + * Represents an Ethereum node that we interacting with through the discovery and wire protocols. + */ +public class DiscoveryPeer extends DefaultPeer { + private PeerDiscoveryStatus status = PeerDiscoveryStatus.KNOWN; + + // Timestamps. + private long firstDiscovered = 0; + private long lastContacted = 0; + private long lastSeen = 0; + + public DiscoveryPeer( + final BytesValue id, final String host, final int udpPort, final int tcpPort) { + super(id, host, udpPort, tcpPort); + } + + public DiscoveryPeer( + final BytesValue id, final String host, final int udpPort, final OptionalInt tcpPort) { + super(id, host, udpPort, tcpPort); + } + + public DiscoveryPeer(final BytesValue id, final String host, final int udpPort) { + super(id, host, udpPort); + } + + public DiscoveryPeer(final BytesValue id, final Endpoint endpoint) { + super(id, endpoint); + } + + public DiscoveryPeer(final Peer peer) { + super(peer.getId(), peer.getEndpoint()); + } + + public PeerDiscoveryStatus getStatus() { + return status; + } + + public void setStatus(final PeerDiscoveryStatus status) { + this.status = status; + } + + public long getFirstDiscovered() { + return firstDiscovered; + } + + public PeerId setFirstDiscovered(final long firstDiscovered) { + this.firstDiscovered = firstDiscovered; + return this; + } + + public long getLastContacted() { + return lastContacted; + } + + public void setLastContacted(final long lastContacted) { + this.lastContacted = lastContacted; + } + + public long getLastSeen() { + return lastSeen; + } + + public void setLastSeen(final long lastSeen) { + this.lastSeen = lastSeen; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("DiscoveryPeer{"); + sb.append("status=").append(status); + sb.append(", endPoint=").append(this.getEndpoint()); + sb.append(", firstDiscovered=").append(firstDiscovered); + sb.append(", lastContacted=").append(lastContacted); + sb.append(", lastSeen=").append(lastSeen); + sb.append('}'); + return sb.toString(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgent.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgent.java new file mode 100755 index 00000000000..0950f36639b --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgent.java @@ -0,0 +1,403 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static net.consensys.pantheon.util.Preconditions.checkGuard; +import static net.consensys.pantheon.util.bytes.BytesValue.wrapBuffer; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.DisconnectCallback; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerBondedEvent; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerDroppedEvent; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketType; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryController; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerRequirement; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerTable; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeerId; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; +import net.consensys.pantheon.util.NetworkUtility; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.net.BindException; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.OptionalInt; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.net.InetAddresses; +import io.vertx.core.Vertx; +import io.vertx.core.datagram.DatagramPacket; +import io.vertx.core.datagram.DatagramSocket; +import io.vertx.core.datagram.DatagramSocketOptions; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * The peer discovery agent is the network component that sends and receives messages peer discovery + * messages via UDP. It exposes methods for the {@link PeerDiscoveryController} to dispatch outbound + * messages too. + * + *

How do the peer table and the discovery agent interact with one another?

+ * + *
    + *
  • The agent acts like the transport layer, receiving messages from the wire and exposing + * methods for the peer table to send packets too. + *
  • The table stores and indexes peers in a Kademlia k-bucket table with 256 bins (where bin 0 + * is not used as it's us, i.e. distance 0 == us). It reacts to messages based on its internal + * state. It uses the agent whenever it needs to dispatch a message. + *
+ * + *

The flow

+ * + *
    + *
  1. The discovery agent dispatches all incoming messages that were properly decoded and whose + * hash integrity check passes to the peer table. + *
  2. The peer table decides whether to store the Peer, change its state, send other messages, + * etc. based on its internal state. + *
  3. The agent attaches a callback to the call to the Peer Table. When the Peer Table has + * processed the message, it'll perform a callback passing in an Optional which is populated + * if we recognised the Peer, and empty if we did not. + *
  4. The agent reacts to specific messages (PING->PONG, FIND_NEIGHBORS->NEIGHBORS), if the + * Peer was recognised. Why doesn't the table send these messages itself? Because they don't + * affect the state machine of the Peer, and the table is only concerned with storing peers, + * keeping them alive and tracking their state. It is not bothered to service requests. + *
+ */ +public class PeerDiscoveryAgent implements DisconnectCallback { + private static final Logger LOG = LogManager.getLogger(PeerDiscoveryAgent.class); + + // The devp2p specification says only accept packets up to 1280, but some + // clients ignore that, so we add in a little extra padding. + private static final int MAX_PACKET_SIZE_BYTES = 1600; + private static final long PEER_REFRESH_INTERVAL_MS = MILLISECONDS.convert(30, TimeUnit.MINUTES); + private final Vertx vertx; + /* The peer controller, which takes care of the state machine of peers. */ + private final PeerDiscoveryController controller; + /* The keypair used to sign messages. */ + private final SECP256K1.KeyPair keyPair; + private final PeerTable peerTable; + private final DiscoveryConfiguration config; + + /* This is the {@link net.consensys.pantheon.ethereum.p2p.Peer} object holding who we are. */ + private DiscoveryPeer advertisedPeer; + /* The vert.x UDP socket. */ + private DatagramSocket socket; + + /* Is discovery enabled? */ + private boolean isActive = false; + + public PeerDiscoveryAgent( + final Vertx vertx, + final SECP256K1.KeyPair keyPair, + final DiscoveryConfiguration config, + final PeerRequirement peerRequirement, + final PeerBlacklist peerBlacklist) { + checkArgument(vertx != null, "vertx instance cannot be null"); + checkArgument(keyPair != null, "keypair cannot be null"); + checkArgument(config != null, "provided configuration cannot be null"); + + validateConfiguration(config); + + final List bootstrapPeers = + config.getBootstrapPeers().stream().map(DiscoveryPeer::new).collect(Collectors.toList()); + + this.vertx = vertx; + this.config = config; + this.keyPair = keyPair; + this.peerTable = new PeerTable(keyPair.getPublicKey().getEncodedBytes(), 16); + this.controller = + new PeerDiscoveryController( + vertx, + this, + peerTable, + bootstrapPeers, + PEER_REFRESH_INTERVAL_MS, + peerRequirement, + peerBlacklist); + } + + public CompletableFuture start(final int tcpPort) { + final CompletableFuture completion = new CompletableFuture<>(); + if (config.isActive()) { + final String host = config.getBindHost(); + final int port = config.getBindPort(); + LOG.info("Starting peer discovery agent on host={}, port={}", host, port); + + vertx + .createDatagramSocket(new DatagramSocketOptions()) + .listen( + port, + host, + res -> { + if (res.failed()) { + Throwable cause = res.cause(); + LOG.error("An exception occurred when starting the peer discovery agent", cause); + + if (cause instanceof BindException || cause instanceof SocketException) { + cause = + new PeerDiscoveryServiceException( + String.format( + "Failed to bind Ethereum UDP discovery listener to %s:%d: %s", + host, port, cause.getMessage())); + } + completion.completeExceptionally(cause); + return; + } + initialize(res.result(), tcpPort); + this.isActive = true; + completion.complete(null); + }); + } else { + this.isActive = false; + completion.complete(null); + } + return completion; + } + + public CompletableFuture stop() { + if (socket == null) { + return CompletableFuture.completedFuture(null); + } + + final CompletableFuture completion = new CompletableFuture<>(); + socket.close( + ar -> { + if (ar.succeeded()) { + controller.stop(); + socket = null; + completion.complete(null); + } else { + completion.completeExceptionally(ar.cause()); + } + }); + return completion; + } + + private void initialize(final DatagramSocket socket, final int tcpPort) { + this.socket = socket; + + // TODO: when using wildcard hosts (0.0.0.0), we need to handle multiple addresses by selecting + // the + // correct 'announce' address. + final BytesValue id = keyPair.getPublicKey().getEncodedBytes(); + final String effectiveHost = socket.localAddress().host(); + final int effectivePort = socket.localAddress().port(); + advertisedPeer = new DiscoveryPeer(id, config.getAdvertisedHost(), effectivePort, tcpPort); + + LOG.info( + "Started peer discovery agent successfully, on effective host={} and port={}", + effectiveHost, + effectivePort); + + socket.exceptionHandler(this::handleException); + socket.handler(this::handlePacket); + controller.start(); + } + + /** + * This is the exception handler for uncontrolled exceptions ocurring in the packet handlers. + * + * @param throwable the exception that was raised + */ + private void handleException(final Throwable throwable) { + System.out.println(throwable); + } + + /** + * The UDP packet handler. This is the entrypoint for all received datagrams. + * + * @param datagram the received datagram. + */ + private void handlePacket(final DatagramPacket datagram) { + try { + final int length = datagram.data().length(); + checkGuard( + length <= MAX_PACKET_SIZE_BYTES, + PeerDiscoveryPacketDecodingException::new, + "Packet too large. Actual size (bytes): %s", + length); + + // We allow exceptions to bubble up, as they'll be picked up by the exception handler. + final Packet packet = Packet.decode(datagram.data()); + + OptionalInt fromPort = OptionalInt.empty(); + if (packet.getPacketData(PingPacketData.class).isPresent()) { + final PingPacketData ping = packet.getPacketData(PingPacketData.class).orElseGet(null); + if (ping != null && ping.getFrom() != null && ping.getFrom().getTcpPort().isPresent()) { + fromPort = ping.getFrom().getTcpPort(); + } + } + + // Acquire the senders coordinates to build a Peer representation from them. + final String addr = datagram.sender().host(); + final int port = datagram.sender().port(); + + // Notify the peer controller. + final DiscoveryPeer peer = new DiscoveryPeer(packet.getNodeId(), addr, port, fromPort); + controller.onMessage(packet, peer); + } catch (final PeerDiscoveryPacketDecodingException e) { + LOG.debug("Discarding invalid peer discovery packet", e); + } catch (final Throwable t) { + LOG.error("Encountered error while handling packet", t); + } + } + + /** + * Allows package-private components to dispatch messages to peers. It updates the lastContacted + * timestamp of the {@link DiscoveryPeer}. This method wraps the data in a Packet, calculates its + * hash and signs it with our private key. + * + * @param peer the recipient + * @param type the type of message + * @param data the data packet to send + * @return the sent packet + */ + public Packet sendPacket(final DiscoveryPeer peer, final PacketType type, final PacketData data) { + final Packet packet = Packet.create(type, data, keyPair); + LOG.debug( + ">>> Sending {} discovery packet to {} ({}): {}", + type, + peer.getEndpoint(), + peer.getId().slice(0, 16), + packet); + + // Update the lastContacted timestamp on the peer if the dispatch succeeds. + socket.send( + packet.encode(), + peer.getEndpoint().getUdpPort(), + peer.getEndpoint().getHost(), + ar -> { + if (ar.failed()) { + LOG.warn( + "Sending to peer {} failed, packet: {}", + peer, + wrapBuffer(packet.encode()), + ar.cause()); + return; + } + if (ar.succeeded()) { + peer.setLastContacted(System.currentTimeMillis()); + } + }); + + return packet; + } + + public Collection getPeers() { + return Collections.unmodifiableCollection(controller.getPeers()); + } + + public DiscoveryPeer getAdvertisedPeer() { + return advertisedPeer; + } + + public InetSocketAddress localAddress() { + checkState(socket != null, "uninitialized discovery agent"); + return new InetSocketAddress(socket.localAddress().host(), socket.localAddress().port()); + } + + /** + * Adds an observer that will get called when a new peer is bonded with and added to the peer + * table. + * + *

No guarantees are made about the order in which observers are invoked. + * + * @param observer The observer to call. + * @return A unique ID identifying this observer, to that it can be removed later. + */ + public long observePeerBondedEvents(final Consumer observer) { + return controller.observePeerBondedEvents(observer); + } + + /** + * Adds an observer that will get called when a new peer is dropped from the peer table. + * + *

No guarantees are made about the order in which observers are invoked. + * + * @param observer The observer to call. + * @return A unique ID identifying this observer, to that it can be removed later. + */ + public long observePeerDroppedEvents(final Consumer observer) { + return controller.observePeerDroppedEvents(observer); + } + + /** + * Removes an previously added peer bonded observer. + * + * @param observerId The unique ID identifying the observer to remove. + * @return Whether the observer was located and removed. + */ + public boolean removePeerBondedObserver(final long observerId) { + return controller.removePeerBondedObserver(observerId); + } + + /** + * Removes an previously added peer dropped observer. + * + * @param observerId The unique ID identifying the observer to remove. + * @return Whether the observer was located and removed. + */ + public boolean removePeerDroppedObserver(final long observerId) { + return controller.removePeerDroppedObserver(observerId); + } + + /** + * Returns the count of observers that are registered on this controller. + * + * @return The observer count. + */ + @VisibleForTesting + public int getObserverCount() { + return controller.observerCount(); + } + + private static void validateConfiguration(final DiscoveryConfiguration config) { + checkArgument( + config.getBindHost() != null && InetAddresses.isInetAddress(config.getBindHost()), + "valid bind host required"); + checkArgument( + config.getAdvertisedHost() != null + && InetAddresses.isInetAddress(config.getAdvertisedHost()), + "valid advertisement host required"); + checkArgument( + config.getBindPort() == 0 || NetworkUtility.isValidPort(config.getBindPort()), + "valid port number required"); + checkArgument(config.getBootstrapPeers() != null, "bootstrapPeers cannot be null"); + checkArgument(config.getBucketSize() > 0, "bucket size cannot be negative nor zero"); + } + + @Override + public void onDisconnect( + final PeerConnection connection, + final DisconnectMessage.DisconnectReason reason, + final boolean initiatedByPeer) { + final BytesValue nodeId = connection.getPeer().getNodeId(); + peerTable.evict(new DefaultPeerId(nodeId)); + } + + /** + * Returns the current state of the PeerDiscoveryAgent. + * + *

If true, the node is actively listening for new connections. If false, discovery has been + * turned off and the node is not listening for connections. + * + * @return true, if the {@link PeerDiscoveryAgent} is active on this node, false, otherwise. + */ + public boolean isActive() { + return isActive; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryEvent.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryEvent.java new file mode 100755 index 00000000000..9d188c9489e --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryEvent.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import com.google.common.base.MoreObjects; + +/** An abstract event emitted from the peer discovery layer. */ +public abstract class PeerDiscoveryEvent { + private final DiscoveryPeer peer; + private final long timestamp; + + private PeerDiscoveryEvent(final DiscoveryPeer peer, final long timestamp) { + this.peer = peer; + this.timestamp = timestamp; + } + + public DiscoveryPeer getPeer() { + return peer; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("peer", peer) + .add("timestamp", timestamp) + .toString(); + } + + /** + * An event that is dispatched whenever we bond with a new peer. See Javadoc on + * PeerDiscoveryController to understand when this happens. + * + *

{@link net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryController} + */ + public static class PeerBondedEvent extends PeerDiscoveryEvent { + public PeerBondedEvent(final DiscoveryPeer peer, final long timestamp) { + super(peer, timestamp); + } + } + + /** + * An event that is dispatched whenever we drop a peer from the peer table. See Javadoc on + * PeerDiscoveryController to understand when this happens. + * + *

{@link net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryController} + */ + public static class PeerDroppedEvent extends PeerDiscoveryEvent { + public PeerDroppedEvent(final DiscoveryPeer peer, final long timestamp) { + super(peer, timestamp); + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketDecodingException.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketDecodingException.java new file mode 100755 index 00000000000..bc5f7a56505 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketDecodingException.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +/** Signals that an error occurred while deserializing a discovery packet from the wire. */ +public class PeerDiscoveryPacketDecodingException extends RuntimeException { + public PeerDiscoveryPacketDecodingException(final String message) { + super(message); + } + + public PeerDiscoveryPacketDecodingException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryServiceException.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryServiceException.java new file mode 100755 index 00000000000..07d7eedba54 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryServiceException.java @@ -0,0 +1,9 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +/** Thrown to indicate that an error occurred during the operation of the P2P Discovery Service. */ +public class PeerDiscoveryServiceException extends RuntimeException { + + public PeerDiscoveryServiceException(final String message) { + super(message); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryStatus.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryStatus.java new file mode 100755 index 00000000000..c7670476a9d --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryStatus.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +/** The status of a {@link DiscoveryPeer}, in relation to the peer discovery state machine. */ +public enum PeerDiscoveryStatus { + + /** + * Represents a newly discovered {@link DiscoveryPeer}, prior to commencing the bonding exchange. + */ + KNOWN, + + /** + * Bonding with this peer is in progress. If we're unable to establish communication and/or + * complete the bonding exchange, the {@link DiscoveryPeer} remains in this state, until we + * ultimately desist. + */ + BONDING, + + /** + * We have successfully bonded with this {@link DiscoveryPeer}, and we are able to exchange + * messages with them. + */ + BONDED; + + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Bucket.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Bucket.java new file mode 100755 index 00000000000..926bcbc4df2 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Bucket.java @@ -0,0 +1,133 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static java.lang.System.arraycopy; +import static java.util.Arrays.asList; +import static java.util.Arrays.copyOf; +import static java.util.Collections.unmodifiableList; + +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerId; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +/** + * As peers are discovered on the network, they are added to one of the k-buckets described by this + * class. All peers encountered will be subject for inclusion in this data structure. + * + *

This implementation is driven by an array sorted by access time, where the head is the most + * recently accessed peer and the tail is the least recently accessed peer. If the bucket + * is full, the least recently accessed peer is proposed for eviction, thus aiming to keep + * the bucket filled with alive, responsive peers. + */ +public class Bucket { + private final DiscoveryPeer[] kBucket; + private final int bucketSize; + private int tailIndex = -1; + + /** + * Creates a new bucket with the provided maximum size. + * + * @param bucketSize every k-bucket maintains a constituent list having up to bucketSize entries, + * default is 16. + */ + Bucket(final int bucketSize) { + this.bucketSize = bucketSize; + this.kBucket = new DiscoveryPeer[bucketSize]; + } + + /** + * Returns the peer with the provided ID if it exists in the bucket. + * + *

This operation presupposes that the system has been in recent contact with this peer, hence + * it relocates it to to the head of the list. + * + * @param id The peer's ID (public key). + * @return An empty optional if the peer was not a member of this bucket, or a filled optional if + * it was. + */ + synchronized Optional getAndTouch(final BytesValue id) { + for (int i = 0; i <= tailIndex; i++) { + final DiscoveryPeer p = kBucket[i]; + if (id.equals(p.getId())) { + arraycopy(kBucket, 0, kBucket, 1, i); + kBucket[0] = p; + return Optional.of(p); + } + } + return Optional.empty(); + } + + /** + * Appends the specified element to the head of the bucket array if capacity hasn't yet been + * reached. Shifts the element currently at that position (if any) and any subsequent elements to + * the right (adds one to their indices). If the bucket is empty, the last argument (length to + * copy) will be 0. This method will not "touch" the peer, i.e. relocate it to the head. + * + *

In the case that the bucket is at maximum capacity the peer at the tail of the list, + * necessarily the peer that has been incomunicative for the longest time is returned as a + * potential eviction candidate. + * + * @param peer element to be appended to this list + * @return an empty optional or alternatively the least recently contacted peer (tail of array) + * @throws IllegalArgumentException The peer already existed in the bucket. + */ + synchronized Optional add(final DiscoveryPeer peer) + throws IllegalArgumentException { + assert tailIndex >= -1 && tailIndex < bucketSize; + + // Avoid duplicating the peer if it already exists in the bucket. + for (int i = 0; i <= tailIndex; i++) { + if (peer.equals(kBucket[i])) { + throw new IllegalArgumentException( + String.format("Tried to add duplicate peer to k-bucket: %s", peer.getId())); + } + } + if (tailIndex == bucketSize - 1) { + return Optional.of(kBucket[tailIndex]); + } + arraycopy(kBucket, 0, kBucket, 1, ++tailIndex); + kBucket[0] = peer; + return Optional.empty(); + } + + /** + * Removes the element at the specified position in this list. Shifts any subsequent elements to + * the left (subtracts one from their indices). + * + * @param peer the element to be removed + * @return true + */ + synchronized boolean evict(final PeerId peer) { + // If the bucket is empty, there's nothing to evict. + if (tailIndex < 0) { + return false; + } + // If found, shift all subsequent elements to the left, and decrement tailIndex. + for (int i = 0; i <= tailIndex; i++) { + if (peer.equals(kBucket[i])) { + arraycopy(kBucket, i + 1, kBucket, i, tailIndex - i); + kBucket[tailIndex--] = null; + return true; + } + } + return false; + } + + /** + * Returns an immutable list backed by the k-bucket array. This method provides a convenient way + * to access all peers maintained by the instance of Bucket under consideration. + * + * @return immutable view of the peer array + */ + synchronized List peers() { + return unmodifiableList(asList(copyOf(kBucket, tailIndex + 1))); + } + + @Override + public String toString() { + return Arrays.toString(kBucket); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/FindNeighborsPacketData.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/FindNeighborsPacketData.java new file mode 100755 index 00000000000..ff687fcd51d --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/FindNeighborsPacketData.java @@ -0,0 +1,59 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class FindNeighborsPacketData implements PacketData { + private static final int TARGET_SIZE = 64; + + /* Node ID. */ + private final BytesValue target; + + /* In millis after epoch. */ + private final long expiration; + + private FindNeighborsPacketData(final BytesValue target, final long expiration) { + checkArgument(target != null && target.size() == TARGET_SIZE, "target must be a valid node id"); + checkArgument(expiration >= 0, "expiration must be positive"); + + this.target = target; + this.expiration = expiration; + } + + public static FindNeighborsPacketData create(final BytesValue target) { + return new FindNeighborsPacketData( + target, System.currentTimeMillis() + PacketData.DEFAULT_EXPIRATION_PERIOD_MS); + } + + @Override + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeBytesValue(target); + out.writeLongScalar(expiration); + out.endList(); + } + + public static FindNeighborsPacketData readFrom(final RLPInput in) { + in.enterList(); + final BytesValue target = in.readBytesValue(); + final long expiration = in.readLongScalar(); + in.leaveList(); + return new FindNeighborsPacketData(target, expiration); + } + + public long getExpiration() { + return expiration; + } + + public BytesValue getTarget() { + return target; + } + + @Override + public String toString() { + return "FindNeighborsPacketData{" + "expiration=" + expiration + ", target=" + target + '}'; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/NeighborsPacketData.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/NeighborsPacketData.java new file mode 100755 index 00000000000..c3b6f42cd3a --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/NeighborsPacketData.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import java.util.List; + +public class NeighborsPacketData implements PacketData { + + private final List peers; + + /* In millis after epoch. */ + private final long expiration; + + private NeighborsPacketData(final List peers, final long expiration) { + checkArgument(peers != null, "peer list cannot be null"); + checkArgument(expiration >= 0, "expiration must be positive"); + + this.peers = peers; + this.expiration = expiration; + } + + @SuppressWarnings("unchecked") + public static NeighborsPacketData create(final List peers) { + return new NeighborsPacketData( + peers, System.currentTimeMillis() + PacketData.DEFAULT_EXPIRATION_PERIOD_MS); + } + + public static NeighborsPacketData readFrom(final RLPInput in) { + in.enterList(); + final List peers = + in.readList(rlp -> new DiscoveryPeer(DefaultPeer.readFrom(rlp))); + final long expiration = in.readLongScalar(); + in.leaveList(); + return new NeighborsPacketData(peers, expiration); + } + + @Override + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeList(peers, Peer::writeTo); + out.writeLongScalar(expiration); + out.endList(); + } + + public List getNodes() { + return peers; + } + + public long getExpiration() { + return expiration; + } + + @Override + public String toString() { + return String.format("NeighborsPacketData{peers=%s, expiration=%d}", peers, expiration); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Packet.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Packet.java new file mode 100755 index 00000000000..a56dcbd28b8 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/Packet.java @@ -0,0 +1,178 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.crypto.Hash.keccak256; +import static net.consensys.pantheon.util.Preconditions.checkGuard; +import static net.consensys.pantheon.util.bytes.BytesValues.asUnsignedBigInteger; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.PublicKey; +import net.consensys.pantheon.crypto.SECP256K1.Signature; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryPacketDecodingException; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.ethereum.rlp.VertxBufferRLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; +import net.consensys.pantheon.util.uint.UInt256Bytes; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Optional; + +import io.vertx.core.buffer.Buffer; + +public class Packet { + + private static final int PACKET_TYPE_INDEX = 97; + private static final int PACKET_DATA_INDEX = 98; + private static final int SIGNATURE_INDEX = 32; + + private final PacketType type; + private final PacketData data; + + private final BytesValue hash; + private final Signature signature; + private final PublicKey publicKey; + + private Packet(final PacketType type, final PacketData data, final SECP256K1.KeyPair keyPair) { + this.type = type; + this.data = data; + + final BytesValue typeBytes = BytesValue.of(this.type.getValue()); + final BytesValue dataBytes = RLP.encode(this.data::writeTo); + + this.signature = SECP256K1.sign(keccak256(BytesValue.wrap(typeBytes, dataBytes)), keyPair); + this.hash = + keccak256( + BytesValue.wrap(BytesValue.wrap(encodeSignature(signature), typeBytes), dataBytes)); + this.publicKey = keyPair.getPublicKey(); + } + + private Packet( + final PacketType packetType, final PacketData packetData, final BytesValue message) { + final BytesValue hash = message.slice(0, SIGNATURE_INDEX); + final BytesValue encodedSignature = + message.slice(SIGNATURE_INDEX, PACKET_TYPE_INDEX - SIGNATURE_INDEX); + final BytesValue signedPayload = + message.slice(PACKET_TYPE_INDEX, message.size() - PACKET_TYPE_INDEX); + + // Perform hash integrity check. + final BytesValue rest = message.slice(SIGNATURE_INDEX, message.size() - SIGNATURE_INDEX); + if (!Arrays.equals(keccak256(rest).extractArray(), hash.extractArray())) { + throw new PeerDiscoveryPacketDecodingException( + "Integrity check failed: non-matching hashes."); + } + + this.type = packetType; + this.data = packetData; + this.hash = hash; + this.signature = decodeSignature(encodedSignature); + this.publicKey = + PublicKey.recoverFromSignature(keccak256(signedPayload), this.signature) + .orElseThrow( + () -> + new PeerDiscoveryPacketDecodingException( + "Invalid packet signature, " + "cannot recover public key")); + } + + public static Packet create( + final PacketType packetType, final PacketData packetData, final SECP256K1.KeyPair keyPair) { + return new Packet(packetType, packetData, keyPair); + } + + public static Packet decode(final Buffer message) { + checkGuard( + message.length() >= PACKET_DATA_INDEX, + PeerDiscoveryPacketDecodingException::new, + "Packet too short: expected at least %s bytes, got %s", + PACKET_DATA_INDEX, + message.length()); + + final byte type = message.getByte(PACKET_TYPE_INDEX); + + final PacketType packetType = + PacketType.forByte(type) + .orElseThrow( + () -> + new PeerDiscoveryPacketDecodingException("Unrecognized packet type: " + type)); + + final PacketType.Deserializer deserializer = packetType.getDeserializer(); + PacketData packetData; + try { + packetData = deserializer.deserialize(RLP.input(message, PACKET_DATA_INDEX)); + } catch (final RLPException e) { + throw new PeerDiscoveryPacketDecodingException("Malformed packet of type: " + packetType, e); + } + + return new Packet(packetType, packetData, BytesValue.wrapBuffer(message)); + } + + public Buffer encode() { + final BytesValue encodedSignature = encodeSignature(signature); + final VertxBufferRLPOutput encodedData = new VertxBufferRLPOutput(); + data.writeTo(encodedData); + + final Buffer buffer = + Buffer.buffer(hash.size() + encodedSignature.size() + 1 + encodedData.encodedSize()); + hash.appendTo(buffer); + encodedSignature.appendTo(buffer); + buffer.appendByte(type.getValue()); + encodedData.appendEncoded(buffer); + return buffer; + } + + @SuppressWarnings("unchecked") + public Optional getPacketData(final Class expectedPacketType) { + if (data == null || !data.getClass().equals(expectedPacketType)) { + return Optional.empty(); + } + return Optional.of((T) data); + } + + public BytesValue getNodeId() { + return publicKey.getEncodedBytes(); + } + + public PacketType getType() { + return type; + } + + public BytesValue getHash() { + return hash; + } + + @Override + public String toString() { + return "Packet{" + + "type=" + + type + + ", data=" + + data + + ", hash=" + + hash + + ", signature=" + + signature + + ", publicKey=" + + publicKey + + '}'; + } + + private static BytesValue encodeSignature(final SECP256K1.Signature signature) { + final MutableBytesValue encoded = MutableBytesValue.create(65); + UInt256Bytes.of(signature.getR()).copyTo(encoded, 0); + UInt256Bytes.of(signature.getS()).copyTo(encoded, 32); + final int v = signature.getRecId(); + encoded.set(64, (byte) v); + return encoded; + } + + private static SECP256K1.Signature decodeSignature(final BytesValue encodedSignature) { + checkArgument( + encodedSignature != null && encodedSignature.size() == 65, "encodedSignature is 65 bytes"); + final BigInteger r = asUnsignedBigInteger(encodedSignature.slice(0, 32)); + final BigInteger s = asUnsignedBigInteger(encodedSignature.slice(32, 32)); + final int recId = encodedSignature.get(64); + return SECP256K1.Signature.create(r, s, (byte) recId); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketData.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketData.java new file mode 100755 index 00000000000..23d21696573 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketData.java @@ -0,0 +1,19 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +public interface PacketData { + + /** + * Expiration is not standardised. We use Geth's expiration period (60 seconds); whereas Parity's + * is 20 seconds. + */ + long DEFAULT_EXPIRATION_PERIOD_MS = 60000; + + /** + * Serializes the implementing packet data onto the provided RLP output buffer. + * + * @param out The RLP output buffer. + */ + void writeTo(RLPOutput out); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketType.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketType.java new file mode 100755 index 00000000000..35d33cadf28 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketType.java @@ -0,0 +1,52 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; + +import java.util.Arrays; +import java.util.Optional; +import javax.annotation.concurrent.Immutable; + +public enum PacketType { + PING(0x01, PingPacketData::readFrom), + PONG(0x02, PongPacketData::readFrom), + FIND_NEIGHBORS(0x03, FindNeighborsPacketData::readFrom), + NEIGHBORS(0x04, NeighborsPacketData::readFrom); + + private static final int MAX_VALUE = 0x7F; + private static final int BYTE_MASK = 0xFF; + + private static final PacketType[] INDEX = new PacketType[PacketType.MAX_VALUE]; + + static { + Arrays.stream(values()).forEach(type -> INDEX[type.value] = type); + } + + private final byte value; + private final Deserializer deserializer; + + public static Optional forByte(final byte b) { + return b > MAX_VALUE ? Optional.empty() : Optional.ofNullable(INDEX[b]); + } + + PacketType(final int value, final Deserializer deserializer) { + checkArgument(value <= MAX_VALUE, "Packet type ID must be in range [0x00, 0x80)"); + this.deserializer = deserializer; + this.value = (byte) (value & BYTE_MASK); + } + + public byte getValue() { + return value; + } + + public Deserializer getDeserializer() { + return deserializer; + } + + @FunctionalInterface + @Immutable + public interface Deserializer { + T deserialize(RLPInput in); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java new file mode 100755 index 00000000000..1914d902cc2 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryController.java @@ -0,0 +1,547 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Collections.emptyList; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerTable.AddResult.Outcome; + +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryAgent; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerBondedEvent; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerDroppedEvent; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.util.Subscribers; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import com.google.common.annotations.VisibleForTesting; +import io.vertx.core.Vertx; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This component is the entrypoint for managing the lifecycle of peers. + * + *

It keeps track of the interactions with each peer, including the expectations of what we + * expect to receive next from each peer. In other words, it implements the state machine for + * (discovery) peers. + * + *

When necessary, it updates the underlying {@link PeerTable}, particularly with additions which + * may succeed or not depending on the contents of the target bucket for the peer. + * + *

Peer state machine

+ * + *
{@code
+ *                                                                +--------------------+
+ *                                                                |                    |
+ *                                                    +-----------+  MESSAGE_EXPECTED  +-----------+
+ *                                                    |           |                    |           |
+ *                                                    |           +---+----------------+           |
+ * +------------+         +-----------+         +-----+----+          |                      +-----v-----+
+ * |            |         |           |         |          <----------+                      |           |
+ * |  KNOWN  +--------->  BONDING  +--------->  BONDED  |                                 |  DROPPED  |
+ * |            |         |           |         |          ^                                 |           |
+ * +------------+         +-----------+         +----------+                                 +-----------+
+ *
+ * }
+ * + *
    + *
  • KNOWN: the peer is known but there is no ongoing interaction with it. + *
  • BONDING: an attempt to bond is being made (e.g. a PING has been sent). + *
  • BONDED: the bonding handshake has taken place (e.g. an expected PONG has been + * received after having sent a PING or a PING has been received and a PONG has been sent in + * response). This is the same as having an "active" channel. + *
  • MESSAGE_EXPECTED (*): a message has been sent and a response is expected. + *
  • DROPPED (*): the peer is no longer in our peer table. + *
+ * + *

(*) It is worthy to note that the MESSAGE_EXPECTED and DROPPED states are + * not modelled explicitly in {@link PeerDiscoveryStatus}, but they have been included in the + * diagram for clarity. These two states define the elimination path for a peer from the underlying + * table. + * + *

If an expectation to receive a message was unmet, following the evaluation of a failure + * condition, the peer will be physically dropped (eliminated) from the table. + */ +public class PeerDiscoveryController { + + private static final Logger LOG = LogManager.getLogger(PeerDiscoveryController.class); + private static final long REFRESH_CHECK_INTERVAL_MILLIS = MILLISECONDS.convert(30, SECONDS); + private final Vertx vertx; + private final PeerTable peerTable; + + private final Collection bootstrapNodes; + + /* A tracker for inflight interactions and the state machine of a peer. */ + private final Map inflightInteractions = + new ConcurrentHashMap<>(); + + private final AtomicBoolean started = new AtomicBoolean(false); + + private final PeerDiscoveryAgent agent; + private final PeerBlacklist peerBlacklist; + + private RetryDelayFunction retryDelayFunction = RetryDelayFunction.linear(1.5, 2000, 60000); + + private final long tableRefreshIntervalMs; + + private final PeerRequirement peerRequirement; + + private long lastRefreshTime = -1; + + private OptionalLong tableRefreshTimerId = OptionalLong.empty(); + + // Observers for "peer bonded" discovery events. + private final Subscribers> peerBondedObservers = new Subscribers<>(); + + // Observers for "peer dropped" discovery events. + private final Subscribers> peerDroppedObservers = new Subscribers<>(); + + public PeerDiscoveryController( + final Vertx vertx, + final PeerDiscoveryAgent agent, + final PeerTable peerTable, + final Collection bootstrapNodes, + final long tableRefreshIntervalMs, + final PeerRequirement peerRequirement, + final PeerBlacklist peerBlacklist) { + this.vertx = vertx; + this.agent = agent; + this.bootstrapNodes = bootstrapNodes; + this.peerTable = peerTable; + this.tableRefreshIntervalMs = tableRefreshIntervalMs; + this.peerRequirement = peerRequirement; + this.peerBlacklist = peerBlacklist; + } + + public CompletableFuture start() { + if (!started.compareAndSet(false, true)) { + throw new IllegalStateException("The peer table had already been started"); + } + + bootstrapNodes + .stream() + .filter(node -> peerTable.tryAdd(node).getOutcome() == Outcome.ADDED) + .forEach(node -> bond(node, true)); + + final long timerId = + vertx.setPeriodic( + Math.min(REFRESH_CHECK_INTERVAL_MILLIS, tableRefreshIntervalMs), + (l) -> refreshTableIfRequired()); + tableRefreshTimerId = OptionalLong.of(timerId); + + return CompletableFuture.completedFuture(null); + } + + public CompletableFuture stop() { + if (!started.compareAndSet(true, false)) { + return CompletableFuture.completedFuture(null); + } + + tableRefreshTimerId.ifPresent(vertx::cancelTimer); + tableRefreshTimerId = OptionalLong.empty(); + inflightInteractions.values().forEach(PeerInteractionState::cancelTimers); + inflightInteractions.clear(); + return CompletableFuture.completedFuture(null); + } + + /** + * Handles an incoming message and processes it based on the state machine for the {@link + * DiscoveryPeer}. + * + *

The callback will be called with the canonical representation of the sender Peer as stored + * in our table, or with an empty Optional if the message was out of band and we didn't process + * it. + * + * @param packet The incoming message. + * @param sender The sender. + */ + public void onMessage(final Packet packet, final DiscoveryPeer sender) { + LOG.debug( + "<<< Received {} discovery packet from {} ({}): {}", + packet.getType(), + sender.getEndpoint(), + sender.getId().slice(0, 16), + packet); + + // Message from self. This should not happen. + if (sender.getId().equals(agent.getAdvertisedPeer().getId())) { + return; + } + + // Load the peer from the table, or use the instance that comes in. + final Optional maybeKnownPeer = peerTable.get(sender); + final DiscoveryPeer peer = maybeKnownPeer.orElse(sender); + final boolean peerKnown = maybeKnownPeer.isPresent(); + final boolean peerBlacklisted = peerBlacklist.contains(peer); + + switch (packet.getType()) { + case PING: + if (!peerBlacklisted && addToPeerTable(peer)) { + final PingPacketData ping = packet.getPacketData(PingPacketData.class).get(); + respondToPing(ping, packet.getHash(), peer); + } + + break; + case PONG: + { + matchInteraction(packet) + .ifPresent( + interaction -> { + if (peerBlacklisted) { + return; + } + addToPeerTable(peer); + + // If this was a bootstrap peer, let's ask it for nodes near to us. + if (interaction.isBootstrap()) { + findNodes(peer, agent.getAdvertisedPeer().getId()); + } + }); + break; + } + case NEIGHBORS: + matchInteraction(packet) + .ifPresent( + interaction -> { + // Extract the peers from the incoming packet. + final List neighbors = + packet + .getPacketData(NeighborsPacketData.class) + .map(NeighborsPacketData::getNodes) + .orElse(emptyList()); + + for (final DiscoveryPeer neighbor : neighbors) { + if (peerBlacklist.contains(neighbor) || peerTable.get(neighbor).isPresent()) { + continue; + } + bond(neighbor, false); + } + }); + break; + + case FIND_NEIGHBORS: + if (!peerKnown || peerBlacklisted) { + break; + } + final FindNeighborsPacketData fn = + packet.getPacketData(FindNeighborsPacketData.class).get(); + respondToFindNeighbors(fn, peer); + break; + } + } + + private boolean addToPeerTable(final DiscoveryPeer peer) { + final PeerTable.AddResult result = peerTable.tryAdd(peer); + if (result.getOutcome() == Outcome.SELF) { + return false; + } + + // Reset the last seen timestamp. + final long now = System.currentTimeMillis(); + if (peer.getFirstDiscovered() == 0) { + peer.setFirstDiscovered(now); + } + peer.setLastSeen(now); + + if (peer.getStatus() != PeerDiscoveryStatus.BONDED) { + peer.setStatus(PeerDiscoveryStatus.BONDED); + notifyPeerBonded(peer, now); + } + + if (result.getOutcome() == Outcome.ALREADY_EXISTED) { + // Bump peer. + peerTable.evict(peer); + peerTable.tryAdd(peer); + } else if (result.getOutcome() == Outcome.BUCKET_FULL) { + peerTable.evict(result.getEvictionCandidate()); + peerTable.tryAdd(peer); + } + + return true; + } + + private void notifyPeerBonded(final DiscoveryPeer peer, final long now) { + final PeerBondedEvent event = new PeerBondedEvent(peer, now); + dispatchEvent(peerBondedObservers, event); + } + + private Optional matchInteraction(final Packet packet) { + final PeerInteractionState interaction = inflightInteractions.get(packet.getNodeId()); + if (interaction == null || !interaction.test(packet)) { + return Optional.empty(); + } + interaction.cancelTimers(); + inflightInteractions.remove(packet.getNodeId()); + return Optional.of(interaction); + } + + private void refreshTableIfRequired() { + final long now = System.currentTimeMillis(); + if (lastRefreshTime + tableRefreshIntervalMs < now) { + LOG.info("Peer table refresh triggered by timer expiry"); + refreshTable(); + } else if (!peerRequirement.hasSufficientPeers()) { + LOG.info("Peer table refresh triggered by insufficient peers"); + refreshTable(); + } + } + + /** + * Refreshes the peer table by generating a random ID and interrogating the closest nodes for it. + * Currently the refresh process is NOT recursive. + */ + private void refreshTable() { + final BytesValue target = Peer.randomId(); + peerTable.nearestPeers(Peer.randomId(), 16).forEach((peer) -> findNodes(peer, target)); + lastRefreshTime = System.currentTimeMillis(); + } + + /** + * Initiates a bonding PING-PONG cycle with a peer. + * + * @param peer The targeted peer. + * @param bootstrap Whether this is a bootstrap interaction. + */ + @VisibleForTesting + void bond(final DiscoveryPeer peer, final boolean bootstrap) { + peer.setFirstDiscovered(System.currentTimeMillis()); + peer.setStatus(PeerDiscoveryStatus.BONDING); + + final Consumer action = + interaction -> { + final PingPacketData data = + PingPacketData.create(agent.getAdvertisedPeer().getEndpoint(), peer.getEndpoint()); + final Packet sentPacket = agent.sendPacket(peer, PacketType.PING, data); + + final BytesValue pingHash = sentPacket.getHash(); + // Update the matching filter to only accept the PONG if it echoes the hash of our PING. + final Predicate newFilter = + packet -> + packet + .getPacketData(PongPacketData.class) + .map(pong -> pong.getPingHash().equals(pingHash)) + .orElse(false); + interaction.updateFilter(newFilter); + }; + + // The filter condition will be updated as soon as the action is performed. + final PeerInteractionState ping = + new PeerInteractionState(action, PacketType.PONG, (packet) -> false, true, bootstrap); + dispatchInteraction(peer, ping); + } + + /** + * Sends a FIND_NEIGHBORS message to a {@link DiscoveryPeer}, in search of a target value. + * + * @param peer the peer to interrogate + * @param target the target node ID to find + */ + private void findNodes(final DiscoveryPeer peer, final BytesValue target) { + final Consumer action = + (interaction) -> { + final FindNeighborsPacketData data = FindNeighborsPacketData.create(target); + agent.sendPacket(peer, PacketType.FIND_NEIGHBORS, data); + }; + final PeerInteractionState interaction = + new PeerInteractionState(action, PacketType.NEIGHBORS, packet -> true, true, false); + dispatchInteraction(peer, interaction); + } + + /** + * Dispatches a new tracked interaction with a peer, adding it to the {@link + * #inflightInteractions} map and executing the action for the first time. + * + *

If a previous inflightInteractions interaction existed, we cancel any associated timers. + * + * @param peer The peer. + * @param state The state. + */ + private void dispatchInteraction(final Peer peer, final PeerInteractionState state) { + final PeerInteractionState previous = inflightInteractions.put(peer.getId(), state); + if (previous != null) { + previous.cancelTimers(); + } + state.execute(0); + } + + private void respondToPing( + final PingPacketData packetData, final BytesValue pingHash, final DiscoveryPeer sender) { + final PongPacketData data = PongPacketData.create(packetData.getFrom(), pingHash); + agent.sendPacket(sender, PacketType.PONG, data); + } + + private void respondToFindNeighbors( + final FindNeighborsPacketData packetData, final DiscoveryPeer sender) { + // TODO: for now return 16 peers. Other implementations calculate how many + // peers they can fit in a 1280-byte payload. + final List peers = peerTable.nearestPeers(packetData.getTarget(), 16); + final PacketData data = NeighborsPacketData.create(peers); + agent.sendPacket(sender, PacketType.NEIGHBORS, data); + } + + // Dispatches an event to a set of observers. Since we have no control over observer logic, we + // take + // precautions and we assume they are of blocking nature to protect our event loop. + private void dispatchEvent( + final Subscribers> observers, final T event) { + observers.forEach( + observer -> + vertx.executeBlocking( + future -> { + observer.accept(event); + future.complete(); + }, + x -> {})); + } + + /** + * Returns a copy of the known peers. Modifications to the list will not update the table's state, + * but modifications to the Peers themselves will. + * + * @return List of peers. + */ + public Collection getPeers() { + return peerTable.getAllPeers(); + } + + public void setRetryDelayFunction(final RetryDelayFunction retryDelayFunction) { + this.retryDelayFunction = retryDelayFunction; + } + + /** + * Adds an observer that will get called when a new peer is bonded with and added to the peer + * table. + * + *

No guarantees are made about the order in which observers are invoked. + * + * @param observer The observer to call. + * @return A unique ID identifying this observer, to that it can be removed later. + */ + public long observePeerBondedEvents(final Consumer observer) { + checkNotNull(observer); + return peerBondedObservers.subscribe(observer); + } + + /** + * Adds an observer that will get called when a new peer is dropped from the peer table. + * + *

No guarantees are made about the order in which observers are invoked. + * + * @param observer The observer to call. + * @return A unique ID identifying this observer, to that it can be removed later. + */ + public long observePeerDroppedEvents(final Consumer observer) { + checkNotNull(observer); + return peerDroppedObservers.subscribe(observer); + } + + /** + * Removes an previously added peer bonded observer. + * + * @param observerId The unique ID identifying the observer to remove. + * @return Whether the observer was located and removed. + */ + public boolean removePeerBondedObserver(final long observerId) { + return peerBondedObservers.unsubscribe(observerId); + } + + /** + * Removes an previously added peer dropped observer. + * + * @param observerId The unique ID identifying the observer to remove. + * @return Whether the observer was located and removed. + */ + public boolean removePeerDroppedObserver(final long observerId) { + return peerDroppedObservers.unsubscribe(observerId); + } + + /** + * Returns the count of observers that are registered on this controller. + * + * @return The observer count. + */ + @VisibleForTesting + public int observerCount() { + return peerBondedObservers.getSubscriberCount() + peerDroppedObservers.getSubscriberCount(); + } + + /** Holds the state machine data for a peer interaction. */ + private class PeerInteractionState implements Predicate { + /** + * The action that led to the peer being in this state (e.g. sending a PING or NEIGHBORS + * message), in case it needs to be retried. + */ + private final Consumer action; + /** The expected type of the message that will transition the peer out of this state. */ + private final PacketType expectedType; + /** A custom filter to accept transitions out of this state. */ + private Predicate filter; + /** Whether the action associated to this state is retryable or not. */ + private final boolean retryable; + /** Whether this is an entry for a bootstrap peer. */ + private final boolean bootstrap; + /** Timers associated with this entry. */ + private OptionalLong timerId = OptionalLong.empty(); + + PeerInteractionState( + final Consumer action, + final PacketType expectedType, + final Predicate filter, + final boolean retryable, + final boolean bootstrap) { + this.action = action; + this.expectedType = expectedType; + this.filter = filter; + this.retryable = retryable; + this.bootstrap = bootstrap; + } + + @Override + public boolean test(final Packet packet) { + return expectedType == packet.getType() && (filter == null || filter.test(packet)); + } + + void updateFilter(final Predicate filter) { + this.filter = filter; + } + + boolean isBootstrap() { + return bootstrap; + } + + /** + * Executes the action associated with this state. Sets a "boomerang" timer to itself in case + * the action is retryable. + * + * @param lastTimeout the previous timeout, or 0 if this is the first time the action is being + * executed. + */ + void execute(final long lastTimeout) { + action.accept(this); + if (retryable) { + final long newTimeout = retryDelayFunction.apply(lastTimeout); + timerId = OptionalLong.of(vertx.setTimer(newTimeout, id -> execute(newTimeout))); + } + } + + /** Cancels any timers associated with this entry. */ + void cancelTimers() { + timerId.ifPresent(vertx::cancelTimer); + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerRequirement.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerRequirement.java new file mode 100755 index 00000000000..dceac5dc328 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerRequirement.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import java.util.Collection; + +public interface PeerRequirement { + + boolean hasSufficientPeers(); + + static PeerRequirement aggregateOf(final Collection peers) { + return () -> peers.stream().allMatch(PeerRequirement::hasSufficientPeers); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java new file mode 100755 index 00000000000..a8ee8e10026 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTable.java @@ -0,0 +1,276 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static java.util.Collections.unmodifiableList; +import static java.util.Comparator.comparingInt; +import static java.util.stream.Collectors.toList; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerId; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.stream.Stream; + +import com.google.common.hash.BloomFilter; + +/** + * Implements a Kademlia routing table based on k-buckets with a keccak-256 XOR-based distance + * metric. + */ +public class PeerTable { + private static final int N_BUCKETS = 256; + private static final int DEFAULT_BUCKET_SIZE = 16; + private static final int BLOOM_FILTER_REGENERATION_THRESHOLD = 50; // evictions + + private final Bucket[] table; + private final BytesValue keccak256; + private final int maxEntriesCnt; + + private final Map distanceCache; + private BloomFilter idBloom; + private int evictionCnt = 0; + + /** + * Builds a new peer table, where distance is calculated using the provided nodeId as a baseline. + * + * @param nodeId The ID of the node where this peer table is stored. + * @param bucketSize The maximum length of each k-bucket. + */ + public PeerTable(final BytesValue nodeId, final int bucketSize) { + this.keccak256 = Hash.keccak256(nodeId); + this.table = + Stream.generate(() -> new Bucket(DEFAULT_BUCKET_SIZE)) + .limit(N_BUCKETS + 1) + .toArray(Bucket[]::new); + this.distanceCache = new ConcurrentHashMap<>(); + this.maxEntriesCnt = N_BUCKETS * bucketSize; + + // A bloom filter with 4096 expected insertions of 64-byte keys with a 0.1% false positive + // probability yields a memory footprint of ~7.5kb. + buildBloomFilter(); + } + + public PeerTable(final BytesValue nodeId) { + this(nodeId, DEFAULT_BUCKET_SIZE); + } + + /** + * Returns the table's representation of a peer, if it exists. + * + * @param peer The peer to query. + * @return The stored representation. + */ + public Optional get(final PeerId peer) { + if (!idBloom.mightContain(peer.getId())) { + return Optional.empty(); + } + final int distance = distanceFrom(peer); + return table[distance].getAndTouch(peer.getId()); + } + + /** + * Attempts to add the provided peer to the peer table, and returns a struct signalling one of + * three outcomes. + * + *

Possible outcomes:

+ * + *
    + *
  • the operation succeeded and the peer was added to the corresponding k-bucket. + *
  • the operation failed because the k-bucket was full, in which case a candidate is proposed + * for eviction. + *
  • the operation failed because the peer already existed. + *
+ * + * @see AddResult.Outcome + * @param peer The peer to add. + * @return An object indicating the outcome of the operation. + */ + public AddResult tryAdd(final DiscoveryPeer peer) { + final BytesValue id = peer.getId(); + final int distance = distanceFrom(peer); + + // Safeguard against adding ourselves to the peer table. + if (distance == 0) { + return AddResult.self(); + } + + final Bucket bucket = table[distance]; + // We add the peer, and two things can happen: (1) either we get an empty optional (peer was + // added successfully, + // or it was already there), or (2) we get a filled optional, in which case the bucket is full + // and an eviction + // candidate is proposed. The Bucket#add method will raise an exception if the peer already + // existed. + Optional res; + try { + res = bucket.add(peer); + } catch (final IllegalArgumentException ex) { + return AddResult.existed(); + } + + if (!res.isPresent()) { + idBloom.put(id); + distanceCache.put(id, distance); + return AddResult.added(); + } + + return res.map(AddResult::bucketFull).get(); + } + + /** + * Evicts a peer from the underlying table. + * + * @param peer The peer to evict. + * @return Whether the peer existed, and hence the eviction took place. + */ + public boolean evict(final PeerId peer) { + final BytesValue id = peer.getId(); + final int distance = distanceFrom(peer); + distanceCache.remove(id); + + final boolean evicted = table[distance].evict(peer); + evictionCnt += evicted ? 1 : 0; + + // Trigger the bloom filter regeneration if needed. + if (evictionCnt >= BLOOM_FILTER_REGENERATION_THRESHOLD) { + ForkJoinPool.commonPool().execute(this::buildBloomFilter); + } + + return evicted; + } + + private void buildBloomFilter() { + final BloomFilter bf = + BloomFilter.create((id, val) -> val.putBytes(id.extractArray()), maxEntriesCnt, 0.001); + getAllPeers().stream().map(Peer::getId).forEach(bf::put); + this.evictionCnt = 0; + this.idBloom = bf; + } + + /** + * Returns the limit peers (at most) closest to the provided target, based on the XOR + * distance between the keccak-256 hash of the ID and the keccak-256 hash of the target. + * + * @param target The target node ID. + * @param limit The amount of results to return. + * @return The limit closest peers, at most. + */ + public List nearestPeers(final BytesValue target, final int limit) { + final BytesValue keccak256 = Hash.keccak256(target); + return getAllPeers() + .stream() + .filter(p -> p.getStatus() == PeerDiscoveryStatus.BONDED) + .sorted(comparingInt((peer) -> distance(peer.keccak256(), keccak256))) + .limit(limit) + .collect(toList()); + } + + public Collection getAllPeers() { + return unmodifiableList( + Arrays.stream(table).flatMap(e -> e.peers().stream()).collect(toList())); + } + + /** + * Calculates the XOR distance between the keccak-256 hashes of our node ID and the provided + * {@link DiscoveryPeer}. + * + * @param peer The target peer. + * @return The distance. + */ + private int distanceFrom(final PeerId peer) { + final Integer distance = distanceCache.get(peer.getId()); + return distance == null ? distance(keccak256, peer.keccak256()) : distance; + } + + /** + * Calculates the XOR distance between two values. + * + * @param v1 the first value + * @param v2 the second value + * @return the distance + */ + static int distance(final BytesValue v1, final BytesValue v2) { + assert (v1.size() == v2.size()); + final byte[] v1b = v1.extractArray(); + final byte[] v2b = v2.extractArray(); + + if (Arrays.equals(v1b, v2b)) { + return 0; + } + + int distance = v1b.length * 8; + for (int i = 0; i < v1b.length; i++) { + final byte xor = (byte) (0xff & (v1b[i] ^ v2b[i])); + if (xor == 0) { + distance -= 8; + } else { + int p = 7; + while (((xor >> p--) & 0x01) == 0) { + distance--; + } + break; + } + } + return distance; + } + + /** A struct that encapsulates the result of a peer addition to the table. */ + public static class AddResult { + /** The outcome of the operation. */ + public enum Outcome { + + /** The peer was added successfully to its corresponding k-bucket. */ + ADDED, + + /** The bucket for this peer was full. An eviction candidate must be proposed. */ + BUCKET_FULL, + + /** The peer already existed, hence it was not overwritten. */ + ALREADY_EXISTED, + + /** The caller requested to add ourselves. */ + SELF + } + + private final Outcome outcome; + private final Peer evictionCandidate; + + private AddResult(final Outcome outcome, final Peer evictionCandidate) { + this.outcome = outcome; + this.evictionCandidate = evictionCandidate; + } + + static AddResult added() { + return new AddResult(Outcome.ADDED, null); + } + + static AddResult bucketFull(final Peer evictionCandidate) { + return new AddResult(Outcome.BUCKET_FULL, evictionCandidate); + } + + static AddResult existed() { + return new AddResult(Outcome.ALREADY_EXISTED, null); + } + + static AddResult self() { + return new AddResult(Outcome.SELF, null); + } + + public Outcome getOutcome() { + return outcome; + } + + public Peer getEvictionCandidate() { + return evictionCandidate; + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PingPacketData.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PingPacketData.java new file mode 100755 index 00000000000..8a2ce4a177a --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PingPacketData.java @@ -0,0 +1,85 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.util.Preconditions.checkGuard; + +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryPacketDecodingException; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import java.math.BigInteger; + +public class PingPacketData implements PacketData { + + /* Fixed value that represents we're using v4 of the P2P discovery protocol. */ + private static final int VERSION = 4; + + /* Source. */ + private final Endpoint from; + + /* Destination. */ + private final Endpoint to; + + /* In millis after epoch. */ + private final long expiration; + + private PingPacketData(final Endpoint from, final Endpoint to, final long expiration) { + checkArgument(from != null, "source endpoint cannot be null"); + checkArgument(to != null, "destination endpoint cannot be null"); + checkArgument(expiration >= 0, "expiration cannot be negative"); + + this.from = from; + this.to = to; + this.expiration = expiration; + } + + public static PingPacketData create(final Endpoint from, final Endpoint to) { + return new PingPacketData( + from, to, System.currentTimeMillis() + PacketData.DEFAULT_EXPIRATION_PERIOD_MS); + } + + public static PingPacketData readFrom(final RLPInput in) { + in.enterList(); + final BigInteger version = in.readBigIntegerScalar(); + checkGuard( + version.intValue() == VERSION, + PeerDiscoveryPacketDecodingException::new, + "Version mismatch in ping packet. Expected: %s, got: %s.", + VERSION, + version); + + final Endpoint from = Endpoint.decodeStandalone(in); + final Endpoint to = Endpoint.decodeStandalone(in); + final long expiration = in.readLongScalar(); + in.leaveList(); + return new PingPacketData(from, to, expiration); + } + + @Override + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeIntScalar(VERSION); + from.encodeStandalone(out); + to.encodeStandalone(out); + out.writeLongScalar(expiration); + out.endList(); + } + + public Endpoint getFrom() { + return from; + } + + public Endpoint getTo() { + return to; + } + + public long getExpiration() { + return expiration; + } + + @Override + public String toString() { + return "PingPacketData{" + "from=" + from + ", to=" + to + ", expiration=" + expiration + '}'; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PongPacketData.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PongPacketData.java new file mode 100755 index 00000000000..295c728f495 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PongPacketData.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class PongPacketData implements PacketData { + + /* Destination. */ + private final Endpoint to; + + /* Hash of the PING packet. */ + private final BytesValue pingHash; + + /* In millis after epoch. */ + private final long expiration; + + private PongPacketData(final Endpoint to, final BytesValue pingHash, final long expiration) { + this.to = to; + this.pingHash = pingHash; + this.expiration = expiration; + } + + public static PongPacketData create(final Endpoint to, final BytesValue pingHash) { + return new PongPacketData( + to, pingHash, System.currentTimeMillis() + PacketData.DEFAULT_EXPIRATION_PERIOD_MS); + } + + public static PongPacketData readFrom(final RLPInput in) { + in.enterList(); + final Endpoint to = Endpoint.decodeStandalone(in); + final BytesValue hash = in.readBytesValue(); + final long expiration = in.readLongScalar(); + in.leaveList(); + return new PongPacketData(to, hash, expiration); + } + + @Override + public void writeTo(final RLPOutput out) { + out.startList(); + to.encodeStandalone(out); + out.writeBytesValue(pingHash); + out.writeLongScalar(expiration); + out.endList(); + } + + @Override + public String toString() { + return "PongPacketData{" + + "to=" + + to + + ", pingHash=" + + pingHash + + ", expiration=" + + expiration + + '}'; + } + + public Endpoint getTo() { + return to; + } + + public BytesValue getPingHash() { + return pingHash; + } + + public long getExpiration() { + return expiration; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/RetryDelayFunction.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/RetryDelayFunction.java new file mode 100755 index 00000000000..407dc39f3b3 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/RetryDelayFunction.java @@ -0,0 +1,15 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import java.util.function.UnaryOperator; + +/** + * A function to calculate the next retry delay based on the previous one. In the future, this could + * be enhanced to take in the number of retries so far, so the function can take a decision to abort + * if too many retries have been attempted. + */ +interface RetryDelayFunction extends UnaryOperator { + + static RetryDelayFunction linear(final double multiplier, final long min, final long max) { + return (prev) -> Math.min(Math.max((long) Math.ceil(prev * multiplier), min), max); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/AbstractHandshakeHandler.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/AbstractHandshakeHandler.java new file mode 100755 index 00000000000..05ec13c46c3 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/AbstractHandshakeHandler.java @@ -0,0 +1,139 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.rlpx.framing.Framer; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.Handshaker; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.p2p.wire.messages.HelloMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.WireMessageCodes; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToByteEncoder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +abstract class AbstractHandshakeHandler extends SimpleChannelInboundHandler { + + private static final Logger LOG = LogManager.getLogger(); + + protected final Handshaker handshaker = new ECIESHandshaker(); + + private final PeerInfo ourInfo; + + private final Callbacks callbacks; + private final PeerConnectionRegistry peerConnectionRegistry; + + private final CompletableFuture connectionFuture; + private final List subProtocols; + + AbstractHandshakeHandler( + final List subProtocols, + final PeerInfo ourInfo, + final CompletableFuture connectionFuture, + final Callbacks callbacks, + final PeerConnectionRegistry peerConnectionRegistry) { + this.subProtocols = subProtocols; + this.ourInfo = ourInfo; + this.connectionFuture = connectionFuture; + this.callbacks = callbacks; + this.peerConnectionRegistry = peerConnectionRegistry; + } + + /** + * Generates the next message in the handshake sequence. + * + * @param msg Incoming Message + * @return Optional of the next Handshake message that needs to be returned to the peer + */ + protected abstract Optional nextHandshakeMessage(ByteBuf msg); + + @Override + protected final void channelRead0(final ChannelHandlerContext ctx, final ByteBuf msg) { + final Optional nextMsg = nextHandshakeMessage(msg); + if (nextMsg.isPresent()) { + ctx.writeAndFlush(nextMsg.get()); + } else if (handshaker.getStatus() != Handshaker.HandshakeStatus.SUCCESS) { + LOG.debug("waiting for more bytes"); + } else { + + final BytesValue nodeId = handshaker.partyPubKey().getEncodedBytes(); + if (peerConnectionRegistry.isAlreadyConnected(nodeId)) { + LOG.info("Rejecting connection from already connected client {}", nodeId); + ctx.writeAndFlush( + new OutboundMessage( + null, DisconnectMessage.create(DisconnectReason.ALREADY_CONNECTED))) + .addListener( + ff -> { + ctx.close(); + connectionFuture.completeExceptionally( + new IllegalStateException("Client already connected")); + }); + return; + } + + LOG.debug("Sending framed hello"); + + // Exchange keys done + final Framer framer = new Framer(handshaker.secrets()); + + final ByteToMessageDecoder deFramer = + new DeFramer(framer, subProtocols, ourInfo, callbacks, connectionFuture); + + ctx.channel() + .pipeline() + .addFirst(new ValidateFirstOutboundMessage(framer)) + .replace(this, "DeFramer", deFramer); + + ctx.writeAndFlush(new OutboundMessage(null, HelloMessage.create(ourInfo))) + .addListener( + ff -> { + if (ff.isSuccess()) { + LOG.debug("Successfully wrote hello message"); + } + }); + msg.retain(); + ctx.fireChannelRead(msg); + } + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable throwable) { + LOG.warn("Handshake error:", throwable.getMessage()); + connectionFuture.completeExceptionally(throwable); + ctx.close(); + } + + /** Ensures that wire hello message is the first message written. */ + private static class ValidateFirstOutboundMessage extends MessageToByteEncoder { + private final Framer framer; + + private ValidateFirstOutboundMessage(final Framer framer) { + this.framer = framer; + } + + @Override + protected void encode( + final ChannelHandlerContext context, + final OutboundMessage outboundMessage, + final ByteBuf out) { + if (outboundMessage.getCapability() != null + || outboundMessage.getData().getCode() != WireMessageCodes.HELLO) { + throw new IllegalStateException("First wire message sent wasn't a HELLO."); + } + out.writeBytes(framer.frame(outboundMessage.getData())); + context.pipeline().remove(this); + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/ApiHandler.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/ApiHandler.java new file mode 100755 index 00000000000..71d6dedb349 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/ApiHandler.java @@ -0,0 +1,98 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.p2p.wire.messages.PongMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.WireMessageCodes; +import net.consensys.pantheon.ethereum.rlp.RLPException; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +final class ApiHandler extends SimpleChannelInboundHandler { + + private static final Logger LOGGER = LogManager.getLogger(ApiHandler.class); + + private final CapabilityMultiplexer multiplexer; + private final AtomicBoolean waitingForPong; + + private final Callbacks callbacks; + + private final PeerConnection connection; + + ApiHandler( + final CapabilityMultiplexer multiplexer, + final PeerConnection connection, + final Callbacks callbacks, + final AtomicBoolean waitingForPong) { + this.multiplexer = multiplexer; + this.callbacks = callbacks; + this.connection = connection; + this.waitingForPong = waitingForPong; + } + + @Override + protected void channelRead0(final ChannelHandlerContext ctx, final MessageData originalMessage) { + final CapabilityMultiplexer.ProtocolMessage demultiplexed = + multiplexer.demultiplex(originalMessage); + + final MessageData message = demultiplexed.getMessage(); + + // Handle Wire messages + if (demultiplexed.getCapability() == null) { + switch (message.getCode()) { + case WireMessageCodes.PING: + LOGGER.debug("Received Wire PING"); + try { + connection.send(null, PongMessage.get()); + } catch (final PeerNotConnected peerNotConnected) { + // Nothing to do + } + break; + case WireMessageCodes.PONG: + LOGGER.debug("Received Wire PONG"); + waitingForPong.set(false); + break; + case WireMessageCodes.DISCONNECT: + final DisconnectMessage disconnect = DisconnectMessage.readFrom(message); + DisconnectReason reason = null; + try { + reason = disconnect.getReason(); + LOGGER.info( + "Received Wire DISCONNECT ({}) from peer: {}", + reason.name(), + connection.getPeer().getClientId()); + } catch (final RLPException e) { + // It seems pretty common to get disconnect messages with no reason, which results in an + // rlp parsing error + LOGGER.warn( + "Received Wire DISCONNECT, but unable to parse reason. Peer: {}", + connection.getPeer().getClientId()); + } catch (final Exception e) { + LOGGER.error( + "Received Wire DISCONNECT, but unable to parse reason. Peer: {}", + connection.getPeer().getClientId(), + e); + } + + connection.terminateConnection(reason, true); + } + return; + } + callbacks.invokeSubProtocol(connection, demultiplexed.getCapability(), message); + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable throwable) { + LOGGER.error("Error:", throwable); + callbacks.invokeDisconnect(connection, DisconnectReason.TCP_SUBSYSTEM_ERROR, false); + ctx.close(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/Callbacks.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/Callbacks.java new file mode 100755 index 00000000000..1c15a077cd2 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/Callbacks.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.ethereum.p2p.api.DisconnectCallback; +import net.consensys.pantheon.ethereum.p2p.api.Message; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.DefaultMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.Subscribers; + +import java.util.Map; +import java.util.function.Consumer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Callbacks { + private static final Logger LOGGER = LogManager.getLogger(Callbacks.class); + private static final Subscribers> NO_SUBSCRIBERS = new Subscribers<>(); + + private final Map>> callbacks; + + private final Subscribers disconnectCallbacks; + + Callbacks( + final Map>> callbacks, + final Subscribers disconnectCallbacks) { + this.callbacks = callbacks; + this.disconnectCallbacks = disconnectCallbacks; + } + + public void invokeDisconnect( + final PeerConnection connection, + final DisconnectReason reason, + final boolean initatedByPeer) { + disconnectCallbacks.forEach( + consumer -> consumer.onDisconnect(connection, reason, initatedByPeer)); + } + + public void invokeSubProtocol( + final PeerConnection connection, final Capability capability, final MessageData message) { + final Message fullMessage = new DefaultMessage(connection, message); + callbacks + .getOrDefault(capability, NO_SUBSCRIBERS) + .forEach( + consumer -> { + try { + consumer.accept(fullMessage); + } catch (final Throwable t) { + LOGGER.error("Error in callback:", t); + } + }); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexer.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexer.java new file mode 100755 index 00000000000..c28020ceae8 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexer.java @@ -0,0 +1,180 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import static java.util.Comparator.comparing; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableRangeMap; +import com.google.common.collect.ImmutableRangeMap.Builder; +import com.google.common.collect.Range; +import io.netty.buffer.ByteBuf; + +public class CapabilityMultiplexer { + + static final int WIRE_PROTOCOL_MESSAGE_SPACE = 16; + private static final Comparator CAPABILITY_COMPARATOR = + comparing(Capability::getName).thenComparing(comparing(Capability::getVersion).reversed()); + + private final ImmutableRangeMap agreedCaps; + private final ImmutableMap capabilityOffsets; + private final Map subProtocols = new HashMap<>(); + + public CapabilityMultiplexer( + final List subProtocols, final List a, final List b) { + for (final SubProtocol subProtocol : subProtocols) { + this.subProtocols.put(subProtocol.getName(), subProtocol); + } + agreedCaps = calculateAgreedCapabilities(a, b); + capabilityOffsets = calculateCapabilityOffsets(agreedCaps); + } + + public Set getAgreedCapabilities() { + return capabilityOffsets.keySet(); + } + + public SubProtocol subProtocol(final Capability cap) { + return subProtocols.get(cap.getName()); + } + + /** + * Prepares a message to send by offsetting its code based on the agreed capabilities. + * + * @param cap The capability (protocol) associated with this message. + * @param messageToSend The message to send. + * @return Returns message with the correctly offset code. + */ + public MessageData multiplex(final Capability cap, final MessageData messageToSend) { + final int offset = null == cap ? 0 : capabilityOffsets.get(cap); + return offsetMessageCode(messageToSend, offset); + } + + /** + * Given a message from a peer, determine which capability the message corresponds to and maps the + * message code to the appropriate value. + * + * @param receivedMessage The message received from a peer. + * @return The intepreted message. + */ + public ProtocolMessage demultiplex(final MessageData receivedMessage) { + final Entry, Capability> agreedCap = + agreedCaps.getEntry(receivedMessage.getCode()); + + if (agreedCap == null) { + return new ProtocolMessage(null, receivedMessage); + } + + final int offset = -agreedCap.getKey().lowerEndpoint(); + final Capability cap = agreedCap.getValue(); + + final MessageData demultiplexedMessage = offsetMessageCode(receivedMessage, offset); + return new ProtocolMessage(cap, demultiplexedMessage); + } + + private MessageData offsetMessageCode(final MessageData originalMessage, final int offset) { + // Return wrapped message with modified offset + return new MessageData() { + @Override + public int getSize() { + return originalMessage.getSize(); + } + + @Override + public int getCode() { + return originalMessage.getCode() + offset; + } + + @Override + public void writeTo(final ByteBuf output) { + originalMessage.writeTo(output); + } + + @Override + public void release() { + originalMessage.release(); + } + + @Override + public void retain() { + originalMessage.retain(); + } + }; + } + + private ImmutableRangeMap calculateAgreedCapabilities( + final List a, final List b) { + final List caps = new ArrayList<>(a); + caps.sort(CAPABILITY_COMPARATOR); + caps.retainAll(b); + + final Builder builder = ImmutableRangeMap.builder(); + // Reserve some messages for WireProtocol + int offset = WIRE_PROTOCOL_MESSAGE_SPACE; + String prevProtocol = null; + for (final Iterator itr = caps.iterator(); itr.hasNext(); ) { + final Capability cap = itr.next(); + final String curProtocol = cap.getName(); + if (curProtocol.equalsIgnoreCase(prevProtocol)) { + // A later version of this protocol is already being used, so ignore this version + continue; + } + prevProtocol = curProtocol; + final SubProtocol subProtocol = subProtocols.get(cap.getName()); + final int messageSpace = subProtocol == null ? 0 : subProtocol.messageSpace(cap.getVersion()); + if (messageSpace > 0) { + builder.put(Range.closedOpen(offset, offset + messageSpace), cap); + } + offset += messageSpace; + } + + return builder.build(); + } + + private static ImmutableMap calculateCapabilityOffsets( + final ImmutableRangeMap agreedCaps) { + final ImmutableMap.Builder capToOffset = ImmutableMap.builder(); + for (final Entry, Capability> entry : agreedCaps.asMapOfRanges().entrySet()) { + capToOffset.put(entry.getValue(), entry.getKey().lowerEndpoint()); + } + return capToOffset.build(); + } + + public static class ProtocolMessage { + private final Capability capability; + private final MessageData message; + + ProtocolMessage(final Capability capability, final MessageData message) { + this.capability = capability; + this.message = message; + } + + public Capability getCapability() { + return capability; + } + + public MessageData getMessage() { + return message; + } + + @Override + public String toString() { + return "ProtocolMessage{" + + "capability=" + + capability + + ", messageCode=" + + message.getCode() + + '}'; + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/DeFramer.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/DeFramer.java new file mode 100755 index 00000000000..d2eb4b3ffb1 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/DeFramer.java @@ -0,0 +1,114 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.netty.exceptions.IncompatiblePeerException; +import net.consensys.pantheon.ethereum.p2p.rlpx.framing.Framer; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.p2p.wire.messages.HelloMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.WireMessageCodes; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.timeout.IdleStateHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +final class DeFramer extends ByteToMessageDecoder { + + private static final Logger LOG = LogManager.getLogger(DeFramer.class); + + private final CompletableFuture connectFuture; + + private final Callbacks callbacks; + + private final Framer framer; + private final PeerInfo ourInfo; + private final List subProtocols; + private boolean hellosExchanged; + + DeFramer( + final Framer framer, + final List subProtocols, + final PeerInfo ourInfo, + final Callbacks callbacks, + final CompletableFuture connectFuture) { + this.framer = framer; + this.subProtocols = subProtocols; + this.ourInfo = ourInfo; + this.connectFuture = connectFuture; + this.callbacks = callbacks; + } + + @Override + protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) { + MessageData message; + while ((message = framer.deframe(in)) != null) { + + if (!hellosExchanged && message.getCode() == WireMessageCodes.HELLO) { + hellosExchanged = true; + // Decode first hello and use the payload to modify pipeline + final PeerInfo peerInfo = parsePeerInfo(message); + message.release(); + LOG.debug("Received HELLO message: {}", peerInfo); + if (peerInfo.getVersion() >= 5) { + LOG.debug("Enable compression for p2pVersion: {}", peerInfo.getVersion()); + framer.enableCompression(); + } + + final CapabilityMultiplexer capabilityMultiplexer = + new CapabilityMultiplexer( + subProtocols, ourInfo.getCapabilities(), peerInfo.getCapabilities()); + final PeerConnection connection = + new NettyPeerConnection(ctx, peerInfo, capabilityMultiplexer, callbacks); + if (capabilityMultiplexer.getAgreedCapabilities().size() == 0) { + LOG.info( + "Disconnecting from {} because no capabilities are shared.", peerInfo.getClientId()); + connectFuture.completeExceptionally( + new IncompatiblePeerException("No shared capabilities")); + connection.disconnect(DisconnectReason.USELESS_PEER); + return; + } + + // Setup next stage + final AtomicBoolean waitingForPong = new AtomicBoolean(false); + ctx.channel() + .pipeline() + .addLast( + new IdleStateHandler(15, 0, 0), + new WireKeepAlive(connection, waitingForPong), + new ApiHandler(capabilityMultiplexer, connection, callbacks, waitingForPong), + new MessageFramer(capabilityMultiplexer, framer)); + connectFuture.complete(connection); + } else { + out.add(message); + } + } + } + + private PeerInfo parsePeerInfo(final MessageData message) { + final HelloMessage helloMessage = HelloMessage.readFrom(message); + final PeerInfo peerInfo = helloMessage.getPeerInfo(); + helloMessage.release(); + return peerInfo; + } + + @Override + public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable throwable) + throws Exception { + LOG.error("Exception while processing incoming message", throwable); + if (connectFuture.isDone()) { + connectFuture.get().terminateConnection(DisconnectReason.TCP_SUBSYSTEM_ERROR, false); + } else { + connectFuture.completeExceptionally(throwable); + ctx.close(); + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerInbound.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerInbound.java new file mode 100755 index 00000000000..984d87bf37e --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerInbound.java @@ -0,0 +1,38 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.Handshaker; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import io.netty.buffer.ByteBuf; + +public final class HandshakeHandlerInbound extends AbstractHandshakeHandler { + + public HandshakeHandlerInbound( + final SECP256K1.KeyPair kp, + final List subProtocols, + final PeerInfo ourInfo, + final CompletableFuture connectionFuture, + final Callbacks callbacks, + final PeerConnectionRegistry peerConnectionRegistry) { + super(subProtocols, ourInfo, connectionFuture, callbacks, peerConnectionRegistry); + handshaker.prepareResponder(kp); + } + + @Override + protected Optional nextHandshakeMessage(final ByteBuf msg) { + final Optional nextMsg; + if (handshaker.getStatus() == Handshaker.HandshakeStatus.IN_PROGRESS) { + nextMsg = handshaker.handleMessage(msg); + } else { + nextMsg = Optional.empty(); + } + return nextMsg; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerOutbound.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerOutbound.java new file mode 100755 index 00000000000..7bf1ecdae7e --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/HandshakeHandlerOutbound.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.Handshaker; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class HandshakeHandlerOutbound extends AbstractHandshakeHandler { + + private static final Logger LOGGER = LogManager.getLogger(HandshakeHandlerOutbound.class); + + private final ByteBuf first; + + public HandshakeHandlerOutbound( + final SECP256K1.KeyPair kp, + final BytesValue peerId, + final List subProtocols, + final PeerInfo ourInfo, + final CompletableFuture connectionFuture, + final Callbacks callbacks, + final PeerConnectionRegistry peerConnectionRegistry) { + super(subProtocols, ourInfo, connectionFuture, callbacks, peerConnectionRegistry); + handshaker.prepareInitiator(kp, SECP256K1.PublicKey.create(peerId)); + this.first = handshaker.firstMessage(); + } + + @Override + protected Optional nextHandshakeMessage(final ByteBuf msg) { + final Optional nextMsg; + if (handshaker.getStatus() == Handshaker.HandshakeStatus.IN_PROGRESS) { + nextMsg = handshaker.handleMessage(msg); + } else { + nextMsg = Optional.empty(); + } + return nextMsg; + } + + @Override + public void channelActive(final ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + ctx.writeAndFlush(first) + .addListener( + f -> { + if (f.isSuccess()) { + LOGGER.debug( + "Wrote initial crypto handshake message to {}.", ctx.channel().remoteAddress()); + } + }); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/MessageFramer.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/MessageFramer.java new file mode 100755 index 00000000000..4da463b2909 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/MessageFramer.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.ethereum.p2p.rlpx.framing.Framer; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +final class MessageFramer extends MessageToByteEncoder { + + private final CapabilityMultiplexer multiplexer; + + private final Framer framer; + + MessageFramer(final CapabilityMultiplexer multiplexer, final Framer framer) { + this.multiplexer = multiplexer; + this.framer = framer; + } + + @Override + protected void encode( + final ChannelHandlerContext ctx, final OutboundMessage msg, final ByteBuf out) { + out.writeBytes(framer.frame(multiplexer.multiplex(msg.getCapability(), msg.getData()))); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java new file mode 100755 index 00000000000..83372ad48fb --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyP2PNetwork.java @@ -0,0 +1,390 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.DisconnectCallback; +import net.consensys.pantheon.ethereum.p2p.api.Message; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.config.NetworkingConfiguration; +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryAgent; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerRequirement; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.Subscribers; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.vertx.core.Vertx; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * The peer network service (defunct PeerNetworkingService) is the entrypoint to the peer-to-peer + * components of the Ethereum client. It implements the devp2p framework from the Ethereum + * specifications. + * + *

This component manages the peer discovery layer, the RLPx wire protocol and the subprotocols + * supported by this client. + * + *

Peer Discovery

+ * + * Ethereum nodes discover one another via a simple UDP protocol that follows some of the techniques + * described in the Kademlia DHT paper. Particularly nodes are classified in a k-bucket table + * composed of 256 buckets, where each bucket contains at most 16 peers whose XOR(SHA3(x)) + * distance from us is equal to the index of the bucket. The value x in the distance function + * corresponds to our node ID (public key). + * + *

Upper layers in the stack subscribe to events from the peer discocvery layer and initiate/drop + * connections accordingly. + * + *

RLPx Wire Protocol

+ * + * The RLPx wire protocol is responsible for selecting peers to engage with, authenticating and + * encrypting communications with peers, multiplexing subprotocols, framing messages, controlling + * legality of messages, keeping connections alive, and keeping track of peer reputation. + * + *

Subprotocols

+ * + * Subprotocols are pluggable elements on top of the RLPx framework, which can handle a specific set + * of messages. Each subprotocol has a 3-char ASCII denominator and a version number, and statically + * defines a count of messages it can handle. + * + *

The RLPx wire protocol dispatches messages to subprotocols based on the capabilities agreed by + * each of the two peers during the protocol handshake. + * + * @see Kademlia DHT + * paper + * @see Kademlia Peer + * Selection + * @see devp2p RLPx + */ +public final class NettyP2PNetwork implements P2PNetwork { + + private static final Logger LOGGER = LogManager.getLogger(NettyP2PNetwork.class); + private static final int TIMEOUT_SECONDS = 30; + + final Map>> protocolCallbacks = + new ConcurrentHashMap<>(); + + private final Subscribers> connectCallbacks = new Subscribers<>(); + + private final Subscribers disconnectCallbacks = new Subscribers<>(); + + private final Callbacks callbacks = new Callbacks(protocolCallbacks, disconnectCallbacks); + + private final PeerDiscoveryAgent peerDiscoveryAgent; + private final PeerBlacklist peerBlacklist; + private OptionalLong peerBondedObserverId = OptionalLong.empty(); + + private final PeerConnectionRegistry connections = new PeerConnectionRegistry(); + + private final AtomicInteger pendingConnections = new AtomicInteger(0); + + private final EventLoopGroup boss = new NioEventLoopGroup(1); + + private final EventLoopGroup workers = new NioEventLoopGroup(1); + + private volatile PeerInfo ourPeerInfo; + + private final SECP256K1.KeyPair keyPair; + + private final ChannelFuture server; + + private final int maxPeers; + + private final List subProtocols; + + /** + * Creates a peer networking service for production purposes. + * + *

The caller is expected to provide the IP address to be advertised (normally this node's + * public IP address), as well as TCP and UDP port numbers for the RLPx agent and the discovery + * agent, respectively. + * + * @param vertx The vertx instance. + * @param keyPair This node's keypair. + * @param config The network configuration to use. + * @param supportedCapabilities The wire protocol capabilities to advertise to connected peers. + * @param peerBlacklist The peers with which this node will not connect + * @param peerRequirement Queried to determine if enough peers are currently connected. + */ + public NettyP2PNetwork( + final Vertx vertx, + final SECP256K1.KeyPair keyPair, + final NetworkingConfiguration config, + final List supportedCapabilities, + final PeerRequirement peerRequirement, + final PeerBlacklist peerBlacklist) { + + this.peerBlacklist = peerBlacklist; + peerDiscoveryAgent = + new PeerDiscoveryAgent( + vertx, keyPair, config.getDiscovery(), peerRequirement, peerBlacklist); + subscribeDisconnect(peerDiscoveryAgent); + subscribeDisconnect(peerBlacklist); + subscribeDisconnect(connections); + + maxPeers = config.getRlpx().getMaxPeers(); + this.keyPair = keyPair; + this.subProtocols = config.getSupportedProtocols(); + + server = + new ServerBootstrap() + .group(boss, workers) + .channel(NioServerSocketChannel.class) + .childHandler(inboundChannelInitializer()) + .bind(config.getRlpx().getBindHost(), config.getRlpx().getBindPort()); + final CountDownLatch latch = new CountDownLatch(1); + server.addListener( + future -> { + final InetSocketAddress socketAddress = + (InetSocketAddress) server.channel().localAddress(); + ourPeerInfo = + new PeerInfo( + 5, + config.getClientId(), + supportedCapabilities, + socketAddress.getPort(), + this.keyPair.getPublicKey().getEncodedBytes()); + LOGGER.info("P2PNetwork started and listening on {}", socketAddress); + latch.countDown(); + }); + + // Ensure ourPeerInfo has been set prior to returning from the constructor. + try { + if (!latch.await(1, TimeUnit.MINUTES)) { + throw new RuntimeException("Timed out while waiting for network startup"); + } + } catch (final InterruptedException e) { + throw new RuntimeException("Interrupted before startup completed", e); + } + } + + /** @return a channel initializer for inbound connections */ + public ChannelInitializer inboundChannelInitializer() { + return new ChannelInitializer() { + @Override + protected void initChannel(final SocketChannel ch) { + final CompletableFuture connectionFuture = new CompletableFuture<>(); + ch.pipeline() + .addLast( + new TimeoutHandler<>( + connectionFuture::isDone, + TIMEOUT_SECONDS, + () -> + connectionFuture.completeExceptionally( + new TimeoutException( + "Timed out waiting to fully establish incoming connection"))), + new HandshakeHandlerInbound( + keyPair, subProtocols, ourPeerInfo, connectionFuture, callbacks, connections)); + + connectionFuture.thenAccept( + connection -> { + // Reject incoming connections if we've reached our limit + if (connections.size() >= maxPeers) { + LOGGER.info( + "Disconnecting incoming connection because connection limit of {} has been reached: {}", + maxPeers, + connection.getPeer().getNodeId()); + connection.disconnect(DisconnectReason.TOO_MANY_PEERS); + return; + } + // Reject incoming connections that are blacklisted + if (peerBlacklist.contains(connection)) { + connection.disconnect(DisconnectReason.USELESS_PEER); + return; + } + + onConnectionEstablished(connection); + LOGGER.info( + "Successfully accepted connection from {}", connection.getPeer().getNodeId()); + logConnections(); + }); + } + }; + } + + private int connectionCount() { + return pendingConnections.get() + connections.size(); + } + + @Override + public Collection getPeers() { + return connections.getPeerConnections(); + } + + @Override + public CompletableFuture connect(final Peer peer) { + LOGGER.trace("Initiating connection to peer: {}", peer.getId()); + final CompletableFuture connectionFuture = new CompletableFuture<>(); + final Endpoint endpoint = peer.getEndpoint(); + pendingConnections.incrementAndGet(); + + new Bootstrap() + .group(workers) + .channel(NioSocketChannel.class) + .remoteAddress(new InetSocketAddress(endpoint.getHost(), endpoint.getTcpPort().getAsInt())) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, TIMEOUT_SECONDS * 1000) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(final SocketChannel ch) { + ch.pipeline() + .addLast( + new TimeoutHandler<>( + connectionFuture::isDone, + TIMEOUT_SECONDS, + () -> + connectionFuture.completeExceptionally( + new TimeoutException( + "Timed out waiting to establish connection with peer: " + + peer.getId()))), + new HandshakeHandlerOutbound( + keyPair, + peer.getId(), + subProtocols, + ourPeerInfo, + connectionFuture, + callbacks, + connections)); + } + }) + .connect() + .addListener( + (f) -> { + if (!f.isSuccess()) { + connectionFuture.completeExceptionally(f.cause()); + } + }); + + connectionFuture.whenComplete( + (connection, t) -> { + pendingConnections.decrementAndGet(); + if (t == null) { + onConnectionEstablished(connection); + LOGGER.info("Connection established to peer: {}", peer.getId()); + } else { + LOGGER.warn("Failed to connect to peer {}: {}", peer.getId(), t); + } + logConnections(); + }); + return connectionFuture; + } + + private void logConnections() { + LOGGER.info( + "Connections: {} pending, {} active connections.", + pendingConnections.get(), + connections.size()); + } + + @Override + public void subscribe(final Capability capability, final Consumer callback) { + protocolCallbacks.computeIfAbsent(capability, key -> new Subscribers<>()).subscribe(callback); + } + + @Override + public void subscribeConnect(final Consumer callback) { + connectCallbacks.subscribe(callback); + } + + @Override + public void subscribeDisconnect(final DisconnectCallback callback) { + disconnectCallbacks.subscribe(callback); + } + + @Override + public void run() { + try { + peerDiscoveryAgent.start(ourPeerInfo.getPort()).join(); + final long observerId = + peerDiscoveryAgent.observePeerBondedEvents( + peerBondedEvent -> { + if (connectionCount() < maxPeers + && peerBondedEvent.getPeer().getEndpoint().getTcpPort().isPresent() + && !connections.isAlreadyConnected(peerBondedEvent.getPeer().getId())) { + connect(peerBondedEvent.getPeer()); + } + }); + peerBondedObserverId = OptionalLong.of(observerId); + } catch (final Exception ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public void stop() { + peerDiscoveryAgent.stop().join(); + peerBondedObserverId.ifPresent(peerDiscoveryAgent::removePeerBondedObserver); + peerBondedObserverId = OptionalLong.empty(); + peerDiscoveryAgent.stop().join(); + workers.shutdownGracefully(); + boss.shutdownGracefully(); + } + + @Override + public void awaitStop() { + try { + server.channel().closeFuture().sync(); + } catch (final InterruptedException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public void close() { + stop(); + } + + public Collection getDiscoveryPeers() { + return peerDiscoveryAgent.getPeers(); + } + + @Override + public InetSocketAddress getDiscoverySocketAddress() { + return peerDiscoveryAgent.localAddress(); + } + + @Override + public PeerInfo getSelf() { + return ourPeerInfo; + } + + @Override + public boolean isListening() { + return peerDiscoveryAgent.isActive(); + } + + private void onConnectionEstablished(final PeerConnection connection) { + connections.registerConnection(connection); + connectCallbacks.forEach(callback -> callback.accept(connection)); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnection.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnection.java new file mode 100755 index 00000000000..40da085d2c0 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnection.java @@ -0,0 +1,150 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason.TCP_SUBSYSTEM_ERROR; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; + +import java.net.SocketAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import com.google.common.base.MoreObjects; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +final class NettyPeerConnection implements PeerConnection { + + private static final Logger LOGGER = LogManager.getLogger(NettyPeerConnection.class); + + private final ChannelHandlerContext ctx; + private final PeerInfo peerInfo; + private final Set agreedCapabilities; + private final Map protocolToCapability = new HashMap<>(); + private final AtomicBoolean disconnectDispatched = new AtomicBoolean(false); + private final AtomicBoolean disconnected = new AtomicBoolean(false); + private final Callbacks callbacks; + private final CapabilityMultiplexer multiplexer; + + public NettyPeerConnection( + final ChannelHandlerContext ctx, + final PeerInfo peerInfo, + final CapabilityMultiplexer multiplexer, + final Callbacks callbacks) { + this.ctx = ctx; + this.peerInfo = peerInfo; + this.multiplexer = multiplexer; + this.agreedCapabilities = multiplexer.getAgreedCapabilities(); + for (final Capability cap : agreedCapabilities) { + protocolToCapability.put(cap.getName(), cap); + } + this.callbacks = callbacks; + ctx.channel().closeFuture().addListener(f -> terminateConnection(TCP_SUBSYSTEM_ERROR, false)); + } + + @Override + public void send(final Capability capability, final MessageData message) throws PeerNotConnected { + if (isDisconnected()) { + message.release(); + throw new PeerNotConnected("Attempt to send message to a closed peer connection"); + } + if (capability != null) { + // Validate message is valid for this capability + final SubProtocol subProtocol = multiplexer.subProtocol(capability); + if (subProtocol == null + || !subProtocol.isValidMessageCode(capability.getVersion(), message.getCode())) { + message.release(); + throw new UnsupportedOperationException( + "Attempt to send unsupported message (" + + message.getCode() + + ") via cap " + + capability); + } + } + + LOGGER.debug("Writing {} to {} via protocol {}", message, peerInfo, capability); + ctx.channel().writeAndFlush(new OutboundMessage(capability, message)); + } + + @Override + public PeerInfo getPeer() { + return peerInfo; + } + + @Override + public Capability capability(final String protocol) { + return protocolToCapability.get(protocol); + } + + @Override + public Set getAgreedCapabilities() { + return agreedCapabilities; + } + + @Override + public void terminateConnection(final DisconnectReason reason, final boolean peerInitiated) { + if (disconnectDispatched.compareAndSet(false, true)) { + LOGGER.info("Disconnected ({}) from {}", reason, peerInfo); + callbacks.invokeDisconnect(this, reason, peerInitiated); + disconnected.set(true); + } + // Always ensure the context gets closed immediately even if we previously sent a disconnect + // message and are waiting to close. + ctx.close(); + } + + @Override + public void disconnect(final DisconnectReason reason) { + if (disconnectDispatched.compareAndSet(false, true)) { + LOGGER.info("Disconnecting ({}) from {}", reason, peerInfo); + callbacks.invokeDisconnect(this, reason, false); + try { + send(null, DisconnectMessage.create(reason)); + } catch (final PeerNotConnected e) { + // The connection has already been closed - nothing left to do + return; + } + disconnected.set(true); + ctx.channel().eventLoop().schedule((Callable) ctx::close, 2L, SECONDS); + } + } + + private boolean isDisconnected() { + return disconnected.get(); + } + + @Override + public SocketAddress getLocalAddress() { + return ctx.channel().localAddress(); + } + + @Override + public SocketAddress getRemoteAddress() { + return ctx.channel().remoteAddress(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("clientId", peerInfo.getClientId()) + .add("nodeId", peerInfo.getNodeId()) + .add( + "caps", + String.join( + ", ", + agreedCapabilities.stream().map(Capability::toString).collect(Collectors.toList()))) + .toString(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/OutboundMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/OutboundMessage.java new file mode 100755 index 00000000000..c6f843eee52 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/OutboundMessage.java @@ -0,0 +1,24 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; + +final class OutboundMessage { + + private final Capability capability; + + private final MessageData messageData; + + OutboundMessage(final Capability capability, final MessageData data) { + this.capability = capability; + this.messageData = data; + } + + public MessageData getData() { + return messageData; + } + + public Capability getCapability() { + return capability; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java new file mode 100755 index 00000000000..02a7e9d19a3 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistry.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import static java.util.Collections.unmodifiableCollection; + +import net.consensys.pantheon.ethereum.p2p.api.DisconnectCallback; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class PeerConnectionRegistry implements DisconnectCallback { + + private final ConcurrentMap connections = new ConcurrentHashMap<>(); + + public void registerConnection(final PeerConnection connection) { + connections.put(connection.getPeer().getNodeId(), connection); + } + + public Collection getPeerConnections() { + return unmodifiableCollection(connections.values()); + } + + public int size() { + return connections.size(); + } + + public boolean isAlreadyConnected(final BytesValue nodeId) { + return connections.containsKey(nodeId); + } + + @Override + public void onDisconnect( + final PeerConnection connection, + final DisconnectReason reason, + final boolean initiatedByPeer) { + connections.remove(connection.getPeer().getNodeId()); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/TimeoutHandler.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/TimeoutHandler.java new file mode 100755 index 00000000000..6ce89a04aa1 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/TimeoutHandler.java @@ -0,0 +1,45 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; + +/** + * This handler will close the associated connection if the given condition is not met within the + * timeout window. + */ +public class TimeoutHandler extends ChannelInitializer { + private final Supplier condition; + private final int timeoutInSeconds; + private final OnTimeoutCallback callback; + + public TimeoutHandler( + final Supplier condition, + final int timeoutInSeconds, + final OnTimeoutCallback callback) { + this.condition = condition; + this.timeoutInSeconds = timeoutInSeconds; + this.callback = callback; + } + + @Override + protected void initChannel(final C ch) throws Exception { + ch.eventLoop() + .schedule( + () -> { + if (!condition.get()) { + callback.invoke(); + ch.close(); + } + }, + timeoutInSeconds, + TimeUnit.SECONDS); + } + + @FunctionalInterface + interface OnTimeoutCallback { + void invoke(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/WireKeepAlive.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/WireKeepAlive.java new file mode 100755 index 00000000000..d5975bbaaf0 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/WireKeepAlive.java @@ -0,0 +1,49 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.p2p.wire.messages.PingMessage; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +final class WireKeepAlive extends ChannelDuplexHandler { + private static final Logger LOGGER = LogManager.getLogger(WireKeepAlive.class); + + private final AtomicBoolean waitingForPong; + + private final PeerConnection connection; + + WireKeepAlive(final PeerConnection connection, final AtomicBoolean waitingForPong) { + this.connection = connection; + this.waitingForPong = waitingForPong; + } + + @Override + public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) + throws IOException { + if (!(evt instanceof IdleStateEvent + && ((IdleStateEvent) evt).state() == IdleState.READER_IDLE)) { + // We only care about idling of incoming data from our peer + return; + } + + if (waitingForPong.get()) { + // We are still waiting for a response from our last pong, disconnect with timeout error + LOGGER.info("Wire PONG never received, disconnecting from peer."); + connection.disconnect(DisconnectReason.TIMEOUT); + return; + } + + LOGGER.debug("Idle connection detected, sending Wire PING to peer."); + connection.send(null, PingMessage.get()); + waitingForPong.set(true); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/exceptions/IncompatiblePeerException.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/exceptions/IncompatiblePeerException.java new file mode 100755 index 00000000000..0407138cb43 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/netty/exceptions/IncompatiblePeerException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.p2p.netty.exceptions; + +public class IncompatiblePeerException extends RuntimeException { + + public IncompatiblePeerException(final String message) { + super(message); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeer.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeer.java new file mode 100755 index 00000000000..9bbefff2c18 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeer.java @@ -0,0 +1,190 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static net.consensys.pantheon.util.Preconditions.checkGuard; + +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryPacketDecodingException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.NetworkUtility; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.net.URI; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.primitives.Ints; + +/** The default, basic representation of an Ethereum {@link Peer}. */ +public class DefaultPeer extends DefaultPeerId implements Peer { + + public static final int PEER_ID_SIZE = 64; + + public static final int DEFAULT_PORT = 30303; + private static final Pattern DISCPORT_QUERY_STRING_REGEX = + Pattern.compile("discport=([0-9]{1,5})"); + + private final Endpoint endpoint; + + /** + * Creates a {@link DefaultPeer} instance from a String representation of an enode URL. + * + * @param uri A String representation of the enode URI. + * @return The Peer instance. + * @see enode URL format + */ + public static DefaultPeer fromURI(final String uri) { + return fromURI(URI.create(uri)); + } + + /** + * Creates a {@link DefaultPeer} instance from an URI object that follows the enode URL format. + * + * @param uri The enode URI. + * @return The Peer instance. + * @see enode URL format + */ + public static DefaultPeer fromURI(final URI uri) { + checkNotNull(uri); + checkArgument("enode".equals(uri.getScheme())); + checkArgument(uri.getUserInfo() != null, "node id cannot be null"); + + // Process the peer's public key, in the host portion of the URI. + final BytesValue id = BytesValue.fromHexString(uri.getUserInfo()); + + // Process the ports; falling back to the default port in both TCP and UDP. + int tcpPort = DEFAULT_PORT; + int udpPort = DEFAULT_PORT; + if (NetworkUtility.isValidPort(uri.getPort())) { + tcpPort = udpPort = uri.getPort(); + } + + // If TCP and UDP ports differ, expect a query param 'discport' with the UDP port. + // See https://github.com/ethereum/wiki/wiki/enode-url-format + if (uri.getQuery() != null) { + udpPort = extractUdpPortFromQuery(uri.getQuery()).orElse(tcpPort); + } + + final Endpoint endpoint = new Endpoint(uri.getHost(), udpPort, OptionalInt.of(tcpPort)); + return new DefaultPeer(id, endpoint); + } + + /** + * Creates a {@link DefaultPeer} instance from its attributes, with a TCP port. + * + * @param id The node ID (public key). + * @param host Ip address. + * @param udpPort The UDP port. + * @param tcpPort The TCP port. + */ + public DefaultPeer(final BytesValue id, final String host, final int udpPort, final int tcpPort) { + this(id, host, udpPort, OptionalInt.of(tcpPort)); + } + + /** + * Creates a {@link DefaultPeer} instance from its attributes, without a TCP port. + * + * @param id The node ID (public key). + * @param host Ip address. + * @param udpPort UDP port. + */ + public DefaultPeer(final BytesValue id, final String host, final int udpPort) { + this(id, host, udpPort, OptionalInt.empty()); + } + + /** + * Creates a {@link DefaultPeer} instance from its attributes, without a TCP port. + * + * @param id The node ID (public key). + * @param host Ip address. + * @param udpPort the port number on which to communicate UDP traffic with the peer. + * @param tcpPort the port number on which to communicate TCP traffic with the peer. + */ + public DefaultPeer( + final BytesValue id, final String host, final int udpPort, final OptionalInt tcpPort) { + this(id, new Endpoint(host, udpPort, tcpPort)); + } + + /** + * Creates a {@link DefaultPeer} instance from its ID and its {@link Endpoint}. + * + * @param id The node ID (public key). + * @param endpoint The endpoint for this peer. + */ + public DefaultPeer(final BytesValue id, final Endpoint endpoint) { + super(id); + checkArgument( + id != null && id.size() == PEER_ID_SIZE, "id must be non-null and exactly 64 bytes long"); + checkArgument(endpoint != null, "endpoint cannot be null"); + this.endpoint = endpoint; + } + + /** + * Decodes the RLP stream as a Peer instance. + * + * @param in The RLP input stream from which to read. + * @return The decoded representation. + */ + public static Peer readFrom(final RLPInput in) { + final int size = in.enterList(); + checkGuard( + size == 3 || size == 4, + PeerDiscoveryPacketDecodingException::new, + "Invalid number of components in RLP representation of a peer: expected 3 o 4 but got %s", + size); + + // Subtract 1 from the total size of the list, to account for the peer ID which will be decoded + // by us. + final Endpoint endpoint = Endpoint.decodeInline(in, size - 1); + final BytesValue id = in.readBytesValue(); + in.leaveList(); + return new DefaultPeer(id, endpoint); + } + + private static Optional extractUdpPortFromQuery(final String query) { + final Matcher matcher = DISCPORT_QUERY_STRING_REGEX.matcher(query); + Optional answer = Optional.empty(); + if (matcher.matches()) { + answer = Optional.ofNullable(Ints.tryParse(matcher.group(1))); + } + return answer.filter(NetworkUtility::isValidPort); + } + + /** {@inheritDoc} */ + @Override + public Endpoint getEndpoint() { + return endpoint; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof DefaultPeer)) { + return false; + } + final DefaultPeer other = (DefaultPeer) obj; + return id.equals(other.id) && endpoint.equals(other.endpoint); + } + + @Override + public int hashCode() { + return Objects.hash(id, endpoint); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("DefaultPeer{"); + sb.append("id=").append(id); + sb.append(", endpoint=").append(endpoint); + sb.append('}'); + return sb.toString(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeerId.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeerId.java new file mode 100755 index 00000000000..9e7d9df03a3 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/DefaultPeerId.java @@ -0,0 +1,42 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Objects; + +public class DefaultPeerId implements PeerId { + protected final BytesValue id; + private Bytes32 keccak256; + + public DefaultPeerId(final BytesValue id) { + this.id = id; + } + + @Override + public BytesValue getId() { + return id; + } + + @Override + public Bytes32 keccak256() { + if (keccak256 == null) { + keccak256 = Hash.keccak256(getId()); + } + return keccak256; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || o.getClass().isAssignableFrom(this.getClass())) return false; + final DefaultPeerId that = (DefaultPeerId) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Endpoint.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Endpoint.java new file mode 100755 index 00000000000..f46e94ed483 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Endpoint.java @@ -0,0 +1,152 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.util.Preconditions.checkGuard; + +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryPacketDecodingException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.NetworkUtility; + +import java.net.InetAddress; +import java.util.Objects; +import java.util.OptionalInt; + +import com.google.common.net.InetAddresses; + +/** Encapsulates the network coordinates of a {@link Peer}. */ +public class Endpoint { + private final String host; + private final int udpPort; + private final OptionalInt tcpPort; + + public Endpoint(final String host, final int udpPort, final OptionalInt tcpPort) { + checkArgument( + host != null && InetAddresses.isInetAddress(host), "host requires a valid IP address"); + checkArgument( + NetworkUtility.isValidPort(udpPort), "UDP port requires a value between 1 and 65535"); + tcpPort.ifPresent( + p -> + checkArgument( + NetworkUtility.isValidPort(p), "TCP port requires a value between 1 and 65535")); + + this.host = host; + this.udpPort = udpPort; + this.tcpPort = tcpPort; + } + + public String getHost() { + return host; + } + + public int getUdpPort() { + return udpPort; + } + + public OptionalInt getTcpPort() { + return tcpPort; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof Endpoint)) { + return false; + } + final Endpoint other = (Endpoint) obj; + return host.equals(other.host) + && this.udpPort == other.udpPort + && (this.tcpPort.equals(other.tcpPort)); + } + + @Override + public int hashCode() { + return Objects.hash(host, udpPort, tcpPort); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Endpoint{"); + sb.append("host='").append(host).append('\''); + sb.append(", udpPort=").append(udpPort); + tcpPort.ifPresent(p -> sb.append(", getTcpPort=").append(p)); + sb.append('}'); + return sb.toString(); + } + + /** + * Encodes this endpoint into a standalone object. + * + * @param out The RLP output stream. + */ + public void encodeStandalone(final RLPOutput out) { + out.startList(); + encodeInline(out); + out.endList(); + } + + /** + * Encodes this endpoint to an RLP representation that is inlined into a containing object + * (generally a Peer). + * + * @param out The RLP output stream. + */ + public void encodeInline(final RLPOutput out) { + out.writeInetAddress(InetAddresses.forString(host)); + out.writeUnsignedShort(udpPort); + if (tcpPort.isPresent()) { + out.writeUnsignedShort(tcpPort.getAsInt()); + } else { + out.writeNull(); + } + } + + /** + * Decodes the input stream as an Endpoint instance appearing inline within another object + * (generally a Peer). + * + * @param fieldCount The number of fields RLP list. + * @param in The RLP input stream from which to read. + * @return The decoded endpoint. + */ + public static Endpoint decodeInline(final RLPInput in, final int fieldCount) { + checkGuard( + fieldCount == 2 || fieldCount == 3, + PeerDiscoveryPacketDecodingException::new, + "Invalid number of components in RLP representation of an endpoint: expected 2 o 3 elements but got %s", + fieldCount); + + final InetAddress addr = in.readInetAddress(); + final int udpPort = in.readUnsignedShort(); + + // Some mainnet packets have been shown to either not have the TCP port field at all, + // or to have an RLP NULL value for it. + OptionalInt tcpPort = OptionalInt.empty(); + if (fieldCount == 3) { + if (in.nextIsNull()) { + in.skipNext(); + } else { + tcpPort = OptionalInt.of(in.readUnsignedShort()); + } + } + return new Endpoint(addr.getHostAddress(), udpPort, tcpPort); + } + + /** + * Decodes the RLP stream as a standalone Endpoint instance, which is not part of a Peer. + * + * @param in The RLP input stream from which to read. + * @return The decoded endpoint. + */ + public static Endpoint decodeStandalone(final RLPInput in) { + final int size = in.enterList(); + final Endpoint endpoint = decodeInline(in, size); + in.leaveList(); + return endpoint; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Peer.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Peer.java new file mode 100755 index 00000000000..34abb48d3f9 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/Peer.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import net.consensys.pantheon.crypto.SecureRandomProvider; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; + +public interface Peer extends PeerId { + + /** + * A struct-like immutable object encapsulating the peer's network coordinates, namely their + * hostname (as an IP address in the current implementation), UDP port and optional TCP port for + * RLPx communications. + * + * @return An object encapsulating the peer's network coordinates. + */ + Endpoint getEndpoint(); + + /** + * Generates a random peer ID in a secure manner. + * + * @return The generated peer ID. + */ + static BytesValue randomId() { + final byte[] id = new byte[64]; + SecureRandomProvider.publicSecureRandom().nextBytes(id); + return BytesValue.wrap(id); + } + + /** + * Encodes this peer to its RLP representation. + * + * @param out The RLP output stream to which to write. + */ + default void writeTo(final RLPOutput out) { + out.startList(); + getEndpoint().encodeInline(out); + out.writeBytesValue(getId()); + out.endList(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklist.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklist.java new file mode 100755 index 00000000000..d656f6d1fb0 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklist.java @@ -0,0 +1,80 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import net.consensys.pantheon.ethereum.p2p.api.DisconnectCallback; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +public class PeerBlacklist implements DisconnectCallback { + private static final int DEFAULT_BLACKLIST_CAP = 500; + + private static final Set locallyTriggeredBlacklistReasons = + ImmutableSet.of( + DisconnectReason.BREACH_OF_PROTOCOL, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION); + + private static final Set remotelyTriggeredBlacklistReasons = + ImmutableSet.of(DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION); + + private final int blacklistCap; + private final Set blacklistedNodeIds = + Collections.synchronizedSet( + Collections.newSetFromMap( + new LinkedHashMap(20, 0.75f, true) { + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > blacklistCap; + } + })); + + public PeerBlacklist(final int blacklistCap) { + this.blacklistCap = blacklistCap; + } + + public PeerBlacklist() { + this(DEFAULT_BLACKLIST_CAP); + } + + private boolean contains(final BytesValue nodeId) { + return blacklistedNodeIds.contains(nodeId); + } + + public boolean contains(final PeerConnection peer) { + return contains(peer.getPeer().getNodeId()); + } + + public boolean contains(final Peer peer) { + return contains(peer.getId()); + } + + public void add(final Peer peer) { + add(peer.getId()); + } + + private void add(final BytesValue peerId) { + blacklistedNodeIds.add(peerId); + } + + @Override + public void onDisconnect( + final PeerConnection connection, + final DisconnectReason reason, + final boolean initiatedByPeer) { + if (shouldBlacklistForDisconnect(reason, initiatedByPeer)) { + add(connection.getPeer().getNodeId()); + } + } + + private boolean shouldBlacklistForDisconnect( + final DisconnectMessage.DisconnectReason reason, final boolean initiatedByPeer) { + return (!initiatedByPeer && locallyTriggeredBlacklistReasons.contains(reason)) + || (initiatedByPeer && remotelyTriggeredBlacklistReasons.contains(reason)); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerId.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerId.java new file mode 100755 index 00000000000..7abfccd7e1a --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/peers/PeerId.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public interface PeerId { + /** + * The ID of the peer, equivalent to its public key. In public Ethereum, the public key is derived + * from the signatures the peer attaches to certain messages. + * + * @return The peer's ID. + */ + BytesValue getId(); + + /** + * The Keccak-256 hash value of the peer's ID. The value may be memoized to avoid recomputation + * overhead. + * + * @return The Keccak-256 hash of the peer's ID. + */ + Bytes32 keccak256(); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/CompressionException.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/CompressionException.java new file mode 100755 index 00000000000..b8e9c278daf --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/CompressionException.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.framing; + +/** Thrown when an error occurs during compression and decompression of payloads. */ +public class CompressionException extends RuntimeException { + + public CompressionException(final String message) { + super(message); + } + + public CompressionException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Compressor.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Compressor.java new file mode 100755 index 00000000000..2f1c2940cde --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Compressor.java @@ -0,0 +1,25 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.framing; + +/** A strategy for compressing and decompressing devp2p subprotocol messages. */ +public interface Compressor { + + /** + * Compresses the provided payload. + * + * @param decompressed The original payload. + * @throws CompressionException Thrown if an error occurs during compression; expect to find the + * root cause inside. + * @return The compressed payload. + */ + byte[] compress(byte[] decompressed) throws CompressionException; + + /** + * Decompresses the provided payload. + * + * @param compressed The compressed payload. + * @throws CompressionException Thrown if an error occurs during decompression; expect to find the + * root cause inside. + * @return The original payload. + */ + byte[] decompress(byte[] compressed) throws CompressionException; +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Framer.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Framer.java new file mode 100755 index 00000000000..6c1ef9e5dd4 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/Framer.java @@ -0,0 +1,371 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.framing; + +import static io.netty.buffer.ByteBufUtil.hexDump; +import static io.netty.buffer.Unpooled.wrappedBuffer; +import static org.bouncycastle.pqc.math.linearalgebra.ByteUtils.xor; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.HandshakeSecrets; +import net.consensys.pantheon.ethereum.p2p.utils.ByteBufUtils; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RlpUtils; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; + +import com.google.common.base.Preconditions; +import io.netty.buffer.ByteBuf; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.StreamCipher; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.SICBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; + +/** + * This component is responsible for reading and composing RLPx protocol frames, conformant to the + * schemes defined in the Ethereum protocols. + * + *

These frames are encrypted and authenticated using the secrets generated during the + * cryptographic handshake ({@link net.consensys.pantheon.ethereum.p2p.rlpx.handshake.Handshaker}. + * + *

This component is well-versed in TCP streaming complexities: it is capable of processing + * fragmented frames, as well as streams of multiple messages within the same incoming buffer, as + * long as the order of incoming bytes matches the underlying TCP sequence. + * + * @see RLPx framing + */ +public class Framer { + private static final int LENGTH_HEADER_DATA = 16; + private static final int LENGTH_MAC = 16; + private static final int LENGTH_FULL_HEADER = LENGTH_HEADER_DATA + LENGTH_MAC; + private static final int LENGTH_FRAME_SIZE = 3; + private static final int LENGTH_MESSAGE_ID = 1; + private static final int LENGTH_MAX_MESSAGE_FRAME = 0xFFFFFF; + + private static final byte[] IV = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + private static final byte[] PROTOCOL_HEADER = + RLP.encode( + out -> { + out.startList(); + out.writeNull(); + out.writeNull(); + out.endList(); + }) + .extractArray(); + + private final HandshakeSecrets secrets; + private static final Compressor compressor = new SnappyCompressor(); + private final StreamCipher encryptor; + private final StreamCipher decryptor; + private final BlockCipher macEncryptor; + private boolean headerProcessed; + private int frameSize; + private boolean compressionEnabled = false; + + /** + * Creates a new framer out of the handshake secrets derived during the cryptographic handshake. + * + * @param secrets The handshake secrets. + */ + public Framer(final HandshakeSecrets secrets) { + this.secrets = secrets; + + final KeyParameter aesKey = new KeyParameter(secrets.getAesSecret()); + final KeyParameter macKey = new KeyParameter(secrets.getMacSecret()); + + encryptor = new SICBlockCipher(new AESEngine()); + encryptor.init(true, new ParametersWithIV(aesKey, IV)); + + decryptor = new SICBlockCipher(new AESEngine()); + decryptor.init(false, new ParametersWithIV(aesKey, IV)); + + macEncryptor = new AESEngine(); + macEncryptor.init(true, macKey); + } + + public void enableCompression() { + this.compressionEnabled = true; + } + + public void disableCompression() { + this.compressionEnabled = false; + } + + /** + * Deframes a full message from the byte buffer, if possible. + * + *

If the byte buffer contains insufficient bytes to extract a full message, this method + * returns null. + * + *

If the buffer contains at least a header, it offloads it and processes it, setting an + * internal expectation to subsequently receive as many bytes for the frame as the header + * specified. In this case, this method also returns null to inform the caller that it + * requires more bytes before it can produce an output. + * + *

This method can be called repetitively whenever new bytes appear in the buffer. It is worthy + * to note that the byte buffer is not consumed unless the next expected amount of bytes appears. + * + *

If there is more than one message in the byte buffer, only the first one is returned, + * consuming it from the byte buffer. The caller should call this method again with the same byte + * buffer to continue extracting more messages, if possible. + * + *

When this method throws an exception, it is recommended that the caller scraps away the RLPx + * connection, as the digests and stream ciphers could have become corrupted. + * + * @param buf The buffer containing no messages, partial messages or multiple messages. + * @return The first fully extracted message from this buffer, or null if no message + * could be extracted yet. + * @throws FramingException Thrown when a decryption or internal error occurs. + */ + public synchronized MessageData deframe(final ByteBuf buf) throws FramingException { + if (buf == null || !buf.isReadable()) { + return null; + } + + if (!headerProcessed) { + // We don't have enough bytes to read the header. + if (buf.readableBytes() < LENGTH_FULL_HEADER) { + return null; + } + frameSize = processHeader(buf.readSlice(LENGTH_FULL_HEADER)); + headerProcessed = true; + buf.discardReadBytes(); + } + + final int size = frameSize + padding16(frameSize) + LENGTH_MAC; + if (buf.readableBytes() < size) { + return null; + } + + final MessageData msg = processFrame(buf.readSlice(size), frameSize); + buf.discardReadBytes(); + headerProcessed = false; + return msg; + } + + /** + * Parses, decrypts and performs MAC verification on a packet header. + * + *

This method expects a buffer containing the exact number of bytes a well-formed header + * consists of (32 bytes at this time). Returns the frame size as extracted from the header. + * + * @param tainedHeader The header. + * @throws FramingException If header parsing or decryption failed. + * @return The frame size as extracted from the header. + */ + private int processHeader(final ByteBuf tainedHeader) throws FramingException { + if (tainedHeader.readableBytes() != LENGTH_FULL_HEADER) { + throw error( + "Expected %s bytes in header, got %s", LENGTH_FULL_HEADER, tainedHeader.readableBytes()); + } + + // Decrypt the header. + final byte[] hCipher = new byte[LENGTH_HEADER_DATA]; + final byte[] hMac = new byte[LENGTH_MAC]; + tainedHeader.readBytes(hCipher).readBytes(hMac); + + // Header MAC validation. + byte[] expectedMac = new byte[16]; + macEncryptor.processBlock(secrets.getIngressMac(), 0, expectedMac, 0); + expectedMac = secrets.updateIngress(xor(expectedMac, hCipher)).getIngressMac(); + expectedMac = Arrays.copyOf(expectedMac, LENGTH_MAC); + + validateMac(expectedMac, hMac); + + // Perform the header decryption. + decryptor.processBytes(hCipher, 0, hCipher.length, hCipher, 0); + final ByteBuf h = wrappedBuffer(hCipher); + + // Read the frame length. + final byte[] length = new byte[3]; + h.readBytes(length); + int frameSize = length[0] & 0xff; + frameSize = (frameSize << 8) + (length[1] & 0xff); + frameSize = (frameSize << 8) + (length[2] & 0xff); + + // Discard the header data (RLP): being set to fixed value 0xc28080 (list of two null + // elements) by other clients. + final int headerDataLength = RlpUtils.decodeLength(h.nioBuffer(), 0); + h.skipBytes(headerDataLength); + + // Discard padding in header (= zero-fill to 16-byte boundary). + h.skipBytes(padding16(LENGTH_FRAME_SIZE + headerDataLength)); + + if (h.readableBytes() != 0) { + throw error( + "Expected no more readable bytes while processing header, remaining: %s", + h.readableBytes()); + } + + h.discardReadBytes(); + return frameSize; + } + + /** + * Parses, decrypts and performs MAC verification on a frame. + * + *

This method expects a well-formed frame, sized according to the length indicated in this + * packet's header. + * + * @param f The buffer containing + * @param frameSize The expected + * @return + */ + private MessageData processFrame(final ByteBuf f, final int frameSize) { + final int pad = padding16(frameSize); + final int expectedSize = frameSize + pad + LENGTH_MAC; + if (f.readableBytes() != expectedSize) { + throw error("Expected %s bytes in header, got %s", expectedSize, f.readableBytes()); + } + + final byte[] frameData = new byte[frameSize + pad]; + final byte[] fMac = new byte[LENGTH_MAC]; + f.readBytes(frameData).readBytes(fMac); + + // Validate the frame's MAC. + final byte[] fMacSeed = secrets.updateIngress(frameData).getIngressMac(); + final byte[] fMacSeedEnc = new byte[16]; + macEncryptor.processBlock(fMacSeed, 0, fMacSeedEnc, 0); + byte[] expectedMac = secrets.updateIngress(xor(fMacSeedEnc, fMacSeed)).getIngressMac(); + expectedMac = Arrays.copyOf(expectedMac, LENGTH_MAC); + + validateMac(fMac, expectedMac); + + // Decrypt frame data. + decryptor.processBytes(frameData, 0, frameData.length, frameData, 0); + + // Read the id. + final BytesValue idbv = RLP.decodeOne(BytesValue.of(frameData[0])); + final int id = idbv.isZero() || idbv.size() == 0 ? 0 : idbv.get(0); + + // Write message data to ByteBuf, decompressing as necessary + ByteBuf data; + if (compressionEnabled) { + // Decompress data before writing to ByteBuf + final byte[] compressedMessageData = Arrays.copyOfRange(frameData, 1, frameData.length - pad); + final byte[] decompressedMessageData = compressor.decompress(compressedMessageData); + data = NetworkMemoryPool.allocate(decompressedMessageData.length); + data.writeBytes(decompressedMessageData); + } else { + // Move data to a ByteBuf + final int messageLength = frameSize - LENGTH_MESSAGE_ID; + data = NetworkMemoryPool.allocate(messageLength); + data.writeBytes(frameData, 1, messageLength); + } + + return new RawMessage(id, data); + } + + private void validateMac(final byte[] candidateMac, final byte[] expectedMac) { + if (!Arrays.equals(expectedMac, candidateMac)) { + throw error( + "Frame MAC did not match expected MAC; expected: %s, received: %s", + hexDump(expectedMac), hexDump(candidateMac)); + } + } + + /** + * Frames a message for sending to an RLPx peer, encrypting it and calculating the appropriate + * MACs. + * + * @param message The message to frame. + * @return The framed message, as byte buffer. + */ + public synchronized ByteBuf frame(final MessageData message) { + Preconditions.checkArgument( + message.getSize() < LENGTH_MAX_MESSAGE_FRAME, "Message size in excess of maximum length."); + // Compress message + if (compressionEnabled) { + try { + // Extract data from message + final ByteBuf tmp = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(tmp); + // Compress data + final byte[] uncompressed = ByteBufUtils.toByteArray(tmp); + final byte[] compressed = compressor.compress(uncompressed); + tmp.release(); + // Construct new, compressed message + final ByteBuf compressedBuf = NetworkMemoryPool.allocate(compressed.length); + compressedBuf.writeBytes(compressed); + return frameAndReleaseMessage(new RawMessage(message.getCode(), compressedBuf)); + } finally { + // We have to release the original message because frameAndRelease only released the + // compressed copy. + message.release(); + } + } else { + return frameAndReleaseMessage(message); + } + } + + private ByteBuf frameAndReleaseMessage(final MessageData message) { + try { + final int frameSize = message.getSize() + LENGTH_MESSAGE_ID; + final int pad = padding16(frameSize); + final int bufSize = LENGTH_FULL_HEADER + (frameSize + pad + LENGTH_MAC); + final ByteBuf buf = NetworkMemoryPool.allocate(bufSize); + + final byte id = (byte) message.getCode(); + + // Generate the header data. + final byte[] h = new byte[LENGTH_HEADER_DATA]; + h[0] = (byte) ((frameSize >> 16) & 0xff); + h[1] = (byte) ((frameSize >> 8) & 0xff); + h[2] = (byte) (frameSize & 0xff); + System.arraycopy(PROTOCOL_HEADER, 0, h, LENGTH_FRAME_SIZE, PROTOCOL_HEADER.length); + Arrays.fill(h, LENGTH_FRAME_SIZE + PROTOCOL_HEADER.length, h.length - 1, (byte) 0x00); + encryptor.processBytes(h, 0, LENGTH_HEADER_DATA, h, 0); + + // Generate the header MAC. + byte[] hMac = Arrays.copyOf(secrets.getEgressMac(), LENGTH_MAC); + macEncryptor.processBlock(hMac, 0, hMac, 0); + hMac = secrets.updateEgress(xor(h, hMac)).getEgressMac(); + hMac = Arrays.copyOf(hMac, LENGTH_MAC); + buf.writeBytes(h).writeBytes(hMac); + + // Sanity check. + assert buf.writerIndex() == LENGTH_FULL_HEADER; + + // Encrypt payload. + final byte[] f = new byte[frameSize + pad]; + + final BytesValue bv = id == 0 ? RLP.NULL : RLP.encodeOne(BytesValue.of(id)); + assert bv.size() == 1; + f[0] = bv.get(0); + + // Zero-padded to 16-byte boundary. + final ByteBuf tmp = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(tmp); + tmp.getBytes(tmp.readerIndex(), f, 1, tmp.readableBytes()); + encryptor.processBytes(f, 0, f.length, f, 0); + tmp.release(); + + // Calculate the frame MAC. + final byte[] fMacSeed = Arrays.copyOf(secrets.updateEgress(f).getEgressMac(), LENGTH_MAC); + byte[] fMac = new byte[16]; + macEncryptor.processBlock(fMacSeed, 0, fMac, 0); + fMac = Arrays.copyOf(secrets.updateEgress(xor(fMac, fMacSeed)).getEgressMac(), LENGTH_MAC); + + buf.writeBytes(f).writeBytes(fMac); + + // Sanity check: all expected bytes are written. + assert buf.writerIndex() == LENGTH_FULL_HEADER + (frameSize + pad + LENGTH_MAC); + + return buf; + } finally { + message.release(); + } + } + + private static int padding16(final int size) { + final int pad = size % 16; + return pad == 0 ? 0 : 16 - pad; + } + + private static FramingException error(final String s, final Object... params) { + return new FramingException(String.format(s, params)); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramingException.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramingException.java new file mode 100755 index 00000000000..a351297e22e --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramingException.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.framing; + +/** Thrown when the framer encounters an error during framing or deframing. */ +public class FramingException extends RuntimeException { + + public FramingException(final String message) { + super(message); + } + + public FramingException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressor.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressor.java new file mode 100755 index 00000000000..e4bb8cf78f6 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressor.java @@ -0,0 +1,35 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.framing; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.io.IOException; + +import org.xerial.snappy.Snappy; + +/** + * A strategy for compressing and decompressing data with the Snappy algorithm. + * + * @see Snappy algorithm + */ +public class SnappyCompressor implements Compressor { + + @Override + public byte[] compress(final byte[] uncompressed) { + checkArgument(uncompressed != null, "input data must not be null"); + try { + return Snappy.compress(uncompressed); + } catch (final IOException e) { + throw new CompressionException("Snappy compression failed", e); + } + } + + @Override + public byte[] decompress(final byte[] compressed) { + checkArgument(compressed != null, "input data must not be null"); + try { + return Snappy.uncompress(compressed); + } catch (final IOException e) { + throw new CompressionException("Snappy decompression failed", e); + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeException.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeException.java new file mode 100755 index 00000000000..82e0b831ac8 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeException.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake; + +/** Signals that an error occurred during the RLPx cryptographic handshake. */ +public class HandshakeException extends RuntimeException { + + public HandshakeException(final String message) { + super(message); + } + + public HandshakeException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeSecrets.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeSecrets.java new file mode 100755 index 00000000000..30c23218124 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/HandshakeSecrets.java @@ -0,0 +1,188 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; +import java.util.Objects; + +import org.bouncycastle.crypto.digests.KeccakDigest; + +/** + * Encapsulates the secrets generated during the RLPx crypto handshake, and offers a facility for + * updating some values as messages are exchanged during the lifetime of the connection. + * + *

The following secret materials are modelled: + * + *

    + *
  • AES secret: shared secret used to cipher and decipher message payloads. + *
  • MAC secret: shared secret used to update ingress and egress MACs as + * messages are exchanged. + *
  • Token: identifies this session, currently unused. + *
  • Ingress MAC: continuously-updating MAC for received bytes. + *
  • Egress MAC: continuously-updating MAC for sent bytes. + *
+ * + * @see RLPx + * Encrypted Handshake + */ +public class HandshakeSecrets { + private final byte[] aesSecret; + private final byte[] macSecret; + private final byte[] token; + private final KeccakDigest egressMac = new KeccakDigest(Bytes32.SIZE * 8); + private final KeccakDigest ingressMac = new KeccakDigest(Bytes32.SIZE * 8); + + /** + * Creates an instance with empty MACs. + * + * @param aesSecret The AES shared secret. + * @param macSecret The MAC shared secret. + * @param token The session token. + */ + public HandshakeSecrets(final byte[] aesSecret, final byte[] macSecret, final byte[] token) { + checkArgument(aesSecret.length == Bytes32.SIZE, "aes secret must be exactly 32 bytes long"); + checkArgument(macSecret.length == Bytes32.SIZE, "mac secret must be exactly 32 bytes long"); + checkArgument(token.length == Bytes32.SIZE, "token must be exactly 32 bytes long"); + + this.aesSecret = aesSecret; + this.macSecret = macSecret; + this.token = token; + } + + /** + * Updates the egress mac with the provided bytes. + * + * @param bytes The bytes of the outgoing message. + * @return Returns this instance for fluent chaining. + */ + public HandshakeSecrets updateEgress(final byte[] bytes) { + egressMac.update(bytes, 0, bytes.length); + return this; + } + + /** + * Updates the ingress mac with the provided bytes. + * + * @param bytes The bytes of the incoming message. + * @return Returns this instance for fluent chaining. + */ + public HandshakeSecrets updateIngress(final byte[] bytes) { + ingressMac.update(bytes, 0, bytes.length); + return this; + } + + /** + * Returns the AES shared secret. + * + * @return The AES shared secret. + */ + public byte[] getAesSecret() { + return aesSecret; + } + + /** + * Returns the MAC shared secret. + * + * @return The MAC shared secret. + */ + public byte[] getMacSecret() { + return macSecret; + } + + /** + * Returns the token that identifies a session (unused). + * + * @return The token. + */ + public byte[] getToken() { + return token; + } + + /** + * Returns a snapshot of the current egress MAC, without finalising the underlying digest. + * + * @return Snapshot of the current egress MAC. + */ + public byte[] getEgressMac() { + return snapshot(egressMac); + } + + /** + * Returns a snapshot of the current ingress MAC, without finalising the underlying digest. + * + * @return Snapshot of the current ingress MAC. + */ + public byte[] getIngressMac() { + return snapshot(ingressMac); + } + + /** + * TODO: It's not wise to print secrets. Maybe print only the first and last 8 bytes (ellipsize + * the middle). That might be enough for testing. + */ + @Override + public String toString() { + return "HandshakeSecrets{" + + "aesSecret=" + + BytesValue.wrap(aesSecret) + + ", macSecret=" + + BytesValue.wrap(macSecret) + + ", token=" + + BytesValue.wrap(token) + + ", egressMac=" + + BytesValue.wrap(snapshot(egressMac)) + + ", ingressMac=" + + BytesValue.wrap(snapshot(ingressMac)) + + '}'; + } + + private static byte[] snapshot(final KeccakDigest digest) { + final byte[] out = new byte[Bytes32.SIZE]; + new KeccakDigest(digest).doFinal(out, 0); + return out; + } + + @Override + public boolean equals(final Object obj) { + return equals(obj, false); + } + + /** + * Performs an equals comparison with the ability to flip the MAC comparison, catering for + * scenarios where we want to compare the handshake secrets on opposing ends of a channel. + * + * @param o The object whose equality to test with this. + * @param flipMacs Whether the egress MAC should be compared against the ingress MAC, and + * viceversa. + * @return Whether both objects are equal or not. + */ + public boolean equals(final Object o, final boolean flipMacs) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final HandshakeSecrets that = (HandshakeSecrets) o; + final KeccakDigest vsEgress = flipMacs ? that.ingressMac : that.egressMac; + final KeccakDigest vsIngress = flipMacs ? that.egressMac : that.ingressMac; + return Arrays.equals(aesSecret, that.aesSecret) + && Arrays.equals(macSecret, that.macSecret) + && Arrays.equals(token, that.token) + && Arrays.equals(snapshot(egressMac), snapshot(vsEgress)) + && Arrays.equals(snapshot(ingressMac), snapshot(vsIngress)); + } + + @Override + public int hashCode() { + return Objects.hash( + Arrays.hashCode(aesSecret), + Arrays.hashCode(macSecret), + Arrays.hashCode(token), + egressMac, + ingressMac); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/Handshaker.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/Handshaker.java new file mode 100755 index 00000000000..a3c5356ef68 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/Handshaker.java @@ -0,0 +1,148 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker; + +import java.util.Optional; + +import io.netty.buffer.ByteBuf; + +/** + * A protocol to perform an RLPx crypto handshake with a peer. + * + *

This models a two-party handshake with a potentially indefinite sequence of messages between + * parties, culminating with the creation of a {@link HandshakeSecrets} object containing the + * secrets that have been agreed/generated as a result. + * + *

The roles modelled herein are that of an initiator and a responder. It is + * assumed that the former is responsible for dispatching the first message, hence kicking off the + * sequence. Nevertheless, implementations of this interface may choose to support concurrent + * exchange of messages, as long as the backing crypto algorithms are capable of handling it. + * + *

When a party has no more messages to send, it signals so by returning an empty {@link + * Optional} from the {@link #handleMessage(ByteBuf)} method. At this point, the consumer class is + * expected to query the final result by calling {@link #getStatus()} and, if successful, it should + * obtain the {@link HandshakeSecrets} outputs by calling {@link #secrets()}. + * + *

All methods can throw the {@link IllegalStateException} runtime exception if they're being + * called at an illegal time. Refer to the methods Javadocs for more insight. + * + *

TODO: Declare a destroy() that securely destroys any intermediate secrets for security. + * + * @see ECIESHandshaker + */ +public interface Handshaker { + + /** Represents the status of the handshaker. */ + enum HandshakeStatus { + + /** This handshaker has been created but has not been prepared with the initial material. */ + UNINITIALIZED, + + /** + * This handshaker has been prepared with the initial material, but the handshake is not yet in + * progress. + */ + PREPARED, + + /** The handshake is taking place. */ + IN_PROGRESS, + + /** The handshake culminated successfully, and the secrets have been generated. */ + SUCCESS, + + /** The handshake failed. */ + FAILED + } + + /** + * This method must be called by the initiating side of the handshake to provide the + * initial crypto material for the handshake, before any further methods are called. + * + *

This method must throw an IllegalStateException exception if the handshake had + * already been prepared before, no matter if under the initiator or the responder role. + * + * @param ourKeypair The keypair for our node identity. + * @param theirPubKey The public key of the node we're handshaking with. + * @throws IllegalStateException Indicates that preparation had already occured. + */ + void prepareInitiator(SECP256K1.KeyPair ourKeypair, SECP256K1.PublicKey theirPubKey); + + /** + * This method must be called by the responding side of the handshake to prepare the + * initial crypto material for the handshake, before any further methods are called. + * + *

This method must throw an IllegalStateException exception if the handshake had + * already been prepared before, whether with the initiator or the responder role. + * + * @param ourKeypair The keypair for our node identity. + * @throws IllegalStateException Indicates that preparation had already occured. + */ + void prepareResponder(SECP256K1.KeyPair ourKeypair); + + /** + * Retrieves the first message to dispatch in the handshake ceremony. + * + *

This method must only be called by the party that's able to initiate the + * handshake. In the {@link ECIESHandshaker initial implementation} of this interface, nobody but + * the initiator is allowed to send the first message in the channel. Future implementations may + * allow for a concurrent exchange. + * + *

This method will throw an IllegalStateException if the consumer has prepared this + * handshake taking the role of the responder, and the underlying implementation only allows the + * initiator to send the first message. + * + * @return The raw message to send, encrypted. + * @throws IllegalStateException Indicates that this role taken by this party precludes it from + * sending the first message. + * @throws HandshakeException Thrown if an error occurred during the encryption of the message. + */ + ByteBuf firstMessage() throws HandshakeException; + + /** + * Handles an encrypted incoming message, and produces an optional reply. + * + *

This method must be called whenever a message pertaining to this handshake + * is received. Implementations are expected to mutate their underlying state accordingly. If the + * handshake protocol defines a response message, it must be returned from the + * call. + * + *

If the handshake has arrived at its final stage and no more messages are to be exchanged, an + * empty optional must be returned. Consumers must then query the status by + * calling {@link #getStatus()} and obtain the generated {@link HandshakeSecrets} if the status + * allows it (i.e. success). + * + * @param buf The incoming message, encrypted. + * @return The message to send in response, or an empty optional if there are no more messages to + * send and the handshake has arrived at its final stage. + * @throws IllegalStateException Indicates that the handshake is not in progress. + * @throws HandshakeException Thrown if an error occurred during the decryption of the incoming + * message or the encryption of the next message (if there is one). + */ + Optional handleMessage(ByteBuf buf) throws HandshakeException; + + /** + * Returns the current status of this handshake. + * + * @return The status of this handshake. + */ + HandshakeStatus getStatus(); + + /** + * Returns the handshake secrets generated as a result of the handshake ceremony. + * + * @return The generated secrets. + * @throws IllegalStateException Thrown if this handshake has not completed and hence it cannot + * return its secrets yet. + */ + HandshakeSecrets secrets(); + + /** + * Returns the other party's public key, after the handshake has completed. + * + * @return The party's public key. + * @throws IllegalStateException Thrown if this handshake has not completed and hence it cannot + * return the other party's public key yet. + */ + SECP256K1.PublicKey partyPubKey(); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESEncryptionEngine.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESEncryptionEngine.java new file mode 100755 index 00000000000..2b58cf86d9e --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESEncryptionEngine.java @@ -0,0 +1,416 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; + +import org.bouncycastle.crypto.BasicAgreement; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.DerivationFunction; +import org.bouncycastle.crypto.DerivationParameters; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.DigestDerivationFunction; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.Mac; +import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.modes.SICBlockCipher; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.IESWithCipherParameters; +import org.bouncycastle.crypto.params.KDFParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.BigIntegers; +import org.bouncycastle.util.Pack; + +/** + * An Integrated Encryption + * Scheme engine that implements the encryption and decryption logic behind the ECIES crypto + * handshake during the RLPx connection establishment. + * + *

This class has been inspired by the IESEngine implementation in Bouncy Castle. It has + * been modified heavily to accommodate our usage, yet the core logic remains unchanged. It + * implements a peculiarity of the Ethereum encryption protocol: updating the encryption MAC with + * the IV. + */ +public class ECIESEncryptionEngine { + public static final int ENCRYPTION_OVERHEAD = 113; + + private static final byte[] IES_DERIVATION = new byte[0]; + private static final byte[] IES_ENCODING = new byte[0]; + private static final short CIPHER_BLOCK_SIZE = 16; + private static final short CIPHER_KEY_SIZE_BITS = CIPHER_BLOCK_SIZE * 8; + + private static final IESWithCipherParameters PARAM = + new IESWithCipherParameters( + IES_DERIVATION, IES_ENCODING, CIPHER_KEY_SIZE_BITS, CIPHER_KEY_SIZE_BITS); + private static final int CIPHER_KEY_SIZE = PARAM.getCipherKeySize(); + private static final int CIPHER_MAC_KEY_SIZE = PARAM.getMacKeySize(); + + // Configure the components of the Integrated Encryption Scheme. + private final Digest hash = new SHA256Digest(); + private final DerivationFunction kdf = new ECIESHandshakeKDFFunction(); + private final Mac mac = new HMac(new SHA256Digest()); + private final BufferedBlockCipher cipher = + new BufferedBlockCipher(new SICBlockCipher(new AESEngine())); + + private final SECP256K1.PublicKey ephPubKey; + private final byte[] iv; + + // TODO: This V is possibly redundant. + private final byte[] V = new byte[0]; + + private ECIESEncryptionEngine( + final CipherParameters pubParam, + final CipherParameters privParam, + final SECP256K1.PublicKey ephPubKey, + final byte[] iv) { + this.ephPubKey = ephPubKey; + this.iv = iv; + + // Compute the common value and convert to byte array. + final BasicAgreement agree = new ECDHBasicAgreement(); + agree.init(privParam); + final BigInteger z = agree.calculateAgreement(pubParam); + final byte[] Z = BigIntegers.asUnsignedByteArray(agree.getFieldSize(), z); + + // Initialise the KDF. + this.kdf.init(new KDFParameters(Z, PARAM.getDerivationV())); + } + + /** + * Creates a new engine for decryption. + * + * @param privKey The private key of the deciphering end. + * @param ephPubKey The ephemeral public key extracted from the raw message. + * @param iv The initialization vector extracted from the raw message. + * @return An engine prepared for decryption. + */ + public static ECIESEncryptionEngine forDecryption( + final SECP256K1.PrivateKey privKey, + final SECP256K1.PublicKey ephPubKey, + final BytesValue iv) { + final byte[] ivb = iv.extractArray(); + + // Create parameters. + final ECDomainParameters dp = SECP256K1.CURVE; + final CipherParameters pubParam = new ECPublicKeyParameters(ephPubKey.asEcPoint(), dp); + final CipherParameters privParam = new ECPrivateKeyParameters(privKey.getD(), dp); + + return new ECIESEncryptionEngine(pubParam, privParam, ephPubKey, ivb); + } + + /** + * Creates a new engine for encryption. + * + *

The generated IV and ephemeral public key are available via getters {@link #getIv()} and + * {@link #getEphPubKey()}. + * + * @param pubKey The public key of the receiver. + * @return An engine prepared for encryption. + */ + public static ECIESEncryptionEngine forEncryption(final SECP256K1.PublicKey pubKey) { + // Create an ephemeral key pair for IES whose public key we can later append in the message. + final SECP256K1.KeyPair ephKeyPair = SECP256K1.KeyPair.generate(); + + // Create random iv. + final byte[] ivb = ECIESHandshaker.random(CIPHER_BLOCK_SIZE).extractArray(); + + // Create parameters. + final CipherParameters pubParam = + new ECPublicKeyParameters(pubKey.asEcPoint(), SECP256K1.CURVE); + final CipherParameters privParam = + new ECPrivateKeyParameters(ephKeyPair.getPrivateKey().getD(), SECP256K1.CURVE); + + return new ECIESEncryptionEngine(pubParam, privParam, ephKeyPair.getPublicKey(), ivb); + } + + /** + * Encrypts the provided plaintext. + * + * @param in The plaintext. + * @return The ciphertext. + * @throws InvalidCipherTextException Thrown if an error occured during encryption. + */ + public BytesValue encrypt(final BytesValue in) throws InvalidCipherTextException { + return BytesValue.wrap(encrypt(in.extractArray(), 0, in.size(), null)); + } + + public BytesValue encrypt(final BytesValue in, final byte[] macData) + throws InvalidCipherTextException { + return BytesValue.wrap(encrypt(in.extractArray(), 0, in.size(), macData)); + } + + private byte[] encrypt(final byte[] in, final int inOff, final int inLen, final byte[] macData) + throws InvalidCipherTextException { + byte[] C; + byte[] K; + byte[] K1; + byte[] K2; + + int len; + + // Block cipher mode. + K1 = new byte[CIPHER_KEY_SIZE / 8]; + K2 = new byte[CIPHER_MAC_KEY_SIZE / 8]; + K = new byte[K1.length + K2.length]; + + kdf.generateBytes(K, 0, K.length); + System.arraycopy(K, 0, K1, 0, K1.length); + System.arraycopy(K, K1.length, K2, 0, K2.length); + + // Initialize the cipher with the IV. + cipher.init(true, new ParametersWithIV(new KeyParameter(K1), iv)); + + C = new byte[cipher.getOutputSize(inLen)]; + len = cipher.processBytes(in, inOff, inLen, C, 0); + len += cipher.doFinal(C, len); + + // Convert the length of the encoding vector into a byte array. + final byte[] P2 = PARAM.getEncodingV(); + + // Apply the MAC. + final byte[] T = new byte[mac.getMacSize()]; + + final byte[] K2hash = new byte[hash.getDigestSize()]; + hash.reset(); + hash.update(K2, 0, K2.length); + hash.doFinal(K2hash, 0); + + mac.init(new KeyParameter(K2hash)); + mac.update(iv, 0, iv.length); + mac.update(C, 0, C.length); + + if (P2 != null) { + mac.update(P2, 0, P2.length); + } + + if (V.length != 0 && P2 != null) { + final byte[] L2 = Pack.intToBigEndian(P2.length * 8); + mac.update(L2, 0, L2.length); + } + + if (macData != null) { + mac.update(macData, 0, macData.length); + } + + mac.doFinal(T, 0); + + // Output the triple (V,C,T). + final byte[] Output = new byte[V.length + len + T.length]; + System.arraycopy(V, 0, Output, 0, V.length); + System.arraycopy(C, 0, Output, V.length, len); + System.arraycopy(T, 0, Output, V.length + len, T.length); + return Output; + } + + /** + * Decrypts the provided ciphertext. + * + * @param in The ciphertext. + * @return The plaintext. + * @throws InvalidCipherTextException Thrown if an error occured during decryption. + */ + public BytesValue decrypt(final BytesValue in) throws InvalidCipherTextException { + return BytesValue.wrap(decrypt(in.extractArray(), 0, in.size(), null)); + } + + public BytesValue decrypt(final BytesValue in, final byte[] commonMac) + throws InvalidCipherTextException { + return BytesValue.wrap(decrypt(in.extractArray(), 0, in.size(), commonMac)); + } + + private byte[] decrypt( + final byte[] inEnc, final int inOff, final int inLen, final byte[] commonMac) + throws InvalidCipherTextException { + byte[] M; + byte[] K; + byte[] K1; + byte[] K2; + + int len; + + // Ensure that the length of the input is greater than the MAC in bytes + if (inLen <= (CIPHER_MAC_KEY_SIZE / 8)) { + throw new InvalidCipherTextException("Length of input must be greater than the MAC"); + } + + // Block cipher mode. + K1 = new byte[CIPHER_KEY_SIZE / 8]; + K2 = new byte[CIPHER_MAC_KEY_SIZE / 8]; + K = new byte[K1.length + K2.length]; + + kdf.generateBytes(K, 0, K.length); + System.arraycopy(K, 0, K1, 0, K1.length); + System.arraycopy(K, K1.length, K2, 0, K2.length); + + // Use IV to initialize cipher. + cipher.init(false, new ParametersWithIV(new KeyParameter(K1), iv)); + + M = new byte[cipher.getOutputSize(inLen - V.length - mac.getMacSize())]; + len = cipher.processBytes(inEnc, inOff + V.length, inLen - V.length - mac.getMacSize(), M, 0); + len += cipher.doFinal(M, len); + + // Convert the length of the encoding vector into a byte array. + final byte[] P2 = PARAM.getEncodingV(); + + // Verify the MAC. + final int end = inOff + inLen; + final byte[] T1 = Arrays.copyOfRange(inEnc, end - mac.getMacSize(), end); + final byte[] T2 = new byte[T1.length]; + + final byte[] K2hash = new byte[hash.getDigestSize()]; + hash.reset(); + hash.update(K2, 0, K2.length); + hash.doFinal(K2hash, 0); + + mac.init(new KeyParameter(K2hash)); + mac.update(iv, 0, iv.length); + mac.update(inEnc, inOff + V.length, inLen - V.length - T2.length); + + if (P2 != null) { + mac.update(P2, 0, P2.length); + } + + if (V.length != 0 && P2 != null) { + final byte[] L2 = Pack.intToBigEndian(P2.length * 8); + mac.update(L2, 0, L2.length); + } + + if (commonMac != null) { + mac.update(commonMac, 0, commonMac.length); + } + + mac.doFinal(T2, 0); + + if (!Arrays.constantTimeAreEqual(T1, T2)) { + throw new InvalidCipherTextException("Invalid MAC."); + } + + // Output the message. + return Arrays.copyOfRange(M, 0, len); + } + + /** + * Returns the initialization vector. + * + *

When encrypting a payload this value is automatically generated and accessible via this + * getter. + * + * @return The initialization vector in use. + */ + public BytesValue getIv() { + return BytesValue.wrap(iv); + } + + /** + * Returns the ephemeral public key. + * + *

When encrypting a payload this value is automatically generated and accessible via this + * getter. + * + * @return The ephemeral public key. + */ + public SECP256K1.PublicKey getEphPubKey() { + return ephPubKey; + } + + /** + * Key generation function as defined in NIST SP 800-56A, but swapping the order of the digested + * values (counter first, shared secret second) to comply with Ethereum's approach. + * + *

This class has been adapted from the BaseKDFBytesGenerator implementation of Bouncy + * Castle. + */ + private static class ECIESHandshakeKDFFunction implements DigestDerivationFunction { + private static final int COUNTER_START = 1; + private final Digest digest = new SHA256Digest(); + private final int digestSize = digest.getDigestSize(); + private byte[] shared; + private byte[] iv; + + @Override + public void init(final DerivationParameters param) { + checkArgument(param instanceof KDFParameters, "unexpected expected KDF params type"); + + final KDFParameters p = (KDFParameters) param; + shared = p.getSharedSecret(); + iv = p.getIV(); + } + + /** + * Returns the underlying digest. + * + * @return The digest. + */ + @Override + public Digest getDigest() { + return digest; + } + + /** + * Fills len bytes of the output buffer with bytes generated from the derivation + * function. + * + * @throws IllegalArgumentException If the size of the request will cause an overflow. + * @throws DataLengthException If the out buffer is too small. + */ + @Override + public int generateBytes(final byte[] out, final int outOff, final int len) + throws DataLengthException, IllegalArgumentException { + checkArgument(len >= 0, "length to fill cannot be negative"); + + if ((out.length - len) < outOff) { + throw new DataLengthException("output buffer too small"); + } + + final int outLen = digest.getDigestSize(); + final int cThreshold = (len + outLen - 1) / outLen; + final byte[] dig = new byte[digestSize]; + final byte[] C = Pack.intToBigEndian(COUNTER_START); + int counterBase = COUNTER_START & ~0xFF; + int offset = outOff; + int length = len; + + for (int i = 0; i < cThreshold; i++) { + // Ethereum peculiarity: Ethereum requires digesting the counter and the shared secret is + // inverse order + // that of the standard BaseKDFBytesGenerator in Bouncy Castle. + digest.update(C, 0, C.length); + digest.update(shared, 0, shared.length); + + if (iv != null) { + digest.update(iv, 0, iv.length); + } + + digest.doFinal(dig, 0); + + if (length > outLen) { + System.arraycopy(dig, 0, out, offset, outLen); + offset += outLen; + length -= outLen; + } else { + System.arraycopy(dig, 0, out, offset, length); + } + + if (++C[3] == 0) { + counterBase += 0x100; + Pack.intToBigEndian(counterBase, C, 0); + } + } + + digest.reset(); + return length; + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java new file mode 100755 index 00000000000..127f3d4f333 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java @@ -0,0 +1,412 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import static com.google.common.base.Preconditions.checkState; +import static net.consensys.pantheon.crypto.Hash.keccak256; +import static net.consensys.pantheon.util.bytes.Bytes32s.xor; +import static net.consensys.pantheon.util.bytes.BytesValues.concatenate; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SECP256K1.PublicKey; +import net.consensys.pantheon.crypto.SecureRandomProvider; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.HandshakeException; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.HandshakeSecrets; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.Handshaker; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.security.SecureRandom; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.annotations.VisibleForTesting; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.crypto.InvalidCipherTextException; + +/** + * An Elliptic Curve Integrated Encryption Scheme implementation, following the handshake ceremony + * of Ethereum. + * + * @see RLPx + * encrypted handshake + */ +public class ECIESHandshaker implements Handshaker { + private static final Logger LOG = LogManager.getLogger(); + private static final SecureRandom RANDOM = SecureRandomProvider.publicSecureRandom(); + + static final int SIGNATURE_LENGTH = 65; + static final int HASH_EPH_PUBKEY_LENGTH = 32; + static final int PUBKEY_LENGTH = 64; + static final int NONCE_LENGTH = 32; + static final int TOKEN_FLAG_LENGTH = 1; + + // Keypairs under our control. + private SECP256K1.KeyPair identityKeyPair; + private SECP256K1.KeyPair ephKeyPair; + + // Party's material, only public keys. + private PublicKey partyPubKey; + private PublicKey partyEphPubKey; + + // Messages, for later MAC calculation. + private InitiatorHandshakeMessage initiatorMsg; + private ResponderHandshakeMessage responderMsg; + private BytesValue initiatorMsgEnc; + private BytesValue responderMsgEnc; + + // Nonces. + private Bytes32 initiatorNonce; + private Bytes32 responderNonce; + + // Whether we are the party who initiated this handshake or not. + private boolean initiator; + + // See Javadoc on #secrets() to understand the state machine. + private final AtomicReference status = + new AtomicReference<>(Handshaker.HandshakeStatus.UNINITIALIZED); + private HandshakeSecrets secrets; + + private boolean version4 = true; + + @Override + public void prepareInitiator(final SECP256K1.KeyPair ourKeypair, final PublicKey theirPubKey) { + checkState( + status.compareAndSet( + Handshaker.HandshakeStatus.UNINITIALIZED, Handshaker.HandshakeStatus.PREPARED), + "handshake was already prepared"); + + this.initiator = true; + this.identityKeyPair = ourKeypair; + this.ephKeyPair = SECP256K1.KeyPair.generate(); + this.partyPubKey = theirPubKey; + this.initiatorNonce = Bytes32.wrap(random(32), 0); + LOG.debug( + "Prepared ECIES handshake with node {}... under INITIATOR role", + theirPubKey.getEncodedBytes().slice(0, 16)); + } + + @Override + public void prepareResponder(final SECP256K1.KeyPair ourKeypair) { + checkState( + status.compareAndSet( + Handshaker.HandshakeStatus.UNINITIALIZED, Handshaker.HandshakeStatus.IN_PROGRESS), + "handshake was already prepared"); + + this.initiator = false; + this.identityKeyPair = ourKeypair; + this.ephKeyPair = SECP256K1.KeyPair.generate(); + this.responderNonce = Bytes32.wrap(random(32), 0); + LOG.debug("Prepared ECIES handshake under RESPONDER role"); + } + + @Override + public ByteBuf firstMessage() throws HandshakeException { + checkState(initiator, "illegal invocation of firstMessage on non-initiator end of handshake"); + checkState( + status.compareAndSet( + Handshaker.HandshakeStatus.PREPARED, Handshaker.HandshakeStatus.IN_PROGRESS), + "illegal invocation of firstMessage, handshake had already started"); + + final Bytes32 staticSharedSecret = + SECP256K1.calculateKeyAgreement(identityKeyPair.getPrivateKey(), partyPubKey); + if (version4) { + initiatorMsg = + InitiatorHandshakeMessageV4.create( + identityKeyPair.getPublicKey(), ephKeyPair, staticSharedSecret, initiatorNonce); + } else { + initiatorMsg = + InitiatorHandshakeMessageV1.create( + identityKeyPair.getPublicKey(), + ephKeyPair, + staticSharedSecret, + initiatorNonce, + false); + } + try { + if (version4) { + initiatorMsgEnc = EncryptedMessage.encryptMsgEip8(initiatorMsg.encode(), partyPubKey); + } else { + initiatorMsgEnc = EncryptedMessage.encryptMsg(initiatorMsg.encode(), partyPubKey); + } + } catch (final InvalidCipherTextException e) { + status.set(Handshaker.HandshakeStatus.FAILED); + throw new HandshakeException("Encrypting the first handshake message failed", e); + } + + LOG.debug("First ECIES handshake message under INITIATOR role: {}", initiatorMsg); + + return Unpooled.wrappedBuffer(initiatorMsgEnc.extractArray()); + } + + @Override + public Optional handleMessage(final ByteBuf buf) throws HandshakeException { + checkState( + status.get() == Handshaker.HandshakeStatus.IN_PROGRESS, + "illegal invocation of onMessage on handshake that is not in progress"); + + // Take as many bytes as expected in the next message. + int expectedLength = ECIESEncryptionEngine.ENCRYPTION_OVERHEAD; + expectedLength += + initiator + ? ResponderHandshakeMessageV1.MESSAGE_LENGTH + : InitiatorHandshakeMessageV1.MESSAGE_LENGTH; + + if (buf.readableBytes() < expectedLength) { + buf.markReaderIndex(); + final int size = buf.readUnsignedShort(); + if (size > buf.readableBytes() + 2) { + buf.resetReaderIndex(); + return Optional.empty(); + } + expectedLength = size; + buf.resetReaderIndex(); + } + + buf.markReaderIndex(); + final ByteBuf bufferedBytes = buf.readSlice(expectedLength); + BytesValue bytes = BytesValue.wrapBuffer(bufferedBytes); + + BytesValue encryptedMsg = bytes; + try { + // Decrypt the message with our private key. + try { + bytes = EncryptedMessage.decryptMsg(bytes, identityKeyPair.getPrivateKey()); + version4 = false; + } catch (final Exception ex) { + // Assume new format + final int size = bufferedBytes.readUnsignedShort(); + if (buf.writerIndex() >= size) { + bufferedBytes.readerIndex(0); + final byte[] fullMessage = new byte[size + 2]; + bufferedBytes.readBytes(fullMessage, 0, expectedLength); + buf.readBytes(fullMessage, expectedLength, size - expectedLength + 2); + encryptedMsg = BytesValue.wrap(fullMessage); + bytes = EncryptedMessage.decryptMsgEIP8(encryptedMsg, identityKeyPair.getPrivateKey()); + version4 = true; + } else { + throw new HandshakeException("Failed to decrypt handshake message", ex); + } + } + } catch (final InvalidCipherTextException e) { + status.set(Handshaker.HandshakeStatus.FAILED); + throw new HandshakeException("Decrypting an incoming handshake message failed", e); + } + + Optional nextMsg = Optional.empty(); + if (initiator) { + // If we are the initiator, we have already sent our request and we're waiting for the + // responder's ack; + // when we receive it, we can build the handshake secret material and declare a SUCCESS. + checkState( + responderMsg == null, + "unexpected message: responder message had " + "already been received"); + + // Store the message, as we need it to generating our ingress and egress MACs. + responderMsgEnc = encryptedMsg; + if (version4) { + responderMsg = ResponderHandshakeMessageV4.decode(bytes); + } else { + responderMsg = ResponderHandshakeMessageV1.decode(bytes); + } + + // Extract the responder's nonce and ephemeral pubkey, which will be used to generate the + // shared secrets. + responderNonce = responderMsg.getNonce(); + partyEphPubKey = responderMsg.getEphPublicKey(); + + LOG.debug( + "Received responder's ECIES handshake message from node {}...: {}", + partyPubKey.getEncodedBytes().slice(0, 16), + responderMsg); + + } else { + // If we are the responder, we are waiting for an initiator message; after we generate our + // message and + // we can build the handshake secret material and declare a SUCCESS. + checkState( + initiatorMsg == null, + "unexpected message: initiator message " + "had already been received"); + + // Store the message, as we need it to generating our ingress and egress MACs. + initiatorMsgEnc = encryptedMsg; + if (version4) { + initiatorMsg = InitiatorHandshakeMessageV4.decode(bytes, identityKeyPair); + } else { + initiatorMsg = InitiatorHandshakeMessageV1.decode(bytes, identityKeyPair); + } + + LOG.debug( + "[{}] Received initiator's ECIES handshake message: {}", + identityKeyPair.getPublicKey().getEncodedBytes(), + initiatorMsg); + + // Extract the initiator's data. + initiatorNonce = initiatorMsg.getNonce(); + partyPubKey = initiatorMsg.getPubKey(); + partyEphPubKey = initiatorMsg.getEphPubKey(); + + checkState( + keccak256(partyEphPubKey.getEncodedBytes()).equals(initiatorMsg.getEphPubKeyHash()), + "keccak hash of recovered ephemeral pubkey does not match announced hash"); + + // Build the response message. + if (version4) { + responderMsg = + ResponderHandshakeMessageV4.create(ephKeyPair.getPublicKey(), responderNonce); + } else { + responderMsg = + ResponderHandshakeMessageV1.create(ephKeyPair.getPublicKey(), responderNonce, false); + } + + LOG.debug( + "Generated responder's ECIES handshake message against peer {}...: {}", + partyPubKey.getEncodedBytes().slice(0, 16), + responderMsg); + + try { + if (version4) { + responderMsgEnc = EncryptedMessage.encryptMsgEip8(responderMsg.encode(), partyPubKey); + } else { + responderMsgEnc = EncryptedMessage.encryptMsg(responderMsg.encode(), partyPubKey); + } + } catch (final InvalidCipherTextException e) { + status.set(Handshaker.HandshakeStatus.FAILED); + throw new HandshakeException("Encrypting the next handshake message failed", e); + } + nextMsg = Optional.of(responderMsgEnc); + + // Compute the secrets and declare this handshake as successful. + } + computeSecrets(); + + status.set(Handshaker.HandshakeStatus.SUCCESS); + LOG.debug("Handshake status set to {}", status.get()); + return nextMsg.map(bv -> Unpooled.wrappedBuffer(bv.extractArray())); + } + + /** + * Returns the current status of this handshake. + * + *

Starts UNINITIALIZED and moves to PREPARED when a prepared* method is + * called, or to IN_PROGRESS if we're the responder part and have nothing to prepare + * since we're awaiting the initiator's message. + * + *

As soon as we receive the expected message, the status transitions to SUCCESS if + * the message is well formed and we're able to generate the resulting secrets. + * + * @return Returns the current status of this handshake. + */ + @Override + public Handshaker.HandshakeStatus getStatus() { + return status.get(); + } + + @Override + public HandshakeSecrets secrets() { + checkState( + status.get() == Handshaker.HandshakeStatus.SUCCESS, + "cannot obtain secrets from an unsuccessful handshake"); + return secrets; + } + + @Override + public PublicKey partyPubKey() { + checkState( + initiator || status.get() == Handshaker.HandshakeStatus.SUCCESS, + "under the role of responder, cannot return the party's public " + + "key until the handshake has completed"); + return partyPubKey; + } + + /** Computes the secrets from the two exchanged messages. */ + void computeSecrets() { + final BytesValue agreedSecret = + SECP256K1.calculateKeyAgreement(ephKeyPair.getPrivateKey(), partyEphPubKey); + final BytesValue sharedSecret = + keccak256( + concatenate(agreedSecret, keccak256(concatenate(responderNonce, initiatorNonce)))); + + final Bytes32 aesSecret = keccak256(concatenate(agreedSecret, sharedSecret)); + final Bytes32 macSecret = keccak256(concatenate(agreedSecret, aesSecret)); + final Bytes32 token = keccak256(sharedSecret); + + final HandshakeSecrets secrets = + new HandshakeSecrets( + aesSecret.extractArray(), macSecret.extractArray(), token.extractArray()); + + final BytesValue initiatorMac = concatenate(xor(macSecret, responderNonce), initiatorMsgEnc); + final BytesValue responderMac = concatenate(xor(macSecret, initiatorNonce), responderMsgEnc); + + if (initiator) { + secrets.updateEgress(initiatorMac.extractArray()); + secrets.updateIngress(responderMac.extractArray()); + } else { + secrets.updateIngress(initiatorMac.extractArray()); + secrets.updateEgress(responderMac.extractArray()); + } + + this.secrets = secrets; + } + + static BytesValue random(final int size) { + final byte[] iv = new byte[size]; + RANDOM.nextBytes(iv); + return BytesValue.wrap(iv); + } + + // --------------------------------------------- + // The methods below are for testing purposes. + // --------------------------------------------- + + @VisibleForTesting + SECP256K1.KeyPair getIdentityKeyPair() { + return identityKeyPair; + } + + @VisibleForTesting + SECP256K1.KeyPair getEphKeyPair() { + return ephKeyPair; + } + + @VisibleForTesting + void setEphKeyPair(final SECP256K1.KeyPair ephKeyPair) { + this.ephKeyPair = ephKeyPair; + } + + @VisibleForTesting + PublicKey getPartyEphPubKey() { + return partyEphPubKey; + } + + @VisibleForTesting + Bytes32 getInitiatorNonce() { + return initiatorNonce; + } + + @VisibleForTesting + void setInitiatorNonce(final Bytes32 initiatorNonce) { + this.initiatorNonce = initiatorNonce; + } + + @VisibleForTesting + Bytes32 getResponderNonce() { + return responderNonce; + } + + @VisibleForTesting + void setResponderNonce(final Bytes32 responderNonce) { + this.responderNonce = responderNonce; + } + + @VisibleForTesting + void setInitiatorMsgEnc(final BytesValue initiatorMsgEnc) { + this.initiatorMsgEnc = initiatorMsgEnc; + } + + @VisibleForTesting + void setResponderMsgEnc(final BytesValue responderMsgEnc) { + this.responderMsgEnc = responderMsgEnc; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java new file mode 100755 index 00000000000..c1bff21b621 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java @@ -0,0 +1,152 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.crypto.SecureRandomProvider; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; + +import org.bouncycastle.crypto.InvalidCipherTextException; + +final class EncryptedMessage { + + private static final int IV_SIZE = 16; + + private static final SecureRandom RANDOM = SecureRandomProvider.createSecureRandom(); + + private EncryptedMessage() { + // Utility Class + } + + /** + * Decrypts the ciphertext using our private key. + * + * @param msgBytes The ciphertext. + * @param ourKey Our private key. + * @return The plaintext. + * @throws InvalidCipherTextException Thrown if decryption failed. + */ + public static BytesValue decryptMsg(final BytesValue msgBytes, final SECP256K1.PrivateKey ourKey) + throws InvalidCipherTextException { + + // Extract the ephemeral public key, stripping off the first byte (0x04), which designates it's + // an uncompressed key. + final SECP256K1.PublicKey ephPubKey = SECP256K1.PublicKey.create(msgBytes.slice(1, 64)); + + // Strip off the IV to use. + final BytesValue iv = msgBytes.slice(65, IV_SIZE); + + // Extract the encrypted payload. + final BytesValue encrypted = msgBytes.slice(65 + IV_SIZE); + + // Perform the decryption. + final ECIESEncryptionEngine decryptor = + ECIESEncryptionEngine.forDecryption(ourKey, ephPubKey, iv); + return decryptor.decrypt(encrypted); + } + + /** + * Decrypts the ciphertext using our private key. + * + * @param msgBytes The ciphertext. + * @param ourKey Our private key. + * @return The plaintext. + * @throws InvalidCipherTextException Thrown if decryption failed. + */ + public static BytesValue decryptMsgEIP8( + final BytesValue msgBytes, final SECP256K1.PrivateKey ourKey) + throws InvalidCipherTextException { + final SECP256K1.PublicKey ephPubKey = SECP256K1.PublicKey.create(msgBytes.slice(3, 64)); + + // Strip off the IV to use. + final BytesValue iv = msgBytes.slice(3 + 64, IV_SIZE); + + // Extract the encrypted payload. + final BytesValue encrypted = msgBytes.slice(3 + 64 + IV_SIZE); + + // Perform the decryption. + final ECIESEncryptionEngine decryptor = + ECIESEncryptionEngine.forDecryption(ourKey, ephPubKey, iv); + return decryptor.decrypt(encrypted, msgBytes.slice(0, 2).extractArray()); + } + + /** + * Encrypts a message for the specified peer using ECIES. + * + * @param bytes The plaintext. + * @param remoteKey The peer's remote key. + * @return The ciphertext. + * @throws InvalidCipherTextException Thrown if encryption failed. + */ + public static BytesValue encryptMsg(final BytesValue bytes, final SECP256K1.PublicKey remoteKey) + throws InvalidCipherTextException { + // TODO: check size. + final ECIESEncryptionEngine engine = ECIESEncryptionEngine.forEncryption(remoteKey); + + // Do the encryption. + final BytesValue encrypted = engine.encrypt(bytes); + final BytesValue iv = engine.getIv(); + final SECP256K1.PublicKey ephPubKey = engine.getEphPubKey(); + + // Create the output message by concatenating the ephemeral public key (prefixed with + // 0x04 to designate uncompressed), IV, and encrypted bytes. + final MutableBytesValue answer = + MutableBytesValue.create(1 + ECIESHandshaker.PUBKEY_LENGTH + IV_SIZE + encrypted.size()); + + int offset = 0; + // Set the first byte as 0x04 to specify it's an uncompressed key. + answer.set(offset, (byte) 0x04); + ephPubKey.getEncodedBytes().copyTo(answer, offset += 1); + iv.copyTo(answer, offset += ECIESHandshaker.PUBKEY_LENGTH); + encrypted.copyTo(answer, offset + iv.size()); + return answer; + } + + /** + * Encrypts a message for the specified peer using ECIES. + * + * @param message The plaintext. + * @param remoteKey The peer's remote key. + * @return The ciphertext. + * @throws InvalidCipherTextException Thrown if encryption failed. + */ + public static BytesValue encryptMsgEip8( + final BytesValue message, final SECP256K1.PublicKey remoteKey) + throws InvalidCipherTextException { + final ECIESEncryptionEngine engine = ECIESEncryptionEngine.forEncryption(remoteKey); + + // Do the encryption. + final BytesValue bytes = addPadding(message); + final int size = bytes.size() + ECIESEncryptionEngine.ENCRYPTION_OVERHEAD; + final byte[] sizePrefix = {(byte) (size >>> 8), (byte) size}; + final BytesValue encrypted = engine.encrypt(bytes, sizePrefix); + final BytesValue iv = engine.getIv(); + final SECP256K1.PublicKey ephPubKey = engine.getEphPubKey(); + + // Create the output message by concatenating the ephemeral public key (prefixed with + // 0x04 to designate uncompressed), IV, and encrypted bytes. + final MutableBytesValue answer = + MutableBytesValue.create(3 + ECIESHandshaker.PUBKEY_LENGTH + IV_SIZE + encrypted.size()); + + answer.set(0, sizePrefix[0]); + answer.set(1, sizePrefix[1]); + // Set the first byte as 0x04 to specify it's an uncompressed key. + answer.set(2, (byte) 0x04); + int offset = 0; + ephPubKey.getEncodedBytes().copyTo(answer, offset += 3); + iv.copyTo(answer, offset += ECIESHandshaker.PUBKEY_LENGTH); + encrypted.copyTo(answer, offset + IV_SIZE); + return answer; + } + + private static BytesValue addPadding(final BytesValue message) { + final byte[] raw = message.extractArray(); + final int padding = 100 + RANDOM.nextInt(200); + final byte[] paddingBytes = new byte[padding]; + RANDOM.nextBytes(paddingBytes); + return BytesValue.wrap( + ByteBuffer.allocate(raw.length + padding).put(raw).put(paddingBytes).array()); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessage.java new file mode 100755 index 00000000000..8f8b8dd567f --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessage.java @@ -0,0 +1,18 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public interface InitiatorHandshakeMessage { + + BytesValue encode(); + + Bytes32 getNonce(); + + SECP256K1.PublicKey getPubKey(); + + SECP256K1.PublicKey getEphPubKey(); + + Bytes32 getEphPubKeyHash(); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java new file mode 100755 index 00000000000..cc52dede8a7 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java @@ -0,0 +1,163 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import static com.google.common.base.Preconditions.checkState; +import static net.consensys.pantheon.crypto.SECP256K1.calculateKeyAgreement; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.NONCE_LENGTH; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.PUBKEY_LENGTH; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.SIGNATURE_LENGTH; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.TOKEN_FLAG_LENGTH; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.Bytes32s; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytes32; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +/** + * The initiator's handshake message. + * + *

This message must be sent by the party that initiates the RLPX connection, as the first + * message in the handshake protocol. + * + *

Message structure

+ * + * The following describes the message structure: + * + *
+ *   authInitiator -> E(remote-pubk,
+ *                      S(ephemeral-privk, static-shared-secret ^ nonce)
+ *                       || H(ephemeral-pubk)
+ *                      || pubk
+ *                      || nonce
+ *                      || 0x0)
+ * 
+ * + * @see Structure of the + * initiator request + */ +public final class InitiatorHandshakeMessageV1 implements InitiatorHandshakeMessage { + public static final int MESSAGE_LENGTH = + SIGNATURE_LENGTH + HASH_EPH_PUBKEY_LENGTH + PUBKEY_LENGTH + NONCE_LENGTH + TOKEN_FLAG_LENGTH; + + private final SECP256K1.PublicKey pubKey; + private final SECP256K1.Signature signature; + private final SECP256K1.PublicKey ephPubKey; + private final Bytes32 ephPubKeyHash; + private final Bytes32 nonce; + private final boolean token; + + protected InitiatorHandshakeMessageV1( + final SECP256K1.PublicKey pubKey, + final SECP256K1.Signature signature, + final SECP256K1.PublicKey ephPubKey, + final Bytes32 ephPubKeyHash, + final Bytes32 nonce, + final boolean token) { + this.pubKey = pubKey; + this.signature = signature; + this.ephPubKey = ephPubKey; + this.ephPubKeyHash = ephPubKeyHash; + this.nonce = nonce; + this.token = token; + } + + public static InitiatorHandshakeMessageV1 create( + final SECP256K1.PublicKey ourPubKey, + final SECP256K1.KeyPair ephKeyPair, + final Bytes32 staticSharedSecret, + final Bytes32 nonce, + final boolean token) { + final Bytes32 ephPubKeyHash = Hash.keccak256(ephKeyPair.getPublicKey().getEncodedBytes()); + + // XOR of the static shared secret and the generated nonce. + final MutableBytes32 toSign = MutableBytes32.create(); + Bytes32s.xor(staticSharedSecret, nonce, toSign); + final SECP256K1.Signature signature = SECP256K1.sign(toSign, ephKeyPair); + return new InitiatorHandshakeMessageV1( + ourPubKey, signature, ephKeyPair.getPublicKey(), ephPubKeyHash, nonce, token); + } + + /** + * Decodes this message. + * + * @param bytes The raw bytes. + * @param keyPair Our keypair to calculat ECDH key agreements. + * @return The decoded message. + */ + public static InitiatorHandshakeMessageV1 decode( + final BytesValue bytes, final SECP256K1.KeyPair keyPair) { + checkState(bytes.size() == MESSAGE_LENGTH); + + int offset = 0; + final SECP256K1.Signature signature = + SECP256K1.Signature.decode(bytes.slice(offset, SIGNATURE_LENGTH)); + final Bytes32 ephPubKeyHash = + Bytes32.wrap(bytes.slice(offset += SIGNATURE_LENGTH, HASH_EPH_PUBKEY_LENGTH), 0); + final SECP256K1.PublicKey pubKey = + SECP256K1.PublicKey.create(bytes.slice(offset += HASH_EPH_PUBKEY_LENGTH, PUBKEY_LENGTH)); + final Bytes32 nonce = Bytes32.wrap(bytes.slice(offset += PUBKEY_LENGTH, NONCE_LENGTH), 0); + final boolean token = bytes.get(offset) == 0x01; + + final Bytes32 staticSharedSecret = calculateKeyAgreement(keyPair.getPrivateKey(), pubKey); + final Bytes32 toSign = Bytes32s.xor(staticSharedSecret, nonce); + final SECP256K1.PublicKey ephPubKey = + SECP256K1.PublicKey.recoverFromSignature(toSign, signature) + .orElseThrow(() -> new RuntimeException("Could not recover public key from signature")); + + return new InitiatorHandshakeMessageV1( + pubKey, signature, ephPubKey, ephPubKeyHash, nonce, token); + } + + @Override + public BytesValue encode() { + final MutableBytesValue bytes = MutableBytesValue.create(MESSAGE_LENGTH); + signature.encodedBytes().copyTo(bytes, 0); + ephPubKeyHash.copyTo(bytes, SIGNATURE_LENGTH); + pubKey.getEncodedBytes().copyTo(bytes, SIGNATURE_LENGTH + HASH_EPH_PUBKEY_LENGTH); + nonce.copyTo(bytes, SIGNATURE_LENGTH + HASH_EPH_PUBKEY_LENGTH + PUBKEY_LENGTH); + bytes.set(MESSAGE_LENGTH - 1, (byte) (token ? 0x01 : 0x00)); + return bytes; + } + + @Override + public SECP256K1.PublicKey getPubKey() { + return pubKey; + } + + @Override + public Bytes32 getEphPubKeyHash() { + return ephPubKeyHash; + } + + @Override + public Bytes32 getNonce() { + return nonce; + } + + @Override + public SECP256K1.PublicKey getEphPubKey() { + return ephPubKey; + } + + @Override + public String toString() { + return "InitiatorHandshakeMessage{" + + "pubKey=" + + pubKey + + ", signature=" + + signature + + ", ephPubKey=" + + ephPubKey + + ", ephPubKeyHash=" + + ephPubKeyHash + + ", nonce=" + + nonce + + ", token=" + + token + + '}'; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java new file mode 100755 index 00000000000..c2d6da3f066 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4.java @@ -0,0 +1,101 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.Bytes32s; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytes32; + +public final class InitiatorHandshakeMessageV4 implements InitiatorHandshakeMessage { + + public static final int VERSION = 4; + + private final SECP256K1.PublicKey pubKey; + private final SECP256K1.Signature signature; + private final SECP256K1.PublicKey ephPubKey; + private final Bytes32 ephPubKeyHash; + private final Bytes32 nonce; + + public static InitiatorHandshakeMessageV4 create( + final SECP256K1.PublicKey ourPubKey, + final SECP256K1.KeyPair ephKeyPair, + final Bytes32 staticSharedSecret, + final Bytes32 nonce) { + final MutableBytes32 toSign = MutableBytes32.create(); + Bytes32s.xor(staticSharedSecret, nonce, toSign); + return new InitiatorHandshakeMessageV4( + ourPubKey, SECP256K1.sign(toSign, ephKeyPair), ephKeyPair.getPublicKey(), nonce); + } + + /** + * Decodes this message. + * + * @param bytes The raw bytes. + * @param keyPair Our keypair to calculat ECDH key agreements. + * @return The decoded message. + */ + public static InitiatorHandshakeMessageV4 decode( + final BytesValue bytes, final SECP256K1.KeyPair keyPair) { + final RLPInput input = new BytesValueRLPInput(bytes, true); + input.enterList(); + final SECP256K1.Signature signature = SECP256K1.Signature.decode(input.readBytesValue()); + final SECP256K1.PublicKey pubKey = SECP256K1.PublicKey.create(input.readBytesValue()); + final Bytes32 nonce = input.readBytes32(); + final Bytes32 staticSharedSecret = + SECP256K1.calculateKeyAgreement(keyPair.getPrivateKey(), pubKey); + final Bytes32 toSign = Bytes32s.xor(staticSharedSecret, nonce); + final SECP256K1.PublicKey ephPubKey = + SECP256K1.PublicKey.recoverFromSignature(toSign, signature) + .orElseThrow(() -> new RuntimeException("Could not recover public key from signature")); + + return new InitiatorHandshakeMessageV4(pubKey, signature, ephPubKey, nonce); + } + + private InitiatorHandshakeMessageV4( + final SECP256K1.PublicKey pubKey, + final SECP256K1.Signature signature, + final SECP256K1.PublicKey ephPubKey, + final Bytes32 nonce) { + this.pubKey = pubKey; + this.signature = signature; + this.ephPubKey = ephPubKey; + this.ephPubKeyHash = Hash.keccak256(ephPubKey.getEncodedBytes()); + this.nonce = nonce; + } + + @Override + public BytesValue encode() { + final BytesValueRLPOutput temp = new BytesValueRLPOutput(); + temp.startList(); + temp.writeBytesValue(signature.encodedBytes()); + temp.writeBytesValue(pubKey.getEncodedBytes()); + temp.writeBytesValue(nonce); + temp.writeIntScalar(VERSION); + temp.endList(); + return temp.encoded(); + } + + @Override + public Bytes32 getNonce() { + return nonce; + } + + @Override + public SECP256K1.PublicKey getPubKey() { + return pubKey; + } + + @Override + public SECP256K1.PublicKey getEphPubKey() { + return ephPubKey; + } + + @Override + public Bytes32 getEphPubKeyHash() { + return ephPubKeyHash; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessage.java new file mode 100755 index 00000000000..9939b5353f8 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessage.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public interface ResponderHandshakeMessage { + + SECP256K1.PublicKey getEphPublicKey(); + + Bytes32 getNonce(); + + BytesValue encode(); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV1.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV1.java new file mode 100755 index 00000000000..c6cd6148c1e --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV1.java @@ -0,0 +1,93 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.NONCE_LENGTH; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.PUBKEY_LENGTH; +import static net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies.ECIESHandshaker.TOKEN_FLAG_LENGTH; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +/** + * The responder's handshake message. + * + *

This message must be sent by the party who responded to the RLPX connection, in response to + * the initiator message. + * + *

Message structure

+ * + * The following describes the message structure: + * + *
+ *   authRecipient -> E(remote-pubk,
+ *                      remote-ephemeral-pubk
+ *                      || nonce
+ *                      || 0x0)
+ * 
+ * + * @see Structure of the + * responder response + */ +public final class ResponderHandshakeMessageV1 implements ResponderHandshakeMessage { + public static final int MESSAGE_LENGTH = PUBKEY_LENGTH + NONCE_LENGTH + TOKEN_FLAG_LENGTH; + + private final SECP256K1.PublicKey ephPublicKey; // 64 bytes - uncompressed and no type byte + private final Bytes32 nonce; // 32 bytes + private final boolean token; // 1 byte - 0x00 or 0x01 + + private ResponderHandshakeMessageV1( + final SECP256K1.PublicKey ephPublicKey, final Bytes32 nonce, final boolean token) { + this.ephPublicKey = ephPublicKey; + this.nonce = nonce; + this.token = token; + } + + public static ResponderHandshakeMessageV1 create( + final SECP256K1.PublicKey ephPublicKey, final Bytes32 nonce, final boolean token) { + return new ResponderHandshakeMessageV1(ephPublicKey, nonce, token); + } + + public static ResponderHandshakeMessageV1 decode(final BytesValue bytes) { + checkArgument(bytes.size() == MESSAGE_LENGTH); + + final BytesValue pubk = bytes.slice(0, PUBKEY_LENGTH); + final SECP256K1.PublicKey ephPubKey = SECP256K1.PublicKey.create(pubk); + final Bytes32 nonce = Bytes32.wrap(bytes.slice(PUBKEY_LENGTH, NONCE_LENGTH), 0); + final boolean token = bytes.get(PUBKEY_LENGTH + NONCE_LENGTH) == 0x01; + return new ResponderHandshakeMessageV1(ephPubKey, nonce, token); + } + + @Override + public BytesValue encode() { + final MutableBytesValue bytes = MutableBytesValue.create(MESSAGE_LENGTH); + ephPublicKey.getEncodedBytes().copyTo(bytes, 0); + nonce.copyTo(bytes, PUBKEY_LENGTH); + BytesValue.of((byte) (token ? 0x01 : 0x00)).copyTo(bytes, PUBKEY_LENGTH + NONCE_LENGTH); + return bytes; + } + + @Override + public SECP256K1.PublicKey getEphPublicKey() { + return ephPublicKey; + } + + @Override + public Bytes32 getNonce() { + return nonce; + } + + @Override + public String toString() { + return "ResponderHandshakeMessage{" + + "ephPublicKey=" + + ephPublicKey + + ", nonce=" + + nonce + + ", token=" + + token + + '}'; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV4.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV4.java new file mode 100755 index 00000000000..49a7678f1c7 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV4.java @@ -0,0 +1,53 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPInput; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public class ResponderHandshakeMessageV4 implements ResponderHandshakeMessage { + + private final SECP256K1.PublicKey ephPublicKey; + + private final Bytes32 nonce; + + public static ResponderHandshakeMessageV4 create( + final SECP256K1.PublicKey ephPublicKey, final Bytes32 nonce) { + return new ResponderHandshakeMessageV4(ephPublicKey, nonce); + } + + public static ResponderHandshakeMessageV4 decode(final BytesValue raw) { + final RLPInput input = new BytesValueRLPInput(raw, true); + input.enterList(); + return new ResponderHandshakeMessageV4( + SECP256K1.PublicKey.create(input.readBytesValue()), input.readBytes32()); + } + + private ResponderHandshakeMessageV4(final SECP256K1.PublicKey ephPublicKey, final Bytes32 nonce) { + this.ephPublicKey = ephPublicKey; + this.nonce = nonce; + } + + @Override + public SECP256K1.PublicKey getEphPublicKey() { + return ephPublicKey; + } + + @Override + public Bytes32 getNonce() { + return nonce; + } + + @Override + public BytesValue encode() { + final BytesValueRLPOutput temp = new BytesValueRLPOutput(); + temp.startList(); + temp.writeBytesValue(ephPublicKey.getEncodedBytes()); + temp.writeBytesValue(nonce); + temp.writeIntScalar(InitiatorHandshakeMessageV4.VERSION); + temp.endList(); + return temp.encoded(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/utils/ByteBufUtils.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/utils/ByteBufUtils.java new file mode 100755 index 00000000000..36f235e6ec2 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/utils/ByteBufUtils.java @@ -0,0 +1,39 @@ +package net.consensys.pantheon.ethereum.p2p.utils; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; + +import io.netty.buffer.ByteBuf; + +/** Utility methods for working with {@link ByteBuf}'s. */ +public class ByteBufUtils { + + private ByteBufUtils() {} + + public static byte[] toByteArray(final ByteBuf buffer) { + final byte[] bytes = new byte[buffer.readableBytes()]; + buffer.getBytes(buffer.readerIndex(), bytes); + return bytes; + } + + /** + * Creates an {@link RLPInput} for the data in buffer. The data is copied from + * buffer so that the {@link RLPInput} and any data read from it are safe to use even after + * buffer is released. + * + * @param buffer the data to read as RLP + * @return an {@link RLPInput} for the data in buffer + */ + public static RLPInput toRLPInput(final ByteBuf buffer) { + return RLP.input(BytesValue.wrap(toByteArray(buffer))); + } + + public static ByteBuf fromRLPOutput(final BytesValueRLPOutput out) { + final ByteBuf data = NetworkMemoryPool.allocate(out.encodedSize()); + data.writeBytes(out.encoded().extractArray()); + return data; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/AbstractMessageData.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/AbstractMessageData.java new file mode 100755 index 00000000000..3c249dbbe24 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/AbstractMessageData.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; + +import io.netty.buffer.ByteBuf; + +public abstract class AbstractMessageData implements MessageData { + + protected final ByteBuf data; + + protected AbstractMessageData(final ByteBuf data) { + this.data = data; + } + + @Override + public final int getSize() { + return data.readableBytes(); + } + + @Override + public final void writeTo(final ByteBuf output) { + data.markReaderIndex(); + output.writeBytes(data); + data.resetReaderIndex(); + } + + @Override + public final void release() { + data.release(); + } + + @Override + public final void retain() { + data.retain(); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/Capability.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/Capability.java new file mode 100755 index 00000000000..7ccde0633b1 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/Capability.java @@ -0,0 +1,85 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +import static net.consensys.pantheon.util.bytes.BytesValue.wrap; + +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Represents a client capability. + * + * @see Capability wire + * format + */ +public class Capability { + private static final Logger LOGGER = LogManager.getLogger(Capability.class); + private final String name; + private final int version; + + private Capability(final String name, final int version) { + // Quorum reports IBFT as "istanbul", breaking wire protocol conventions. + // As such, this check cannot prevent connection. + if (name.length() != 3) { + LOGGER.warn("Capability name '{}' is too long", name); + } + this.name = name; + this.version = version; + } + + public static Capability create(final String name, final int version) { + return new Capability(name, version); + } + + public String getName() { + return name; + } + + public int getVersion() { + return version; + } + + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeBytesValue(wrap(getName().getBytes(StandardCharsets.US_ASCII))); + out.writeUnsignedByte(getVersion()); + out.endList(); + } + + public static Capability readFrom(final RLPInput in) { + in.enterList(); + final String name = in.readBytesValue(BytesValues::asString); + final int version = in.readUnsignedByte(); + in.leaveList(); + return Capability.create(name, version); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Capability that = (Capability) o; + return version == that.version && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, version); + } + + @Override + public String toString() { + return name + "/" + version; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/DefaultMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/DefaultMessage.java new file mode 100755 index 00000000000..b273173ede5 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/DefaultMessage.java @@ -0,0 +1,31 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +import net.consensys.pantheon.ethereum.p2p.api.Message; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; + +/** + * Simple implementation of {@link Message} that associates a {@link MessageData} instance with a + * {@link PeerConnection}. + */ +public final class DefaultMessage implements Message { + + private final MessageData data; + + private final PeerConnection connection; + + public DefaultMessage(final PeerConnection channel, final MessageData data) { + this.connection = channel; + this.data = data; + } + + @Override + public PeerConnection getConnection() { + return connection; + } + + @Override + public MessageData getData() { + return data; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/PeerInfo.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/PeerInfo.java new file mode 100755 index 00000000000..a306c0639b9 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/PeerInfo.java @@ -0,0 +1,129 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +import static net.consensys.pantheon.util.bytes.BytesValue.wrap; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import io.netty.buffer.ByteBuf; + +/** + * Encapsulates information about a peer, including their protocol version, client ID, capabilities + * and other. + * + *

The peer info is shared between peers during the HELLO wire protocol handshake. + */ +public class PeerInfo { + private final int version; + private final String clientId; + private final List capabilities; + private final int port; + private final BytesValue nodeId; + + public PeerInfo( + final int version, + final String clientId, + final List capabilities, + final int port, + final BytesValue nodeId) { + this.version = version; + this.clientId = clientId; + this.capabilities = capabilities; + this.port = port; + this.nodeId = nodeId; + } + + public static PeerInfo readFrom(final RLPInput in) { + in.enterList(); + final int version = in.readUnsignedByte(); + final String clientId = in.readBytesValue(BytesValues::asString); + final List caps = + in.nextIsNull() ? Collections.emptyList() : in.readList(Capability::readFrom); + final int port = in.readIntScalar(); + final BytesValue nodeId = in.readBytesValue(); + in.leaveList(true); + return new PeerInfo(version, clientId, caps, port, nodeId); + } + + public int getVersion() { + return version; + } + + public String getClientId() { + return clientId; + } + + public List getCapabilities() { + return capabilities; + } + + public int getPort() { + return port; + } + + public BytesValue getNodeId() { + return nodeId; + } + + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeUnsignedByte(getVersion()); + out.writeBytesValue(wrap(getClientId().getBytes(StandardCharsets.UTF_8))); + out.writeList(getCapabilities(), Capability::writeTo); + out.writeIntScalar(getPort()); + out.writeBytesValue(getNodeId()); + out.endList(); + } + + public ByteBuf toByteBuf() { + // TODO: we should have a RLPOutput type based on ByteBuf + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + writeTo(out); + + final ByteBuf data = NetworkMemoryPool.allocate(out.encodedSize()); + data.writeBytes(out.encoded().extractArray()); + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("PeerInfo{"); + sb.append("version=").append(version); + sb.append(", clientId='").append(clientId).append('\''); + sb.append(", capabilities=").append(capabilities); + sb.append(", port=").append(port); + sb.append(", nodeId=").append(nodeId); + sb.append('}'); + return sb.toString(); + } + + @Override + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof PeerInfo)) { + return false; + } + final PeerInfo peerInfo = (PeerInfo) o; + return version == peerInfo.version + && port == peerInfo.port + && Objects.equals(clientId, peerInfo.clientId) + && Objects.equals(capabilities, peerInfo.capabilities) + && Objects.equals(nodeId, peerInfo.nodeId); + } + + @Override + public int hashCode() { + return Objects.hash(version, clientId, capabilities, port, nodeId); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/RawMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/RawMessage.java new file mode 100755 index 00000000000..598122b453a --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/RawMessage.java @@ -0,0 +1,18 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +import io.netty.buffer.ByteBuf; + +public final class RawMessage extends AbstractMessageData { + + private final int code; + + public RawMessage(final int code, final ByteBuf data) { + super(data); + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/SubProtocol.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/SubProtocol.java new file mode 100755 index 00000000000..c0ffe339a93 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/SubProtocol.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +public interface SubProtocol { + + /** + * Returns the 3 character ascii name of this Wire Sub-protocol. + * + * @return the name of this sub-protocol + */ + String getName(); + + /** + * The number of message codes to reserve for the given version of this sub-protocol. + * + * @param protocolVersion the version of the protocol + * @return the number of reserved message codes in the given version of the sub-protocol + */ + int messageSpace(int protocolVersion); + + /** + * Returns true if the given protocol version supports the given message code. + * + * @param protocolVersion the version of the protocol + * @param code the message code to check + * @return true if the given protocol version supports the given message code + */ + boolean isValidMessageCode(int protocolVersion, int code); +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/WireProtocolException.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/WireProtocolException.java new file mode 100755 index 00000000000..bd577c52ee3 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/WireProtocolException.java @@ -0,0 +1,13 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +/** Signals that an exception occurred in the Wire protocol layer of the RLPx stack. */ +public class WireProtocolException extends RuntimeException { + + public WireProtocolException(final String message) { + super(message); + } + + public WireProtocolException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/DisconnectMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/DisconnectMessage.java new file mode 100755 index 00000000000..5126c5b1729 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/DisconnectMessage.java @@ -0,0 +1,137 @@ +package net.consensys.pantheon.ethereum.p2p.wire.messages; + +import static com.google.common.base.Preconditions.checkArgument; +import static net.consensys.pantheon.util.Preconditions.checkGuard; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.utils.ByteBufUtils; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.p2p.wire.WireProtocolException; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPOutput; + +import java.util.stream.Stream; + +import io.netty.buffer.ByteBuf; + +public final class DisconnectMessage extends AbstractMessageData { + + private DisconnectMessage(final ByteBuf data) { + super(data); + } + + public static DisconnectMessage create(final DisconnectReason reason) { + final Data data = new Data(reason); + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + data.writeTo(out); + final ByteBuf buf = ByteBufUtils.fromRLPOutput(out); + + return new DisconnectMessage(buf); + } + + public static DisconnectMessage readFrom(final MessageData message) { + if (message instanceof DisconnectMessage) { + message.retain(); + return (DisconnectMessage) message; + } + final int code = message.getCode(); + if (code != WireMessageCodes.DISCONNECT) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a DisconnectMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new DisconnectMessage(data); + } + + @Override + public int getCode() { + return WireMessageCodes.DISCONNECT; + } + + public DisconnectReason getReason() { + return Data.readFrom(ByteBufUtils.toRLPInput(data)).getReason(); + } + + @Override + public String toString() { + return "DisconnectMessage{" + "data=" + data + '}'; + } + + public static class Data { + private final DisconnectReason reason; + + public Data(final DisconnectReason reason) { + this.reason = reason; + } + + public void writeTo(final RLPOutput out) { + out.startList(); + out.writeByte(reason.getValue()); + out.endList(); + } + + public static Data readFrom(final RLPInput in) { + final int size = in.enterList(); + checkGuard(size == 1, WireProtocolException::new, "Expected list size 1, got: %s", size); + final DisconnectReason reason = DisconnectReason.forCode(in.readByte()); + in.leaveList(); + + return new Data(reason); + } + + public DisconnectReason getReason() { + return reason; + } + } + + /** + * Reasons for disconnection, modelled as specified in the wire protocol DISCONNECT message. + * + * @see ÐΞVp2p Wire + * Protocol + */ + public static enum DisconnectReason { + REQUESTED((byte) 0x00), + TCP_SUBSYSTEM_ERROR((byte) 0x01), + BREACH_OF_PROTOCOL((byte) 0x02), + USELESS_PEER((byte) 0x03), + TOO_MANY_PEERS((byte) 0x04), + ALREADY_CONNECTED((byte) 0x05), + INCOMPATIBLE_P2P_PROTOCOL_VERSION((byte) 0x06), + NULL_NODE_ID((byte) 0x07), + CLIENT_QUITTING((byte) 0x08), + UNEXPECTED_ID((byte) 0x09), + LOCAL_IDENTITY((byte) 0x0a), + TIMEOUT((byte) 0x0b), + SUBPROTOCOL_TRIGGERED((byte) 0x10); + + private static final DisconnectReason[] BY_ID; + private final byte code; + + static { + final int maxValue = + Stream.of(DisconnectReason.values()).mapToInt(dr -> (int) dr.getValue()).max().getAsInt(); + BY_ID = new DisconnectReason[maxValue + 1]; + Stream.of(DisconnectReason.values()).forEach(dr -> BY_ID[dr.getValue()] = dr); + } + + public static DisconnectReason forCode(final byte taintedCode) { + final byte code = (byte) (taintedCode & 0xff); + checkArgument(code < BY_ID.length, "unrecognized disconnect reason"); + final DisconnectReason reason = BY_ID[code]; + checkArgument(reason != null, "unrecognized disconnect reason"); + return reason; + } + + DisconnectReason(final byte code) { + this.code = code; + } + + public byte getValue() { + return code; + } + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/EmptyMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/EmptyMessage.java new file mode 100755 index 00000000000..441d47749e5 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/EmptyMessage.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.p2p.wire.messages; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; + +import io.netty.buffer.ByteBuf; + +/** A message without a body. */ +abstract class EmptyMessage implements MessageData { + + @Override + public final int getSize() { + return 0; + } + + @Override + public final void writeTo(final ByteBuf output) {} + + @Override + public final void release() {} + + @Override + public final void retain() {} +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/HelloMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/HelloMessage.java new file mode 100755 index 00000000000..0fc89fe6f77 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/HelloMessage.java @@ -0,0 +1,58 @@ +package net.consensys.pantheon.ethereum.p2p.wire.messages; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.utils.ByteBufUtils; +import net.consensys.pantheon.ethereum.p2p.wire.AbstractMessageData; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; + +import io.netty.buffer.ByteBuf; + +public final class HelloMessage extends AbstractMessageData { + + private HelloMessage(final ByteBuf data) { + super(data); + } + + public static HelloMessage create(final PeerInfo peerInfo) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + peerInfo.writeTo(out); + final ByteBuf buf = ByteBufUtils.fromRLPOutput(out); + + return new HelloMessage(buf); + } + + public static HelloMessage create(final ByteBuf data) { + return new HelloMessage(data); + } + + public static HelloMessage readFrom(final MessageData message) { + if (message instanceof HelloMessage) { + message.retain(); + return (HelloMessage) message; + } + final int code = message.getCode(); + if (code != WireMessageCodes.HELLO) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a HelloMessage.", code)); + } + final ByteBuf data = NetworkMemoryPool.allocate(message.getSize()); + message.writeTo(data); + return new HelloMessage(data); + } + + @Override + public int getCode() { + return WireMessageCodes.HELLO; + } + + public PeerInfo getPeerInfo() { + return PeerInfo.readFrom(ByteBufUtils.toRLPInput(data)); + } + + @Override + public String toString() { + return "HelloMessage{" + "data=" + data + '}'; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PingMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PingMessage.java new file mode 100755 index 00000000000..2aa42839abe --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PingMessage.java @@ -0,0 +1,22 @@ +package net.consensys.pantheon.ethereum.p2p.wire.messages; + +public final class PingMessage extends EmptyMessage { + + private static final PingMessage INSTANCE = new PingMessage(); + + public static PingMessage get() { + return INSTANCE; + } + + private PingMessage() {} + + @Override + public int getCode() { + return WireMessageCodes.PING; + } + + @Override + public String toString() { + return "PingMessage{data=''}"; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PongMessage.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PongMessage.java new file mode 100755 index 00000000000..74455b15ef0 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/PongMessage.java @@ -0,0 +1,17 @@ +package net.consensys.pantheon.ethereum.p2p.wire.messages; + +public final class PongMessage extends EmptyMessage { + + private static final PongMessage INSTANCE = new PongMessage(); + + public static PongMessage get() { + return INSTANCE; + } + + private PongMessage() {} + + @Override + public int getCode() { + return WireMessageCodes.PONG; + } +} diff --git a/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/WireMessageCodes.java b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/WireMessageCodes.java new file mode 100755 index 00000000000..c906fd79085 --- /dev/null +++ b/ethereum/p2p/src/main/java/net/consensys/pantheon/ethereum/p2p/wire/messages/WireMessageCodes.java @@ -0,0 +1,10 @@ +package net.consensys.pantheon.ethereum.p2p.wire.messages; + +public final class WireMessageCodes { + public static final int HELLO = 0x00; + public static final int DISCONNECT = 0x01; + public static final int PING = 0x02; + public static final int PONG = 0x03; + + private WireMessageCodes() {} +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java new file mode 100755 index 00000000000..69b0b9cc501 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NettyP2PNetworkTest.java @@ -0,0 +1,393 @@ +package net.consensys.pantheon.ethereum.p2p; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.NetworkingConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.RlpxConfiguration; +import net.consensys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; +import net.consensys.pantheon.ethereum.p2p.netty.exceptions.IncompatiblePeerException; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.net.InetAddress; +import java.util.List; +import java.util.OptionalInt; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import org.junit.After; +import org.junit.Test; + +/** Tests for {@link NettyP2PNetwork}. */ +public final class NettyP2PNetworkTest { + + private final Vertx vertx = Vertx.vertx(); + + @After + public void closeVertx() { + vertx.close(); + } + + @Test + public void handshaking() throws Exception { + final DiscoveryConfiguration noDiscovery = DiscoveryConfiguration.create().setActive(false); + final SECP256K1.KeyPair listenKp = SECP256K1.KeyPair.generate(); + final Capability cap = Capability.create("eth", 63); + try (final P2PNetwork listener = + new NettyP2PNetwork( + vertx, + listenKp, + NetworkingConfiguration.create() + .setDiscovery(noDiscovery) + .setSupportedProtocols(subProtocol()) + .setRlpx(RlpxConfiguration.create().setBindPort(0)), + singletonList(cap), + () -> false, + new PeerBlacklist()); + final P2PNetwork connector = + new NettyP2PNetwork( + vertx, + SECP256K1.KeyPair.generate(), + NetworkingConfiguration.create() + .setSupportedProtocols(subProtocol()) + .setRlpx(RlpxConfiguration.create().setBindPort(0)) + .setDiscovery(noDiscovery), + singletonList(cap), + () -> false, + new PeerBlacklist())) { + + final int listenPort = listener.getSelf().getPort(); + listener.run(); + connector.run(); + final BytesValue listenId = listenKp.getPublicKey().getEncodedBytes(); + assertThat( + connector + .connect( + new DefaultPeer( + listenId, + new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), + listenPort, + OptionalInt.of(listenPort)))) + .get(30L, TimeUnit.SECONDS) + .getPeer() + .getNodeId()) + .isEqualTo(listenId); + } + } + + @Test + public void preventMultipleConnections() throws Exception { + + final DiscoveryConfiguration noDiscovery = DiscoveryConfiguration.create().setActive(false); + final SECP256K1.KeyPair listenKp = SECP256K1.KeyPair.generate(); + final List capabilities = singletonList(Capability.create("eth", 62)); + final SubProtocol subProtocol = subProtocol(); + try (final P2PNetwork listener = + new NettyP2PNetwork( + vertx, + listenKp, + NetworkingConfiguration.create() + .setSupportedProtocols(subProtocol) + .setDiscovery(noDiscovery) + .setRlpx(RlpxConfiguration.create().setBindPort(0)), + capabilities, + () -> true, + new PeerBlacklist()); + final P2PNetwork connector = + new NettyP2PNetwork( + vertx, + SECP256K1.KeyPair.generate(), + NetworkingConfiguration.create() + .setSupportedProtocols(subProtocol) + .setRlpx(RlpxConfiguration.create().setBindPort(0)) + .setDiscovery(noDiscovery), + capabilities, + () -> true, + new PeerBlacklist())) { + final int listenPort = listener.getSelf().getPort(); + listener.run(); + connector.run(); + final BytesValue listenId = listenKp.getPublicKey().getEncodedBytes(); + assertThat( + connector + .connect( + new DefaultPeer( + listenId, + new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), + listenPort, + OptionalInt.of(listenPort)))) + .get(30L, TimeUnit.SECONDS) + .getPeer() + .getNodeId()) + .isEqualTo(listenId); + final CompletableFuture secondConnectionFuture = + connector.connect( + new DefaultPeer( + listenId, + new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), + listenPort, + OptionalInt.of(listenPort)))); + assertThatThrownBy(secondConnectionFuture::get) + .hasCause(new IllegalStateException("Client already connected")); + } + } + + /** + * Tests that max peers setting is honoured and inbound connections that would exceed the limit + * are correctly disconnected. + * + * @throws Exception On Failure + */ + @Test + public void limitMaxPeers() throws Exception { + final DiscoveryConfiguration noDiscovery = DiscoveryConfiguration.create().setActive(false); + final SECP256K1.KeyPair listenKp = SECP256K1.KeyPair.generate(); + final int maxPeers = 1; + final List cap = singletonList(Capability.create("eth", 62)); + final SubProtocol subProtocol = subProtocol(); + try (final P2PNetwork listener = + new NettyP2PNetwork( + vertx, + listenKp, + NetworkingConfiguration.create() + .setDiscovery(noDiscovery) + .setRlpx(RlpxConfiguration.create().setBindPort(0).setMaxPeers(maxPeers)) + .setSupportedProtocols(subProtocol), + cap, + () -> true, + new PeerBlacklist()); + final P2PNetwork connector1 = + new NettyP2PNetwork( + vertx, + SECP256K1.KeyPair.generate(), + NetworkingConfiguration.create() + .setDiscovery(noDiscovery) + .setRlpx(RlpxConfiguration.create().setBindPort(0)) + .setSupportedProtocols(subProtocol), + cap, + () -> true, + new PeerBlacklist()); + final P2PNetwork connector2 = + new NettyP2PNetwork( + vertx, + SECP256K1.KeyPair.generate(), + NetworkingConfiguration.create() + .setDiscovery(noDiscovery) + .setRlpx(RlpxConfiguration.create().setBindPort(0)) + .setSupportedProtocols(subProtocol), + cap, + () -> true, + new PeerBlacklist())) { + + final int listenPort = listener.getSelf().getPort(); + // Setup listener and first connection + listener.run(); + connector1.run(); + final BytesValue listenId = listenKp.getPublicKey().getEncodedBytes(); + final Peer listeningPeer = + new DefaultPeer( + listenId, + new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), + listenPort, + OptionalInt.of(listenPort))); + assertThat(connector1.connect(listeningPeer).get(30L, TimeUnit.SECONDS).getPeer().getNodeId()) + .isEqualTo(listenId); + + // Setup second connection and check that connection is not accepted + final CompletableFuture peerFuture = new CompletableFuture<>(); + final CompletableFuture reasonFuture = new CompletableFuture<>(); + connector2.subscribeDisconnect( + (peerConnection, reason, initiatedByPeer) -> { + peerFuture.complete(peerConnection); + reasonFuture.complete(reason); + }); + connector2.run(); + assertThat(connector2.connect(listeningPeer).get(30L, TimeUnit.SECONDS).getPeer().getNodeId()) + .isEqualTo(listenId); + assertThat(peerFuture.get(30L, TimeUnit.SECONDS).getPeer().getNodeId()).isEqualTo(listenId); + assertThat(reasonFuture.get(30L, TimeUnit.SECONDS)) + .isEqualByComparingTo(DisconnectReason.TOO_MANY_PEERS); + } + } + + @Test + public void rejectPeerWithNoSharedCaps() throws Exception { + final DiscoveryConfiguration noDiscovery = DiscoveryConfiguration.create().setActive(false); + final SECP256K1.KeyPair listenKp = SECP256K1.KeyPair.generate(); + + final SubProtocol subprotocol1 = subProtocol(); + final Capability cap1 = Capability.create(subprotocol1.getName(), 63); + final SubProtocol subprotocol2 = subProtocol2(); + final Capability cap2 = Capability.create(subprotocol2.getName(), 63); + try (final P2PNetwork listener = + new NettyP2PNetwork( + vertx, + listenKp, + NetworkingConfiguration.create() + .setDiscovery(noDiscovery) + .setSupportedProtocols(subprotocol1) + .setRlpx(RlpxConfiguration.create().setBindPort(0)), + singletonList(cap1), + () -> false, + new PeerBlacklist()); + final P2PNetwork connector = + new NettyP2PNetwork( + vertx, + SECP256K1.KeyPair.generate(), + NetworkingConfiguration.create() + .setSupportedProtocols(subprotocol2) + .setRlpx(RlpxConfiguration.create().setBindPort(0)) + .setDiscovery(noDiscovery), + singletonList(cap2), + () -> false, + new PeerBlacklist())) { + final int listenPort = listener.getSelf().getPort(); + listener.run(); + connector.run(); + final BytesValue listenId = listenKp.getPublicKey().getEncodedBytes(); + + final Peer listenerPeer = + new DefaultPeer( + listenId, + new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), + listenPort, + OptionalInt.of(listenPort))); + final CompletableFuture connectFuture = connector.connect(listenerPeer); + assertThatThrownBy(connectFuture::get).hasCauseInstanceOf(IncompatiblePeerException.class); + } + } + + @Test + public void rejectIncomingConnectionFromBlacklistedPeer() throws Exception { + final DiscoveryConfiguration noDiscovery = DiscoveryConfiguration.create().setActive(false); + final SECP256K1.KeyPair localKp = SECP256K1.KeyPair.generate(); + final SECP256K1.KeyPair remoteKp = SECP256K1.KeyPair.generate(); + final BytesValue localId = localKp.getPublicKey().getEncodedBytes(); + final BytesValue remoteId = remoteKp.getPublicKey().getEncodedBytes(); + PeerBlacklist localBlacklist = new PeerBlacklist(); + PeerBlacklist remoteBlacklist = new PeerBlacklist(); + + final SubProtocol subprotocol = subProtocol(); + final Capability cap = Capability.create(subprotocol.getName(), 63); + try (final P2PNetwork localNetwork = + new NettyP2PNetwork( + vertx, + localKp, + NetworkingConfiguration.create() + .setDiscovery(noDiscovery) + .setSupportedProtocols(subprotocol) + .setRlpx(RlpxConfiguration.create().setBindPort(0)), + singletonList(cap), + () -> false, + localBlacklist); + final P2PNetwork remoteNetwork = + new NettyP2PNetwork( + vertx, + remoteKp, + NetworkingConfiguration.create() + .setSupportedProtocols(subprotocol) + .setRlpx(RlpxConfiguration.create().setBindPort(0)) + .setDiscovery(noDiscovery), + singletonList(cap), + () -> false, + remoteBlacklist)) { + final int localListenPort = localNetwork.getSelf().getPort(); + final int remoteListenPort = remoteNetwork.getSelf().getPort(); + final Peer localPeer = + new DefaultPeer( + localId, + new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), + localListenPort, + OptionalInt.of(localListenPort))); + + final Peer remotePeer = + new DefaultPeer( + remoteId, + new Endpoint( + InetAddress.getLoopbackAddress().getHostAddress(), + remoteListenPort, + OptionalInt.of(remoteListenPort))); + + // Blacklist the remote peer + localBlacklist.add(remotePeer); + + localNetwork.run(); + remoteNetwork.run(); + + // Setup disconnect listener + final CompletableFuture peerFuture = new CompletableFuture<>(); + final CompletableFuture reasonFuture = new CompletableFuture<>(); + remoteNetwork.subscribeDisconnect( + (peerConnection, reason, initiatedByPeer) -> { + peerFuture.complete(peerConnection); + reasonFuture.complete(reason); + }); + + // Remote connect to local + final CompletableFuture connectFuture = remoteNetwork.connect(localPeer); + + // Check connection is made, and then a disconnect is registered at remote + assertThat(connectFuture.get(5L, TimeUnit.SECONDS).getPeer().getNodeId()).isEqualTo(localId); + assertThat(peerFuture.get(5L, TimeUnit.SECONDS).getPeer().getNodeId()).isEqualTo(localId); + assertThat(reasonFuture.get(5L, TimeUnit.SECONDS)) + .isEqualByComparingTo(DisconnectReason.USELESS_PEER); + } + } + + private SubProtocol subProtocol() { + return new SubProtocol() { + @Override + public String getName() { + return "eth"; + } + + @Override + public int messageSpace(final int protocolVersion) { + return 8; + } + + @Override + public boolean isValidMessageCode(final int protocolVersion, final int code) { + return true; + } + }; + } + + private SubProtocol subProtocol2() { + return new SubProtocol() { + @Override + public String getName() { + return "ryj"; + } + + @Override + public int messageSpace(final int protocolVersion) { + return 8; + } + + @Override + public boolean isValidMessageCode(final int protocolVersion, final int code) { + return true; + } + }; + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java new file mode 100755 index 00000000000..b31a53d5ecd --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingServiceLifecycleTest.java @@ -0,0 +1,189 @@ +package net.consensys.pantheon.ethereum.p2p; + +import static java.util.Collections.emptyList; +import static net.consensys.pantheon.ethereum.p2p.NetworkingTestHelper.configWithRandomPorts; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.P2PNetwork; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.NetworkingConfiguration; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryServiceException; +import net.consensys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; + +import java.io.IOException; + +import io.vertx.core.Vertx; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Test; + +public class NetworkingServiceLifecycleTest { + + private final Vertx vertx = Vertx.vertx(); + + @After + public void closeVertx() { + vertx.close(); + } + + @Test + public void createPeerDiscoveryAgent() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + try (NettyP2PNetwork service = + new NettyP2PNetwork( + vertx, + keyPair, + configWithRandomPorts(), + emptyList(), + () -> true, + new PeerBlacklist())) { + service.run(); + final int port = service.getDiscoverySocketAddress().getPort(); + + assertEquals("/0.0.0.0:" + port, service.getDiscoverySocketAddress().toString()); + assertThat(service.getDiscoveryPeers()).hasSize(0); + } + } + + @Test(expected = IllegalArgumentException.class) + public void createPeerDiscoveryAgent_NullHost() throws IOException { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + final NetworkingConfiguration config = + NetworkingConfiguration.create() + .setDiscovery(DiscoveryConfiguration.create().setBindHost(null)); + try (P2PNetwork broken = + new NettyP2PNetwork(vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + Assertions.fail("Expected Exception"); + } + } + + @Test(expected = IllegalArgumentException.class) + public void createPeerDiscoveryAgent_InvalidHost() throws IOException { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + final NetworkingConfiguration config = + NetworkingConfiguration.create() + .setDiscovery(DiscoveryConfiguration.create().setBindHost("fake.fake.fake")); + try (P2PNetwork broken = + new NettyP2PNetwork(vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + Assertions.fail("Expected Exception"); + } + } + + @Test(expected = IllegalArgumentException.class) + public void createPeerDiscoveryAgent_InvalidPort() throws IOException { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + final NetworkingConfiguration config = + NetworkingConfiguration.create() + .setDiscovery(DiscoveryConfiguration.create().setBindPort(-1)); + try (P2PNetwork broken = + new NettyP2PNetwork(vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + Assertions.fail("Expected Exception"); + } + } + + @Test(expected = IllegalArgumentException.class) + public void createPeerDiscoveryAgent_NullKeyPair() throws IOException { + try (P2PNetwork broken = + new NettyP2PNetwork( + vertx, null, configWithRandomPorts(), emptyList(), () -> true, new PeerBlacklist())) { + Assertions.fail("Expected Exception"); + } + } + + @Test + public void startStopPeerDiscoveryAgent() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + try (NettyP2PNetwork service = + new NettyP2PNetwork( + vertx, + keyPair, + configWithRandomPorts(), + emptyList(), + () -> true, + new PeerBlacklist())) { + service.run(); + service.stop(); + service.run(); + } + } + + @Test + public void startDiscoveryAgentBackToBack() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + try (NettyP2PNetwork service1 = + new NettyP2PNetwork( + vertx, + keyPair, + configWithRandomPorts(), + emptyList(), + () -> true, + new PeerBlacklist()); + NettyP2PNetwork service2 = + new NettyP2PNetwork( + vertx, + keyPair, + configWithRandomPorts(), + emptyList(), + () -> true, + new PeerBlacklist())) { + service1.run(); + service1.stop(); + service2.run(); + service2.stop(); + } + } + + @Test + public void startDiscoveryPortInUse() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + try (NettyP2PNetwork service1 = + new NettyP2PNetwork( + vertx, + keyPair, + configWithRandomPorts(), + emptyList(), + () -> true, + new PeerBlacklist())) { + service1.run(); + final NetworkingConfiguration config = configWithRandomPorts(); + config.getDiscovery().setBindPort(service1.getDiscoverySocketAddress().getPort()); + try (NettyP2PNetwork service2 = + new NettyP2PNetwork( + vertx, keyPair, config, emptyList(), () -> true, new PeerBlacklist())) { + try { + service2.run(); + } catch (final Exception e) { + assertThat(e.getCause()).hasCauseExactlyInstanceOf(PeerDiscoveryServiceException.class); + assertThat(e.getCause()) + .hasMessageStartingWith( + "net.consensys.pantheon.ethereum.p2p.discovery." + + "PeerDiscoveryServiceException: Failed to bind Ethereum UDP discovery listener to 0.0.0.0:"); + assertThat(e).hasMessageContaining("Address already in use"); + } finally { + service1.stop(); + service2.stop(); + } + } + } + } + + @Test + public void createPeerDiscoveryAgent_NoActivePeers() { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + try (NettyP2PNetwork agent = + new NettyP2PNetwork( + vertx, + keyPair, + configWithRandomPorts(), + emptyList(), + () -> true, + new PeerBlacklist())) { + assertTrue(agent.getDiscoveryPeers().isEmpty()); + assertEquals(0, agent.getPeers().size()); + } + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingTestHelper.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingTestHelper.java new file mode 100755 index 00000000000..37b7a808a09 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/NetworkingTestHelper.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.ethereum.p2p; + +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.NetworkingConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.RlpxConfiguration; + +public class NetworkingTestHelper { + + public static NetworkingConfiguration configWithRandomPorts() { + return NetworkingConfiguration.create() + .setRlpx(RlpxConfiguration.create().setBindPort(0)) + .setDiscovery(DiscoveryConfiguration.create().setBindPort(0)); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/AbstractPeerDiscoveryTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/AbstractPeerDiscoveryTest.java new file mode 100755 index 00000000000..bc2156b1047 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/AbstractPeerDiscoveryTest.java @@ -0,0 +1,273 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static io.vertx.core.Vertx.vertx; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketType; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.vertx.core.Vertx; +import io.vertx.core.datagram.DatagramSocket; +import junit.framework.AssertionFailedError; +import org.junit.After; + +/** + * A test class you can extend to acquire the ability to easily start discovery agents with a + * generated Peer and keypair, as well as test sockets to communicate with those discovery agents. + * + *

Call {@link #startDiscoveryAgent(List)} and variants to start one or more discovery agents, or + * {@link #startTestSocket()} or variants to start one or more test sockets. The lifecycle of those + * objects is managed automatically for you via @Before and @After hooks, so you don't need to worry + * about starting and stopping. + */ +public abstract class AbstractPeerDiscoveryTest { + private static final String LOOPBACK_IP_ADDR = "127.0.0.1"; + private static final int TEST_SOCKET_START_TIMEOUT_SECS = 5; + private static final int BROADCAST_TCP_PORT = 12356; + private final Vertx vertx = vertx(); + + List discoveryTestSockets = new CopyOnWriteArrayList<>(); + List agents = new CopyOnWriteArrayList<>(); + + @After + public void stopServices() { + // Close all sockets, will bubble up exceptions. + final CompletableFuture[] completions = + discoveryTestSockets + .stream() + .filter(p -> p.getSocket() != null) + .map( + p -> { + final CompletableFuture completion = new CompletableFuture<>(); + p.getSocket() + .close( + ar -> { + if (ar.succeeded()) { + completion.complete(null); + } else { + completion.completeExceptionally(ar.cause()); + } + }); + return completion; + }) + .toArray(CompletableFuture[]::new); + try { + CompletableFuture.allOf(completions).join(); + } finally { + agents.forEach(PeerDiscoveryAgent::stop); + vertx.close(); + } + } + + /** + * Starts multiple discovery agents with the provided boostrap peers. + * + * @param count the number of agents to start + * @param bootstrapPeers the list of bootstrap peers + * @return a list of discovery agents. + */ + protected List startDiscoveryAgents( + final int count, final List bootstrapPeers) { + return Stream.generate(() -> startDiscoveryAgent(bootstrapPeers)) + .limit(count) + .collect(Collectors.toList()); + } + + /** + * Start a single discovery agent with the provided bootstrap peers. + * + * @param bootstrapPeers the list of bootstrap peers + * @return a list of discovery agents. + */ + protected PeerDiscoveryAgent startDiscoveryAgent(final List bootstrapPeers) { + return startDiscoveryAgent(bootstrapPeers, new PeerBlacklist()); + } + + /** + * Start a single discovery agent with the provided bootstrap peers. + * + * @param bootstrapPeers the list of bootstrap peers + * @param blacklist the peer blacklist + * @return a list of discovery agents. + */ + protected PeerDiscoveryAgent startDiscoveryAgent( + final List bootstrapPeers, final PeerBlacklist blacklist) { + final DiscoveryConfiguration config = new DiscoveryConfiguration(); + config.setBootstrapPeers(bootstrapPeers); + config.setBindPort(0); + + return startDiscoveryAgent(config, blacklist); + } + + protected PeerDiscoveryAgent startDiscoveryAgent( + final DiscoveryConfiguration config, final PeerBlacklist blacklist) { + final PeerDiscoveryAgent agent = + new PeerDiscoveryAgent(vertx, SECP256K1.KeyPair.generate(), config, () -> true, blacklist); + try { + agent.start(BROADCAST_TCP_PORT).get(5, TimeUnit.SECONDS); + } catch (final Exception ex) { + throw new AssertionError("Could not initialize discovery agent", ex); + } + agents.add(agent); + return agent; + } + + /** + * Start multiple test sockets. + * + *

A test socket allows you to send messages to a discovery agent, as well as to react to + * received messages. A test socket encapsulates: (1) a {@link DiscoveryPeer} and its {@link + * net.consensys.pantheon.crypto.SECP256K1.KeyPair}, (2) an {@link ArrayBlockingQueue} where + * received messages are placed automatically, and (3) the socket itself. + * + * @param count the number of test sockets to start. + * @return the test sockets. + */ + protected List startTestSockets(final int count) { + return Stream.generate(this::startTestSocket).limit(count).collect(Collectors.toList()); + } + + /** + * Starts a single test socket. + * + * @return the test socket + */ + protected DiscoveryTestSocket startTestSocket() { + final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(100); + + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + final BytesValue peerId = keyPair.getPublicKey().getEncodedBytes(); + + final CompletableFuture result = new CompletableFuture<>(); + // Test packet handler which feeds the received packet into a Future we later consume from. + vertx + .createDatagramSocket() + .listen( + 0, + LOOPBACK_IP_ADDR, + ar -> { + if (!ar.succeeded()) { + result.completeExceptionally(ar.cause()); + return; + } + + final DatagramSocket socket = ar.result(); + socket.handler(p -> queue.add(Packet.decode(p.data()))); + final DiscoveryPeer peer = + new DiscoveryPeer( + peerId, + LOOPBACK_IP_ADDR, + socket.localAddress().port(), + socket.localAddress().port()); + final DiscoveryTestSocket discoveryTestSocket = + new DiscoveryTestSocket(peer, keyPair, queue, socket); + result.complete(discoveryTestSocket); + }); + + DiscoveryTestSocket discoveryTestSocket; + try { + discoveryTestSocket = result.get(TEST_SOCKET_START_TIMEOUT_SECS, TimeUnit.SECONDS); + } catch (final Exception ex) { + throw new AssertionError("Could not initialize test peer", ex); + } + discoveryTestSockets.add(discoveryTestSocket); + return discoveryTestSocket; + } + + protected void bondViaIncomingPing( + final PeerDiscoveryAgent agent, final DiscoveryTestSocket peerSocket) + throws InterruptedException { + final DiscoveryPeer peer = peerSocket.getPeer(); + + final PingPacketData ping = + PingPacketData.create(peer.getEndpoint(), agent.getAdvertisedPeer().getEndpoint()); + final Packet pingPacket = Packet.create(PacketType.PING, ping, peerSocket.getKeyPair()); + peerSocket.sendToAgent(agent, pingPacket); + + // Wait for returned pong packet to finish bonding + peerSocket.getIncomingPackets().poll(1, TimeUnit.SECONDS); + } + + /** + * Encapsulates a test socket representing a Peer, with an associated queue where all incoming + * packets are placed. + */ + protected static class DiscoveryTestSocket { + private final DiscoveryPeer peer; + private final SECP256K1.KeyPair keyPair; + private final ArrayBlockingQueue queue; + private final DatagramSocket socket; + + public DiscoveryTestSocket( + final DiscoveryPeer peer, + final SECP256K1.KeyPair keyPair, + final ArrayBlockingQueue queue, + final DatagramSocket socket) { + this.peer = peer; + this.keyPair = keyPair; + this.queue = queue; + this.socket = socket; + } + + public DiscoveryPeer getPeer() { + return peer; + } + + public ArrayBlockingQueue getIncomingPackets() { + return queue; + } + + public DatagramSocket getSocket() { + return socket; + } + + public SECP256K1.KeyPair getKeyPair() { + return keyPair; + } + + /** + * Sends a message to an agent. + * + * @param agent the recipient + * @param packet the packet to send + */ + public void sendToAgent(final PeerDiscoveryAgent agent, final Packet packet) { + final Endpoint endpoint = agent.getAdvertisedPeer().getEndpoint(); + socket.send(packet.encode(), endpoint.getUdpPort(), endpoint.getHost(), ar -> {}); + } + + /** + * Retrieves the head of the queue, compulsorily. If no message exists, or no message appears in + * 5 seconds, it throws an assertion error. + * + * @return the head of the queue + */ + public Packet compulsoryPoll() { + Packet packet; + try { + packet = queue.poll(5, TimeUnit.SECONDS); + } catch (final Exception e) { + throw new RuntimeException(e); + } + + if (packet == null) { + throw new AssertionFailedError( + "Expected a message in the test peer queue, but found none."); + } + return packet; + } + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgentTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgentTest.java new file mode 100755 index 00000000000..a84605b3b89 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryAgentTest.java @@ -0,0 +1,329 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static net.consensys.pantheon.util.bytes.BytesValue.fromHexString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.FindNeighborsPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.NeighborsPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketType; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.net.SocketAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import io.vertx.core.Vertx; +import org.junit.Test; + +public class PeerDiscoveryAgentTest extends AbstractPeerDiscoveryTest { + + @Test + public void neighborsPacketFromUnbondedPeerIsDropped() throws Exception { + // Start an agent with no bootstrap peers. + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList()); + assertThat(agent.getPeers()).isEmpty(); + + // Start a test peer and send a PING packet to the agent under test. + final DiscoveryTestSocket discoveryTestSocket = startTestSocket(); + + // Peer is unbonded, as it has not replied with a PONG. + + // Generate an out-of-band NEIGHBORS message. + final DiscoveryPeer[] peers = + PeerDiscoveryTestHelper.generatePeers(PeerDiscoveryTestHelper.generateKeyPairs(5)); + final NeighborsPacketData data = NeighborsPacketData.create(Arrays.asList(peers)); + final Packet packet = + Packet.create(PacketType.NEIGHBORS, data, discoveryTestSocket.getKeyPair()); + discoveryTestSocket.sendToAgent(agent, packet); + + TimeUnit.SECONDS.sleep(1); + assertThat(agent.getPeers()).isEmpty(); + } + + @Test + public void neighborsPacketLimited() { + // Start 20 agents with no bootstrap peers. + final List agents = startDiscoveryAgents(20, Collections.emptyList()); + final List agentPeers = + agents.stream().map(PeerDiscoveryAgent::getAdvertisedPeer).collect(Collectors.toList()); + + // Start another bootstrap peer pointing to those 20 agents. + final PeerDiscoveryAgent agent = startDiscoveryAgent(agentPeers); + await() + .atMost(10, TimeUnit.SECONDS) + .untilAsserted( + () -> { + assertThat(agent.getPeers()).hasSize(20); + assertThat(agent.getPeers()) + .allMatch(p -> p.getStatus() == PeerDiscoveryStatus.BONDED); + }); + + // Send a PING so we can exchange messages with the latter agent. + final DiscoveryTestSocket testSocket = startTestSocket(); + Packet packet = + Packet.create( + PacketType.PING, + PingPacketData.create( + testSocket.getPeer().getEndpoint(), testSocket.getPeer().getEndpoint()), + testSocket.getKeyPair()); + testSocket.sendToAgent(agent, packet); + + // Wait until PONG is received. + final Packet pong = testSocket.compulsoryPoll(); + assertThat(pong.getType()).isEqualTo(PacketType.PONG); + + // Send a FIND_NEIGHBORS message. + packet = + Packet.create( + PacketType.FIND_NEIGHBORS, + FindNeighborsPacketData.create(agents.get(0).getAdvertisedPeer().getId()), + testSocket.getKeyPair()); + testSocket.sendToAgent(agent, packet); + + // Wait until NEIGHBORS is received. + packet = testSocket.compulsoryPoll(); + assertThat(packet.getType()).isEqualTo(PacketType.NEIGHBORS); + + // Assert that we only received 16 items. + final NeighborsPacketData neighbors = packet.getPacketData(NeighborsPacketData.class).get(); + assertThat(neighbors).isNotNull(); + assertThat(neighbors.getNodes()).hasSize(16); + + // Assert that after removing those 16 items we're left with either 4 or 5. + // If we are left with 5, the test peer was returned as an item, assert that this is the case. + agentPeers.removeAll(neighbors.getNodes()); + assertThat(agentPeers.size()).isBetween(4, 5); + if (agentPeers.size() == 5) { + assertThat(neighbors.getNodes()).contains(testSocket.getPeer()); + } + } + + @Test + public void shouldEvictPeerOnDisconnect() { + final Vertx vertx = Vertx.vertx(); + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + + final DefaultPeer peer = new DiscoveryPeer(id, "127.0.0.1", 30303); + final PeerDiscoveryAgent peerDiscoveryAgent = + new PeerDiscoveryAgent( + vertx, + keyPair, + DiscoveryConfiguration.create() + .setBindHost("127.0.0.1") + .setBootstrapPeers(Lists.newArrayList(peer)), + () -> true, + new PeerBlacklist()); + peerDiscoveryAgent.start(30303).join(); + + assertThat(peerDiscoveryAgent.getPeers().size()).isEqualTo(1); + + final PeerConnection peerConnection = createAnonymousPeerConnection(id); + peerDiscoveryAgent.onDisconnect(peerConnection, DisconnectReason.REQUESTED, true); + + assertThat(peerDiscoveryAgent.getPeers().size()).isEqualTo(0); + } + + @Test + public void doesNotBlacklistPeerForNormalDisconnect() throws Exception { + // Start an agent with no bootstrap peers. + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList(), blacklist); + // Setup peer + final DiscoveryTestSocket peerSocket = startTestSocket(); + final PeerConnection wirePeer = createAnonymousPeerConnection(peerSocket.getPeer().getId()); + + // Bond to peer + bondViaIncomingPing(agent, peerSocket); + assertThat(agent.getPeers()).hasSize(1); + + // Disconnect with innocuous reason + blacklist.onDisconnect(wirePeer, DisconnectReason.TOO_MANY_PEERS, false); + agent.onDisconnect(wirePeer, DisconnectReason.TOO_MANY_PEERS, false); + // Confirm peer was removed + assertThat(agent.getPeers()).hasSize(0); + + // Bond again + bondViaIncomingPing(agent, peerSocket); + + // Check peer was allowed to connect + assertThat(agent.getPeers()).hasSize(1); + } + + @Test + public void blacklistPeerForBadBehavior() throws Exception { + // Start an agent with no bootstrap peers. + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList(), blacklist); + // Setup peer + final DiscoveryTestSocket peerSocket = startTestSocket(); + final PeerConnection wirePeer = createAnonymousPeerConnection(peerSocket.getPeer().getId()); + + // Bond to peer + bondViaIncomingPing(agent, peerSocket); + assertThat(agent.getPeers()).hasSize(1); + + // Disconnect with problematic reason + blacklist.onDisconnect(wirePeer, DisconnectReason.BREACH_OF_PROTOCOL, false); + agent.onDisconnect(wirePeer, DisconnectReason.BREACH_OF_PROTOCOL, false); + // Confirm peer was removed + assertThat(agent.getPeers()).hasSize(0); + + // Bond again + bondViaIncomingPing(agent, peerSocket); + + // Check peer was not allowed to connect + assertThat(agent.getPeers()).hasSize(0); + } + + @Test + public void doesNotBlacklistPeerForOurBadBehavior() throws Exception { + // Start an agent with no bootstrap peers. + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList(), blacklist); + // Setup peer + final DiscoveryTestSocket peerSocket = startTestSocket(); + final PeerConnection wirePeer = createAnonymousPeerConnection(peerSocket.getPeer().getId()); + + // Bond to peer + bondViaIncomingPing(agent, peerSocket); + assertThat(agent.getPeers()).hasSize(1); + + // Disconnect with problematic reason + blacklist.onDisconnect(wirePeer, DisconnectReason.BREACH_OF_PROTOCOL, true); + agent.onDisconnect(wirePeer, DisconnectReason.BREACH_OF_PROTOCOL, true); + // Confirm peer was removed + assertThat(agent.getPeers()).hasSize(0); + + // Bond again + bondViaIncomingPing(agent, peerSocket); + + // Check peer was allowed to connect + assertThat(agent.getPeers()).hasSize(1); + } + + @Test + public void blacklistIncompatiblePeer() throws Exception { + // Start an agent with no bootstrap peers. + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList(), blacklist); + // Setup peer + final DiscoveryTestSocket peerSocket = startTestSocket(); + final PeerConnection wirePeer = createAnonymousPeerConnection(peerSocket.getPeer().getId()); + + // Bond to peer + bondViaIncomingPing(agent, peerSocket); + assertThat(agent.getPeers()).hasSize(1); + + // Disconnect + blacklist.onDisconnect(wirePeer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, false); + agent.onDisconnect(wirePeer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, false); + // Confirm peer was removed + assertThat(agent.getPeers()).hasSize(0); + + // Bond again + bondViaIncomingPing(agent, peerSocket); + + // Check peer was not allowed to connect + assertThat(agent.getPeers()).hasSize(0); + } + + @Test + public void blacklistIncompatiblePeerWhoIssuesDisconnect() throws Exception { + // Start an agent with no bootstrap peers. + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList(), blacklist); + // Setup peer + final DiscoveryTestSocket peerSocket = startTestSocket(); + final PeerConnection wirePeer = createAnonymousPeerConnection(peerSocket.getPeer().getId()); + + // Bond to peer + bondViaIncomingPing(agent, peerSocket); + assertThat(agent.getPeers()).hasSize(1); + + // Disconnect + blacklist.onDisconnect(wirePeer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, true); + agent.onDisconnect(wirePeer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, true); + // Confirm peer was removed + assertThat(agent.getPeers()).hasSize(0); + + // Bond again + bondViaIncomingPing(agent, peerSocket); + + // Check peer was not allowed to connect + assertThat(agent.getPeers()).hasSize(0); + } + + @Test + public void shouldBeActiveWhenConfigIsTrue() { + final DiscoveryConfiguration config = new DiscoveryConfiguration(); + config.setActive(true); + + final PeerDiscoveryAgent agent = startDiscoveryAgent(config, new PeerBlacklist()); + + assertThat(agent.isActive()).isTrue(); + } + + @Test + public void shouldNotBeActiveWhenConfigIsFalse() { + final DiscoveryConfiguration config = new DiscoveryConfiguration(); + config.setActive(false); + + final PeerDiscoveryAgent agent = startDiscoveryAgent(config, new PeerBlacklist()); + + assertThat(agent.isActive()).isFalse(); + } + + private PeerConnection createAnonymousPeerConnection(final BytesValue id) { + return new PeerConnection() { + @Override + public void send(final Capability capability, final MessageData message) + throws PeerNotConnected {} + + @Override + public Set getAgreedCapabilities() { + return null; + } + + @Override + public PeerInfo getPeer() { + return new PeerInfo(0, null, null, 0, id); + } + + @Override + public void terminateConnection(final DisconnectReason reason, final boolean peerInitiated) {} + + @Override + public void disconnect(final DisconnectReason reason) {} + + @Override + public SocketAddress getLocalAddress() { + return null; + } + + @Override + public SocketAddress getRemoteAddress() { + return null; + } + }; + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBondingTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBondingTest.java new file mode 100755 index 00000000000..1c7a196f283 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBondingTest.java @@ -0,0 +1,82 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.p2p.discovery.internal.FindNeighborsPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketType; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PongPacketData; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +public class PeerDiscoveryBondingTest extends AbstractPeerDiscoveryTest { + + @Test + public void pongSentUponPing() throws Exception { + // Start an agent with no bootstrap peers. + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList()); + + // Start a test peer and send a PING packet to the agent under test. + final DiscoveryTestSocket discoveryTestSocket = startTestSocket(); + + final PingPacketData ping = + PingPacketData.create( + discoveryTestSocket.getPeer().getEndpoint(), agent.getAdvertisedPeer().getEndpoint()); + final Packet packet = Packet.create(PacketType.PING, ping, discoveryTestSocket.getKeyPair()); + discoveryTestSocket.sendToAgent(agent, packet); + + final Packet pongPacket = discoveryTestSocket.getIncomingPackets().poll(10, TimeUnit.SECONDS); + assertThat(pongPacket.getType()).isEqualTo(PacketType.PONG); + assertThat(pongPacket.getPacketData(PongPacketData.class)).isPresent(); + + final PongPacketData pong = pongPacket.getPacketData(PongPacketData.class).get(); + assertThat(pong.getTo()).isEqualTo(discoveryTestSocket.getPeer().getEndpoint()); + + // The agent considers the test peer BONDED. + assertThat(agent.getPeers()).hasSize(1); + assertThat(agent.getPeers()).allMatch(p -> p.getStatus() == PeerDiscoveryStatus.BONDED); + } + + @Test + public void neighborsPacketNotSentUnlessBonded() throws InterruptedException { + // Start an agent. + final PeerDiscoveryAgent agent = startDiscoveryAgent(emptyList()); + + // Start a test peer that will send a FIND_NEIGHBORS to the agent under test. It should be + // ignored because + // we haven't bonded. + final DiscoveryTestSocket discoveryTestSocket = startTestSocket(); + final FindNeighborsPacketData data = + FindNeighborsPacketData.create(discoveryTestSocket.getPeer().getId()); + Packet packet = + Packet.create(PacketType.FIND_NEIGHBORS, data, discoveryTestSocket.getKeyPair()); + discoveryTestSocket.sendToAgent(agent, packet); + + // No responses received in 2 seconds. + final Packet incoming = discoveryTestSocket.getIncomingPackets().poll(2, TimeUnit.SECONDS); + assertThat(incoming).isNull(); + + // Create and dispatch a PING packet. + final PingPacketData ping = + PingPacketData.create( + discoveryTestSocket.getPeer().getEndpoint(), agent.getAdvertisedPeer().getEndpoint()); + packet = Packet.create(PacketType.PING, ping, discoveryTestSocket.getKeyPair()); + discoveryTestSocket.sendToAgent(agent, packet); + + // Now we received a PONG. + final Packet pongPacket = discoveryTestSocket.getIncomingPackets().poll(2, TimeUnit.SECONDS); + assertThat(pongPacket.getType()).isEqualTo(PacketType.PONG); + assertThat(pongPacket.getPacketData(PongPacketData.class)).isPresent(); + + final PongPacketData pong = pongPacket.getPacketData(PongPacketData.class).get(); + assertThat(pong.getTo()).isEqualTo(discoveryTestSocket.getPeer().getEndpoint()); + + // No more packets. + assertThat(discoveryTestSocket.getIncomingPackets()).hasSize(0); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBootstrappingTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBootstrappingTest.java new file mode 100755 index 00000000000..b569dee333e --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryBootstrappingTest.java @@ -0,0 +1,124 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketType; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.junit.Test; + +public class PeerDiscoveryBootstrappingTest extends AbstractPeerDiscoveryTest { + + @Test + public void bootstrappingPingsSentSingleBootstrapPeer() throws Exception { + // Start one test peer and use it as a bootstrap peer. + final DiscoveryTestSocket discoveryTestSocket = startTestSocket(); + final List bootstrapPeers = singletonList(discoveryTestSocket.getPeer()); + + // Start an agent. + final PeerDiscoveryAgent agent = startDiscoveryAgent(bootstrapPeers); + + final Packet packet = discoveryTestSocket.getIncomingPackets().poll(2, TimeUnit.SECONDS); + + assertThat(packet.getType()).isEqualTo(PacketType.PING); + assertThat(packet.getNodeId()).isEqualTo(agent.getAdvertisedPeer().getId()); + + final PingPacketData pingData = packet.getPacketData(PingPacketData.class).get(); + assertThat(pingData.getExpiration()) + .isGreaterThanOrEqualTo(System.currentTimeMillis() / 1000 - 10000); + assertThat(pingData.getFrom()).isEqualTo(agent.getAdvertisedPeer().getEndpoint()); + assertThat(pingData.getTo()).isEqualTo(discoveryTestSocket.getPeer().getEndpoint()); + } + + @Test + public void bootstrappingPingsSentMultipleBootstrapPeers() { + // Start three test peers. + startTestSockets(3); + + // Use these peers as bootstrap peers. + final List bootstrapPeers = + discoveryTestSockets.stream().map(DiscoveryTestSocket::getPeer).collect(toList()); + + // Start five agents. + startDiscoveryAgents(5, bootstrapPeers); + + // Assert that all test peers received a Find Neighbors packet. + for (final DiscoveryTestSocket peer : discoveryTestSockets) { + // Five messages per peer (sent by each of the five agents). + final List packets = Stream.generate(peer::compulsoryPoll).limit(5).collect(toList()); + + // No more messages left. + assertThat(peer.getIncomingPackets().size()).isEqualTo(0); + + // Assert that the node IDs we received belong to the test agents. + final List peerIds = packets.stream().map(Packet::getNodeId).collect(toList()); + final List nodeIds = + agents + .stream() + .map(PeerDiscoveryAgent::getAdvertisedPeer) + .map(Peer::getId) + .collect(toList()); + + assertThat(peerIds).containsExactlyInAnyOrderElementsOf(nodeIds); + + // Traverse all received packets. + for (final Packet packet : packets) { + // Assert that the packet was a Find Neighbors one. + assertThat(packet.getType()).isEqualTo(PacketType.PING); + + // Assert on the content of the packet data. + final PingPacketData ping = packet.getPacketData(PingPacketData.class).get(); + assertThat(ping.getExpiration()) + .isGreaterThanOrEqualTo(System.currentTimeMillis() / 1000 - 10000); + assertThat(ping.getTo()).isEqualTo(peer.getPeer().getEndpoint()); + } + } + } + + @Test + public void bootstrappingPeersListUpdated() { + // Start an agent. + final PeerDiscoveryAgent bootstrapAgent = startDiscoveryAgent(emptyList()); + + // Start other five agents, pointing to the one above as a bootstrap peer. + final List otherAgents = + startDiscoveryAgents(5, singletonList(bootstrapAgent.getAdvertisedPeer())); + + final BytesValue[] otherPeersIds = + otherAgents + .stream() + .map(PeerDiscoveryAgent::getAdvertisedPeer) + .map(Peer::getId) + .toArray(BytesValue[]::new); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> + assertThat(bootstrapAgent.getPeers()) + .extracting(Peer::getId) + .containsExactlyInAnyOrder(otherPeersIds)); + + assertThat(bootstrapAgent.getPeers()) + .allMatch(p -> p.getStatus() == PeerDiscoveryStatus.BONDED); + + // This agent will bootstrap off the bootstrap peer, will add all nodes returned by the latter, + // and will + // bond with them, ultimately adding all 7 nodes in the network to its table. + final PeerDiscoveryAgent newAgent = + startDiscoveryAgent(singletonList(bootstrapAgent.getAdvertisedPeer())); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(newAgent.getPeers()).hasSize(6)); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryObserversTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryObserversTest.java new file mode 100755 index 00000000000..64438455fd8 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryObserversTest.java @@ -0,0 +1,174 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static io.vertx.core.Vertx.vertx; +import static net.consensys.pantheon.ethereum.p2p.NetworkingTestHelper.configWithRandomPorts; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.awaitility.Awaitility.await; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryEvent.PeerBondedEvent; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.core.ConditionTimeoutException; +import org.junit.Test; + +public class PeerDiscoveryObserversTest extends AbstractPeerDiscoveryTest { + private static final Logger LOG = LogManager.getLogger(); + private static final int BROADCAST_TCP_PORT = 26422; + + @Test + public void addAndRemoveObservers() { + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList()); + assertThat(agent.getObserverCount()).isEqualTo(0); + + final long id1 = agent.observePeerBondedEvents((event) -> {}); + final long id2 = agent.observePeerBondedEvents((event) -> {}); + final long id3 = agent.observePeerBondedEvents((event) -> {}); + final long id4 = agent.observePeerDroppedEvents((event) -> {}); + final long id5 = agent.observePeerDroppedEvents((event) -> {}); + final long id6 = agent.observePeerDroppedEvents((event) -> {}); + assertThat(agent.getObserverCount()).isEqualTo(6); + + agent.removePeerBondedObserver(id1); + agent.removePeerBondedObserver(id2); + assertThat(agent.getObserverCount()).isEqualTo(4); + + agent.removePeerBondedObserver(id3); + agent.removePeerDroppedObserver(id4); + assertThat(agent.getObserverCount()).isEqualTo(2); + + agent.removePeerDroppedObserver(id5); + agent.removePeerDroppedObserver(id6); + assertThat(agent.getObserverCount()).isEqualTo(0); + + final long id7 = agent.observePeerBondedEvents((event) -> {}); + final long id8 = agent.observePeerDroppedEvents((event) -> {}); + assertThat(agent.getObserverCount()).isEqualTo(2); + + agent.removePeerBondedObserver(id7); + agent.removePeerDroppedObserver(id8); + assertThat(agent.getObserverCount()).isEqualTo(0); + } + + @Test + public void removeInexistingObserver() { + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList()); + assertThat(agent.getObserverCount()).isEqualTo(0); + + agent.observePeerBondedEvents((event) -> {}); + assertThat(agent.removePeerBondedObserver(12345)).isFalse(); + } + + @Test + public void peerBondedObserverTriggered() throws TimeoutException, InterruptedException { + // Create 3 discovery agents with no bootstrap peers. + final List others1 = startDiscoveryAgents(3, Collections.emptyList()); + final List peers1 = + others1.stream().map(PeerDiscoveryAgent::getAdvertisedPeer).collect(Collectors.toList()); + + // Create two discovery agents pointing to the above as bootstrap peers. + final List others2 = startDiscoveryAgents(2, peers1); + final List peers2 = + others2.stream().map(PeerDiscoveryAgent::getAdvertisedPeer).collect(Collectors.toList()); + + // A list of all peers. + final List allPeers = new ArrayList<>(peers1); + allPeers.addAll(peers2); + + // Create a discovery agent (which we'll assert on), using the above two peers as bootstrap + // peers. + final PeerDiscoveryAgent agent = + new PeerDiscoveryAgent( + vertx(), + SECP256K1.KeyPair.generate(), + configWithRandomPorts().getDiscovery().setBootstrapPeers(peers2), + () -> true, + new PeerBlacklist()); + + // A queue for storing peer bonded events. + final ArrayBlockingQueue queue = new ArrayBlockingQueue<>(10); + agent.observePeerBondedEvents(queue::add); + assertThatCode(() -> agent.start(BROADCAST_TCP_PORT).get(5, TimeUnit.SECONDS)) + .doesNotThrowAnyException(); + + // Wait until we've received 5 events. + try { + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(queue.size()).isEqualTo(5)); + } catch (ConditionTimeoutException | AssertionError e) { + final List events = new ArrayList<>(); + queue.forEach(evt -> events.add(evt.toString())); + LOG.error("Queue:\n" + String.join("\n", events), e); + throw e; + } + // Wait one second and check we've received no more events. + Thread.sleep(1000); + assertThat(queue.size()).isEqualTo(5); + + // Extract all events and perform asserts on them. + final List events = new ArrayList<>(5); + queue.drainTo(events, 5); + + assertThat(events) + .extracting(PeerDiscoveryEvent::getPeer) + .extracting(DiscoveryPeer::getId) + .containsExactlyInAnyOrderElementsOf( + allPeers.stream().map(DiscoveryPeer::getId).collect(Collectors.toList())); + assertThat(events).extracting(PeerDiscoveryEvent::getTimestamp).isSorted(); + } + + @Test + public void multiplePeerBondedObserversTriggered() { + // Create 3 discovery agents with no bootstrap peers. + final List others = startDiscoveryAgents(3, Collections.emptyList()); + final Peer peer = others.stream().map(PeerDiscoveryAgent::getAdvertisedPeer).findFirst().get(); + + // Create a discovery agent (which we'll assert on), using the above two peers as bootstrap + // peers. + final PeerDiscoveryAgent agent = + new PeerDiscoveryAgent( + vertx(), + SECP256K1.KeyPair.generate(), + configWithRandomPorts() + .getDiscovery() + .setBootstrapPeers(Collections.singletonList(peer)), + () -> true, + new PeerBlacklist()); + + // Create 5 queues and subscribe them to peer bonded events. + final List> queues = + Stream.generate(() -> new ArrayBlockingQueue(10)) + .limit(5) + .collect(Collectors.toList()); + queues.forEach(q -> agent.observePeerBondedEvents(q::add)); + + // Start the agent and wait until each queue receives one event. + agent.start(BROADCAST_TCP_PORT); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(queues).allMatch(q -> q.size() == 1)); + + // All events are for the same peer. + final List events = + queues.stream().map(ArrayBlockingQueue::poll).collect(Collectors.toList()); + assertThat(events).extracting(PeerDiscoveryEvent::getPeer).allMatch(p -> p.equals(peer)); + + // We can event check that the event instance is the same across all queues. + final PeerBondedEvent event = events.get(0); + assertThat(events).allMatch(e -> e == event); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketPcapSedesTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketPcapSedesTest.java new file mode 100755 index 00000000000..dc514736005 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketPcapSedesTest.java @@ -0,0 +1,122 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static com.google.common.net.InetAddresses.isInetAddress; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.p2p.discovery.internal.FindNeighborsPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.NeighborsPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PongPacketData; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.util.NetworkUtility; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import io.pkts.Pcap; +import io.pkts.protocol.Protocol; +import io.vertx.core.buffer.Buffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class PeerDiscoveryPacketPcapSedesTest { + + private final Instant timestamp; + private final byte[] data; + + public PeerDiscoveryPacketPcapSedesTest(final Instant instant, final byte[] data) { + this.timestamp = instant; + this.data = data; + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection parameters() throws IOException { + final Pcap pcap = + Pcap.openStream( + PeerDiscoveryPacketPcapSedesTest.class + .getClassLoader() + .getResourceAsStream("udp.pcap")); + + final List parameters = new ArrayList<>(); + pcap.loop( + packet -> { + if (packet.hasProtocol(Protocol.UDP)) { + final byte[] data = packet.getPacket(Protocol.UDP).getPayload().getArray(); + parameters.add( + new Object[] {Instant.ofEpochMilli(packet.getArrivalTime() / 1000), data}); + } + return true; + }); + return parameters; + } + + @Test + public void serializeDeserialize() { + final Packet packet = Packet.decode(Buffer.buffer(data)); + assertThat(packet.getType()).isNotNull(); + assertThat(packet.getNodeId()).isNotNull(); + assertThat(packet.getNodeId().extractArray()).hasSize(64); + + switch (packet.getType()) { + case PING: + assertThat(packet.getPacketData(PingPacketData.class)).isPresent(); + final PingPacketData ping = packet.getPacketData(PingPacketData.class).get(); + + assertThat(ping.getTo()).isNotNull(); + assertThat(ping.getFrom()).isNotNull(); + assertThat(isInetAddress(ping.getTo().getHost())).isTrue(); + assertThat(isInetAddress(ping.getFrom().getHost())).isTrue(); + assertThat(ping.getTo().getUdpPort()).isPositive(); + assertThat(ping.getFrom().getUdpPort()).isPositive(); + ping.getTo().getTcpPort().ifPresent(p -> assertThat(p).isPositive()); + ping.getFrom().getTcpPort().ifPresent(p -> assertThat(p).isPositive()); + assertThat(ping.getExpiration()).isPositive(); + break; + + case PONG: + assertThat(packet.getPacketData(PongPacketData.class)).isPresent(); + final PongPacketData pong = packet.getPacketData(PongPacketData.class).get(); + + assertThat(pong.getTo()).isNotNull(); + assertThat(isInetAddress(pong.getTo().getHost())).isTrue(); + assertThat(pong.getTo().getUdpPort()).isPositive(); + pong.getTo().getTcpPort().ifPresent(p -> assertThat(p).isPositive()); + assertThat(pong.getPingHash().extractArray()).hasSize(32); + assertThat(pong.getExpiration()).isPositive(); + break; + + case FIND_NEIGHBORS: + assertThat(packet.getPacketData(FindNeighborsPacketData.class)).isPresent(); + final FindNeighborsPacketData data = + packet.getPacketData(FindNeighborsPacketData.class).get(); + assertThat(data.getExpiration()) + .isBetween(timestamp.getEpochSecond() - 10000, timestamp.getEpochSecond() + 10000); + assertThat(data.getTarget().extractArray()).hasSize(64); + assertThat(packet.getNodeId().extractArray()).hasSize(64); + break; + + case NEIGHBORS: + assertThat(packet.getPacketData(NeighborsPacketData.class)).isPresent(); + final NeighborsPacketData neighbors = packet.getPacketData(NeighborsPacketData.class).get(); + assertThat(neighbors.getExpiration()).isGreaterThan(0); + assertThat(neighbors.getNodes()).isNotEmpty(); + + for (final Peer p : neighbors.getNodes()) { + assertThat(NetworkUtility.isValidPort(p.getEndpoint().getUdpPort())).isTrue(); + assertThat(isInetAddress(p.getEndpoint().getHost())).isTrue(); + assertThat(p.getId().extractArray()).hasSize(64); + } + + break; + } + + final byte[] encoded = packet.encode().getBytes(); + assertThat(encoded).isEqualTo(data); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketSedesTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketSedesTest.java new file mode 100755 index 00000000000..ce9afce323c --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryPacketSedesTest.java @@ -0,0 +1,117 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper.generateKeyPairs; +import static net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper.generatePeers; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Offset.offset; +import static org.junit.Assert.assertNotNull; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.FindNeighborsPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.NeighborsPacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketData; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketType; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import io.vertx.core.buffer.Buffer; +import org.junit.Test; + +public class PeerDiscoveryPacketSedesTest { + + @Test + public void serializeDeserializeEntirePacket() { + final byte[] r = new byte[64]; + new Random().nextBytes(r); + final BytesValue target = BytesValue.wrap(r); + final SECP256K1.KeyPair kp = SECP256K1.KeyPair.generate(); + + final FindNeighborsPacketData packetData = FindNeighborsPacketData.create(target); + final Packet packet = Packet.create(PacketType.FIND_NEIGHBORS, packetData, kp); + final Buffer encoded = packet.encode(); + assertNotNull(encoded); + + final Packet decoded = Packet.decode(encoded); + assertThat(decoded.getType()).isEqualTo(PacketType.FIND_NEIGHBORS); + assertThat(decoded.getNodeId()).isEqualTo(kp.getPublicKey().getEncodedBytes()); + assertThat(decoded.getPacketData(NeighborsPacketData.class)).isNotPresent(); + assertThat(decoded.getPacketData(FindNeighborsPacketData.class)).isPresent(); + } + + @Test + public void serializeDeserializeFindNeighborsPacketData() { + final byte[] r = new byte[64]; + new Random().nextBytes(r); + final BytesValue target = BytesValue.wrap(r); + + final FindNeighborsPacketData packet = FindNeighborsPacketData.create(target); + final BytesValue serialized = RLP.encode(packet::writeTo); + assertNotNull(serialized); + + final FindNeighborsPacketData deserialized = + FindNeighborsPacketData.readFrom(RLP.input(serialized)); + assertThat(deserialized.getTarget()).isEqualTo(target); + // Fuzziness: allow a skew of 1.5 seconds between the time the message was generated until the + // assertion. + assertThat(deserialized.getExpiration()) + .isCloseTo( + System.currentTimeMillis() + PacketData.DEFAULT_EXPIRATION_PERIOD_MS, offset(1500L)); + } + + @Test + public void neighborsPacketData() { + final List peers = Arrays.asList(generatePeers(generateKeyPairs(5))); + + final NeighborsPacketData packet = NeighborsPacketData.create(peers); + final BytesValue serialized = RLP.encode(packet::writeTo); + assertNotNull(serialized); + + final NeighborsPacketData deserialized = NeighborsPacketData.readFrom(RLP.input(serialized)); + assertThat(deserialized.getNodes()).isEqualTo(peers); + // Fuzziness: allow a skew of 1.5 seconds between the time the message was generated until the + // assertion. + assertThat(deserialized.getExpiration()) + .isCloseTo( + System.currentTimeMillis() + PacketData.DEFAULT_EXPIRATION_PERIOD_MS, offset(1500L)); + } + + @Test(expected = RLPException.class) + public void deserializeDifferentPacketData() { + final byte[] r = new byte[64]; + new Random().nextBytes(r); + final BytesValue target = BytesValue.wrap(r); + + final FindNeighborsPacketData packet = FindNeighborsPacketData.create(target); + final BytesValue serialized = RLP.encode(packet::writeTo); + assertNotNull(serialized); + + NeighborsPacketData.readFrom(RLP.input(serialized)); + } + + @Test(expected = PeerDiscoveryPacketDecodingException.class) + public void integrityCheckFailsUnmatchedHash() { + final byte[] r = new byte[64]; + new Random().nextBytes(r); + final BytesValue target = BytesValue.wrap(r); + + final SECP256K1.KeyPair kp = SECP256K1.KeyPair.generate(); + + final FindNeighborsPacketData data = FindNeighborsPacketData.create(target); + final Packet packet = Packet.create(PacketType.FIND_NEIGHBORS, data, kp); + + final BytesValue encoded = BytesValue.wrapBuffer(packet.encode()); + final MutableBytesValue garbled = encoded.mutableCopy(); + final int i = garbled.size() - 1; + // Change one bit in the last byte, which belongs to the payload, hence the hash will not match + // any longer. + garbled.set(i, (byte) (garbled.get(i) + 0x01)); + Packet.decode(Buffer.buffer(garbled.extractArray())); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTestHelper.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTestHelper.java new file mode 100755 index 00000000000..ecbf1d7c820 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTestHelper.java @@ -0,0 +1,27 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; + +import java.util.OptionalInt; +import java.util.stream.Stream; + +public class PeerDiscoveryTestHelper { + + public static SECP256K1.KeyPair[] generateKeyPairs(final int count) { + return Stream.generate(SECP256K1.KeyPair::generate) + .limit(count) + .toArray(SECP256K1.KeyPair[]::new); + } + + public static DiscoveryPeer[] generatePeers(final SECP256K1.KeyPair... keypairs) { + return Stream.of(keypairs) + .map(kp -> kp.getPublicKey().getEncodedBytes()) + .map(bytes -> new DiscoveryPeer(bytes, new Endpoint("127.0.0.1", 1, OptionalInt.empty()))) + .toArray(DiscoveryPeer[]::new); + } + + public static DiscoveryPeer[] generateDiscoveryPeers(final SECP256K1.KeyPair... keypairs) { + return Stream.of(generatePeers(keypairs)).map(DiscoveryPeer::new).toArray(DiscoveryPeer[]::new); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTimestampsTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTimestampsTest.java new file mode 100755 index 00000000000..ac6d8bdc710 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/PeerDiscoveryTimestampsTest.java @@ -0,0 +1,136 @@ +package net.consensys.pantheon.ethereum.p2p.discovery; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.Packet; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PacketType; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerDiscoveryController; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerTable; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PingPacketData; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import io.vertx.core.Vertx; +import org.junit.Test; + +public class PeerDiscoveryTimestampsTest extends AbstractPeerDiscoveryTest { + + @Test + public void lastSeenAndFirstDiscoveredTimestampsUpdatedOnMessage() { + // peer[0] => controller // peer[1] => sender + final SECP256K1.KeyPair[] keypairs = PeerDiscoveryTestHelper.generateKeyPairs(2); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keypairs); + + final PeerDiscoveryAgent agent = mock(PeerDiscoveryAgent.class); + when(agent.getAdvertisedPeer()).thenReturn(peers[0]); + + final PeerDiscoveryController controller = + new PeerDiscoveryController( + mock(Vertx.class), + agent, + new PeerTable(agent.getAdvertisedPeer().getId()), + Collections.emptyList(), + TimeUnit.HOURS.toMillis(1), + () -> true, + new PeerBlacklist()); + controller.start(); + + final PingPacketData ping = + PingPacketData.create(peers[1].getEndpoint(), peers[0].getEndpoint()); + final Packet packet = Packet.create(PacketType.PING, ping, keypairs[1]); + + controller.onMessage(packet, peers[1]); + + final AtomicLong lastSeen = new AtomicLong(); + final AtomicLong firstDiscovered = new AtomicLong(); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + assertThat(controller.getPeers()).hasSize(1); + + final DiscoveryPeer p = controller.getPeers().iterator().next(); + assertThat(p.getLastSeen()).isGreaterThan(0); + assertThat(p.getFirstDiscovered()).isGreaterThan(0); + + lastSeen.set(p.getLastSeen()); + firstDiscovered.set(p.getFirstDiscovered()); + }); + + controller.onMessage(packet, peers[1]); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + assertThat(controller.getPeers()).hasSize(1); + + final DiscoveryPeer p = controller.getPeers().iterator().next(); + assertThat(p.getLastSeen()).isGreaterThan(lastSeen.get()); + assertThat(p.getFirstDiscovered()).isEqualTo(firstDiscovered.get()); + }); + } + + @Test + public void lastContactedTimestampUpdatedOnOutboundMessage() { + final PeerDiscoveryAgent agent = startDiscoveryAgent(Collections.emptyList()); + assertThat(agent.getPeers()).hasSize(0); + + // Start a test peer and send a PING packet to the agent under test. + final DiscoveryTestSocket discoveryTestSocket = startTestSocket(); + + final PingPacketData ping = + PingPacketData.create( + discoveryTestSocket.getPeer().getEndpoint(), agent.getAdvertisedPeer().getEndpoint()); + final Packet packet = Packet.create(PacketType.PING, ping, discoveryTestSocket.getKeyPair()); + discoveryTestSocket.sendToAgent(agent, packet); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(agent.getPeers()).hasSize(1)); + + final AtomicLong lastContacted = new AtomicLong(); + final AtomicLong lastSeen = new AtomicLong(); + final AtomicLong firstDiscovered = new AtomicLong(); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + final DiscoveryPeer peer = agent.getPeers().iterator().next(); + final long lc = peer.getLastContacted(); + final long ls = peer.getLastSeen(); + final long fd = peer.getFirstDiscovered(); + + assertThat(lc).isGreaterThan(0); + assertThat(ls).isGreaterThan(0); + assertThat(fd).isGreaterThan(0); + + lastContacted.set(lc); + lastSeen.set(ls); + firstDiscovered.set(fd); + }); + + // Send another packet and ensure that timestamps are updated accordingly. + discoveryTestSocket.sendToAgent(agent, packet); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + final DiscoveryPeer peer = agent.getPeers().iterator().next(); + + assertThat(peer.getLastContacted()).isGreaterThan(lastContacted.get()); + assertThat(peer.getLastSeen()).isGreaterThan(lastSeen.get()); + assertThat(peer.getFirstDiscovered()).isEqualTo(firstDiscovered.get()); + }); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/BucketTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/BucketTest.java new file mode 100755 index 00000000000..f4b7fae5fa1 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/BucketTest.java @@ -0,0 +1,122 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static junit.framework.TestCase.assertFalse; +import static net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper.generateDiscoveryPeers; +import static net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper.generateKeyPairs; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.Test; + +public class BucketTest { + + @Test + public void successfulAddAndGet() { + final Bucket kBucket = new Bucket(16); + final DiscoveryPeer[] peers = generateDiscoveryPeers(generateKeyPairs(10)); + for (int i = 0; i < peers.length - 1; i++) { + kBucket.add(peers[i]); + } + final DiscoveryPeer testPeer = peers[peers.length - 1]; + kBucket.add(testPeer); + assertThat(testPeer).isEqualTo(kBucket.getAndTouch(testPeer.getId()).get()); + } + + @Test + public void unsuccessfulAdd() { + final Bucket kBucket = new Bucket(16); + final DiscoveryPeer[] peers = generateDiscoveryPeers(generateKeyPairs(17)); + for (int i = 0; i < peers.length - 1; i++) { + kBucket.add(peers[i]); + } + final DiscoveryPeer testPeer = peers[peers.length - 1]; + final Optional evictionCandidate = kBucket.add(testPeer); + assertThat(evictionCandidate.get()).isEqualTo(kBucket.getAndTouch(peers[0].getId()).get()); + } + + @Test + public void movedToHead() { + final Bucket kBucket = new Bucket(16); + final DiscoveryPeer[] peers = generateDiscoveryPeers(generateKeyPairs(5)); + for (final DiscoveryPeer peer : peers) { + kBucket.add(peer); + } + kBucket.getAndTouch(peers[0].getId()); + assertThat(kBucket.peers().indexOf(peers[0])).isEqualTo(0); + } + + @Test + public void evictPeer() { + final Bucket kBucket = new Bucket(16); + final DiscoveryPeer[] peers = generateDiscoveryPeers(generateKeyPairs(5)); + for (final DiscoveryPeer p : peers) { + kBucket.add(p); + } + kBucket.evict(peers[4]); + assertFalse(kBucket.peers().contains(peers[4])); + } + + @Test + public void allActionsOnBucket() { + final Bucket kBucket = new Bucket(16); + final DiscoveryPeer[] peers = generateDiscoveryPeers(generateKeyPairs(30)); + + // Try to evict a peer on an empty bucket. + assertThat(kBucket.evict(peers[29])).isFalse(); + + // Add the first 16 peers to the bucket. + Stream.of(peers) + .limit(16) + .forEach( + p -> { + assertThat(kBucket.getAndTouch(p.getId())).isNotPresent(); + assertThat(kBucket.add(p)).isNotPresent(); + assertThat(kBucket.getAndTouch(p.getId())).isPresent().get().isEqualTo(p); + assertThatThrownBy(() -> kBucket.add(p)).isInstanceOf(IllegalArgumentException.class); + }); + + // Ensure the peer is not there already. + assertThat(kBucket.getAndTouch(peers[16].getId())).isNotPresent(); + + // Try to add a 17th peer and check that the eviction candidate matches the first peer. + final Optional evictionCandidate = kBucket.add(peers[16]); + assertThat(evictionCandidate).isPresent().get().isEqualTo(peers[0]); + + // Try to add a peer that already exists, and check that the bucket size still remains capped at + // 16. + assertThatThrownBy(() -> kBucket.add(peers[0])).isInstanceOf(IllegalArgumentException.class); + assertThat(kBucket.peers()).hasSize(16); + + // Try to evict a peer that doesn't exist, and check the result is false. + assertThat(kBucket.evict(peers[17])).isFalse(); + assertThat(kBucket.peers()).hasSize(16); + + // Evict a peer from head, another from the middle, and the tail. + assertThat(kBucket.evict(peers[0])).isTrue(); + assertThat(kBucket.peers()).hasSize(15); + assertThat(kBucket.evict(peers[7])).isTrue(); + assertThat(kBucket.peers()).hasSize(14); + assertThat(kBucket.evict(peers[15])).isTrue(); + assertThat(kBucket.peers()).hasSize(13); + + // Check that we can now add peers again. + assertThat(kBucket.add(peers[0])).isNotPresent(); + assertThat(kBucket.add(peers[7])).isNotPresent(); + assertThat(kBucket.add(peers[15])).isNotPresent(); + assertThat(kBucket.add(peers[17])).isPresent().get().isEqualTo(peers[1]); + + // Test the touch behaviour. + assertThat(kBucket.getAndTouch(peers[6].getId())).isPresent().get().isEqualTo(peers[6]); + assertThat(kBucket.getAndTouch(peers[9].getId())).isPresent().get().isEqualTo(peers[9]); + + assertThat(kBucket.peers()) + .containsSequence( + peers[9], peers[6], peers[15], peers[7], peers[0], peers[14], peers[13], peers[12], + peers[11], peers[10], peers[8], peers[5], peers[4], peers[3], peers[2], peers[1]); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/MockPacketDataFactory.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/MockPacketDataFactory.java new file mode 100755 index 00000000000..f0dcedc0668 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/MockPacketDataFactory.java @@ -0,0 +1,59 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Arrays; +import java.util.Optional; + +public class MockPacketDataFactory { + + public static Packet mockNeighborsPacket( + final DiscoveryPeer from, final DiscoveryPeer... neighbors) { + final Packet packet = mock(Packet.class); + + final NeighborsPacketData pongPacketData = NeighborsPacketData.create(Arrays.asList(neighbors)); + when(packet.getPacketData(any())).thenReturn(Optional.of(pongPacketData)); + final BytesValue id = from.getId(); + when(packet.getNodeId()).thenReturn(id); + when(packet.getType()).thenReturn(PacketType.NEIGHBORS); + when(packet.getHash()).thenReturn(Bytes32.ZERO); + + return packet; + } + + public static Packet mockPongPacket(final Peer from, final BytesValue pingHash) { + final Packet packet = mock(Packet.class); + + final PongPacketData pongPacketData = PongPacketData.create(from.getEndpoint(), pingHash); + when(packet.getPacketData(any())).thenReturn(Optional.of(pongPacketData)); + final BytesValue id = from.getId(); + when(packet.getNodeId()).thenReturn(id); + when(packet.getType()).thenReturn(PacketType.PONG); + when(packet.getHash()).thenReturn(Bytes32.ZERO); + + return packet; + } + + public static Packet mockFindNeighborsPacket(final Peer from) { + final Packet packet = mock(Packet.class); + final BytesValue target = + BytesValue.fromHexString( + "0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40"); + + final FindNeighborsPacketData packetData = FindNeighborsPacketData.create(target); + when(packet.getPacketData(any())).thenReturn(Optional.of(packetData)); + final BytesValue id = from.getId(); + when(packet.getNodeId()).thenReturn(id); + when(packet.getType()).thenReturn(PacketType.FIND_NEIGHBORS); + when(packet.getHash()).thenReturn(Bytes32.ZERO); + + return packet; + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketTest.java new file mode 100755 index 00000000000..8f83950bceb --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PacketTest.java @@ -0,0 +1,51 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.OptionalInt; + +import io.vertx.core.buffer.Buffer; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +public class PacketTest { + + private static final String VALID_PONG_PACKET = + "a1581c1705e744976d0341011c4490b3ab0b48283407ae5cf7526b948717489613ad897c4cf167117196d21352c15bcbaec23227b22eb92a15f5cd4b0a4ef98124a679935c16bd334fbd26be55ba4344843ac4710a3f3e3684d719d48c4980660002f2cb84b4b57a1a82040182765fa046896547d3b4259aa1a67bd26e7ec58ab4be650c5552ef0360caf9dae489d53b845b872dc8"; + + @Test + public void shouldDecodeValidPongPacket() { + final Packet packet = decode(VALID_PONG_PACKET); + final PongPacketData packetData = packet.getPacketData(PongPacketData.class).get(); + + assertThat(packet.getType()).isSameAs(PacketType.PONG); + assertThat(packetData.getTo()) + .isEqualTo(new Endpoint("180.181.122.26", 1025, OptionalInt.of(30303))); + assertThat(packetData.getPingHash()) + .isEqualTo( + BytesValue.fromHexString( + "0x46896547d3b4259aa1a67bd26e7ec58ab4be650c5552ef0360caf9dae489d53b")); + assertThat(packetData.getExpiration()).isEqualTo(1535585736); + assertThat(packet.getNodeId()) + .isEqualTo( + BytesValue.fromHexString( + "0x669f45b66acf3b804c26ce13cfdd1f7e3d0ff4ed85060841b9af3af6dbfbacd05181e1c9363161446a307f3ca24e707856a01e4bf1eed5e1aefc14011a5c1c1c")); + assertThat(packet.getHash()) + .isEqualTo( + BytesValue.fromHexString( + "0xa1581c1705e744976d0341011c4490b3ab0b48283407ae5cf7526b9487174896")); + } + + @Test + public void shouldRoundTripPacket() { + final Packet packet = decode(VALID_PONG_PACKET); + assertThat(Hex.toHexString(packet.encode().getBytes())).isEqualTo(VALID_PONG_PACKET); + } + + private Packet decode(final String hexData) { + return Packet.decode(Buffer.buffer(Hex.decode(hexData))); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java new file mode 100755 index 00000000000..30d1117a4c6 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerDistanceCalculatorTest.java @@ -0,0 +1,68 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Random; + +import org.junit.Test; + +public class PeerDiscoveryControllerDistanceCalculatorTest { + + @Test + public void distanceZero() { + final byte[] id = new byte[64]; + new Random().nextBytes(id); + assertThat(PeerTable.distance(BytesValue.wrap(id), BytesValue.wrap(id))).isEqualTo(0); + } + + @Test + public void distance1() { + final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); + final BytesValue id2 = BytesValue.fromHexString("0x8f19400001"); + assertThat(PeerTable.distance(id1, id2)).isEqualTo(1); + } + + @Test + public void distance2() { + final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); + final BytesValue id2 = BytesValue.fromHexString("0x8f19400002"); + assertThat(PeerTable.distance(id1, id2)).isEqualTo(2); + } + + @Test + public void distance3() { + final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); + final BytesValue id2 = BytesValue.fromHexString("0x8f19400004"); + assertThat(PeerTable.distance(id1, id2)).isEqualTo(3); + } + + @Test + public void distance9() { + final BytesValue id1 = BytesValue.fromHexString("0x8f19400100"); + final BytesValue id2 = BytesValue.fromHexString("0x8f19400000"); + assertThat(PeerTable.distance(id1, id2)).isEqualTo(9); + } + + @Test + public void distance40() { + final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); + final BytesValue id2 = BytesValue.fromHexString("0x0f19400000"); + assertThat(PeerTable.distance(id1, id2)).isEqualTo(40); + } + + @Test(expected = AssertionError.class) + public void distance40_differentLengths() { + final BytesValue id1 = BytesValue.fromHexString("0x8f19400000"); + final BytesValue id2 = BytesValue.fromHexString("0x0f1940000099"); + assertThat(PeerTable.distance(id1, id2)).isEqualTo(40); + } + + @Test + public void distanceZero_emptyArrays() { + final BytesValue id1 = BytesValue.EMPTY; + final BytesValue id2 = BytesValue.EMPTY; + assertThat(PeerTable.distance(id1, id2)).isEqualTo(0); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java new file mode 100755 index 00000000000..57452fd6ecf --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryControllerTest.java @@ -0,0 +1,932 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryAgent; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper; +import net.consensys.pantheon.ethereum.p2p.peers.Endpoint; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Value; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +import io.vertx.core.Vertx; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class PeerDiscoveryControllerTest { + + private static final byte MOST_SIGNFICANT_BIT_MASK = -128; + private static final RetryDelayFunction LONG_DELAY_FUNCTION = (prev) -> 999999999L; + private static final RetryDelayFunction SHORT_DELAY_FUNCTION = (prev) -> Math.max(100, prev * 2); + private static final PeerRequirement PEER_REQUIREMENT = () -> true; + private static final long TABLE_REFRESH_INTERVAL_MS = TimeUnit.HOURS.toMillis(1); + private final Vertx vertx = spy(Vertx.vertx()); + private PeerDiscoveryAgent agent; + private PeerDiscoveryController controller; + private DiscoveryPeer peer; + private PeerTable peerTable; + + @Before + public void initializeMocks() { + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + peer = PeerDiscoveryTestHelper.generatePeers(keyPairs)[0]; + + agent = mock(PeerDiscoveryAgent.class); + when(agent.getAdvertisedPeer()).thenReturn(peer); + peerTable = new PeerTable(peer.getId()); + } + + @After + public void stopTable() { + if (controller != null) { + controller.stop().join(); + } + } + + @Test + public void bootstrapPeersRetriesSent() { + // Create peers. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(3); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keyPairs); + + // Mock the creation of the PING packet, so that we can control the hash, + // which gets validated when receiving the PONG. + final PingPacketData mockPing = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet mockPacket = Packet.create(PacketType.PING, mockPing, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> mockPacket); + + startPeerDiscoveryController(SHORT_DELAY_FUNCTION, peers); + + // Wait at most 4 seconds until all PING packets have been sent. + await() + .atMost(4, TimeUnit.SECONDS) + .untilAsserted(() -> verify(vertx, times(15)).setTimer(anyLong(), any())); + + // Within this time period, 4 timers should be placed with these timeouts. + final long[] expectedTimeouts = {100, 200, 400, 800}; + for (final long timeout : expectedTimeouts) { + verify(vertx, times(3)).setTimer(eq(timeout), any()); + } + + // Check that 5 PING packets were sent for each peer (the initial + 4 attempts following + // timeouts). + Stream.of(peers) + .forEach(p -> verify(agent, times(5)).sendPacket(eq(p), eq(PacketType.PING), any())); + + controller + .getPeers() + .forEach(p -> assertThat(p.getStatus()).isEqualTo(PeerDiscoveryStatus.BONDING)); + } + + @Test + public void bootstrapPeersRetriesStoppedUponResponse() { + // Create peers. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(3); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keyPairs); + + // Mock the creation of the PING packet, so that we can control the hash, + // which gets validated when receiving the PONG. + final PingPacketData mockPing = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet mockPacket = Packet.create(PacketType.PING, mockPing, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> mockPacket); + + startPeerDiscoveryController(SHORT_DELAY_FUNCTION, peers); + + // Wait at most 3 seconds until many PING packets attempts have been sent. + // Assert timer was invoked several times. + verify(vertx, timeout(3000).times(12)).setTimer(anyLong(), any()); + + // Assert PING packet was sent for peer[0] 4 times. + verify(agent, timeout(1000).times(4)).sendPacket(eq(peers[0]), eq(PacketType.PING), any()); + + // Simulate a PONG message from peer 0. + final PongPacketData packetData = + PongPacketData.create(peer.getEndpoint(), mockPacket.getHash()); + final Packet packet = Packet.create(PacketType.PONG, packetData, keyPairs[0]); + controller.onMessage(packet, peers[0]); + + // Ensure we receive no more PING packets for peer[0]. + verify(agent, timeout(1000).times(4)).sendPacket(eq(peers[0]), eq(PacketType.PING), any()); + } + + @Test + public void bootstrapPeersPongReceived_HashMatched() { + // Create peers. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(3); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keyPairs); + + // Mock the creation of the PING packet, so that we can control the hash, which gets validated + // when receiving + // the PONG. + final PingPacketData mockPing = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet mockPacket = Packet.create(PacketType.PING, mockPing, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> mockPacket); + + startPeerDiscoveryController(SHORT_DELAY_FUNCTION, peers); + assertThat( + controller + .getPeers() + .stream() + .filter(p -> p.getStatus() == PeerDiscoveryStatus.BONDING)) + .hasSize(3); + + // Simulate a PONG message from peer 0. + final PongPacketData packetData = + PongPacketData.create(peer.getEndpoint(), mockPacket.getHash()); + final Packet packet = Packet.create(PacketType.PONG, packetData, keyPairs[0]); + controller.onMessage(packet, peers[0]); + + // Ensure that the peer controller is now sending FIND_NEIGHBORS messages for this peer. + await() + .atMost(3, TimeUnit.SECONDS) + .untilAsserted( + () -> + verify(agent, atLeast(3)) + .sendPacket(eq(peers[0]), eq(PacketType.FIND_NEIGHBORS), any())); + + assertThat( + controller + .getPeers() + .stream() + .filter(p -> p.getStatus() == PeerDiscoveryStatus.BONDING)) + .hasSize(2); + assertThat( + controller.getPeers().stream().filter(p -> p.getStatus() == PeerDiscoveryStatus.BONDED)) + .hasSize(1); + } + + @Test + public void bootstrapPeersPongReceived_HashUnmatched() { + // Create peers. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(3); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keyPairs); + + // Mock the creation of the PING packet, so that we can control the hash, which gets validated + // when + // processing the PONG. + final PingPacketData mockPing = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet mockPacket = Packet.create(PacketType.PING, mockPing, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> mockPacket); + + startPeerDiscoveryController(peers); + + assertThat( + controller + .getPeers() + .stream() + .filter(p -> p.getStatus() == PeerDiscoveryStatus.BONDING)) + .hasSize(3); + + // Send a PONG packet from peer 1, with an incorrect hash. + final PongPacketData packetData = + PongPacketData.create(peer.getEndpoint(), BytesValue.fromHexString("1212")); + final Packet packet = Packet.create(PacketType.PONG, packetData, keyPairs[1]); + controller.onMessage(packet, peers[1]); + + // No FIND_NEIGHBORS packet was sent for peer 1. + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> + verify(agent, never()) + .sendPacket(eq(peers[1]), eq(PacketType.FIND_NEIGHBORS), any())); + + assertThat( + controller + .getPeers() + .stream() + .filter(p -> p.getStatus() == PeerDiscoveryStatus.BONDING)) + .hasSize(3); + } + + @Test + public void findNeighborsSentAfterBondingFinished() { + // Create three peers, out of which the first two are bootstrap peers. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keyPairs); + + // Mock the creation of the PING packet, so that we can control the hash, which gets validated + // when + // processing the PONG. + final PingPacketData mockPing = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet mockPacket = Packet.create(PacketType.PING, mockPing, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> mockPacket); + + // Initialize the peer controller, setting a high controller refresh interval and a high timeout + // threshold, + // to avoid retries getting in the way of this test. + controller = + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Collections.singletonList(peers[0]), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + new PeerBlacklist()); + controller.setRetryDelayFunction((prev) -> 999999999L); + controller.start(); + + // Verify that the PING was sent. + await() + .atMost(2, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, times(1)).sendPacket(eq(peers[0]), eq(PacketType.PING), any()); + }); + + // Simulate a PONG message from peer[0]. + final PongPacketData packetData = + PongPacketData.create(peer.getEndpoint(), mockPacket.getHash()); + final Packet pongPacket = Packet.create(PacketType.PONG, packetData, keyPairs[0]); + controller.onMessage(pongPacket, peers[0]); + + // Verify that the FIND_NEIGHBORS packet was sent with target == self. + final ArgumentCaptor captor = ArgumentCaptor.forClass(PacketData.class); + await() + .atMost(2, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, times(1)) + .sendPacket(eq(peers[0]), eq(PacketType.FIND_NEIGHBORS), captor.capture()); + }); + + assertThat(captor.getValue()).isInstanceOf(FindNeighborsPacketData.class); + final FindNeighborsPacketData data = (FindNeighborsPacketData) captor.getValue(); + assertThat(data.getTarget()).isEqualTo(peer.getId()); + assertThat(controller.getPeers()).hasSize(1); + assertThat(controller.getPeers().stream().findFirst().get().getStatus()) + .isEqualTo(PeerDiscoveryStatus.BONDED); + } + + @Test + public void peerSeenTwice() throws InterruptedException { + // Create three peers, out of which the first two are bootstrap peers. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(3); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keyPairs); + + // Mock the creation of the PING packet, so that we can control the hash, which gets validated + // when + // processing the PONG. + final PingPacketData mockPing = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet mockPacket = Packet.create(PacketType.PING, mockPing, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> mockPacket); + + // Initialize the peer controller, setting a high controller refresh interval and a high timeout + // threshold, to avoid retries + // getting in the way of this test. + controller = + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Arrays.asList(peers[0], peers[1]), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + new PeerBlacklist()); + controller.setRetryDelayFunction((prev) -> 999999999L); + controller.start(); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(eq(peers[0]), eq(PacketType.PING), any()); + verify(agent, atLeast(1)).sendPacket(eq(peers[1]), eq(PacketType.PING), any()); + }); + + // Simulate a PONG message from peer[0]. + final PongPacketData packetData = + PongPacketData.create(peer.getEndpoint(), mockPacket.getHash()); + Packet pongPacket = Packet.create(PacketType.PONG, packetData, keyPairs[0]); + controller.onMessage(pongPacket, peers[0]); + + // Simulate a NEIGHBORS message from peer[0] listing peer[2]. + final NeighborsPacketData neighbors = + NeighborsPacketData.create(Collections.singletonList(peers[2])); + Packet neighborsPacket = Packet.create(PacketType.NEIGHBORS, neighbors, keyPairs[0]); + controller.onMessage(neighborsPacket, peers[0]); + + // Assert that we're bonding with the third peer. + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + assertThat(controller.getPeers()).hasSize(2); + assertThat(controller.getPeers()) + .filteredOn(p -> p.getStatus() == PeerDiscoveryStatus.BONDING) + .hasSize(1); + assertThat(controller.getPeers()) + .filteredOn(p -> p.getStatus() == PeerDiscoveryStatus.BONDED) + .hasSize(1); + }); + + // Send a PONG packet from peer[2], to transition it to the BONDED state. + pongPacket = Packet.create(PacketType.PONG, packetData, keyPairs[2]); + controller.onMessage(pongPacket, peers[2]); + + // Assert we're now bonded with peer[2]. + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> + assertThat(controller.getPeers()) + .filteredOn( + p -> p.equals(peers[2]) && p.getStatus() == PeerDiscoveryStatus.BONDED) + .hasSize(1)); + + // Simulate bonding and neighbors packet from the second boostrap peer, with peer[2] reported in + // the peer list. + pongPacket = Packet.create(PacketType.PONG, packetData, keyPairs[1]); + controller.onMessage(pongPacket, peers[1]); + neighborsPacket = Packet.create(PacketType.NEIGHBORS, neighbors, keyPairs[1]); + controller.onMessage(neighborsPacket, peers[1]); + + // Wait for 1 second and ensure that only 1 PING was ever sent to peer[2]. + Thread.sleep(1000); + verify(agent, times(1)).sendPacket(eq(peers[2]), eq(PacketType.PING), any()); + } + + @Test(expected = IllegalStateException.class) + public void startTwice() { + startPeerDiscoveryController(); + controller.start(); + } + + @Test + public void stopTwice() { + startPeerDiscoveryController(); + controller.stop(); + controller.stop(); + // no exception + } + + @Test + public void shouldAddNewPeerWhenReceivedPingAndPeerTableBucketIsNotFull() { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 1); + startPeerDiscoveryController(); + + final Packet pingPacket = mockPingPacket(peers[0], peer); + controller.onMessage(pingPacket, peers[0]); + assertThat(controller.getPeers()).contains(peers[0]); + } + + @Test + public void shouldNotAddSelfWhenReceivedPingFromSelf() { + startPeerDiscoveryController(); + final DiscoveryPeer self = new DiscoveryPeer(peer.getId(), peer.getEndpoint()); + + final Packet pingPacket = mockPingPacket(peer, peer); + controller.onMessage(pingPacket, self); + + assertThat(controller.getPeers()).doesNotContain(self); + } + + @Test + public void shouldAddNewPeerWhenReceivedPingAndPeerTableBucketIsFull() { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 17); + startPeerDiscoveryController(); + // Fill the last bucket. + for (int i = 0; i < 16; i++) { + peerTable.tryAdd(peers[i]); + } + + final Packet pingPacket = mockPingPacket(peers[16], peer); + controller.onMessage(pingPacket, peers[16]); + + assertThat(controller.getPeers()).contains(peers[16]); + // The first peer added should have been evicted. + assertThat(controller.getPeers()).doesNotContain(peers[0]); + } + + @Test + public void shouldNotRemoveExistingPeerWhenReceivedPing() { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 1); + startPeerDiscoveryController(); + + peerTable.tryAdd(peers[0]); + assertThat(controller.getPeers()).contains(peers[0]); + + final Packet pingPacket = mockPingPacket(peers[0], peer); + controller.onMessage(pingPacket, peers[0]); + + assertThat(controller.getPeers()).contains(peers[0]); + } + + @Test + public void shouldNotAddNewPeerWhenReceivedPongFromBlacklistedPeer() + throws InterruptedException, ExecutionException, TimeoutException { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 3); + final DiscoveryPeer discoPeer = peers[0]; + final DiscoveryPeer otherPeer = peers[1]; + final DiscoveryPeer otherPeer2 = peers[2]; + + final PeerBlacklist blacklist = new PeerBlacklist(); + controller = + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Collections.singletonList(discoPeer), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + blacklist); + + final Endpoint agentEndpoint = agent.getAdvertisedPeer().getEndpoint(); + + // Setup ping to be sent to discoPeer + SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + PingPacketData pingPacketData = PingPacketData.create(agentEndpoint, discoPeer.getEndpoint()); + final Packet discoPeerPing = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(discoPeerPing).when(agent).sendPacket(eq(discoPeer), eq(PacketType.PING), any()); + + controller.start(); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongFromDiscoPeer = + MockPacketDataFactory.mockPongPacket(discoPeer, discoPeerPing.getHash()); + controller.onMessage(pongFromDiscoPeer, discoPeer); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)) + .sendPacket(eq(discoPeer), eq(PacketType.FIND_NEIGHBORS), any()); + }); + + // Setup ping to be sent to otherPeer after neighbors packet is received + keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + pingPacketData = PingPacketData.create(agentEndpoint, otherPeer.getEndpoint()); + final Packet pingPacket = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(pingPacket).when(agent).sendPacket(eq(otherPeer), eq(PacketType.PING), any()); + + // Setup ping to be sent to otherPeer2 after neighbors packet is received + keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + pingPacketData = PingPacketData.create(agentEndpoint, otherPeer2.getEndpoint()); + final Packet pingPacket2 = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(pingPacket2).when(agent).sendPacket(eq(otherPeer2), eq(PacketType.PING), any()); + + final Packet neighborsPacket = + MockPacketDataFactory.mockNeighborsPacket(discoPeer, otherPeer, otherPeer2); + controller.onMessage(neighborsPacket, discoPeer); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(2)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongPacket = MockPacketDataFactory.mockPongPacket(otherPeer, pingPacket.getHash()); + controller.onMessage(pongPacket, otherPeer); + + // Blaclist otherPeer2 before sending return pong + blacklist.add(otherPeer2); + final Packet pongPacket2 = + MockPacketDataFactory.mockPongPacket(otherPeer2, pingPacket2.getHash()); + controller.onMessage(pongPacket2, otherPeer2); + + assertThat(controller.getPeers()).hasSize(2); + assertThat(controller.getPeers()).contains(otherPeer); + assertThat(controller.getPeers()).doesNotContain(otherPeer2); + } + + @Test + public void shouldNotBondWithBlacklistedPeer() + throws InterruptedException, ExecutionException, TimeoutException { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 3); + final DiscoveryPeer discoPeer = peers[0]; + final DiscoveryPeer otherPeer = peers[1]; + final DiscoveryPeer otherPeer2 = peers[2]; + + final PeerBlacklist blacklist = new PeerBlacklist(); + controller = + spy( + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Collections.singletonList(discoPeer), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + blacklist)); + + final Endpoint agentEndpoint = agent.getAdvertisedPeer().getEndpoint(); + + // Setup ping to be sent to discoPeer + SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + PingPacketData pingPacketData = PingPacketData.create(agentEndpoint, discoPeer.getEndpoint()); + final Packet discoPeerPing = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(discoPeerPing).when(agent).sendPacket(eq(discoPeer), eq(PacketType.PING), any()); + + controller.start(); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongFromDiscoPeer = + MockPacketDataFactory.mockPongPacket(discoPeer, discoPeerPing.getHash()); + controller.onMessage(pongFromDiscoPeer, discoPeer); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)) + .sendPacket(eq(discoPeer), eq(PacketType.FIND_NEIGHBORS), any()); + }); + + // Setup ping to be sent to otherPeer after neighbors packet is received + keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + pingPacketData = PingPacketData.create(agentEndpoint, otherPeer.getEndpoint()); + final Packet pingPacket = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(pingPacket).when(agent).sendPacket(eq(otherPeer), eq(PacketType.PING), any()); + + // Setup ping to be sent to otherPeer2 after neighbors packet is received + keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + pingPacketData = PingPacketData.create(agentEndpoint, otherPeer2.getEndpoint()); + final Packet pingPacket2 = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(pingPacket2).when(agent).sendPacket(eq(otherPeer2), eq(PacketType.PING), any()); + + // Blacklist peer + blacklist.add(otherPeer); + + final Packet neighborsPacket = + MockPacketDataFactory.mockNeighborsPacket(discoPeer, otherPeer, otherPeer2); + controller.onMessage(neighborsPacket, discoPeer); + + verify(controller, times(0)).bond(otherPeer, false); + verify(controller, times(1)).bond(otherPeer2, false); + } + + @Test + public void shouldRespondToNeighborsRequestFromKnownPeer() + throws InterruptedException, ExecutionException, TimeoutException { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 1); + final DiscoveryPeer discoPeer = peers[0]; + + final PeerBlacklist blacklist = new PeerBlacklist(); + controller = + spy( + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Collections.singletonList(discoPeer), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + blacklist)); + + final Endpoint agentEndpoint = agent.getAdvertisedPeer().getEndpoint(); + + // Setup ping to be sent to discoPeer + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + final PingPacketData pingPacketData = + PingPacketData.create(agentEndpoint, discoPeer.getEndpoint()); + final Packet discoPeerPing = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(discoPeerPing).when(agent).sendPacket(eq(discoPeer), eq(PacketType.PING), any()); + + controller.start(); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongFromDiscoPeer = + MockPacketDataFactory.mockPongPacket(discoPeer, discoPeerPing.getHash()); + controller.onMessage(pongFromDiscoPeer, discoPeer); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)) + .sendPacket(eq(discoPeer), eq(PacketType.FIND_NEIGHBORS), any()); + }); + + final Packet findNeighborsPacket = MockPacketDataFactory.mockFindNeighborsPacket(discoPeer); + controller.onMessage(findNeighborsPacket, discoPeer); + + verify(agent, times(1)).sendPacket(eq(discoPeer), eq(PacketType.NEIGHBORS), any()); + } + + @Test + public void shouldNotRespondToNeighborsRequestFromUnknownPeer() + throws InterruptedException, ExecutionException, TimeoutException { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 2); + final DiscoveryPeer discoPeer = peers[0]; + final DiscoveryPeer otherPeer = peers[1]; + + final PeerBlacklist blacklist = new PeerBlacklist(); + controller = + spy( + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Collections.singletonList(discoPeer), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + blacklist)); + + final Endpoint agentEndpoint = agent.getAdvertisedPeer().getEndpoint(); + + // Setup ping to be sent to discoPeer + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + final PingPacketData pingPacketData = + PingPacketData.create(agentEndpoint, discoPeer.getEndpoint()); + final Packet discoPeerPing = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(discoPeerPing).when(agent).sendPacket(eq(discoPeer), eq(PacketType.PING), any()); + + controller.start(); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongFromDiscoPeer = + MockPacketDataFactory.mockPongPacket(discoPeer, discoPeerPing.getHash()); + controller.onMessage(pongFromDiscoPeer, discoPeer); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)) + .sendPacket(eq(discoPeer), eq(PacketType.FIND_NEIGHBORS), any()); + }); + + final Packet findNeighborsPacket = MockPacketDataFactory.mockFindNeighborsPacket(discoPeer); + controller.onMessage(findNeighborsPacket, otherPeer); + + verify(agent, times(0)).sendPacket(eq(otherPeer), eq(PacketType.NEIGHBORS), any()); + } + + @Test + public void shouldNotRespondToNeighborsRequestFromBlacklistedPeer() + throws InterruptedException, ExecutionException, TimeoutException { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 1); + final DiscoveryPeer discoPeer = peers[0]; + + final PeerBlacklist blacklist = new PeerBlacklist(); + controller = + spy( + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Collections.singletonList(discoPeer), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + blacklist)); + + final Endpoint agentEndpoint = agent.getAdvertisedPeer().getEndpoint(); + + // Setup ping to be sent to discoPeer + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + final PingPacketData pingPacketData = + PingPacketData.create(agentEndpoint, discoPeer.getEndpoint()); + final Packet discoPeerPing = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + doReturn(discoPeerPing).when(agent).sendPacket(eq(discoPeer), eq(PacketType.PING), any()); + + controller.start(); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongFromDiscoPeer = + MockPacketDataFactory.mockPongPacket(discoPeer, discoPeerPing.getHash()); + controller.onMessage(pongFromDiscoPeer, discoPeer); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)) + .sendPacket(eq(discoPeer), eq(PacketType.FIND_NEIGHBORS), any()); + }); + + blacklist.add(discoPeer); + final Packet findNeighborsPacket = MockPacketDataFactory.mockFindNeighborsPacket(discoPeer); + controller.onMessage(findNeighborsPacket, discoPeer); + + verify(agent, times(0)).sendPacket(eq(discoPeer), eq(PacketType.NEIGHBORS), any()); + } + + @Test + public void shouldAddNewPeerWhenReceivedPongAndPeerTableBucketIsNotFull() { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 1); + + // Mock the creation of the PING packet to control hash for PONG. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + final PingPacketData pingPacketData = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet pingPacket = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> pingPacket); + + controller = + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Arrays.asList(peers[0]), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + new PeerBlacklist()); + controller.setRetryDelayFunction((prev) -> 999999999L); + controller.start(); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongPacket = MockPacketDataFactory.mockPongPacket(peers[0], pingPacket.getHash()); + controller.onMessage(pongPacket, peers[0]); + + assertThat(controller.getPeers()).contains(peers[0]); + } + + @Test + public void shouldAddNewPeerWhenReceivedPongAndPeerTableBucketIsFull() { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 17); + + // Mock the creation of PING packets to control hash PONG packets. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + final PingPacketData pingPacketData = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet pingPacket = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> pingPacket); + + final DiscoveryPeer[] bootstrapPeers = Arrays.copyOfRange(peers, 0, 16); + startPeerDiscoveryController(bootstrapPeers); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(16)).sendPacket(any(), eq(PacketType.PING), any()); + }); + + final Packet pongPacket = MockPacketDataFactory.mockPongPacket(peers[0], pingPacket.getHash()); + controller.onMessage(pongPacket, peers[0]); + + final Packet neighborsPacket = MockPacketDataFactory.mockNeighborsPacket(peers[0], peers[16]); + controller.onMessage(neighborsPacket, peers[0]); + + final Packet pongPacket2 = + MockPacketDataFactory.mockPongPacket(peers[16], pingPacket.getHash()); + controller.onMessage(pongPacket2, peers[16]); + + assertThat(controller.getPeers()).contains(peers[16]); + // Explain + assertThat(controller.getPeers()).doesNotContain(peers[1]); + } + + @Test + public void shouldNotAddPeerInNeighborsPacketWithoutBonding() { + final DiscoveryPeer[] peers = createPeersInLastBucket(peer, 2); + + // Mock the creation of the PING packet to control hash for PONG. + final SECP256K1.KeyPair[] keyPairs = PeerDiscoveryTestHelper.generateKeyPairs(1); + final PingPacketData pingPacketData = + PingPacketData.create(peer.getEndpoint(), peers[0].getEndpoint()); + final Packet pingPacket = Packet.create(PacketType.PING, pingPacketData, keyPairs[0]); + when(agent.sendPacket(any(), eq(PacketType.PING), any())).then((invocation) -> pingPacket); + + startPeerDiscoveryController(peers[0]); + + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted( + () -> { + verify(agent, atLeast(1)).sendPacket(eq(peers[0]), eq(PacketType.PING), any()); + }); + + final Packet pongPacket = MockPacketDataFactory.mockPongPacket(peers[0], pingPacket.getHash()); + controller.onMessage(pongPacket, peers[0]); + + await() + .atMost(3, TimeUnit.SECONDS) + .untilAsserted( + () -> + verify(agent, atLeast(1)) + .sendPacket(eq(peers[0]), eq(PacketType.FIND_NEIGHBORS), any())); + + assertThat(controller.getPeers()).doesNotContain(peers[1]); + } + + private static Packet mockPingPacket(final Peer from, final Peer to) { + final Packet packet = mock(Packet.class); + + final PingPacketData pingPacketData = + PingPacketData.create(from.getEndpoint(), to.getEndpoint()); + when(packet.getPacketData(any())).thenReturn(Optional.of(pingPacketData)); + final BytesValue id = from.getId(); + when(packet.getNodeId()).thenReturn(id); + when(packet.getType()).thenReturn(PacketType.PING); + when(packet.getHash()).thenReturn(Bytes32.ZERO); + + return packet; + } + + private static DiscoveryPeer[] createPeersInLastBucket(final Peer host, final int n) { + final DiscoveryPeer[] newPeers = new DiscoveryPeer[n]; + + // Flipping the most significant bit of the keccak256 will place the peer + // in the last bucket for the corresponding host peer. + final Bytes32 keccak256 = host.keccak256(); + final MutableBytesValue template = MutableBytesValue.create(keccak256.size()); + byte msb = keccak256.get(0); + msb ^= MOST_SIGNFICANT_BIT_MASK; + template.set(0, msb); + + for (int i = 0; i < n; i++) { + template.setInt(template.size() - 4, i); + final Bytes32 newKeccak256 = Bytes32.leftPad(template.copy()); + final DiscoveryPeer newPeer = mock(DiscoveryPeer.class); + when(newPeer.keccak256()).thenReturn(newKeccak256); + final MutableBytesValue newId = MutableBytesValue.create(64); + UInt256.of(i).getBytes().copyTo(newId, newId.size() - UInt256Value.SIZE); + when(newPeer.getId()).thenReturn(newId); + when(newPeer.getEndpoint()).thenReturn(host.getEndpoint()); + newPeers[i] = newPeer; + } + + return newPeers; + } + + private void startPeerDiscoveryController(final DiscoveryPeer... bootstrapPeers) { + startPeerDiscoveryController(LONG_DELAY_FUNCTION, bootstrapPeers); + } + + private void startPeerDiscoveryController( + final RetryDelayFunction retryDelayFunction, final DiscoveryPeer... bootstrapPeers) { + // Create the controller. + controller = + new PeerDiscoveryController( + vertx, + agent, + peerTable, + Arrays.asList(bootstrapPeers), + TABLE_REFRESH_INTERVAL_MS, + PEER_REQUIREMENT, + new PeerBlacklist()); + controller.setRetryDelayFunction(retryDelayFunction); + controller.start(); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryTableRefreshTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryTableRefreshTest.java new file mode 100755 index 00000000000..9552a1fd997 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerDiscoveryTableRefreshTest.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryAgent; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Vertx; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class PeerDiscoveryTableRefreshTest { + private final Vertx vertx = spy(Vertx.vertx()); + + @Test + public void tableRefreshSingleNode() { + final SECP256K1.KeyPair[] keypairs = PeerDiscoveryTestHelper.generateKeyPairs(2); + final DiscoveryPeer[] peers = PeerDiscoveryTestHelper.generateDiscoveryPeers(keypairs); + + final PeerDiscoveryAgent agent = mock(PeerDiscoveryAgent.class); + when(agent.getAdvertisedPeer()).thenReturn(peers[0]); + + // Create and start the PeerDiscoveryController, setting the refresh interval to something + // small. + final PeerDiscoveryController controller = + new PeerDiscoveryController( + vertx, + agent, + new PeerTable(agent.getAdvertisedPeer().getId()), + emptyList(), + 100, + () -> true, + new PeerBlacklist()); + controller.start(); + + // Send a PING, so as to add a Peer in the controller. + final PingPacketData ping = + PingPacketData.create(peers[1].getEndpoint(), peers[0].getEndpoint()); + final Packet packet = Packet.create(PacketType.PING, ping, keypairs[1]); + controller.onMessage(packet, peers[1]); + + // Wait until the controller has added the newly found peer. + await() + .atMost(1, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(controller.getPeers()).hasSize(1)); + + // As the controller performs refreshes, it'll send FIND_NEIGHBORS packets with random target + // IDs every time. + // We capture the packets so that we can later assert on them. + // Within 1000ms, there should be ~10 packets. But let's be less ambitious and expect at least + // 5. + final ArgumentCaptor packetDataCaptor = ArgumentCaptor.forClass(PacketData.class); + verify(agent, timeout(1000).atLeast(5)) + .sendPacket(eq(peers[1]), eq(PacketType.FIND_NEIGHBORS), packetDataCaptor.capture()); + + // Assert that all packets were FIND_NEIGHBORS packets. + final List targets = new ArrayList<>(); + for (final PacketData data : packetDataCaptor.getAllValues()) { + assertThat(data).isExactlyInstanceOf(FindNeighborsPacketData.class); + final FindNeighborsPacketData fnpd = (FindNeighborsPacketData) data; + targets.add(fnpd.getTarget()); + } + + assertThat(targets.size()).isGreaterThanOrEqualTo(5); + + // All targets are unique. + assertThat(targets.size()).isEqualTo(new HashSet<>(targets).size()); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTableTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTableTest.java new file mode 100755 index 00000000000..747f4290175 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/discovery/internal/PeerTableTest.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.ethereum.p2p.discovery.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryTestHelper; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerTable.AddResult.Outcome; +import net.consensys.pantheon.ethereum.p2p.peers.Peer; + +import org.junit.Test; + +public class PeerTableTest { + + @Test + public void addPeer() { + final PeerTable table = new PeerTable(Peer.randomId(), 16); + final DiscoveryPeer[] peers = + PeerDiscoveryTestHelper.generateDiscoveryPeers(PeerDiscoveryTestHelper.generateKeyPairs(5)); + + for (final DiscoveryPeer peer : peers) { + final PeerTable.AddResult result = table.tryAdd(peer); + assertThat(result.getOutcome()).isEqualTo(Outcome.ADDED); + } + + assertThat(table.getAllPeers()).hasSize(5); + } + + @Test + public void addSelf() { + final DiscoveryPeer self = new DiscoveryPeer(Peer.randomId(), "127.0.0.1", 12345, 12345); + final PeerTable table = new PeerTable(self.getId(), 16); + final PeerTable.AddResult result = table.tryAdd(self); + + assertThat(result.getOutcome()).isEqualTo(Outcome.SELF); + assertThat(table.getAllPeers()).hasSize(0); + } + + @Test + public void peerExists() { + final PeerTable table = new PeerTable(Peer.randomId(), 16); + final DiscoveryPeer peer = + PeerDiscoveryTestHelper.generateDiscoveryPeers(PeerDiscoveryTestHelper.generateKeyPairs(1))[ + 0]; + + assertThat(table.tryAdd(peer).getOutcome()).isEqualTo(Outcome.ADDED); + + assertThat(table.tryAdd(peer)) + .satisfies( + result -> { + assertThat(result.getOutcome()).isEqualTo(Outcome.ALREADY_EXISTED); + assertThat(result.getEvictionCandidate()).isNull(); + }); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexerTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexerTest.java new file mode 100755 index 00000000000..5e5feab06ea --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/CapabilityMultiplexerTest.java @@ -0,0 +1,102 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.p2p.NetworkMemoryPool; +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.netty.CapabilityMultiplexer.ProtocolMessage; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.netty.buffer.ByteBuf; +import org.junit.Test; + +public class CapabilityMultiplexerTest { + + @Test + public void multiplexer() { + // Set up some capabilities + final Capability eth61 = Capability.create("eth", 61); + final Capability eth62 = Capability.create("eth", 62); + final Capability eth63 = Capability.create("eth", 63); + final Capability shh1 = Capability.create("shh", 1); + final Capability shh2 = Capability.create("shh", 2); + final Capability bzz1 = Capability.create("bzz", 1); + final Capability fake = Capability.create("bla", 10); + + // Define protocols with message spaces + final List subProtocols = + Arrays.asList( + subProtocol(eth61.getName(), 8), + subProtocol(shh1.getName(), 11), + subProtocol(bzz1.getName(), 25)); + + // Calculate capabilities + final List capSetA = Arrays.asList(eth61, eth62, bzz1, shh2); + final List capSetB = Arrays.asList(eth61, eth62, eth63, bzz1, shh1, fake); + final CapabilityMultiplexer multiplexerA = + new CapabilityMultiplexer(subProtocols, capSetA, capSetB); + final CapabilityMultiplexer multiplexerB = + new CapabilityMultiplexer(subProtocols, capSetB, capSetA); + + // Check expected overlap + final Set expectedCaps = new HashSet<>(Arrays.asList(eth62, bzz1)); + assertThat(multiplexerA.getAgreedCapabilities()).isEqualTo(expectedCaps); + assertThat(multiplexerB.getAgreedCapabilities()).isEqualTo(expectedCaps); + + // Multiplex a message and check the value + final ByteBuf ethData = NetworkMemoryPool.allocate(5); + ethData.writeBytes(new byte[] {1, 2, 3, 4, 5}); + final int ethCode = 1; + final MessageData ethMessage = new RawMessage(ethCode, ethData); + // Check offset + final int expectedOffset = CapabilityMultiplexer.WIRE_PROTOCOL_MESSAGE_SPACE + 25; + assertThat(multiplexerA.multiplex(eth62, ethMessage).getCode()) + .isEqualTo(ethCode + expectedOffset); + assertThat(multiplexerB.multiplex(eth62, ethMessage).getCode()) + .isEqualTo(ethCode + expectedOffset); + // Check data is unchanged + final ByteBuf multiplexedData = NetworkMemoryPool.allocate(ethMessage.getSize()); + multiplexerA.multiplex(eth62, ethMessage).writeTo(multiplexedData); + assertThat(multiplexedData).isEqualTo(ethData); + + // Demultiplex and check value + final MessageData multiplexedEthMessage = new RawMessage(ethCode + expectedOffset, ethData); + ProtocolMessage demultiplexed = multiplexerA.demultiplex(multiplexedEthMessage); + final ByteBuf demultiplexedData = NetworkMemoryPool.allocate(ethMessage.getSize()); + demultiplexed.getMessage().writeTo(demultiplexedData); + // Check returned result + assertThat(demultiplexed.getMessage().getCode()).isEqualTo(ethCode); + assertThat(demultiplexed.getCapability()).isEqualTo(eth62); + assertThat(demultiplexedData).isEqualTo(ethData); + demultiplexed = multiplexerB.demultiplex(multiplexedEthMessage); + assertThat(demultiplexed.getMessage().getCode()).isEqualTo(ethCode); + assertThat(demultiplexed.getCapability()).isEqualTo(eth62); + assertThat(demultiplexedData).isEqualTo(ethData); + } + + private SubProtocol subProtocol(final String name, final int messageSpace) { + return new SubProtocol() { + @Override + public String getName() { + return name; + } + + @Override + public int messageSpace(final int protocolVersion) { + return messageSpace; + } + + @Override + public boolean isValidMessageCode(final int protocolVersion, final int code) { + return true; + } + }; + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnectionTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnectionTest.java new file mode 100755 index 00000000000..ae2a0b8d1dc --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/NettyPeerConnectionTest.java @@ -0,0 +1,47 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import static java.util.Collections.emptyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection.PeerNotConnected; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.ethereum.p2p.wire.messages.HelloMessage; +import net.consensys.pantheon.util.bytes.BytesValue; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoop; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; + +public class NettyPeerConnectionTest { + + private final ChannelHandlerContext context = mock(ChannelHandlerContext.class); + private final Channel channel = mock(Channel.class); + private final ChannelFuture closeFuture = mock(ChannelFuture.class); + private final EventLoop eventLoop = mock(EventLoop.class); + private final CapabilityMultiplexer multiplexer = mock(CapabilityMultiplexer.class); + private final Callbacks callbacks = mock(Callbacks.class); + private final PeerInfo peerInfo = new PeerInfo(5, "foo", emptyList(), 0, BytesValue.of(1)); + + private NettyPeerConnection connection; + + @Before + public void setUp() { + when(context.channel()).thenReturn(channel); + when(channel.closeFuture()).thenReturn(closeFuture); + when(channel.eventLoop()).thenReturn(eventLoop); + connection = new NettyPeerConnection(context, peerInfo, multiplexer, callbacks); + } + + @Test + public void shouldThrowExceptionWhenAttemptingToSendMessageOnClosedConnection() { + connection.disconnect(DisconnectReason.SUBPROTOCOL_TRIGGERED); + Assertions.assertThatThrownBy(() -> connection.send(null, HelloMessage.create(peerInfo))) + .isInstanceOfAny(PeerNotConnected.class); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java new file mode 100755 index 00000000000..b56aa96cb44 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/netty/PeerConnectionRegistryTest.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.ethereum.p2p.netty; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Before; +import org.junit.Test; + +public class PeerConnectionRegistryTest { + + private static final BytesValue PEER1_ID = BytesValue.wrap(new byte[] {1}); + private static final BytesValue PEER2_ID = BytesValue.wrap(new byte[] {2}); + private final PeerConnection connection1 = mock(PeerConnection.class); + private final PeerConnection connection2 = mock(PeerConnection.class); + + private final PeerConnectionRegistry registry = new PeerConnectionRegistry(); + + @Before + public void setUp() { + when(connection1.getPeer()).thenReturn(new PeerInfo(5, "client1", emptyList(), 10, PEER1_ID)); + when(connection2.getPeer()).thenReturn(new PeerInfo(5, "client2", emptyList(), 10, PEER2_ID)); + } + + @Test + public void shouldRegisterConnections() { + registry.registerConnection(connection1); + assertThat(registry.getPeerConnections()).containsOnly(connection1); + assertThat(registry.size()).isEqualTo(1); + + registry.registerConnection(connection2); + assertThat(registry.getPeerConnections()).containsOnly(connection1, connection2); + assertThat(registry.size()).isEqualTo(2); + } + + @Test + public void shouldUnregisterConnections() { + registry.registerConnection(connection1); + registry.registerConnection(connection2); + registry.onDisconnect(connection1, DisconnectReason.TCP_SUBSYSTEM_ERROR, false); + assertThat(registry.getPeerConnections()).containsOnly(connection2); + assertThat(registry.size()).isEqualTo(1); + } + + @Test + public void shouldReportWhenPeerIsNotConnected() { + assertThat(registry.isAlreadyConnected(PEER1_ID)).isFalse(); + } + + @Test + public void shouldReportWhenPeerIsConnected() { + registry.registerConnection(connection1); + assertThat(registry.isAlreadyConnected(PEER1_ID)).isTrue(); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklistTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklistTest.java new file mode 100755 index 00000000000..d8699710ece --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerBlacklistTest.java @@ -0,0 +1,127 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.ethereum.p2p.api.PeerConnection; +import net.consensys.pantheon.ethereum.p2p.wire.PeerInfo; +import net.consensys.pantheon.ethereum.p2p.wire.messages.DisconnectMessage.DisconnectReason; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class PeerBlacklistTest { + private int nodeIdValue = 1; + + @Test + public void doesNotBlacklistPeerForNormalDisconnect() throws Exception { + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerConnection peer = generatePeerConnection(); + + assertThat(blacklist.contains(peer)).isFalse(); + + blacklist.onDisconnect(peer, DisconnectReason.TOO_MANY_PEERS, false); + + assertThat(blacklist.contains(peer)).isFalse(); + } + + @Test + public void blacklistPeerForBadBehavior() throws Exception { + + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerConnection peer = generatePeerConnection(); + + assertThat(blacklist.contains(peer)).isFalse(); + + blacklist.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, false); + + assertThat(blacklist.contains(peer)).isTrue(); + } + + @Test + public void doesNotBlacklistPeerForOurBadBehavior() throws Exception { + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerConnection peer = generatePeerConnection(); + + assertThat(blacklist.contains(peer)).isFalse(); + + blacklist.onDisconnect(peer, DisconnectReason.BREACH_OF_PROTOCOL, true); + + assertThat(blacklist.contains(peer)).isFalse(); + } + + @Test + public void blacklistIncompatiblePeer() throws Exception { + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerConnection peer = generatePeerConnection(); + + assertThat(blacklist.contains(peer)).isFalse(); + + blacklist.onDisconnect(peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, false); + + assertThat(blacklist.contains(peer)).isTrue(); + } + + @Test + public void blacklistIncompatiblePeerWhoIssuesDisconnect() throws Exception { + final PeerBlacklist blacklist = new PeerBlacklist(); + final PeerConnection peer = generatePeerConnection(); + + assertThat(blacklist.contains(peer)).isFalse(); + + blacklist.onDisconnect(peer, DisconnectReason.INCOMPATIBLE_P2P_PROTOCOL_VERSION, true); + + assertThat(blacklist.contains(peer)).isTrue(); + } + + @Test + public void capsSizeOfList() throws Exception { + + final PeerBlacklist blacklist = new PeerBlacklist(2); + final PeerConnection peer1 = generatePeerConnection(); + final PeerConnection peer2 = generatePeerConnection(); + final PeerConnection peer3 = generatePeerConnection(); + + // Add first peer + blacklist.onDisconnect(peer1, DisconnectReason.BREACH_OF_PROTOCOL, false); + assertThat(blacklist.contains(peer1)).isTrue(); + assertThat(blacklist.contains(peer2)).isFalse(); + assertThat(blacklist.contains(peer3)).isFalse(); + + // Add second peer + blacklist.onDisconnect(peer2, DisconnectReason.BREACH_OF_PROTOCOL, false); + assertThat(blacklist.contains(peer1)).isTrue(); + assertThat(blacklist.contains(peer2)).isTrue(); + assertThat(blacklist.contains(peer3)).isFalse(); + + // Adding third peer should kick out least recently accessed peer + blacklist.onDisconnect(peer3, DisconnectReason.BREACH_OF_PROTOCOL, false); + assertThat(blacklist.contains(peer1)).isFalse(); + assertThat(blacklist.contains(peer2)).isTrue(); + assertThat(blacklist.contains(peer3)).isTrue(); + + // Adding peer1 back in should kick out peer2 + blacklist.onDisconnect(peer1, DisconnectReason.BREACH_OF_PROTOCOL, false); + assertThat(blacklist.contains(peer1)).isTrue(); + assertThat(blacklist.contains(peer2)).isFalse(); + assertThat(blacklist.contains(peer3)).isTrue(); + + // Adding peer2 back in should kick out peer3 + blacklist.onDisconnect(peer2, DisconnectReason.BREACH_OF_PROTOCOL, false); + assertThat(blacklist.contains(peer1)).isTrue(); + assertThat(blacklist.contains(peer2)).isTrue(); + assertThat(blacklist.contains(peer3)).isFalse(); + } + + private PeerConnection generatePeerConnection() { + final BytesValue nodeId = BytesValue.of(nodeIdValue++); + final PeerConnection peer = mock(PeerConnection.class); + final PeerInfo peerInfo = mock(PeerInfo.class); + + when(peerInfo.getNodeId()).thenReturn(nodeId); + when(peer.getPeer()).thenReturn(peerInfo); + + return peer; + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerTest.java new file mode 100755 index 00000000000..748406b03e0 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/peers/PeerTest.java @@ -0,0 +1,187 @@ +package net.consensys.pantheon.ethereum.p2p.peers; + +import static net.consensys.pantheon.util.bytes.BytesValue.fromHexString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import net.consensys.pantheon.ethereum.p2p.discovery.DiscoveryPeer; +import net.consensys.pantheon.ethereum.p2p.discovery.PeerDiscoveryStatus; +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class PeerTest { + + @Test + public void createPeer() { + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + final String host = "127.0.0.1"; + final int port = 30303; + + final DiscoveryPeer peer = new DiscoveryPeer(id, host, port); + assertEquals(id, peer.getId()); + assertEquals(host, peer.getEndpoint().getHost()); + assertEquals(port, peer.getEndpoint().getUdpPort()); + } + + @Test(expected = IllegalArgumentException.class) + public void createPeer_NullHost() { + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + final String host = null; + final int port = 30303; + + new DiscoveryPeer(id, host, port); + } + + @Test(expected = IllegalArgumentException.class) + public void createPeer_NegativePort() { + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + final String host = "127.0.0.1"; + final int port = -1; + + new DiscoveryPeer(id, host, port); + } + + @Test(expected = IllegalArgumentException.class) + public void createPeer_ZeroPort() { + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + final String host = "127.0.0.1"; + final int port = 0; + + new DiscoveryPeer(id, host, port); + } + + @Test(expected = IllegalArgumentException.class) + public void createPeer_TooBigPort() { + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + final String host = "127.0.0.1"; + final int port = 70000; + + new DiscoveryPeer(id, host, port); + } + + @Test + public void notEquals() { + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + final Peer peer = new DiscoveryPeer(id, "127.0.0.1", 5000); + final Peer peer2 = new DiscoveryPeer(id, "127.0.0.1", 5001); + assertNotEquals(peer, peer2); + } + + @Test + public void differentHashCode() { + final BytesValue id = + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"); + final DiscoveryPeer peer = new DiscoveryPeer(id, "127.0.0.1", 5000); + final DiscoveryPeer peer2 = new DiscoveryPeer(id, "127.0.0.1", 5001); + assertNotEquals(peer.hashCode(), peer2.hashCode()); + } + + @Test(expected = IllegalArgumentException.class) + public void nullId() { + new DiscoveryPeer(null, "127.0.0.1", 5000); + } + + @Test(expected = IllegalArgumentException.class) + public void emptyId() { + new DiscoveryPeer(BytesValue.wrap(new byte[0]), "127.0.0.1", 5000); + } + + @Test + public void getStatus() { + final DiscoveryPeer peer = new DiscoveryPeer(Peer.randomId(), "127.0.0.1", 5000); + assertEquals(PeerDiscoveryStatus.KNOWN, peer.getStatus()); + } + + @Test + public void createFromURI() { + final Peer peer = + DefaultPeer.fromURI( + "enode://c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b@172.20.0.4:30403"); + assertEquals( + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"), + peer.getId()); + assertEquals("172.20.0.4", peer.getEndpoint().getHost()); + assertEquals(30403, peer.getEndpoint().getUdpPort()); + assertEquals(30403, peer.getEndpoint().getTcpPort().getAsInt()); + } + + @Test(expected = IllegalArgumentException.class) + public void createFromURIFailsForWrongScheme() { + DefaultPeer.fromURI("http://user@foo:80"); + } + + @Test(expected = IllegalArgumentException.class) + public void createFromURIFailsForMissingId() { + DefaultPeer.fromURI("enode://172.20.0.4:30303"); + } + + @Test(expected = IllegalArgumentException.class) + public void createFromURIFailsForMissingHost() { + DefaultPeer.fromURI( + "enode://c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b@:30303"); + } + + @Test + public void createFromURIWithoutPortUsesDefault() { + final Peer peer = + DefaultPeer.fromURI( + "enode://c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b@172.20.0.4"); + assertEquals( + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"), + peer.getId()); + assertEquals("172.20.0.4", peer.getEndpoint().getHost()); + assertEquals(30303, peer.getEndpoint().getUdpPort()); + assertEquals(30303, peer.getEndpoint().getTcpPort().getAsInt()); + } + + @Test + public void createPeerFromURIWithDifferentUdpAndTcpPorts() { + final Peer peer = + DefaultPeer.fromURI( + "enode://c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b@172.20.0.4:12345?discport=22222"); + assertEquals( + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"), + peer.getId()); + assertEquals("172.20.0.4", peer.getEndpoint().getHost()); + assertEquals(22222, peer.getEndpoint().getUdpPort()); + assertEquals(12345, peer.getEndpoint().getTcpPort().getAsInt()); + } + + @Test + public void createPeerFromURIWithDifferentUdpAndTcpPorts_InvalidTcpPort() { + final Peer[] peers = + new Peer[] { + DefaultPeer.fromURI( + "enode://c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b@172.20.0.4:12345?discport=99999"), + DefaultPeer.fromURI( + "enode://c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b@172.20.0.4:12345?discport=99999000") + }; + + for (final Peer peer : peers) { + assertEquals( + fromHexString( + "c7849b663d12a2b5bf05b1ebf5810364f4870d5f1053fbd7500d38bc54c705b453d7511ca8a4a86003d34d4c8ee0bbfcd387aa724f5b240b3ab4bbb994a1e09b"), + peer.getId()); + assertEquals("172.20.0.4", peer.getEndpoint().getHost()); + assertEquals(12345, peer.getEndpoint().getUdpPort()); + assertEquals(12345, peer.getEndpoint().getTcpPort().getAsInt()); + } + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramerTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramerTest.java new file mode 100755 index 00000000000..38d13b076bd --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/FramerTest.java @@ -0,0 +1,176 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.framing; + +import static io.netty.buffer.ByteBufUtil.decodeHexDump; +import static io.netty.buffer.Unpooled.buffer; +import static io.netty.buffer.Unpooled.wrappedBuffer; +import static java.util.stream.Collectors.toList; +import static java.util.stream.StreamSupport.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import net.consensys.pantheon.ethereum.p2p.api.MessageData; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.HandshakeSecrets; +import net.consensys.pantheon.ethereum.p2p.wire.RawMessage; + +import java.io.IOException; +import java.util.List; +import java.util.Random; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Test; + +public class FramerTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void shouldThrowExceptionWhenMessageTooLong() { + final byte[] aes = { + 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, + 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2 + }; + final byte[] mac = { + 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, + 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2, 0xa, 0x2 + }; + + final byte[] byteArray = new byte[0xFFFFFF]; + new Random().nextBytes(byteArray); + final ByteBuf buf = wrappedBuffer(byteArray); + final MessageData ethMessage = new RawMessage(0x00, buf); + + final HandshakeSecrets secrets = new HandshakeSecrets(aes, mac, mac); + final Framer framer = new Framer(secrets); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> framer.frame(ethMessage)) + .withMessageContaining("Message size in excess of maximum length."); + } + + @Test + public void deframeOne() throws IOException { + // Load test data. + final JsonNode td = MAPPER.readTree(FramerTest.class.getResource("/peer2.json")); + final HandshakeSecrets secrets = secretsFrom(td, false); + + final Framer framer = new Framer(secrets); + final JsonNode m = td.get("messages").get(0); + + assertThatCode(() -> framer.deframe(wrappedBuffer(decodeHexDump(m.get("data").asText())))) + .doesNotThrowAnyException(); + } + + @Test + public void deframeManyNoFragmentation() throws IOException { + // Load test data. + final JsonNode td = MAPPER.readTree(FramerTest.class.getResource("/peer1.json")); + final HandshakeSecrets secrets = secretsFrom(td, false); + + final JsonNode messages = td.get("messages"); + final ByteBuf buf = buffer(); + + messages.forEach(n -> buf.writeBytes(decodeHexDump(n.get("data").asText()))); + + final Framer framer = new Framer(secrets); + int i = 0; + for (MessageData m = framer.deframe(buf); m != null; m = framer.deframe(buf)) { + final int expectedFrameSize = messages.get(i++).get("frame_size").asInt(); + assertThat(expectedFrameSize).isEqualTo(m.getSize() + 1); // +1 for message id byte. + } + // All messages were processed. + assertThat(i).isEqualTo(messages.size()); + } + + @Test + public void deframeManyWithExtremeOneByteFragmentation() throws IOException { + // Load test data. + final JsonNode td = MAPPER.readTree(FramerTest.class.getResource("/peer1.json")); + final HandshakeSecrets secrets = secretsFrom(td, false); + + final JsonNode messages = td.get("messages"); + + // + // TCP is a stream-based, fragmenting protocol; protocol messages can arrive chunked in smaller + // packets + // arbitrarily; however it is guaranteed that fragments will arrive in order. + // + // Here we test our framer is capable of reassembling fragments into protocol messages, by + // simulating + // an aggressive 1-byte fragmentation model. + // + final ByteBuf all = buffer(); + messages.forEach(n -> all.writeBytes(decodeHexDump(n.get("data").asText()))); + + final Framer framer = new Framer(secrets); + + int i = 0; + final ByteBuf in = buffer(); + while (all.isReadable()) { + in.writeByte(all.readByte()); + final MessageData msg = framer.deframe(in); + if (msg != null) { + final int expectedFrameSize = messages.get(i++).get("frame_size").asInt(); + assertThat(expectedFrameSize).isEqualTo(msg.getSize() + 1); // +1 for message id byte. + assertThat(in.readableBytes()).isZero(); + } + } + // All messages were processed. + assertThat(i).isEqualTo(messages.size()); + } + + @Test + public void frameMessage() throws IOException { + // This is a circular test. + // + // This test decrypts all messages in the test vectors; it then impersonates the sending end + // by swapping the ingress and egress MACs and frames the plaintext messages. + // We then verify if the resulting ciphertexts are equal to our test vectors. + // + final JsonNode td = MAPPER.readTree(FramerTest.class.getResource("/peer1.json")); + HandshakeSecrets secrets = secretsFrom(td, false); + Framer framer = new Framer(secrets); + + final JsonNode messages = td.get("messages"); + final List decrypted = + stream(messages.spliterator(), false) + .map(n -> decodeHexDump(n.get("data").asText())) + .map(Unpooled::wrappedBuffer) + .map(framer::deframe) + .collect(toList()); + + secrets = secretsFrom(td, true); + framer = new Framer(secrets); + + for (int i = 0; i < decrypted.size(); i++) { + final ByteBuf b = framer.frame(decrypted.get(i)); + final byte[] enc = new byte[b.readableBytes()]; + b.readBytes(enc); + final byte[] expected = decodeHexDump(messages.get(i).get("data").asText()); + assertThat(expected).isEqualTo(enc); + } + } + + private HandshakeSecrets secretsFrom(final JsonNode td, final boolean swap) { + final byte[] aes = decodeHexDump(td.get("aes_secret").asText()); + final byte[] mac = decodeHexDump(td.get("mac_secret").asText()); + + final byte[] e1 = decodeHexDump(td.get("egress_gen").get(0).asText()); + final byte[] e2 = decodeHexDump(td.get("egress_gen").get(1).asText()); + final byte[] i1 = decodeHexDump(td.get("ingress_gen").get(0).asText()); + final byte[] i2 = decodeHexDump(td.get("ingress_gen").get(1).asText()); + + // 3rd parameter (token) is irrelevant. + final HandshakeSecrets secrets = new HandshakeSecrets(aes, mac, mac); + + if (!swap) { + secrets.updateEgress(e1).updateEgress(e2).updateIngress(i1).updateIngress(i2); + } else { + secrets.updateIngress(e1).updateIngress(e2).updateEgress(i1).updateEgress(i2); + } + + return secrets; + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressorTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressorTest.java new file mode 100755 index 00000000000..101396323aa --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/framing/SnappyCompressorTest.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.framing; + +import static io.netty.buffer.ByteBufUtil.decodeHexDump; +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +public class SnappyCompressorTest { + + @Test + public void roundTrip() { + String input = "Uncompressed sample text for round-trip compression/decompression"; + input = input + input + input + input; // Give it some repetition for good sample data + final byte[] data = input.getBytes(StandardCharsets.UTF_8); + final SnappyCompressor snappy = new SnappyCompressor(); + assertThat(snappy.decompress(snappy.compress(data))).isEqualTo(data); + } + + @Test + public void roundTripEmptyByteArray() { + final byte[] data = new byte[0]; + final SnappyCompressor snappy = new SnappyCompressor(); + assertThat(snappy.decompress(snappy.compress(data))).isEqualTo(data); + } + + @Test(expected = IllegalArgumentException.class) + public void compressNull() { + final SnappyCompressor snappy = new SnappyCompressor(); + snappy.compress(null); + } + + @Test(expected = IllegalArgumentException.class) + public void decompressNull() { + final SnappyCompressor snappy = new SnappyCompressor(); + snappy.decompress(null); + } + + @Test + public void roundTripEthereumData() { + // First data set. + byte[] compressed = + decodeHexDump( + "ab01a8f8a9f8a74083282fff82945194fc2c4d8f95002c14ed0a7a" + + "a65102cac9e5953b5e80b844a9059cbb00000015024c3463934897d356b8659cbdfe15209e3bc32291" + + "05151d3a0100f04a45636408bcb6e0001ca06a4ed94062719ae58d392b253268da005a4fb2d8d33b19" + + "ec84a7312a34ecbfc2a0055c660cc59f5dad52ae4d6fd5f2fc081d706ee0bce4195ecfff07a1f85d1b" + + "d6"); + + byte[] decompressed = + decodeHexDump( + "f8a9f8a74083282fff82945194fc2c4d8f95002c14ed0a7aa651" + + "02cac9e5953b5e80b844a9059cbb0000000000000000000000003463934897d356b8659cbdfe15209e" + + "3bc322910500000000000000000000000000000000000000000000000045636408bcb6e0001ca06a4e" + + "d94062719ae58d392b253268da005a4fb2d8d33b19ec84a7312a34ecbfc2a0055c660cc59f5dad52ae" + + "4d6fd5f2fc081d706ee0bce4195ecfff07a1f85d1bd6"); + + final SnappyCompressor snappy = new SnappyCompressor(); + assertThat(snappy.compress(decompressed)).isEqualTo(compressed); + assertThat(snappy.decompress(compressed)).isEqualTo(decompressed); + assertThat(snappy.decompress(snappy.compress(decompressed))).isEqualTo(decompressed); + assertThat(snappy.compress(snappy.decompress(compressed))).isEqualTo(compressed); + + // Second data set. + compressed = + decodeHexDump( + "ac01a8f8aaf8a880843b9aca0082ea609466186008c1050627f979d464eab" + + "b258860563dbe80b844a9059cbb000019024c7cecb041d044ae699f9830b53256c7e1446430a3191e3" + + "20100f04b02b5e3af16b188000025a03f691708219e6d099c0c022ac86c6745b98bce1417a94c32d2e" + + "e5a4e48c0e550a05df314c4202ac2aff5fd13bd5ede29b6967ffdb3063b203c571641fa8dd11c5c"); + + decompressed = + decodeHexDump( + "f8aaf8a880843b9aca0082ea609466186008c1050627f979d464eabb258" + + "860563dbe80b844a9059cbb0000000000000000000000007cecb041d044ae699f9830b53256c7e1446" + + "430a3000000000000000000000000000000000000000000000002b5e3af16b188000025a03f6917082" + + "19e6d099c0c022ac86c6745b98bce1417a94c32d2ee5a4e48c0e550a05df314c4202ac2aff5fd13bd5" + + "ede29b6967ffdb3063b203c571641fa8dd11c5c"); + + assertThat(snappy.compress(decompressed)).isEqualTo(compressed); + assertThat(snappy.decompress(compressed)).isEqualTo(decompressed); + assertThat(snappy.decompress(snappy.compress(decompressed))).isEqualTo(decompressed); + assertThat(snappy.compress(snappy.decompress(compressed))).isEqualTo(compressed); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java new file mode 100755 index 00000000000..cb819614788 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java @@ -0,0 +1,196 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.crypto.SECP256K1.PrivateKey; +import net.consensys.pantheon.ethereum.p2p.rlpx.handshake.Handshaker.HandshakeStatus; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import junit.framework.AssertionFailedError; +import org.junit.Test; + +/** Test vectors taken from https://gist.github.com/fjl/3a78780d17c755d22df2 */ +public class ECIESHandshakeTest { + + // Input data. + private static class Input { + + // Keys. + private static final KeyPair initiatorKeyPair = + KeyPair.create( + PrivateKey.create( + h32("0x5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"))); + private static final KeyPair initiatorEphKeyPair = + KeyPair.create( + PrivateKey.create( + h32("0x19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"))); + private static final KeyPair responderKeyPair = + KeyPair.create( + PrivateKey.create( + h32("0xc45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"))); + private static final KeyPair responderEphKeyPair = + KeyPair.create( + PrivateKey.create( + h32("0xd25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"))); + + // Nonces. + private static final Bytes32 initiatorNonce = + h32("0xcd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e1c6b7ec66f077bb11"); + private static final Bytes32 responderNonce = + h32("0xf37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d8c7453c0089687a7"); + + // PyEVM + private static final byte[] pyEvmInitiatorRqEnc = + h("0x04a0274c5951e32132e7f088c9bdfdc76c9d91f0dc6078e848f8e3361193dbdc43b94351ea3d89e4ff33ddcefbc80070498824857f499656c4f79bbd97b6c51a514251d69fd1785ef8764bd1d262a883f780964cce6a14ff206daf1206aa073a2d35ce2697ebf3514225bef186631b2fd2316a4b7bcdefec8d75a1025ba2c5404a34e7795e1dd4bc01c6113ece07b0df13b69d3ba654a36e35e69ff9d482d88d2f0228e7d96fe11dccbb465a1831c7d4ad3a026924b182fc2bdfe016a6944312021da5cc459713b13b86a686cf34d6fe6615020e4acf26bf0d5b7579ba813e7723eb95b3cef9942f01a58bd61baee7c9bdd438956b426a4ffe238e61746a8c93d5e10680617c82e48d706ac4953f5e1c4c4f7d013c87d34a06626f498f34576dc017fdd3d581e83cfd26cf125b6d2bda1f1d56") + .extractArray(); + } + + // Messages. + private static class Messages { + + // Encrypted messages -- we cannot assert against these because the Integrated Encryption Scheme + // is designed to + // use a different ephemeral key everytime. + private static final byte[] initiatorMsgEnc = + h("0x04a0274c5951e32132e7f088c9bdfdc76c9d91f0dc6078e848f8e3361193dbdc43b94351ea3d89e4ff33ddcefbc80070498824857f499656c4f79bbd97b6c51a514251d69fd1785ef8764bd1d262a883f780964cce6a14ff206daf1206aa073a2d35ce2697ebf3514225bef186631b2fd2316a4b7bcdefec8d75a1025ba2c5404a34e7795e1dd4bc01c6113ece07b0df13b69d3ba654a36e35e69ff9d482d88d2f0228e7d96fe11dccbb465a1831c7d4ad3a026924b182fc2bdfe016a6944312021da5cc459713b13b86a686cf34d6fe6615020e4acf26bf0d5b7579ba813e7723eb95b3cef9942f01a58bd61baee7c9bdd438956b426a4ffe238e61746a8c93d5e10680617c82e48d706ac4953f5e1c4c4f7d013c87d34a06626f498f34576dc017fdd3d581e83cfd26cf125b6d2bda1f1d56") + .extractArray(); + private static final byte[] responderMsgEnc = + h("0x049934a7b2d7f9af8fd9db941d9da281ac9381b5740e1f64f7092f3588d4f87f5ce55191a6653e5e80c1c5dd538169aa123e70dc6ffc5af1827e546c0e958e42dad355bcc1fcb9cdf2cf47ff524d2ad98cbf275e661bf4cf00960e74b5956b799771334f426df007350b46049adb21a6e78ab1408d5e6ccde6fb5e69f0f4c92bb9c725c02f99fa72b9cdc8dd53cff089e0e73317f61cc5abf6152513cb7d833f09d2851603919bf0fbe44d79a09245c6e8338eb502083dc84b846f2fee1cc310d2cc8b1b9334728f97220bb799376233e113") + .extractArray(); + } + + private static class Expectations { + private static final byte[] aesSecret = + h32("0xc0458fa97a5230830e05f4f20b7c755c1d4e54b1ce5cf43260bb191eef4e418d").extractArray(); + private static final byte[] macSecret = + h32("0x48c938884d5067a1598272fcddaa4b833cd5e7d92e8228c0ecdfabbe68aef7f1").extractArray(); + private static final byte[] token = + h32("0x3f9ec2592d1554852b1f54d228f042ed0a9310ea86d038dc2b401ba8cd7fdac4").extractArray(); + private static final byte[] initialEgressMac = + h32("0x09771e93b1a6109e97074cbe2d2b0cf3d3878efafe68f53c41bb60c0ec49097e").extractArray(); + private static final byte[] initialIngressMac = + h32("0x75823d96e23136c89666ee025fb21a432be906512b3dd4a3049e898adb433847").extractArray(); + } + + @Test + public void authPlainTextWithEncryption() { + // Start a handshaker disabling encryption. + final ECIESHandshaker initiator = new ECIESHandshaker(); + + // Prepare the handshaker to take the initiator role. + initiator.prepareInitiator(Input.initiatorKeyPair, Input.responderKeyPair.getPublicKey()); + + // Set the test vectors. + initiator.setEphKeyPair(Input.initiatorEphKeyPair); + initiator.setInitiatorNonce(Input.initiatorNonce); + + // Get the first message and compare it against expected output value. + final ByteBuf initiatorRq = initiator.firstMessage(); + // assertThat(initiatorRq).isEqualTo(Messages.initiatorMsgPlain); + + // Create the responder handshaker. + final ECIESHandshaker responder = new ECIESHandshaker(); + + // Prepare the handshaker with the responder's keypair. + responder.prepareResponder(Input.responderKeyPair); + + // Set the test data. + responder.setEphKeyPair(Input.responderEphKeyPair); + responder.setResponderNonce(Input.responderNonce); + + // Give the responder the initiator's request. Check that it has a message to send. + final ByteBuf responderRp = + responder + .handleMessage(initiatorRq) + .orElseThrow(() -> new AssertionFailedError("Expected responder message")); + assertThat(responder.getPartyEphPubKey()).isEqualTo(initiator.getEphKeyPair().getPublicKey()); + assertThat(responder.getInitiatorNonce()).isEqualTo(initiator.getInitiatorNonce()); + assertThat(responder.partyPubKey()).isEqualTo(initiator.getIdentityKeyPair().getPublicKey()); + + // Provide that message to the initiator, check that it has nothing to send. + final Optional noMessage = initiator.handleMessage(responderRp); + assertThat(noMessage).isNotPresent(); + + // Ensure that both handshakes are in SUCCESS state. + assertThat(initiator.getStatus()).isEqualTo(HandshakeStatus.SUCCESS); + assertThat(responder.getStatus()).isEqualTo(HandshakeStatus.SUCCESS); + + // Compare data across handshakes. + assertThat(initiator.getPartyEphPubKey()).isEqualTo(responder.getEphKeyPair().getPublicKey()); + assertThat(initiator.getResponderNonce()).isEqualTo(Input.responderNonce); + } + + @Test + public void encryptedInitiationRqFromPyEvm() { + // Create the responder handshaker. + final ECIESHandshaker responder = new ECIESHandshaker(); + + // Prepare the handshaker with the responder's keypair. + responder.prepareResponder(Input.responderKeyPair); + + // Set the test data. + responder.setEphKeyPair(Input.responderEphKeyPair); + responder.setResponderNonce(Input.responderNonce); + + // Pusht the request taken from PyEVM. + final Optional responderMsg = + responder.handleMessage(Unpooled.wrappedBuffer(Input.pyEvmInitiatorRqEnc)); + assertThat(responderMsg).isPresent(); + assertThat(responder.getStatus()).isEqualTo(HandshakeStatus.SUCCESS); + } + + @Test + public void ingressEgressMacsAsExpected() { + // Initiator end of the handshake. + final ECIESHandshaker initiator = new ECIESHandshaker(); + initiator.prepareInitiator(Input.initiatorKeyPair, Input.responderKeyPair.getPublicKey()); + initiator.firstMessage(); + initiator.setInitiatorMsgEnc(BytesValue.wrap(Messages.initiatorMsgEnc)); + initiator.setEphKeyPair(Input.initiatorEphKeyPair); + initiator.setInitiatorNonce(Input.initiatorNonce); + + // Responder end of the handshake. + final ECIESHandshaker responder = new ECIESHandshaker(); + responder.prepareResponder(Input.responderKeyPair); + responder.setEphKeyPair(Input.responderEphKeyPair); + responder.setResponderNonce(Input.responderNonce); + + // Exchange encrypted test messages. + responder.handleMessage(Unpooled.wrappedBuffer(Messages.initiatorMsgEnc)); + initiator.handleMessage(Unpooled.wrappedBuffer(Messages.responderMsgEnc)); + + // Override the message sent from the responder with the test vector, and regenerate the + // secrets. + responder.setResponderMsgEnc(BytesValue.wrap(Messages.responderMsgEnc)); + responder.computeSecrets(); + + // Assert that the initiator's secrets match the expected values. + assertThat(initiator.secrets().getAesSecret()).isEqualTo(Expectations.aesSecret); + assertThat(initiator.secrets().getMacSecret()).isEqualTo(Expectations.macSecret); + assertThat(initiator.secrets().getToken()).isEqualTo(Expectations.token); + assertThat(initiator.secrets().getIngressMac()).isEqualTo(Expectations.initialIngressMac); + assertThat(initiator.secrets().getEgressMac()).isEqualTo(Expectations.initialEgressMac); + + // Assert that the responder's secrets match the expected values, where the ingress and egress + // are reversed. + assertThat(responder.secrets().getAesSecret()).isEqualTo(Expectations.aesSecret); + assertThat(responder.secrets().getMacSecret()).isEqualTo(Expectations.macSecret); + assertThat(responder.secrets().getToken()).isEqualTo(Expectations.token); + assertThat(responder.secrets().getIngressMac()).isEqualTo(Expectations.initialEgressMac); + assertThat(responder.secrets().getEgressMac()).isEqualTo(Expectations.initialIngressMac); + } + + private static BytesValue h(final String hex) { + return BytesValue.fromHexString(hex); + } + + private static Bytes32 h32(final String hex) { + return Bytes32.fromHexString(hex); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessageTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessageTest.java new file mode 100755 index 00000000000..8d3c17e5f65 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessageTest.java @@ -0,0 +1,26 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.concurrent.ThreadLocalRandom; + +import org.assertj.core.api.Assertions; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.junit.Test; + +/** Tests for {@link EncryptedMessage}. */ +public final class EncryptedMessageTest { + + @Test + public void eip8RoundTrip() throws InvalidCipherTextException { + final SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.generate(); + final byte[] message = new byte[288]; + ThreadLocalRandom.current().nextBytes(message); + final BytesValue initial = BytesValue.wrap(message); + final BytesValue encrypted = EncryptedMessage.encryptMsgEip8(initial, keyPair.getPublicKey()); + final BytesValue decrypted = + EncryptedMessage.decryptMsgEIP8(encrypted, keyPair.getPrivateKey()); + Assertions.assertThat(decrypted.slice(0, 288)).isEqualTo(initial); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4Test.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4Test.java new file mode 100755 index 00000000000..420231748c2 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV4Test.java @@ -0,0 +1,49 @@ +package net.consensys.pantheon.ethereum.p2p.rlpx.handshake.ecies; + +import net.consensys.pantheon.crypto.SECP256K1; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + +import com.google.common.io.Resources; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +/** Tests for {@link InitiatorHandshakeMessageV4}. */ +public final class InitiatorHandshakeMessageV4Test { + + private static final BytesValue EXAMPLE_MESSAGE; + + private static final SECP256K1.KeyPair EXAMPLE_KEYPAIR; + + static { + try { + EXAMPLE_KEYPAIR = + SECP256K1.KeyPair.load( + new File(InitiatorHandshakeMessageV4.class.getResource("test.keypair").toURI())); + } catch (final IOException | URISyntaxException ex) { + throw new IllegalStateException(ex); + } + try { + EXAMPLE_MESSAGE = + BytesValue.fromHexString( + Resources.readLines( + InitiatorHandshakeMessageV4Test.class.getResource("test.initiatormessage"), + StandardCharsets.UTF_8) + .get(0)); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } + } + + @Test + public void encodeDecodeRoundtrip() { + final InitiatorHandshakeMessageV4 initial = + InitiatorHandshakeMessageV4.decode(EXAMPLE_MESSAGE, EXAMPLE_KEYPAIR); + final BytesValue encoded = initial.encode(); + Assertions.assertThat(encoded).isEqualTo(EXAMPLE_MESSAGE.slice(0, encoded.size())); + } +} diff --git a/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/wire/WireMessagesSedesTest.java b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/wire/WireMessagesSedesTest.java new file mode 100755 index 00000000000..4d9e3de6bb9 --- /dev/null +++ b/ethereum/p2p/src/test/java/net/consensys/pantheon/ethereum/p2p/wire/WireMessagesSedesTest.java @@ -0,0 +1,50 @@ +package net.consensys.pantheon.ethereum.p2p.wire; + +import static io.netty.buffer.ByteBufUtil.decodeHexDump; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.BytesValue; + +import io.netty.buffer.ByteBuf; +import org.junit.Test; + +public class WireMessagesSedesTest { + + @Test + public void deserializeHello() { + byte[] rlp = + decodeHexDump( + "f88105af476573632f76302e322e302d737461626c652d61383430646534302" + + "f6c696e75782d616d6436342f676f312e392e34ccc5836574683fc5836574683e80b84067d910939be40f3" + + "b35761b0fe3f0de19cb96092be29a0d0c033a1629d3cf270345586679aba8bbda61069532e3ac7551fc3a9" + + "7766c30037184a5bed48a821861"); + + assertSedesWorks(rlp); + + rlp = + decodeHexDump( + "f87b05af476574682f76312e372e332d737461626c652d34626233633839642f6c696e" + + "75782d616d6436342f676f312e392e32c6c5836574683f80b8406a68f89fbfa11ca6dbe13a8c09300047b2" + + "dd83a6a6580b2559c3c2d87527b83ea8f232ddeed2fff3263949105761ab5d0fe3733046e0e75aaa83cada" + + "3b1e5d41"); + + assertSedesWorks(rlp); + } + + private static void assertSedesWorks(final byte[] data) { + final PeerInfo peerInfo = PeerInfo.readFrom(RLP.input(BytesValue.wrap(data))); + + assertThat(peerInfo.getClientId()).isNotBlank(); + assertThat(peerInfo.getCapabilities()).isNotEmpty(); + assertThat(peerInfo.getNodeId().extractArray().length).isEqualTo(64); + assertThat(peerInfo.getPort()).isGreaterThanOrEqualTo(0); + assertThat(peerInfo.getVersion()).isEqualTo(5); + + // Re-serialize and check that data matches + final ByteBuf buffer = peerInfo.toByteBuf(); + final byte[] serialized = new byte[buffer.readableBytes()]; + buffer.getBytes(buffer.readerIndex(), serialized); + assertThat(serialized).isEqualTo(data); + } +} diff --git a/ethereum/p2p/src/test/resources/log4j2.xml b/ethereum/p2p/src/test/resources/log4j2.xml new file mode 100755 index 00000000000..82f9c0b4cb2 --- /dev/null +++ b/ethereum/p2p/src/test/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + INFO + + + + + + + + + + + + diff --git a/ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.initiatormessage b/ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.initiatormessage new file mode 100755 index 00000000000..53474a92e5e --- /dev/null +++ b/ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.initiatormessage @@ -0,0 +1 @@ +0xF8A7B841E7E0115F16A49A8C41D01C0AB2AEE4AEC676D61CE3ECC3F05617E63EBB02E41255C732BDA518C4225578CD315F82306049C852F5101F9DD3FF4B42300C4B6AFF00B840A979FB575495B8D6DB44F750317D0F4622BF4C2AA3365D6AF7C284339968EEF29B69AD0DCE72A4D8DB5EBB4968DE0E3BEC910127F134779FBCB0CB6D3331163CA0E915451769B9ECBB3C1669A0575F00B19DE75389C960ACEAC273D0DE4C1A9EFE040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.keypair b/ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.keypair new file mode 100755 index 00000000000..1ac1754c699 --- /dev/null +++ b/ethereum/p2p/src/test/resources/net/consensys/pantheon/ethereum/p2p/rlpx/handshake/ecies/test.keypair @@ -0,0 +1 @@ +0x9110356BC3E5D4E30175B8ECAFB0AE8AAAF04A2B613084A1B30288BB4E7534C9 \ No newline at end of file diff --git a/ethereum/p2p/src/test/resources/peer1.json b/ethereum/p2p/src/test/resources/peer1.json new file mode 100755 index 00000000000..c1345bc412f --- /dev/null +++ b/ethereum/p2p/src/test/resources/peer1.json @@ -0,0 +1,46 @@ +{ + "aes_secret": "ff7125b185fc71ed4462af35482324271f4794b53145266b36df5a2918757576", + "mac_secret": "60365bb626709962bf32f6e464fed7f7de704ad3ad5f4171637d65146f7ad65a", + "egress_gen": [ + "c3f34cfbc15c477b6c2a95f4967f37519ec06c91abfc4ce0c4e672b5d80d88a0", + "019b045dcdd28b22f28bbc07165116d7380d5d11f78c5e2d1ed43edc99b3f2932ff2aa04f84bd4fcbcc7a47112717a1f1e36ed7651b89b66168a40d95cf230ff6f12581fdab375d964735c63343abf19b0ea1d8887d3c8e705db62662334e04448f22b55591ec0a4a8f0857437f7741aa4850935318bccb48f914c35d3a4841d7230c21b4e1c30438b7584d64deeaf1892b8afe92171c6ab5153f8623d6c651b1ca0dc2894dc8921cfd67cba66801bf06522133acaa36a3469e7dd26c6c2b3158e6aa19f5f3bbf656e630f09929b21ff2a7c1a165096777289dbfd8df6d3933becf11df3c7f55c27b29242b0cc4a4c2f7130d8d3561239df9c4425aaa3661823cc62c8566d21f8c5364b7781825797cc973d55b8a553c72bc2f1adc0fbf40863847e99b01fa5b64ad6e8d051b086dd8304abde0349a16d36b512582d2b988a933a6fb4b217b2c30c6f4dfac8c665e822a00cef0312ce490765807746176732cbb74173145851c98b75e562a6a014728df541d6df9724f0925fbcb8731cc72c9f482f22dc086aaa9592db4305694272de0e987c1c3631a4c458d547ac99" + ], + "ingress_gen": [ + "ff3cc0066e68fa5971fddfb10ac3c9cb9dc0e2068efe5bf748c24e2e9999ceb2", + "01590490769cd693c5f6dabe05e9e824a3165773bcc4f1257608aeb390c0e6b3b26da96abae37ead9ca8cacbe0732d0a75cd0563c01dd7d7b37f1e20d8dacf73299d0f1d07f68aac3512a0639508963871f5f530e153612da589d1b2c3186fe4ce29fd72f5fa986adf3146f32d7996e0e36e300269e346be04f0192ef1f9f267fdc36310e6fd55dbbb3109bc4aad06ed7e5056e0b4a8265a5b5a3a01f579ee4a8ae77b7cc89e5557719f2d6e22b57e44ff93aec115f93b89c13a4023b9fe8c7a85b5e57516dc076790d5d0d66d81317be3b76426f584d3bae1c351df8c7419a33347f639667f7e9dc5c8142e3c8c05c9d2280561c776594b545f90adbe2e6ffdcede687842ff689e603b954b2ab8403ce3fff13392cada5b3d561c36fcbdc24e193619183460f0f4577b271741e8e03ba3457393f0c8942388d868df6cfa9c98912b4e22021534c247b9ddb2c6a593af09d2f12dc18f7b9294fd58" + ], + "messages": [ + { + "data": "7058ad63766a62734e070f12eeea9c8385a8eee5eed6b038c42676df0993f8f8aa339adb7235346633c96446aa309acfe84535a74e99bcd3cb613deadca74043a68129c3f720899d18304d1061d91ef25e114cca9cabe1e32ec4e83b3a3367d0ae5ac3ab8256f9ecf0e8a51cfb0e4d7ba2fb029e5cce87c2199d210a8201fe14cdf6836b93644600b1fd9d255481b49a96064a12642a54c6574dd043d32e4b39ab14ebb88c5dab4dcc464a0983640e70", + "frame_size": 126 + }, + { + "data": "0dbcb49190a7e65f6ed21d497e09e182eb7919f05c3e7fac8af7794d5b0767718883428be09a3610e84fc07440f916c7a6eb6c683f94e47b32336ec13c2d524ffb4eb21aac98f676118749f8d73f2e2d7bcee91eda02cf5d1a8ffad0b0d8da6a2895f4b50f79daf4075baa411a949de41eb88e02ea67ef827e3886329db900a2f6becf246c8314309744e89ca416d9c4", + "frame_size": 84 + }, + { + "data": "0bbdf79232749ab163d716b009da148f66119b88a5ae27d2170a2230489251ce8b659fc72a2a0ae9c324d007fcb4a8b7b0dd14bc883b6bc69096202608357b5e", + "frame_size": 11 + }, + { + "data": "37830acdaee936b0d434b62e16f521e24dd8166b7a055d49df40d90b128cd32c42dad2bc1c140f961df5d6400e44829d6e4fab68cc3c5001d4839000c2596b9ba3fdfa4cf2e733db7de5d2bba6be4d0de56aaa457fdbfe7a8de095182c242c41f41f88a467aafa68aa5b1410d38b8543c9c5faff0c39e5cd68b6ea52d5f2c0440e77b36963eda19271169fb08e156f863ec511c7430674ed0f01ddf1c6097f83ee0146f62cd28ec0aa3605b334f238a02b907978fb4e820bffbcffcf6196aba23ef39da6345ed2e7c5b209c92bb95ca90e706b4d6cbcdd5b8ac0d3c779cd82bf431ddf548a2aaf10f431aee066c411a57f25a73dd8d4e26a663e475880cf4a1983a5390452021d79a2b6102b1b3af60fbeffe99f29fbf822790030d76064db88206029019695f7e5c826f277918ad1a6e2e1414dfb98e2e656197b858a1a2508fec4c2ddb81962bcf5e589964e6ecba54b824dbd27c202051f54e4146d3062951e7c4e47a6b89dab69a2581b4670124a989cea856189ff57c9945fff07bccc61fa887605b3b95b9ee35419d302164b3d20e5fd50447b83939586a5158737ab85881f873b720be17ac118ab22b426cf5f74dc17770cabde9e5b50a49ad05101a3f8700c1eb2b74838b34886efe4c1e245a7742ac8470b8f6defe3f152be6cc0a3eddb63ed872ada0e07b3691cc1e0520503f9711c65bb2844a514eed037382343903218c3847dd37287a42d7daccdb59ab55704e5d9d306e17df294a2faf4f77a15072ad3f621e4fbd6e431bd85845bbcb8eb72fd553971257d42576dc7bc2d89a1b43ee0a3c5e7b9cd2ef0d4658dd67851de7dda6f80f3402796216c4bda93f985108d545bbb89ed47a1925cd1f8786a4744ac889f17c4bc7f59f5e8a8bd037eb366999ca502babd9484c2e6d18e1db457d27f5b9b144e2390d3a510d096deca6b3e0993ae70a71c7d4202dd4b065f492cfdaf5c289cff27b995eb5971303d2dd744c5a1314b5590adc50378643c508a14df8130ae9469d6e16afa0d6b3b2843e1caf7c191caa04b7653896a6ec399642a59dcd0df8bb22c02c07e2b4206c6ce4c0494f70a67cc197a73c7bc93a2d32dbef9453f66cf31c158061d545124ceece5f4977df9a36c8d23af7740eb7542cf1a2b8e3259ac8a8ffb51af022ae2e20f62d73cd4618383c80f3d3eb980b88d9e16b1949f97075e1d60e951e23bd47c8896d0430fe9974b1c72405569e573de5a5f547c64a08c35b0e4acef2f0d39db3db3496816a06188541528bcabeb6bbe9d33964eb0e55cf2a2a27b6d3ef9e56482d3ac7e350a4ea221fd9b7f4ba3dfa70884d26e6c54c1503fc53ccc0991c46c8787e37d824a5ab08e8dcb5a8eb5dc59a65cabb5308267ddb0bf362fcfaa12bcf85d4e9de41f9c9897a1c95b3d8e00c3688d4b84dd472fcc39270ab5bafdcc300a402fd791fcdaa22f1bbf28d9bdccb016403ee6a66caf7acc5b12ecf03bcaa73664ef84ffb9513228dcbbd32436547bccac36ec1d4ac1281d86080db4c3863eecce543d9bd0e02f3955072161466568d7eb045feb7010cebf58a738d9283a0e6dc43a6e63de1d60437af9747652d6c2855acc3c320139fdbb2b7935467c8ff857535ee1a344e100a6295875cbc5efd4ca796f7625144ae80e812b3b2388c77acb0a95bf5080e62a1955df81bd757cb944542eb12cc56c70ec40c2761864b49dbc4d75ff1dad1d1dfc6e5a7a7f4f3699ef697adfab57e61f74c38143b4a7e3c09d13665204f95dd87d0b9465e201a930b1929b8ed77c3aed6c6dccc9bfece21f43f603005afe7c61c9756ef0378e5e4e54279f881daf0d7ae3718c9024611f8169485bfcc8c440379c123a29dddfa94a6afc296db1b18e503ec592d8b35e81a4973fd7595246ad6ea9a8ac28eae59bb84350e63b3dd8f3c1b7f11d2c71117354df00ee1deb95298f471c669338cc7102d45cd0aca0fd66268f6a9cb0714e6a2e237cfbdf2fc42de5b061ff88526d7b33e480f9de914a2496515f7f5a2773db031853f3673d768ccc578da2df362754576ccebb3d306bffd52ec35a5a4ea51df9870f675c5319c6ae65bc48da1d814f17ddf3175425072083db08350d962890c0655ea6379725e567c6203bee6577835d191cb1c28017863d80d750715453364ffb538ce3cf4fb7bdc41abe1e53cd5cd7498058d04fd630caa07effd1365a548d5446d39c92c36186d6c6900b483ccab921e9a08224d3deb91aadc80e541a9c8dee8df0ebee4877a72679a7f5bbf9db487a661cc40f2dbcf68daa037a8d4f3af955f898a3db8c155c33857c3ff1086e941fcf94270c6001c52a02046d4ee6a9a1be3a90dbef3bb7db521ee44202f07b2038643d45911bd48294e714b8ba9ad51add54ac304a450d93c6e7b6c45327428c26ca6255e65a9019189dd5717e720f1da671b5860ca44586d30b0d477753c843b8d092d139007014581c4f29b0b6cea95dccd419913853a605be75245fdb0a326e713dec213318c9c1cbe0b836d1af0849c2e248b557fe5f04c5e212c4ad96270e5d3a24ed9e2e013d69d17caea428e2ec70520f2b9494a7837c607cc329ef89bcdb61b97a5f8fb00b44b63506dd2546f73a76f0d4c6e42f65d445154b1eef44d57e754b83b7e7af3a437c60ee6cb0b48ad38dda9f4f60d1d0142805c285f1bd470b78acce1b97c653b33e10f5bd06d7a80a6393b5157a8315ae48a0732b2519152dfc53a8fa9a9bae3bbe68b08d5536c4dec90779d966856b745b86cb033d723449c25535db639f9cd5988cb521f3e281c5560a656fb2c3cebbe95a745cfcc6da3a49b234503c553a3d4138a1ecad7016a01a67146da1f762a95712225c9d32d4e9d8b0f1e401b9d5850f129789d059ce608ef27b493233ab24429a26a174e1b1f6e9abca9e86e1c238f4d069e8e6bfaa65c085f5be1b12e81946cd93e1a1b1d991fb4d6ea18f817ac5db951eb1617c17f228c90be1c8dda3de531ab7a0701272843348f65c1f41dc22df4a23c19ad4cf6e3a9c6e927588fe4f3af5052ddcad5645a36f9e1467c2dbaa2a7dea426ecd24685411a8a0e23227744dfffbce48e777c4f8d5d31a5ced9c02f856a5dd0040dfb9e26e44358f8027f3a5e28a59ce4ce9904e8311594e7b993966baa482ea47c4555a477053595f93c0b25d3740cc62f2116d9f2697abc13f00260244069f9fb2125200dc793c529acead2b97465417b681547e3a8a67f0c394b8f1b848c947150a21cc2cb7710e106e1d8b7072b9e13780f6beb6a63906d39bca2ea02a292b34af2afcbcb4f3e63ba0e86312fa7590281fbe9f3e9dfe26282d0a5973951b9d242abd909acf065aa9fdc56d8858b2c088c4bc1953ff1e1fb4555a70135383809dc9cf612524d8a3c5f19259f4d4b2c18cd0b7d191d71cbf08ecffe204fde84b7a61c423f838542c6b7e19c21bf13a800c336d46618d8b9b88fd0ae118ea4f7bb6bf395a785abb8af71cedd0f6f131eb2437c1c0560a99a2c2ca6e51169c25800e060f1ac8bd24e64f795cacc51dd7b5657fd3a1ed69e69e38c272d933a87bd34edccf88868105992ffbb55cf45696f9ab7923f62c8f0c8e4d1d90338afc3bbd75b5192d0714aadb3efcf92587152734489b518c1e2e244a31717844443a49b0eb3a7a40ffc791bb69d394e6d13b420243c867a5777bc5b476bdacb26faaecfd07916d8c00edf2be2ac704258af7740082d5283b013ee9d9b9aa345af09883b3b9a970000f7d06a8e9ce1152c284caf33c3e6b618a399b78a31d6666b5f472a3eab9066b3d8c9b5ee30d09e28dd86546e2078f316c5fe9b806fa1b9fdc97fa5cc96c67249072653e9c3f259ea4f45c76e6449c8f962ee0604f45c216d3a1865ece96546bd3e1c60daf04bdf782097df96a315b8a7b6b41050b025521922b55502a73964859de8c3c65f38973e99b04cf8a9ac89e268ef76de5242467320238a2f368b57501c551cb5e24a35d99256a22c582858d1e6d3fc83570ca66f1f37f69358468c0e5c55cbe4b9f92be0f483ce441b07b53ad04e9de6aaabc2068e87826cf8b3c148673f429dc3a41074d4c0759496fe195ac707392de54034c74b77e05e22ff5e8e084db92c3fca975f49c0aa03847f456ef0bea64388831adef304a0f8e810470fbebb873c938950dd2543f540f24769490eb354371e0711223c639c3cc8e89fb795664a34094eb4058673420749db6da98060320333ae28cc51cd6004e163f2689019d62294ae3e3e745430c39c1aa53480bb5eaf05c70b1e8d12d3c1121f121c4f7a8db655c4d264db59b9cfab444978552e2be4af4a3c34e2217a823eed701bdda2be2a71f14634bddc1378509036dbd34065e9311b8d95ad166fdb1525e38abe3bc0b5f324069849694f858991d48d2e979c5850839610f0e4199b6bf52fb283632ddb1375e2e03f8259b8d8a0798b55b68833f280de8ea8d58addf53b111624b0e13714b92b9d8f339b25a85ddfdfd04889f840b22117cef7ba4bb2e2389e7fa0848c16b97f2eee4982202e46acf33f3d1742e58cfd187f75767736c05cabd64a160b39c06ca6a38b453505e35dedc4bccdce13de548320644d94a031252c0cfac9fac459bbfacb32c08dfc0d2a596e556b9638f6c37a6238f6dc6551aaf15cf6f84817d0c64c65d31aa5b6fac7327dbf01c977539ebfc3c6d00a916278983067b6e0056ed37de7474e131f91d04cb04bccf5b420471103550dbd6157329d86c584e8668a7c1461566a367a11b48ed21e639d70fe17cac9db7c6aae70232bb49b55ec1635b6651807691bfd122c8c2b7ff1b6fb84f1b0f2147516db668e6200fb71a14116a690e3f7a2582e79f922f29b41e46440f6f59102969a451f00e880e283854b64ef9bde4fb00adbe1eab46f7399a62b2a60cc62dc49fd71759e8394cb75e0b6ed6afad4617880dbc4297eb886db998904b02a72c273e32ca21f799bdc753b83bd0ebefccda3df0b97a29d1071bafec453537fdb3a00e0e5ec83f55c3f7dd79e30a5653f0134e18ec74ca955a9a0a4b30fc52de75fc401669e51b5e9a4349088a89e365fa0fd87342001e4466cd68a707175a0f8fdd7b185a317bf0cfcf142865bbd941b94a79d96307f05f5bbd7ff840cdd4e58459f962d54b9814eb9756632f1abfddff7ed8dd8b66f89bae40baae955e42513e965be4bfe0223cb45e23c99260cd736d063a55745e6afda6704980732e219f28d823bf7b0e3622ee2face898e484cf8f048dff89058a4d1c7fffff85b300c9c62229595a41c4a7be869a43bdc3523997b3a03a60ba4b48a01b90614a184571e5869a884c9a6ba05f23261b6cc6a153dcb74cba9321371d076f27bca576f7f97d8f37126a2e88afaadef5a2239b235f01df4b4b1592c8566a0037f069c2f801b315f3b08b63ca14a697c7807385f65dc943cffcdc989df2b1f2fa3c10ddec1e41aa834dd95cd233c7f4a68348e1aa5fee657e3feaf49841e5b8947df75d276c01bf268563816471338624c8291465ed1962f8ea1386d666e163cdbc6ee0ebbd542ab0f7d4b4420b93df64927eecf870137fa3a5717b04c2a99f405239a5283b95ccbc4a487b3c94cdf1453a1aa20b301a8fa45f392051da256e1a21af80fbf2653c74c73e18ef18849690d3d9d681014b9ca27f6f28f55e8d6a738afc699aad09baaa0c9fb12559092cdf683db9b629f10b2e4241ae614f728320b86fdfad16aaa07c32285e3c752d0815617b67b10cf481fbb8e90b7157d2f9b0ceb5620b7cd9b4ac3486a75a1ac3e50461180ccba4ef6388172664673309c12df6b434fa60e8be38ba54a76a95c8b6292db1ffe6c5ac773dd910e4b00f6f93d0f57328ee4725cf8ea0432cc1998e3f3be6d3e2379d159b7db7389da4768e75d14219a3ddf9cc56ae047a3081f0f97164be8c1a12e2732e73a11b9a2e3394bc8ed57fa470993684ceaa869fae31c1069453d740d8e32c2b478c8c03011ffd58bc132381c8f32df878a5ec5b4ae07cb7652065ea0493231e203b9cb430799735d1021dcd8ff7ce00180253c96964450ea59b705d7ce2cafe35f3d2cae63370179369db3bb17e2f224ee8905d6a474f9663106c48ae8850bf3108397f0fd8339bab3d17a28e46f97fbe0603d511a7fe8b62fd321620b77ccb3ee11bf221b6f14ed1d7ce358b3e5f2b5a5dd1977bd2d1ae4359cf1169704903ac4e011fb428fc4fdb5534cb9cfb0404c5df01f976f9564ce06f4a22e0c39999a316634c4e9e9659684c4bf662d06bb1e1eca8bded6b15411391160c653286a52d90e86782a532b7d4447ae8f4ca0e172fb1b68f548a101c145e9e94845c49dfd6771e5749a813f5a73f9b246d7870320c8de9eba21514004a78a7c4f1101de816e82ab32bd18fa5f71eca8146d652db3603b3c23441037a412e705851d2b5e50be26db157242877a51e6d169b3865874ee393fe78eb674dc1e7bc0011dd2b655e6981798bbfc6218106dcf89f8aa99d61b9da6ee6ca7725852bb26823fe661444e76db73d0775a52b19f2f31d2ba2e2356ef033696249464c6765c0e661eb4954ad2f177646e540f8cb21ab109c5a0e21d66a6f155951fa713dd1126eb49d56f62b0fe088f6d09119834ece901a2a392efd26da26ea702910427cb75fc42ef4716e6c4335e0dc74222200e13d43209ab66a647e15004c8789e99aafcfedbc97ef163cfe7ad2df3b3c143f477526641c0ff411412a00f6856df600979863c4c831ca9230315f63a84adae653dc01d0b3fae12e05f1705c8292cdaa8be3e053c538ef747eb7917814683d4bb22e1a6f98a346d87a374ff1c196e0fe438a4ecf149516f56413165a5699f7db7264ca76de245244655322f8eafc1924583f1f45219b5255e08e8a0a7f05f0168f8654f408a53241c4da64ca9879100c63142858dc3039affc90eff34fa4c47b7e4f79dc526c8a54205881dcf6466b3ed23d80a9aa2f962cc5793e543fb84dd40b0cb93f405ee3ebdc6f4eb908ef9be17c575d6046d3b94953e35282d15fb5d2e4d83a4f0adb5f9b240c19274a9bdfe4b1ab8e50f3a8f80e455623e5c9564575410df71aa71695d0772e2a52bbf3cf764196b4814c0cf7f3cc06a920e6667eefecd338e976a8627997d9fe413ca252179b9acc49aef5aa15e41cf674403161f6dc6a0559f677c4de3c5c5445375a2afa0ad012ee885de6f493253f31f140ba521f27fb67b5ee81ce6ee4cb6c1c4bf6e0353f6672f59da3599911c2376ed5a5be2c8690beba20817fdeba76eafdeef4a19546fe1f9ae9cb38a4fa08e8209e7c778d953d42641fea617d2f1cb246412702f5de5f000afa7f001be98fe48ebc54b5eb7d20325310bddf73fccab74915f54d82fe2be7c08b108159fa811c174feeb2fbd02e61c87e05908933756cb25c99837a5ff65fd6eeb0c90acc24551f3d5d1e6be42bfd7578f912cd7d632f62b93a461390048c21a4442970a7b2655d9f3d9267d69ebf0445f8f74809af43831df1d9ed0b23924788c88d6521ec61ce7ba3e192425a7062f829e15fe8df75a25e2ec86264968915bd53baa00548f51b420e85d5fad54c8254c6f9f9809c934e48dd34782f2ac6f267661ebe024a400032b8cd5274d74ad9e8acd23a9374a8809f4beed271ccf608b7f98a7e0685118ebf6ce57cc5c10b3acd98b31f5c6c87f6f5b76e29e3de172cf9f00a86a7159e306e1577abe5026d71e42070e2760ddfcdc81c30c8b58bd0a7428347c0d4b019b4433d71543a620437505a7ade24c85d42fe839381ea2b64fac81bd91a55b95dab2cae9b6f6170dca50cec76571eca2dabe76c71ac6d4ac9c32dd9c4e7ffae41625a3aeb551aae991917e98ac42dba839182be5e45ebe60221988babfb12ecad5328559c034cbc800de806356caf86940eeac3bed06d76f6c9b0decfcc66dc46c71c9d08f5bcd4dbd0ee9e8d9eeb7adbc7a9640fc54fc1d407a06d295b1e540673dfbd2db1cfafc4db37d83ce11c9920fc5d1e295366da4993bf94b6db68bb77a0f1e02a61121a9d1e87248633a50bacb818a69bc43b975e0d7edb8b3acaec058c111adb99d974625679f5c18d6bda7198b143b1c6e547f29ebd3b06a5a8cf2237eb992fd17168de527537253981c52da13b1e5fc0671cb6f637b752221119508e93d972d45bea710d84803c57e6a9d6fe67d3f9909d8bb4c7090142ab7d51505b291fcba910edaba993af8210fb235e05922b3a6014e6c8ebc3bb6cd4c047c0bcd0c6268b22010d6df6a3469251d85a04c8c2f594a9625e02a91a96ebb3b372625b89da5e2b0c8830ae5ea73f2c78d30f0c580e1f722ade274c2002657ad505b9ce454cd790ff965c48d4cda5d362a33eb240abbd6508ddc74b2250ebaf7a2e3a2132a0acca4275427c9f88cac5d8f514aa0c126fa14286c490daf02d8be5a03bbf8098c2cbb5efc9b4fe3f4542a72fac8bdd79e67dcb555ddfca852818d696980a927a2ff43fd3daba10d1eec9036efbbee0cfb653943d8da01449a67e5f169e78bd1d3d6e3d87b242813a338cf5058db165710fd65ffc4842a3c510726a63e4045ecace8d773c25d578c337be065d6fa084a67b194ed07b25b4db1bb966144c1cca21081cc1f5cbe2de350e4f8c5a9c4e68fd3a2032ca68b032a5e3a1c629081da4bdd7ea6ff277717c0918c3fc6871fc30670a63552d12f92c965ed64aa98011f2275ae4efea80bcce713d1dc3eff975850b99064e243cabb7088be13b3a79e9dd547369aa20758059e606c5654c4f31e4088184fffb12804107e6768045ffbdaa30db3eb707a49fd2684dc79e88ba8a73fcf18bcfef08b1d695c811c639a45574495ebb3f07b1cb4d6856c464179e7c95edcf34789b89a18cb3be535fd80c0d5f4e68e664e1e37e23d2c41db08f147336caca1ef5c9adf7de2c547613a53233d89626dd2e92cf4d09ba648ca0313c21f3ea57382b0d3352de21a8da2ae1862a9c0a8ff44368090c55b6ad5a6b9e6b0e289cd554f838b160c27f2678ee7c08a07d0d69df0802c605661511617c80cb2f6ce47da2a5078299a9f9b714d32d8bacae9bb1f5abe75b72e31c2bdba0fd4e90967708a0b009a38ebe1296b3bb8c19ba5adf31c8dc5c3c9abd7d2f67ea69401463a3c4f6101a82418c35baf90a7b32857227748ac501e56d0b533c8665dded4b2f498de62bff9ec043ff912b5f4eda8ebd9eb6cbf20e18a23a6669b740a6051cf07696886e3871d7821adf0fbe518c923a0f1f29ca198a89a5a6934c5b29465cb98a63ea0ad4b63e8626ccddd26c2e89139b88cb221e94d4382c1a2b457e6756420b82f83517dd4e1206d0af7f39141a82c6d4c8130ffdcf6d017521540438d218a33aa5fc8ab8d45f03a12f3c6ead5981444f3fa2c6b73972f175e49846bc6e5d40518031f2a8cd265dc8d1bf4511f5666ec511612da7854912c017e0be60901e02751ed55e16b556559383993bd28d84326b05ec2471244d433cb75d311ed1e37e7c2321f4d445488891ffaa29bdc9b055baf57ba6b33563cdb821be2b91d8bb739f3126624128ae4a0a0f4b2314945361aef60727edde133243b507009cfd3a1fb28bc71cef9a0c9ac2bc0cc42b7a4a0d7cff016d0f74156a1c597103e29974a7fb8b7f9a717dcd3d707b1f7c10fc0d78e6c6f4b9db2e827bfba65c782f50edcacd3c73182efb197d252ddb374907d0c7a2189343f4a0987b3352d8b8582c1f3e3a5193dfed3a0cb736fceefd71cc7909f857399a4b6199c77626e9597b9761e0a7f9b2509d7112c5944c7c143302ff4bf60237f42cfef5fee8a98102bb300607ea343d20e4da60569112e4680075f1173d79fdb92e1d2718e7e55677ec77fdca4726d75c8c1b3a74ba4f1043e7edf904e1de70c6160d9abce58825471da81fcda619123bafa237d626bc45df75ace69d0aac38c05623dc2c2a7ba8c9c26077f47f7a0bbbf950981569d255eba8b7316c935c04c85a06ef80f7ba68dee2d3743c8a61f3d2a93c9065f2b087962ac02866655b79474eedb649430f43ea21b555006e1de6c516e299dc6b671c6cfecd1db14fae14b98ecf7d1c3c491d0539faf56f03b5ff17d83a4d02d4b52eaa706cc559e5d3df078c473c4f70e167125186f83c9187838c1a0bb6dcf18aa5adb2e86cadb05dfcfe97c798b63f2b3179e74fcf8a61436d99b91cb1926d30c00fe9cc97c98dd3ceff8c22c41aea1d85bc3904f4f88f5bc374ad33a4641e0cf61483d862056410dd4fff8feeb1fd2edbbcc2c3fff6c8fe4d28b05b3defc3d486dbc3fdf781e88c48e7b462dfabc77fec157a2519faa08fcc6b765366da90c2ba6542ac8274b4200e3707f4f0dfe59aca47c02e9a4bc35fe376e83cbb4243a28e046dffba4c23f71416effc4c86b46dca6fa7211bfedab81e46bcad6292291b12ca5f1a344a874971828e2bcf2572f788b570942b94850962e4df39e56b2b5f9521b9275e1ed8c6ff1a5544826ad1b41d29f255ea552ca56e21c9d87c08d37360f90949724da9341171af802000fb4aa65ca1b9fe790938f2b5a513d181d446af97d6c0947191a0134df45bac4f3fdc7b94c265262ce7196d4fe8bdf5cbd6658f5aeecd3e9a2d50fbb6703c4b8f1bdaf806c2531585f9181599dbb4f01a7fbc04c185533f2408cb5b985f6d591be18723239bf22b3382ea40bf87bb9b199794d75c3898fcd1fff5202188bd8aec226ddce20a8fccb9aa26cb224927216e932a832279afda46eb439d347885ce809d8604357b1a220daa4e4d8577492fa5ff5bc696ff16c293d1c0cb9e8368552cbd017fcf68066bf717487ea8cba07273b612afcd56efbcecd9bd49b73b0163406340a8678c0ffb97c7ac12f09c50dbd9a7e6f0119160c00677657409d5ef58b776d4c2a08d274f6475a7279423841b5cd82109139eea25225a5d34dbafd00da109d10c1260cb222f5bbe3d45c6ae24a8939df393594f18bf8a89b48195b1394f0752400bfaf8f1b78725f9243b80601320a6e8d500e524438f3fb66c65a406f34a3979c900931bb08e82180b86b8168440c4cde74e1d89df1cc777c329fc7800cf9f5b5fa6acc69bb03abc8212f34d3e769a41a328add21f3c75db003f6c1de8589b97eb2a2102dfac24bbb149bae5fd87459f7b4b7d72bfb75455c57a6d15ff23a8cd4bb3456ff2995238977da141fe5bd94ed3aa74058816e9f0e219c4b7a4cf89dc61f2c3959403ec95a0b3b1321e285038376f6d981764c3cf7f17be763cfdcc86379632a4416d6670875a4da34c0553c8971d291f3782137751e8ea0aafc0578d72e5cbaf22c25c2a7a862865a70d2baf09809b26476c8459f1168d18eeabc1e946ce805e6757cf7ff4138f86e810791c819cd5db0df2f3ec3b7bc335b9ca42d3287e9cae10599c45e2a475b78a19767c3f7a6604df4e963ff34950b561839df48b6809f6a94244e67b26f310d9aef88080ac81f1963fdaa9c8abc0975f9cb52d613a270ca21dd561baceaa2884de447b937db70c7694dcb0fa8ff4c1ef565495678e67d3b764eeecb4bd1cae0869195449b475695a689b7516f6c8e296a422a5f9c2d4da492e746da70e1bc256bd9a7c28852b38e6d95608a60c92984d42b809aae03a1c6144a9ecec7e0b3045e1f2ab7d3ab1f359aaa6278b813f00fdd53828d68b3ca0434621da96fa0e82ecefaaa80c12076b3babecb8c1638a7b54778ca3983e48b4db0254b80047602e95ea303f2fdb30d8a4399e67de74d888ecc9a99ac81388247b04d21dfb7a5c58cd1e38bafa278ee64b61405aa233aaee70c68d5832b5425b8568677e81463bc9039c38269390044d5cd3e877b65ffb44b4bb998442a45a24fbd2aade25980ef31866a85a2eb699e18fec2f793f7b1eab247ae3e52b4c056463990a17ee805a4eae35ddc8d0caed94a57fd2eff210d545f2d18efff5261f72e265fe12f7e59fb41c9f25b4c7a1c06c7dbfe8a733665e82bd07ebd6ae465103ab60e10af4b37c6b8afdee55cb813dd1ab3d5c5c4f6dbd6d26b8062993bf91c3362cd48cee5cc36eda102f6d01f6094b1872965f7d2a8354017e4595c1b932dad30affd498975528b408a7a6bc3116ea833e9abcaf106127c03b68d4c81ae1e38ad7c110f11fef23e57f637114ae3c68597cf92417556aa9b1f3b6ef84896e35e44e347478663367ff1a4a8e3e80cbd8256d1971cec2fb18b00f33dd706588e437227281e42523fdc4937d595e77a38940a6f8331f7bc085c2cec6595adec3fa65627ef4dce64bfe89649e25f1160e9c207846fe7a246134a91a2c4c6a7cb4793963180d0e60d03fb264a1608b7281c9c5fa0b6b372bed7f0e834e747d956369c0b4185a511b4a4b6acb2fd95bcb7be585ff17859603c14ad3fac2eb3dc72b7c8076979f213e27e6e0f211b0e98de080e07d16f9ebb4e13b0b9842833b312e248b77f42f997fe1c33287bd9776e24f04df1d3df287af4b7079998e3dae0d502830b85aeeb587d1ec5bfd7245e641669e857dd763ce71defce86f3ed3657fdcf0d4b54e06ace6f2e8e804ea38e81d84c4046c46968333cbcf73d4fd537123b1f7270d13c890654ccc1b8ff4b694483742a33b3ef5fc06182f0c076a88c14069ccecaa5315827d3518bd2e3d114da697e9f3fabf69045c656ef65b1da6d82335285f74809bcffa6a22210abf104dda9ec80d72c91f54967c70bf7063343ed193535d9e6f3b3312170994689ea97b259c9ab7f144414470ac0113cbf0d26d1aec7c38c8c32e2196a0609b55a17d9272be873ebc3964b826c5db2a34d9f5a22ac4429b5d2c4429b79a8429048845ee5731914108f39a6a5f3f74a54e36f2bb76b88c3389c296b9772cdf3c98d3476f4f7e50e0badf1639aadacc79e14286f0cfbbe1071f9157db323b222d2fd6bbf2320cbf6998b563f63844d112161a9a9505b57b31817efa68b0e84b41fd6e4eaab1b9512fbff5cbab9c24eaccfed566c307624fe1cd02086bb2d26d383e52d7e043197f8267cdb6f6adfac9e38daf5deed03d6696df531f38964b023d6cb96042fbf7832058889b130144da5b5186c1d4b2e155a982b1e3ab14db892c316fad9b47dfdcc6b5c16e13c3c24dc2d39512e1ca709603196868b4c6b4e8247af4d193f823731bf60e5a4b5ac3e7ae93b30c920351bc7a9f3bb3027ac73ee635e8d1db2fdeccf0a98a377d5b3aea9bfda7d2db88a3f0192c0b82107bc55661b8bd3a5fc44fe2cac9489395c4bbd1be02b1d9c6b940c5e7b8813ae6d710cba7dac5364f6180f9f7352db0cce2da1971b7d5475a1c2d5b54f7530ec439010871300822411910fa57d42133790087cf85a33534e4644f7b609fc82e9a696ff4201548649474cf7da000d33d356b2e1f6231bf5ddc8163f14111b06e90002c289711df4022f4859a92f20c6b0128b5f8a9dcddbf89b1d50abd91322223d4a08bf1c954f19fabb011a6286723c2577c8ae68bc6f2867f17491a6868fddb125b1937543298f98956773db84e6a4d8bb48baa76863da4668a9462756bb24cec27a545b19dc969f53e9e585adb0ac28f8504508e030fc9a14672840859a29f7c510b313ed29977f14416c2368e1b6db1a3fe9611b0dddd483ccceb64bb3f20a6c36717341597b8d5023fd03041afaeb7b1f7797380947c511f1e97682128b82ffb3d37b54ac012fd95f130ab8ae0e7591efb470a1de724fb62cfb05896019cc2fcdf0132ca334851af6912221f63816e936b2e6086d56c6151ecb3b07263af3ac764630aec3fda1a35513fe1a0ba9e3e0d8e77836b27fc2fb4fa9db88ddf34b3503667bd63f6c2e65f6b6f57d78c4c5210450f7fd0d55b8c1492b1166867ddd0e4f31e1c22ffcae2fac3082bddc61d33d8fc4333a108cb5f50144e45809ad3275ff9c68caaf19fea6c3fb46266ab9c1d6dea010ffa557da80303eded3b505cd130624195c2033dec31ec9f5a2685ffc5a1e35467bbdd0c41a3ab1d45239f1b810c6959031fb9762da22e75aad32bd25fe093736230bd9d0f3767ce9735d940a885cc0e7feee599c29676b03f0feaba69007a4e0e11dad6549df5ade6af527149fc1ebc57778534cef2e1a0743d73e6c1b7051380552a5482fbf5318b3ad60e2fdd3db699bba8c8e174c06eff961de59f4762654eb4cc052ec5a15353a6a87dcc9a02203f63217a91451137d662406d40a9e2f23dd7c9253a4af5bb110177645801d76954d360c91c2680ad4cfd03e0b6e050c27b29169e7eb3362c87521cf0aaad08a6f27a0ee6f3e714ac6bf8b42482d8a20b820f6847d5a93c33039ecaa9b07106ad6440bcfe337347e8821f56d577e7cdeb4dbe326896c5374ffa1fee26441374ceadeb7f3a2bd69db90a2691e4df3dc40e2162606949de705046ff14f1a9654c8effe5ffdbf08b6cb66456057d0186ad6f20f3a58c736cede83e0794d6a689e26c51cd422ceaa077a1a826145b1fedf0f36cbbcb4f6a5da4506114c6c7e07daad886767556f33a8eebec2e64dcfc3d4d6dfc45de9f3e7aa21131b5817e9b1e927f2f2ad7aa492d6f97201676773ff3253a62b9240e424787d67e32ad860bf54928cc4056a9e646565bebeca03aef13982dd4517dde639b17af7ccfd2406ae03fcbb140f79940d54e7e834211adf1ade32f11b55191d0d7a75ca8bd542dfe4bd557358cb39d40be6d7dc10b9c770499c6142538e5dd1badd81a2df8add75432e7a510d02c1edac41261b6e8977e04340afeb072a345c409e8033b8d19fb02cfa2b3f202092325648d9f79214582326359345be6913c1ef300aae6147475741e947c901e905e8af1dd299fe820437696442340e873768a1b6ebfd838fc853992ff2aa35c15d334022ec138fbb9b77c8e3a5068998a2053b686d068445cb2a5b2099e2e43e77621111bc9aaa58053949f069c74a955334f7c1842f3b983028d9668abfb6ccb0b1430b095fcdffaeec256671f7d4739aec83f9c163f43509b1fe0a0a958ea2cf4ac8777e341e22a81ea8032f7f28456158ad4d19fb47dac7c9402928f8fe2c1a45ea05b5831db09a5fddf3055dcddc2457cb867ef7c11154fec83f1464a3fa162ab4fc4fa8169aaad7d3a1131d3d98a138c577545ec294b4064425bfcf82e4e05e1a8ce5acf208981e1653fcaef24130d4cdc9c6bbaf0528d8a816846945a756a762d2790a3315d2e5e81ccce0e2f4f568d65ee84e76426cd22105887cc7cee9cdfd6f9b5440c3df4fe4eda7373248d2c7eb339c8c890b6f50827daecdf2ee52301a17f6b826ac296f1d70bf73675341b1c944fca0da933bc691166bb3ba88b144beba21deb9dbfba90e4ff8fb5b39053ae57317a17252c1c4ca79bd0ee998556bced83e1638dcbc850ae6c7dc726f558c22b649448e9c04cfac7660c48295207f2149551588bf9540fd5f6e2d9b839339cca1cdf6fc7431494c1440d7354cb7193b9cf0183cba3c34ca146f7617b99e383177be3e9fb3255596d82f2eb204a618b46d8fb7b24c01fe985c8e64cf7c4cd5497887aaf321b3d344779962879e888a2dcce02224eb4fefbb74d64e0bd99d571b633f263db011ea7b204607be62c3f4ae8f96d2da668e9d2f2c65686f7a56e47b3bec176337806e164cfb5a90ade1c755fc62095f98637ffc63ee08f233578e3498d00b50b65629c00ff04bbe74c00896ae3eac1ebe204617291c4652f5a936212ec5ec829ca1c59815c3c55b54cacc5fbb662964de91ce337743f73e0f3f88c06837b55a5a5f6259415c552ac082e33ce14efa49ca904072ff55eaaf53887fe6edde02fa8aecfeb09a115c153702c0cd07ee2e88aee8e19b81f8f02d34ebb62ed6da8ef098f758a9d59a862c5088630812eb7217fa39b581a1f0599ad2fe625af440c7f362076f9a6bbb2bf30559137120788b1dd9e069761b51d2ee166c45c615dbd9e054d1eba33aca7abc6149554e3ae37c18954de39c496a40c190eb5b9425cbb02234023d245351a81147ed9a5ae9010d709f981129db81a7056b27934baab28a70b2a3ec6c70e3abc5014a28584d9d3e10d39299f0862bb78ee4083c62f7ab25a8d4c62ff26facd2ecdbefa2d26f0c66d778c2a0a42e92ccd1f1002c2db90c9d9d8644108313cfbb3b5d945582c087341f9280c9bad6156c1fd12b34e62fbfe320c145f0c55cd9019bc8d4d36bad08a9c86d8cd8b61a20ee7e6748870ec07893377ccf9773773db0ecda7a7682936622a80a4023b9e7907350b1f943626854b7fd3f6d034fa5b5ba81af80c779df3e95059e9eb1ec3291625581e7a2fb2bdb0e43a5f34a5121b6b5e92d4f25ea1acd7599cad34774bf5398081f4d356af4d20637838908358b70cfbad7d6412d41b7bc2dd53a0fe5d895436383b383573ca7fd1c6ee52134f0cf836045a76cc44a5221faee3d1a36b7b8dbbfbee7eff0585c3a5efe7505c619a1165fde9f7fa153389a08dc4cee0f26b22d585033fb479499c4c8bded0859db0086dad63a5bed2acbfa0ccb1d35f909c8c7d3155de078aaf1cd9cd0c4643b07e0a05d9b99c27e91480b5588981bc43727f17797c38a1f99d8ebe79ce118dc2a43031463434e9e263f511c31b48ee12d3f43dd518def5f40cfc1fa97c1b8cbc8eea7bd434c07f348fbe26f203c841330b6fd4b1c76e45e926265df4084ab4cecf19022aa6edf420b8f1e27f6d03f9e229da1027123eed3bf16e9d62f6ff573c96a7be9fc3df3eb1d6c518163283905aee63bfdd7133c685ca7eff06bc8c2b60fb4f247155332b32af882d06794bcb671273757ab6925a0b9e86a0c018c7052ba106df82bbb64f961b2c6f09d693b4c2baaf03a468f1afee5ce90b91bde3a66d755e4bf7d24c0e64eae31d0218ae86fd2b64d9db74e9d140310a861c500cf7569b0bb149b6b76e1dd419620a333e1e51dc07fe135fb3c58ac6c8870591b936be1b8a3b991647a452103ad9cd2cbc9bf54d944375a816644cc07bb8d8307944287a5b1acb113b08d64f84c86eb35d13c3b9266de03e63ff2a4e2ff98726252d0a12d5c7ad873f30fb79270ab615f7972e62d85754322349d94d1bd9723fc2c4c845369ccd94a8b7f8b60d5d34917973b5879434b0ce8da0c8be0df5fa055c31b0c69c264d63e1bc07293b71cb6bfa9e81eabacbbbe886b5091fb7dcbe18bcfeb4054983d922f1e12df05cb1e62e6e27135a050dc5a6cc13f5570f178498eefeada2e6e2a3b74fe24931b8dce0af1f7177a3c634368c5950766b66983950a8a6990536e1196338927b831d5874eb3ca6012b05c59d4b29366d363de7f7d87f76108d5c7fba6bb2fd1f0386b35d63a50e9f5c6531c716da846775bb6f23c2c3ae99e1734f1768e06d601ad034116a1f6fb0f0d6c1da630c0af080cc406cd54a23df78599dfcebd5a301c393759a9bf2853bdcc5f50181e5ae7947d65f275e7f9c23ac63a2527b8cfb81d1d5333465ed4404e9498d2449cd7afd4870ea2cbdad3946f682de884f5566f515389e63450a0b80729d4b2043425f0c5c00ebdffad2dad2889beed3ce668a279f8774f0cf791ef6659cac55babee370f59b0f03f0b128b2acedca0acb95d37acd767dbdad7f651280bd24567784b6779923dfe8cde67b467d7601ed5901e96354d2dc7b8775b97bb8375f999160e06c0b26ae72ac7c9e763268aee15ebc999bdedd01a22818ca7b0288420a26740bf393c4e58e21cdfe755ee447e9fd67bb556e4caeaa73200a66a96850a243492d0114f5e415f3555314c0c80b6c43b1a56c039a7260abd8a2dd7f050b5d4373788fd69e3a0deb229277a51931b845eaa1574f40a388c74b7a20317045643a20ffd7443236df0c141261741683b0efd908b1d8b50bd61492f2ababfc0093d753eca66db196ae628d9b947767637e0e984fdbf302a476433b5bf36223faf4911ff90672a6064bf259c466d472f261150716b24b2e16c01f810ead68243b46b1c846fbb6e59b44c2862bdf0254aea4e043ab2d8a1740af2d83dc98bd63b2547d6f6c3ddea067c482b5fd775e7027605ab6f100aebfa9b65965d222829b66aaa8b9492f9e5a70a09019e93713180c768763f3ba079f6612293b7403e314f8e90daec3e59965eeb578ae75efefc8c241e5488d1bebe5a979a6d3bf788189ddb2b00bdd2a590a980371d19a1d8026cc808d4b47ff16badef8cc0feeee8b7f484644fa1c2530efc6af98d3826d3deb59a06d2105d1717ac28261e6a43143316d3985bb0fefdd28556f8e0b1a8d92af8a6e52c6e4a9c2e946b7f78988fd26a993e8673aee8a6195955edf054bdf4b328448d6c384fd9472fec242142a703fca9844db9f44890d9bcba5cc47a72fd948f40844a36ddbb0a6a5054c4afed00b9c75ade36e27b4726814ec7456a3a13e4e2885f5884b00139a662559d0fb2e17de52c0e7338b78199d000f2bf3c4b5fcbd2674df024ac8979f7115e7706bc07405ee1587714f076342cb34467483948af6e14a45ace5b8066fedc7b54190c43767c6dc07be94fc97b44a3b89b4689e7eff3700d31d7fade4828dbfc7cfb6b032ba6072d53bbea2e940f6a56e24f0c3b9787aa5ebdfa5793886ab2abb4d2df3303f5f11ef77232b061e8b01e17a600c0342d1efba70dd596c787dd00f062c8ff90c9d0a92c9e8cd572f9815ced31982ef208c4d9f5daf5dedffedb54d6e713a3a4bdc6c6489dd4ab0c7c0f13becc252e2dc1e498f73827689d3944f21e973932562ad115e27698baac1393726006c3531e7ac25ef8b581a44e022d391b5909a22d35fc3d56ba561f477f77f3475c8d0e7c13fcdb73722bd6c83073fa0fc0df270e35c3b763bbc4a3b676414d874551a50ef97c33a065aba4d7b57814a584dbdd22091db35fdb28bbcbad110116d4e8222a84dd14918949e3d20da9b981a9d7155b1a7c30819cf61495a9eb12b842cbc78cb5b708fa2a60d1b62f7c0759f37d541c851074be421b1534b17e5064473dbc4e2dc166df6f144534c913f776df170b96807a0fc3ee13fc0ddbc83427b58ae8e9179a8642c5695001a865f1eb79d9633f3a10365a93cd324e412041ed42a4a3dd0c3fe2fa6602515902798c443981b707f4420411333353b7f08838efb717c0d2474a73ced510aa656aad01b03886646da40b90421781b237fe161ef44018f42a429d3f675f696b0e4bc10fb691b79666bb6e2fc37c860535b6b38e42efe333b3bebc4b93cedee29e949e5cc639cb8c977152f29403fb7908f1fb9953582ccc323b9c920ac90f9f594d3fa59722f4aa37454ca2bd2af56cc130d2b4b68fcc686735e917930995070aeeed42c52786c9c0ad54ba4fffddee1c29677b9aad590d9f5dc64f22231996f8a6c13dbf157cd04e8d33ffc1759fc32561e7bf53ef6ee8e90bb742cb191f06b66ef454f75df0e78cac5829ffa56bb8fefa6c5c24d1d4aa68f2dc67a5d7add353aa21f5ca38afc225f4f5448e3a8459bd598fb6d66c8d9a9eba7f09845712d745645e8b4944d8c639e2a45d888ac4c8460cebb61b241290ed3fa1cf3ac026220f57fb81b5c77b001433524aa047c8e77b6868ec7675223d6c57bc079be1150c7f59f665bb0c85b2d96a407ba8df9a86f0afdb7c8dc07e202707ae7a84e0ba3437c056775bbabab2a9479a2bcf76651fd37a9a27dc66c4afbe48c984a1c4d29f02d63c8f499738d3447c28a59ed7c387ae88efc8a8f278da50958d3e1eca3ae5f659231605242a5cc0957fd46c99280ac1525e53511dbd87d09b89bc99b15bf2fb00862fbde571bd28887262a50aa337e0d96abebedaedd760f3294616218f87115eece011c6e72ee39e4b4bd3a35f79214718113fb06aa1e9c29c4188b3015bda0f8c2ed22d3c2385c44e4ff1c6418c72e09dbc8b204cac37d91cf62a22f05914df131fbf53a190fb2c40c027396e1ce080b2d6e8c3dc5f755704c50e3712a7cad8f7fcb7e79894c6238d9814b929d0cb5f81f413e4799a7bb7c109e644b9116e6ec22119318d4c09f9504d0fd747a52bfa467fcddd4d0184797610bb3e411462ff7bbf2f9bd4ddeaf3c3183e542fcabcf2dfef647eafce6f3b0fd5d896a0278e7c6a559aeb2b765503e1a717380bbd12f2a84436d2fe1bd80324d53d1f00cfa4c603982198959c69e9941b0a44fb3c15642bf0d31ca3a13907dc0538b7ae2c4d51cc3917368f9576f407788c128265db3baacbfedb036741b9fb75f4d0ccebc2f8a8b7ddde531250c68ac90a30827b1e4b9e27768528697a4805bee9f34cd490e856f79f1e506eb098be03f9e75341a1495af83bf37829e62a3a90720d85801b41d77bb18a39ed45016d2bdb1eec34269ff86231794c7890adafbcae13bffebfb98840ac7ba6188c27936e898e72e93d070089a90c6967d9ab9077a5dd6c36632904962999f540a8497ab1eec8d0a140534e20da86c4257c8af0d4847171b38787161ae02ac1e9d7c0bb838ce6198eb7b57504b7f19074ba928395366ff7a1bd724a77ac10224b3d733244f997e83dc1115a7e5bf86b41ef226f7afafb75e2c422b40e3453ac150028feb3dedf3f5d42cd37c34a2d375f880ae8dcf87f429a7ab42c3b2db7ebff3f09e7a502fc7e00a34d367f4e533f44f8fe526fe9c36f47f25026b4ecff9635f787b15978fb6d2988f3a6eb7485d466f02473850334643ee14da0457ad4dd2c77e28316e6e399b78560126f1bcba4b622a02481ace171d9d514b020830c82471b1b8b2126abd987bb91c0e850af8d8ab61dfd8ad685185ad6427f9b85179be9d9350712dc19d57f15109f08bb1a9ef4e417c1dbdc867f021fbaa8796be90f9bc32322dc328b291e5bd24b5df4e6af314c3ece5e1c67a6711eb945f04ff5e7e845984ca9bdd7514057256b900c26356440732e25fcd324a2f2d39878bcb7bd0bc3f3b21452a8833403688a15abbae762253aadbc7aba457e07312504be0cb046b32618d92af3791bd0aadb438797a5d762428ecad397304679d5587c569c44d3e8b2d94d585f42e2c2a42c18de00381732e2cf678e5a8001c393cd6166a888a96ba18d7039e9ccf27c2a6e766027d8a7b897e757422d0654e5656fcab789fe8fb71867646b89d49889921df99f2812fbf57c90d72086e932d6451f33f7b4efc65a0225e29fcff2a154317634ddc323b17167b436e813f1308595f46554fbeab98d8e8189a825bfab57088cd8a09f3a71c6b82f05f601f4e7084fa7289dec3f09954e6a2ed05f9212333ff544efb3f72d8bdc155abe6ef2c7db43dcca0c81de27b9d6959ecf8d4d535a89bde5a68e1085447e8a5e0ac5bfd5927ae3bd0dcc8ddcf5ceb33ff0f7552def9bd9508fa69a5b328bd8dac4722b1cb95e259101ca4d72beabf6f0e69040713c2f15c03b0e0a646dcfb4c22d127a5b147afb0cebeaea92736e19ef1fcdc1795275bedb9826bba6a688d0e6452c4779ecedbfa05548980275297d4b6c556bb521424a98c05364fb264a4e3e23ef62269948d0e47c36976191da1e79140752646c44ad4c7b925621026b3d4f8eb8724be3ceffbe68d8c56bba88d96d7e24bf6bf917584edebe9830e7f1c6bc93eadc0e4ec0b2084cbd6946a3c6f09e6a26f9db23336f0a1cd421dd46bbe7d5062f4b803312849d3f1d2533a107f57925b132ae4052e1c1012b11ea3954dddb84edc2fd59191c240e7b61992e1502b9c144ddcc94cd1a99b3ac6bda269a0ea878a3ef0a218cf8ee4ca6ec3e4abe9511323084ad1175785e8dfc3a14a3dae4d43ff45982e1b881cc1b2afd6963c0d43cb8b98ab9dd56461d8299b46fa346373de4b3e2f6f472d912e50248904e99c4442dc6e6e798c78c2612e5e81364e638756fb595cc2ef2fef5492d3b47db3b2835d9a9f61ec52a0e873c5933e475cc6471797e23be32b8d4b3338144fe7b1cb0f3a8b9317b7ef5d616b08d3dd7f9d0f1953c2e5128167213a1542a1d081e34ca27bd033ab1a8ef78e687ed433ba138e6819523fb6a188e48bc984ff112b84015a39aaab4f9666955b43eb1751bf3368a2b000ab240ec010a5f97b0b509d58d270f58a24b941b522393e00f780ba47a4f90db5b31443ba12d5ac6ec8632ada6bb0b2bf500f74e91e8dc4004c9a35bb933941e4f48bcec57797590253a5b229f630fc5d118354de012cc13645dee62d956b528fb416a7fb98f5b2f22a7f1d62c2baa9077149c77df8f1a156ff7c9611a6bea418e0419a75af0b571194a4be1db5d25b4c0e92d890f885a10ecfb191b38a660793276f51e08de143034779ef81c5c914fc711bb79700089120c1bc6be1ace924c862c31a3900dddc775d13d63e8553427a7573871bd3669abcbe640d56ecdc7794f253b686895345453bbb849bf07bd32d0ff7904532057568ba6ee74f5d807d1bf163fdf3720796e2d64d5ea74ae04e83c41dc8d949e37150431cc5aa436513f28c0adef4b7769d6930b35acb337d93318a472b21bbdeec42f7bf90b595f5ac1d60349648ac3a91d635288120aca0af5f5b26035e1c7d206422cd801e03f4bf96a354ecd31a9d873761134dc91f3a0c1d6b3c6cbd7765c6cff3bb73c98bcea87d052307a76e7da035c5270a777717fca784d6161de4c980a90d6b3c15525f8eebd1e45c1c1b3df6d242c107aebb23440a2753642d695a33f7cc8e6dd8bae525985972d17c83fc28415d18f3e7efed0d2207630cf54fa29a184eea14ca5f6befbe97aa74019285cd7b3f1bbf3b7c531e1e90b9b039eab296677b64223c479309eb2d53dbe843cdd93df7441439d758e58b58d5e8450c77007f90fae767efe097432a739e8361c6e75f23e09275f583754d0ab50d5398314aa35329fa96ff8c6ded94003b7c275e927af12ba2cf764e749b70b331ef63ab605c02979426a631d1a32a8554e615617ff44c1b68271a258384efc2687511c4063634a9205e637983d7a1fd7b146e545e039bc13faba7309ea80da50b8ccf470353ab39a7b6a01bc1863ff3ecde47f9a7342fdd54827dae16b8da747f77f1c40284a8174c9d8edca1fd744ee48e5d240f6e0a8ba18459e6eadfb2fe45417791633c0e202a85bf7ba0af656dd552cf99254f73dddda24e35ef6de50f40caca8bd73c6e1f46181f11a232898ed760e7b3ee227be524291a675aac19ea2957fe63d5a2b08037202cc04b0f1de5d1fb0c905873d1ba326db3ed8e612ce16e9837d6bb3c8a8716a390c62aaa9d9006f774a0a24a47d3d4dbe61d3edb6f106bc85fe5ec3fadd88ee4ccfdbe1dddc626baab23c982ab9655052b98ab859bd58fceb08fdc77172047c28d651a7a0880a4a20c7096d9d7454c5562488cc20a1134f5cc4bbbeddb5514a77c92265e61d71b7621ceb215a866fe42caa7b800a45fb921043c3196273b5f30cbbf02e9f96f4c8c7503d4ee27f98d87bdb1083b27245253405a86bd9a44fadb38005039a13972e2c5dbda0142ee7f637fc47dd358f023168945f2719df3c549b11aadc255bc354799a0089ed2c39981144ac5beb35923d2d5367f95c67e8ba5a729b70778a027bc658a422d58aba8bf30be35802268d7442a45821f9b82c133ecf6421b92578675210ab6159eb2fb296af448aa81e6661e4d3532e9b733720485c621c499f652bae9d6179c39149da8010eecea20cb1c1ceff1838c3ebfe83e33d979e9acb9e3a3e90c294ca9a524f8fa4ef73888214b2dc03f8cf9467fddc22b79f6b3f6c75778415380fedbdf3f63f2fbb8211633eec5de3e973ef03092c304f23caba1e333caded307cb9169a5d829bbcdd14de618ad48a59d1fedc59ae598b1ae6ceff928815c486dc74e31900dea7b21e64a367e9e0651cd7083a1bcae890c79cf4b1aa49d5521cba6a4c83284e2f504c6c6f2472713eeaa3652278615e1db367041578a2a3bf13584c3b24dbcdbc07b28cc3271251d62c9ad47c6490afdafa7e66b0cf34e789fddf7dfd6c6061d76e2ad0f83931fb355f95314f0934e17a00060c0a6e0fe11d06c20f9ddd6c0a3122748a8dfabef33c5f9e83ff345cbcaabb273cbc84d891bd7ae2e31d2eec851e80ffd37eb3bffe8acd1a7312174c0ce718adcbacbc9fd014b691b473226fc91f632eb098679c93b2a5fc569c5c87fccb6507bd20d37a5b9de10707bf2d8383f935b1a75f5c278e5e3670ca5b6a6f3682aba0ecb57a6416b8c64e0aa03467cce289bbeed828e61db769220a26db0595f5a0330754b98b6e45d6c7950d152beaa8e70fdd2f294be4adbacad3288eba8ec0182d781e9524468c0a0cfe2fd59a082fb7e1a0a14861aa544b6f969fc935247ea523c2bce4080d241dcb125106faeff2260bf59fdb81fe8db7b0526da525225629d11d17e18cb7b7c4d9ce3fbbea3b963571771e85d578b76aaa5289d7a217908bb540c67ca444b2783962827abc82f0cbc5e050354394d2b6a71326a15b0cad145ecfeae2cebb9cc7d9da7e0fc09370c3f7afddd89755d59ea1228442da733e04b97d8424ab8cc8b1769a55940f3465f80923dfeb602de4c7d0baeb7c6e74cc38ac4fa92cef2b05349bd7847648cec73e4ea4662a155833a640e2b1ca810c036a17a053b914b1037e8f459a0307084a1b9cc28eb6cfed88c01e3238b83e65717cadba6a95abb2f6ac796ddd95393546fd074db1fa9cd5152961dd00c66a64319a0e7d2aad4dcf820597d3ed572855bea13575cb018918abcd52b21589780ba37665a6576f29213dbd98b999020257ba64450328e8f20d5ebf4fe8d00ac62b255d65ec76e27180a017a722317e507335ecd25a75d41f97eb9eb2641aeca0898a1c8936f4127404fb2c7f507237d7428cb349bcdde3ba03dc0e6e03460f8e8cc0cbb505aadd627aa0836a7dcd095c34171484263a8943e6bada58235400ea889a5aa55ad49b3a3aa5f562c3f1e72fb52da6e05285063f856e56cdafa156db0df901e9c8006b3e13186a38c841741b85ff4aa2062b6b7c73692bd99ca10a4cca82b66a0d03148e7e8c34d6f521c731efca1d09cd5e9bf07e65bff778c8ca914fb4ccdfce0e0ddf56febbb9393ed1ac4da1958a3bd734aa9325385de1ec797122ddd49b43f060edd43f788c6a5e237c3f75f6b9dda7079bab5e3b1f2d29a4c4f366490b97f93957669a8103cc2a5e1c48aa80985c9410c37acedeeafbecaeca8aa1cf57e70089ab325f5393793a80a291f383a004c0c66c0baa7dc72d2dbcef3721fc013d93fea1c6c04938ce424b41a881d1580bacd6b31d0254bc86ca258aca2bd882ce5fbf4c4478a123ca2552318c2527bc67ae73b0740e384da6a23421366354f7232668b3fe7e354a2f43ba4dfbb95c2d61d7702547f42f85820dca4caf5c66e96a9fac6f03500cc5b5b73e095fab6d794689087aaae8f22871992ebc81ba758155dc642bb6e38a259b38d209c5e1290ff0d40b06a86cae978df94bdc803a5b24891e99bdb241ecab7052d5dd3e58c7be18a0a01176f64003f4413184a3332cacef7327f92250afe8c0a28466c26f88d9d960f9129e7b09f108e08d788c567d509bf30119ed0552847911ed57699fb4af7f7bd327b2e0a3dcefc161dc73c9767ece607900f09dd4c22f1357d17fc8a4f489a71ad21a91ef2cfcda725f621d4758340fc176c85e95de15c7b4daeecb82fa285d24797aeec01d1547203035103c2555b2c404e7816c20df6f4425df5d8273092dd48935d1b0e7220acd0b9735b6c5a8ba2be0424ec0e0768b561d5c0172e2172a84ac4127678d0fa97b256e5fc92bcf40d41c7d32f0c6deb7c17c04b116a64d6a25a4a27030b14054dcabe51188299ea370868d3cf53c510d1181d02baaf1f33c00f86d41f94d03cc397a29aaa8b7da2cac57247bfec7e568ad81cf97f9f17036def469bdd00030776a6ad0c5ed811bf62cc2a3686666bdc3752e6e8f0564f0e67e7aa636f55127c8f2cc51a6a05fe0285359bb98a7c4424792fc59a3369ce964673226f1445c3aed13c92775cfd19bce2c89ab4c30b633aba62c28d42e3a75dd01fa3a6c20be3123c29d81eafdd3783b0fbbafdc7ab3264378b29a101da81e2152fa5e4fa3284d456c2497c00adcadee15734b6dfffd88594270a4d3f47c28f42dcb8c98e78241621e34063b85b73d3aac61106b5a9fb71fbd5373923e264a3e278c8d77402f74f1029cc1ced9723323614bcfcc56c991a5dbbad8c879ad68ae50e55e8ac8fa94eafd99780ce785264444b4bc837b96b654e630c0ea3b94935f0c88484c8fe751e23101cc9b307806a87e90846acd95213568c54798de63c94ed35d8b3b57b83b08b6b50f4eaffcf6926e20f859eada1ab1a159eee85e8e33da7d58b96065d90ecb652cac23831d597f9e3bb1c98cf6babf8cc9a6b2f3dd28b2f9c8816fe9514cbd04b60c2d8f282003e4ebef6169b1e9fc028c6cc0c468d02a5d37e3f5cb8dd445d27d0e045402308941d8bae453a102bfabc6506b6420a0b1e303cdec58e25abd66b6a46d1814412198024f45668b382ce4dce9b95f6d224bcd95e3e81101dac7d98ee03b3f83eb9744de0cfadebde56f49fc46102a5c7c9eab8d83f25719880470de96fa3bcf089f30bb84a1aa21ddfda5589c5b70ad6c71d22041a0db428cc571d2b642310af1ade292c19431dcf63af8a1849242ea7bd4b6061f204f637dd23505ce63fa14d8dda96c2dcfe6c4763761089ee30e2a9bf2f68a5d32ed742fd17f7abbc82fc7b47fa4ca20e006d976ed71b9fab92e3b89bb3dcc1be6dfcfe83c62ccf9c6d590a0687f3c89e5783031601efae56e86138db6f7dbb0799604c15a6c6af8e36d29a9a518311eb8650831619edef10e44002784ac2abfceadf8a29804122c6b76225b86ed8fc2cc444d47204a84cb3e10524afd5d65feff0c00919c3488a1b1dd6d78cf8d8b0a71345acca49a25e8fc4f1d5bd1a8d7afdc31c7b7770ad09c97a16b9267543e4956e963673cf73f6cb1cf001db1adfdac1f9068c22667013de2fe3b7547a1080b279e6a2930b82fd2a7a31ccc633661a9496f2418162ca62bcb7be47c4765e03598f5b705abd05585c32188c274e25bb430e58cbaa75c849d2b829bcb537fec64f4d4b775a745758d59c8d650a601b95ac29389ab592b1309a9df35351471a4935a4b6c6fd9bbe60f7ae675df5d5befdf6e529b8cf3e5359478c6e8797d04422d33cd8eb444311e1d08c90424fbb1c862031f54e49b82fbb225d61694f6255004fc5028b0c6ef3de1fee85d60beac5393e60a5205a65ea2615d75e01437527931d8c19d13540b35d97f7f913b2d628a8aabbb1c51ac0e1e5a0f759b9e0135dfc7c904473f466f6c759105506c01159a86e5b7623926cb8a58bc824c5a0dc5586d695cc70a01f1128f67becb0773bc8ed234e17c23e756a6d9c89c861f9137fa18b1855c27a5b82d8f5f56bf2f8487563e657eb017b87f3044cf72ea27a45b5cd22e6e7fcd78d61cd7dfcba898ce304cb593f6bf78be7b5f5e153d975fabb7994bca23a6505fc87703a8547e2ab1b02a724d398e74deb4d4b5631a441941a050c066c4e3766fd19a6e2f9a109b4a0fb59279a81d3fc8e19c42e9a6454973d985100dfa5055466e4919e22faf38ad866c6a0313a2a220e354561f7d8b2e74e7ea14a90ed5663036edda54262631c9671d1fd112b62036601c5d04c2c836cfadf825620c4e5cdd7331b469bd873eb3b5d97dbdfe10571b8403121d276263b4e05b10f0246c90f631f992c2980f25bce7db62d4d6b40a53d3bbcc57e6a2971451178d79f1b2003ff7d33fe7011d942260a20f9b21bf2cb43228a53d489c2471bddcc02b584c6b62b70bebf8e9f534a17564d6426c7ffb3d7b42004eef7a61a9c625f04bd9ed33d7436d8c45561478a927461c672a00cc7881848aa25dd760489a8ffb650dff24dde84e413cdeb09a302383f8966a91b7ce5800a97e5d701692695110d6251f67dc170d60d0dc845a8a4b1553ff7c2a53fabe9cc4d2063d4bac0ac7c35c49c8ddd89eb1779067424724c6b611711de85b71149beaca507b1d21701f13aa3507421afcce2681581d0c98220413680ae2e953f8a2bf504e541ff1c0529c17254ab77bc605c6b8e163b60e3636571dac652baf6d3852ab40b15e083bddd79a2d212913f10e862500c87ebd31deaf9a61a3ed7d0317f93969fdfd92be38a7f267a314d39f71d94c1f02a7ec811dd3a5c48bca3cd4c41ef75f8311adcc65b955860f90e904f8a1512c4047f8545328cbd152b25219fd49b9c6deb91ebf976d9b190d165f9b88551cc462f019a1a45ac04c4fcbfd5f8d659febf4b00ebd0030fa8626a59d975f594f767b056ac641d5a96663e4609afd3bf731db6d470dc4fb8f51856bd2b31095412c527676993f5cba1a8fc326b61f5d2704c58cf1207e954c99f4673e1b64015e8f4dfefdb54ff7254810cc7979e4d8785f22859e801b4658791e15d87360ac61ed104d8c7ecb599a51ed3f7580237d7dd4918d6e40df7a89e48890d768829ac1a1aabfcded56af715eed2ec0a9672afcb954ca2e29000f00121564c90004fdc63e152dd0f3b8039569cd1a98fe1227facfc3fccb9cd2cd1f5190fc075f7663b8237ad8fc23d5e994631e730dee0f6391f421acc9dcbe94d666fc4290592fa856d7a523cabdeb036df5d0e7d970f928c9b53b55a3268862cb25f8ed67505dfdc6f7a56fe562fcfa55c77a24d915872f48560534ce212b0ac9c202cbbf24891eea92ebce58f1e6b6c6bddaadbae03626a3d7d0a893f80657682c42fe35d96dbbc405260cead9fea21d4bc74c2a9204e19f85ec3ed6c8820f4b5776f762887b05e17470be1683f6e70bf4e0e00778a04ddbb55d1d4e93de6158dfd3b154557a838da589a10f41fa06541c0afdc2874396467e8957248e6023fb1762c359818745c662de9c4c06784788696e7c3d451e77fc72c9f614463083f632f522afc48a62c1993ef1f97149a970f4af7fa446795b02d1a560a8b379ee7b4236a4f557d3386a9e764ebbf867d4076152646db5d5b21fd9ac16b5da29540c857cd4a9d131c00e077996aaae4821889f56e9d967ac6787a535f0948b0cc4692434bec162132c6e76e426e2dcce4ff1da0f622f2c66663ea0a8b894fce304815e9b5b49c71cd73e864f0af090dd8cf838b853c0c06017c018c91bb42dab1936d6a794b5a23a87e3cb215d197c18a984ec4d516dd42c0cf2250a8f83b2e2db14ce90c8b1e2c53ac050c142941627c50ffc280e1ec56760c05327b4f00000835bc38f74119ee3944a7b5197f2f5acf77f54b8ce0f0c581329ed0716b202df07d4fe99fb44c803d045fc9d61e36a5a93da67c033620d37226dadcdaa2e1c76603a81e5d2b70c231c63f54f9c44a4aac60ca1c91901efd17101f2f4c386b4b9ebc6d16af537fe8bc6747f667049c1eb430c830bdb6f8acf41b640fb486f9e90f56e74dae9bdbb760adbe0ead631fb49bd82da5c278a4b8cc834b2cbb3462847ed193f990ceaac9f876fe79b9d7ea6d2fa511e9bd50874a1a9746ef97bc1b879bc80e62926562b267683e64947f66c12e4a09ed1f679d9f94ea79f0c5787820d5cdeb3b2d4321898d1a937c476aed2f0caf01c4be381f8be8cefd50ed56bcfccdb592ef6f7f4917fa02cfd07c2f6217314133a3a9074cd8690fd4cee88c4ae030b0519f1e372866309d7d63c4ff8605ef1e74eb62fe5c4e2f12af4c0f7cfe12d137df9d01c6738425b587a3beef41b5fa5f4cfdcefd8d0459be417cd5bfd2c0bfef826b5d09d2068d61b4bfc78da3d89f3ca576262c34d5e626394b099fef3665d8a53bfc2e03c698d57b2a3670b4a8662a82b5059b567ed3eae4b8a5d0d3d7cf5da9a727fb5d1439f44445cb7d77ee049c7d68f9a920481cf2ad78c648e34609338c0fc96dfd908cbd7949acc03eb125f84be720ab9452234ed1c70fc6f0486d4ac5dd08a58c282151854a3106596b9ae401df9613eb94814c2b2c4715a5805fdf7693a19019f1bcda88f6beae577c9578a9b0847a3f413b86087f1b3885423964d9d079a6110114e243754de7dcf0eea886c54012ff2cce1a53b5c96d8dfa80bf9fa6fc0754b08168085016ba4323137bc0cded5682fc9bd0acad043ae98faa81d712fae609243e1d3e7afd184a72f43ac26c6e4d4f04da1b125129b4cf15d4a43ad2b23e57a0c4e1da917880914906bb40731d14d0bde3a1b07af68bce4a9494108f0ca958ce4e9c733e212c8a5a3d0b44702ae8a10a7221edbaa5e363838e5c61cfb0bfecab6bbdbc5b9d80047d75f8bf40f181ecc2eeda3029f051d2cd750677a4a10f13025f66232f6b40a6f6cfc55fa9b6460addf95485a022d943a5ca922422957696ee2f5a4def4cc31b1c5b1edb91d4d683e99637709f0ffba5cff4a24c2e2fa304e74aaafdb512479aa1a60172f12405b71bd7aecc16c1501a8f3f9f10d56234946b0991b2b374a8f2133d269d27a2039219e4aeaa8af803e1567e10a8c220a2a5512b1bee1016c6392a47b9533a69505c24cb83c7465f4a1ed7e1346b6d14ee53f232031d5e7072a60668cd26ba0df2f1d96482c017576f3828b3ed0faa3f36fc2d9805ca3472e99babe4caa553315f407ce17c76149e0ce9332b54ec4a8c48fef2e839d3e5e4aafcca9897ff594f1ce92ddd23a96973984f537557245909b28397c00702f27fdb9445f89d33508658005b8581edbba123e15a42d412e7b2e11e24caf38c6d389a166c9aac7434bb933810e50af748fef8e25b19f38a42f1d00db6880e383af0b79f51b094991bc9fae1f43e095c59cbdb1ae3d1274b316e48c0d7dcdaa2cf0cfb1b43ea3c042169fb9fd29a8d5f019caead3dbdb3531a2ffe5b08123f9f5fd0b32752e5562e4cce12f8568a6eccd265621ec5f5613a579cc2b37a2bd33c101d0fedc7d8fa8f4dfa6185674fac4a33b408425d5fd22dbf1402bc24267cceb404b93577f1d070c46694f55760d2f0e616b099df132755dd499cc8824eaa66595eab73c89dd5b971ffdae10fceab7d63b46150160cd50ab682d8ba3e2cca6a96c4d4663ae44000f61045c1e2d04e3907134e6c8991e0f133084146572cde2e531856e61b88aa1ad9f9e4d0d85d8ade562a5aaff5c1f751a9f2aeeb35982796229bbdb4f2c8b46cc0d3032ce7f55addbd19c55140e66c82c692b65f8fcb30c1a844b407555608912dbe6e92ebe1dacf2e8a64060f83d36a9f9939029aba42108d8aa9c3225872beb4d0d4c775446530346872b78d67828d5fba1a74813fd62e7ccc6a275bfd70d755e96aad58adbefa406a1f1688428bfe8bba0ef03029696894df5f789b82c814f800c16b1e0372b8546663526ee5dfd1a82a253d2600c6652c64435d1d4ae7f8218e77d484e11e7fa74c355a6f84290922d572f7b3d77ccd31f7219d5d845576f82ea8134ad026820ef4438c318d44c89665017ad6b15b790a77998154789159ea90fe1039b1d3111ccbb4ebdf59dd68f71bd24008219cff731d51a58501461650884ed6478200aad327af70bc36a1d19c8073125044e06289666b5f7724e6d33b87a05d2327f45d9ea3674ad034df51cc7ed0906377a3b51a4462190e2d638e4d1a7e5b7196918a02df62b7c31d6fcaf1ba9cca84d3696a3f141ce238d898de84ecde2c66498641c6167c11398214025adf9b270de08d97172e5fbdccf789c4cf9a7f779c0dd72dabe86964b139faa0dd097f12173902b41ea29c2f98e12eb4bf515f342ca8e064cf15b83e1cc96abfb6f2ee0e2e0cd52f4d0aefc51df526e79efcd6ed047361626f826b12e752f79c5b477fd960b03a049b671a240f75c27237ca1c74333886bb55af8d7ac16ff044b50152df95ca6c14b859359e6b01ff4adf7cef62b609ccfc42e833f9c468871b2d4f4f6a866b524cfce78e2eb69d89967489c2543eba66f5d585f5d2e245774721f224bf56669bef835983a406ea9d6191030a33948ade40e73ead6ba8e9bfde60f2164aa05166c6ac1edf17c752780a07ea934a353f078d8dc2906fd4cdbf4d704bc5bc19a2d2b17feb280d0cad3e86a6495a4bf6fcc2ec568b8a3fd5432e4ffc0aeb07007b9764e4c5f2482fa980dc35113677111e9adcf69a27ee96b13a0a689cfc98ee2e93d28521b6a58731be67c68e075a23b0e6f110285018612f46c3d25c5b0223ed0cd89df1a7ee39c16331cfb32ddd909fdfa6d12e0a4ee4d1e78d407beccb79c467c5d30ea8f4605ed16ec84034d26fdd19288fea1d8111cbbfce16527b48c22a49c13ebde2902bb8f69d86d404291e91ca9ebab95dc8e7b2215c972e86920a430362addab0630b672342fe88859fbd6201d5540425424b2633297b0f888605b4ff9da1548af9c6429ac2ffeb8766c66a31749e479837de2063a150a60bfb1876b59851bfd056badb27a27684ed9a0a3f0d9cc4efe54089f39afdaa9d57aaad4349293a1abee91cc0a7371f8c3951b548d105d3778bf581625bed8e530ddd3994c14de4452f8532a192815ac8c61593f446044ddd18baee14ea447b1c9b17ae67c90081a17d6e08db2f9cb70eb9ee68cd2bad57981c266628c2ecc000caa6624081d8ff8167b9053eee5b7082b34569f71cdc3a09f8971678d03b5e372b7a0cc13435403ccafa1a73999f14ade2c526e8046cbee4ecbd6d156c6a48e27dab5307a74875c20a6677099bf225474fd7b8b42466269008cec1982f1cc18afea90a9b71b569f6359a8bee22fb47519438109443b67423aaad093b4d5b6d5c749d4e07e54365e9c8d94eb5263b575c48159c9c771e57dec29c27dabd9b0c10644d2e33ff32b4cebac1d33d9bfb0edeca30979f2af369d1cff76140fe9b89dc5eaeb2e257f9e4bffde6f325e60f9422fc1a228443ca4397fe616dc992ed1a9daf2cf2a8a8f3a13d618300722933babe1f8ac2f2f92aaef19ccd924b62c6cdecfa6af2ac74a1497c6793424e1170471cf668694b9f34412c9e7ecf39aa239a855610bda82908b6f02d5691cc64a217970348441e3a1527f55cdbd1ff9193976d1a8f645c1bf7046fcfc0abe501176888bd9ab18971ba08dfbe3d893d43e0f32869e690790b08e05e3a1169af6ccbdbb022a66d681f42a1aa8efb5ab5f63213591dfd25450b55feeea40e8ad07ad711109c878629bf88917506db2675f8c2de6470537c64b3f3c1a898adacc388b238f22c4fb7b67ec3ee33b603561d35b7f175e463bc9207435b8e1e7b8355ac76c097ce2cdaff4bc99f15a41c24c6f3854ba99866cb06a6efbd3bbfa1928ee51c71a663584a38671d624aebbdf56231b26cf3c2175b2fb012293c3141e5ff01110cd6e1f23041cb075e57320d2b5e5ba52f34f37d6d2088afbfdf370997a137ce1436b7e5c9c35c289a04ce07d3b637af4f8a02b29821b5dd890b4733e30952c846e7dce7f711ef77892136d3a1db29babe989801089212d4022aceb5804dcba318b461fdaf222558234633c615a64a4afe6682d0957e755e2840e982059ae2a85887fa63e5b79186c026d1cd22fab7591b50559bbf7c75c7a4ca9064991ec2c1c6e6f60c16ad7f616219cd59f0a14e15ea47f6114213be4d6a859b1c099cb3da238f7f51acb460ff216faf59731f83477f12784c6b5eea14bca9d829a17b3ed84668cd88e63c12b7e9aeda0e10a1f5cbb521c076d74edaa4b3d440e59b8527500321c8b401af6dd83fd45113899301fecb355f017801a1f4f166f349c7dff4e351c0238f117f0344b8221169110ef42cbdb8e1bcb76621bd0ff28df1b5f349dc329f450291529be68285be3ecc7764d0484af3e474924ebf5311da65f5ed604ac2cebeb5fa49aa419f0f92276bb840e773204b37efcd40b2d80b3614e91b094cc19e3da2850518ec930f321b6a249d7c583232ace02643f6ea915084b7c8caa72066f4b311395e25395ba413a7e759b0d4d4bd49ad721c12259271bf93ae322dc092ad216e3cf6510263acd30f3394df9ec1ba2d6de9d05034bf657b6a36d4aa2ceb1c14554d9827c83fdd13c0cc22adac45f2170acdb61d901a99019c449ead023cc319924d2f25cb14cb8c3f387b07b9a990ca733690651607b95fc31c59176b1e4da8f3861eeb44f92b80a5b5e5a04a915384e6639e78b20433a990030ce19727d26111da3581a40988315f6d0102fe64d9ee6e37d13e6ba7253f19784e4ea6e7f2ad84edbd82141851f5b4ab78d02e439187234bbc7fff47da71889cd6be36217ad4e50030ab418af8b5a57a52d40846381bae8e12820bf0eab7a1e986ab9b92af140775bae7489453fa82368eed94ba6431fe95fd694e604401ad24d3a042f4d35f57fd0fa44fad4e16619216f636412d1dd8a95f52ca364fd810373ae3d1d7579fc7f879047733f44f45d8f6c3654cb8ccdbf69d24ca66f644106c256c7c28a1b4b943af56e87788ea98965e1113ecb86d289aea2d537c55209b1575f686b424f8b305952e620fc4ba5fecc29f7704fe1fb7bbfbbf7a4da8f9a88ecb3d48d2295549429899ef8a64bcd151bd1f35998ba31f2d70fcbefc7d38817d4fd3fabf061e5fc640d0ceb0bdf3b6e418f93e023038fcc7f5d3d6641d7afeaf6eb533594d918d9818afde7df3c1b5698ec5920d61bee0acf0ee211204bc915742c3c51a099fcdfcfbe57e381c6a4d6c6296a4b8064f7829b4dfc4c9729c215e04cbd595c9c795a54446529d0acf3577c7501b3e5c37b9107a1fc8287dd4367df39c98a3a7475e4706293a569516995e06e359e230c816be973a9b532e5e197a7015d1f41d074a67aebafe69d9617ac6840ca49b2579496ff7183197f642ed3821805230d9a9052a6517e928f00973e62b5b7bd531e108d2906de201559aeeb01625b60cb4f19796cfded2eb25f0c4666cc28b89b5bf4ef7b9bcfe9c672caacac67322d00f2dbc5186d17cab4a49ab38739b1165613a903e9b5a39f1281d4df83841892edb9aefb1cbb47d95b788a3f56cdbe99000ac0ec6f3af5addd1ec3b481d43be55de9a018c5a66dc2911919d7f626bdf068d46d6bbf2f697ade74bee31f9efee26e32c8decd06675283707d642f5b692a1d9d74ea9b8202a62c86b7cea7fac45af5ad9d7bfab63a12fb712662013550716d0fc40c5ccd492e52416bc4279660eb41fa7210add767b8ec8f3877ddd6e17a8e60e2573d00c60120daef0cdd3f2de1b140dc0ab0e78c1e6eb16c378e5a932df9c5e7f15df0b793ea1577488e796b76ad191424aeff3ab9c1cae43afb0f057f89187d0ae6b794d2824e7c81b8afbfb3e8ffc12efd7d0662aa8dfb18037bd0eb95b2d3d28100bab78e35bd08485c5bcdf017755c1ebb9f7cbd53af5f925ebd6dccdfe1f5289ecbd93ed6997129528baac7b3f7c79cd19ceeb3f0def62632d36270480f26640b65d863e58803d67f2471926f452d2c5561856b09971499826b2ddfaab0c963b852a490cacdefc872368ab84feaf3b73596d40f67337c76c397ee989aca7ae649d5e9199adcfa8b607bff2d99e333c59ded0c98ccdc177cf9cea289d07bd10959a6e4553cdd0c4774c359db6f71f3fbbd52f5d62f591f35d5098b6b524325128f02e0c6906295d6a1a8a08f23e5b8dc453fdb2a2cd0ceb5279bdb718d813d631d0dcce16f4d73c2221c5f588da2c351208de5288076ef42b8f67539745cb73f68d9d012e1bc3c665859538d5aaefc2329ad9bb07c84a5970c6297b8899978ed85274113af33a60bb235023a5963e443b8e41663e39bc79cd7696673b55cb1d8c69e7a889988205519fd883b51296e55168a8165287d4c17506660fb91c362b707165ce93f87e5174baca9bf8906cc2e986062784326966ca48d16f4a224d4e011e61db49e6835d122652fe24a4efb80ab879f0f7bcb46c16d8380d5aa92d72701656050a363e1d7230c5160096be3e2e9b3250417e94aa2684c7662178d0d6ee819dce8da40a50b7a80421da780eb22879db01e84f6e8df549082b0050191f018cbe39dfa2a2d0cc854e1bac8dda10768493e121f01fc6aea57ac32b3ae18bd093233f879de2c596c03d33308ac0f0e5e320617df7e13a1a94c6d56cd78ee366c4159927710882f7842c2cde82b997e33fdc342c5185daeb30e127a8055a19b1ddbf4b817c5c2ae07c877eca6a8c23792f30593631b4bf961e1bd79963a5a9092e512a9b964ec503d377aa31bbc64c30591fc85dd718c7922d7e878ca6c2c0fc82c0192f8dadab2c7dc55fcae500be3a92e08031faff194de868e1fa681e1597b0897f82b52efeb8466650b12d74766a0eb199e860b6c374b663788a310eec8c9137bf45174e84f4fe5c6f92fa3e28fe93b8254a5ea9d9a047d8b64ca90fc44efffdeffb0a5b0453c1fda1da0177c884bc19437c156df0f00cb71eed223afc3c8dfef4f5c75ce19aef73ef084385e4e992a05c53b65f6dd7d7223149644c1f05e12774232dc1926dafbf793e526870f7aa19c6c489ea284f7b031a4e50d232a27c094562a9155d71b032e3011afd1342d6d48bbf1d4561681801634be2710b3c42ebbb5a581c96afb9e5fcc2457ae72fd4198142eb94d3dcd40a1de6747f7a5882710a31754fad9fcea32a4e30dbfa5c801c8451966ba727ceb18820b855a81ed7340f725112c731b76fa5fe5939b6a94b63b582679db1f57ca32b2fdd43afa96c500b534b7bcfc2be4f98c6b226b66ca319b9df2011fda80c78d785d982bf7f12ddeee1f8e07fb34866e51c6f33a92e1d6a4814b5adc3c4aba507d52301c219e10e352fc8da86adcb1c02bc260e6caf22ca7cc0084bf0164b51ee13e7fc26f226946efdddd2e054908d39fc01ad9fde6220ecebd0d1179e697b36bc3c37bcaaf91e417013e5297f9f2894da04cdcec09963b6310ea449be6b21a21792838dd91635ccb99ee715f497c0e187f2a3dd87a1df0d5e97954a51aebfc9243da2edb1103b4dc831c9dd48579bfe371388545d38bbb34b0597920356cbe2b8c52f3495db920247da58b6c5813dc0e501e1dafabafc6de3b0065156252212cad818cd09d498654872e618f9eb1acfbc741c80bd5002dede211a15ea7810f66abf8fa162b3552d9f9687fbb9d1306b7e7eea8db7a870c25e92bca9a1d3eb5ce81ea97ec29d33dabf57b7751f96898cb4e4051335661256abe7230b05b5a462396ed48c1a067c42b50e4c1017d06bad4f6952f05d1ef38e0da4861c5dea3915b2505fea2f765b962141a28b87626a42cc1f6af6875e4b82ba55c8c5d32722d54c7c5bde7ad0bdf89cc4d3a5856fa47c3e1588e31be2b3fc97aacda57936f263704d8b8201c72af0a7367415b7ec62cddfd5ce14723a29701f2e50f8519236e10c7228e3859dcdb727566f3c13d13de090b7467b1ec9eb6903326f7f45127321c125b720f73803afac2047e53546210715611ab9517938ca4649ae83f8acf69678a4b0e56ee0f1745c3513f4b0666d9874696e63cb795f7086ce31d971e298189e15ccf822af9e9a9594ab0933ba48046804054065322b1ded1aca6980b8be7a6688992e0f27fb738752685ccfe30013b8160c4faa52c839245c082b3718cd34dbe6e74cfd5901d35f0ce44f8084e1fc7fd89a80081ae48ed317bf92cfa64f2fe5ca4484a88589c73c16532677e37b2df76bbe5443b80fc4f30bb4107be20c05cbaaa4c5e31d2b6546897757e2becc363c18f2ee5c326e0dd6cf33539c770c2742bbe9b6d7fddd39837f039c97c063f417ad63489f4c18417ff1cabbd94327289e04630ff8a8d044824bf91760e8d2463c819255233e203c8a3a246bec65f070fa274d2e9bf6b0dcac25a7bbcd9f108118ecdae251fb5aba1f1e2f9264601df587630864626535e019a1b2e3753674ba45533fe20cef3ef4ccf322c97892fa70adc3f84883db8906303eee8867ca9bb551542f46330080c3ea79d16d402b0af00a0bb638f38239214fcf16b03e27f466c3bd06e9c938a7c389daa265cc4f6510172a238cc69fc073ad3b5d8f45bd810558ea2b4a89840787d4dc1b693bc8308a6dd36edd8722e9b095d8fc8eaf1333f87c83489aac418100670aa741e778c5780076b75027f22170de37c92a44daa2ea99e725d5a8d8f8668801cf79070a6c8d7f4fa0fbb9db2c2249361bfca9fce4d2fe3decf4de3fd18d36a9956eb1dbd1adcfc8380328831441558c0e806a346c308b95a1f8cce97e7def9014b84288cf9025e33b33cfd86f35fbaf15236318b4690d0ea18d8eda3fc189da4a4bf3268847959d95bc1cb626bf675fc6918186ff5e18a4b502c197182c1be6a98380ae85836b31c3ac39214efbc0c7bb76878324ab226397caa295a124ea343bac839ff0de7f941075abbd8395c1402ea348d8f3ae3e29be3a51208d9fbcd05b71a94001dd0e6bda40347cee9f78ca008eba8a28f5459624ef3a3b7e4fdd6ef31ea9b8ca989032965dd6ba25004efbd4b577d4264cdd1e81336d377d8759ce70610aa6d9e358c283dd8bab58ea3898b233de7d5e538985d8a96c011fbdf7088232235d158bb69a2bf455bba7627736988f4b02c989ea0c6ecc85281e96b387084c5b601832f97fb0ecfa221dc1d12fcb8121624dd70e183f1e8222efb087cd4129d3cc210fdf950e985833b820b856808b8f0f462b380e140092b24aa5911a3b1459a62483003fc550d86e474450937558e0e774881e4a3fa052e574ec376452860e50697df94d7929a83c28044279bb9e2deb65216c79ec95556bd46c3936370e5da667a5bd45d6c1e8faac359d25261ba0f3d4bff2c43b5575c396ec078a09b5889b30ddcfb5ac053ade08417be55911f1aa9d977504ffc48d998e56f579a98fe68a5d728eb0e0ef46f3f0c4f44ede9ef6e2034feec97df205517f6343e322b3bf70e0eae5027080178ad92c785cab279bbd8da76386e8e30998237f4b2c71c132d5c76513a0e455474b1add040a79b3c2e912cfd314baaadf5f85a1b1d6ef3822e023d44656ed5146f42a2ca2c06f56e67db21dbb40203802e3e9051450b334ac6c01e3d910040cbb9765a88a6b65f2afa62b278ee30ab89c7d5ecce0f2f88f26f43cff612ab9bdd2868019089242a3f3359d39dc635666c7200af6a34c9fbf4d7689e7dda1dff2ca14a443d4e8c80495aa171559f0a2247540cc806c9ee0ca6565806fcec53847f0874587c9aaf42fac566e75a73ab31e8ef040203db63bca5f1e2e33af6f7b216e24eeff0b69fd680c1c06fdb67ff03f758da242b4792584969dbc95e22a8a3eef3192fa996ea366744fd53211a640c91d642973952cf728cb177a075dfca3b081359e787948e67e971ac7f1428e5321fa55e0b6203608595054e2d0988e46815464c557619f31f56f885754ffd5a732aed0959228884c7c7cd6e0cb5c27039ad4dda6b2c2f9c5931ab21ee8d731936585776a3495515b1c0ebe09d6074e303e7c060ad9dbbc56c46917f9bcddec884f10ebe20b7eb716574b057dce89d4564b799694bca8de3fb91d56b3f622a14dd405f2ba2d8c195f3d2acb3482befd72399b67b3325a9212a38e39f817f8db45c0acac8f31fa06491cf21a899d15e71ce3cb82d44e8c975edb009f62ee87ab325fff6fdabe2f62583ac64938e11127ca8e8ad02217782f25ea9ac46d3155717154bcee8834072c45d6e6afa6e8a9877961fc81dcccd2126dc2c027b8ec3ef8f3f7846a794e0ae16df74acce7d8d5a23562b283a06d104d9b9ac3011601ed9c903a9da09c1b0fd7e073468710b767ea8860b17a7a112a1c47ac5566ea794aa1217b8320bd520c8595cb9dc07e561da0de21aa3fe3b3916eea35c4f97971a4c92ab78c0360ffc88ad2fe7c35425dbf4975edb46e38edce40a177624fa745ff82236962b1c2a62b34b3a94010e8176ec1f225730a6a1da17be8cf207836b5f6204e74f2b7a556535769fa45f42334d7f1c9068c3ebe6594ee4b29949a4288cb3ab8e018c1f0f188d20e256a25347bf727eb93edaffbbbb1d274daadfb3dea36d685e955c566423a8e490fe533909fc959bacd28067076dc0d1d4bd51d858102a4f7249a011aa59036f51542f2dcb2cc992f0cbbe8db1156b24459085ecc8e197144c1d22c670ec9ab5de8c101c3ec38e91dd314c4d13d3aaa027b5bdfdf798129247db44133a00fdb39c33da09721dea84a51a73dbf7023d466c71b34baf8f3999b2797da49520102b1392ffd07d1b0385325f80a80958b862f0baeca4b9332544994e5718493376fc516235091788aac9d0c63921d4ed66ec377294d24141a819e531199ce2af3e108e8d4e8c9f7e3b24efdca84aaaf7a97442b304625ad21c2d69d5b530ce67b8eae74b07d52e3de5b3b1aad640477a84401e7d149861978fcfa41bba4a54403b0ad20537b8c2f429b64dedcd109fd5714c887738b73bfe5517500f34307a970b437cc0376e392a2b64f4ffddf6d80228dcb645d3ebad24d8d017c305dc664b5ac32ee3f851f8d1afe453d1feed6afe92fa9be6f3691998249660bf621bf19a899334699b4ee66cfb1cbbeac81d388e8a9d82e6b6d010924a2eb0ef81e18ae8ecd9c4a5783cb1cb685afa6c0b63f53756c634a1bbf8e22125560c5276feb4be49836be3e73303469f3826d4ab952aad1ca98da5f40658571e0f5cb46363b5d48a53221a3e973f8a82e02465724dce322175f5daf49a8e516016a76a82294474407569491b12933deb2ce8f9e72c28c7b72c6517c07b43a5d14e24604e1901514be1c93c70c955c7752d1ef6da8fe830f204b535cefbd2a55809b45b1fe674b82401a913241b03aa0ceb681b1f39fd3ed8d47425f8628186a27d5ed051bcca574b89b3dc913b8a6081bdd47763dcca7e485318d3a8db438a863e9f5862c68d3011a87251e6028b7429ebad70bc6572c7825d70bab3bfc870bc269f002f04a54b09b4b00b9a0ba724d0dc684c4bde28d2c617710ec5834a2002830bd7a748c508e9689d344677c1bc0e24707be06cff2ea444d586da03f399ed6d44cbaef61344529253d1c85558b36f4ff1c76265d697083890956f954e2dc7a357581f6fc619e23fc7e70bdcf922edccdf644f1b7f3b04ff246f49eec9287b547d1d72c700a2dcc568b0b58e9378524498065aa3f907e38034183a5c5c80d79b232848e3677946013b7ad138f6715c4b87ed223022bc6b826078acd90e5518830d348f0b1495268c99fb3eb22dccf038b168e481b166605534a8a17568ebe5da811991fbb20dcb9d9ac79bf6c4553537a3e26778d0e8420d17a63fb3307eff02461c0d0e57f06789771c0d35dc85dea60421774de85cad09b14b7cd6d285ab6c0d65ec4d28eda57399d5efb98bbf6d538623b7725b4cc08dd9a7254b8075059d1a1b531823f82682b144e06893af36415a03887c9a7c9438b27bed28d223337a2291917bc4c3939c3da7962469d6aefa43fcb3c47928e03191a33e1b5ecaa1d971d38b32874267d64bdc68d4d3bd120159dc654e08448cdcba22f7b671c2c046dc86a827c897234ef927e48c385c0ab9cc83a1fc529e1bc0526bdcc090eface6fccd645fa5850a6c5d2a47fcbe1204eb6848de35b9d85457b7c606cbffc455d68aee2f7a79cf083ef07b8acc4fce9c648abba8c7ff24892c0d39c9df4b2e4537aff5ea1cfac92b048510d10a5717e44fa8edcbf6098a12cd3118c2e46e8e5359c17e1b224ace4f989079622c6b68afc593299618248691fd12f047959936c2818b92e0956aa22b4128f65af31addda3b95368ca1d234b8d9a270c4db22be8b26d4c1b527a0d2512105c47a6cee85d87cbc90135a77895e45e42b61f1aa6496c36464ee75a75326642daa619b86b1e375886f95c1897ba81b98288a10cc844f1cdbb8a159cb4c3cce6904e7f6e866b508519ff2e2842c3c9fce59342ab12b4b9671e643dea2158d69a647edb9718e5bf8a68add8cb4d211cd7a44e03b9d54a72bd0d24fe7ff5854c9de4e7440c020bd600f8c5a131dcbad94c198e36b31a7c9214676560ba7cf3ee38286b2d9efed22011d2cae12aabc872d1544e8d59a5fea556e876f56a87f1b29490c3d94e2a7ad8d0cee7d3e418dcb9cdf5e9abeb795bcbb406bcb575ade6d4452cee3c4a5b59f5ea197b14e5d7db6217f0aba05de90f61309c7eea664bd7431323e3a3c0c9c833721e6574078778ca4e2859beeecec7f3efcbd3c8cebfe63709209108a152e43864977055c137237203ba31dedde490a4886d98e58e5087de32df7452343ba698e234fd51d9ac5d3fd97420841c77c8e651cbdfffbbff12153a0d1b6c8ed428e54c48898f1f5561159f2ec3665c65d6ee236481f07e2cd0d52dffd3b06ddce52381430d1801780646b670d2ad62ff4bbc922407c587a8e832bda0ffcdc7fb9a966b787566e057a828a229aed416ca7dc4d63a992956241cfaf5eb8a5ec2c0d618fcaa93f06c58ba3f012060958c8c19e943b6c76eefe9a0fe0757dc702b549e3666505e55eff45035dcb518a7d2ef2db0bbacb41839090c584f37163f63a9d4dad6a1fe6d7e24e7f77f88f8415322616af45e7aad66d0f4f9ef04afb9ffd7d06f376f6d0739d5eb38102ae3360585003b7ddcccdd43e8322b23e84266572941a12dffb031c8965e3d77e8fc562c8cd7794e5a7e7e5834568f6099af311d3cea20a77fea2b68112bc3cb2a307f790800a79cd2eb023ef3769acb9cffa2f8c07c2e552a349245d8c197b1b13fea840c704e09806b753086b44f8c27005421134b07f368663f5184f2d0a0da2e9ad2c9ecb2e4a9620177193f9fb3d556f1f80ea245588cca3cbb3c39be7206d5e771dba069d5d4b9c020b2652b65ca1dc725b2aec64a02c55e86752555262e1850def3e5f4f48e9ba2b2cd4fe5fcf3d7d7e74d89324e607275b8b458f2857566c289079dfe955d8d510fee58e1ebeb8b95e3135aacf4c218e2c48fe39eb17edcfe27502d637c0bc0b3efd5502a386aa22f6b1c4d9f8293e33c4fafa8aa25658110d99c73d072b36cbe7014ec6f790ba1ff5383d9984eaeb8bf888cb413bb60b93f32a4b3a9d1c00b461459bd2d8c600c89b28767f42e70e2cee28faa8420317cc0d1707c81504c1ecbda0fb4efb1b3f1e0e540dfa22658bd8fef7b36e93833a9cd37c1cf30007c0090e7a1d9904448fea0c31d8f4b7223c74db313ccc20fdea2b044ab4e06905413815f4075c3fbfd156dc0205bc5408edd8781b7e59b640a0f001302864d22a165aee5962d95923be325c9c8b81f8848ffe682511b1503ee23c0acb1c61a560bc8f98434093080782aaa3740a98289d0b756a71349b08065273f5736a063f0e443ea500920aa7f97ea002f788ff8bbcb22eb55e35d3de272d878ce57f5999b8156692c7ca7d5231f105c3b059c989a498816f5d4025d5506846799369db5432d0eae4180213b967b0bf4916e756477e561e273a217246c8e36bbf909d08b18b76b071f8f03bf5e95af22709dc8469c2247d60660131a181ad3cadf86582e1d4fb0bc06bc7e97ca7193932bdfb3791ece5b0c65b67abf41cddef27cc52c1bf8b8cc51b1be122ee064fad6b47803d82d0ca58090758f6972809e9b8e7e33d77d439a9bbfbba80fdc4bef34dd746a157be3020507629b2ee53d54608d015d219f89c73f5efe9bc75d17cfe52cb3977753465378239aa8a695f76de941eeecaf770c3446c53bc1944dc93400849f486d4fcbaae557db61055c8d17b303a92cbb57a3bb35d3bec56e1b5488c597ea4a831c90a6e3302a85470e465d3575e43abe6f81e88e4b8c1236f252d2daafd903d8ba34b6840514488f173ea05123b722d6f09c87d43b99845f1ccc54b95d96bd2b971b6b8d36de5f6aadf0ac37bc65ed2f922e503c80355a94e3bbdbd19bdb534ce23ef5ec3adff996fc90fdce9774ae12c3d80e8501f78bf9d60ba549fe6a0bd16de39698b379ada04589388c0c2622fdec135421dde7431db4509aeb75f6b2c0546be07221ac68fdc6cebdc8e8ef8b536271131b54ea6e1a7297865629b9ed6e5a29c1a40cccaa6094bbf25888c748c0a246f2996c20ee4a893621bb7f9d4abacbf47d3d1da1575667f034cbdd7cb46d68bf0327c030701ce3bb9e28d88d308e492223847ddc2627f9aae4d554edc2a7570644badf5d63270c43ee2e6fb79caffefa538cef85215ed5c25396af56ebaae4aff50f3820c0dcfe55d89eb30ad4f86dbe9aac986ea433e384ff09f99a6efe9c1c773093d9c2016657e131b63e41b08edda36516e760d19979c81d6366467ae1881b9602bfc0e7e676d636a5cecd6cdc6134a983fa38fa18dd36130422cfc2263f10eb225ba0ead9f0dc5b6465ebb81c7bf75f2cdf6c93ad0b1819299edc2dc7779e20839ce0cd5d30b421852ea1e2a60a32f56792e8ace7c1c945f373bb3b14bfbfbec256289060424aa04111d4d608e89d9690465c0dcb8ec69294f9c8ad18099df8c6efa8a829abd165a286bca8fa7600bade4928a17e5bc026c604eee751ea2a0c602aef6f2ecba32f5a3a380123d7dc237d6f1718b67627271f0417cef056d9f288f22941ad8c54779a5f9f93f10cf7b1a45405f47973155fd8e6ae17e37adeb2b13458420d9adae8bab0b79ace74ddb91f09d9f7da6e422888437d6552cdcb474857fbb37c47a80fadd10ebf4d1823b1306038fb27eb10dc5b2ced3422d0fc65a67ba3989c8f50bd182b0b638637510e3cd3ffe41b7890a76e451d2f77fcbc15b24bf0f237fd85c7dd155acfd85416aa71f6935b77f3574e582c68454c7f3f18d387a0c4161bd4d6aedb9d365b711b6a357aa658ca5a8e1679567ff75600c7d6f7dc8a119f6912b0dec12799fed4141ec5304790190332486f0e275502482dfa308c1081729ab8614c37fb7d7648c70f6df89ce2fc0f1802980ddf430d635312d60ac78a4a64ce62a7fa0790aa1ba2a9e2405c78de096d8461bd263f95d8af706326f266a735a8e79ee006cf261664dc918d2bc907b8c524d2c10b19a811f61800bb41d9977cac552e90394cb7b7351d2b392bc9ca1f7943b46b71cd94c617bcc6e20b0b46c1f1b51f89cbb5a944abea5725d33509faa0c607bac515b9b1b293507b7f3243325a9c3c4ee23b93168833f6107fdd140c3780e69027e203fb276c2f88bb63152fc09ba7228b3c6a6d0c3a4abe87ab5921d49e34d6d8cdd0cecb49906bac80700df33552e52d935b9cd03dce8dffc07f76a70e800e8ec8f1134b0702229a2a5bae12eceab22f49646e2f215aaedff5ab7b15934d31e5dcf39333980322ac0d26ac3cdc5da57f1f3f2b07ee7f718eafedfc5e5f4f069f98100a89a31a30739525e89d65cc6bc4140410c0ccb339dbf25e657a44971e0927b680c5c938771a72caa4e05c9699acb4c9d6bba941779d0c4f56f1356b73f1f05e88dd8618ecf6f061870196607c2b9fd108a77dd4e70bd0dd5e1efcd3b78e41c1c9133491a29845e98cad25cb78ef9ed196d02f3b01a02e7f03cd5d69931dee3961e606220b73f4dfa6743eaff79df0ee5c114b2bbf945430b6805a88ec8adcb41a1fa3955d32e4bea2d8b1d15c14e51dc49e71a0c3dafb10bcdcea1f74aab83b9993385a2cabac8f4f400b5453ecbeeeb48aa0119a25d487a1cd56d8d1b73e15fa037d1838d6e33c17d9fe2876216e73078aa1ac622e76a30c20dc4f043a310d5c79c0cd547a6dc189115ec1f9a21012acf8dabff99093ac2d93d52404a0c8674309fdcd7448a4c7cde5556b8d95b3f0016f3131f2a6495e1aab2b802a7d8001ce00e8d01f90d0ad6b4f0da9e8d598c63de0d92d60fe7c6844e8715cc378382d05bb1fb12eef4b01f680e8ab197478174224b7803b837b0df1c38003dcc45dddd4ce7bb925a6e879a075862d10137d7864ed1a6a938a9fe8cf9800200c199ca40665ff97787743b1cb0499e01a6cce7bbb6fccfa1faef33e35cd1f6b988215469f01d0d1f3b68f3a06f204e5a98e7a75643803cfd48e542011e0f0e02e71256ed3d55b8df59e4a8ef9e0fbf577933e2b502b78aab7bef3359f3908bbbea7fbbb2fef23ca0fdbd7c63d705f76c0702e7c2ee33322ca4377834b48c129934b2d2a8afe99130039fcfedfa9604f258ac040cf4f0d62f34dc04c84bfc2a27664b9507ba53cb3ab63186af16d021a362255ff6e403a08b37b0d4b4179abe9b229f9eb30d013eb1e98a17145298921a96c9b7af67a1fa6df384cac7f32fef7915178106dc51c271d75ad3a2dc4259354eeee4b2986aa68aa3d2a451c596008c8b8c0c662b61b160bad49cc64e3668743008dcaf0fbf60b781bc58b05e04ca928adad51e1090297f7c7868e0af393a7de3cdb55186d792b451a99cb426967891fb9f16f4518545a5a9ea2c286435c8223e668d92c643b7fdec9fb460e71d48a3fb0a663f4b4b819a809547f6583ecba64274a3e7ac6cdc3ad0db1d3d1138464f7e67082c2be69cba7779f4366f9383cf93288fea81d002d578353a07063961c6096a0464bf8e9bcf716564f4c67ec6b9f9659530d083c62f736ec05deabe55cbaff8515f87fd70f7f1c7af5af9ace5dfb74443d851660d2734b2703bfc33b13a3aca1b5a4d6e47e941c14680eca78908de0d988b314518e6cb2911ba9a92683756c56b702d95b426299627dd9e5f0798719d664661de107bf3392242c222b67382a4472637c52f8d48c29e73d38a7e6455e166d23700f685a6d4cbdda3599589d9d6ff3685211897f69a17b215e05527b7a1769712c4ccf158b4ef6c5e63e049b3f08405b436f114518f04769545454207778340ab9b35a57fe03d9900b18bbb92ec3ad920d9aad6914812854eb8fcb6cef15326a59757ab19ba4bb2ba5d42906fe0ad7ec8329d1c3845b91ad64356d49bb6baefc4d5b6e8b23fb079d8273488f3eb5dcbe3b28ef0dde08d711dc99e08f2494657b5e3078a0b4a34912a8dfb4aeace278e9c7bf2e26e977677449c801da75dddb61095418f88b5140fba881ddf3553cd29799d7581f189545bd299f3942ca4a31fd54ee91adf2b1bc18c8ef9689274d10d9826e6387d5906526491ca0958d77ee67528cf6f020352838ca56ba80e7384fedd67fe793c4a5daf8eff024f8680160c5de2fda9e7bd3a8da3507c59b73208a70d63734f4e08f05aeaf9a5d0ddac36e932fc59001bbb914e831ad63942bb6f8d82491e1003cd9828683af2ec77e404f095e904b81775f762e771d9d8f5d5997b755d7e1773620a28f24f350cc0d36f2113e60609d58899570f12b814394aefef61854690de8d4a9ee8a28ea42f3f9dab7090019e49e6c8b9c13b93a2569d5ecaffbca74006e119d1b1937b6716a0fec4ee0a930bb5379edb238efe0a75705f5c9bd2342375811e212a884c17b1d5081db0b5e282f6fabb3e76088810aa5f424b51103b5dcf2c3e72fb6ebdab4a851b6c6c6ed0a7859e6c5a4521cdb8c19463583a5381a4c5bfb184164fc7b006786178c0d49480b044f5264b4506ae8533ff91a8563ecf20c5e203cbced9437da2c10d4f047734166402e39e9b90394e353276ef21270040c71830af37c5e8eebaf8c904cbd130cb7b7406a74dc2e5111d9e82ec532acbef1ef8a149030a77085e30ed429a4a9e17d8a760c57e552df67ca19e0e7570826814366641cf9169f150bbd267c2074c854a2f7fd0881398c9747803bc7bb0b818f520fc2194c92eb6da6e0c01dcffe3e3325f532bbb7fb6a5d405ab4ee33fc4056cf9c8e224f01b19d398aa19a87cc7d51611bf4c5342de0a42aa441c207e74e57d1ccbc543f4125e5eb59f2a62ded09dcace1e821c8125ef103841b4c4734bcd55d3b8582327e1dd952a663238ca0eee9f09d469217cb268b9c2f558a5b97aa4cf64fa9079b5320b66c507345928326d6995c394f0bfe80036b63cba10447ac082e261f4b193fcb8ae53468570f84e95040caeadcdd87bf4e63a95d41b60bf7b3e3041e80d757e89c6c1b07ec22cc2f1072c1644530dec25d81bbd79beaffad4c61f493e2454880493b1c9c6318da3568e3a2023f831ab779ae80de80216bdc760bb9545c863fab7f49b2907fa6b0e43221afb657c906be3162961ecbe0906503d55226b90dde91e1085a75e0ca65e306e37e26fa6f50e2dcf9a7da8c93803a373e0090423550980e19c081b50dc9b077efb4a61439ba7aa4111045d973b2a84b8190a2fa1b869a52f29b07363e06248f9dd983b9379ac39696020d7ea2da842aceaa65aacba1ed69cdf05fd56ce9b5be8c8ed1e0c4f726ca0e787c598c0d5d3b0246467456788cb4e63a64246c97d4f91aa3ba77964fe08d07497eff1122ff0cb99fb75f8d4dd7ed9ee28d5691da68c33b2620ea0aab49c6b7cf7837a4092a03e52cdd78c6e56d9c726347c6bf3f6822032561f8d68e6dfa415a533b1ce4788acfb16d00e3f53e76bbb5b1018c4fcadfb5e90a6742543269360971778c7562b23267d3e097dd361328534eedd6bd494369c3c46c30465e9fdf88381a5c1c1c594306546ee3555d19631b83758eb42c7f750adda85de091f28bf5b1de9ffad8e4feecebf37c0e038336f2267195a54f886689a0e06bc5d5963f4c05d8c2313c950c6e6d316e47d7d9f50cd3ebe579a9e1b0cb4b3a43c98b2e3db2adfc53e571a90e05d629a7b8ffbd6eaf24eddeccc2fd5c21f2514a0bda3b5cdd77ed4e039fb7c3740d7371ef9ce22e8810ffa757bd3970866a5e492c770ee271cca936974454e7410fc3278757c05fef1548257e4cf9ce1679a9f5fa92eb7728fd6ba67c04330f2d6443fddbaca87bb2920a8fc7910698c755337ba0ef002b5ceb3f3343694097353fdeefb2f0189c8093712198e9e27be23e7b2b51cf8b983c80f9e222c615f9b23d60a4dfcc7ffdf675e35d3f0cc78c9737202bb945b3903c8f554381620c2341d4ba5d471d92c6ffc0872a7c9d78f4776aea431b0c8b84a53a639a93f99ca5e64e5aec73a3eb631a7e11edaf085a236a4e3b3b80b3ecff27bd0a0767306d755e022774c5492684b5ce96c99c8316dc10b82749758ddd46f19b3ac9edd1f6a8c238b2c0cb4e3acf5766b12295681795c5a61e6efd4e2f1ad33639a640ed237c08219d9809a6a78d836f99f0e78b7a751c25b32522d58d0133882fbd5f3d842ded1ebc61f5ce08169efedf36ad50fd9760765f68283e1c8f734f6e604199eecaf306151c8af4693d974583171b5fb6fa59e0f4b193a1f02193fda53eaa6728b84735d92494cf4139e4c31ed77bf1551d9f5450757e3f14fb094dcd547abf76395344bf31836c0d942a993229cc571f407db22059393ea3feeb9c7b390e9608a1a058002efaaf81279854fcc67b6e80482d858aebcbbefae0e9c13b8c30ec2c90845c432fd338f67d5e07482b8578538fd235469935e75640c21bc864075b124048becd1394252ebce75c72569e4d72f56a2f2be0f6b21ce6490d20aeaa5b61de59891cc7750d70d1967227ff434793d8c69eb75499dbe70446eb36064c3d11571972aebeda699cecb1d8b44717fed235d40c6950d4d30fbe20aa3d769b4045d47f1cc9c72df90bdf8acc2b7204c4cee4a65d3fa7c9135a78d1cb93516c7e497dfaf06a17c13bce9dd5f5e0fc79859cdeba7d38c6f9ae2e360c4931325a30d3796d3c0f5a455fdf9ec7a834f4fc51a27cfbdbd9fcf0207ae67465181ee59ad26f40aa370e07204b79a90982a037fe35b2ebb878fb3f524b3b237e58834f9d8d168b132797e467e79648c662850cbaab88a526d0bd59e2f29cf59d73cf21497388215d1587f9c6163661979889b83c220d0ce5558e938de8000a61ecf2916c0e44822f25a94c3a634957702c232a28faf4da57ce2264b1ece05a78abba66063faa2501f4cdca038450954474a98105d5cd287d8af5cfefbaa93d7af4520a7a75c42948944bba00161255afd2014df2759fd967cee133dffd200150066d41cb02629a86370499782ac20b35a8ded7b6351b6940a0d7bd4419b5ef57fc42b2fc5c1cad024da04248e7486b0feace62ee969e2ad57c0e31e48dac30e5f2a3662841f2075eab60c10c273c2f187c53f64e7a265eac53603b914522f39b8d29ecfbf09a0d5a3a591d5e87d0185a35ecfbb6d6b5594426bdffe8fa56c93c4a932eeddf4c260b34a78c53833961784dcb5fdabf670d9a590624b66129e49a2a6800ebe02c7314ed71f03bbb5f76f0484ff36ee405b8be8df712abd9e954ca139e32e8788cc405b29c8bd88ea994d49bfe0d888b691e3293c79527e3040760f302b15fd9b8c36a91f45764694315cdc97c2e2eb28772737576b6c9c77b06f359158d758bbc875981fcb1a534d95bf4164c38bd605df893c8dda4e88635a0a6d3fdedd46933fe771c207f26da0da851d1fbcc6d21ebb22bbcc4d36742addcfdbc3e70d5e8fbfb8028445401c96d0752ce9e417942f483a6d272eb99a49ba4e48dab67dec1ee7cc7b1e3199c092c64b3ff8e329e821f77e75c2376f1c99accce9b7c46d871be8a32a57e10ff71301a6cbbc2446a71566c556a80b4fa91ef5d0c6722664fc63e0b48fe09809e8a7077d2bdf5b656d08eb4d2956229870fe494c0c9f0a36c7b9f639279fac8c05ab99e610c87c9797aff1e413df40e8303e0db203a64711a1f42eadb092410e8ec983ba148a9929d4e21f973e2994d97cee96f3085a49c97fd36a5f56d80f8c0be119ff63ccbc23c5663b28e94d7458a970d9fd322bd7f29e31a49b991aca41e756c1fbb732a0596a88056a5cb9002d64ebbb1e3987232d0c0ce5e6b3c212dcea6934343cbe093f0aed4fa1e84e27a75bec70ba510d3989e54b9b71e94d814d1d7413454ca8249c72d7f4b50718410bb4188a68dd077d9c77262f8dbe6e5779071b80d81d971eaddcba3372ffd1a58d2efc8ea9ee1802517d1033a23d4895d72ec4179c338fb0a9a428de7ac9e39b70197f865ec76f2f487db281ef8e8a221bfdc132817aa3fb32082cd76cb75a7f07cd0961fbcb13e0f8d09f751930bf4c0dafc84a577adc2cd778d0f3e16069f5215a5dce10a217abf70114ab342b49fd4482746524669583afe139d9e7820b366bf10a60ad60bea7e3f97c444f3450465548c38af5ca3025500348e81875265ac55dc1924855193ddc9c85de688a81f1a24f94ba23692185a1ae91a32a04a27495807cdd147079ba21e474bb17017f5290e5ddd526c39c4aab1eb91fc2a203852d063e5a34402c331f4b8fa03635a28738a598d28f25ed3e3df4471916347a0c03fdd75bbe5cc1b69e3f16c53a388d3b7a413b82dc6c3906fccd40dcca2abdce7124cb3a941bcd05a69747ba5d0b06dcd625f99e5f30a3eedd682dc9a72cef15a69f01833b100df4c01acaa48c134e930ca78f6cb3b5072829d51016708bffc50282c8c2bf340f67c4fd8f7abc1c590b3fc93cdd034c6aef447a9d5ed861ee9a89f28f6b1c48d6c82f6ff39bbbd3d0c00aeb03be07b5402d4c08f37d070ab8b509b27bcb302e86d6e845cb45e55b3d98e607528bc921049dc9292a3fcc9f5de796aee54a4e62ba21b1cd374745db1fb5a87b5a873abf4b1ccd70f7ae7d98e3b314fb06b18bfef55d2dcd14b618894c02df6f6710c6e1080b5c8f682beb47926d6c33f577ce53dfcfd0509689b2eaa1734626ad75dbcd0d361dc0b81b3f6b9cdfac939e039c34810207191492dca23b48efc6224191471e9dcd1e8763a39243d70fd8876b25565073e18003335503b03a7a7e9999da12bab4c10c424856847871306a32c60743a659a80b8b493a3ba2e1590bbb77f30204a2c1cefbf6d492739859e4bdd7009df30826de8a498473015d534a1484d1dc5515f271ad330d8bc8a1d3322ca5bcf26ffbe39a3339ed3247ed8c153fdfec163f7de44069f567c17b3a5f1591220d7cc5b9eedca7adb07085e20ee1cb170bb85673467ad832d436a5f959f2a834fa48df2cd1e8af79a412cd1f2447dc901df03fd40e1df674213cb27a004b806cb79ef52501effb7a51cfb74909f476a5d4166e15227e37595e4b4e41a2ede7b415dc5defe67ad2384228d1840fd377a2ce92c8aa2ae0cdc6e1244123189993ed6b31946c214db7691ea6dcede21379994a6de001e4ca0df8dfea26cfca3b590ec2ed3037ea2c725f9d106f8346e266dbcb2113125a7f45e57bb1d5691ff3367923a66a4cd70e0239f1af7e0ad636c28e38b1d107ae1470e440c6feac0c1d59d28664716a3d2b48f73cd7cdfe2697c2ae6ddfe1442df54d8c00b9f7c86798224dd4b3d322bf431eeba18f9d3d2137ace075668b48e1b5c1109abbc688090a22d04d1661b38b2be3d42c30854ba65a3b55aa21b33e93abebdcc330e6a5ee538e0ba9b89b8d041e28c73fb80560da6ae6bfc5b7f4d1ac83d3c7f056c88acb63063f5b0f64f6e6c4ec56f3e95b1c3f56b50ede8962185bac08c1739f2f730222e236f04fec964deb7b2f69d89430aae23ad5829860e25520ee4ede56f75eee75e2f6f767e7b321e2d0cbcddde9d86442ede20a782582793302a7ce4dcb55a52f6778d6f32a0f96fed15a9aeaa73549ef129aa80de41105b175ffdad15d6e85c82f168806e5c87ce87baf49eec6e33fb89b10218703305e30edc283bb149fce2a35c1f160e094c85c0001ff68c4cbe9dca02270e5b0eed49d8951f0c8adb9393a91b2430d60bd08f4f61815c37f6c5975da097df8ae69dee2106a9276c2da138ae9ed203d15e2b8eca7bb0c359cc2b91c6456a2cbec3319534af349ae291ab2004745981d89dcfca1f714e9b3a22a60629e609a49e1cb49917ec6cebc4132c3caa3bfef1ba8f11093ea08aa36f56605a58c7b5def94a290b3ca4748a708e56cb9907e9013cc3bce3c75407a6ba827d5fe62274df2e1adcc3eaf1e709bb106b39e4afbf440811576ccbb6d3e41bc5c0b440aeda6d122924c98a0ef92ea7246593086e861c259322b5eb436cb0d641594e186601eca6b3259a38f4147a18741adc18892e96c9a2ea5ba97740c27c4bdb85d559de809ad86d2e1c9a53d7d42ed560b896fad647e576c42d2c5fa25460aeb64faa16ec075e148b4ce136a2384993d855e3a84bb32aa56870f0818d1ce1875c3e580c7f9fcbabfe73e8ef76662e303bcd6a0b52ef9d7da955ab392599a0d3ad2290bda9c8d44cdcfab8f0926a05761e58f1a1237efe590bbf1d58ca16916b5d967621408e4048285f1f2d6f3e65123a0d83e6ee401fa4a48216a2fa75a01cd2d468bde93bf12f6592ec760ac170cb4a0a6cbb0142cdbc57d5e89bffcbe80765f3cd3cd95220f62e85dd1d4bd2beb765657d310341bdca650b03032e4e9f62f325def15c2ca71362061b01ff821814fef28d3725e3eda0e8bd2165ad0b12cbe38d1092ee24a12f61d6be4eaed0feceaf15bcdf870c77a85acb5290afe8148c43b1460636cce4370515039823f8f5e32c3415c63290eb6b8b889920e765bf088e0180f4c16038688a2a96e2817e5d6d5e3be85673ead313e485b7ccfa655a5b599fcd46a7dd3a0fa4276bc2029dac8f10c037215987c6eb8122d6717f34d3cc7d49221953044d1e23feaf28424b7f10c87e74e7dd837ab0f58cf8a5c02ec6c9ecfc884a3903a5215fcd8a9fb5ae3dee6ad7d58747b297cbb7f170210c69f591f3c492157ad28187965a3c71025a7f8cb6148e3ddc0e8abb07bd181fb730e36171b3b9a2978c180754c0100dcfe43453475270b5997726fc6e33c68bea22c4172e6104b6dad3e89c3ccfafcd925ead1f95da65eae38d093c5c72310f6faab45c425285f0d3dda62ff48492e3b61165713de99cbcd52a2e515797a68c3bb7c41e6c6c95dccb95e9c4e5fc889fb00a5ee5df93d511f61ba38505394f1a9d955dd76a4bc543380d59aa6d766b271a6e57736c641fdc6920549d66875e7a71c51e9f898b0d3ea5f9d8c2b6739247534a197f85b421224a09bff968e0d613f9ba0d8e77901b84894cba23d26a69166cedb2376b41bc3bce95a794755e7fa10755c7071b03103b1f85d641d3f713e01b31d7e52a3a76667cc8fdd431b0357152085dbc9c0faafb973f33dc319768defebdf418c05d1b22943c32ae45846b8a1f762a6fffea4d95bd82962987f50d4091d16c00658ff8c79db31912b9ec08da338e18dfa536a73dfe2b2a1dac3c831ae7b936bba3333c2f349535d9b2232a18298b4e7b978a00c500eec98f0b788339a8c3408c9a0f77d90d484af0949dfe4a701a2387b93e9307d1d1a13e6da3d2e256a059623c9a945cf2e239fdb16af05e489b215b497b7eaf2ff6ed92cc091d5fa010a3d564b5260dcd2f6def6edaf50aa37e01178ec8fee4ded14044a5868978062dfadb0e8989b00bd36612cdd90bf0bdd4a22732f16ad69ee43e916ce587b697cf8f651010fb0411e4c9b8c6faf50d424d4c7f735f25b48f192221c7d8d3d61eab06be74b09687b6ad7b9bf97b7d9a1183aa4f0fe3d91a680fdeeae825c63a47c408875a7fa554a80e9c11d33b07066118ef64300d0c27f85d9bcb71e70f59d02e9e6bc77bba8cf9d4043de5ababb8d62c80e9ab99cfc1cc423afe98109f8e3555fed3e8ea45d03842062aba31ea284124aa9f70b67d10e488b0570db82093e5fa52fd46ff87d72a451aa8b6c8938bc4af99684e1b3f9f4dab51a6abf33b121b52585cec1ae6c610cc957878b0c38dc513f06f2ece449ede1b14bab94ed9488af194ec10f763a097c5c3671f57b299962d8cae931ec6918d849b59a2f00f3bfbbc418c5d67d5d4f46e7ec22507b278afeab6633db49c0aa649a8f6c4bc2de8e091b24dddfcb3e6b8e3580644fc7043cdf2c9f8321ab448a1a41eb30af05ac7641d92370c4c90177901d26ca5c30126cca3333055b005a3bf74e479f8bb797d533e4da4fdae451fa31ecbe10d3797d08f30b535ff02b25a7508fe282e226057aff0b2abc2a5e6d6dc799d29c7f84ef6fdf0e808734ccbb4822f21f2377f86ad6f54edbe73a748ae89c18c4cd150e168ebe5de7713c491836e9e11010a8161d5206a67013296420f0a8c28d557cb564e6196513123498a321b60d797722fe54bd078382184a19c95ebf2af7fe42c4d542435c8b4b8a2cd2496c01438ed6d61b2935ad06997c6233cf76e2973f3b900d3631475141ed7c89fdb04dcfb4381b383bacbc6f3c68ff2fa2844eadf212dd4039adece4b3d39f4ae3e15b01b31c0a66e5040719f0ac89e58c5514aa3beeb856edfb0464a7429d748d4c76196e9894412150e4bfadc5002100c0934ddcd0a835694bf9fde8525beed26b3b818b02a68844b2b8c247b18dca967c220e11fdebb91979b21972b8f02c9251b82e82ec95acc9096887198b4b6c3d9869d571683f050ec17326821db56a716664ce06e5e74ae7469f72b04bcf1699a66f93b84aec911208be931368617f310bb9385f1d8f06033e2b008062220e0fb9a8bc25de28fea97f12fc36f8b8834de5a4d46b4438ad8b3ad6f9eaf3bae8849c92e0c2065872b05f341f57c48727c355c2c2d984d974e6fd5e5835ad58ad1002bfb40bea4e9e5988b0dc645327dbdef712586eedd1d830472abdae5baa7f2d979cdb6361163a61bea66f82b84782e4b02850c13b4ff5883ff4b708f739472ed3c0db80c6c042996b1915c47d58c70ad789961c296b3534a811c426673ed3d0fdbc23fc43fee626e70fe9088097a12960def2789908a9654e5e3f8d15222e0f199bccd8ea282cd78177ddd970875f3efbdd0d075587ea578c31c52b93b65cd48453b033c11c4db7756bb342b2b210bbe6ee0ec1b9ddde9548c02ea89cd4c90ec0ff1c052d4434de673509b9bc0e26bc0bac256ab9c00e4e6795186a8afa363b78a15f8eeab00dd07fb1000a07fdee2233dc65c5956ef72f3779b7f049d876cf87277c339df9bd268c20c87c2e9136444335300649d851026985a7ad551e59d8b5e2bb1a3d4b5f8b6c7294d0514ad79b8766f830e6e44bdf83006c6e1834b1349e2c35904043f85a77297a6936c1bf93517db6235e6e539c61c34e606a867365daa3a2c2aa50b0c040a71304ecbab0085651e93b13d2e30c64071daaccde20a91746819e6b40dbe7c2f7f2aa546a51877f0d6843b5ca212520c94c4cad6985e833fc03d3c22ec25b844e434e130ef5120cdec88226b56cef06604567f332d11641b22344a9ba795c8103f7d30ea48b0eefca6bb62ffe63333f8a5acb7dd49ca3b7f959075bf725b89f62cc7d24dd2686faffdb4f12b887f1b729419ba7004582d19cd6d89dd81ff16a45fd05bf692209b99963347c7d99160524a4b98c1fc55f143f3f64ee35db43a91d9f314bd604915e315259eb3fba1d8e0731d317609307d50d29930b72ef7bfcb75b3da88f4a3f6eb4e887ccba0c3e05f4123ce4cb5c54547536f9cdce2eef34dbb1a121212684dd5c7ce5c9f70bc40d40093cee0a71f9dd761c01c61c01039872dbbb8a21ad7f3c2f15ac2df1639c5f09099a24e5b03e7488097f2582fb44bbb3b65e84506b5847f9f945f836bf2df1e862f9ba869f37360006a49ff2b8e2f2b4050f91931e2ab6ad6f06155efc04276641a696c39be83185a6162f8e7a68f0ceb0cd6813af32b1ab4414a5248239979771c3a6256c282e2a3b26bfc0e9380808199891fc3730761f7865cc2c8f5aef9992472acd280c148ca11b9635beab6befa8fed261857bf3736353c481a83207c6ad24a1b188595d6f19181a3575afd9592b461337f1ec5cfaa447938b7b2d30422ec17b822b3e73df0a8fd3b9827b04ea9035b2cbb7d83d15059ec0ff8d604009bb970bec0454ea1e32092c61b8b5fe0ae052178d2bb9cca52d7fd74365b3861b3e4b253a8350ba5bfbc3dc8c69f154033ffcdf7026b03ab45e4a112b48804db603bf811fddee7827a79190aef10074af25072551777da796be953491d0721eef1158d96bb4708c406cd782345f523f2053b84acb07d4dfb04dc624d4a1792929dd0472e61335fe7f7c7a7d2bb316e8a81aaf1ded29acb8cf15ef4f97d234c9675a2f326e0c4b4eda45d9e5b3039426ae918ff8d31a4cde67ab0bd34341bca754bdb08b52857a5dee90ba242d74f02773edb80a5de457e20e12a5b2e7b8507052b8d2a5ed90c5067359a90ae7a37f08b9bb6bc4127eccd93f82687efe911d3fff77929897c39390b22020325cad24bf0e135398faf4aba6cbfc6a3c3f2cc4755f3c4c3606d6b9da746ca5f67f55c061bc33ed93f595d167bc43e5676e6d647e07fb9d47068457207d8488eb8807efb53a8ea35306f335e194d80062679e552091a19d81f7efedc15d01a8a0a2f85b1fa9f497e8155388bb1849f0bd1047442f0d669e267c30f4e693c429569404b4a10027fad50ece3dde58e5a893b68bdeeb079d1e4608ecc7dc4dd372955f959e143622f2ddb6f1ad33887c055f04239ce5614a4ec9d3bace0db1cb1482a8e4816d16ab7d91cce2fab543ba096b6769db0722d60878599e42eb21dd762978895d53a63ccf59f849259f9935f7bf0fbb7e7edf9536b329243b89107db5b5697d1e558679d1cf030c940604491851bc46c27442279fe598390b16d19bf7e302db89a229bbd49b0a888f08778a1cfd379f0db5df8bd71159c56f351aef00d69e814218ffe33a1c2d31c02a6ecefce42e7815afc1b07d3e7119213c34b92f51c375116e2a9b79f386c9dcefae3e7ed908050393dff3efaea0e1f9cbe8fb5023ef9f562c31bdf5c072a128d842e579dd91a2de258d4797a8324b917c33eebaec637f328f3381cdf89555c8d817d1c01b99fa6ffcc6c437087f059d2d3bb273fb10e69db81d8385971ae05ff45f82ee25e38a20e6dcaa41fc8d51a9a5110258cba5a535f0d018256df1cc2822fa05677de21d6579117b43c99bde87d0fbedbfb8869c7031aa97871fe2779dcdfd3bc22ed3dbcd41db62d7cf24fb530b2559142b6ca10961da23884d9e29768020f43dd250079e8848ab64a5af5f72db7a29b06758445de6189574409a527d2c99013f764223b5da11af200528ece765095dd64d4b0e10ad5c4e69b62d05ef85f9a1f58906b442a8accc0aa2421a9a3ebf42664303d015fb5664544de9a899771f5df64bdf0208b0e12424a5fc76ee881599c97b70301f67c950ad40c1227a03db237dfd36333c8f58b96189ef96717019b13a1ac21e5858c02c15c67d066831cba4352f0e8697a993f8c5767bd855869ad97c556503c7dcb762bc64ff378462671b0e982ff10e513018938eb35e1c737acc361917b3830da8d685e66155afef8f8c162b57f5a766e081ee8b0acd0bc3eeacdffde6331046ebbd8cc8698ea9849c97dbbfaad24fb77eac45a7acb065580d94278b784ecc594b62ea7aca0afd674be04ad32217791e834e88f7f2987cfc9bea5f692b20ebd771419037cdda3f99c40fcdb37756ab334eddba8371ab60ac96518ab4d8cec12ccc00737622a0a3d7c2e4f814f45d7f64e1c178a11469e6a3d57114c13c2108b4b891182cf2ac3f53c30236fd3e81daaf7a13107b6ad0dd547c4ccac7e7a579bf68b61de7e9505f643bd7054e7a6960cbca58e22a91f2a0a5a4471417fd9c9a4d41cc8e0830fd19c0a04b577904d6448ddcf24e46511d3461e6ca59b317276259f1eb01730e41a824706251724be1ecf859833af56f470ba2a9a89ce546f39d2ebc5031fb870e09d837f0e222f91fc88a06821167faf8f8e0caf6dcdada74b2edff305873d4d6d990cb471cd80cadb03d1a58f1ba56c6a126f519aa59fe87fdb5aa768e140f58691b80d89dedc118f3a8f960ca5198ec5d69b4c6387c53b1f1516acc5ef5820277f1e2b6012d6d9bd57d21d15908cc0e69cbd2f5dd157f909756c4d163222d5891bc2c03dc60949942ed66ade329446331e997f2e5da14d5df461abdfdbd91943ebd37b74523d8f3dba3961373e1d9b37fe225c745a7e57f89ee912a22f3e5b912b49e74f79626ef713a6bf893715c27aa2f1f942074a1799e924ea35e14110eba751d79888202e3d51884e4ba7f18c6427cb4597824820dfae0cb4e93edde8f3775560732db977a3f0cfaf7ca6f57d8637d2872fc0b94ff1ef18f4ea15e38677548a7ae15fee5937a406d16bc4640c2b2c0857267cba8a040ad0680e7f31c8409f2a32e87fbd8119e91dd5995e401e562baa6d1a76d6b2adf5c5a896c8775ce1725b26d89e8dfdca3dc4918b3c109f1c2808f8ce73c2b3030eaa28a8b4ef268d93efa17de522b731579c255f349f4d848e54ede9a3c292f49a5d1d0e12a12ad6376dfeee32317cdcb57da151c936bc4cb215bfa84142a9b01b4f9b1c22608a473d34e3e8d9715e54ab88ced80160a23dd857ef8c5a92d5bb1e7cd2cdcf5f481936761ad07a61b3ac51f8b3d0f473b8162ece3cf7439f3ae06c3de7218a8be441ce72222222e243dd050f3aa2ec78e0a878a233264890530adb42532c3615b5953c6ee1d01feb23cb25e302b52b008a24db07fa2b2c2a9b9e8b793090a0d45ccb664cbd79501a09fe41468d457136a91eac8ae798bb20a0eeff23e153fcadc349afadd97a64e377a35e8c41a53ffa88cc4c8f77b2068900f8d2f714a4ddf3a23d189e5ba131c5cba4cb3032e115d15248b634af4b55fc9feeb6f31770d7cec7f10f9257627b5eaddab9622354baf16966af68802823b12121ae5408fc12cace1ecbccf8d66beafc3acd720b5ed28c8c20391fbe5447ed769bd9e712dda1f2976d37fa3c31b8b0a4c4170e6f65c85fc296a616bf1ae4f0c5ed2aa687cecfc8d54b797e2400a0eb1f38ea3c821b3b88c4fb8bc0bf5c3f042443a4f0a781aa3078132e967b0f2eb5767404945be969ab2ec1b6eef93eeaeb4aab8a370a78cdf6c1631b5b0319d3e3413d74203e3c0b37a006e8a1369032002420b992b1e8ed7ffcf46dadd22e219eeb9d4a6b237afbdaaadb5349b444e46fd231256abdd4200ac4d9240748b0072922f6d030a8fb1e80b9ca49c9fbd36bd1ddd28f4ba42b6cfa6d11706557d6468e075aa3728e9c2fe257246634b2ca290c6c6bc156a276d736b2e0ebd835233e94fb58647a1bd93c7b6ed59c48807d3906dfcbc8fad471a8604a8039b2d107ab169718407007e82e6dd261804eb4f94ad03c5c5c37796f46eef7464a467743d2f6b47befd08b7fca7e6edbcca5402124d0fc35cd9b6601cf08236f0e346619489d668fdd38dadb4c35469fadf8904c14c4866c922eb5be2d0ae618fac0a946f4713da8d602d751cc0601c99927abc428677b00ab02632a70d98f7e00b746dfa5207f931442b9b3740686376985b842c6a445e0a4f7e1cfd905416a7b86f7efb88e4a527a4b3f23b0b3faf505f284ec1a508685e5a9b9ca70a5f1e48f3cde4aec2ec672cfee592a80f6f15255b5c4a875bbbbdb2f280d71568dc56b6b32b6cef891d3b9c125dcb81999a25ff21466786e98bd63d4432fba2332a7000215c062a2040b5980568bbf7ff132a97a722bbeb2006bc15bbda3855a5f310e21eee50daffbec98653fb5724b0014a37fb56d70c81a9f6bf28629562a63b74738538c0b581694638e585ca4d8eb47fc72e0f04850d7022dc1cf3530c0600dc9e1047f21cce9af03ba1eb26d697beadceeab4a9695b310375632b4cf58990acc481cfe18e6937ec7f228c62a7af8cfc7e835497cb53eb9adbd3e0984c8a43bfce76ced6aab5dde7c3656070e6bf3da6779c55d27fe7956ccac787ffab7fe0f325f8c1e70e4f76cd41e2d86aa4d2a4c80adba3ce0c48a745d7f3a14a72e952ed782fef80e819055fa56d884a4cefc4ec1cbb55549c51e0157a7da1d8be79fe2500143abe5ead0668e852bcbb806a1ea0b9376a1b626104dd38d54b8909c830f56e104aa6f68ba8b1bf1402f41f6d35d7863d1378197cbb02df554d0746724c8e615067d1bf43e8bda561025a3475d467bde378bb9c179e8d369bc8f96b9639d0aa0ac8e716f862a7a18e69ec42f68b8a23d9f5a65b70121832a77a66aa2e8a1b0311c728cb9a94a9c8a825d748054db86ec785769b2eba15b8c8f3b471384cdd6d47c9dfafb950d9a90d9f13ef6a41ea9ec533c50083c65725b911def6607044820a6bcaf8338900b6368fc82ac35962eb27e04c01cb4e7ad686b6332681bfe2f6f30b1c018b37507a136969951613c757e63e1ee3101f81a18f07e12b5d926547940c8b9ce56a543175466e9b6917817dacc0c95a01ebbf6bdeaa8af09b88e553de62631a7af7fe3af21957e6b9bd4589d5504b51223070f5654c131903a35e4184dc0fb257f146510cc8e0bc53467c8dfa929cc69a48a4cfa9142bc0b953da3d4d98b01d4c548ff3ed3bbdb5f14906cbe6af2dea4923e8f2871db3ff01c357c82390fb8d85f6bf63f1356602c53fed115a0e5970c73f1b5340a50d9fbe2733578ae6462967eeccf16313892f541ecfd9b42168580f490f37619599b55322f66fa60f8bddc682480384fba5902513b384296696da8696c7170bf4858cc457ed698f02f175805ec7b25cff776878bf17276d29b27411d8fc48ef95ce92979a7bc6cc341b17078bc26aab8fecdffd8153763662a722981050addaaf06940ddf41f8641101ff3fdb23a8b91557199484a38ae26b658897319a6b7bd63324228f021bf0343227434c22a75020162dda01721eaa1594f41a7e850d576aa4aa53d661855c21d5ce04d51ff1ab6d79b188400ce16e1dc5d4c7e879769f480dc57e02c37e915d4a660d51d625218da5f6e04ffca922629e81137878a90ebcc3f173f42876e37023a328b2ad6353610e1daeb0d2a04673c6e1cd4ab9e1eec9af4bbb07aa5655f9ca56bd5f94c119570f63f6f62be6ff9ae62adb00cb64a57c4b857455556a1a43839b4d39a2c63449b182f0605da5f8d30be6ec1398d766cb1ca284f85ab801882cc64c8b6a677528898c9f9d9ac91dcff4aa3233e68fb04f95bf9de8ed7ac1da5aacf1e7c2894926d16a8a8541420609af1f3a8c08e59241810239fae90f86ac580b426357a3cb25a57e9d9e7df11b250dc3d3a87a466d029c4e476b80602640036fc70bb8925ebc1a971ee671ecf3816d2916e57a643def003dc47f3fe62f0804d7705e340dda348283a68b62b86de6ecdf10cfa77277c789fb2c52eeb89cecab1a55949c319a1d605497c35584ad31067e0ffaea9495a46205b2264853070324c3e357c668e2b98299ba8edcdcba282630933770d4a03911c70cdeda0f77a0a53d5e4db436d33789ded49598262dcc552260070d9b25c0c9b2ebe2184126c53b5c025d22ec0ce82726e95980078806494978d4aecced487cccc177a7d589fb706e150dc74ca3f3f2619e37a772f768c75d888939c03a8c9a281da2f37b41b62ec352d7c028d978606e9a3e541ac292bef62e37550aaf4fcdbd3ba6b48e1d86f7d3a88bc4035b4243c14fdbe828728384c875d9cce07b0bb72a3a6cb07fbaead64b10d582c7bc54481bbdf34395c4651a20ceb46e55a2ff935e70c7881af48329943fa9f06bf2f7032995dc3eb1e9ce27ba557b7c3cd58711da9a9e6cd36d33990095ccfbe42606ecc3aaa65df17b7454a4943606dbaec5ba0401091f3e550ffeb3001497ec0acd12b98df6c50564c9554365432cec8e2ae12909bfb90b513da62e8d6d86201822b972aab2eaef7c102c0e91e5ce203d07d3d899de80d0ae4c9d1aa737c135b0085a46f7c6640be2938e21e3b463c45412251402ab9acdc923c49dbfa1d1839f7e8dd72a9ef016412d086e2c31ec41abbe77955ee13e366576ddb16e37aa54d51dc988f30516fd5301f681209b6bfb5a6e08617899dad922fec5539ded470b786c4c174937fc38e0cc1fa3c561845c859704ccb23f4acf389ad1f978356e9786f139ad742bf0a2c02774fc90de07cd86b25734ca95495be97014ef0feb9294d85b23d950de2c267e94c960ac76c250a0ecf111b8b4c5c2c08e58670694b2f3ed53c1e63e6508b8896bdc61d391c721dcb5cd53077015fe509e8e8f5b5b2f2b51f37debf19a51b52b55d9bb8eef1b75c319dfcbe00c4d34992edc5c83e11c401576adf04c46eb5959683facd7013e7ac16ce7e070b81bc077981c8eb6c3bd7944b8a311ed2de5360dfa541986f96dd34f847ced4c71b435faf746e1005c803169c7b5d9702099c4bcc387790ed9456e1326b16f332fd4d6183a5407c3b08299c34ffc405dc4b794d6183e41b13c9f6aea0c3301cdc37bfc0a39aaf9a2c1c1cabeb790689138f6772937475e1d15b7fcc547746835fcd119f8e4f8449e4273e9902440b5bd8259caa0c7ba0e40eb7dcd0255a4fab4e2a6fea34d1d8c33812603b1096d2668c380f64230fd1a26721c94fe8368d8902c16c04dc160f3ffe7ec3bbaf52c843e3b51af0d4d2d57e5114729ef6d00e9cd158266af228c434e1ab3ff0bd36f4efecd78d6615e386f09188907c6c48ec17374f2cd3a5db7cbef1a8934c3a7beaa4b0f4827d689e235392034c5d27861e8061919cf3bd06ab8ba5021dbbc7f06c1da0de6a47798c291711ce7c4218914f7368222ee992b56f5c2ac5e899e3542143d39fd49a5387636a7b799b189bfbcaddd699514562c47451b969fb0f70ef3208bff05dcd34037f1e82cb4a8e2b736fcabedac9a7f2a0fca59b9390495c459335c1566b92a07c9b4cf9cf918b33843b38c043421f9911a916fdcce609f8216892155efac16b17fec79c6e37d9f547a34da9424e0a02c4fc9285ca541b8c43ae903231d96a4f5f85c9002bb6115f5a8633f0437a10069dd55b322294ad84f9134356ec02a4b6c690677d7686325809aa84c3da8f41d25c1da74d3e4430f01a161213ae671927aefa9a0d7b14a02c6817768da12d214f9bb4e8ca8080a8295e454ab3093c9c126f1f8875084b20221c43b97c7ccda0836f862f23a3d8440768081e8a6366ece0aa92a4a007fab48114720c7c03a52e647a29988ac9b56ef4f0aa35f2a846e847d4603808a76d85f7f71f78e1516fc3d157a5557189fb601e7c295c5d03e86f58fe0127fc6a6814d1e78730206d2a9a4bfa819ea7b787b5d9e9c34b0e4d4b386cc34caa2f2613d1d8730686555ac12f1d8f124dd3a20ada5c59b19a3bfe1bfa70c5e8783abc07118f083e59c012ad341ec6df92819a2073dd2393fe8a1cc07153196278693e29f5f2a1268c7b587f4960c92723c329d8dc5a5969c53e92a6042d91742137756f9edf3dab067d84f73547948525633acef3a71e555fac98399e43a36a5dc82397441db3c3510057a4009d0d1b5977fd949120c0854699c6c0f98bea5fd25766285a44ba3aafd2d88950fdb336f302a25812f69d58fbc6cd4075225287315e7cad4e8f7d7f729b1a7add5c60c910902017d191c444c49e8d845fbaf58688ebaa31a729565dd7afbf3369f62a55bd7328a286da254391bb89f27372e991cd7595dd5402daa58287e456e57f8206365d0d861e5d9e6534bf447ba8faf09d16206b1630a15164cbc3ff14d9056cdebf4e1bdb121b5bf3ce4a8b871f46546ae68f8b87135f8ea6b0babe64356f9e3f5477b4526ccc45f0760a3ed0b908bcd3d802b18c85c92723f627e4dfd5560e61ed9204af3b3e902b09e7fe5efeb06ad71d22aadd1ff67644392fda093cba747185985a736a544ca85e0cbb9baa1f39ceb38fb3d8d9b3a207f3cd6a0c6e59662c83ae61c007e898af46d42151f845bb1ff6dc03470ae852af2323da0a41b01791d145f950548fab5d8afc2733afa21eaf3714b1625cef2ee8fae2c0efccfc9fa389f1dbac5ba452eec3acbcff8e7c355d6576822b526f5fdb68155fe464fdef942b9e006f90ef28a34b6ff6556bdb8f1a28e5ddb6adcdcc22d9c86ec64398a766a27a8846b49e7b4aa2369bd515ac6a99af48482c11cfbe8079e847e3cc1330750368dac8d84f9cf061dbd7c41bca408ebdd946dbde92a2148b9f05136b5ec6dc2efcd6cf180c8f3f5e926d5f355dc52b9ba6d3bb1e1635f5d22b4e3ccf5bd18aa4fbb737a0783318111cfff8a464bdaffafa337c17a3b86afb502dfa8f5b8ae2fe79d8a663de7e90fd518170ecc93ec027be4b86a760ceb7ba5bf233038d28f2f7abfb6b09e89488ebe0e591b9f99c509a0269a8432cd9241b79cd1c1242cd587393b4e1bc8df0728563b634ec8335625ffb882c7f8cd1bf5c562ac7671aab163fe28a699f06f283dd7c75ec390e850ceb693b32150f257a6c111e2b06ceb46833ca64367fb02b5081f8e8bac95be929682f731cc0fe6dce155ac2ef788436797d0a9407f22b635c933812752461348e5ed04eeddca2e115227452f5efa24098e883577c69aba044cd0b1881e00c3e03052e45e252a047bc0ee584996c28f8502b9758a71510fd7e16dde3cc6d7d3ce769c548da1bfaac77835ba3af68be1d5cac1c56eb32d13d263067664d4de6183c6b653ef60ffb12524c3f8b5a83d85091c056f3091ea3443450a66ed13c439520049595de7156b165fd9e7f8a1494a8f11595abac5113be95442b4665159a2a30c3e1aa19b01bb32e6a6070bb81caa6dd47feccfea5af8b32a7019f5632f39d14758495c7c24a50bd0cbedffb68fc921634a4e90ee7a677a8161aea70937bf23bbd6fcc2f689ba06b32f6ba1054e660a9fa663edcb7da0f9ea6df06db61019eefd8aaac0d924bb75dbaba3d06d7340ee90fbb7e929440d677640c139ed23d592ece404a4e8b9cdc93718973b20aa64bd1373033935aa39d1ca6cb0a912273cdcc2ad404504717f74e4d1c8b63360cfe8e8c2b880572e1d3f488393bfecc98cec765f3bc7246366aaa5d4d56104fb0506f09f7762f5456a8c13db0981dedd80e8701ebc54c8512591a354c41620baea5d9e903fbbf0dbce027ac5ba54e64ae6e90000155c4c5d42f5d29652f3bd5be3fba024b95e7d2b1179ace7f467c295a7fadc8921d993a2943eaa6cde14c9ed9ab6ef668c7604eb14474fd7b6979b30d01bc8c8173de09e554c356e0e5c7e97e8af86037578ba5791cea57704a1d58e3da1acdfb288429149e1d88b6b3349bd00c6ad734dab3b214bb7b1e9487a57b8db8cd34f4a443f91d308589a2b94a0c17c49d0c2d6f9e907041403bf89ae301b6866c4c38440765dd92ebc76d2c7952ea1a137ef4eb184866337fb46a54f792221b92539b271db2ec4582a9364004fa655ace0fd8a3e98dccb790b06a263d60481679f37c1a3923acbce099b71a70ae2b0557ac468aededd2b301cf890ac275295f684a74397f5b7c7852a21c6999f4cae7f18b2ec37a82bca05432891705dd2014f17d72ad8763f13a64a3aa425854579642fabae264c1c201aced20ebae90dc423958d9acb061c566f5024c94e1749d4d9df2b6f8a9f598c3a7fbd43186220c890a845dd8dacb4e81291f8804e065280565d02052362f61f58a99e80c08286f546304a45a16f83d9501e7ccd14dfae0c45a94aa81a1dc9f6fa9e16875e2b8b3b842ff2f2e762c431aa75153645e06fd8af5341c1695af9757394a141e3302fa6cb0cb4a6dcd3e488af9fba4679108cfad271556130aa0b9fe1935eb0275cfb1531200d09364045626d54887b7dff15d6c2fc5557593d99cfdec4b55d69ecf95284b3a9299d3d1260016dc036b947e8283a056166dd097ef900bca04e370142976c6c302b912a85f02b248d00862909e4e519ffa43c3bce76093b5af4fa84cce2ff8e62648d864610ad1c0b08261bc4458f55db243c4db26af801178e711b41f926def9a432497d4fa3b3b68c7fcd4f6a722bacf07083c28b28da796a1db17f65a59248ab41997c21aff4187f3c7fb68fc2c8aada3da74c90b12d095a7a150531b2006c416047d14c6bfa92dd68bab63c7d9537b592f3a42c408c323123306306c0c7642579824e800d79ba444430e5b0cfe2714a3a923e3cba60a34f97dc7ca67ef607b83512924eaf7f5b3e523979e944adf08c41b71e15694c3825595b65a5cdec8a14a38eac4e01641abd5822e2df5b1897c1a3ee92e26ca3beede71a4a13d1da59b4d186df869c63774516cc0ca999cbd0ea81d730d346578ffd1444d8f63e64ffd3c8705b384f0840b8e83f50efdc156e5fea2bf99fd39507c2b0f0d9eb642c87d8706bdad77215a9e2bc01592ebd6d965be1dae5047132c0c8858e918bcea4f5301adbfb6d0c8af9392ddd7f5eb8b7a67308352431314d632d7a39a758d753dde75a0b0264a314826048fb5fda504ceff54a28e9f2aa8f73e103ed0272779ce130e2b617132aa55def9b4b38d1f162cd74097fe00d9361adf9e4bae89c058a81e95cce8f0e235f9218d645e83d975cbdbbc3c42af5d8e4b16179323e62654792e2d7a804072ee6ba6e42efa858f6b113d22210fe805498ae73bf402bf55c1cbe8635fc5037a3815d216d9b36ccc1a9a3579cb52035e4aff8aca8c27c496a2aa877be37d3429a65501db139d8890078c3af105059173d9c5322163eea1c475dab2a4c8e9de02c50ab534bdf8d696218699f1a94b93ce21840dd6ff1026289ca6efbbbf8aa2dcdec41168cce59486ef8a7ee2cb06f000aef89c1aeee31b1bc7637c8d052ce9646a5b39713872c4b7ad75d4efd502c1161c011739d439540991735813aa21977dce4e7a195e1f79e34dbb15e5d5c1cb0a7d0f7ae640f453980d982dcf97142e2f6bcb771b79af7e62dbf285cc6a9eea484f7b1badf0f3cc515db224164631855db784fde3d45b092243149024706b6867f4251ba9e93314bbd0d9b6d05f2e58ed7875c6f12b3ddefe1c7b990a3dd6641975060b4fd43d6792d96b901dbb74abbcb1b2722bf431a142ce200594bcf3d6223dce3826956a53020870a6483b34d9387a72c071b531f4f6de9ca49c660fab909b902444c0a64e4c44f1f95e44ccdf4e531eb53c65adf90bfc4ad7e2e7dba63335b5f6bbcec02744f9c85ae059dc7cf9627edfcf5929c5d804a3192d99e355e50585ecfc600d0197ded0eb18779031d19f210973a56928599c9ae7f1352112be9e66e98cff62ff36631a04cd6c4f0eb5a6fd03be666e55b0b24e137fa0d8741c7c9609e89c5111cd5a7fe28b8cf283416fab04597a2206f66bd78beefa14d8463bb983433743d4a84297b6664a984d969c7bfea04a2b2d819cbfa5d6228d8405c127df5feea713b029ae55bd7bb873fd20203d39d5f3a93103e899a57b88369b37d8b32dd7997d3b3e00f04c29cdadb77fd538ff0d41e881307de508ae78798c15b7d3a80470fd047a420805f7b0fbbd8c75452911b34da5b9f8ff7e0c65042c1c3730b0c90ce09eaf50274665e4ff55149f2b1358010f2d0ee7a420816b5cf62698c6bf33b610ab66c5cbc803e4f84a7235264dfbd683758837be6d214b01e6861fe7824f28f200532ca023cdb258001656548370916f7406249acc29f0ab4b5c773ef658612fdf34c949ddd02e3469b741fc7036ece46f4c59c65cc37b13ce7260c1c2985c13fca85c0d36b4a2655f15f79edbfd23b8825e830a94de880f3f793196b3f7b1a37b3532027c61a1bbda88410643e8d70521a0a0809693887247e099f3c9dfa6a44231d2f474981fa8ac3d50b94ba33f25a9db8677a618fbb3802d013f08cad3cb197ae065fc7b02207091c2166467d061663b3ab61180deebf3839ae7598037e8f1fbb3a68b1bc38a654b5942cb26486359ef6336f17c3adbbdb95a94d43b03e6fa0b68417b97e7c0b81381bb89a41f46e72e9303a9f81d21c6d98c68c6a57f92b7757bf4edb24c472eff92effe196a1e7ea9041e7e7fb7d53394a3c210f7f83840a5b762661be2195c22bdf4284fc9f6f55e2b933a7d1cfde5fd4db79d155c8b48c092b85340393fcc084c4c3d6e198ad2f26abab91a2e69fbafdba1b68f81a374a436e05ecf9e2d078658ea7c2e343b00ea07fb53280613197f6690c5e29622b358f1f29d1a99bfdde6ba43b8931f012c507bc96175d261f6571fb6c659a48842531aae3bebd171f1056fe8764933bae822f3c585cecb55ef07ee55ca36d6771a4e2d344f625267e55c2295d32f18ba5c6e0c5a865ab26d0c81e73e2dc6b9f2772229933f319273605aaf701e0e08d11b860a76fbcb318dc2365835dfd199b3f79885b39754b80d7b206c64b7a5aa228a1f3153ad0ba6afd643ee32cab3b234c5a95b4babb4d20144e552719d495b4e9fff408fa9cef845e6aecc9c6e04b8fa2eac950b663cb8b3e3c353d53686584abea13b15cc707b8a6de4ff8a07c312dc1c6a9ca177c25f9253089c7445ca88386ebbccca8e801a674eecf3dcfc686133f26abd6dd6863302ff6318e3ef3239bc0b10bdee11bac9d831b553098a95cb132cc7cf7a80c16a3e8518b5722a6167e8e16c3b6d5c5db44bf32bb110be6bebe2560f1205ed636f694ad1c0d8984605595f5d8e19e6232773f691939d8cfdc0d31e70d0052bbc7fba53118376c275061848f6d805fd2e0bac21a831224773503d9df57dbcbcdb1c0419cd799b0f909a4e0ef71b9d4bf79b49c2c0e79325e465784cfe607132b5d860464632c0292fecaa6763d5b14190f8df89767566b3e1b6867c480a55af564925a1753980e70e12a49c108ad5995f18b52b55bd9b2c0c8f1b4dfda6f680f19e59877de290bbc8a10b010f71e5049c88e03d4d43c1243059555a9ba856c7e8b4375d2e42ce894de4cab7811cf4030a9e8dea57fed11175601b71e1fc59498f116ed997999eef162672325abe61fb8c6683a27726a89f7303a084400c69fac13f7232e268c3618966ee81fc698948edc1c7965f626c46f98a211bc46df000626f44c43e337976f3259c21db1287d5ca9676a763ecfadcb2aa454c1676000213953c64e8c2e7a70dade5b90c36cf3539f58c3d3cad3ae5f2ce4306c0556815eeb6b6b69552f53bd6d1d52755f829a5e398da4a01cb3978324d8642cfbbbf4a91d580ad66d5e13207edfa6d59ff28599de0f2150d3470a511033e409ad0dfeb3a8c8535d3c5b591e7fc1477cf0caa9515a6c83eceedd770b9670dd7379c392e5c3e09311fb579da91de439f6bdcb00b25f7f6534f5724d729ee46039d6da89af629589c58bd2daa2f0554dcc6d180ab5be9bc67838417adcad4110429528f2d1f2913616f1fcbe7c2b9ea0afe08894047fe48209fe2f0b50de82d52096fb6bcb41ae0ec779e213ddc83bcdf210c611df3bce7a09b53c5c1fa9d63e26832bd3fa3557511b0bc9e0c58d292d6bef764a0ee2f1b01b3f98af7c563ba08a48baa8a519182316bcb144c5b84f06841d1a1068b6eed238393a6fc0a1bcf257acb40765d1c8e7e2729c626b62ca2627d2ab7b8aac5f96cb94ef73bd2c5dbd4eb633449254cbf4d23ba611f42ab0a44014651c1934ce0499b6711b85aeabb11cdf802a37b5b95cb2701d0c6fc50d84e2fcdb1681852efda2dec41762688fc9b606f67a16379a0c05ca7761a8153984dd339d9bf4141e75559b3d03227ba52d85db40f9b732bb70fe4b9e46bab60deb3e05c8a6032a050c56aa48794fed60ecf50d139b1e155a912f2ec02f4104ba917624797787e42e7e8e0f4297cf645d46fcb3160de669af619995f713f0f714e70d5a34685c1c2ea20d970c8ef878b4b6ca1a47813e6d1ef0e6b05c15613bcbd5f543c2f3b6b95296e4575e768e7940625542090c605b3ffd65a0a19d192788e8b655ece525636842322995b8235955c36482c4f62a375ba21b5f1edd20289b14daaaa53f488b90f697f525cff57c3d5a4ea843e9dfb350f6e30be3784bca55a46fe3414c3a8de9299bb52c18eff1025819e16961588ff5894facd7d24733bc143029ff95c76f5ea2f10d759e8f3bf80111de3db7e4b34852d070f5a08455825c6e5e4d277f9fe21d623b5db87502078f717c99b605c756a9922a31cde5632244872c880a6769526c4a7c7473755f86717129677320d319941e64db839d8649feb7d4bbc9947429efbdfd6633b225a991ea3ffa7bb7e04960d9dda3f9e7b9cfdd119d6ff0d450d38d86c3b6f4355ac17f3cdcc0d9414f074288cc961c66977e8bc4e297a8c17f8bd28cf1647f9c87c8984d81c218b15e78d4976e298bf29d0533f7e76078d2ae0267a1e8d521b5770a4c155630dcea3df83b70bce555f274befa7537cb924089aa6e072af442b00027901acbffd324bd59ae5aadd1f8ee3b13b0d1757d96287c8ac05b8c2b5c63e6559dbef246e75eeba21b6df478d618cc8b197044e1f0345ed6f3a4aacca9393212169da065fb293a67208113f19fdb57dedaa344fb551b755bba8d8b226568545c7aa54e1e69df9cad1986ca732815e143492fd4508ac1760fe4e1f55144b2149f0788883c5e5d7339470fc5b9cc5a21d3277cc6ee55b5b7f395ace6aa334aa276bc2f1b2c967ed70938efe293b1e8d92dd1a1927f331d33e8990cfc3c695880f6502218a1edf25150058fd7e7df358c30bf7f19c2bd8924a4b500ac3d3aeb241811394f7f94ac9125d9d9013c93948c44b00e21e0ae857e6e00685b256f22bd74224b459e24d52922a5bf8f8821731e102d8f960d19550de4fdb3f82ad5ff7f2b52be423daa5b2f307d9c37506eac507ebbb1f437a652fb3ca2cea656865bdf130879191b9010a196a7a44d520047e8c9493e832495ee8530eea01a8b765af340487a1687d07de84041108646c75983a292064d3138432b0a0ff897b797aa9c6fd0ed58f04b4d95b81a341f64d7ca27b611204569d11dfc407f9488eea3942b6109ca69e535c879f57db4f1794a98bd0692f70e3daf86a22b6a3faaa5f212519132c522d6220e004c248c103303ad85c04a51ab31e82b584446d8b8bceb1132c32a344e57b2b9ffea144c7b161ab082d09d88ccbe3ebb6402f3ec36aa6b67365a39786909556f5b2cfd9e27704934c64ae96cbe242e6a8661db4de773a9a9f975a6907c7ad6aabdfd346b8c944cf2a8c130f8a218ac145618ccf5ed456b755dabd9dff208e993ead2d6bd19a852096942aaf84252bd00625885ae4546a66d202ab76b99df638aa34bcbe2a28df47b4f6fd9fbb62d23e5f21b39a05f3c2d3418a0e85d957510cbda773bfde7f70d822a11742db2cdbfbdfff72ba30eb06fb36ea56419c50b3a0c513fdc1a2efde5cb4cb57984e5e76dabb4a2a11486ffffc7b51102f05c86365f9fbb5f6b900ed5cda1e28b19f9652c3ee9bc5ffd3c399014141ad72f6e81283450edc6c688e40ba2abb9adfbb1c282a3fb5c1d088592ea09bc3a09f536bdeb57babfd2f1dba066455b6068666eddc79ff257d5758060369adacf4d028544248f9d14394b8050c8adb09d9775567316c5748683ac3d00bcf28daf18cdb565b62301e79678f6a45650f70bc94f4afafc6319c7a038484c56c59a59280a88a65585698f997bc2a1a26ffae7379df0368380b9bb007ca98e399c0bb78bdeaa32d4fc8bdc887ce3ca4d3cc0986685fd60fb220ab61a73d168937bc377e9bb4fdb0ea6d8835d3f44264bd8ad6a6f8adecc7cd157b81c227eba78be1507131424253473afcc0558a8dc521520f0166cb2712d7e5b4f35cd4f872a68e2cc5ef3d004aa6ac8a781bf868e75ccb0979ef0aecf56efbe15010a00875b445cd1b7a0966c5a553927280b6ebd4debb0b291fab3bf56cc7a1548ccebd579c29429e765f0bc7cd4f7411d1fea7321f0e01d93e1b93b8977f30a896f92b34a299c5ec7cbef0191f955e8811f79b6fee3700671450020b9e8912255c5707ab5bd9dd2af6fcbd0d8754f4de7a08b95cfd83fb187e7e6645512a0a6b9f6baeb542d3d71113c79f6b632c2ee0c5acdb3fd71ebeb7af69a71eb81a17b75590e61068fcd12dc101ba96f9d83b81cd704a4130b45afd58c092fc8eb9936433edf5fc0b3f2ac61b83864f75a7a1fe225bd935ef68f27b90294d8e517978292fdfc4d4ffd042fc87391a191f86cf40da079771b6dcbf330ef3edcc2da11d268b25eafe1a86f46eb830d09cd727ecbc8d72c798fc3d81c9b231b44d3746a9b8ea275ab0355686478662d0760a99d534b14b9ef66c5db2485f6b2f8be449dda87b94baa1b1ddcb7eccb5314e47781e909fd9d194ff49aec60e32eef71f826b74aae6e237ea9dba731f3d64a49f9c6d822acc9b9de30502dd6e791b562a7d88cd7fb57346ed4341df51212fba74b5acb3c5cb1a45ced7fb5ab24dbde48cdb12b126664d0a55ba869f72829229f17b56a2f712bfe48da4a4e7b53cac1a9a8799c23d93fc901804c540cbb1aeaf9962004a2866d5676eb851d5ed363b0c70f5f935d31d473e48dcd3cc8b2cd00167e182f40456768b38dbaeec5619aa377a944a9874cfd58f0d0b1fa230de5cb1cffd8834a5b073b37d8f293daaa87306977cd523f8bea3e46bd5a3e024223cf52dffddf3fbf061e80d6c3c0a0457bc8796089a2f47c83e78658873a34ba139289d46f9d5a97cce9112134afafc8f6b454178a20f8d2c1d4a301c8c1c1e6d546de376fc098de2ad5090c55633d47109edcfb217c5a898d94c88ecc26aee25805298ae01b77c01310a5fbb51456a4ea7d8a55ea445b40a3f04b8e92f8e00802f826d0b4f24c8fc70d467640fb01f0adf22e2752353fc57f605e361e3a454b16e58ea87f96c8540b6f1f28a2f4926f8a83557483dc758c4a84de4128fd13f031ad59180d3952346dee03a64592a0aa4a3b4b97188b6e7713bc8b6bb878e5a9d7c0e52befcb07bba644cc5d2ec475749c6bcb248047174c5f5cca8876412f25e4f92fb07076ce71b4818f231c89599d14ffaefcc0c8a5d72831269076e5d2760898bdd257f86c0823c14ec4c7fd441dcf37b2ba14eeb52963f25f2fa54aa9055fe7f64aee86764c2a61bcfdad7af4c9e57d987e69c483bd8c30c8ee72c43f48043fac10caade333251d4e84c9f9ae13bf4daff6aeb92e45a89169d865367fd685dcfaffad914db64e04d8d7c1c603b4738ffe7d4e8f14c5ad7137e314ced7da8f4994b636205d43d5d824becf537fb65080e82fd95f3a3b83a31f732d7709ff271e6317f86c8acea8e7feefb9991d0bdf70481f10dbff30945f91f885a61ab6d69982ec8cbdeb94a4e726a8c7ba4e87180f8d2facf99009384cdefc27c23540721a325b85f2a737d7d10e9223e545fa2b7614eb2de7a2de061400c136d6546b6e61207f071e4746d3e4fd2c6017a521ab3b7aa7584303024647f27936fe8cedbfc0cc1b6c63cc647cd310acf5b275810b0748d58d35120b078d163f37dddfeadeab607e63e7c35e7d19e7a196a8893a9a779e76b0ad551dc310b35122c8d763a63ba95887f5f94f45805b2c2e971b78a54fc26ea9b7487380a2331a504e9bb85903ab06225683b501f596f8d7794028f8a1e2137f429b54126cc3595458a9e934bb34eb2fab1778920ccb9d9c9d7177454f2f623ce88e43eebe7b5481300ea717e6180744fbfc09f750e39e014a05fff5bcff120195e5a87d7342b4282f8bc52a535c6cdf38dc36a97a97a7f39e4fafc4a2e4988f5aa48f619806a053674d974560d7f5496f5be679c171788ad20a1115c44e1145a4817525dab49eb9a91dfb377065f3d63ad8f27f0e60c9cc1f91389e8dd8f231e837a1d8051a4387fd34cf415e620332e0e4034b698c8a74c197f2e3117ad446f47c37cfec6012f11ccfd323f3b37d4e10bc9084be14036aa2eae057c5da4ffafc2aa025e3491c065a699fd0aba874b47b85be9f8360953798dbca4b95363629cf725f245cc1fa0e50a5d22a55b79bcaf04106d0dd84c2267f84afa2b3c250a497be7274cd27b39c72c58c1cf1986b3579d5d4323c8c0b6b2561157673d507bdbd68271686ed51d6e3ecc0e2513988f821ab390efb54b3d0eee6ad3a208755689cb3317429f49d3dddbfe184269a7a137fdf8b414ec734e737b925c81879776f7dc74b0a8b58a57a72b7f428f87772861286b9b9b8dd644a1311de3fd4e12688794bceb7fe308657ee7e499fc040d4ffddcf1203188f82ee32e84832bab00ca08a38ae51a99b311bc4c5678fda64aec5cda8b5f9d404ff113a5d8ae6d1d1e81ed4c040ce72477ba37599816b5fe310064cb3ed3d44a4eb494b65b9aebf1f01b53e2d89b8b9d645cd052e5440ce739a5e1b12e3fc92fb45088dbd32be0b1ed7b0d8391b534afcb3c53b8ff1f6a20341c4c2feb642d3bda1fa0cdbaa26ff2ec344ce9d665c3cdba564bfff2283a332f4c4dc9f82408262f7055be3aa1d2d444dc3016e8107ab1f90fa88c03e6f1c94a6eae14cc60b9dc8119debaeebee65fa70978439ddfe5ec12b8e3480794f9efb2c33eb03f71a9e703f0463dbdfc42a3beda5ac7f13cacc2e50e666c4bd65cab3cdc002abc4a0d6b21d71906840e8dee117959edafb62f22f3d59747558c9b0d0953434e73b5361d444dc3211747539315af06b0ae1a8507c82a339446f3fe21563256ba3dc648787644df73f5ae8de900de82b92a9e4969118fd38883d2388a11e33c9bd06fe07fdc5d2d41b14b8b64d376bf55b111f0be63e73e451230a34dac730bf7d5a8f6d8e77b08d8e0aab4d9597734a58dcb6a0675ed46363c974100fced21c2e450d2690c0e53240d21e7ffe38158cf8cc26c29ba6df7311f4080d1885bc951f411148dfbb9d6bcb21aa6e5558e843e0127a2f2a019f78c99d342e628a8935b03a7030b4bade88fcbf3259b030c4781357df5e2477dbb7f512451e3f31db1d08380e62a8559b60dc77d7175de28e3bad71268284361ec6376d0740bfd76192e9ad841861b266d66f14781a4bfd7496c7d1420417600761628dd80bb08baa5266ac18a0c8fe5b9b8db55d68ea4e6106d64d4eca693fdaf3f75396c839d59570ee46c2d16e43b9e618064d87720c7b752ec233e55165d39b9b0e8e472089d92156e0642ae1a487e8a440bfc06157f483eadb987ff83102a91cacf9fdade992c9e67ed678fb7e37c1595b3ded084d33b01c98113e45ffa3f124e0a62ee54d79619ef934433a8b9714897227f334135075a91a50f1fb538947ec38e6faa11f00694abef0fc390cc10a774d6daf88ff71687a084d60cd2a1a16e5f12c48bb8290ae7f167a9988fcbb1cec4b03b0e384be8930771a7704ee43bbc985594737c23a7b4a820120f6c0e4c74b77962ed66bb55c2799cb5d6a82871834b22696f5cb859e64906591d8f2b4018440a2722e405f7d798551db9aeb0793e4af406ba8215518603a4b93b7b2c33dd3d485a4039b93e1a0a11ca8656c3401e1d1194feb5dfffea4ddd96f6a020127f4156ba15831e804ea56a97c4ee5ed1976c2b644be229173f91d52088ec8ed7aaab094706ce5cdc56d6389d35f0d407ec6dc00900afac8e535e38cbf9b6f86b7f74815b29ce6860694d32bfa0d4e058cba3bd8032f4fe181f6a8709e9327ebef8c31fa575a93344b2cb8bef802b38af8541da81e90fff6587dc7582e4f3020e8535ea74afccf6ac744d706e3734b7819c377309f870615dc80b7b31b537e4871fb5d70dfc0f79317d949cb89e1cc141c9f93ecb3cc25a297c81448440231ce04d9561ce5aa565ab6cb03caacffca841fa4f3c2478c077aadc2bafd1ce38714714545eeeb78d07c8c79e35791236f712fbbb65e58320657b5c351b2382ee999257b4fe095d9a1fea903b5f31419a41a56fe4e4504328afaf2eb48c124c088b693c29c870db440b7c10c7cefeba8f4c53e4591e7a1b8f56625152417dbde708ca310099a36c5086a70fff5cca35eb03cb7f8a92ebea8dfc7ca3c5e221a35a68bd6ad0f304a03661032f44986fdf308a6f3ad29fffc68f12d4ad6958d906940e08e76743a4af4275520de8e30f3d5ad9f34532e93699f6dee6f5e58b4812b5bee3dd468d4d89c7135f7cc3dd05ed0c106bc7093a12dbdd6990f606bdcbc5145e97d3d19381c435489a24f2ae49849bbcc7c4e1a91eece3f1453205f89b1e7cd3e8a5c7cd7111344af235001606d48a3897faf9e3234012ea9ac280fe437ab17b1361bd09278be768e46cbd96c49df6f73bd3ffc57957a752b5cc7c646c0f2ec43ca2e82d4826901a02c0ce9f2fbf130931a308c6e67f8e6efa6786d33e1ac18ceb3ce9153249b049b2b1220273aef609b8b98648c58801d0a81e930ef49a4b067687a872db30a79be0904374bc4771d71cb3204ddb09c19247ebe4c46d5004428be812108ee126e9d9ce7dda23c01bf7226080cff935ca09c85c3b821c3c418ef527219ea962db43e0bcc8fe06d954dd41fdf7a489544bf6bc0295f92251cd0d24dbbe4e9ed0e942ac18aaddb730837a18d6f3d6a9671162a8ae37f29419dd53c22dd51e4eb6efe3282ba9b20ccf9293cbdc2c0d392668490c671d427f75405112876f5615f016306709f09334f8fa8306bdfd624242d6afed48f0c781d202e2b8fa6f649bf774df0030a75d723ad0bbf7f5b7ef41f6a397fa96a867f0b3d4e1534cb4a7b1e64277feaeda8574c4513630fe8663fc328f048eb2bb83b643a42c7c5ce4e8c216a3eefc5ff30adef0b209a2eb3e3a6695cafaa35ec7386028abd9adb0f7008bf8c6990ebc449000b3e0b9f8254fbc0ff2bab22136970cec6c20ffe2fd587d8c37b21549a1b08e1d18b1da0fdccb2a9f3969647ccbb1a12c3b8027b499ae243691a960c8408facb3ebfd11a85068f33973ac5601a4d19dee5ca30490292d94baf6815bf1b7ab999023d3625ea1d05c3ec50dbb163ded7e65f38161ed9d1a1ef36cec4acecbf7a4811da210b669d2df730792935115a6d1a08a6d3e0f4318a7196a7d01b67bd714b43306410104e02b3a2f238a3569c1378f74b089f5397b987b49ee84d78c0694ae4ff4b98ec76ca61ee15911069f1241817a4f674219af4a446f55781809576f7d0f149379871861e294b41622e09f6089411b64955ea4faa01151bc4046e64c49a1439deeef31d525db692c7f51055ebf1515e3f90608415780d8ec739b85c7ed550c28d9b39732e51244e842a981f7c356caf16649d3643026e51c0a457e95e78bd1f9895840e8c40f4b6a868ce726a2c4a66005ff34961006789af80b170f3783843addc7a5ca8d6d29094f8d326a00dab9b6045afb3c50529dca732fd5a59e7c8c6bc418fc1dd722bb5fde0b3103bda67e0c74d35ff797d230b19023f1b5d22ce2be9cbd545d793c47bfae28bcc85f78d0d2de60c9f453a3cf4ca3330c072d0e99553e66de35c4124d025e1cdad69ee4ed22ddf4351e080b25dc8bad110dad8daec251c01560a5ebd45d2397871e17e69d778f6256a835a79a6bb85f5858634e5412d7f2ac5ea96b830da23ee5273c8471c067715acf0a6386048d7f24c548565f1a3c8597ded56ef3537450ca1b9d3a9d778cf15efbd1332aa2611dbab76041ef5f255750699881eb922b44e90ccb15a5236bf6708d21aea54e6fdfced6396eab79b4b70ed1ede2db0e446cf2405c37d87f77eec2372ab4b1ef7b101fdfebdefd679c18f5c400a71cbb8549b3c793b29139c25c54eb90b03172f4acb014622eb7519140f357661bb13293f4818e9bec2195d7b8ee002157e15b011cc68a49c6ef1a5dd09d38588098531fbb928c3ea815677bd8a83a7fd233ec8e921271f209519ab449dd74d89a7e8baf130d6a6a26eff40c60224cd93a27aeebfe76731c265af758091968c89eedfb6fdeb5281ea5cded88d4e6f5e0945eb87ce513a7f66d25ec5ab90690f816474f04fc0e85ae95fe65189ed07ca192778642db20d267983510ca197cf74226fcb3a0fc67f691f110a9fd1b9b96bbec34f34cd5842a40952b80d469b671d506c9bb7b1151ace011433d99c2625870f6d919018db1212bd7d68759ba813248b93cb99e14aa48b95ed9d2e54aec3edec29d945e845df95985cfb030f0c381985c48aa4f9f444291b7654af0fa9bac777562422b3274794b306f8554f884ebca62f1ecf66082357d711b89937b7e36001217368ba7445d04306ccd24a926773b5f7e439f2d196a36026424c7f5ee22a0c6e3333bdf626f04be70b35c3b3c6327723313835da88f5802f8a070f996416536e22d5ec52aec2e3c8c76e9dbfa37119bef7c6f779dd542acfa0203081f0fe1dbdf069fbee6a78c074627e61fbc1a086013d3a1793b30771679bd00647ab3e1e4ee8887107a9d43c363eeace2b512e234842f4bc5c0fa9150e6928fbbf8d95bc30a6cef91da8337c605f34a8fd572bcc9cab2f7755c9886dcb5c94a209890a1cac365aff97198a72248729efff7fce6cf04f04e19409e89daaf674e7e2ae819652559d8bdd4633fd9a034466106dcb9b86074bfcec0221cab4cf918e10fe0e20dfead447b02fd765a19ebe446920be926edc41db89f1bf7d5140dd6e395b21a89173ab2b2e5de7cb68abae29d8253c64f9921aa826b164258cd353d607d0e6f65908e8e3a0b97bf6880e6c8d175e4657acb8afd47ff3ed072d51f03f0c52d12e94682839989388f8689a8a061e37690afdfb0a725dfd38d84cea1314dfad9ce845de62ce0470b58ad94680e240421136491529b3c31c99cff932087542d09a4d5f11dd7e2c64ed0c972a09794a329f3020db600bed3f3fc6bd228a5585d04753b204b7af230bdd53ee83b4a4d72eb1eb868188a8f6473a5b161be4560dfafc88be2506fdfd38c034817c2f1dcbaa3648972af026ce34c7ca6c6279c59ddcc0b30bc4594e8e32113118cd21795deef41ce65a6e8629afbdd0f9b151b67e79029d7075fca03a24763ae41de8385ad037a98ec570b11560b3fb19c5344489b395c0788a186bcbed35a813ef37cc17c57ec7bc17151e19f06db9a9b16605ad12471c1460064c2ae82774b83626f86ee58c6e216d75aa09f0460353ac70a5b8f195fc39c500e5ee542c32d25f372f1bc95577a4f18c6b9122f5102a1b231a92be993eeda09a52aa065b6c44ead1b802d0f6201163db577369b8ad3fcec93cf8aed3912fc378f8527d3eda045e7a5f700e06d21823f638ad37b1f8506b0a2b8167bc3560442548f16c5bf642165e32e3756586d1068c99111e0129cfdea90273f1c30e7a27de86e9dedd0f9ebd78ad0da736b0201ce24c123df54ac350b0374bc270dbe3c688ae55be7bbfb630f6e1a5ad090481585c89d5730e3eb3b7c1f6879adf9f27ba0ae2314d4e7695622b74021c3eee57287608e674752b4da4980e7a526f129f2f57f29ed50dfc49d92fe0def27cb0fddf03ac907f6dc18b25d42c8accce213dc58c34a072e68445d255462913a1bfb2e58e511a4306e9b19bbd8ca3cfbe804378a904374a58c070184e006ff0e58d9ff444ba212dc24d0e092ef49639d9fb888cdd973c86cb2da6a01dc102618cdbdabb3ebeaa8ba9804d9b6aca8a4a1ab0a439b3839c96d01a3621400fed101947b28ec67df9b462d953d599e499fb6be2b83355c0eb407caa66d65f78ba9b7ada04838fc01fda68d67b360da3fc246ba688760b57fe7f6bcc50d3ca03775ced8356aa2285ede7d17034decae80b54dcf1aacbf8524fbc0e51f0765fccac825e296574ee393e24250c52ca61ef7c8ba262d9ca9857a28a53a24f8621756cc4895db1df559f9e0f4296aa464a275638b21ebc54e02b6e56362c00555337cbe6fbeb52459e02d0c390d742b9ce962f91a98cc24d6b89bc53bb766be235197f39288b0b1be48375f3e3c0bcb71b9bbf74946cbdb51f47d40a3d5638fa139129e537c05e42ad508708c45b1c4220678a20079878a96ca57ffd690a22ecde0be38ab5802a520b383f4fc1d8d870eb6cf2a8e2fbc7f82aaa91d3f8d48836edd525ce4a452df16f2d60ebd3013b0be881658e86e27471e054b391ccb4d2ff20f5a3e7711afee9c37ed1a2649fbe19949ad02c437e4693ea689686375fa40f20f833b61d013eebe14049a01d2b3ee673b31302466243fbf91bcc39494f0c8450c2c4f79bb1fe6e08cd16346c532f5844c22b96955e8d62fc1d70b4ee30d8cf86fca1c9080ad47481087fcf8655a0bafd392ffb9715aeb77a86a53e15d61666d7bee3423b625d83978a0c43da140e134258950d0ab3524bbc28ade34442cf8eed678f65e1f342391074166edac913432f5c973e01b6318e8b9a53b4d5d885acbb2a92bf22fb769cef39c49ffb79dd295517da7476ffaab771abb5ab706b612576cda0bae8476305c75d479fd853da1ce8e72e3ac22afe17614b918e446d2ad3ce2052a38c53a58ecb6db2c6586532bd6076d2de1ebdd18f7365095a9303e9d6fca4a7cc31003f32ac30774601a8592dcc72de3b1439dfb6f09947d573c96854757c1f015d977b4e59c84765942cdbef3f5620638cc624e644ec8d1d7722b032bbf56196b23fb1695dae93bedcbf93777d00c275c43ee75f9f70ceeca386b14d131e5a411463cbbc8c8e050df2d2de0b609dd28d22c095aafdb356bb0ae840178c94d6a305294f270f30d08cc661ce6d04062102c6dcc9db65323914d4c222b2d752e467f5fdc0ebdd8c62a6571350b63a9fa43775391e05da956d778bc1ed172945044eeffbdd7d36b0182dc0abd3d48bc81e9375b6526b4cd1da04c9929f34336889b3b721e9623ff0283f012aa520cf5192372040e730bf88bf43dd6adeed320ea0cbbc709344318678f1640719886baae36588b602ed604b2acd38ad33621718598fe79e53a56124503301745c249e974034d92b4a24c5028ed76ba6370176eedea5342e25829f50da2e5d083924f1425dbd7579d6fb3dde81210e80943303d63b872a8b1784bf345dab1245b7ab34f2e1b0ae16d5d463f7a0b872f9cad0f5c549b0772e86523a86ed013312ae7761451db7839d290493df3d1a1ffcc0cb04573238ba1c0418f64a7d7ad9101af2e713302f63b3af2f71611884860aaaa681bfe25a588e4afac12f8f5e87e6a465646dca1c00a6ab00ed20500eec255ec55d36f7dd27f55f3917b0771376bb1400286690f9d429d1e9a38206fa206bb36ab1b87574a52d9d5fb883fa0ac9481f620315f15833d2066793e38b7ccaf6b5dfc69b9527f9ffcf4d23407b63904ed70102515b52dbd82a1673faeca2754cb2a4b6c472ba373ec4ea93894b9d15052aae18e6faafe8f3e690f3e84bc4185a430e856215c39d7b124f33da11b2636fb589aa5436fbde33aff65135a3ece5b0af6a134b6f7730e735eedd180b740140caa9459204e6407720dbc8ed5b228388e9d43799d2726edd1a57da7fd1342f0506fea9691ba182063e8547d1da972d138f198c6ccecd25dda729bcf099681d7af4f9826a503689acc1087dbe723f90bd69921b449ec04bc26f59d70f88c89219a36703da2dc686d3e723cd618cb4086c188ba7fc772e6de248dcadcd7f23c17f1a3f80e2db23bf3d44f3bc47f9f02ebb5be23433186363cc6726bf4a59d9af3a62f053d9f3129c3596f669c2f9cf80c221bce37237b8e399ec9bd641f0077db419811d3d776ba3361616553109231bd75830f5c267959f30ea1e4a2bbbd00534609b9436040884972d9a4170228f56b1c6accd9984fb04845a3025d3db9a9c6665145b41d16e152c3780057e66cf96f7e38d2e4057f131ce76ae4e474e73ef651dfdc36c1728f60af4c579c0037a7b2a9c100833dc6adc60c721baad3306e8ee8d7bfdec90765e92a3cc4a82b53e67b3d6b58bdb4150713b2823fcfa47805e2843c4ecfb0a97aaf8562620aa80a659b4efe4c3271d802e26009f2b923b4ecd89ec66ba7633d95bff6ea12f18e17b7a5d4fbca48deeb46a255794fc1713872e47398b5b234d7c6ecc3074779c413b711ca5760a9e948809e5acc069ad29eb4e9c86fbb4e071c75139dee84b64e1c2d5dade6bfabbabd94973507cb9fcc87f44ae30d0b90187999b126d4623556604edbe4a24bbaba27929fa68173732d4e2024d9542561b6e45e302084126a0ebf8d53d8dfd9f92d97c9890af19447177b047855c3a52c4a858781c522e67e7c8ada74b88dd2fa3b416a7e1ea019b4c338aef95839e3a6aa3ff5bdefcdbfe85149e879e700816112dcad5be105a55884a5c9da1fe7d602e3f8d742c372d27807117a20a361e4b159273d72e60a11733d0a701ec1d43863644acffac0ba7709f690ba23fdca6e43e5dc582dcca9090e99eca66e1074f91c3502f3075623e22308672d2198538d525dedfcaf97506af453c0f8d21f7dfcb0ad8baf56d2e5c2aa83440cf1add5f8edb0bdd09889e404d956c71f37ef0f333a9f56e45560f38afdde722dc7afa6e7564508b183f51484e0b42e525f1818ee661cbd5eea504d680347d59597d20e1671b4d8458f2b10507ab8e3582c6957c73f1606071dffa03297cb09419045a789654fc378a664af269def8d41ea19e25e4a86cc4c8843c54d6798371a6429d5215c2d6022f7bb9fb88b2fe897556d0762b22cdb02e2df0321f7b9ef7ebf38c7b7fd24d355648613d4163ae9f5c2d42aec24bcefe79479b2fbad130907b943bbe7599260c0ab2912a56174cf9ca7f3782af5e2076d8e9ae330537b23deec55eaf08f347c0f1a3830936e6c7a54dbf2c1d74143b6bbff2b2c4eba9ef4a318d6f9da635cd70d894431fd1243a8954e0972476c0d6ecc3144765a3eb1b40c51af583d58286e97fda9063399a1721c48aedafcc012b1bd1d79a753f6c06c8e8130739e0b43e4072dc3fcab20722bc10bff80df8170a302d28c10f0113ab564ed05d31933a9de99d959c4b8020259ae6dd06b1ae8177676ed3e66fa7134263cf1563c14d6c9904f35ceaec4a4c1dd2319fd26f4ffcb89914d07fa0ebae0f43642cf42b33c8afdf311d141496d147a174be6d4e2ef420f5319eb011cbd8895a0974b706dfa2c3f421e4d2a5fbf90e58ca49e35de54c8185bd9b47ac2089f630910f01c11c7ebdce639111fbec8bb95bb48b979d47612f81f659a2fcd4b737374ce5b53df1dfc9e89d0b1a9690072e721be7d059d0aae1f3fea0ad84d2c80ce6037bb3c0f5cbfbcbf61fda3b6cd8420c1d941b79b523001686141f174d55aacfdc932e2a433e02a17533e086e9b28e4c1d3797287bdd1fa0c1e594f774dfbdf2a28737823df48677420319aa3ac0ef119b39830292e2167ce0456b952f52063a98745a208b2da175af8f85e7a50bd2c0e580fea50b1764af7b31b4e3e58542b20c26fc765f91db6483009860f566ee8f0cae181aab449766084a6f50e92f5e4a38eefb5de3355c2b3472a11793f7c8b195c1265b9c4613ce33e04aea158df5cc150fe89608d343143ce8097800f26422771e9d98da987eb2bd7412dda2a0f0e409b2f6400d2d06b4ee6a1ca19669bbf738563a1a9d0ba689aeb7f1437b3a6e7fcc58472d5579bae9684f321d3f69f0721f14bb83248fc1317bba6b46ff059836139675d9782c28b92dc628e6f9b304c4f958d3e184c8e5cdbce7ee85773886b5f949e070d66a051cb4462f9a2b185ce34adbdfc41f89933d3e2598c31a101922bea05bfd2043747f12c9caee58bd03a4392078dbd1b76cbdb09207c6812ffdaf5aa325fb9c7e71bd88a70111ff4eff01c7c51b7f813980229903f4c01c6bb9a93bae9e9bb8d784b95a882ed59dba19dabd9300d1a9d9e3c2fc4562f2c6a567ad193bd3bed2c470634159b8d1f419f325d4d5479fbd4ea654096604ddbafa14f5fa5eb00ac33a5c5afc42a5e056f3fdf67480335da13bf2cdb6f7b996afdf17f04e9d2720ecbddbc19bc0a305ac9d7574ea74fb05bc6a9706ab6144c6dc8eb3e5e643df841f37b079838549f2205dec8cfa141092491b9e223cd020481dc39430e3bc8b11961392f2c3c961c6fcdadfcc0da89c86cb1ba91b20e151fb4b9498530f2323cd93f24416f4d31b14f25ce4e799ddd7bdb0063a5b7ad7ec1b700ef2bd043bf70335fb76be25a0fd4ba3a36f5caa57545cf7fd5cf5dbd23fdf0575904c12e496b276f7016105321de1fc74317b5367a169de07a2c740bad467c8c05f38f2e70cf9cdc94523e2563db101a5b97ceac9f8a4563f7c6db29c2bd0729636c2ea56afe3b96a361c516c85c90e574afc97122fd1bbd8ea453f2946f1b454aeb15c16ced910a93980a6e72722225ddee1e8e6b5074d857c90ab084f0261ab002e0ae0911d2816bc299d6476a3aa51e2125143afbc02f3d240206422ebdc056e2b6a233818cc829af5fffe59e6b3b3d5b6dc3d03476b7bd2c5f4e90e5c1886e3bbfb0c3beba3702ce4365438966c6ac64a214889338d0586a1fc4e378b3db2f5cd6b13b900330efdbd9494658bcd6baa56a2d05508282d0365b47266e37b2b2ee3f3d1e6dbfbf3f9b4b9abc970bcfa4d5afa0eb8e094e7445536c7447b1742d6a1b91267e55ff9e87218ff329a48cbfc4846cca2570ecab948c61737d6feec630ab9f834ca5985763891a88feb1e07d6460d49915751a115ef086d46f43fc70fb012a2b0a3f4d813cdc93bbe4d9fb2636e63044a7480782c85b082a76305ccf7b7292b54e6c3b9f5c0b8f1a4ae56f3038eacac078274e48ee4b63f04dcba0200ee0bd205526cac6149cc19e0f61e3a7f6f6ee2bb4889197bd9aa008d8a1edbcad793cb9517be8d7a195fdccbca15db40de528f0df059347b28886123ed59dd7b652695098d06ea813a53415a55a0774facb46016b19b9ff08fbaf6f92de80338027660751fa44234c7a5330eb09f8360621512e1ac8cf6385930b3f866c894a6077152d127a33d625477fd42749cacad760e642deacd9c6ab4cfaf71dcfe5bc5b5eb08f060069d4c2b0082d6f17b48f67d8943a5ab91e8c6403e4e9747dc5bb7817bb2e976e98b5004779e04e947ebc2568159c4dac2bcd7347649cf3ba3e764caf1821a1880e7e80326198fc76446750d650d9dd9fbdaf9e36767276e5bb327692b3765a770801cf75b33dac534a647f5b3d85ce0f263ece9943ac3a54406300cc316acc7ec4e04b99ec03f2aa40fee3784b5ffe51dec40663f601168a29410f242e5f70556af54cd213133a20d13e76f835b4379b4ae6e2af00683e6814bdceb6f990760146d70121eb3acadb741f2ffd0014d801d8ddb2a8f127174539b58404f55b5cab36c56dc0437e2a425240affad4adac545f91e74c8fde820ba24825555b1f5b7ee48cf031726afc9422b8173b00d30f1709b3a6beaf4a5b484ee67ef224d06036e1a456a45622d15f0f10e0b9fa9f40a73a5757ca61bba60554aa23eaff0c2d39b11eae2d98fcce8d89c603c5af93338ddd13ff8e885cfb3c539061285d9f8e959189ef33c076a42b58298919601cece21f0b3570c609a6bdb8b320672be75fec6be1d28f4768c723444e4af35867fdbb77ac030625ac499bf53e7d00db40790bd48a76b79699c6e2d1e09da74ee6fe585de1d0f71e30cadd13346f61afa77e32850df01b272b91caf8f860a77174fa21be5afab8996100823b363f9ddd48655744fcf4808ec8f29d4edd9f7029bc52a664f6a03817cd39a1a952b3d083ad7693106435ba47b888e768722052ecb5be42c24fcd8727be4f0909f06b757d3d971ea7f638afd6980401697ace7764b1a57573bcaecbefae63610fb3499efbe90c7bdd987770ea6f06a252ab3fea8f050900ab4f7e78057cd144f3c937fcf0097c52efea1ab446166e9912d067d7f56ddea500fdf263aa4e06cd0fb6b2da46f9c814950adecda6d17615715cc8f3006de6168a76305ca38dd3023c753ea1fb8cea64a7d0bc3c4b6a88e0160cb8aff28e2949bea38612c5ce07c6223680799e83e90f5c3cd45cc2a1b550c920e18b416ac129f6817a5e9755d2e4a88a3889d1b984c8e16e25537148deb6c3e84b7b8da9a7778c0b20e3488651ee52ab534c53f33ae73704929e4c3edb2d3aac41c3010c1e069ef6b3af18a590bd436e7b511855a78fe63f6d01d3b57af89c923d5cf0d0c787d9f9a70c5f9bae6439eaa9c1969493eb9f4d40272d270ed0631cd9a56e91176e810434c1be975b1f4c6e84fe0a33a64a1d17e3523c7a5b5dcba5560615e0996088ddc4732fdf1ca24116d0e6d11a00248362e6ccac3c672f404dcc3e7d6209b8b25d17fe1f7e31fb4989aca12ce3700ae44e31fec102181125598e3e8b989fd1b36dbab09ec69c1e98c36f57df2b2394c6a916ab8d0c87b8cf4ba2fd370133069626198973b917083014acdf4dc352419514f8512074956d5319fb154bf8df3c8bf83d3115a41de27ec17e3ae58fab76693432a862e6b6f395be367cd9cee3c70d2c0572b4642d88fbd0dedce5dfe23be6e73c935dc60fbb10df440158cc6cd39264d743866da11e29d22ab9c889ac4eb48f6384a021d53e0be1be20598ad8ff8ac07953a030845cfc7e1bb7e164fb3e08da8564ff6de53c7aaeeb70c5311fd1f1ffa8b80399e4d6e7d898eb2832a297f93f7a52bacb4d1910fcbb0fb830c5d20d5efcce5b3372cc9217237ce9a30bef6125af98fc312d9bdc615f3924e7e1449c902f9811527b659ef8f210bbf5957deb200d32a13df3fb628e28417b3f453014bf9b9eb4df649c7b07674cd2adfdaffb17ae9df5466706cf0eabf1bef111581fad8165b2c3e01de8bd869d4c31a918ab917ccf4d407aab2ae78404d7cf59dc927ca8f70c906429e4d76d611d06f74c5add53160f601cabdb4eda5ea7b814b71c6cba50166cd6c6794bfa37acc9ecb57498b34a8ad13e87287cd0723a763c24cd763a4c6d70eeceaca4d15a0fa3b6664617ee7beaed99266a0ee23b734109867185e9ff2f251a7da6396693a5c1b1207ecb9d623eea6d02a3e682b7f87a43abf34e90d03c9dc87d6d9270f1add9f2d67ccd24b3bfbe71e30e1f2b93d8fe1ea7055d35b7259a80b9aefa60d893a0c10638df0e39ae6763f04386e420fcd7ce17bfd9ac5ab95a5233ec578e3924ed6ee016ff98f15ea82c5db183167c8f51ec96a128caeb0e1651947fb6fe0a04398c8c0469f9ddc0c3b65f25d008326a08839a56498af3f72a910a73b9f7bd274829189c694863859de41b30f1807dd684d0ba92fef9520d18baeeb50213402016b61270f1650ea215f3bf5ebecb39d1bbee37d6ca64e018464864947315eba808c5c3ba91ff6f296580199cd1299397eb82a777807b3c0be07b31a3c288f8977a7f2b6fa8682487db2df87ded48651ac5824f2c5c0a54be40f883e4e5af03376cc6c2656994aca7ac76f8f346554fe1f19ef1bd69c5957061d8731d0fb1534ec2dae33bf304864dd54bb13e2d0a1e6b5c309f575bbba33062bd627c1238d5911674c91f269054bc283ba244182b20e846f39036b7ef5d69afe5f4ceff16c174973d104f9d5ab8fa1b2798be2e4e2b748c68a698201a376697405d636da76c7fb93f0b8c3376d18394a0b35b7bfc759f425079219fe28a6273069a821841ce4c1ecbbed20bc2872fc2d2ea82bb059f86b2ad5709f9ff15904fd4e3fe3ef2a30123daf157a73b128e7812b1da918c1d7258b413d1c83eeb61f459fe3c52c8a64821fae5590059fda6a076d475e230948c9eccdb7fc096d59fbddeee298d4124f6e1c2fc5ef6608bc01f0489f6dc0540e431a0f47e19f993decd2f2ce3a2d77128ef40b9ec8db3aff112a9712c9deeabcb9fa59d06f74c838f1c4abf23d339fa66283a9b89bd5a46f0ef41b3c3fb5735c11473edc8c93523d70f5d7e5ab017cd088c81eb7db5cbd9daaafccdf53db8eb15df3f5c97edea0868c5d99b6b5783053424e739497fbef318f4c3c0f36173572ddedea59da7d091e7aabf006b3eee02c0fcd877a24f794b254c9272f7688e1133965f4e5f2848c71c7fc37ca09ca759c351eefaedc96f2d31a46dae85b2958f761d2eb7a040b65b079d869dba7b909db0a9676ef8b4e79e0f0be713083963c8caaf7c35cd4a98f60d4887f5b86c10aa1df6373c33dac48386af240fdb21b8f03ed1ee0a66404fae24fee02150bafc18c020d8da4c991bc455fa3c381067cb3e9a088d2f0d3e51d7e495814954b030426209113dbf0f438d77ea1bf790d2672e526926fd89d31d43e8b210babfd7d58ecde75abe3b305e5b11ba863cb96e22e62a4264460763847417495b0db7146e934b441dbde8626b8ac50d64d2284d48de159d692d57f31c00399f39de2c13f67ca6ce2d81ae8eaf1e3a825893ac816e53faf99ee677986204d9f5f724e44960ba3ac8945327805f144a3287d6ff224735bade4c6bef34c8db2c75e98e3e0177ae575a026dde365db15e54a5d1cfd95f38d2ea86a3ae2ecca6d3d2a0e3310e3c60a8ac88617da89a98fdae165e9726aad4ee3bc3819648c1c8923fe49622144454544dbc6ce34f275138fe5cc6f2daba13e2aed43a0832d297b07aac6191c170a196ceeec77f7921a2d882a766847580a534fe3f2a512a4931df4c4e0a83dc0d6e27c3f2e641ec7e25302065c53e45e1fb752370326ed1620cfddbe86943a69ac0dd5db678dc5c4b6740b683dfe67e471416239683a540d141943ea217b80649ec501fc9201cf8a6ef61a19b4fc224a8c6da02635875aaedb76fcc6248cd23eaac9d85e91d3f2d684b08ac9a26019138d6802c7781a9e83ce44459a8cf3e35a3cf6afa2cda37b04a97eddd626037ba9d3aa1d1bb2c067a907cde691603959b4e85e28eb902430420f62909f11713b5021ada6d195ab899d3522fd08e8ec86f68cbc421ef404c6664ff799d86694ee1b7e3003146cc6b860bca27557b5f36a99c03770b7d2703a18b372d15a55f80358fffde7c88db21e0a6f5bb8fa714447c9ee265057a6c115e9c846c7db6dcc5f70bcd9f5dc1f65e639407c37ae0961d205afab0e114220c0062867c7a80345a2485ae3bfd51534c3dfa193903c9b5d7e23183ee3cefea2eccd2567cc539c68e91cc613fe42baea3f6ff95687ae5f35f39ae9b05cd23dc990d1c74919abf1a8093dc76bb7448acaeaeac7b20e2bf6d67f5634453c9d269d41eae5ac321a70d1a1c5e5da6ab3c3b16f4f938887b04db4495b4b8e33bc849ecd7c22e49bbe65a8222b0fd8017a250795b2ec7ac585876a35ef2271f45f49a7b63e5fc1c8d70045a9eba0572a3e514d93ed3ca6b3f76da47ccac3759963f7fffaaa36f42b907aa654e71b3c207c8330022c1b042f58c500d86fee6a36a917aad2680b2ed5f7b346e06d0d12a47641e2298cf6edfcb0d8056e3e23e14a3e57dfb3e9f4c3d737286e033234490e33c296f2c438ebc5d34afbe02554cb4de93ed4c5b605bdcf31348c13cda4ed753988745dca347ac047d68ea1833e137d74d58a952a035e1a791e22e9a26861d578d3062be59152147d0727196e74db5172170bfa67870bbb0fab59ee2ff3bc912c64c150c0cbd8196d804fc34c87fd08c769b6a7845c55c0c61450cfd3f0551795c4eb1512452a6475c5ed8af4ed08ea762c9d57cac1471e6def0fea96121e76b521a18774b81fa82694dc4c74f98e4f7bb29ec19f7ddae2bfd3082e68c4539c6e0737193b133d682c47bdcf5f7962a985d3623339a84ade62d5d01fd91c90e39afbc35a7aff173f59e2aeec49befbc60f9b1cb401162b807832ab02b6b89aa25fc405772e083dc5724d6c53bd61dcf74a959a683c5f8301fc1ff2ef04a3ecc0a481bd4931470288bb892b3383923aabee0c91e2a9c9d2928c75e6453031cb8943874292de024ebf0868a4c4ec80f4f35cdf468b878188e3d4b6f0623f735770935bfce6fbc057b97b9884a0db8ea72f1bfa5b0c8a8e1aa026308a031bbe3d6bfaa1913b04e2c26c933f1afca8226ff1d30cd96c98e2297bc429e86bfc2d478940eac8a125d9b9e14ce34f639027953e51fc8266fba8c447cb2d68d1c08df411806547856916e44d606f28e4788d97083c5a2527e7bd708883800cbd29d643bbbb030960947263a8e15c578d5dd5271e55c3eca55259fd463233d88c6d5a397748254515f6107136c5fda47014cdd5d503965929fc6e40ba9d1a5e34af7838b06c8d78faa3943104bf9929afb8de38fa60f69fd322677eaea19308205174139ee2fdd153052ebf6e81701cc907249365be207f3bc8e21ed0e6a21fcc90c582f811b86a2cbf2fe605175eb7e39537825c306ced5963327172c338e0699c786d8b997b378dd447815b8f088dda289b507461e0be2d3430fb3de4efd029b5beb56ac9579e6f7c3913a9b942c3dc88c0bd5cb5c8bfdc2f2490cb83bbd25d6d081e7bbc76c4930a309f83d4bc5820318ad96621b49a8ac032a022a3dd2a847faf775fc9ccf3a6007d1560a20bec6370122658d2dd0235c00868592bf3059ed8c55c34160cd8bfc255e2bbe9a81d6b3a634fa6bd31dee8a6ffcd3eb7b0e058b5293cd4682a30a8b2c3517df2c31bfc55da47db3677e8c238571edca29e672752a80ee2dd63b95658f2eb31236be00c0a88afdeb9c0128759413df738f42380226acac6a76a85d4c29af99c41a15c2f138df3481aca33da8b128497f74d9db1f06e8ff2d2b132a331ee6c0293047fa08397d1c43cdae47271554e0853e54456e01e1e57997f4adf20b54458571f719862fd2c6d52888caf2baeb9f257946a8738f93169f6a415c43e5405391c562f59edb8f4e9e1ab6e22857460ce40b03ec85fc8672934ed804be377bc1976fc1899416919d1587e410ba2e7a821a337950bdb2ad021ce143e721b9844309416b7dd5601a9d655ffe358afcf485e487481311d434f09bb0fa81613c1f06875685b03cf227fc2a02c1b8502f1d8044c94cb2b5194d9f94169d9d9d351d50354de8f9516ed4469c00fa", + "frame_size": 64627 + }, + { + "data": "bad8531cfb589bcbba6b4b3784359116059a7b2c594fa188e65a54092ed27728ec94c496f0d9e4b05e89540fed28b3dc8664287c5c7c19cbb0440535747f693bfb1fd11ef71f75d202761e6544842e31a97c5cfee4b01b01c54d9e61f7ee8d046034eb96da58d4b8ea7131f14d7bbba9a95e278db3fb5858cbb832bda138373cb2cdc010e9159f26bbaa1dab781a8ae3f1168ab2d5c021966f127ed2ad40ceecbd674ea86670f66036d603ddf4a717e8fc7fc9b9959f3f78d5720a20fc9630abf458703017fc12ce2228a185df8e8d71c4c532b1c3dce9927bb681c245943f01307d26e8883f8fbb2b470c18acb467d8d15c219341d7f37b8f5cb74d9bbd08966741f9dda33257d2373ea5c3d79f761c8b5c0a47429bbc5f6d0d261eacc0be23b67dcd144786d1e5d4f8f8679dba5c73c6d2826ba75e9fa78e22ed380eb4fda14a5fb311ffd4df80062dd107040edd818479bca528022a60e3f2e703a77bc6b4", + "frame_size": 301 + }, + { + "data": "7324ddc1dd00e413855f722467f8ef51d502c47ad431ebc061ef77151fdbbff34ddea07de7b599a599d38055d9ff5939782e301374592f481e4d5dabc58030a3e63e120b596fbaccf8520866245a53b5eb10386fdd608fc2bff83d6d646c890b558ba514940116ebe4092b6b1c487bbe46b1859e0a714018f6ed64fa810755c3a7f31f2daeeb52371a8a3536547b27085beb395818799811e26d809cf105e737ec024822c974a2b3b847d0417dca0c05b335e3bd2fce68b007ef6441c7b36b8600ab6eda03dbc44a95e989d7dfbf040d551d200b4c7670a39894d6dcd31cee7db52e515ecb399f613b920119132694f9645ba1ffea07c6f595b4fdeda2734a5a15326e8c5c6f90b854c98e1143803b8b82b5b548f21a3bf57a68c7d226a61bfb89ed2db75e19b738c0524ff756013fc1d26efe73579d8b713ef577f99cf7c7c7b8ca1323fe2071ab11586149ef6224e50479a4b640355ad77ab7dbd3b8cc8a9f96c8f314ea938b294d4cf54d73ccdee27e5a0d96fb31ce48539f646734ec8576720ad8b497fe8b3a35896aa04de2af5575b6111e931f45c0707bf0c961b5434020634bff323d617e975ed4cbed6babe4d7eb77cada25c8ecaa512b53504b1bdd38a95e9bc21edb14f8e4f6ff7ee356877f432e6a16f90f5a3e163aef1de79352d3ceb48efa6d3b5a93170822559843652b99babf6179555039778220d892d04d7ae983968c9121ca44b3560ec5f7a8c39cfc033401fab4dbd5a491692de50115f7cabe44dc747178c2c72a91cad5785613841a3f7532dbcdeec083643142bfe8ac3158cb0bf47149086d5ac998bb34797de6214afa8c50fd15e9f0cb82879050246f41ac3230c13bcb25667119ac9ace00c1226305679668d1b86609a962b33af7331e859ac60769628cd03dade0774b2bb54fa5856c22fd5097908e35d022c59f7f5c9a73c06267e2dfe2dcbdb5f42109cac4b5eb7005bb2df61f863cab9979842c3384723efa8ea0fceb5acf5b2f39f19c93fd9c7349e87da1857750fd59ba3b8c48a65610e732d7924de18e4ed2f409f5d35fcb79f22a2effcb113501274fa7238711e466d90feb1dc29f7d582044a46c0dbdfb941d55d9ab537a6e3693082621289f5995c8f989b23433bba4ec810418671731720848cf0871754fabfd7a8dfcf7b52af5e8b3c162009fad68ac810ffe1cbe02d5962767448e0543d0ea54aed3506806816025bac7829a8625deaf9fb9ddf6e137c49fe90d967d442e94b5b1f36c56e319886b8b132f2b5720056b2997ba930c0b064e6935b9bf635da8bbab0534d0c65dcf48f08b286b772a3146683282c84b680b23fb9c53d670e62fc7bf8dc34d379f113c5d32a2638fcb36a39d003a15d0e0ce525a3d37d321074fbece6721c3aef9a03d72b179060ed9f8c56a1fd1a5de633e06b4f996e648a0c9423cb2310613283c675efdaca5b3ca9288703500877e7d4db3115f60dffd7ba304ef9327a5c7d329333ea3db035f8fe102834ffb50494f9923771dc81b6c4d3ae14abf4bf7e01cabca819e2c5d489c069eccc8aeddff3d221b763e53d372663e0dc6f0c68c4b0adea536e752fb5f7d02a9b201a9f4be7d17fb33017d36187faee2bf54da35f273be83347ccce9421d4f84e7e619ddb6d9f304d17ab710b4f490025399cda3dd59f7262395a4ba6e094f98377379322052ef0074499d52bc64a49ea79f8006e15415836b7c8f696a6a0ac9329a6f4df260b979f3c376cf2e0d9071bb378a185867be9abf615b8bba74cbdf093d6317b6df4ad36a5695788761a06b43203d20c6477a14eaa8a967ab5ed42db22e3dc1d03a16366ffd7ed8175714bd6e5734242d6e84d631eb906b5b161706ec852620a5c4e2cc8681effe09bcd2e8bc8c667e0100e3f3eb6406a24067ed6e2cc440bdabacaa0175f49ecb647ec1301d65f4da8bb9307d63a9981782909e043ef6a5f19a038f5c74cfd2d0d789cc093f509dc0bad70dbf72ae40cdd9f65e10cd676e4abc92cb20e1ed356516d66d31d937e1d6a2b279c25172e3e5cb908a8b45891d0df6228c6d114083ecaef46eceb8368c6ad6945936db0bbaaafe90374386a2fbf73898cb690dfcc4363b3db8b4e2871e0b53d58c485f59052a83c22f59bf39cf75467e847a4d82fe58ddf913c97ec3bafe9e968dad28e2107007dc588e87f23b98b9327dd7b74e51ad3004a4e825482cc0fdfd83de551a2f8235732182aed74a80043aa882259b65da64e3c9542cfa4ba5cb6e71db9861ad6e85693792092e1a887065b9e0a8438fbffd2c8d161f01bb8814fd9f8b6034710bd85ee2e956809a8eab8e968ce6c87e045a1577c1791b497acfd2e1a51c797ac8d2253bfb9c7966576cc747445d41c1cbcbb31cf7fbe95d322f60772c12f7d5dd4b2aba46cd0c8b811f1f8e3f2b9abd4be957b592d9e8569690ce6b7569f051a448658271a57423df4aeb88f69c2a8a42b9a95dcb8f648269f4325fb9187afbe32874d4789aa61e0aded2b54b2e5a99cf5e7084f9584078f98a2a9d3107c44be58a9f200fe782103a61c7c944d0dfa0cf2ead6bbf4207666d435b369e723fdfbf42d4f35fa889f5fff75ffe1cce3e484ce57809418b582a3314057d3e59076673dbc5f0b8979bb28d8ee369b0ba1cac5f21d4f725b32a6e6b465b8cf36eff2a40a3ef390e5dbfaef4f9e2ff6dfd57e39218d201df685c1795a1306e703a56c9b708d70cd21acee80fe536c97c0390cfc9462028b2ffd205ecfdf4f620c28f53225598ed11686dc80c01a8ec8d918a5603ea6e0ab3df698770eec2b035e221a6a56d480053704c75c3c589a9d5d82aa66e68775cad5a00815eadc6786f3796ccf7a42aca224f8eb84be2fdd1a7980e3aee7e1a885bad59704d637fe1c405e4d1c6f87a3dcad70fdb5ddf21d5bbc71db53a78305ed5a7e3d4cfe52827e4730ef69e6da69ffb7a38bed41d19508100f7e1b3850c83630fc60d4ad805d041c4d9b99f7c41a806ad1989e5b2be05998cc100dceda35e336ea48611428393655375ed96286ca566890089e866bea360b7fd675d9ffbf01933a26f5fbccaf1d1213044f18fc26d7535acc7d16eb01bd60f35fed0c32b80c40d4c62e8161653c8de32e92af6b4940f0834b5e6225ea3fe41676086751a8ed5a3a175bd885dd350f6899bb84b17f3f9f9cbcd2416976f799495f4d1a0141dc8edb7ccfd71a159f211bdaf1308dfb716f99a26635d9f7feeaf8ef913471d727d793c5ea5dbaee12182b2bfa01a179c5fc130f928eebe6d65db2c94c8cb235f7440d85021faecebd594cdbbc87c808ffb7e4b7c29cb579510ef4e6884ae46c7ed4d03458531dc9eb66a301d5a1bbbc49977d22601b944ad00f4ab20e2bd11fc976134915b0e84454f98a59de65292246dc7d30a26785d9ad3a02c1e46ef0f99ef7073bbf59736dd6a25b0b59e83923f02ef224377559492ad4c574972344bf3e97265e08365eb3dfffb5c8e0eca87eca87d281fb0cce1571ce500cfa1f202664f57e2b8f66f689cb371277d97a5c622ecd56990cb9ebd7f5fcc96786d913796649ada7b34dc36527d584843c3ca8611751c3fe38c8ffb53e12aa8201060ef8911c503af3d7fa9c6c02cb31e26d82b5378319a06765093060492add7d7d082f3732c55687f710ddaf877da567d29ae0650b1eb0e955eb4985f73d610e7983195317902b057686b5eef1eef99e595f21e2bf4116ba27cf10096336e8b44aac7f8e990807fb27cb67c90deb7106a0f5e6d758ce82887380d29f5ee2957f695d33e60ebad580ead5c33b1c8026c5a4866c93322d21e7e394543d5bb06ba99ce8243a90cc36744dc64755219498ef1902baab183fa106e0c31fce88c9d59c2f0bf07c06f45a6cc80f3a5bc78d25c6779757bf93c9080dbd70c6a97e2580209d5e6fbef4f5ffb7de1c65f0ffe4794519a72ebe8dfa5919f87c7d9386dc1d28e7c0f2053735c94cfd3ab1d48279ae89536436529e9e24c5a4bb90ee6473bdb9ae03ec29e28634776915533e3da327e1205b56f1b72b53ce2f8df58f503587c1a28633301f4dfad845e13e8bde19d1517a6571fb3880cd3f68eb085df502589919ff9e2dd4528aa749e6e5c69b5457c3ef5c40fac3d4b9f5c0a5f8ce5db564b35c3ca3e585edbefd225f8fcf335d75c76cf9cd4387847a771239142e6772994cf27e6da79d2c19e83dbc7abe0f38d3aaf225e45992d52935e45baf39d1d28f8c2a047f74c36777948642d5af53a2514c68c4761b553398295566748a112f982cc8165bca1bd494e36345e10576cc93c37bbc7a7853e145a2fc5067f89691e8343a39f0974defdafefc4ee318eb59e8d846dbf5403c85b7cb810c34a47b346ae91cedf78a3586770efa111d0ec7079613e56353902d71660dd31827921c08c9d0de193209227ae6ef1b9113d15e400d9d5cd2a0a02e720f261f458f0d2a8d1ff135ac4d204733fc801afb5082155884b00a65ecabd6cd3f089c543451f92f77f18be96fe75aeca920fb1e6896fd792bd40dce29733674d236f323a25a6760054cefd7c02704bf77066313f4fdad329d2676129244eb4cafbf949752b23275c31f7351562628eeee247528fd107fff55975425d8854de9910f894d5be78e4ce285871eebf37f0fda975a1d43071fe4d0815b4b126b9646ccb753b4defe821a99f4a2b246b61a946f91d45984f332c7e5d271c46fe52526443c5b6ea0639f23a257907c235bc0aca5ce26a9d56ec86a3eb83c4efd26cb2b49660ddb44dfd980a6ba418c208d77347363807c64bb2256e545be455bd8aec9270844d3a3c3c9a68852d47480dac445cb48f7eff4a4127ed77b5977865c1c0059c240c67063be352c0e8450753c1a3d89512252bcf446aac4cfa711ed71c435621b820ab03b17fa5264417bac87b1ab05d9a5bb18f489e0df515d7881448aa1dc26838a3677dee2836788c9f59469f8a1526e2821f772da7b982ccd24d7a46f6fe844a553f763e4822575df3d7a745f4ae46b67584ec24a40ffe43ac000c10bfbec24666f216c7f4afad0f6cc38f0b7e02146547821aab427b23c5d062a8418dc29d84a787e07c39c9d02b8eaa71cae3842b99511e367dee193665d7d203d4c852bfd318053fe3bab3745abefe55762617e8881ca5dac386694609316960f9a3e90c39288dbe68e7ea6625c01c8226ee5dcf8e5c319979fb639e726fa4b78cfe1eea0895ca84903f2fcc8d46e39b2e1c81eb04981aeb918af9ec26d3516684b88256154b3ae1ad6e0d54ec05d212078ecc58c9ef61cdacd33631798da066bff1fc988f98cd7e513877f0902ffa33a3d2be2e7179f56897309712053159afbea60668d71fb256486a634251a330de4bae4ca63ad790344b6ae43c702199f1f1ef03c59022e3ec5a95dd93f689604ba29e3423693e3e54b910a518da110c97069fa485a24fb6e54b74429e2bc01b563f8659b562b09a38d19b3ef03dc1ba85758d8ad4018dc450ad4cfce01006f1379f1dfc271c923723d17377a78c4d46ec33b995f3239093531bc6a026804b2696cf41b4fd5cfb7bae6bb5a15662a9f1627f275deb54328f0197b8a6a138af5356bb87f52dda33c0effffa3b21f714916251882c72df24ee460ab021d16af799838afe816844d334645a9b6082255a10907620bedbe58d88be77b2d97f0c11d1a199f5b9463fd7b9ef83b512d9f2f459b090e2a24975cace0face44aacc715c42010ad79d2212aa3948b458b0467fd9ec66a49b77df3b740e0bec5a4e482a2d8dc69e5b71663f68a7c25f44a9f80f60171bd0a3f2648cfb6c87995c11d07bb0a3a37adbf76de64502512715a6877cf117e86943a8e470c02ac4ca17b89f95832fa7172d5307c4f52867310d8a3ee37ef03b39a2f774bfa99f29da0a71f063ec2d1f55a49b4111b9177000cb4f2271e55bd8511daa1d4e63362d60f3c346dcab75e08e787e6215c84668859c2105d06362a5b0d928c88dee5526479cf79263eb04d11ce28e202f699681d251b67aafdfd2be8c5b526d08ddc4d710805d19b8116049b1424585b4b26106bf4f1c886cc13513891083df6a89c9445d4381fd9c750f028605c4503c7881b3ca1bc14480085c36a85e6398c52c1b0ccd9e1b7bf89a23f39947a3af30b2435eb454c629b78a9db752757be2b76d65f773ea0a591110649fad44830b14d81ce10ff7e874ef6762ed165adb4dc42587db47b43a3e1dc148f65933463ae18f2d43afa384cf4cd95d1381ff25a7d2a28f0ff00c12cf6b3c109907680b5bc0bd1da3437e3906a83a326aaf68cd1c9d02183471126b0cd7e201f1fd1a6c22fa08bef09db44ebe42cb723736c92f3f3eac47b208c98e7cf7f8e3807958476066f9aaaad55d81c7c57436f5ed198f7c898539795c84b5c1eec43d40509279d68aa93e24f3ff90a01162914da1619dc1debeabf60824f6689bba3e99fcb4a2553aba53ecbad4e0ab28d58e2052d1bbaa47d87b9c9cf807dcfff7a5d23ee3a814db33b4c3b8511cb60f81fe5dc08e3f4770768dd5c0bc39dc73c990bab62370acc887ec751f542f7fbcaba1b3e7e3c2bdf58b7b34150288ede20d3c02830e5857613c9261b62d4ed377cc4c1a440a7b9cc348172233324b99485bc91d5c07cc81489be8c5f3bb474eca6bfd5dbddabffd355b2323344e05e793baefc17bdfc5fda28a6b61716f9ce4a4b57d7a8fe5c09b8e28be82d31b92dfdde0cf243c7c9397ed8e2455478eb917bab9529b5ee30a936bc1e363d71bfd641c35414762ebba9f7b0c6e3d6f825378694651330082185dee71885354987d781173f68608c9deb90111412943d21f65b901821e37873715f492af702f543ee3c5333fe2dbfead02a7452fd83e0d948197c2cf62bcff6c0ba986b97252e0dff0fa5b318a672efa50589a58901180269483c432feaeaf18a6d7d1baabb7a6ee6c99e4e1e280a9562a5ba06e9c9faa7195c2f844dd5f9c09b5d87f6aebc3b0e3aada255b836b9c58bb2609056db67d386d4fd6be461ae64a832901e3182eda1785ea04bcadebc26745814f8e8c4a28bbcad44649b13327ee3e1ec1639ebab60588f7ebc9744a5c4e48fd1d97e5adc758622bc9feecbbd5f077510cac8b6f94b9a3566119f501dd7b20aa6b3e2d7cc0fbd0309d3e1b24144b5a682c2a4ddcbed256f224e3bbd127d33920dcca8e0de2b1bf7fa8a47786c4559298182e98ca7b28c70dd6c86d48eb186f624548e1e9cd32f1be8ebc36958ae3791ffc3b3b37ebc85e0c602dbfef490f5ec470d079adc45d74a959af3523c64bf88a9f6c808f88f93124480a25758593ff801df3b662a962c9bf3f12655b066721895bc1e14af749900e67b3e4ce0cf95d695ea5bc0e5a1eb0b230cd6dd35344f230ac6b57c42a87e5c58d034efcc593a298bf9f1265ca783c17c2570b68cc8c4d5b19274ebe77ade72e8426f8f77998b6b4edbb4124488e1d6f6060c581e856b8396ed94ea48b269c06458a95a4862dd235d46522111df6ba2a78135302194df02cdd06f0468bcc301378aa4e3f13aaa08471ef51b0356ad3d917eabde3d1aa882b15bd510cd1e9e73ddf1c579af254bc94bf55c12b075904196a3a9a7f1965c6718a01f09b2b61c3495869b55468f939e722053fe405969e1042ae7986abb7cb05312376f12c0f142172899c99038e3de6594ebcbcab8ebfb51ec2f858d186dc1c23f10f2d7a8c48582ad5fdae784b22009de0e4c87efa15aa77a6f4d6c83c065740b618bec826975425cc11b5fcff5d1eeb5d56a7e8c78f55294ffdbf52fc92af26640adbbf232fc7dbbf29f10b8a15efec591e53507e85dbd9a3969e002d4c8b9d5dbd35d169afd0fd5acfc5453da198afee493c12e52e4b8dbd9b50de3a25ab00ab14cc6a9210d6a77cb33cda5960b39fcf0992052120922e71c0f9787056f33744e7be83fbffa30fce57917b8a783297af95095e10c40350930547b33e45ac144198e5d9d3d52fd99f51a76125f7a5e3211cd2f19f86e13ae9dd079714edb8213fe52c902ed014b8acd5655f680fd8faae4c81fb2cb4042784b37aada56be16ab8f58397794c94eb8b12a56a48d55990ba6cc3522a6a6ba3a08b67b983ad81d5671964ab454e5bf2e8e9fca74c4d693723b6134a98fe34d55d7e8f8d3eadee004187e71761502fccc1a0f8371f269e1eb53045a810f78650ccb2f91623858b5639d48b03a70b1007e6ee15c40f01d1cf7fe88e4f1af4094219e8a84f876505fdf84974ee60c785befba6ec50f5e7951db2f5a6990b968d87163694c0c05c6a7556fa783573e5eb75968224afdf851c57f6eac62804819c9fb50296779b88770bab128d77facaf580a401602854613aac0b11b794f92e88e25453b444b751aefbbfcd5e451540e6406919229328c8180cbd36449cea59842f4832a7bf054a8606adb90ed4d82dedf6aae35401214350f0b99014466d4fb4a6ffaa7f3397c1dce3e23f072047a4c5fa8c19bd37e5e07601a783f1059ed179f5a3a7bccf9d5073eb591987e0c728a5ba1388092656eef3af51778afd8b01e8c31ffc12b63334bc283bfb36d29a5efd191b5cea678323c4d0da99af5b2bed16b31268d5fe35cd5a9a875bad65ea69b143afc7c3f5115eace313f5dbe47cdf9887b6ea8060fea1abc2232892b25786ff136b679787edfc6aa0f928d2fc5972a3a7ead6b3c96e8b5535a5536c13f82216cea7e4ba2f9d697a1911795bf8c0f1005c7260a6efeb0c9cd9c0c8449928ec85cd7a04c8810d0916363607d05cf84d438c12e297229a28f97fe6611262594a489b4ac96200d6bb198aa906ac82f1b0c6d910d0eec932469d6fa9d4a5b5efee53f01ed96d09019b7150eb74a829677d1ce903d1185c7b858de01fb69bd99eaacd847860bd5e94c9b670faa205c0cf0f26f0d2c59a908669b5fb04458586f5458259a85ccfe990fc46dff07bd9f02120ff912e1ca858d51c5edf03832fb1a3788736480c39d3093b0e2c5b4301941c942e1011c437eca075c988e55d5a7f31d6609415de2e9eafa8f1a8bd51f9dbc0314e763ad8fdddc45931554428f47c9e4ef8bb414c023742504715fa7cfca00846e253c9fe232c1a838e48ddbe4bbef3dc23dcb122610cd3475fe5cb096bf2d86e443efd3de5479b8e13ef6d8713ed8d2ac38cd3576f7acbcce68ea3f381dbd287846e4f05c0fc8d42ccd498fa526f9fd120faf955f0bb744cc08f7a7d932ef6dcedbcd4ec470e59056190d4ae20ee834b7d6e6a23cedbf975dace9de379a84985a8cc5fe2ee6c26d116de386ceedccb7bf7232b4fa868b41ca0d84d008e05e5b11381d94dfc61cf3e447cda3d37543506baf44c3b1085f277fd28895027ff3de24dd84b4c7573d8daa6f53c6e728f571ebb04d209b8a503cb557e820a7ba8b3da50450d7cc7a5a518eafa8cf4009b4d4bae6be88f5072dcf8ed482ec717d81524b4d4d4f26a477d6c2fad4859e18f6689111f44d38b10e0f2301b0d65f97e2cb3b1139f7d2fe4fbec4dc1481247d647b855b308ac9fbe69aec3488d8cd6dcab8278364460a834b32b476e0bb60a17df3fd98993b2a9b938f27f05965c33511ecb33233d10dc5fe0d9416feb73f98339eafe4b89a4b82e6367b394e55d2b0a4eab0c96bc4a854f409d1334d8629058335f7aef5fe1a064b7dd0111f33628a6445f53ec217e57c7b523d76b67cca523750b15b3647f33b5b62a4777f9fd4bc64a337029fb4397c35ee22851c2066464643ed6c207d2bcbba70f90c99d56731cf0d12005831ede5985a5595a84775feb84191327790004429a53234389b86a60a5ecb8953bd3d420ff4356330bc203ac44bc7cca861041522343ebffb9fc0b01e50dcf53da7a92ee57aee439d1f3c25724aa779fb2bec5fdb8c4725a87d013615213c16b3d4cf90675b4ee87c54511dd22def5e7610576abe90192a10ba4d0fc1805041f11e41a94e08c1b1cf897b037bc1c7414433823ef5ce596bb1ae7d711cea3e4dd2cc1cef215c4a13bcea0b9421e07514df20a13e0855e4e450f9266a541b7bfa7efb4675e3c9a78eef8117005f53640b044c29c2c2fda3496ea16e0c9f6ec25e2d90750fdd8e99e2957214e12d65b1bd467f8eb2d865a76743c85deaf1cf577ccfb84619a69207c0689f661981698d281a41c0c2d1fe7df0b3dd8ae814d8c2fba6a5b8937831c5e2e685e06c3a748be50a44b89502dea1d05572aa82b104acf0016c0ecc08a2e4e5cbeab69fc62a83cfb31f52581e73d2f0312222a121847430bd961ff6b1523be0afe8555cdc5f6dbf4bf5ca98f1de90cf956fd5b21cb9f028994ed627952aa98aab7b9d5109d45e7c1cd013825bbbaa0d48176027330cc8955700352b463af8c6c1b011f7ef1e3b09c785af9737186c30120580e045f525777a9576ab8f08696fe8e10c0d39b6bfabf85ff1b242e63a4bcb2364cd7af61c3317c3bf952215a6f99b826cd00bd0c4c3d5b79b24b028974a0c1b15bcd5ec038e08bc8ed8bc51b36ed0552ae63083406b1ffc5a51060bc2c6e3d5df63dc66437786655a90c368f88e18fc0b2ba4841b958162d10f1c02fb64247e4ae72197711dea0b43f65e989b744aab960fde51f4b628204fda19349920c0849b7fe580f26a99829d74ff6d184f152323d5f6368f83bd2791b557be02225b075f9c2bf33303743ce4819e5cb24dae58c3cbfecf725cf880612e1a16768910627febd58ff77af247bbf4ee6dcd68cb4d6fb5ae0d1802c4ee2c4eccbb68961693b9913401ad83e023d7d2eac7ed9abd6ed10948aef9df232734d4ec7651dd69478fd5f4bf5c3fc3eb051f578c28f16960ff1ca6b15772bae96d2fc608b0f8efa6a6dd11b01f52b54e40831d632814aaf4e976d5e06afb3cbf98118c9a4b09959c1bbc3fc13214a28793973a4a1812e9c981ab77b8f4f9fc5c711433b28f179026784c2c25e95c628e8e5ec7080759514a5ec486e4b7c5cb9732403b218cc176f992321bd9b387e212ebc73e8a2447e3fe130b904e0e4f5511efcd871fa8dddccc69fb7357279930e6d82b554377dd096d0b81cd8894373b6e75692127d7975ca1e961e9c3f82f5cdbd795e3a617f37763a6b8c05db0348fdfbe66b8306a8ef2464bb163d3d869477e688cdd535da13e50c324a111a9001f389e454e88ac587d86bcaf059903335e9e5e63fa3810735c5fb1fd415063f7c6cbe25cc74ad438ec57c79b77c4df7b71b1a78a73425fb17b0e7362ca9907e1e5645b25cf1c0054a9470ca35c482184de6aeea61d0298635a2769ca13dafd350a0ce78e184d4db24334d46e1307de20faaff46dfa83a3e87ab4b6926f4d606559824b7d449090528dc4a604e95eec7f4db8bcd0a391e37ea5d46c6b6a2568658f5dd1fcf6a234380aa89b8724ef7967f2a77e881d06cacd2b8b0fc7b01cf53d275aec9042d2d6ce24c8822bf5ac0bfc6119d31cf0c10556d71a18b054b02ceaaf18bc918184154da9ddea365ba5cf3837f9d179060b178dfc7b1c81d2ad11fe86d2aa26bbdaa3b7cbe5b8732a8e474b42511f6b6c122848337a873022a9a7e2bdd9d303c6d8e5708b08079e9239d29ee2e0fdd05e167a0c9e66e751d5c28029e8819c99cb1cea6c9466acc5c9b5076446f1c9119b34eb2e4196eee66d33d1338f90e850707593b045e6b8724045e159926837f13d959818c7fd3a7eb605dbba8e1b235aaf9c3d6804ac6ecac740110e54a5899783c3e47d50fb303d5868a0758d17f33fbef67261d8a2543696e4aadb95fc1c00f0b0d02430c432f6d4a5fc7e5871e23e03744e66df913a7e91b71f4e8e9d90bf58b53f9428cdb29baa8f86807c328b53e21323627ba0e81a3581e78ad75275617d4ceba5c117d62e94a0438a085cdb0cef37064d2415266a96dd1079bd93bc013ea279af5d4c67211207b77d643b65834d9f284cc5f68639f7663529c629b6b20cef7ee51b2375d30f2982e703070bb37b9988734f0dc02d7b84c7d330e0c6b42f6e4e897fbd678a35533657a50c1d13253e102ceefeb2926f627b8c9f4209f1fbcd5863ed7231138edb15bc5428a5749350dc8d8d01305c82ab66a8c081a12eb266604b4f9c7ec64fb25e6e4ea70a6c0857aad77869bcec380858aa4d33d528b060c00bfb704c7972468d445181aeac869a77f689c8004203a5a18bd9bd4b1fd71b7dca6e5366e07339e22d32f14103e06bf711ad1f8f53567524e01de76ccbb05f3aae2b22b5cd6bfc6efcf8c1315e5b92d402e45d959c0ecde09fd696bb38ccea6842824d19980f6e404a8f3b8c90150fd5cc9969bd9009c369613f3e9ee27ee47753555318d79bad5eee382e4ef61e2ba390746523d97bd8376a32c6ee79ba19c9b7b853a963adf67c41504606e97710fa04f3a5cd828e67110c706a62bd30e3172637da6e8968e06e378ff888373ad055056c8cdc01960189c2446830501721863dc42b27bbdd9d329d4986cc9c7d9cfb8b8874d9a5c80b587500cfb7b0bf89566ddb708e1ffae45b4c03ea432123e09b76a52347f85ee04e3658e90b41332b3181feba4023ce93ffac7f09e808cfadeae4eac7fc3a2dc8959db7ffab40ef0f08be1e3e7f39c7e071c3d273be8ed32ad3bbd5c0298227f1632c1802e8eb7c0a4936fd70c9151f9e4b078ed8567a7a8cbbd678a667a84dbc4fa2f0c10a9cd3aaf9f187ec1c86eaeb59659f91abe6d7cd1cb0b57a1d5e009e8e8b4b4e6d88c476567fa0feedee68fb59128327915c45a4d89066b3eaf6043c1ef3185ce16b307b0dee696169fceaa9bc7a868c1eb06e342cf9b192cafadb12020871cd1ea1dd93510e668e74fb55a67271b07b6322e59be36f32c436416807dd7bea6d74894d830604e724ed84ba6454421a54f022c600385f5563191c6484810414399df7fc37dd01273f9fe517b2717304beea9fbed0487b72da1e49b4ef17d073b3eb7f0a048a2f95bf438afaadfda521465a7706de754f0a369005a6af61923fa77a57dba73c6b2ed1a92740315bedfa3514390ea991e4b6cdc983455af766b6b0df8fd45a762b13c8d151085f44a079a26ede6b2c248b1b8ae6a978546a9d1ad984194f794f7247874478594b13e9b10ccc1e4b570097862d6ad3cd8ad78fdd5624b1ed133909dfc1432bb2028ad001f9bb011227f4528d8ed0cd357b694b945f641f7f6f9ae03b77d92d7dbb62527b1bf6e4accbdc8306952812b7c3ac914de1bfbd92de971540b8264827aa5b64f90fbb172e9a684b7f3b101b54f4be6c986d2f806c42424ce43c2d0ed8c912e516b2541945a8fdf38d44c3750e4092f328dcebe39af11d75affb1b2c52afcd81c7978251b3caf9037fc6257fcb1e72708c9db9bec87b579e30fa82b927091c68bf391d42231b759b7fb3570b4f3f50d51c90141392e2b6d12d6443b43e2369bca7cd2362f9dd151c403568ef06efed40de4bcb820570c1159df07aabc1637e4686c190c5517c91b64d2424416ea3036a98f0ad517d7d348e51309f7be1772c38493d5fd79f489ef6786cb5f2675df2d849efceb675fa01f51f5587596d1cc68a0be4ee48826f8d1cfe5d7e284537b587d10c10139d7b583e8f201c05868fffc3cb93d2fa8e7b10f9485d3679fe196eb582408c1d8127ab8791d13cae473a06dd9eb7b84372d32cf4d6c6fe86f0c0002d3671acd0b67c4dd7ae4ab6735d41954e6a7374acc22a342fd3ca4a17001d3ea3a6f2be12f4fff2dd74d6e2e9d322043a71a339174b938db3a456caca35369c195f60f00c2c098c2d7a9c2896ed513c502c37c38ca6be9a34303cfcb7622ccadf5f95c6ca340321f0104e18e00489cbf2389e3cf679e5da2261b564a2c09ce6d5d52bc021964dfcc3444c4492eeaec000115e6f11309462a63e4881071654d5b859d3bb23afab465f7c3e6d49135decee83755d3375437d0ffa453f89f1256422c4a32b74e572de039c89814b382b9c65f9ec7a3798cb93c3f66ebbee6f9c285ed38dcfff751076b710bb95993604a88a49b4165be0d6ffc8cb5a9ecfd7319976439719c578f2cbfc41059e5dab187f66dfba49f0beb5861f107766750a375d0014309340e5d7198c5824372c1dee1030a80bf7a9e5c71b548445fd21c397b3c81baedbb3842586c3ae653e322d14a9a79883331eb633ff875c64f9ca28458881d9a8c42af358b8830333e69c2535ec335328994d0265d9ade8628daed72e68f73795159f0571c6e50b5d513c3496b81c834bfbeaf4c7f1bd4e9cc8ed7fa1708bd9ae575d0ef2fa34a4f9c3479be9903c200ca4d1c0705dd24c3686484b198b4050d6c289c84fbf6efa20766d081170d96a2190b2a1950426b6aae1d508b917bd46081050c619b8108d174be68d6bdd427d0119b74cb97fad68d55edec9677dbb7190bf1ab5d435f046e24df0853346b7cb1b48b31cee8c648c1ee4080705eb0c48cbeab9e8fbf8dff163e88c735c43a2e666b824d7e499bd5b4308b601cc502cfb3dc6d6ee7b5274b1ad6def96e450339c6d7a2ad7f328662431b2edde2fa26559335ab288baa54e9de05880a89b5e0b77e2ce3d6a36cbde8bed93c2f1a522f6835670c6cfa8f15246e59533fe2c2333aa3cf72c84ad556df64c7c909c2b77bfb39a35a90a5fbc622be6a7964566653f4a5b9f7823f1fbb27f7c1d7e7924fe697c5b427ea441c2a52542c18f7ef64e56f33c6e3a1f7da9c8e7651f37311de178e93cc2056e39ed13ba3b049c23899ed504bc40fc01e9122542b26c254b5a2bdfac5868c7a7c1d29d08883157cbd700408c8584f7df5b751b1ccb6db1a7110b28d88c6ce565df30cf9483a1071e8cefce20b28c194cb2b69d63e8c64fd4e6072ae761fef305672538b1a9dea76d9dcfa1395bc38e9c04155d10e92c6ebe40daf3c595544cb54249d3c523590592e2632186e1ee21cd42d46f58323ea90443cdf41007a2222f413dc75e0e65f4ab8baba6d4de6314b301d9f7f48c226322b567fe41ccdc6c4efb53c668408109c9dfcbb7132691e8e6858f3e4dcc513f005b617c0c84fc640b2451aae2b83fe8282a887d18411ac8af71673a7c95ff2513652db0984984229688faba93e0825271e6e0270289ef0fa4951aebfd8aba3c2c37d03e3be5116b32765f1cffe157e8eef4bad2b44861cea3aaf6f8021519d0cdb944d641bb13cdb98c040c9220874da73e08e39abf39b51baa78d43c1c5db14c3ef9a50ae9b56a5716973e1b9f5e28ae21bd69f7fbf7b914d9aeb9906269c1bb7b8d2355f80268bb6120b3a51420ee52ba16be7d7f29ffbfe0b87aad028348a863fa9753d00f777dc98c749fa029a2cec8e1be2745cbf4d1268d81a98ea754d0437c278a06caf83cb153cee2c55c6447584b8eec3e79b8c095a27ab942e340c7646f8181932b5aef6933b9fb953c18d43fc2fed1cc8cd1eb06a37419ee8202d33621d767de8505281d0996c17e89bc21636a91ff989ecbfcfc1831874178aac3ee4f5b1a5afd4fa61e8a98a0e35ce1d2e9d8c8c83033c26d234348405816bd2005698530b73b49adb8066aa620996f73f397845f51a2708c35c31fd989dc4a0ba3d1716daf5c0dce500042d87c911026de55ee0b38ce6744fb763bb993729e6c78f0eea57c8e870ff97eefa6d7b150a10bbaf76ec7bcb64e703516b8bade179d051d2194a77c5c4ac68d004c339eba9ceb1169a38c4fc2ba89b9a3830e6e6050e29e8edad7b505c17ba2bbeebe59cd26feef6f1c2d3cadc6057641b379d25c9256eab141e66652ebf2b5d0ce70291469ee4efd991a8ea77d82ffb5626672f8bd49433fb74ad35c0c220742a99a2088afb8a1acb3252fb6646c554d0edcc0138b108f68af48ae93b80c0d5f82a74fb880bd6c06ca544170e1da0b0e52e8976b6b4406b9de44f2afe6143bd6cb04edc90f7dbdf683db1ec7c96d3573ffe35ccf012d0bbef587380d8ad9dce1de3d561ec34423492a8f59ce35729e801e7d0b9426c608696423a4aa38c86a7395144f423a05a4cf27c6b1a1f01d0d2bc032028b74d549b5ac74c21422e94f5d812c93c48c82491fdfd30e42b99c66698280a2741b98603411a475b0695ce0289012ad366d71c12ad7d3cfb845a1896eca677179944ec1ff0fdb7776cb073abcc150a81b14bb00d983c2b4478c30adcd9235186e6bb372061493188e6966c3f810ca2e66bd3913bf206fa68926bdc54ed90b4c08d576856ae299f47d7b4a74da8484e892de50e61fcfef2edd5b140fc2aa9140fc7f3b9ba33c1a6b3a5fef8645e89b40d2e6d56e5b319e8684ec8a31d06827882d4c3d9dcafa08792819701fff712ede231b80b47c25bf66470e89721b2e72bfe64f1a3c264493309f8c1174e8c98378e8403cc63b29d5ae2d8e04f46e1e4eb558a648017781bf6131a16eba7733e1068fbbf4284aa9bdc9fdf2388af629eff24ebfb84d6c8265c5f84c5fdfb26bd84533bd58ea6f1dbdfc998208d7c5fcf17c4ef0a189e346794e3310a4a8857d09e7011a7c57fe1a856dbc29860af6853c5e0b9a5954a0f07c49a23f8195ac0768c3e73d4f920f5a00cc2dfeabe249cdb3a9cfed8b3d1834ddc59a9009314f5a603da42dda3e3d192e7deb1f09a58d0886517dae84fe75f06727c94d49a9771d7b3a7b92228b653dadb01003da5ef250213a1c028ca30c00e9e3ad0ed310f0d6a4ccfe8917acd7066298110fff069cbb92a2b772bd54b4c4e10a511df733371c2a5a7608411e0d2e3b94a29d3b0ff7502ddf4e3a6614355579c922f9007e61b345e98a311344a0953b813a2823224f2864286098579fe1b206736829aa32314f3132c58d42829b55d658e4579a49b5c96621c4b6c57577953b93b8c35e1fe1595e0741cc28098761c6588863b770dc0a831a8a146c20a0c8ef0f23fe79ff063baf65ca8e9320b15542497524bff4b63a5995b195e3eda41c3229cef386a494f8c350080f6b9cd42bf49536904574d83bdc0dce24cbd72d05124f0bedc6951daf6059a247e735993576a8c3ede7eb925d12c038fd1b429337915df0d09d4de865936997b4a2eda55cb1fa6d736b164b844db39e701dee85603230256f895738ff365a82ba16d61be7a4336410d042cf7a0e210192ebd6ffad1ed882b79d85a74f6540f8959b0c739f7371ea7d350b1aa9becf6bfcf8edf4ee39ad8f67666ae5b8750dc2c0dd04fadab6c41f43f5e9debba83750a2e8645eae604420ffe71cb2c009c7f5a53d9b2b2fa3943ee105a3bc1059bc6dab6e7e2b608e4349d3879c155a8c2a376c30a47abfcb9071265a3023f4ea50dee1ce098b00d2283d4e1a551f56b6b56f6cb8ba27baa019be5198262adba777c9f382204f9c0953a57aa5e416fdabe1bfcbc83dab76cf9e1637d286a30639a9a2feadc54e4aca980d240e0f5396101a40a83c4cef3f13d3359f10b03cd978f80017925e01fdda36f28bb230a7d6b04490145337e4c46d1e06dd2351b11341a2d983e51ef7269e7026793cf17397e112ef40bfb0f8024594ecc44ea0968d1a2272ca44350a84afd6c684b89ee69bbbd83be435002d74033d303eaf266c2628a9c06a958e3f698bc7938b5c7d1270dff137009a5696f2457bf7c7c0cc88b9e76e5e1a0f87ee74cc5c2144cdde1712eeefd85b0703274a9346b4cacc03ce44ea853aec72344e8abc0aada77e536a9c6486091fdb8dd9a28d5e6d737308511f1e49cda64f1d9a66c1b938774b2b004a619a337f76470bae6675c43e5c07f9f007dd2790a694d8cde21f081d247ac9dc77c3489646da9cafc184de8f659935f91e55deeea82f287c72306af30a70009633cd7d4186c4909e01e8246357ffdf4884c5f1c8b45bc52e466bf5d67f04bbab5dc623837b32a78ac48dc3c22a0e206de46186d423e47fa03a9b63f8e56af80985c4c744213c57bbce32cf42860be098cec8a36508a1a0ab5288572938ba24d3f5e18ad75d186402ecffd44349b84e98e6cb9e2d975f439e7afcc474459c1eb3e63a4ba2ffaece835e734aef49b60e3e54e1b210ac04dc36d5e0e12beea49cbd5fa924790a27a1e7b9f17b1c3faf2e7d07f11982d6fe78e9e36ac8d9633434d63b8f789cec6a1f752ae7b6a3bdfb13f07fed0c2fd6bdeb4d11e16b7bd2f15d6c9ef68b03698b0eb521c8cace94fbd779c45bc9b2b04491e4b77f08c4e868b0b902a65e1ee23358a4b5c0dff6e2ea1304eece6845f434a0400ab062073c32668761df6ce81be2fc9c564f404727b1786be12a1127ee78417efa5b0fc48b3540ffd70c8045fd93fcea28e7c8d0da14289e06441180c0bb2321dcf87deea3a1949660de176b73dc44b61e0bde6f903b115afbad57c1d85c0c533e117ce170c621bebdad60ef5ae68c8f7c6859dfa6a106246064b5fa0f1749c2f0dbf4179ee342924c0b3bd4da2a82f299275d052b0ddd826e6fa265859ae565ab94cee859d3937081363c8519cffe42fa2e2979e80895ced8eee02afb1bac7ef6efcbc55d2fe17b0ace8071c47db346da1818cabc467f095d7d79c1659c5879fb68cea9ad9407f643f7323ae8627d59dc6f6862da0737e291da1eec641fffbff24cff4f2856f8b1aeaace34ab8c09a4b29de26a02489d92ce8e70854cff3e96a5e1ffd465406617d29d1b4a2f583f48ec08fd7c53f3606708418b9986e7dadc6e97b59c34a3f5d65ffd62c5f6143e6303f96af73d3beaa990198353110baf83ff8c2971583aeb982830b548c33deed46697d02adea247cebd907c385a9b548f6e3fe9bd693979fb1b077ee168b8b8827b7a21e0ec28afee6c22b7cb85370a2802dd71f69e62bd32fdf71af7ed8e6bb4fd408c6e07b76a472b0983284eb16a2e8fb174ad224bdf026fbe67f38e5ad3beee87fffc3ff76d88c81b6559e2dea87b48e1ee6541576542cccde86be8cd24cf66968334fb39daf6cacd7720b85c8806f1e07be3a41603d83b4d8f4185a31406ab0c34e1fac6af9f1c98c788f550d025ade2a5a187fdd8c5a1b4b79072fc9f4b4dcc695d2302dddc006c6c341f68e6cf1913729fab113b8f49e43aa1a84dbcb13dc1d348318307086e8977a0a41cfb38605acf473ec405457568ed34dd50a44ebce679f4a6b7c5c30fe535cfe835517597dccf2627942ac6a8c9ae1ea540b428c766e820e0b398bc7964ec1beea0e9d3105ab745ac13aec53953fe075ea9aea13e95e01c0038fdd580d4311fe7a740bee67eebfb622794d3dc8fe37e695462f5259fd5742c52026b05e2b493fad99ab4ecdd3373cd1ce16c99d5f2b12ad8d71b9e0de4fbeefe66ba5d9039a5f733a8e2376e5704623a952688b4edfa62bf3f7c1228a75a33305015757c684bbb8b0e1f2584c3d314fcbafb4a1cdd73e043a88cbfc2ffa5dd183918e618af00331f0d7ace3a5525f8c2a1684551a8999d6e0d25524eac49819318a3350a38038d40317ad19ff0a25ba6f06ea7aad33793749b1285117f376d2d8bb326b777d6d2bcde38069ca9ca125f242982efef56acd568de02961bf53927dc3dcf856a6fc405c18d094606ffdb8beb1a50aab2680edba59bc047a72b1479a5061abaa0ccda6725e6d3ca029c49a8f0b067bdba29ed83301b15e293f6a1756a0d9ab9da305a8d7817cbce3b2a421cbbc196cb922da5adeb61d91a7bd5d48ec22a87d6ad41012fee3d3ebe6e1685c0ce958666606673b60c4957cfa03bccaaf19a72e8f72e36c2551027de87c4d30e68c241b7dca511db79eb5c0005422462f8817a9a61fbb72f568650daae6afdb118c76b9a27e32da74c47fdf40caf85899db6e849bd3dd2ef9b6363abf7c407fceb30f247f0d9fe09e05d88b6bd380884de9175c1f0791293f423cccdf473cc3bf98a94a020aa3c932a73a0fdc64b9b5c4c96b9b6cb36233e24381778f53e9fef18af4bf7ebc52a5366b4c9a230e5abf3775fd98710842367dcdc7ac7e8be9b48362c6695bfa5d7ed9424ef636ba98fda490fb7b2b92061be6ee561b1f216deccce437bcbddd0245053d8d7bad44327d286d362a8ecf6685a83d6e436eb057f4423961d0e27be6e834c13a8abe14f9aff275892cffa087820bafdb16f54a51f1288a96a2afd007a219ba422a66716497d9cd7982adf3dadc03ac03f6cd599d5abfaa28e2b9976949496f7926d8b08c428124f380ea51788c9d6fed8aa44feb09151fb7028da470296cf9696547b0fce056785b8f45a05e93e12c73547bba1b167df6654c67f3227b196a50984190a619250cf4683529561223d257ddc35b07b958c8a5c0889b857e4913e7441b14390e5dc199210c7bf7d8fa8d3e3e5febebf8ff1195e55416efc665a6aaa917ce371600eb4474e01789ac588f9e4581123491566ec8718271dd9a1db5130028b08d887cc66b384488346590c34f351ace1debbfb3d123fdb71b9bce69a64efdc2ee8c5b54c83a4e2c188ab3ae2cbc7c3fe8bdf323f8aac18712765f0ce5d4fd72004d9f49b62fe4aa1b3c99c7fe05dc4ebe252ae3a6781659de8edbd153e2bd7f0d3d37f34dddd2a1914f274270dbdeb1b0ad77e40039da7828d38bcd9bf77b9d068b6959e6d6888db3484163fa3c0b52936a4a5c7bed309650d04c1981cdf03522c03243f4165ea238f9677990568b1fe5faa15c519efd35187b36ca0d3749a73b315ea5dce56c7d8006f1b07859de9ccb8b34eebe736feae9e1ce3441ed6f6fc328b1e17a9a0a71698e6bb793be4d0dbaf270310f241c5c2d881181e902e263a4c152d1a6e4658700690ea13567adceb69b659f7cce7c85f5241ebeeeb83f3e3aa1db12301a5dad5d4429a915f803a421d7c42f690ca4a65c32e86fafd2c16b6544cdd548d797bc9588d0d64008c1073291473db430bb1992360ce6447d42974f93eb17e5160b04acaffa838912b2855ab908d8e344ae2fb8b58441c0ca73004470225b0f3a9bf9591ecbca3b7b44003570405a11b0b92eaeb89ab88e43a20e7c3eb5924c682e0e40c3417e622ebb3450329703a4f5e6efc56c3b2e8fd59a5e66fa1636e088877dbc3f464e31d588b2bebddd0a0fde7e3062fb4fb50e21218364b19e1c119c5c01906d4e0ff3dca04c7af0c0c0b69d086d4d362e86ce9b79a1a317cf7b187abbb00f8d188e0a7af1cd9cb54a9df37f96aebd55adbbf1e815802dcaa9783a45e942a1b0e0222722f57d8ba154deadf8501086ac9360def5b237ef9a5e51c0fd00edb5376e0f921a939bfe1421479721d273408f99fe470d3ef2e35fcff1fc0b732956649264ad07eb0808d0a2faf8e64da181eb3fc306fd35e350442971137dd3852ba74ee8d1eac3c6f21444fb4af5515412a95d6dee32bf6bcb2310f951681d40543214b7a004cf58852846721960dc8cd695aad6be39cc32f2c9ea3d70d9a5729c3bf52a87a792964fa4d611fa5c1663419f36b9189de730e7cba5d2e36d6684f0a5b66a6e0fdc4617ef4325da44454bd0d2748c79aa3d459eb2e0b50675e4820041e76a8a51c07e802dc81aff716f9e0e4b657cc62b77ce9ca761eb52728eee85284ee1f54e79ab1298f0b23d6ae5d070a53f9d4ea4ef646268ba3d0b29c683124fe68d3f4b6626dfb5e81745ad30c178b33024c1ff317df994cd12ee26054dd87a440f5167f44d7a341e9b108e1eb3278b388f13d430f1da6b8b865aabb0337ba36eeb41ce476b0e3b7614cc6880f13a4c240dc7ad19d278418e2d069fc1076fdfb23c79a887b4f25828fa430f59409204ec75b955a6209c07902dbb539602cba97f8c08f9ae32e8f4369a9eca5d621bcc52e01be9fb0fd027406b1f5299084c5bf471ab8d57d0d445289ab828642dcfa20cd1a1037af3680910c52a4821a7b10439f4424e78c63557889266f3095fbd61c9d9ee52f8d9e04ce09e802a5230bf76960695e9030c68672d75eee282f2c8a376d4d56a5424de4978456f9a1c8ff5d20f9dec7bed68c016ace1d4a4a4c198c355f4dec867704f2fd63126fec088a30dca590828cc62f0ab86b8244c97f60b4ea624257d25a8bc49f6cf6c2db23fc36b5fb89e920f8018eaae48e91b8172eb864d070eedc08f401d78c24ceb5db67b2680ee03569388a713c0cca20b15d59b6ccb469243d3b11b8610ecc26df17452871dd99c96854d15bd3aea1ee393c115305797d7dd87479a30bbbb8f070893659bcffa4a0b9e6556495d9dc28af691c9b60ca11f19573f7cbdd53069b0854930bbf212eae38fb55d3af96e294b4ee3e2285f13394fba4ba68e834f49f5d48c36c256c814b71af46a42af3681c912749c3aad3e312846d386cbc476cbb88cce3b1368b298824d4e857a67140b3c716b27a1247f8587f687f7c946f013e2f4462b30625a7e27e4ead7802f27ea5575ab51f61857c7c3d82e167584fe7a9a671351cbd2e11f8955eaf90c8ff52b7a679681863644f51a36e4b5ebe1c0bfb9dc72a512badb7cd402bc57ccc8dd209d1c763d4b3ec299d7e437542162a062d878c52ed6ed71510dce81dbb024e56a5ae15be288c3c363ceae7e48a079e6f508495771eed6a2fe84bd15be7f9cf28bd98ee83128e74538ef412a59070f398051ab6774a4ad59667cbc96839804cb18928b4d1bcc0b092ea8055f8e18cc1f240b6b451aa55760a2c817762f3d9c9b78ce56afac85fd1736d4ddedf5af4161f29ab6fa71dfaa36fd624cba45bad2b5081d5eafe1557c9d92f42c93fd5c27ba3b296e810689b49851e0631c205c6cca112ad12c12b7e7294f1f99db7addec3a6cc055747b6a169d4657963468c7ada8bdfb66d442e9fd9fa4281fa24282a6acc6755f41672676b689ae7029345257e716f3cc787a92ceee1f43e6c65e63b32be75fa8d4136948b90374337f2d75d3dc8dbefad3aabc60e6e1ef55b21165aac1f8b7bbb01a15d30590e7b756952a762bbe377bbed0656d5a882016dd538b99a7fcb5d334bed63e633536de511ea45d3394ea055a499ccc6670379e63493b8554e67a565597622629cc7f7cfc74138a2719405907cad7850722ba75f45715baf0e9e83c0ac3c348930d67cc1562b9fab389586d32ce64f1b4e7aea2f9c78b32be0f2a37995778bf5cf4a7afe82af3db6843fbe5b9ea488feb50beab53e482f64f1d72e6d861a9df3bc29760e026c66e56051b0746ca5185410ff4506dab1733ea480f5e358e0b6317bc507d804aa9e929951e5e5ee5fa521897fa31465eb150c253ed4249907cdd844a753df1790f2f12319a8392b8e7f2a98f69a3488bb703a531d0c29929e6101a43ed58d707d07936e73d50718c6719f4a5630314cbaab9cb6b8d4056181ba75a73d56b9ecbf04f5c07f61b096dfaa686f7715f9e0f160bf9706d48ea313722bfc3752cd7ca85c05a78828845b1ec5a486f284d1921e08f4baa2b1310b6a4d7ac87a9470bc4d528cbfec17c6865be95ad551e0966312dc51f6ae74e7d22583cf2fdda013e14221793417d1ac71fc736488dbedb3c313d022fdd6163dd70991ac65a2bd5f0edea83faa803afcbe9130b57f25d9f3f944eae1d531c9a63a99b22dc187493989e199ea8db66b438ef9fef34e6e63b52e1884f951769487e0f2ffb4cc1aed6ec2f1f8479cf9f7a0f9dbb365ace133fbf4cec62fb3bbbfdfb6e5a41682e81e83909a2b317def31b389872c0651c4fc0e25360a07251edcc3c249ee3ad81d8df4ee56bb77ac2f939e62dae2ce379d9130f90b6f7630a87b1c8d78b65b32751390be7ce5b329a48215aad97d05f70cf606e0a00d8a2cfaaa4f5ae71f6cf337f81a1d403ad6aed9351dcf2a9b9c70ee33c596af80e6942b18f3638c24d831ac8a45da73d1ea81517321a6baa3b4025485c2dfc8530d2529f854232ba09b7fb0dc8b1e1fd4ea3e9238ee87274b3d4e88ba32e21d8741c78b4cab0ac198eb379ab74f1c358fe9f8b0c0d32b8234305d72b7303e3150fe42afebe9102b8c230b6a33daad0dd1db35173a8adc33faf4a982cb03ea3758816ec472c6e6d5f93eef0d41beefb2128600e735ac755593cea29b98ef4f6111b6c655678549eb25dddd2f7f8f5793832bd9bf29910294bf01da6b9677204a35ffa75b8d4a44911a331d12680613d86ddbb71e828e0b037743e28c90029ada2d93510b22a3b142988796483098040a6ecc872998ad9de683cfa3aa1c45ad6ae33b04842e081d25a456c05390c86fd4719b1aecd7a3448bb37bc61da63444f30e623d9b954ea85d93d640dca93c068e63c969d016610686d40b25e91ee0f96452d701a5478d0e7d46a90222099bb17ddee0597d8788dd8e4ee9d64dfe1d588f23f62f6da5d92d9af9bcaceea0fbe1fd380eccb20228ab13ba1ab712b34ece1481d0519cc9a2fd7c1a5e8a462ebbb3722700033541b9e529ecd5606ce20156e4d13f66055cba40de16bce2bc7a19d8121c77589ea11a82b43443711e01893e0902aada920eb6a8d36c15acf1fcae835903070eae008792ad04815cbad8dbb64595a4cd60eec7a30c2a760f57ae0c9ad2d2a6dfcb9fe8d34ca63f9eae919989badea123745a2388e3af2c7d6bbcc7d55722195ff079b49abc42685030fa56cb7ecdceb49143142756202862b446de56fc9c39807e2ae7958b90b49b8ddb87696bdcfb60bceaeb3d6ea2707009c54cac167a430c339c7cdc826f4ee27dfc582264eebb1b1f91f943a409bc58d3efb7861635c4f5ceb9327a400119091388644a19e36833b82a551742581e69389948ed9b753e0a0ea8ba807e91d60d605c77b360352dca818e217c2b3eaa1b4f8221c02e5db4a56915adfc01e12b9de907dcba40683022614ecc0dd6752c358e6db80d3a14d7d4fe7d38a6702706691e99b0ae0a1394d266f996a026408827dd59718caa4a23e267173cf21cb3a1c8828e8f187fdc93cbddec8f4f18065322b4d8e52f2f15ff6e94b5c41c6c32ca75758c71d93d51083a02e5a513aaceb09c5e78b1b1b2d319e7b9691026e208a103587dde7b1fc2387f3c10fc36418e152f3a52c3e0bb04d92d89550feb48963ece99dd86a46bbc29be8c4661620768a4677438e1e6ef1cf490ac116d8cc0d9eaadfd5260783e2188774ddccda9434ac9bb0e54969ca0cf29ec91ce6e84acac104a6ddcb0b812a995b39f1d07dbc665cd0e88563c1200f0d0aca5b414d3e6aff5df17b31a0f6ce20f88d4741da3d34a6625300e0ec0cade44428d759c98609a160c6c123e6cbd4b2c1016383b893ba8b25f0d553f16ed9f1072f24a198f615bae273a959d7513f32fee930bc38d810da9db1855dffb7a841a487a9674eaced2a778f009c650f48b3db77a548b76367c17c07bad773b05f1df1fcb9c3ecc5e1698597fbe47fa42cd62eb3b86e28a0c57eb4e63dd113f89fda35a0556a3ed6056357ae051f4ac28098230de07dfea47fa34ea5ce2c58559eaafdd4f3e358da47b788bec7424ba7cfdc7f3ae877281f656e67fa4c86c72446acaee87a860f4d7f77a99a305136f7c583c06e63c7dd8817958711504c46abe8b17ecf56c4c02562ec624b9bb709d58bb94062db0e7e6d063bd86771b335b017936ef0a2416354891aab113dc4692b7f8daf0bc9426d5ecbb52e1194ceb596ef475c8d67903c5c9a27ae6eeb39e89f0844d5f5e5f20a6108b37dbdce0af90c1d6ca7a7ec98589a978a9aaee13df2c4869f631ed76b7998ff86888834b58c44c640338f650626fe35b77d77fb692a86c17798a6788c5360ef517761e774799121daf7f90c01aac6ca1af9ab9ac12850245ce059d0f26b7a2269b15bb6c85019770c5f93e52023c0b87b0b174cf3adb24a784fb279351d5591302c9073d8d07be9256710c1fe457c446724538cf332ea92de7992a8da59c4105ae3d308ab3214bde4725f0c21f9d8e6b932ad52c384162d255d1f1748c2abfd9a8b84ebf0f02c17dbaf04c41318bd74aaaccae54b3ff9ee02b66fd2249b7b84da9e5ab50ee85bbdcfb5a350f7e3220182359fa2bc3e51b20017362034a815d279a194e13bf0b8205c4a04e388cd412c2aeeb5a62a19c73d901259759720ce0541d9d2ba6f205b393b821a6569805991aa365a0e6bf83b27cc97feaa992644a55746b291d00544eca55d62c241e1c0a733472348554d13d113c0611d77d6cce6e4165bd832fec1437f41667b3f4edaad4640d11ec34ae63f0d005ba05b775acee6593482d7c7f639904964c75ebdbe181dd68343e4f2b1b98e1183323213d181121eb10034cd68ae05c4a5188a99f9c8794714348083840e966be77b1e7809c8ce537462a8b68bab0d673f13cab763c80f351e003db44d18bae3f30710425139fdba78ddf0cc374b7c3d8b06708fbf5836d93e9e91d08c44d4e9b7185f32811482cf33377cde74acddfcbd24e843502edde604c24d923d02055cf5b9c888578004ec2293fb870eab03fe23c80821a35b8ed3c626803ef5fd2db3015178f79822fb1cb5094216cce11551cda8d6594e4d69fb662206101fdbb5f28ef87e11bb641a078fc5815538180a9f7b5fc16d07b5f62395a60e0686ac044b4e122a897d335ecad8c3d4c2d5019b0475e0dfddef667f8206d532ed3fff627dba44a0bac71b161b98d6d4642b54e411bf78e8228878f5dd23ad9915d7e009e5d7d61eead79ba255cc55a38dfa4909b9786f2c20fbdb2afa47c1f35427888452ae2283a036f5bbe680fccec57d9866567945b88f20753351b9a71bf8f99690da5e2313be80e2784223a27ed8d88fddc3e31552c0450fd3031e8a84cae225f19201341367806688505c39e3751254195893811d50cff87fda1741de35aa3e5a935bccd1f8fa57dbe4c668efa68663185fc3c9b3cc57ffda7777b39fc2d0e97d00d2cb24d2cb800c3015e55a80425b5ca73f85b6479f4d145b77a6ab0e86766d2b6732623f398f8310cc08f8e887b74f81fc2a18d7c8b1b6170b5e1ab43b7b953f79414a7891967c3003b8110668b5b97f6274c2825c8e93d15927d75b800c26306646382c8df132cd6a0782186a7a03b65edf6bd03a4da287383a245971bcb4ca3e438b098037865758208a5b948121d9dd049f6a5e87adab57d29b8cdb947558db6d58e17de386194a12c8a4d5ecb179356e3da3b1289009257cfb837a15640455fc2bd43f099a2e107cd9a10b9b4e6759f8e7da665008df4ab3e2eab1589df86de5d6d26f32e986a3ca29ea4a4d996f5f50b8f71ec34e60412b706c6adb3cb29efb97a05b8ce635565dcffbd5993f422d313e57eb8e985241868806b91fb70b939a40184832cdd9c730330da53ad8e8021e411ace375be2a0464b6f0959a1c35b05587a36c21e073cc03604d6d3dec37089a639b4a28c1b5ff0c9df51f7ff58a2bfe364f7aa2399f0dcb0c34301ef0b78a040300119703cbac4caea1f0bd91f86bafd8e903e2402a42ba4138cf4bb947df8fabd7b4312f72c5ad70489717a70990cc869efc2850af58a97c2661a41c1a051eceb4d0cd904c697077b0d27958c9d2b406b2efe264c3a4864ec0900b7375c9b2546062284a40173f6bf3a7b5c43942b5b9878e9dc874f3362c7280cb7327df550a9453ee2c59c3217fad1d625b25632c7d49b73e276b84e781237ce053db36ee8ef4b83763dd052e74fc266a823ad45ca44aae4aeef58f766dd894d78f230e18962bb3203caca8dd6442deeb2f54d0a105ef7dbde90e088a4967198fb2efcbbe855a74c781ca347926ada8c21f5e3ee5707ea409c093f071b42ef93e8e9463264c9aeccfe9391a8d0b8b064ec0b4b371a9e10e1550c176162a2d02f081383d6db200e7d70d37b05b99e10ed2e5a430a8fa4c4828018c2957878f23d789bcb2bdb0190be173abc2e7670409f9cba374b51cae0bff80683d4a338c0d226676830a3c0260c21e71f767b56984eb3b19ca454a703b0dda6613d9f4299da170f7143b04cc3abc8e52960ff985b12c3a39a5254de31eb45bcc79b24b72bae34041c8e2342a90b8d05232276515ca976361bae8fc937cc562b3c901625981be9663739627bcff4bb7622442b2a69a0af6087a45534210fe33e6b1597dc3359e938935090a5cc7de48ab0f83cc099d86877e336f8eaf0bbbd6da869f1cd2972090fc889e5f59a7edf822984632ef8cb5a8f61f6b08c6bf8db92051d62c19b818211f1920f13830b1ac6b93e0aa4e8214c283304caee2d542581f03c8ae38fac4aa0c3b8daad9b762a105c5fb2e1f945d1ad84304fc667e9b6eb577beca8d60e6a6ad57257b858de3cb33136edc0e03363ff55b4855d9709645d04d818e33939b0d52ab47efa8357b1475d5277936e5629f98fc2e088f0cad71b81d2a744a7ec4317a1c5dfdc72a573173027e313aab662952ce2353dfaf3a85ceb8b9723cf78af92d3d4c6c017e0ecee42cf9bcb34102863710d4b96076013d7cd61ed3ee54e3478dd52d43c777cfc9b93ba1a979254438337164e3e5f4d1f29f0b51118b882ba01eed6d928dd228942bf2d2ae2451338ba307decc0c05ee452224a52b3f5e1643df33d01d0aada3121bc068e2640b0ba2dd028ab9c8f550daf875adc57c15bccb4fe400c3621b2538730105fb31339b022a92fc5950f4da9a68b6d0e6c527fc5405ed400ee7c4b38fd5d1ca6f5407afd13162a588dad1f4f9f6524eb33ecb0bf56ec32c10f939da0b12444ac87bf8848ed10ea909566a5be074f1aef01d7848059a03a5bf42533ef7e8c2755d4d26ee045a95f5812a51e78b38376d2c825c5734650b1092dcee4400a6fd857ed69012fa2a7ca49a6dfc26ca1ad74f63e6e1f5e3d9bbbf438edb7e3e91a12fc47f9c0072b743c84e297ad76d6942fff11fa498233c87fee203281a55e2902da735c9ea8332806c83d753e2d89257a4bf0e5a5b5829f43e93c5dd5df1de289d3ffad7dfb83f112b42385084038484581a7fa2f5b5ed1f233eb266843d4791f3b1f4450189ec10a91fcc3c54d0ef339a82ed670febbfe4cd8c9cc9ffaac00ac970032ee7d8aaf7cafd0620760fe4ce9b5fc9ec2c6b4b13e748d6816b27e54d3547f0f73939a61c9748495bbd5c05d03be8a68eb293c8c23635f663eb2b528ea5ba47a5f1435cbdc1e44746a1d59ba536e7ce02e3c79993f32e137564aaa56a49c0c1427799efee71f08615537503d7ecb77f6fefea1d7d28d2bfa0b965890729a00e6db31f318ac9de34d2d417458ea128c1f74583e0767ddac75fbc4cb18d9bff459b6e52a27fbcfe5348be1e62a1667e23d1542395d635515cd0993fb29935f1b25f2e0bc3830b15c0bb1a2fe6ec19b6255aa6c2bdca8903859adfe5314046eedf8f7c638f95a3f23718630f95a1354b7205b79649bb4cc87fc869d98dbc9aff11a0a2d86d302591f373bcd744fcae524040b76b0dc012fce48fe4d7f79ed586d64a588ab89dae1d1209e25f0542c90c5479babf81028db16f09a6e204c837a5b9e7c47eb7d44075ffbb53584dc36e9bb4180ed0bd0407f6434d921fe32a57cf5e06f4b8c2db6c8dd39657fa197584a1936af85b28b42fa4e143c4e561e22c94dd001dd82ca0f29771e463c5cde797301152635ac5bf35b3ceeca7d83ff7a94a0e965672d14a6910a4627652035bbaa14ad84b1ac6bbd173c9f8385aaadf8b0ec88826067fab038fcb9d359ee4b92325d80543ef1bde37d04b9c5f615cbedc50fa6c9125d011973020419bf2446a119ead39f4fb99f119a81d5025c881e0b0e40a36bee628a22956a685b765adb1812d7fcfc3313a86caed431d3a0f2a43fb9a6dcb0597813b2ba38ba44673d50b29d89dcfc39bed2d813e1c103e66211bcf11705423be798630c4713cbd36f17f62299044ade726a65f4471267df0d0a05df539144227bf5656cb249d930e18b1f59db5bdf71a392667bbc3f8bd0c288ba9fee5e4f6ea987eb445e7d51e68cbc584ae456a3da5a1ec0121af6efc99c6d1b81c5da2a1a1388b4724b721032addc18641d99573a293acf515ffd505d702110e65d828c77fd21628e9200fe060efce1e34220bdbf265ebd5986f193d695904baebd534c96ff5e243af23698079553d77ff929b6d489881f811da666fe88f337e9749f029cc15f90517df6f5151ef6da845caa4d8e85e9d7323ecb88a98fccaf3b3b20ffb4d9f10ef06fa702c7a62f723f4bf032e137845b9036b68243f5d2002597455834aa030993b696c47daecc001dfbdc569831a1cb47548c2b6b70fff9e48039bd1b431674b68058ea7751d6ff80ed48a39509ed42e43608e9561a9dc9151ed040f861239d82b35c9596d765fcc5024e775666841e16bc44e3ecac1e3ee1cb5ed6e6bea5d48c75a8e5035571115a774a1213e153a3eab18e214edb5013fe31ac96404be90fc6fda70902b70838c01914d524abb91be754baa8512d372c4b36cfc83c10a57047d19c6168b216d6ef8b9b127ffda778e028c94e3e9ed8431860c4bda161028c5245637377bd53bf4de84affcaa0b68fed0873335e3808fc216e35c15cdf0e337df9684e8cf08ec0b3206fd25e64134b90c72dbb0b96e9932d2873ee9d54bd8f529c930ce6310ecd123c93db5af13679662054a10b6fb36b47a771ee069e4b7456f5b262d9688143cd824e64a97c8861a173702e2c678d38d0dedabe0e8662f155ff441d9d0493d94b5e2f0fbf97283ade5be9040976f228fbb0718572b2ff1c54f8400c2ffd50e9b3cd2cdb3e74654784b994b68fa7a279773bfe22537e188393578e64c5d67bd1c1349676eaeee91269d58cb631fba7abb3c4e724f3628b6e166acb021d3416afbb890f13618f6b3e39ed0cf6122eab5c91a6cbc5ca4e7855ed69081b201573ad1b15d3c56af924e7177de46e51756117fd7878c4654f8a9cbbec852ef99e9de42acd7fe1a19a4195b6c16499bc37ba81c63bc1d4e23552609043ebf257083f2a7323ec2ae6bd109acf184d07bb516909a9138593a4a95bad4d8eb0bc995a1c0f4280eced59932516adbc5ae73cab69879f8c631c3f8aa50ed957cbb381b0a0681c8348cf3432462dcd3a1029ee7db748b719b7769c7d7b8861be57a7017444c3ad95c069438cc24cf8580e7eca0792a1602e52adfad933d7dada8816d1a0acd7ce9c01b6685424bfb15fc5db4d8c826b049dea8904f715088f4d591d26ab2a02a904fda1e56ab0a436b6aa3d925c8463c997b6ec8da4990e2f4b81951dce721edd2ea6c8fa6484e501abf5e4be84ed5d1225db53d00f6bdfc51d66c1c73951d92d9af99b74b7bb123a7e86fe2ce996654e50bc3974ade925624bc4f4d00160e9540494cb127dfe33e2e04b1f6169516475c8a8374916209cefd840da9e850cfc1918168691c47f8fd837c0365f5325d674848bc2b7215d81998ace9e71ca2902344b6c28eaa276b7905aa5eac5498f346db0a85ed22356d5aaa0132a644c9c1bee66fb5b1daf803c0e5d43716373bf12396ff1a8cf291ac007dfef35a4944597d8ebdfe9aa6f852c1dcd8c206b61328b65527c6728a0b8ddcf47582b7caebdb2cbdcc688ac9d0e96d701ab83f4128a405d44d28eca3610b436675e299c6b5488a2835b19a462bfaea1a464289cf616b65d2bd210706198d11841c85379614f2ba79a10dead0eec703a3779672ff17e80171d7cedeb407c708aa0d5d8eb6780e602931246ab725a899b7405e3f3764ad92d3f32cd5f8e090bb9bc3027028057d1cf062da42d789cffedc46c6eeb97ef77c2953d431cb9580d77ae96e4bcb0ff0b51dfcec490ba263ec03166f1d13dceab1e2930ccaf0e8b2d60b5ff196afbdba3fbde6a1c605c72f88a255962c0155bf31295cc204009ab3179ea207b9037f0fbbf2346b54b72073a1ce2539a6548dfd1e856e1d8d4fda4f7b092f46a0d33624f75c1f06805b528a17c00a71b0fd0473f28924fc17443b64a524dbf60921e3dd81677330b4ae69cce2b790ccaba4743c1dd613eba35ee1a19cc605e22627349bd7be8d61831f6acd85657b71f5251f8073bdc93d29b9754ac4ad0888e02f4b08f4574edc31f1c99960f754e9f1c7a1cff274a538afae63856209bf081d4dd531960a2f65dc325ac994429c7e4a496489bb17e5dc1399e73a07c01c418807f88bb2d20fbff64f0f9bbd988d3cbbfb7e931f3d172731b75e09fac0ef9ab4da60a8dd23838e8b06d4ac06a718d9a2b2ecba844d661aefd2c114e58971fe8188c3c517e0b578700ea7bf041e561e4f1eda04b2e801d40e1099a5de0ccfde5df7e91540261632ca5e81891b6d0eced9683b2d919fb0b8c61484d161c558d19e16ebd3aef6c5e8f41887b9dacf366cb3ba46197e0302bb4182fb6bacea1aeed27b82aff5a7a0727ed71ef7308b512ba181119eb162d0d717f6b3bef3c22bf56c22b865ad719888dd7dad6f3bd421a547fc2459db7bc33b69842241a4c4a9a851e3dade7a0fc418e90089de91000f60135226bbf724fde2251982286b4e7d41fb4e1e05e6cdeba799b5c02661f45623d929787af1ca24bb72e45134cfd2774d257022d811c4abc63a90988917e5a90c0971157c731e063e8c5f60074f53b79cc9108cfb1f6608bb26b9725d3f045d383cd8e60df5f1ecc2edcc8fd50fdf243cf08aab888e6bd248a8ae3ebd7934ee9f73de3e74327a3db5d696ca4301b0cd43c5d6efd566504c7f40f65e7969c951e049f95e803bec7d0ad23d255bbb326c2b0b7dce0669e9778bddf2f72660690141d0f8f6705d27767dc16763f7d1c4380ba12aaed033d4da3b3369a0069a9903988d58b61d94ed0227483ad12bd09f0a38399c428c7546589ef85306d6447664d510dafe8e08fa874927810c8d4295011e315f3b598af3bfcfd935426eb071b152e1fd03403b0dde20cbe7f17d7376521f781c0840404a9f5902dc2fbcf20de26bc58a4d8d56db1f07b0f6a8009a901a967531efdc4b0f142c02bf4595134d0765037b0f82d0b50ba45c91236d1083f936c88913326e70182bef7e26cad908f108b8a39f03348b3079129fc18c8d2e9e0986c09e16720175312ef632c1f06ea4753f92948952e4a100c01062f318b52df153319964b6ad50ceb9df82b7bff15f281cf79dc26e46b63dc61f84cfe302d45949b6c2b219306a484df4097026ea8c64d4593cea26761bab49d073b0172f221eb333d878c9c45d88ffff43ca68e56411f860ef44059cc12f14fa77fd7835d24e1b96471a88d32de7b750a6b139b80ba07577bccc7d292022c496647dd15e03d10126de196acb1500770e639df0a8cbe906f3b98f1fdda878530338c13dc3c0cbfa89998f4d4f462556e2455df360c107c594968049bf9b67c77177e31739c54b58a90449136d213ec75e18969043fd949636d5abdf8c0bc7a017d5c713f28d84647fa183524e8b052af35fd29b556a28dfcaa20319bea77935be30e3d10b48f5ab0260f81eaa8e34611bec96950e0f4042c7e002298c567b297a74e017759d183b1a06c553e88f4661d1a89979ed333a28b608b6eb5905051ce24025737a1f705e4ae020832fda2911a75381a12951d7317a3e27bcb32bf5ba672d51623297abb5915bc766c601b02bb3ebcad11ecdde36a9967e3babfaeb5e6da6dbeeed5dfebaa0bfb8573463d40606f6041178c8a1557058dfda83535df748592585a82bbff8c9de8ee1cc952b2ad2224374a62a3bec5d1eecb6e76cf0c92566a018565d7f6f79f8f1e509e17b5704fdcd049f971ed08a13bf25d6c60001969fb63c3a094e5229a309f0336836fbe7d33b6ac9c457acf1b34eeff94b1cb10c440daa1859eaf7b36788d8ccba209f2838ca1329bcc4e643c0ede4eee251d49a51f635ccbc30926b471254f638377b87eebf0122f23ee16da4478352e893158a9417a53dddfd89ad2c321a5c6707dccafca555039919c6e099de3d7bd6273736b6ae3835b94910b3cc6d2b25e384491453b1bb3b671a8edef402a3ac9916f7752affe1e1e04793f52c58ba15df4c861052a5e3f2a055335ab68f96e2343a9d4fe6cd84671c4bb7253cbd6577d318b606679b2d3acfb6e129b7f18c8db4980f73e6738ef14a4966981f302d307f5a311e63b30dd2f5100062488048ce5a08b0a540e07fc252fd02b26f76300650981a96ded6a675fb256f07813dda255f45328ecd3fbe2e8971526c04edafeaa7c50dff0173dac1d51d73f9afa5d303bdc21ca0384ea68eb68ff23e6745e61fd3125b7f073ea152c70b81b68304ec18b431248fb9ba30b00517bea189f2262355190d29aa1c3ae6ab93cdee290b8a196f526b0e6c778e5ad9f398be9ab7cde14cd47b0cab2db6638f0dedf93cc3431e46085e697aeb2cccdddff2a7c038188b11741df0820d96e15b3d3c7ecf4e779e2a556bed3134a72e233bddd9ef94f801190fe99fe23f3a74208e931f00425e76a0788f786c2d2965e20e15d095618811521259a165db969f57fa0db5a26ece966e6fdb8ce167a8ab313e85f3dd022b68f47df30203a2ede1b7242a9142397573576b42ed982f3086246e3c8446ab4e3ee378de575c554f4b016e1aa8d26e3c8b3a0eda4ab65a5f60e0d63d78e4d32970f26aa5b9acdd961aae975e1fdca277c5b0f65418461507f285254eb2c8b644268a803835813dbe31b4880a718b44ef75ef2b801c95abd9674f3a2a90b54b1caec4ba6493218fee46bf5d649aa33acf18fabdfa140dc0498856d35914301dc4dc483985c75b19ccf0ee61d805de5a2170d106283b6779413acf361408fcfd0a1a4ecda07649e83fb750885d0b7c2ab1cdb80630026b9f488c1e907a4aa1991d74f1da617a80288958e7e4bd08801d18e6e8d77162fe9e106576fb886f30411433dfa764ad9ab2da2c61df94dcbd55bba59be05ac290d7f92147c39986ff00da3b37582cdc78d0c01602447abb87dce4999e2c089a24697da46ae90431baa452816dc59bb2ea20450ebd9829cca2af50130bbbf631da8bff71d9809eac8550c61207e8bb65ad6d8cdbcdb375cafae40384864a37f50a16bdf14ae008746f3cd0929b4404ef5aa1123601ea539a5d20a3ed23f4823e68b7ee15ef35abbd636dd199b1757368cb1ae53a2c7cb9a8c577adeeec6a5d5d64fc6dd157798cf05fdfde602a0244381a87cf5282e39e0f37b814a5677ea3186a6f0f630b1a5d8d2e34c5034380d275c297cbae202e931bf88529c831dbacab26572f080fc31457af449a6a42bfc810a6f3900d6e6cd5eab6495cd97981d5642f1c58e8153001fe9f6afc1aa4772c6026364787e61880190516de929affefb79c218a3b1bc32514343f592753418ccfa527f8523bc319c3e31c5b91df53b04889e8de84f47866ed0f15b09e26bdd405a4d6a48b850536da3dd399d8a937475e76a68c94eaa43b638a8581f1345a4d62b0e67ce3faf60d85e176e7b57dbb8395599eb61c5d03dd27a9e95a743c8223a52ccf9af856f656b6a3fb41eed18927fa80120a3bb6aab9a4a96b6f7e4d5b3532f7bdbbf0b4d1910b6470460e7ec23935999c2b1756e232db96bb2e599f141aac83261854eabab5bbce7e5a7b6e4b088b762fd0b64808d3dfe71299319b5ae3365af92e77a8148a579d7216afca3571434a29bb00066f6294b467f2c09a337342046aa7ea84f1627568f932071ee2c450cd7a42185fe273f14944f3a7c3e449db8c0fe8487f08a45d9d33b575d39d42dcc5bcf9a4f7b27c3a8a75ac3fffd91b292c27f9d061db88e30068c4007920a6ef167794db491324ffafc987a79b273044ae52b79e86fb30aab9a3d169aa607f5ba066de8eaec577431e0194a0bebd09a23ff960ed7a0b875ff66fa2c40ee2b075ee8e0109d8b10731ed8a6b41a2b988e9116994e99d48bea851859f67fbc705767fe5a31769808ef1c03e32cdfb4b1260bde176853a7fdadf883c8296a83b235ce23a650d744703f7fd50dcdf623e903795e7121d3a0d9839253a78948daa5e66d48a47ee122cb3d75e1d267ac2633ee08375cd211ae7eff05294ebc2644a1f586d8ba1c1b0444eb589c2228c768255115a2eea3b0b1b0b11bbc852619c204c0589e12c069080cbcf6b7e3343ae6de5e51bb93ab4b34d71da73f6e2cab324c636f2df6698ab2ebc93d843d18596d09e5a3badf301ea5ca105016788fb4811efd3d82c877d76615d7f12053a3a7b7cfc470909e3d45d435b6f77a6ba9dc55efba77a42376b8709d24c17cec2d4e30887c0167a3d2aa7199bd884f19578c4824c0c0d1270a691316ceb7d653a455743fdf11274485cf2707c1e77fcab7f37bddea664cdd552d9c099cf2445a6b495f4d6dab6e69d3ac8b49fffe0e2c168e14e81962d3669f8e7d2be1ce66ffa3c7072df89a6e8653e27387bafa39855087f31c90314cb5d56fae34da7f91a57fafaf98b0320c9d6daa26ed3795713325c46c2a8232ab7c08cf86fe1f697f092b0fbb0387b39c7b29319a656416ad4abbd67706dc2867aa2c056302579300929d0dfa5c52e09bea8cfd63cf4982510bf103b159af877df2f2936f5234c502200044318c80a3cf565863e70c1f8d58e21eb1ea6cb4cf000aa2e5df6eca0f8e6a9ca9b05883ea8d6fea978ea9ab77b58be4493ac665c54f9815c9bf6a4b79bd4ed7ad723bf40a6ac3173fe33ea30070fc8a3930f0515d53536aa1e05c80a5f118b1c31897c65751f759938a8f9978faa8821a1de1c648b6ccb4b3a55a4d62e429f01774892995190cd2e44e9ed5a7bce4c676ef363939d3d18ec0b404d516eaafbcbb0113d782672bcc51a0196bd7d811f24bc8821f15aade5ce3489360a7f75703d13f7a3134df7a9a6bbada3bc377d8613cba879eb271c5b5ba89fdc1ac247565e589e544848fdd46ba4d560be49e536833333b9e23d4d97c8fb05f27e5422dcc0edd65ef0175261da01a9cdb051aa000428e94b8816cf974084c29528bc7a599b0204926cbee4524251144634e766ed088b0432b313204cedd465f70b8445d23c61995b95a9fb42fe6c7fc1ed74d7ec4f427dcc2901c0c165479c431677b3e697cffc24b4944b1c6d9fe5f85706642517b669ae718b010ffd965c513f8332aafaadce644be79a1009de162c39784915954dcb344a64a9b5cf397c89a93fd339953e9759826e958333d1ccc1fc283b668176b06cd75f57d4e59aa193f90dd813a826dfd089794428b8589b1fadbc018d5fe134f4b9685be14553d1b9e10f1b6cd729ec2885ef9f9b47340f2fb58bf687d8cd83350f0301bdcea43b38a44de28ff06227f62af53c64b4e484b5ed2e300730299d41964bf7224e4882c3860713b728a990d9f39a3629ca30d680178abacbe1f5bfa8ee8f488029b3923ebf2ebe6490ea46651cd2ed39ba4512f7dc7fccfd3189904967b98fc5324e1c02b7f7c65e95a3ec03979b05089e6ed4ee939a8849281798560564233d1dec545fbce847a50aa59e96d7f81d121c79658b2660273c0ff308454d116fa0bedbc37ff0b4766d14a5870860501bcb3e17683527dc08e1f253e22581600427ebe576709dc90c78b454785906edbee177607c130b61f9c93e6b5680134619fdc5a73fa0dcba62c8631489e9cddfb21f9c54568d8df210050fa1b3e3e2567b56e02c3312554941f5123ad0f937a016cd0ffdffe47b3c3b9b0a1b918f5316586ce5214428a4a2a8a5b570441e694f74a7993ddfbe21137eae38d2b1fb1a62d3ca19b35af054aec00a55706f317a103b7785c24f10a98e47549042ccc8c29005fc84f27aa8354949adf479acde844e66d1747788e8465f6f835fe1c5cd8322e3a0b652755d815ecf3201df11f908c55815f64071b3ac20f4bd529980f472fe32a31122ea8b071c4c437ccbb8b2b06dc8a19535d1247a25aa876c36a2ee02b7bad245cb2b1b0bc6a7df7f7c26a6b3b81ef71991426e63c19803d58e093898326968b2b49e282127ede7abb81ab7e0594123f25ba527abb863316ae8ae3cc689028a14f5bb16c78221b733a28dfaec6dd089188c2bbbe7f87109335eeb79719458a15b4f02959b98b84a5d40e0a15896de8b71997e2fb53f9d6585613c12ce12a0c30df89de83d848f9909c4bedec4dc4d1a1c40e1bd6ce729ef3ca768d23d4b570df1e2b433c15bcb99e0c2d30b9f9e59dd97dc5a2b3b59aab70082eefe23836bc6aadea0a69e4d2bd4f0b68909844738a72470abad9b740126bea36416929a53757704a7fb02cd316b347cc5f1c571f843ceb73d675f0eb2d35639af7c7a28e0bdaed3c3963ef1c788c2913c45a7593cbc65250ac34dc8ba27c0f87cf132bd6971d809886330b5858b7e933e9a36a77ff1233b0f6e86349ce5566c3d2c5de53e7decf3365c9e41e6d55258d40f3ebc658a7925853ee8edeca47aa34d413aec78a18fd7ab68f95a6d6cf262931dd06bc4f05f03a3af11f53a1b5664dd967c1799046bdcf6c2ee4efdbcd865dcffa5c98db332ec40b44a3077403ef735bfdfd80aac7827bd76c2fe42b19018f50a5ad8392b4d7bb446caaf4b27fc005b3bc9f27b349a54384a740e50c4c9bdd00aafbf1ac4c46dad8521376b98f8a19fe0f50f85a0593b7cb88768227db0a8848d38a296c70badba70f0d233c8a997e555e3037e5d8cfc0427bc1598f5c426929dc7feea6be214ef0a7fc294ef5936c05365ccb803ca904c468458055a817655797db16965d198b7f72fbb7ad56ec55a996ebfafe3857c796d44e61d2de912b2363e10f01888ff6cdae6db2dd95da714e7de082b0a67aa58650fa9b639351faf8eb0d762eb9dd77bba0dc612d960b1b17862c5da16d3b8793c13b965b6975b4c86650f3239cfd162c584880e713d0a5a4c7f008b10c836dd23f65b3facfc17b92ae4fae3a8fff4f950523672a7a569f970785bab61caa0e36dd7b2e5b2a3184a57f7bf4aa862b6c0e9fbf14071f911bb727f8d9f9be6b65172c7292c93964f527c8763c873e7c58e22104692476290a63c9ded8e33143ae62c1b6af0d4ceb2579e9361ead690739aa80bc3d6a9487a332b7ae6862fe9b6db005d6a9cc926d9bba35467353cdf9439e9de84030ed75e0220c7da615eaa7009bc17f29c3d6f18da2195d0a2cac1118ffe0c32ebc6a8b243dec662f9615e109083e109eb3ffd723b01121c1680c4b21a9e93a03ab14f19f27a8770d4273bf8b3ce491ad96021060b71714fa89bcb7a9f2b16eda709f51812104ed66ffc07f8231a77528ae4ff480560c5d627cb23562d8dc4990f0969b520ca1ab586044cde629940c3adcb1d06e3dd5cb07139ba60ab5a507714593dedc8832bdc9d57e49b128bec3fc7919e056a499ed9e7734b3dc4e1f993c934454851529acbc5da9958b50d3c6075d7a363047bf7be9487e42a89321621535f79a0c75c3f63255d25dd56878f391a5beada14bb245ea3882829936e5c67162c735525e823a305aba42ee46868209c0bd3cdc2414faea6d7a0f2dfcb6d8efce4fa2c4fe1659c1f9a3d182c233169d884a905047152ffc124cb7d1250630ed9ec7b343eb51474079edd90441c097250d98cf7897553d83f872e56f978c2efcf72a561ad0fa3feb8fc22d1210d5d5f7e53f86e758029e953c647fd7f1297cbdba9061a6ac16772153afb6dfb3aed987c0de975828e7d3322ddbc1678ce7b2dd10b5ca3e6cfaa4b708a916071008ccd830713362e720de02b200e6f8aae907583dc9df00959039060a29aaaac03f59d7c811e0e898d43070322694550bd98b70c421910803ab010c87ba5c9e43e0356b28e30acb518d6014aaee5682131c8000ef36391881640248834797339d9e9f46c56b042f4ff865ffb9ab50c7db5818caff34c2ac446b066cdcf90fd6f37ff80f9eed612d36b613ee538d2cabe8ca504cbf478ed84afdee23774f22d93d02674c0153f0413e956ca9df1a19f8d532a193769b184a5a5f6f210dd92c39662f4d4d1cf66e4a85068d8d2f0de9e7a097b0b1fcd46e6ba9c265f9478e7e56331faae5644ef8c13c1986956d289a00578a19d193dea16e679edfcf38a0bbde6443dac1027a64bb73a7ca0b41728c3312b46439a76e71dd539e24ad1d5fed20a622d20af2ea72789f3cd28cbf06076dacb02c46b310e82c4df5a0dfbd31da92959997357d7274c98f815687175aadb39e65c3f32680160610ff54f1c8d4fce420e7d04dc709313449e1825fd1732e7a10efdd311d1b2bd4312a30158425420099d9b6a94cfacc02e248b2e658252fea0a68a3d60826821d4315095e443a50ae389bf4a6688aec7c27e3705f3200a697c8cdc4b4fdde3a4a4e939e9a8b56a83271d54bc7032bf4d78c216902bebd68189a1449973f05155061d3b865efadfcc454cfece72bc278a799c5de966ebf9fd51adacfc2e51e762cbb809e96ac4efe0c334899a659ecb63eddaad35bb0906f01ef100bb775b1e8876e3f52a7e5a60f5aaee4cc0eb163dc909253f5d2acbdd906c4c07570d5fd7631656224a406d8d0675db224bf99992feb67cc819a54da989301b38c3a9d0bc8bc4242b71e37b4860690005f282a6cd079f3cf86c4332d8dc5b95939c1eb5f3b54ac890ca594e401aafc6eb44c1e8b36fd14baff8bee965de6f79af947a1404f231e6aa41fde49adb0c0f88745f817385f68898bf6363884d2558364b0d8de8a3c05904d99152af0a135dfe3e237d3d4fddee30a597b7cdc266589e58434611dff43dbf1af32991bf4280be95e6fde4c0534fef0ccd826fd334b06db38f10981e8e1e6ee55191211586acd83e9d0502f7fe0adf7c631e6915e8fb12699499607ed4ddaa99758dd786d3c80e47c3ab6a4a43f1ecf644f27c49c5e39c8340c107a775f4f15b4c1f938d3c6dc4cbd6b6c64f307c371218890d1d21af25598fa1d8816e4b028a06206d4e86635f2b74291f1febd921a682e67e8fda9fa7d4de3b2593df23c1517dd6e9471270b83a97994be9840eea38c7099e17a915183e8286ee23427a2ae72687729d3173c64d011e1b9133394605ad4f9a4fa496af0b8828b5466945e223d4360db3b0bc24afbd3aaed8cc5d129fccdc5b0e7ae9b9f0e3e4f7e10db7c9a59c7641cbdc748e531593e97b71c988cc4a144d64eeba31b89366c4f4970ebf4bdfee1eac2a894567a038fbe977eb4d6a09d735ad290f3eabace4b109920db9effb03680dbbf46598181186d0f3310f8c33ce767aa3172e762548f44fd811eba422ace469ceac65005b9769544f0a106e00f7f8da6f50a1b7600afd3737cc708f3dffba771e7de6fe71a2e41e3a545637b1029574c868cdc8c67c48c3c2f50faf480d17c34b7168c6936167e8042bcee9fee811200e791c80a798352cd4ddde682aa5e7f7a24e9c59ef1eb7e20c1952c17ac74cb7ce1d064340a0f94059e8a563f27017a146d3fdb93303f63f7a6b03d9f2774dbe6863801683dd532961dcf7f9c7e302c28edd5ba66f54d3a4c5b398422e6de5cd27074f9e43f8c03daa7a53de18e2e30452d2d8fec9ecb7f8848b692ae901d0afe1058adf86e552c866872fe503676cbb60506bf272a5e16236708c8b1da589e64585dff16dbb3360084a462229f3c444cfecca938ad3f1c3ab63f916f221c17e27fc6d3c35b8ad9a67e2fca6f2da9934d54e5ea918c5049b1d986400db9674924aed6434627b4644a9a09d973efa8e815980f27444d0ca1a959ab49a3b4c96fc033662c77374cf05fbc623640ee119be86562f2bd18a76f529ca297194c4db22a474b7f41e0b016f6b243000cbc4b8ec3fdc63f54a734f1c081492950fb6bbf47167b9fb3b5b652deda009387df88ce528e70b264ff8666e5f65bc5c94b8d19615e29b2450c4985bb410423316be1ab398dfb85a9344b44e77b04e2335e15d66111ececc3cc4c9c6883eff7e3331f8cb199adfec431a136c1e2d77676791ce27b097e35f22ba62b6213a490da9dc231f30f492b65795094362612c0b617e0b64a46c0ccdbe152a8d51239a9c7d7aea0be2550b1b45e9c3b787550adc922d4c28c9ad5e96506de697aff3f4ecfde9d4fd81966c08c8637302789163dd5d68f406f3507c6cba6bb812607fb79fe62c7f8c28249e372df719cf96c8adb5d1cacca90ace0cc8bea2e711c7de873cd426d70a9f285dd098275378545e95ac9db20619867f376646b8291eb5fdd66198e85d4bf6f5cd26bfd432467344eef1538360891d3965073736d5d4cd434a4fc4dcab4dd87aaf905783c3de29e93b6e5b83d11a861618a604f5505783984b8ab964eda0f9f3f6f56be0c80dc5f76dc490018d1b38fad77ef68cf35974957d81b2b720d562562af523dda11e00e8fc2b2a3165e45748c89e39979388af4b1ed6234ebef6b3bb6dc7e6a6d26be201a997deab0e5fdea1002cc82e41fb5993de081a2c18ad3cda4b83d52e6c30abf7b4b837d4bc73e3ac0ea8fef816614f107f03bc24f9890fa58fd121f4392f5124b0a715d7f40c32e427ac18b3e18283a4eb0dc3e3ff33268b716b1f65e635da267337256152f3682cfcb948cd1e10183896addf25be69fac304813bb42fba6fb75752df632870da8a5d073486c4b5b57f0acc7038c0fd086ae88a7038ce1c0a8672235578467db5327913d0a5ac6d95fac98b4ef53fea8695b2cebc23e7ebd66cb08ee785d1c6899f202570d57b9d97be0f610220e9bc7dd68215e7c2c9296f1b249da48ca13004550669aaa5ed3e11652a6e173505810b78313fe19cdaaef0db70162f60b715c66b7da48a010b61cbf27a51e5b3d643d34d5f5320c2fcf96460fc01b80df9cc090965d205f376a5c6fc8c137f7e0f9c99aefd9d2c526130931644b29897aeaf77d380b46eb669239be3da4ad8426aab32f81adaa08e5d1a7169e5496c8caf772e80a58ea97c795fc05c25ad2af9da9341b08b19f3d01b2ac2d6dd21e8bb5168f9cbd155e026375a63bac9db207d8a02608115a3ef5b0a8a59bbd4f0aed80ff721e457109138e07fd74d854c38a1001737a4fb35edb14be2cb3525849d63870973fcf191b2289595184948b17528bd3c49aa2b8b7cb6e4ce62cdb41afe0947e7f6c61172ab5b4d98ec54fca25303c5f4e10e21d102cd0a291409eaed6ee6b6666abef27b0da1f12b911f7388b428356fdbe73b273c45b848c8587c37b0eb326a57a68a45aab0b772d870d940ec2857e949ff941ad23441b59bef8d8c392a4f12313793d38cae65e4717eafbc0731d599d44f608aa54b5f47aa33bcc731d221c653222fc9bd1656090df0d5ea8fae415270d3c8f848cd5081d6d83a587cf27e2ddd3104ce1c03f9b6215bff578f2613cdb0d49b1511ddd6a4bd544b2d2b36c0b48983de328d0f8c68c09356f41d9b342601cf2277fb4a4f913a43eab9009717bf065fd77ea03c034ba84d6fa6173fc1b4a5acf69ae8c865a0d6eec3c7c44b4fdc97f9c8f6011ceecd8e5dd259993eb29b4f42b67c7c393868de2ecb49ed4ef073c8be8cfcbdfa61923c896ea6cb6b2f8654c2ee7e97752935935fac0320ff44df1d94ee8335c79764373893150d293f357573fcf9d2797ff20dd20d139d0a93b6a7357c9c4a9c7d4a8d14500d044752b1e9d6544ab644c535582539593cd62c77ea7fa0523d39498c2602fee193e5baac0515e996270aa08c4f6efd9b4fe7a4afc0f1987279843f70c6ce56a2e70d9d4b919c0948b26b7e8e1ace33ff779c62f9d413515e4288aba733c0a7e903412e9730957c0559dacc52071145aefefb69c0f3b89eac5e001f0916e8639eaec03f5756576e31e3c0ead061b725ccf7d7fdb5b184d08f2e87d9b17f66acef592b88af570aca0d9dc4815296777df335ba19f1427c481fb09f33030b41313c7b9daea41f2d30e4b2cf241383633db507c4f0f4d7ddec8c4b84cdfe5fd578697c0f123ac89f515904789d9c8516391f830f4bb1c87006806a9306a0810041a5a282007cac7f7f9c5df76e01a623b6c4508987f6527a1434604b85f54456f3295ac91800bb5a6d104f46a03c49c4ffd0fd836aaf50d90abeab0e93d036d0831fdb17dcbc3e683e317a9493ceff1c73e4ce7330b72e4658ab203866c41e5b8e9516ea44bcfcc8facc8086429614bc7851ddef97aa85dc8b80f436492f203445000e3f8ed727e7efec053d7b8f5232ea8f82536af9e63ff3aaecfc3eb19782c7de3db49c67931e280b0a9c8ac743bcb38f7fe79364f700c6bb5b79bb2ee75698ed672fb201a23d4e4f353c9be097e7f74f9e30c22692312dfed1051c0284407b35501d215b73f52738fe8601f2b3b8adc757081bf1b6bfbe75a2aef2b2fe52463374b059ae4c7f39f26cb70c809a5b643f4cfe8a837fa28750f7553a20d7411c33c033479d6e877f18415ee4697535fd3c842a3e880c5d5ddc6b5124cf256325260d6ed228326a07703c99cd8682f1d3d1b49d2c68c6e2f68c06f139cf8cdd19dfec5fde73885797b1e77c32f2581b5f31f729d36ac2a2f3e17fcd380c88fc311d456a408f8ad7c611919c169df7e6d5a796f2820da6045fefcffe35b2d331dcbde084141cfb3a7f0fdbe66cb07d9eaccb64a6092c439542b576bef2b9e9594c77e6981ff7a496e6df57b5a6a66a8b6964d109dc5b8dbdd2cf528f80f927b37b496e15ea76218c800f70dee2ad252b002c84190b648e7c8d267c675b23014e7e9bc0485d6c8efe1ee0ede625e3ae0f4560dd1c6559503569161cc658e4f8f2a416da016c379d65b1c75d37ac612969059aafb839258c50b4a261d50f8338f489c21d08362a98d3ac17097b8c94f0cafd30881d28d195a8aab6c42676005a91feaafa2ef4251dfc36433f0720b9c170fb18eec2742b15f21ff6b6ca4642f5bfa58b201a85276ff5735a08848e0e2e3f6e0c3082508d90ffd63c1ea3637812c32fb16d354f0a925ababcfe239577d439d00f13ba4a3980ef18f1574bc5362b52ed380059c95c050370243bb5157006301ed656097aabc838018dc3180f2d60fc88a0fc17eb7cf5a7d1e729b8d6df5ff7c0d188c30b091b3ec4a38578a87a4d1e4ed78768b4ae92cf14e7662a3a032f45e8ed915940db6137deba4d8d49526cfc05101e5832ddcaa8c21285d5e3655522f3d4ba4138f358be52423c45ca5dc2da37b7318517f6bb722394a3e727f40a59c3f1256c0582997aff2d4f48da7a00d64347d966b9e7b53db7a50cc57d40669b654a2322e0aa6b224ed82864d95b9c660ff0ffc945cb5d0544c7c6eca03d60d2e95b4ed8908c94605edbb99d09915095503f13e6df5fecd26a1b123f32ad3fba654807691004ff70d08a59c0e7fa2b0f41b2e788f4186922a939ff558d63f65a95a1a8facad5a3611bb59f66dc5fec7aac3e06f6c5522bffaac4e9c48cbdafd55ee93036b13318f8222b81f86ee738ed60fdd7e3d99cebaf418e1648168d6f30314130f999b97f8cf0267537ecfdc7b27f849ef88e23aecd414bbe4c9a20c931cf5b0faf71efd69edce676a12368e481f21247789d51745a636819b7ef7ce3caa980ad76c789d8e54e33c7d0bf6c98e067d854df372273ba5c7990abde709d0d1b74c6d1cfa620f2c866314e7e6f754b05ed60a39ae10892e46373da641b1bf0c4ef6a6370f9580de6b7b385827a800158846c6a3593114261c95a9484d987a24b8409d640e7fd1b20392a018655d8b711e7abe452d5774041220183afc9548a7f619ab2114612b2aa6c6bc8b71c41ecbf312aa50b03b0658933e47810bd247d238bdd89cf7547a33475058b0e5fba69788c097bc673c0845ab467f3b84b86b5046f657411208991e83d7b5a087c91c6ec787f1e48e5448408ec835ebd0e5fce9f6810965048caa3c424775f8e2e3c51719bbf212f1941d6d80bce6dcd02d9c21718fbe5299a8a9fdf07e75392e080be5e9d1f9e9fdc34a122a8a4af84a5035027977490cbc4f5ff39db2e6e2fecec8d6fd6047dcb4679c0a724c66ae8713ba8b11a185fe0acfc913eb0fe7b714e901e0de4e7b60feedbdfe9395a42948c5739ed9635a00c63cc487c6e543af641ed6e282601e736c2d5bf0d40e9719ba321c32e471b44da4cd0261197853dffc990d16cf81fb3483630c0ad812a3869c1938ab69ef35ea6251f18664c335765127b7397d2f4d705f4b88637df4aca5e93f45bdb55dffd64a185fb39083dfcd2d518c6bda335d81c5b50e4e2d31c9509da667a273c158c9cfdfa2a447ca3b9819042f9899651b61c2d823a5c63f1aa61793ce76497e3d0c4cdc3dfd52b1f53dbfca3ec3fc5c57851492d0526960cbce4d606ed181f1515ea2a610f59772330d097aadbf8082dc0034d093dc81c3bbc838b417447feb82bd3876752acd08c991e031798eb08c713b40729ef6dba133d72d4b575860d244d044c08a24b2607ec70db7d38498d5790279cc6d631b16df0e02960931a126dd7033c3c18deec845ebe813c39fc73bd1a620da4410ac146ea1aab03b19ed9824d2160bb4c0d328b243953a9d0ddad5cd54728d9a476161e76c5def62710e9a0f94989c6faf1b4706dcaf6fb356be5e6b07b6883dbaa5328c2dd61f600bd8fcc438fc14186cbd3f2451c78e4d926380ff96017f661a9fd53dbbf2bb0233eb919afdf020c797bcebe1ca346109119108f5b063406311244d3d2498390dc30e023e8a7c0de57aa9b2395b290baf57e2bf579429b45f77387a67b60264dbe009922ccfde5a39931ae09dc1866125944bf43cc50d5219288c9bd6e00e1347b20f01b75b175121b29e169bbd391baeebb6a1f902d361a77a39e015cd1861d6c88bc7630c059540b227f3e7e5aeafbbda0cfcdc1e69d8757cc5199d9b322604a545267da81305c35bda65eaea2e54a7686fe60790248b46194c79ea5f55afcfe040879ea365e1e536d79336a0976704deb7c237c4aeee9b2ac4bf826b04895f3c8b7b5f3970969e3705ed903673325996bc4e7f07afbdbac8f00118a186cc11ff3d66e12193c60d023e870c836ca60da059c311ff1eaa35b52efe689448bd1729cf48799049ed31dfd8aeeb157c93200fa95cb2e938311ea20cea90ba4b8a82cebb2c11f00284116e21178be7c714a9c3953d3167cc59c0deb70bb02d9bca45ee2556721c3d569a2a7fcd05167a7958105eb8d51ac4ad07f0450ade275f88c3ec029081b38602a45a2b56aadacefc50a802ece3ce1ad43ac3b0bd15e966a6ea077a82a6ef9fa4859a4da487ae5068ea800c9a2d43ef6dc03c403f980c7d6f99a01a10d3c73f2ac713908bc4dfe820013432aa4f2d2fc4203dd3142f062080b6c8b9f9a2e908b408b26e20d0468368c3edad396f182f97622ac44a6b797383fc5b16455b1abba90a6e1d8dec1ac9ac10071a118818c6df06eef4c50de7789279cef59df7a4a727e15342bce6e821b9e70e2649c59302246d3db718e9149b1dd46605efe18d3daffe04623f2cbfe8bb1b40f524d21cf04e08c5e64827b48c85161505e3dfd339650984a32ec3919584a41d15df48620a993dd71439c7063b99fb80e866c94c7fcab45dcc7a90a3724cb0b41937b87a3331f053642723dcc22c197cf53f09d4ccf7abe0fa253aeb8e84fbfeaa7deedfa04e5bf869aaa6708fa105ad5662a619c7c03c3c0911ffb1280f329e7de643b2bae7d57960323ea99359384b2b2a820f5ee836e557ccca29b9a643b7f005abfab28d4febb8afd34404633cbb3d32d0f7234189401cd92d14c238249fc70be1165d809c0bf702047a6af639d1b909210cd873f2ba4ed5dffba4057aec6c4726310bc3aa1b364380db600261fad0d3e695810cf8a727e7ef0e755a44213fe8273460f31b720e1c2e99db325a5de3fdcdeeee2c95fddcc5c1245663fa9b2a502c9130a51315e0d227a55b696aff28de208eb590824d606ec747ec3396f6d47220f541ef3bc345af1a0484c6da6374730bf1e0d55c034b0e095f09646b99def82984a8ccb0bc97bbe7fac751ad0a62495c9e75239054dc87ceb94d04914510f1f2404d5c2730c7f43eb121dd6a180a64ace686fb1a86cd32400f0a3a17b85678c5361f544c0837209759a1a9901ca101da9eccbc1b22b2579c56dcae00647d6647a4571b83cae36b3f29173deeb6dbc344357bb8dee90b33f331dfdeff6ec586e77322c85d3f1db0994b4e0a3a702c8305fa017ad862aa1e311936b864c1934c1aeecdd9a3f21bf05fd0a773486555f848fb30971f01a7f53bc990e6d9317260676cb162be083b4eeb84e4cf695adaad4d6eb14bd7a80d096331a0a34242168151674877c310636f4ae543521b06dcd9f38a0297adb5c49314bb352eeb7c8b60bc53399f8f1a945b8856d4efbd0632d5067929a16f58fbc4c4752b536ca9bb04f77d9342a71bd12bb8f3667b4b1548f73ba94ef34b9f9dd576a08a02e277e02844e7ad165317c7addad90aec96850a287d30e259c3777bfd32d02df2bb688c0ee03a92baa03dfa0fbb0e57d46acf52a694f60e35efa9864f5a66876d3a87afb0e52600767ce15b18c2e4f415ccca31650786435063fabf877149fce1065c76538bffec8e7444ce1babc059234d96efe9ac20b4ee4212b69ce848f23ccc854a9d943dc1c1abbbece775de7d69282fc9faae4d0f1ec486fb2543d3c0b49b8a1e9c6ec838804efbcfeb1a37d7a08cdfa5eb9dc1e909396709bac0e50dcdb2cb05da8d489bb69d64acd5ea827d01574dae5827f66680bf2deb85b277b6b64965e69b3c496e3d4dd73af8745012ee814fe647eefa5eca3380244221ce813c9c511bb9d4cca66176b65855ed313b964a53613b81f970cd0519580fdaddb9d007fc95aa01138c0fed85b11b9a40eaf156ed9cc9714575cd431a0f0e6b2dff000228ee6b21b787dc5ebb362f1e320d87b5cf34ab3e9a527370d1fec2a2d3ffe6522761c425656bc3594b2106b2dcb397b66a0b5fc3a6b6c62b003c241d8a4823bce0722aa84f67e3e2304c3838fadfa7f14f5057fe3332e4e596e0d0c2346f826937d86672259c67b5b974b23c18762804ac08f8f51e336f63192702b5561149f6b03f913105e11e6b21f4def0813220223c7f059f32ee30078f00a000e3add0d8560158e3587f9d669ac7fa4c3e9c53bceb6d4fb4de7884a5eca8d9b2ef3d5eed35fae1ef4e39c68bfb18d73d1b72292bc4f44d14d0e35516baf2d96f17966e7794e2fa8a7188a19a7166d7163ff1089896010284d357a4a4e62368fa220e3e069d9c1cab56888f7967d232ce47924da7dba48e87488dd61a4d1dfea72a004fc683ef46d497428f393d7b238bbc423574343c97e1fb997499c05f4ebde51337f3e876b394552326da053d1a7997e2900a119f4c63b855daaf1efd01a927670bce4a650400b01debb0aa5de7237ba559b931de6708bc96660f868942f0871bb0eeae1ed04c776d5d76fba096b2e314ab13a4e7bba98d0be79a653a2baf9f2499820f8e2c2c68e2526189b2356052ce27eda1e47c05be99b00f5aa0ea053121f10a8c9cb18ab1228ae7633d399df6ecfe0c6569409153596013ddc81c1422f8070092d6e41efeae9858c2c62efe3de19f068f7189f997589b115eac0797eb13fad550cc7bbe292995f859dbab121e23a40bd47d8789ab0623f69f77d8246d100fe9ee5a313a064976e2287e79c4f97d22a50e0d63b6bead7f19e50042fb47014452dc787b8755fc5f8302e98c71e6210851489d735a6c7a22c022e0e054d5e1e4a2b7512962c4373940a016c24751d1045cbd5a4fb418f49e1c2224384e25e04cf7ba00b880bdc76554ff7bbc8f055d9f2a700727a5eca761524c9317ec32f6f3351869a33ba25e7c940578f2cf10ff1c3470631b90a0b97b7f6d5c2339b49763c1e29b985b0e391802e0486d7e0bb309bdf32811dc99e812d106660562eaa401f6d7647b26f02454c71cc3cf192598df2268f3da43cf41eb339c99415466042c065dfdf1965c2559fa764c2390d06ad85417c1a315d39155b06f31ad4f128d06db1030ff26b730776fd01b6b6d40d15dec75b7d39f7e6cc6873e84f91b4770f8de905ba757f465ff36b7eb2fa215c16ccc05e1438151d1914a2d497e83fa8276590e542672b55595122498dcce022bf096d0930021ac85feff8c299eb6ba773e6aa0ab92bec07dd8e64799d66ecd717fbb93a8e8e0cd22fd4d6577f05d9648ea9932b3f746465475f92fcedf03ccc44e15aac35af658ef0f26076635e449df734eb3d6a0133494d22daaab16dd97dee6f40ab26da3e370c1470cbb2bd323ad0e13120b68d28ed3005353b397caa2b319bb3438c4d2935221e01ffe2ef72d3d197f368cbb2ac3db20accc6af94d98d497543fbb509cf41464a91114a148cc5baa218fe1b25c5ca426eadf3305b6d77f3f8f06bcc5f4a28b5475eda23ac0a890c0287bf87c86b761337eddf3dba37dfa8b2dd85bd34a59086e42cfa80e4f31d911ec3454ef013eadeac8c572e77c67911d7e8356929e7cf049e51bd8bee3085eecef6cead5b2c980b4f5b04aef6df95692a8be2d82ef1902ab7c6f1880cab74473b0e3c5126b11cf3537fe36e9818cb590eb963dce9f242b1504af54abba23433f81bdd78ef18c4f073b78c2db587d0fcc125ef47845052ccef51f436e9d9e5d36d97f0229b21470fc7055df3f76432ae5b7de25bb4f23e11858a3e78026328da9252aa3e9a2cb9e03c768048ab61936b437c7ca8a64f121fe332c975ae6df1530169a9a8aae32d2639e247bc64183ee324c98e4315c01810559f4ea2a22dd40ddc2a2229e761ad84bd3d3fcd82fcfc739474d0a3610efc85d95d52c48168b1c4953e04448982a0583f33e28067bac2c5b9f60901613d1ef1e52e77fec01f273f53893f240eea1cc4c1e9f6db2b0f8c942cbc5703dc8e877eb8d0c3b873a314447cf26bf1bc8127eea41e22ed5822d794954c0025f2b24e1540361472a2a2c1dfd91cf08f611c65da98032b3820c2294e6e308c6f3e05788e240053c1aa39874c29fe1fc5c6c7b57c9e9dbaaca9ccca380c6e18ed2f88d376f3f7b715c0b2659f7cb1288dc924741994452a4d6756e219c248cebaaec63617e3cde173550d94925ba358810051fdca9944d1f7bbc231504b15ce830cbebe15cfe1b967b662dd18dfc73f67c393ac55d4cda71743aad7b05a76e7601d4ba87a4606edee0c1fe54262495498457e52db29cbe574d0304df3a1e77cc7645a12d18d47fdb05a29c4203ec7d6e8ef8e156f9561d575b0ce6643293543d81d2429a2f0c5e1a66a73ca91b2b59e3deb0649a0d70dc2b7857c6abee8c7f09ecdb57983ad291dbbd829d64597bec8a7e04f5eb0ad999fc58ebde916b0ea5c6173882bf377f475ffcb11ff37fe897b508c2a127f35ad83e7a33ab1901e44abe250012e6d968b60fe00a2e18518291c0e8434ff9b57a205834d149ee45c2c82b1c257435c3c5d2695c15d41c8a3b2e99462d5eac7bbcfd109b681e256e50c1b05d97af54cc637f560ddaaaa0e25dc8999e5241e7b39677f3558a7656178edd15709436850db68dfca74f963be93d38206fedf1f4c50cc9ce8f978c9fa51de2cd866b291b51d47fd87fc0d94f7134325e640eb0e1b94724159b6fb9a4c2a2a73dc9a26067d25f00157ce78b464522452e4faa29d5f89a6a54e0f06b7a5b6ad234f9241a1a36aeaca68ae1fe55173da6fda73bce4b95d6d4eedec7c2c1222397d8b7c346cd52db3400a967f4a6d72271b074554c49c8fb44f89bdc0bc7623bfe348783f729b22e1477284ddd0d2ad075ee8e9b4e1a023ecc0455cbbb17f7cd77523d4a44e678ac2a09377a020d503da99aea5aa21419b3d2793cd2b3254a499123692091e15e5b1a4ee0760760ce05e4bcdb99b6cf6f2c70f532305fb7e6f6d2e06d16ced69efe330790b252e16c94cb8e502155340c2f07cd89559cf0f4640e66e572354139747dede4a7562b1a216d101523401db39013f1ad34ce84bd3d1a28b07ab7e44b8aef3f40638aea95541b118ccc62fc1dace2457615385227bf5943028d1ca499707aeaf1a2ee219d7f355d94a882f05b63cc2122ae9953ed3f131e364934af2c0a71800c8cd61e27b9157638b28fb682eba674df240162791c133a8b370022046b80ecd08d8d439faa38304a4975b1f4907d4fe2ff700be016b29bedec43567c61f2e9ddf41d2ba0c30837768fd4d8d55cef50dce9afa0ad5d757719a658dace0e587ec915cb7371af04e8c4884cb9f3b5a7c93e7c4e43f2c99c7d0007f74d99fd4c90fdf92131a1040e103e60259a1c81faa73d9c626ed4e1a58d481c65f72ef40fe8e100c272f1ce6cc05f9e063caa71abec8682f0530ebb648bdade658fbc6ccecfc4cbbc9c012d16d2ef3697cc0679c1132efa784616517ef5a325be8ca9669ae74be4f5a3adc94879e881a926c524f40266e1e83e3a478b3c1a3d8c87a02b12815f3b2ee4d40bd5f8f4c1b6da80de6d5e7ee96431e9fd509c4a02eb077c6992377e09894bc0db0833f03ea75e2f4336ee17d65ac3a9d6c20dfee8bb0df5c74487a53f015642a3d2d62a87efe4db05fa23462f3c3bd53becd454e2e82c8977a313390c6c6390e5b1b9bacee3582e6103e648208ff5ed2ae7728eaa88bd1564f8448ac328e4c27de15816e135eb66569fe7b697e6aed18db514a7ade155afa1dd061c44e01e0b90142aec51e7de61d01b4b24f697d68584ed372ecaac1dc93cc0fbb31afd6fbcf2a4a79254c6bef60f5fed47d4d9d0f4c281f248d36ef6847bbc4055962a54a9821eff59deb1bc4fbdc2c52dc41fb5ee3c7bce83a3aef32705dd31a04cf79b7377762a8ca9d14fc4311ed77389153158c0bcf6816ebf7f79af3fbc9bf7956bfa72872df065a5e81bf3a1903b7c64d7a5e2fba83f3aa34ac1ade47ae9f8a30ab2f602926d9b31a3693e876a09d620fde822512f1c142f0db338621da4108b5218ee3cfb3dc357f1a548357b0977d367f0c08b640b8fb9aae65e4b81605559616864a31f313f861b60b0e10c8cd55d99857fd6f2d28acaf996fa1aef203d4dd2c5cadcaa5c3459080d9835638aa50e11397b0eb45b001d49a747eddaa9e85bbcc48b389b139ae242bc66f3ac4b732f623c7d9a1749d853d549c336ccd9a555233c540850d2f52e1cf717bd12e762cfc1219564a7836216b98293dd686ba5ab171484f6d8c0a31f8cbf524e4742c86de8944b4a7fd093d44486082748c27a02d9364c0adcd70132862cb03af70db1e0d7a0163077428ab4e377051394797ffe2450f27c1e8c9aeabd761dec26372cf270f6f7e5dcb58e7b0cb08a193d12497a424c96b758c389eee4620ca47e8bb1037eba84ad5e731e8bf8a75ee0d325be96a23c7117cc0f1c8cae46bb8655187f71a997412e0ccea2dbcafee886bf74bfd0fa12333868a8cd1447e483cbdc68ef82559d35e6bce7354d4868419f9c62e8eded938630d97f27501555ce70149f1dea80d172745f85ea88879c27593cac78804e0b7c25c392e4adf440ac1551e783e098c0cca9e1ea557d2051282e669b241d289a06e1a18f09df2b9831447ecd16f9582a0500403affcd0eebbcfab27cd373ecc0b9028212bbea2cc22fc6dc414558566ca037c4878a48a2927a67ad6e540305c5c931cac322334ffffd70428d8e5be5a09e74080ad6e3cddf2a8073fde943f2b995cf23bb0828dfa7c7978cc2bee04d1eecdfa1e904f67b64df282bacdf63cfa93f336e145089577af046840984fa78d81553acf5def5d0093ff8cfbb88f3980bd3e4d6e987437496623beda18e9cb716dea91f824b46c63e28dabcac6ec5e00e618ddb25848b28ba6e5b998a1c7b6b2c93505a8778ef0690ca768c7c6f6e1abe86eb0a2dc6749c34977408f3877e8ad2931eeae7cacfe09c568fd7d806a4d108406a0e2b4e0d1826e632b99e4f74b4de625eea3256c388095058e7db0cbd5abf97321fe328caabfa5560e2ff0a113c7cc15546b8fdb4925d48d2869614b703d2d9940c22bcbf19fbd6ad96945614700a249118404b3637183a082cac6b93cccff84d5f6cf7336b871b2a895644d58a17117e30b7e278bb1f29690f07f2e9cfbec12f5195136697540370e5c13b54b5c06c9cc55615334bf76c04aa65ab5af1b1bd6dd6ea420957a8501c7c16702af41fe0110237ab4e67b7d1fe51c29a334118cd74152e8c5390064cc52607eed2b6e7590405c8f0daa8eb5f152c3f98cfc8d10c29c00ebfb7fd605f5467c1e3d3858808ed1a8ebfc975cf33ec95db859dbe21c32b28965d5aee39d9e7e4cd9f02160a64eee4c9994f3a96e9769c677c78c76a789fb23972940fdeeb0cd60436bf36e7882463cd0eb323051ef23e4017fb720e15665d805aa8ef7987fdc8f35a437290411cd4e0b397bc034253d282d0b742ae73bcdbe5e5ff07735c487ebf7322441b8ca0141eca9a7603c58568e9246bcff5b72470d5d3308f5073c516aba010cf03985af8ec1eb37f9ac533a96c13b46613fc5bf1187dfde9f335c9a69be627500c59bcb983246f98d92732468db4bcff52fdd155b763f425d9ebdaadda47353dafe55f65cc9d3c9e6383a6b8988cc13685f1f5dee39f58004f5e3d3d366f9592296febf1876dbc25ccfe441e73d109c2c4c8e578d378acd9a3c0ab5e8f106a9aa50ad1649816adbb80b66f0850df60e54c111ffce710ef70ee4dedc33d5052a0b8bc7f0dbd5e4b2d939c64a77662c9cdc844b270329dddef97807937ac2f8c2cb25e62fd842080ed5eb9791e26b4f15fad36deab1aa9050279b14b49a7d6c014d590d2dbcd7a200889fefaf4873fda154172f931b8dcba3061ba0470afd9fb97a68ca51d52929ecdded8064f694756a32e13f9184ac81a80b9ea5a757b51d65d8b23b16fc4aaf8b90d9f1b035549d3afd47dc710b9c11ffc4ff583c5a75a069e77abafa8af5721f42b5acf57195dc33322c416d1f50e04bc6508563c33e72eb2930973bb5edc1d4ca77338f3ce7c6ac0f1a9babbca11e58d66691733ad2e7d9d01a3140fa0fbcf1b9bbe9fbd29643879e068ab7a18aca7fa8bbd677d88ae17f35e11edf69313bc11468fbe208246e9ec02263125e46f6f88c33ee3b4de8943ebe69377805b7e4b5535a8e4da3b77921b3d937a067c418fd12fe7defa351afdac12f26637be0e6efa5e27ba29a16fa89f74ba501d863bfe5f4d3962d44731f68bc1d55518d4e5871140b92fe29bab1e75631f976dd86753973037483abca983397446f9496d5ea1748ded6a8f0f5a5d238cb051786eaaa0ffbd9ea0556011d9349ec93d6bf890615b8e5529891159d2343338131c5e85ea5c74a9375282b751a71b894dc638bf5dc753555bdadbcee7aa0697e710f210ce668fb41b34919eb35ada599723d291f2857671e7cdab310ffdfc0e0ac2089bc98ab4a8d6583b37949ad18af408abde66afe1927b230232d9efb195244f0e38372f7830ea72dec18f0acb7156b488ac179f9c6b346d901632dc2224e776274a8b13ccc6ef3dcdf2865cb1bf28e3d8a4ce7ef9fc2c8fd73c2e072cf16d8954ecf2250a5931f3a14399e98e128f7c86a1c3eefd2ad772a929414ce4ee17405b40f75da6046d2dc8ded37027cc577e655a3dc39ed6934dab76495114eefd146ee4dbb54b65a2b57272eb896a796fed54591de81a4d1d805fb0e9d1ff76a04c76a0864aade0d626875070d26416f9cf5848f0dee21eac2fea1fdcf1c3ed4b0be3064ddd277aa147f5b656bee6e92930a4a3d044ef555c924e18d6625624d03153537490fe7b12b150fbc7063d2f57a5321fecfda3a685797350c8581f6ee687c2db8a05265d7540387a01dffc8ffa3acf37df6a111e8c68f628890d4f6a2676961ccf1d82299d19a3066079595bcc31e2a742b99e70e79946e98ca38340f6f61eddcb6f45d35db7c5847c18e9c4bbe5fba0124e7feb9589429395ed86c8e4d62afd7eaacf3d679a2befc2db5b4f4d588d19ad875ecc1dd2dc16423f25772d2c7962f7cb98c57bdb3cab52e5ff3e1adcd8360f0807ddc7d55767d90de43795690168b9c1d0d8a0c472a896f5d253a3e7c2ed1b72244881c4306ec0ba6908e1defa5ca29a0dcc1543ee786308dd68941e3e9a4c3ccfd0aad0fda315c082e241692925c8b4c8bfa856610069418f9c9f2e4e6695b557da66ff7e242899545fb11882db32e185e8cc8c0476cd564dc019b4f6b236b10aaa89e6ae7ab132ae7de6f516e1938a5c82bdf25f9c67452e46271891cb92398b9418ed516e7a6a390804f24f49d6dd3be1a5ad688a9e95841c10a65dd45a6caa370ceb66f23051a6cd1ff65b9819800d48425403c4075646324e089d7b719b2f9857f6720a71fcb2feda955f4d43b6a4dc785ce221a19f07340432097dff9e6ab813f99eb42a4db1493c765f76e4907a751e11b14c9ceb04eae67d95c28fadcb5aa95d81e06e28a222c56bc954b59ab99e6195fc879cacc2463d6c1a245c6225aa383b822259c32a0a30e051934b5083eb43961cdd52d9cf67a4e21917a23f78fbb724ae53bf5994e241c01fed9b07676213180008b5964565c5335b6e3b3982818044d7d4fb47a75195d1e656810eca9e83605405bbbfa68931c365a772e301e46df9708fa07bfaaf435b3148e73dc7dda78463c8fa1884613ffbc82fbe534099cf2acf0d132b443bec0d052951c4a23af5009905bbeeaba59ee6596fde4a6d8dd63f3878bdc6478974af3ef17a712679eafb138545e7ef1041b0a975320f27de24f9d699ce415b24304b30662949da6ea8ac7c369c0ccf0406f5f21f96dad12552b628522c516e7347bd2527d296e38dc34fcce0c63471e86460ef6d8e1c8e5f6f98519028cf140b544b9e8c0c62ce9427ffec1a08e6e433e6bcaa59f3ca869d743d869dcca4b71d88c9074a0b453523b9c9f6f6ec77289518d7ef3a2c29b644277fb7da9e21fab5b88819bce720092955dfc83bb6cdd9dc74c4246641de1f8a8b6ff38134b369642a6e0d7ed75fc061c3d0627c19c90b1736a2553d6960a46cc3d1fc380d74c24a768dc9ceeeaa7a6597e3119f9deaa2e4eadc67f10c53d62433a8bd62db3522fb3145debae634c50b7fa7b4910f5cd3c66487a5f1bcea6fffdf14a9b5f18bcfe8d57c978a6329a391c808b7bd2e491c98904ddf05dc2d52839a1b4a8cd7aae510ae74ccc92fddb2a662678aa980746c1a6caf0a1f30bab62bf250b1381901d11d082f4e45ece42cf43771b7664d98f652380f47f437e71560aaf8ef8570849432fb44b6616a5aaedd129f39a4797a2881b906fc3432a25e7ebc5cf423b8e1504b83112e8cfd41e98a8ec93cf6d97b42fda311508bb2d407b55b3e747a2177d548e716a1f0efa8c7661606341583433d02b1e6c4e1fa151a28ba349a7fe0d4709c7c4a5600597212776f78dc49779dc60471ba25c4ed416b0f3d798b8a94c422d821cc55a62278bf03f199676b67d5894c313f5351f41a23c033208421689a737933e6273f886ec25af68877c0dd498efc784c896284f08cd941915d335ad70810010dea70dc706d8e9d0ffa9fe0d00694c0b808fc6ca61c262257fb539f4bc0d1643bbf1fa5b3a564b4b6a35ab56fa1d0442838793727096a7bb111173b5acb2b118147c66b2d92a6bd112b24b591af3907fa5d69d704e2629c5eb7d33e0a8c34af91c9f9c856005bb3320f7060a3fba9207899a0e861009ae0a430625085cf1861ffec9b8f1f71dc11220c016101a45a6467f538078d713b369f014c9013f106475194ab955cf8f2e20cbf656a5211b941aef58082d130d2b134c02fdba367a54ffc5331f2c77835f5a185ddb275729cab712cf777ae85903b085a65d872ae2ddf4305acc3bd3771bee29efde51993181371327692db87a905df786f10ba758c9adbc7cdc636a4d564e38ddd3cec43bba3f85279e725f18d1bfc8dd08b7de141bfddf86e25fc4c972e21bf440e1a30e5a561e3529d67b7dd7ca2b78f7489ba3d2cf2effbea6678ee242d651631b944c64580711c9ad44a2e0f538936bbad74ede49ae57d2cafe37d7c5398e43fdbc0ff98a2b03d608cf832a02c8cd51f9407d19a645601101cacb02a7a9acb84a9b8c6131d2e45f4b452702b1a115b20027a9dd35c51f531c65e1e6fc799135ff4f89c0be3de85ba6ae25450e043125f654191a67835e907c07a703063f5b51566eb619cbf5ebdf071221e907140cad83b37aa1ca3b55db9876c1d9484b1747db515cec1d12d5b313ee1cd71ce82cb6fac5bd1e863ef068b8e38b41b0e43e3ce5358c3ac044622a61e8d03104405755918adaef5cd30559f7c8b00f79b2be815058a714b3eb20f03b5df3d1d816346eaa97082dc4d1e88ef95a57b013849561af5a08bfd55b97373653f15986b532761d3f5944992e02ad4da3aea584797779781a35a2509bcc19865848f87eab6c91ab463bdc70193c82cad453e064c010e97379b2ad4efeb6ecb1a52d034c79853afd4645b57068aea3b3f747a44d5d906b5dbad5f9bd592a863320451f4d4d270f5e93cbc2e9af03d1db34d709e2d094a976750e529eed6303d75f2db543be83255a87c85afba73b96876cb9764b094b4cc92d03cda5f32b24a3c26b46396d0b633dcd22385814afe24af3b023c0d2c8d60e4c38b97813d60126fcd8c8616476e7338c6df4d37b0f087849f471d7bb8cf59bbec16d4b582707395fd4ac913899c1a89b18bc9d860c4d691170b72fbdf680f14aebd3f37cfc4cfa8e21165f628d61bb28f9ddcfe876906c9696ff68e63655fa0e4e50cb3c04ac7123b8c9ef0779b0b74128fafb902a0b283772cc0558686ef547c0f4b6a9990e3599821e8127840164f49a705a506a4fb4b673fe872cb0818d57db5f0e5ee675fcfc76ddd24b4dbcb628d8256f15973985e29fb759d5455f1f724789f72ebe6989235cd13d7c8ba4377e73fe42dc6dffea67fd0a9876d939c61a141e3d0de98e033be82576025668b258ad2c2452ca8cba0138c247a835a329e9ee4ed51560cea9915137a373920f199e681b343d12d813a1818ac259fe8cdfcac99112f85ef376317145563b59723f0d622d2a508741f09f6782a60a2181fcf7bb8ea1772d6facb9f74e2b22c1b49e66e57e28cf8281ebae22ec26ee938c09f194a702f9718f2a9e629024775f71ddc62633f51f06f873e3c8a2a6d29b89c2a17275ea4cb7e58b60145ed540d44b806a2e2e64dd01f09bfe4efb9c21065dd48e55608c776d573b68d91e6a16920785bcd9e8ea47f7af2bd5046532cbe4c6c8a0cb03e00ef72e3b9283a046a030ed7ada6d1cc5c036a02f43a4c9da0ba33c4691c62c2abcd8ec9ae6c43db0aa9289548a515c7947f44305281b3a64037f68cd3ed4cf9f5f2bd9a5e41c15cfaf935c560860e97a0b7567b5ad44510e3871196748faeb1c50bc1e37f79493a79e19eb73c74c848a25b6e8de39dd55e764ad41b98a28d8f29e8e8217376e957cf6840a84879996f88bacccfd25d97b94e7bf4a18a6ab4acf2e0babf22e86441aad87a03eb99a5910f2d9914a84cc4a49492714de5660f5622b13e3b8da3a064c4709b902fe4885ea19906cbe2f1eec87db95c591a244f3f35cbc4c416d1365d8e5c0cbac2a11ca0932c3d2b8956e4ed150b5aab523b195a60489de1115a944223461cd75215d0c500b467ee048a64b2f4d503919a107beac6b4106b0b9b45d1e0b241c7a29b2c320ea4a734883e2bd2b2b5f2ce4c4ac9c7ce08921b2a213873ec7cad0bd13eddc7345466747b4ef79caaedeaee92d41785c3041fc0e5bec9ff74bf3a3adfc793108e31e7cd066dc4e9a67ce5412d97c1b4dc96feb9bae547f44f2598a2c58962bede8b1069720beb560ec53842a5f1d291ca86b8daa945d430017477d7ea56f26a138890a0d6593949d94fb80c9d4968b992d7459bb15be7adfd9d2db7a93daffc726be925fd9486287d2d337312d72c658f3676126c7b3f8904c038fa82dd929ecb56b9bf41a5504f9df05e3718dadd84017017774cca442afabb7394896b20d93e539aefaaa021f7b92c9278d849b86632d1531955eb2c0bea387e353016c69e00dfa824e2649bd21bb6c99944b40e112764b96beb380e70181a9879d079410e1f142936dc1012763de9fcc0c6acc6c97028fae5c633832e4bbbe2588c581e55dbfc2d81ec08a70ec82b6f2b17c337d427fc8f5a2c675541932dd35fe3db6b92b2535d809e5f40ae3d638cc324f6106c2fba66150dc497de5bfd40c38bed82ac6248bf19bffd2433c90445e5ad94daefbb08e3fbc1a8555749850567050c451dd775b01b6ab86dc949ba2fc6f4562c8e2efad685a7d1888f5e248c9caf250582b71182938fdbccfb48c45c6e642b21e25df84cb79c9527771260e996db78fdc66f6a7265f99b380ae3255002462090dc9c1f533464a18082ae5bcd28e92877c15183722ee4cc0d334fdc5b20c7af41237904939daa90a78b75396dc12f3802ac7537cfea730ef3fd8298c831a3afc17493b949381b517e23998110d4bc24cba72ea5150251dcd6b32bb8b18b4009a5cd38bc37a3d817d4db86f1625d47655d38beb69f32f29947b523eaae0618f12f027c11d231291a0b858971b0164b4fabe8563fbebb71d15f374391000b25e2eb3942b1322255accdc078363c3f9809bd5c69fa07ae90b2386b23efb36db2ad50ef15f61479c7585d8374262bf667846b87af3d0b1ec788cd2318efffba577dd4961ba59e0a5b8ad0b9597d938b94fac45fa4dfd91bb786c1cd479672fe19af0a6cda05aaac3334b7dda90f53509b4de73914017ef60dfb4eb0b181641b41e0133fce593d9be99f7f7a4b001752fe281a950f14a1aef7fdd9a76cc1718d4e3d69a9eab35562f61493bc998a27272fef963607029859a5f12fdf71315f0b9260126dfdd60515b1886966d15f9ff99b7c16f26b35c603f6339141a782131fe4a1a13c783e249dcac6e68de3cd1098b0b6f57e29826b52c698fc2ea10c336f78167455eb13f5fbf5560c7ab49b08009971948c459094de4a8a2b513c6976c061cfff74287aa9a0441e028a70d589fa89e16839c2f02ef7a3d9be976efc8d69175c67473c94ce2529324f7e9776b15dda9cd8b1be790197eb268e3439fe4f3a7c53ddbd294338b1329646cb5bf49619d43fa088c004b90085864ccb0eda234d3f30ea0d00d9fe82799d70eb09389f2aefecc543b55e58f8b435fc15bc6f0210a10c7dd31ea0663be30bea728e90ea4569706fc9546320e6acc226cc33ef42b1a83fc07f0e6010477f0834b924bc6b0f43064fa99d625b4768ceecb827154dfd1870fb3cc1a050e5f28318e2c4c8bfa17d46138c5d2809a0e0cc17465f7e858ccfa702cee2d7257ea35961adebbe4d60e69f745185607d4b0f643b8204c400e905d751af85afab794f57735a6afe192c6a030748b608a3426b96b22ee1d64e26d47c8a3fc9eccf4a9f65aee7271f3d8db85eccee21fce3e54c2905c1cf1aa4fcc5affaaecb856a55a75702fea7d4f34338debd170b2a4359a9c35725a1c17bb21546efc89f7a3b89697b13d2b2dc781bfed3b94c35cbbd276449dbe4750021feb9b0e777ba4f611a58bfa1adaa6291a01c97fedc583260017570dedc1091f38d1c491fd74c02b592a28afd89a10ded53fdd835c3a90747cd10d71242ad01ffad1375677454728962490c52276f60820177159c08455bbe8e9fba5e9172f69196f7174a58f80fe3237c5d5e4ba89c11a3a7e81c449a95b9961a46321336a522129b44e84868f098fb19ea885f09bba85dd334d4f730c38315a54d9de39d7030b311736d04114dfbb1903d398a313bd19fa2796a3d3bfc8623a634266d32179b1753382c0c4ded1f0fba960041c6846159652c814ffb4ec04d8c550e70a2b9c60e190a2f3b408fdaa151dc8d9ff83be1986f6d52cd1a3c428f0d3ee6797b8f44bf0a8d274590129e732dcd10fed9081a1ddf6c377585d9eb48bd56e5c92a71d09ab7235aab3a013069c602bbd289a2ad75099339d614e593aa09c96376a7be07880adc6c9971187af5b02b3b26e292e4c6a66e7cfff8102f02775bab9ebf2c1f0a805dde5aad547f33df4bb77d715a0f369a73896abf96113172e6ff8769bb58a686348b48fc717bd0a2173ef3a86cdc5256ed2ee0b6cda006c25f3d1ff3116d8d1b6459526c866a5b113049e50d3933764b47c3fe96a47088a454250a5b9d9896b5206649a8382715f416648464610d6ffd4a171c0142ba272033b8598b0d4c66067b5f0319bebbaf8b81600316fef6bb7bb7bd19573123dc70f2036d28b36aea1a0cb915d6a6f733f51e683dad4b37f3632e94797dccd851770169271ff04d1de2e954740397d0ac2f2fbd75d8f542862fa9335c943d0989e46ff2774421d5870548325b3fd2e571a68aa74df3669db9d9b842d6fc59b395f6967bf3e4fc6417b39a0dc1d60c2157163636d7e8ba6765e71339e7f8176add94d34706aa4780a76c35c691d5df043ce1bdeb54ba8451c8631ff1a18bcf6c1dc7e9db746a1bf2c88ae0bd4367e070573cbceb764db7cf314573194ff322a52e6ee9d050fe99ba1eba634d86e828fed4648abd8b1da36d511d7b7f094868caf4119570e2bfae7f94c7510e480270a7a0901c522fb0dc3f8a5e31b3582254249fd1ad675624e1a54c79d6d4e30efb853aea9e6857f1f850ef9fdd6af6a9d0c700dfd35b0458a2cc9cce815b47ae93649df5d6cdb9a1233c06371e2faf46573f93461f2fec5fe6eee594a1204f582a66b9aa2035d3f021f85f9e02c0bcc48458f25b2c37585f1dbedf20428bb3e4454d18cd3690728085d16ccc575a415307b686cd610f29a87c9a92c6938a236562f6050da4dac2ad82f6d834695714581266ff74936fce46b6b97dda7b0341d1bfc57de8e5f2a45fa2d1a9866dc8e5685f485cabfce5b7c1b85b3868224431fb453ff5063fc1517ffa18ef420480784edc4d167bcf3c399a351c3278b3f43d59d0bc19fc0e1bf277675cbdeac58ab6a55ea86614fe2390cc7a34b596e39ac668a8a0b86c374eee0f718b57775c31a547209977c25f313b27b2716581e838ec582b7643a9d28a57096a1e7b7398bf1000474c46224788f705798dd7be50b3bdaf249b7ec2ea35c61eb49ae6082f627a9d342390f8120ad3e352785bcbfb23400ecc293f3d4be80e3b9061530b99d4974c88b5f7860662624156c483af26bc976b6ff1adcf5681e12eea067336e2cd726df9af4907781184d6f361c04b8f670653b44cba26a4aca91bb09a92bf9c05b15a0ea50a5ef7cf8a7e4b266caac1b37d72e3f6c7f1ee3f27b9f4de39d854d2919565581f09a3c34072f6a97ae505193c63ae7a142eb33f56e9ea1da7b9e1e97e54f46679440afac5e09563cf20c5fcd17d34bbbe9c990b67b9ae96e7f6698281470b59c9de82ab46270d33c245a4f655f3a92a7ccebe6f4d126d71c4c3aa17397d05ee98f0ca38394d808525f6c34d55c688b26f2cd4c2564eb538c35a8882974ce73b7c19e6e9da8c63d3681a4ecce8e358ac11fa42e605e2bf6e6ad684c205683cba22fcc833286daa7cbe05e433822f258455cb65a53fc535e72059b9e1daa10d5cf0f6bce0c11bc30cea4a4d70ee39dd4738ee0e98b469481865879b7b7a073a0e9b635ac1ed7136b42d608823b4df0dc5aa88c7f57da0fd181daf168a8343e6868c171447a033a25db9252578a5e8a1cecd5ef198b63ceff27099f3ca7d923d40e8e43d7c59b3dddba48114274da5f30d848e3e657c851707edc0efe3fc31e04b8c86927ab9d83dc805e2fba00bd01611fea1626dfa04bf87ba0de38bdb77eb61c2076003a68006839412bf4881e58904a935da144a3b1959803d1f575de93a8b5ca637a4079f0bfb45b3acb86402fb3ba79d3daec7ab3628966ac8d65768879299ae937489d1e33f735d08e1ebbe29f545bbcc07b7421cad77dd3fbb6340944aa3681971d5f03784e29a64a8ac2d80933b00b656e9d793b0a248173879c5b9422ddad8894aee6cecada5571a10ee8826b6665fbdd613c0b745c1c6d3193fa7eda326fb51ecd4baedd18ac7aa06973f0f44e34447ebc688ec9ba86173ec1b0ce0afcfa109f415b619c5226cc3fcf753a90428618fae8dca6e9c1b144de081086e743de2ea13763c857c28471fb8f98645f71856ef874d1c1492171ed443627a9aae0af13c051599a003da593cc09816857d7472c5234cd7e688e6afa8d7f749063e6e0f5ae7e21d13209da764a2fceed8b4fea75214adaabb96f20d0938c5ef3d1b17d9ad89b0e66cb78d52d3de54e12c5e150b7704fcfa831cb29f54bff554b4e9ca19d8a6532f6b1476fdf768f447bb0e5fcd678ff0514ae02b48fe5ae5d49372597096a9f30bcf9fa9ac46a35dc063a4c4645e2829def311522c272b5787fbadc533bdf94f75eac56353d2e8017d1d444525d59c36988fec50c1395c9bb04a9a91844fadaffa6b4427b27833286e95bd3468db9d51446082b0105712f4a7dfea7e6d96e9c3cc4fc0a9b427e5d0320915c9098e341d4bcf0f7f1aecd05aae3b2b74a553d9815c184e16486bbfdb042e8208ee7d23282a4dcc04628951514d00c7e9c2dab3b6ce54ccffd2660f8f7205c242de2290f585bb872f841bfc454776426e9c63904b1c4ef47a5216ef0a67e6f68a168e81947f4fa27197da4f98b4ad77ec16cfcc2cfdb13a61f6ae6c5ba06c3a2c306cd8773cd2774f75355fbe6d9b74d85bc518ffe75f37d77701eca73d6de14b37348b8a24bd3cb5844313c505d284c9a62a61e14bd98f710248a60d356e90f92cc9db11c032bd37c27c155907590438271f0ceb92f8bd0d632ac513ec3113e1f6cbd5f598bc02d75d7a67d832fd7c835a8b69fb44cc35ab3e1143f96bb69eb12d155319fb2dca02095a2e29f4a9b78a45409ebc7742cc3e4a5ff02720931fe8ec984fa4e41707a13e2f0cfa3d232abcc382124b6ca279ff9f3f14d03386b9cc7e6486bf4554b010aaaa0708493b8333c8b45c483a6ff12fe7b83ebccbd0c2ce0123cf7a18df08ec495730d1deb23e2f2844b72b0cfd7303a87a159f8970246030b2d02c98247f07dd5f3627b34e4bb2c30cbcb2da427c0a8020430d77fadd200863fe2191ff188ec6e9319ee665d35e8fc08769a66ec61e5f7dbc5a3dda9d581e6c591005f2e2813faddf2d9f6ef54bc462ec15a7affcd648edaecbd2ca4af0bd9cdd5fab42783e29a1e12c1965cb18bdce3406637130b962d47eb5aab2c9deb313450b83174ef6b6318824744add4e9653ddfda2e373409069a1b142f5aed16f8037d373164305e115e4cc00a4601f0eb6c8fdb0a6487c6f4f2dfdd8e98a7308076b7d98b4dc40c387254a1d8741d9c9bce39156a45b3c83004851cd57dbd00be8511926769a806d141eea2ef0eda85580ca355e6c60d0ccdc9695a409e73fc2be7947bcb763bd6acc3104d6a2265a2239c7af1f3926283f4acb7a822d2374e33721f7ecdbd3ac172dafc3292ab64cca1064895afa007969b19eee1a561b9f4cf99acd08504d4bd719378237257e78db9a0b2459b3465d128f08d75c8101399382c6191ec16e031b729a7ee06cd0d9afde494629078c90aa7de6da492111cce23826cbefc89c83bcf4fcdab3f5822de341b8625b769cccc6e67350972dc55d995c002201c11ec2c943974e34fc399c0aa1e7dda8f050a9e7592321f1ba6ff00d1d8f587eeba8597ce623ee63b92951854e60684a8a42e7b2fefc535ff8ee8684785fab0c8b0a538b4e52ab44520971054c4c47060f8c55390c63ac9646291e92bb6283091f1cca8306ffb08531e7259c65857221d78153a39c11dcb9a18537500fa83ca8a32388e45fe078eed3b2f739176fa6186a53b182d112996b9b9eb328de0bdf52f4e0f6ea7e7b4a5bd51a54e9bd13b0d2d89914bbb8eec8b69cd2d8335be5d9bed21038b8b6633ba000fe0075d064643f4c63d816a7e5630ace2d9d88ca4caa5be1d5270e8da8b627d27feb185520af289e5340469d5ea97d232a72a9780133dcacc6a10c7c9d6598757136fe1b192015be9269e4c9286ad6ea9bfb4361dd6958b724fb42765118a67d13703f695b28b824e80ed075196dbcf7c60294490652e8eb2d389d0197816fcfa22b77ebb4897ce268352c45439474434c6d61482ce1ce9d01b0988b2f4b416ce7d2ec7cc04753f3a86d48720f72a8c8fb3cf5dac3b98224fdfbcdb5284377d8227c2a6892c69948528c0e48395c3ba4f11d844b8d23e4b611c1f6d588472931830754a52b2901024c84728b1cbf1fac4a2f63861d02bac5b7c5394b0c8f1b58cd7992105a87650bed06c32b5832cb51cf9b8588ef045653cc5db33b0771256414c5182d9613114d19721df5142b985859e11625219bd6b6fdbbb8d20ff4d36512345c8edcd15dd34df69efe4dd9a8ee0942fb66ca35bebc4f6f4d4ff72c2c2b625bfec15b71f8bd2d0da935afd1f7f7aef2d7c752c9926e767db1c367f6778b3659d8d7508d39a22175b2887fe6d9667dba480304b5eba226a0d3ea5ad49b23cf998fb0356a7932cf0371e263bcde4eeb61bd33bcaae715a2d838e42b224114a92ad4da2fa13ddf9725923acaadca48337dc9d925e56d9cc48badb589b2d46ec214163d332de69000bb1247549e6d0c24d313dad79cffbb349f8ea163b7db57669e0d6dac8be2de051f9c80e7b6ea7f075f39434bd0f07e807c5b7604ef8d5c3fd939a659586c70137a67ce0826a6e6b18aebb24a5855643da58503284d7e4aa5169237741021e4961c9a4ddd6182ad60584355313c3455c263b0692707ec7f5b45fbee59a8e04cc6e96b7d80ecc1a2fd56d3b4891384faab7b73341966468cee989a93ccd73277393cbc73682c9ce94f6783faf59c8dcf6ae66cbf0702edc24c8bb57c2df7138f13ce1195196c39b0d6a7c1d811c7fb6ecc027eadc34c61fbcbcd28bf571f392722fbbde79807074020151e215778d13801aaf55b1c1e4fa960557fc600dec46993d5af7423c93f547501f77cc843fe09e261bc2c48ab622a7fe192cba4c2f29bb04292d89b034b690527272465983e3865294c2fbbc965dda48362e1ce9488a3804f5e9321e26fe30b69c621f2ad097d43b16e65825dfb664ae55774ecc14b6e1f40f1b477546a7c0b94c5c53989bbb630208f2d55ede86506d8255e945d13c4bbbc4d73f98fd3db963a2e30594632ddc1502141a843ab5d3537974ba6efec223c1730026f4e5c3aac01ba6b4cd1223c7cb689ba8d2a657965f44955084bd2243a8706484e4fd1a57b62c96bfc08bade2cab1af1b74f376ea3ae27f6010ae5c9b38ad759dc16e39a6a2b433aa4c1d8688bfc7ccf6e319668e35dbf4a39da9cea8e6dc601efe752a7da808532b8a9ffbb5b46c685e4b255a8ab5f8eb550f057694637fe20483772c08fb751a0274b0e1e06e642e0cfa66ee100a5357e1aa7069e73adda11534065f587c895fdcb48ddacfa12d9cd4b9a79c1f641c91c46aca3c36dac0eda7e40aa807091e6f26a4d679ce4dda0f8de05686feac52f5c96c1a623e68f263ffb8aba84f4f43733c6c8fb882458bda2be61d98ca50fcc8b3713880ea6b09b56a1f7843778cc0024155eefbd7b84464c3608f55cb23ca2b7d649e5b178081704e28d00bb306756e5b01e877a7ff4fd9625aa4fc94147c88abb3019ccffc6280bfb09c73ea54287f6d1291a2383c52d261640cb2348d2515d410cfc3040050672fecb6b5795801943f81b2e6b10e9a184c7f17b4b0fbc4ad577078fd8df4bc47726b2f43df42b8cc911f3c224b677860b92b1361bfc29eb4992aaff8cb1aa4ca4c6feb8e6c0d37d7403d42c64b0205bab885abce9edbc47082fff0630a67ac889a3d33b502be9e746a43b0d048957cca63518060b306cd7acad29376613de36f094072f26b57b4b8e7f4cb2d4bdee1e6ecca9c7f4cc15bbba79a876bdfaf4909e58f12fc5542aad374c288fef892c101de5d4382c4900550cc267ed7426ec983c121ea4bdc9e233c838e76e79b3c408b1f14a9bb1d595ee808dc50af3823087ae5e0a7741dfc8af14f1d8f04b0097ebbe37294a3a163d0bb29ebd74b4f3b5bf252985b605823a130e882732d82585ce2cf115e82df13c657a9a4cfac97e0035187ffe67c8f7dc2d4df2cd6b23adb8e8757ea9088aae742536d1994ecac7a32988c94074d7dc507d73ae5048255ec88445886e62f3eaa127dec38a9145a7f7b46437f107c541d96cf3d22c660e2e234d6a2874ff63939e8126fc5d30af815bb5b465122adf9b171d38277df87132a4e7347a4929d79f91687491f8037ec30071c3deba31b3dada43e9d415e619ca8a9213ec688684ac542a3c2d4c7ba9751a0bbcb2e18eccaf84fb607c9c005a8a7c9efecdd26990185a25c4d568051d39db9df791b2f97f1674342a0b1330c4dc3ed9ce9ae9dcc46c73cb06d84b8c8092d527854300677ac573c7ca758d075f876e2699d460d24ff973ba6d80daa7d3712fb6d7e8ab21204048979779e7fa947b03361350bf8404469c33a140e917d8bdf2af56ce073eb48a90d1037be92ae954ccb4a95461f6d342509d866be8d754db61c857ab018dd864c1baa1c6134beefe6363fda473dbb9014e5e114f5454626dd99c3f0d79d93eea724a92298a2877cc18c576d237e0b061033cd51fa423eb087887f1d4fb8ba49e9a6bd554ce0391ae77d5fe15b69ba4a6869e341dbc0147f67b75bfd26c04bf3ff95be3f35117e779aabaa56734b05914a0073b049a16def3a773d5d44c74907ba4bb0efb6298aeae8c3357de7bbe26da7b93b24afd24188b277e1b69791889bf163051c25bec67cd31cd9775f3aeb03f073bf91fa6e7024362548611ad2ead89fbeabadb5c7e01ccb1e671967a7b7110ffd02bb109c3355928bad6cff1c2970e227dcb5d20c56c0cb29249166d7857bb10629b9020d3b70cbc98e869306f1a0e42ea051f9744be12bf7a653e285a08b3658b583d748aafa267a3ab69082e93bb034332090f517c6b53541f8df264323d84630d32f97d5c7522247cbc16315ff206da3da15d6aebf7bb8860aa0ca3c85b6236dbd4d0f2752a6f33792f2b2cdc9c7a8528dbab04d0457caed0a1b0a8710142d26a14ce5d15b74edd8e06b54f9958b5ca82dd2de033573e05ca32fc9fb06ca435c06e6ea51c1b24ef80186fb95e2762e5c0c57774321cab19776b08c5435a940484385580370e8b112c1927945ba41df30452752eaf58af2e86745391e79063139de3f0c44bbe73bc4e4a032f8701e8666e4f60e3ae0c9f105f791ff345c50399fdb6d26704409095e4170f44bca1434a651fc6be2d75210c265ed5c0716d23a458bf48c7dde40d202cb8ab15691cf453bba1e92e36008c54e1c5553d919ab88554d0990a93339e228e33b1a408494b603503d3aa5f6bac2288a5bacf9187d06ac7bd962af9f58492b521b176b33318ea80a322f58d33e3f0f7b85aa7d5e791ca3fb6e60df93b3d1b5ed10e03bd7ad8b5e412055adcf27003e32adbd193948421fc8754e9d8635745975f013cdbdf40b4a73cd5dc27d7218688f60f78e8a86917df94b21cf5fff811a9fcb40dacbcd595fd3eb825d0615346de74678df071d98d10c443fa427750f55ce042bbfeacaf64834a8683caad6de417c4f5faf0898c856e40720481c57d009b44812634a59af0c72b286608ebb99cb35512bac61e2fa42bd87892f27583f3127f19716a79de958dd8662fba7b06b6092fb1c798dd715a0f6d4d986c897c2b6c75edffe4996d31c391af563844953020950b4dee95ed3f93bdc2e8c695ea70ebeb31930bb65caa2327134d0c46198a9b2dad9c8ecc990837348a914e545a66ce48b29c80651139e774ae4ad3e67e8351da4da1ee7c05591c50d134f082dcff5e91e96cd63657dd6be306ca0e4f927ac7ebb665382d9cee0f9ecacc7a86fa71a90f4e469121fc59b8d1ebbd5148ac2993d2004f3ef094835d1721e55cbe0e6f2771a9f8bcfb7921d3fe82f36c3142a6b3e72027c8b38a47964016aff682072c4c01774f6eeba5118e729333411d058aad0af058f6f7382a040cd927926f530a74fcf022049e69fec51485614aced7cca5fe899cda47845c16dad5bb97198ac58130f2dd08a80707ed172207720c39ddbdb27f7015d7ae636995ca4c7a0a5c6b2a3be91745e447fe4838d7da3671f000dd8f17acced7074fdbe8ecd21195e003cd0e7726671232fa5c0837baf8acb8309ec0c49765e7dc131aa0ded782e6d5dc53642a26f027f55db3a035588518fa71f92c477e3c687739496a88911f2baf08602bacf13624ac498040b2c9e52ec34eb65bb4c2e269adcfefac0b3040449d61422b6c620535fae40fe752f981a69f88d035df4c4abd77e51b2720134e2a6332da5f2c543c560c694996f866659eeefbb9ea522ba3438be6239bad095a49aa980598fe1c04fcd7c095358aa5ed4713be4b2db7046071cbdc0253fcf5e4de502b2b4c3b912e40a2ac1d2beb3c8f8f0fbd05e2756081644038c32f60f70b8ebd1bfa15586ab0cf7b51302fc759bda77fdd04215dde6ad3a26dff0ec3079fe0c9782fab9af96c2c50a270ec192b3f3bdd3c6bfc27276df4766466348432f0fb2a94a759fbdb0e068c6849edbab0051e261b8b02dd679ce168dad8130303d9f494801c5e4fa1401b9e914f554f69ece887f94f04d288ebbff97dceeb26da74567730798558b2332b8998efa0fdc2bcbe56cfbb5455f5e572631c5176ac4d3d55fe6757b084104aabc7b5b490a4bc0b6a8c1bb1259272c8f3b923e52469ff123e1deefde476357ceec07e25dad3d80833a2f5d692782ec98681b716d38d4443ce368dc45d781b5bdebcc792877604bbfdf165bf6b25da295ab636dae594d9e7da4af2c8a7c88cf419dda8f60b21e0facffaa344780ce2ebdc94d054bb2b847f4a144f452bfb426c7636bdf4faca13a6ce0391527bbd7f236fb513da76c199c3ce9629a5d0ec2893ee64304284da3eeb6267e4d53349a4e137d3c46c44b599eedd1f18351d6db350139e5cc6dbcd68ab73389db572a3654c123fdd394b45e7e87076143bbbf8e05bc496b036390902245fd759846b1f03599163e4939849071477222044c9e43b139f927dbd3a6896f596cdb0a12e439e0e92ef2e717f1024f95c9d84218a651fe08faf75a08a3a7d15f59d9338348e153618b1f374416d22f7b48c46642fd46d1a7113bb9960783009787361cf65c2f8b166ab7acf9cabb1a3dadb977eb7e1e6689620a64244a8f392792fd326d85cbb63c58874a4997ce013feb20d18a404ca14821c18c5bf63125e94e13dba9ba1b357247060798ffb83a7d659bbfd346e2c61a8f3f4caa5c6fc377a602cdfae0366338b08c83af23d6d59ae2871252f93c082aab0a2e26ad18ad32d650372d8b2d48f100b35414ff80f262a7de3faa9fa0615163d98687d31d61921079e4502d371462badadf5428018a4f59a22c8182ee19d4fc8bd57a76f6c695cfd52e526218ad6fa1519fe4c950ae8f3b862e39d209d5f0f41bc3890b842794a1a401c7ea73378f48fa1ba29d4d401097af44671c279b3e33e714bc6e0f04892118bc4127a1491333ab807bf8db4c022fdf61a4e1b299d6f6b524b1cae6175f7742c5fe4c7b0384161c1b3fe71f0eb440b680e379ccd74edd4a9005fc63ff23ef27c86126e5574d8aaf3180ef573dacc8f0ab582b6d12b6a68c6eaba3695d15aed49f049ee25f84cb802120fa74c802478360f5adab6a74fa5d39d155b2d0039edb27ec2847147693e1f07aa170a4605480aee0006703f6b4c004adb32a212b86f3edab554f397ded2939543ee43008cad411c36c92db79d3fa47f8b98ad3fc0ffab2e983dbe740941f8fae3412a3558fbdb75cf3f657bea595858d2b78a61fff5143597714f24de7d846875a237a94d89868f30e2117d44d8a51de11a00322d2709315d5b3613d4fb4e8c2859e23d15e91550205251625be4f3eb7685e0a3ce369bde24eb1de65babcf6b50f49508e37392bf8d4870f845aa1b6dfaf02b799dc6b37485c91f24b5290f2957a0bdd6278b1660c1dbe9427f27837c928e5de88f617c5b7ca22867befd3a4f6320652a390225a0f4b3a5765eeedcd8800c3d60fd87ef5829438e6fd4b4e0147764b486ccc3ccfe1305be3b6fd965c7de3583761af00d77ef8e7089a9c0bcfa827e7df930a862e127159eabe586d5146a5494d5944a553c3139d8efd78a6b7e457ca0dfaff40d94f2b773a83b8163220a386140cb8bba932b3cf24b621f94ca000670c447a66da83b14ce73547d08727921688e78fdb603faf0f74240a6dbebc2850ababf630385317ad3b24b8bf22a890b72d306fd199d9a257108bd833c4f9e06e0e94a9c1d5ed9d7c48341bbb8fda0ddad40c483fa3bb3eac87a9b7924e452baf7995d717fbe377ca96382099a0b2ca55349e1266dcf10ce7c1e52ddf68f7aafb6815d90bf095e06193b10a98d2a6c5db3af4f2e690304b21b91a851e7e96cb10686e205aeff1686547efb1976546558db1702ab8948398644f19d8ef5b447c5ddaddb5dec967a3681ff0f570b0fc3b292457f3ab19b3ffd459cb4b1639f44635a12ef814acd85951216cb16e3a2cbe811f37418f0f31c2854713de015b8a0892cb9ba491ca859ae9b79bf97f62ac9bb6c4c1f7e247053095d0029ab5e12b46a8f4f719e8f3de404ae1fbae66418574e1feb41a871fca873b351f554ae0b04814d765f90104e62e2314f35ca34e29b316fef09a159cb2123f22d478c127b6c053bee8b77030402745cd5f6c7749de8769c653ad5580bfa5a6272a5113daf9caad5cd3c118942a72cc39ab55596b984dffa037e970804cbbfafb660b4dee4ce9259e25db40cb5b1c277b000576ca26a0cc3be0fe87579524a68e07c4df87670ba938912e8c7251303c659d7aca50d09ff54a6701eea57289e56716f314857d9d34d9598ce3f2824fb141748fe6da363fced4ad0ffaed5be07bbca1bbe250c2e37352739ba2c6023238a53d34815eb6fc2191c9518fed1c687c3b2420d796a0621df4ff5ff0c47c38a82d7a1635194c59ec4f91a323fdf924292b122d3225631170b51b40a8ed979085aed2e5f4ab6d2d9835e942e976d38fa72b52a3e6a350975ba27155185a4c06f18ef915ce5e089a17717d2439d9cb42904a01dcde6ce376203a1925b305fd2871389cdeef0c40cab4441df6a11a276c8f9fbc0117203f419acecb5b3fd52c012029da99573e9691f68d16714dfe56df0a85e1e21528a345e1376aaae98377acd38ec858ecf21251a1eaf7b9149ac7c976a63f3e9d6e4e94a914503f48a82429705f074937a2edc6c08c68d63463a8c48597583e1195b9f96b43eb5ea14c4dcc4e7ae62f723436aacb0b9c6992f3d01dc39c7b558f9c6bba2380e47a3461bab60215ed9278a4c39d6eb1205467d11fe84ddbad70397063bcebe3e49c3fac5a876b300233992b44b79d3425197d198a02d2b59abc7ca6ca9c236c27ccbbaec8f6a57f1aa8752a85cdb69d1ecb604265c05c0fbad8d9b4b06aed00793a469c1a4a51368190be8faa835f52a2d909abd39131bb3b112e4045b8bc3e0a4298b7a67a84f81402f5fb460904765b6ded9f89d98c1e6d92af9ffd8c2bf50b539fecf2159f781605f824fbcc41570db573a37939df45730225da3fab37ab289ad8ee1aa2ec1106d2352511d39e77d1d7f0831dce5b814325e14238b3333a473cb0ac9d11eae8a9fa96083c5012c12d40189b7010e92bbfdd6c4f1d9e83cfccb69ec2a39c3db3a5273fb1f4f30190cb1eef5ebde570d9902bcd0505c602a7a4ea90e825298f819afdeea2a837e0c17638aeb6e1e0aba3fc8b946c3f515ef1600b867539c615b010b267ea69d3463a623a4f89a381378e4e6f1c8999f275bd082ed7e65d8a68f2995779b5da06521c5aa2c7b63faf00db78c8e69f287d9b8bb9f43da373f0d4a5cc4455b0745d09e7f36a7ab489d011afb47ef82ca0f87cf2a4fecab74134318a3b581de82d2b1d8fb612f412a0caed90b845212447efba26b025be3f7749423b7a176e9f5ca49eda245fdc7a698fff4d640c4bfcffa25a0941e1acd11f6ad17a8acf67b91068048a83e3d8e11243fec629227a2acd7283f56b32a29165e11b3242d759d32521949e8ccd72ac56e2bfb16b37c9b61801d26abb23d034f0eec31de455f1fb242e621fdcc5599231a7f7cf13337d41740900ae410ec6c646a8c077015f8fce38975e0a2ef709d99d4cebb73dd01d84a22f02521af01520fe0081ed21f24f09e6e46c4260ab3941b9d60bb64d68a9aa5a489d9d3c1911aeadfd1edc6bbeeebdab45e2ed73f08d5fffbc9d776d6387cf2e8e2fab09c746bda12a7e599fea9301428b5d21fdc6fb937f18fba23c2c8da3afbe396349d7db5a982baee2c5734047adb8eea12c6464a7cdba5bfb8afd9b99329af9ed1cdd298d4384ab747d503b649fc35bae6521e5f402d3a87a6b111ecc5c00a1c03e4bd140766489600767779c53590fb769b1c24b043286530125d6538c00e121a7820fd85db93fb77123955ba9086b7f3e57c30c85d2c48ab5d63d609659fa172c78b0fe69f59961c91a3a884c577574631b1ef726d1a1d576978076a2d1691634364d7a075bade554afb47a22f82efe3d0faa816efacb753c27a2d4d5e4963c66336fd139c6e4c22008566d9696074d5cc973f743b725012dfd1003f61c2e1434769e1c1ad50e6be51d86e6899ecc462d5692d32b2c4354c414888a2c88f4b767c8bbfd9a0f647a18f42f28f1f531622b8104ba62f67fb6d517bc1db0ed4ec954da89af493475f6ce49999e4ce6c86474834601fd52610c71ff90c2685be12935760fb110efcb19f4d54d2daf5c707ec008c57acce93b9d9e5fe639abec9db381c39ee826841892074d742c85a0e36aa1b888059a6fdfcd3f1d20db0cf04e4d87076253c642e214069fff99089ca5a3b73a614419f764a37ecc24ddaa7f905f8ee53b01cf78a7bc99af8602e8fb2dc526a42068e94c41164d127ce0cd4d6cf28707916101e6bdd932846359d1f0315a9c18bede3a909fdeb099b16f68a2b5e25c768eb64d8e5ec85c6d67346caa35a5611c71ff73aeb37cd3e514e1d63fa8ccfbf8bbea09f30186027763515d746d866e8b926908675663a64fa7412abc9000fd92d1795dc3e577eb77ef0a463855134141f982712464a28b8084ebb19e80cdcb3382b72329e1b851e4efd7469665acb2757d14581b19e663847af3b76017bd3b2fc95594fc7c13993792bb7e7086c26a54a4ccb99b957e6365d490d24bade4b9e022237925ea52f28512d7c639b6dc929e9287bd71001da7dc58d40af1fed1753f4aa4a417b099979bee29c5bb1e47960db9d5382d1b6b9f1c3a39f647f5c14166d3b6e11d68628ab0564e0a751dd627590548b2a560685c13b0c2cfd969950d902f5c0f2866754ca629225159900daa69432add4bf4806241e5b2dccb09f586ee4adf1514e3b2367cec810d91e655fac6471fa321fb9c494a0d8d3c1906bdb45ffc84e31f062991b7701f9653a0315402c6b84bed990cf86baeee5fb933b30182928cbe15c49a6c0e22330943a5ae70657a7e07a0210a7442c9a0124669731bba80f1aa2dbfbda41e0168a2bf407d35f2a57002a0a5d4fc1b40f69ebeccee12f969a57cfe65b273866c3ac9a3217d7ecdcad9811a06a82d3700344b74eb38900c9c7f0cf7b70bb1895716b010ed0ab2433a7d67f6361eec407618e4f192e7e7539b4bb5dc1a2916ff4e6841b4e707fa708a65bce089915e782b51cf82fe99a35ea959d1a374fe46cbc78bc0238f9571c823642d02ad61e62090dad6d2195a2541ea53e53abdbc35f98e0cabf8c6eb4f8604f3d543cead71fc68871b99c73603886cd4a0766196f6bdb106567acc0f4abde96acc3ed0e097fb04f4988319e14fc043e3c5eb7d3eb54c331d9ebd392e969b35200665c6fc99d1ec39f5fc679984641be20a3e201dedc84e78018ddab09eb6fa4606c578f4c4270fcd239c306cfbebf94f2a0741001c00f5e1c48f99149398e21956041cbce5eacfec1fd33f1d8ba816b54e0028c1e050c2479fc5b8e841f6d16d429f81d19cfeb5d2e666f50b944d0e19cdb299ecaa64eaecfd504211f0e9a0ad557697c246a0bfc82f86a534ebb6ad0b67e89e3f4cdd6acc1d053c5d765c2b61d808519957aafb38d94fc9e6e202d15b1f5e9245b06a27ee994afc497b6d5eb095a6e0ad6aa0113d57fe02e2d41f626b7c16409539b4f2019d69b33b0ed91c495f024e954a5022c587171669f9e84c5340d0fbdd450467e31e534ac3ea2d94d910a074d9ba00f50af2a5682539707d15a09de02450f94ceafcfa4a69ab572622caeaa4f7e95402b7a959f801844bf85b0535f09db502371961035c7e104ddef792db317220f56e34456ed56ce5f17b3de23f6bc131afa2c50d5fa69cd985c467e81eba2ecd1cd7db9b01040681a2b885722962747d5e22f5ac4459c7567fe2b1e0e5c95376ea2a7fcca8d93ef39832bab1de70c1208246e22874cecfefe9079f169f619a33407f6e50a1522e7195765c611a3eb767005b921e7156247cacd25fea636cf86df7ec3d1551f2e3075edec2fb3d411d101734bc56d2db44d3d0e57e8cc4e85a4d1b00fd5115629e30b22f33eb0cf86f36a976c49238437359e18b0c10db50766d51b8167143a5963b1d4439f75da050d13a935d5da3b21ae5fd74f524c10466c79e8c755b5e2b103c411db752d370ae4d97a4f95318583255ffd01968c50c9b5bf45f9f0182d56b5c01ac415980f60eced0e0fdf4306570da35629c76188624729d8f17b873ca6ddd3be6113bf45fba2984dc876427b83453d12be91dc597c664cedb313cfdb46d27aa3a61c3db0fbf0e12587c25274eede9485463ee8ac6d1be46f12e81d843951911d0e511d14bef586210fab0682d4c0a10136367fb85e0dc3e57b8330bbabac7be16e75964b5966c6a4e22045f11b01c5ffbc926231ef3f5e80fbf27807d5c2e389655c0249bb4826b54b7e0443690744c5ce55c6eea5c9ccf3a3b8af80ecaac2d4c5679956f6134017794929ac22efa90a37814d0304a1356d3ed696150a70a1ce5181a1a7b8ea2cdea360f84173f9326ef445802f5999bdd9ab7c2637bfcf9a1fa0638204287a8e0edea31140e16a00274bf2e4d4de4ac97fd29880acff18f10528c32330abf9fb6717ed2249291166b425b94c49231dee367d800796a12d128e6eef85766f5c191a1770a9326c9c4162088926e03f28e54739823cd6ec4f9cba4c63782dd2848c5173586a0d34a776e8768253b234cf17943821ca5d34125023277f86404c57baeb1fda0b58f8d649efc12bed774dc29e28d3472fcda661f46f0808a2e3cdbc5a090bf1989e307c63194afd286c9fc386351530fe40f0853f2657670059ee7bb01ad892ef8ca3497cf41352648b39ee683524220a892d7e99bec26aad71763f7f88c9f23e56776034ce49f176ba70468ec5a813dec07b16a8a57d1f3bdbcf381bb0efcbbf687d28345d119e7d6df44ed90475a09003df611a80bb3ca1d0fab9679be8e5d76fa2673a2ff6ba30105c827bce7aee1e6b4d715584ee22bfd0c66fc70753e1bf0316243f2b64c66ab7ba7853e00c2d149ecf74ba7921447179c2d0baefd6c5faa57dc82d5c2e9676ed58aedb28ac94705bcfab81e0667741d74662b0a36eb01fe91817b92c24cc97d6e2d281c8947b444de561dad76c17df105531f61c2219fea356fad1eaf6a135023aa66d9c183e026df06e332f9624fcae7808577b0efb14f44b95713ecbba6937c51cebad9c12efa3690b33ce9d73217a617416bde69b8dc4bfb0a1e292c5c9bf7232414d0194d5dec6a596edcf6bccb8636df0d6e3e9a151163cae3d9ea4d36f08033bac63f483fbfb33ac6c128d725eee5154b1b758e66495f09b5c1d25a39cf563c5e62c5e72f701a2f03cbb3a82de39db3813ba0bfbf95b3bcbcb15dd7c43814d9721f7b6d1ec10a3e6dc871e6f245bd1881a233e154badd98d3c946eb306e0599199d1637cde1a91c6aa9b01822379d9a00dc3c6fccc4c05cf14b4b46e5b702341ea1d5bad207b3afb54a62104970f1ecbcd18395cbab89286674a25ec70e2272859eb73e3c07dd295fbd1c60bb07c3f66847c34bf68faa8e17c9cc6fb4a8d9f01d6c16af647c73466cef40c7e7f545a5f52418d75a3962e876e7856f2aec40042531dee50c4bc6b90d1e134ad324377b5c490ead37670ddb141c116ead5c6aaf8eaa5c37250033c5d50457ef5f7cc307a4f594e4474327806826e4e6e471c96dd1e76ea65372cf976aca81cbc1f64c36bb7cf65fdb45c7d9f2d0ac2fcf1a9f3d985732cdd1b375c4e382e01b498830ea321af4ee1a5dfb925aae696dd74c55e63d0bd2875e72c3bdbd76485359e43164904782b3dee1f3bc9671bfc3fdf4295161ec29669deda68d4f6aa235b57371745e57b24cd3d8e4ee54a42ef4e17f1ed1393089805c3fc660eba7dfb0cced6d57a831c4115442ab78209a6bd4cc3cc8ec11eefbd06953c465f65f830ec570a390be8cda71c1ecd3196b30a3cca66bf16231a1a305cf38e4149aca39b46c411c1204d26bbd2c507db8d684e64d9b29b1c17ccc2bd57a32d71a03a733c6c37be0c092cd912d8dc06732c9f9327900aa4d7af1e55d2985ea32b99178f4a639f46efa36ef581cbe8155fbb2c55047a50a2721a28d9744cc50a5b0f283a72c8f603d060790fb809fbfd2f1ee0a0796f42b4c8147303e2c4a34a10469a9c377f6bd5322edc335c709f62143a0eb4da4ec44bbdced43f70c5c0bd9928a334b8d39ff02cbfcbffae04024bfec09d04515963ee63b7d8e406e41d2c10d21f617d8f18d2ce75b2c8397fdfb08d0de839747da019b74fc16569e6ffc80534c52a97a80004e1d7e59b45d2b0dae6ef0449c857056ff8ace3455fcd5265cf107db58f8d596d2296290643b27ce8b1bbdf52adbf77dedc4256d2dbbff07601a345e7042149856112a88bda513377ebc2213ab8e6a59373e418b0b4dd78636d59a7cbd2c23e9b9a0cb3dc3b660a5ff74ee5361494a17d1b668f683f6c1c6b5cd0131f186a5f4459c6a8633b243015a11ecc4663b8e6940c385195d8bd585f58b7632825a25b40b4708797013f3dafbae3b226cca745c8cc91accd18611d127da915bb7342440522582d0fd916c92aeb182f4a1c2b3daa429ef87c0c528ba38f86eabbce136849e4afd34861cc0fde8dc1b48283671a60916f08a605146588d5fe6d8dd6f5167c110b440cb547188bd1facafab17258e4e5bd391cd5a1fbed6cc996278e01693872a3427731bfa85fbbb2fe8eb119217920a0e2b885c9144ce7c72696c28053ed73f75d741426f4a6f48c6b82232464e0f108d4cc44bd75cc266018d40117b3cff5678f50aeb6a5ab7c034bca9cb0366f413080276ed6770448fc7ed52bb435b7dd04bf8e5dde327f07a9ebbb3337a746ef4f46f1e83583b20fe13fc08346cf13e9cf0d876f5944dc2a834dbded61752f4bcada92c9e4a6fb20e58f62458dc7a56598d4ec73cf096f10a9d0f1a7288b9a204f89e5047825b2c2bfa9e439b3bd79d5e3fa761a1af8597c277b3461b235205c61633ba75f0d29372829e138a4f0f9706de7a978f4c5cf7cbf76d8950d0410d2f9558e0aa9514a47115087bb8b3c7229bb6211263e0801101575ebe5f118815c5d3a7720382fbedc7b43bd7cd342381a9467c44158ebd6823cb93cedafeca86a8606cd56dadfb87bdd0597828d60924989ed5d6339237095821034c3138d771fd868591d5eeb450f730a8740292ff8fdf78e0d50cbda3252c4f85def4358b76a4b976fc7f56ae818484af717a15b8ac767089d3fc285bf5cca78c70fbe6e65a9f4d783e186c6c15df5acba11dd7a5519df8256fa46170825468687babd9ec62843e536801ec57069723c3f52cb85753ed931d747493206158c8b9b8d210778b562b02115cf26f242a2c3b796438b95c77463aae7be0f0bd24c3b417fb77472d4c633a9d9fb2caba95a7e37a795e2c39c42b4cf7b10449bcee67ebe8f072b768e08e63c55699da81758432c1c6264ee4bb4037b5d28df5c3ae9567f86013bdec4e47d14c6a986236023b56581b6fc3d27b22385e390a1c90d693e3f250928c1abb0c3079dd5da94128224d13c7f9f3bed341905756b840edc172596801d81e6b78b72d95f63a0b38ebb3e7c9190e32f1e245dbea9f926e4b2196e9c0923421a5c81d7a10851c0591c634e87d6487975207697e2d8a09eb842322ec6bd2aadf5af55c4a0335c6a0ceeaef4708680bd7bdd9c96e110ee839f2dead0f68c9a90c6b8088b0faa214e11fa0fc04ec9c6a639bb82db54827a7c660ab0a1734bf0492e5d2bb90f59c0cb1a7067534802deed4dc255470942d01c03cab58a0adb9cbe4860896256ddc8a8ccfaa2607d4096b4d63d339a5c0bad4e55825aa6b77d3d42e78b8ca4b5dfea1c9c7a9ca22a728fbeaceeef3e7409907975fd77ffdd54cc16bbb31326888574acf54239e58fede3bb7d180b1a8a8d3f2f63a8d3763e01ec35975f268dfe2ce2017272e04ab40aa730e3ab03e1b33e8273bfc6ba6008135f15e8c4890da189ded9f8418eb5e85cadab95c61fa36bd364b6f4825cc24054dd535d63f9c02e18b8e131e13f352109382ddac6ddf8b6b2f690e94635fc0b1e44d100c99e6b0b23cd3c623304aeec8a737fdb1958dbda754d76f6fd368ebb9d68ab6546444c513d56e92b4f0b3f76f598182cb6970601fefc1d0b17ef7eaf1e530fb83bcdbb9e1f8e61130582568c78f5d4fa68d5ff776b6fa0459bf1a5bea6137127b152122ad2393a49e1cbad82c3e1294ef45cd17c73b17484a9f6280f43afc65f6170a2f7f4528abf08b818de67e5e048798e4e5b6a5c67a85e18710d7f755079d7fc90640f166ed773ee1088018782ab370103aa3b3056369b8cf7b4a38edf529dc1f1fdea82679e20be13d43eb05ab9cba72ab2c681f3307256169e4258629f3f5c3909db1afca06845bd443cad742f5894df915a25b6b817aa0ac97c6607754fc04dd00c8a112a45969d8b42127fea60f9ead39efd10a70613a95afef66a2a39b31d79d86bff3dc7d26fcdd32b9068c80244aebe2c3a2ef248a4069f047e38469aff8d0bc4f3834dd9f5760ffbdd2208770d493d01e9b3be138ff426d66049aa476972e351cf9911d468dee682320b23319e133679532e94accd3b99ebcf173f63a7c15a73e23fbd9a234924a452bcfdcbc39ccd2587f52b9bda03c6b2beaff87f6d937d66f3964cfacc5f6368ff53a824ce0543d68077aa59d7573d27ae9748b8e155ce72ae584b11091a3a5b30645682b4b37d31dac4bc9e06ae0478fea2e1781ae941e596bc17d230126597a548f7bea5c6fa8ca991984ed625d68bb8a8b66feeb5d110d4ad64737f45ec14948fb743d7f3ccf9c389fc680915f0ab053c3071b41209aa32820dd41e12654ce68516d87372f282542a0fa2d37ee483e9613bdb6a59e0e9541780d7a686d2510ae846b5fa50c7ebaf3f106fa6edb64c661f0652e9a72294c9ab8c3643c30ba6b9b5ccd5ef87dbe04d314d3f6c608ebb1501ae89ae52055164d73947c70d36ff52c55ee062b2089d74bffa8ca0c016a8b541377eb4b07b9549cdc5dc6d0e6511b5e1e4eb78d7938d9787dff494809f4372c5c145075e13e307e39a87ec53e32666f87b0045435180ee5cac6d9803a3343da09c160c47e7bc10e94b238d5d503400a9ca58f0da789343a9ff269d6e3d5aa54d32f41544cc0a4e9e0b888422404fd366c5ab06a96bb77e4382dc98c91fc7e40f77601950daa7df238bf2ce7f1c3950957c489243d3ef3e592f70f6b749ab6990be69421f035e08d7bf76bbb041de16e11ddf5be173c879554d4f768d0d50dcff040e6df3215e8f4b46d1b18a6c01ddb6a2502b922d8168f327a7f5e694226962ffa82190fc379cdf24e7c2887de65137f9cd088d15145db8dc43de89198cd2ef6306d1f38bda41581753c4247a477e38483c1a21cb407451eb591d58605325b3bd5df727c6675f4ed99e6de1231ac354d08a1b80d9ffb414334e62d66a546dc170ffb3c8d2e0ef826c86844292d3e35d3087726645236d5d1aadcce9df711bfa11a1d3a3b5b55fd8989d514ba4e046ef361d6524faec97ca33433c682b00c9768fc3cf8346e9ef34ff232d56dd9dde36e85d99fbb79799af44c94dab79277b993b260487c2f7fb3ed6628045835b72b4ae716ae6840965673c2e098d985fb2620234754a6edbe916771f319ba4f6bdefbd81a2d230f5869f6aed36ff7217c1fe785ac22b3b5695c8611ae24b83cb15a7c83029822275bb59f08d98682fc8e7279a797685a353c491c8188e7694321a71efc9f2c2cacfd174bcd676d47b1cfb0dc1a613d1605f8ecbbe0e5209ee6ab6bc362d452b184f1e9dd1d6ae8e12bf65a96fc63c38bb26df9cb0cb2c80a293922068bf27ad9781129e9e4398ca8f3445e178387beb1e87f42e04a474a963f946d9d7c11a81652fc0ab9aea4a87246d7924e184ee27062e0de414c42190f09c1af6774c46a37ef5d5de0ac93b8d531904fdd76f4d2df73560168c49943921d32ddcc1ed3389ec64dbdb315fc1995a04b0770abc79076c6d836142f8019a0f97768cd7018b25a5570ddea1a663ff11460c8e7fc5f3c5e6dbed690b9e630a4d807e717fc14b09050219a4206d04bada7189689741190b166dc8110b0edbf9bc7546fe8e264795a66326d09c3789419f9c09edf107862dfadbb96f29c36f294950625f12942441b9ecd707c1e4143c2489d5a527ae9f27e5957469e423d4ebafac3132a7c770fa54060227a43a48f0bf7dec3b4502e24eb7bc1380e9caee28b8b861161819012474274ab1605a3e374a84eedc6c7c7579aa7012fe04b1fd5b8e5fc61f28552c6820fb0ab73c379f9d0f209bc4b1279a357c735b32d606c0fd26df7c63a0ccf2a6416c47308c08add7ba94d7c36a701c884aa43d239f3bdfae276b3d157f3eb1ff9a460b2c98ee191bed1b75fe987d3b4326ca4e17bfd304f786a14bb95bb7ac813376011e566e240b46c3d763639876a99ff5d59a730debcbca3af8bc12624fa5440831b30e555b8d259632177b6c7f49e22e87425afce5519b91a8b255358bf476bea90e4ee52ef6a6ebd1feb7683ff5c9a00f558a8a3401472cd181ef8a92d4c9d30399764bedf9da973c2902822e79c3840a4c7c27959264c83344e354cac91558590a3fe6b2921ba064563e75f29f05a512f82718182ae452cf75cdb73082e706397c24ed42b7cf58d825d683e91fcce45f881e712e8ee49217f00bf5fd6773bf95188b768dd161fa9b468d9af630987b10082eede113e26bc45f64a74a837aeb382b0d89c10a46abe1c6e721518ed3c07f24a8bbdece674a1ad523402d16f471a8e01b0d9a45f0c112c11d40c326bf8ebbf001dfd7ae5c1f850b1f2a9b8bb4691b4074722d2fd55579af495a2bca292380fc8b79da2736e5bfdd324d194a53c08f0e4e83681fe4c98ac32eb133d9adcf5f25320ee4b4f9344cdb7addc7caaf4a9a14dc2449afdc71f5f844e28e0e40802eca2e99dbb4b3c26ac96a75937a1bafaf1dae4c765eb1c99f326392099b49a88dcbc8b4b1e3a6859862dcfaebf49b6b3425491d2932a1152e30e770a5ad92304bdbb2683d7cfa961eff4f6b951486e39c19c225eb934141f61afa3a11bec28c84b1c6e4e407e86b5f7ad91be406aedd21f3bb4bb0e6405fa85df51e8e56b4312a878f2d7782d193ee9a6fbe2663a2ce3d4f91c0e1864ef28eca51a18db0656ba327e2e29e622635172cd1734cef3111a3581c66cf60c87e94c429ae81e14de90f7aa132e0e80698510bda25e5ded02112030d2aa2ea9bd1a4121afa99da16e63b3f69ccacc909fc7ee012b89752693802240abd2577ea7ba813a152d808fb2e090f3378be75958dfce3b9e59a1346f5c790a1493662b4b638f6e678803266ea25675cfc7ec1d782f0ecd84364f4cdfb335fc94b9c2062a682bc0e9a3c879949236a6d7ab404c0b1534ecdf25425ba017b2f44a717c4e6b0150f98f849347c8290dbc6b4172c48f53f95f313d7c0da5a74825f5f9a6473f19241b985a1b1ecc69afb55d9f388227bd4bb4b9c0336141486a5d66ef7ef35440ca19ba7b95dc0ee086012a4dfbc0e623fc6c57b88abec94339ae26e34b506e7fcb6d738de710d2db2bd33cf765c63d54187ac67fb3581b3723e25d5428958951d713e125464a4c2e8870a909fe80370dca97e69c860b357ae73b9d731dca508b7b1ff8327fdbed4f9eb0e87621ac30b3824f22e19f7d2cd558d701fdbde57b2da37bf781e3845e0eecf6b72f0eed1383751db58136a64dea72da0281e858da4ed97b0200771f1dad3cabe229c89754501806c8b468f78e0767c64baae9ae21bb6db82954568058b5f4a8f67ce9f21381dd23b653d89a7492b546c42bf617e8b82fc4c71acbbba56d81defeb1053b68c455bd3a4e36148f9dc1471648c730d884647b68d08b998a080ef8bfdab0adad88a50b373c5aee0ab3892388266d77571b2accbc304b1af3c93582a208eec6ef9ecd5d739b8f2dbe777a126560ecc0dcd7a75905bdda9f5576ee83a59270a21e3f2a87afc9543802de1904136eb1785260110344e3aae7e2ad6e26123c2ccd255ddc8a1825a550d452b428c683dff72d790a1ea748f52d0a5aeac2c99bbed42dd8d218929756d88c06054c27bc25726d3f710984fff5e688b5256afd9e9a4926c7771b63e59b263881faa57f2eb8e63151cc28369761dc2825778bb3dcc7d661e4334d2b8584b24bea6a8ebd0ce3baa00a232a26f8a3941cde43ea923e682567671c2898af87eec3ad91369172d669cf7ee84c41533ce48c9464a87f5158a58c8fa0ff98a64c87f17595d108d8f04cd47523ce0f325ad7dcec562c41618a8b6f44b2da26767ca584f1b93bfcdcffe3f5256a5124d0a71d0fa8a726bd7a5f8eb94c5f171ec00f7e8baba3e8f83dc295e99548891134d4f53acc56874a603a65466aff683b9683a348be651d30b193dca431c9672e0e5cd424223cd887959960f8c8957602e33af22dba37b03707926da37c63c73b001d473850e750d64cb0add89e7c18a34901a2a54fc274ec1ac9a1cb72408a822e668e87ace37a6fcb7add25f92a3a39ccf40de0034ce399559f592ed4893a74f3e9e3e14b71d69251c5641758f27f7c77cda003d831fa13c05de7cd97c589f26e6de3bd7e07131c0444ef34e46ff16c01e5f0c5efbb71035fb7d70c11736167dc8e3fe86bf07268943e544cce1a575137b9b7664998d276ce163d45475199c74b1caab65118c632312dacea518ba4eb7e6084d2439ff59535d7c62535928750ccee1f952bd663748c14607b3fbecb95babab91a4494194f59769553351e90981c48c66531770eef24374a291cff8305039cbcae5deffaea33341817054f3c0c56917699f0f26dd3d49c0c4ceda8b3fd25a6e5ff8cb8d15f909abde104c838840239d3f2047821b5967b95d135cca3ed88f54b4d652961e04ecb7f62b7ef272cc5028db0d2096cde0e35faf9e1e74e9c344a97facd5c4ea483b312c895c0d8a1a21f27d022f52f025348e372b01bc767674a67da0d69a7711f0f7752f4a97ee67e28449e1aa2270207d48094e9a707422fcc7119b5c3b922dfea07c34b5d50bcc414e62dfb0ef976aed7e20f9dadd97fb68fa4e26780ccaed812bee22ad6f3dd8932a5eb8a87ce0b29b42d0353498440661d63056cbd3abf4dd93200a992cb0c141abb7ab18ce2820f47f305dff2ff09d69e229e712e629a36ec0705821712996269d00ffb19e5366a0a0a5e74370e18b9458bd8685429ec96291ac46cf1a48c34a4e12fb1215ae41e93b3d6701d7ee019be7f205834d0981a7a2525f5638ae2d5f18f8f2237ab25409e8a88e7201c3d70ed9b76172c667910fa89812a5e98a1c69201d4e1db70f85b085fea005a7378425b7e566f27aeb54d713e10b16aa9ed064a92eeb8992774d31aac13819ec877bad3a642b102b2061750dea7e8e05359d8904f8bf3a58b86bf3a41b9826efd30c5c3ae65997f3887b4e821ab4540087337c15d11197e078007e794bef772df1e7df46ed2045e90abf31d7204ec6aa735905991b15c9d767fde961bd1ce626428f14b27bf00a46fefbcf8b4790a8e86e4dba8117281188b35dac05fffa8d10865512ead74d673f7813b4367d32bdb4980ea31aa14e9a036a9ef3a500723d8ebca348ddb3314122494ec42d49c200103fd6daea86326931fd598cb078a83b73d627edbccd260e0846acb6fd7c8d4f7fd96d56cefc65b87da9a0e1faf488f625107da83a2b896794b6480389ef6f88f056826c653c23e115221ae7cf78e03ff8348fdcb9acf2841a7b411da635be82014b61d1500567670d116cf5c3a640410a23f31bd9e6f26bb841064cd4e28867efb4feaba1c72d2097bb175b6f60c73b3697577bc9d155938688d0b3578703bb1b5d26fdf14e559ae075e5ef4cc0fbfe18a7224d9cab254df863e6fbdd2c010a02f96b12fe9b551e64ef0900891a813c941f64401e3242e4a03a992da7593c933f2314a4f8787a19d191b0d08b86d264490c18596c54f500165c3a8e492856e216a26187c0a3d3642ebd07387e40e8a041103dcd99a3ca2fa6c32b579d7d355d24373fa61a922ba77946b05e0383790c05c8155671bde765a70aeb31a539317b89e7598d3953f2f8f90d8cb0eea1368f57faf23a65ecf8ef276b6f5dd0dc6801b1069889da021203591376d9d81107f61e71c129ed94460b73cf3da0cb71d0c065b0d7ce34eec724e2482f85396adb9385ed20cca964da846c1744fbada0ffb1c449d03baa97f99106c5777c2feaa6f65cc9888173c24187d1323715d1c3e1d4697a1bda55a4d446dbcbc6ff7ba558092ad55c86e87b158ba3911186002dcdef8afbe265eff190f4ee06ad134c664abf1a9e6bd58821b46549668f9e5e2c4971e0f8642befa72487ea719910269803e28db4b80fef859ce5c01cdd62e084dcea73016da16a17afc5129ed65f530d5698b612c508cebb210affa35cc49d90c136fc38694ded75c80eab859d84f866c2d655752cfe809431aecb77c313251cb57774c1fa068ce88b0416291db36e84de4377ae68f7fc68e3b8f29df0793fa922bd04b4fe83e73002dfd703b3da97c871b6e338659bf4b8267a4ecc331ead3e82c1da48588a7397fe23a637bee03c0c6fcc762174ba55ecfe3908bc2a8e4f581aa0f49e58ebfd4445556bbe53cd783b63678f2e84a963f34501fba25f35b2eb6ad42a5791aa4b9978d3ae868692e26fcf5fc7227b1b0b11f855c97038c3bf41bb57b7479ce1cff7da9a9c07aebf7b6229aed7941d4ca34f2803f4d480261b7a7c0037b219893913b1cab0aab7129aa9e1aefe71b7ece05132c023632dd164c512ad8393e6fd4be4f6928cb8f4cb81e0d3100b806cbf6a900ea95239f9d21e7393e7d16441644152286b6ee6e7b87070d1bc225aa4800ff2b49a42e98ff2382fd5ef86915d7e5f434f9eff943ef64fa9536e96d544b5659452aecf671008043182c64ff8aca7f4fdb1a79b5e6aa2916d311e53deb5c344e86a3adcc5954004b6dc6336ae846582d8a0612bc93accc376d907b32216d452d01f275bb2ee7e68c34d07127dbfeabf0ce75ec501c3fbd1c9f9b909e885759abdb6afb83b58d87639f693e989a67634ef972ffdc42b64ee2dab67310440e6602ed4c26b32f199c7473afd58c9631abed6d768f9d6970261fb351a3db39529d5b5688a68dca1e4b5cf561239267e94702dc93e6a7faf07c791bfb6860d3790ebc92aaf752c8d52e0fd6bf63c819d4e1a6c64c8d15b935d75a8b106276aa5a77be7ea0092fab04a8bed87e373c358325e0c31e11ff91ddc7395f7c0a4109bcbf038fc88f54c6cd184f6f267c4bfdbdb2a8c720b1338a38d3842080cca55fe7d97012b52bd7f09d28c7160c287ad6a327fed83c3464faa56d37ec5cf273972bfeef0f6c5c4fc3a329aa3478c791232510acac4186920793fc26b633a631d1e96722b328500ca8a3ab21c70286bd5be6226bc67624e1f6c454f38e75849795260a3116b3ea270481423819ff600169d736896f8aa375407c4a844a5ce8d3dedc17963b8426cc21a2c653ccb7a1b43942afaed405717010b31f74f0a2cea31f39fda349cb0c33508e93165de29344dbf822d89b8a13e3ef9f2e86d3dc3c5ffc664f467cdf155b28e37cd1e25dc890ca2cdff3fcdfa97a12dc5a0916f990d6abf1731971d744bdee3196c2c258d1b0937ad35350cc99ac68e1db6961b6f277eca205ad926fd1ac1c13b07ed31594cdb186171835e89d4b653bbfc79896102184eacc96ef50afaa35e46f9d2ff3b21d757f0e3f0d6af192b9ae4298771189e687ad5887f68cea08d44d200d153277833cae807746763b5f41c6bfa70f007ca48e69bdc47efde5d6da06fef72dc58032c7ef2acf8921016a0036067a3fd03db6939225577ed31f40f94e1d964de53bca25082dc224eff87cfd97e1e3848e113f1b145bee50cb3f5679e6b7796b7257610fa8cd0396919a0df0c42ae64b1a4b5291efa509906fde2bee6b0d223818eb2128275ed90aa42311f21be0055c628c92cb8646f5e231a5e2e29b4450993c653d9410509d1db040fe0fe77b323d23ec7c0548fb5b4d0a5818c1394e50b861c6552b2159a2909ea0b71d39ac98dab10c636c4ad3e9c4ad445409214502ddd0d25ea63de5ee7836bf7e098d38d1ff4840bab53dd82df720660b89c5c6cdc3a78144c3d3f3f03410a55b6bdc1d6e4bd0c268c5c8091a545e6208412228825a99570e57449024c757d93d8a0779d3ae33e43ab26f05ea7307553581969b54b8935a14bfdfd63575bb6e5123196e9e7e70818ad3476e8f4d0f7311a9524c92fe0ed552c350fdb3dc66cd2a48f211c257d43e9dc730705973f062e59f2d0de6fbf80ae7f771c94dea63377bd6bdaadc46a8e8c1ecd0715f1f8689782db39c89b2222717e6d9cf831360827450574d0e030b8fd4face780d3bdab2921a91dd18a7923697714c93615bcff8008fec778ea93ed0d98e32b4b9bf9ba06843e8d55fd7591e7f67d885500968c65eceee0368262ff6a0987d91da11952789dea62312faf64b0196c9eb67c98f312ea689723d18230ca7006bc8463ba58dc1ebc3e121b4bec948e222a5e039cef004f1adca6825cdacfca75dfff00dcbd97bdc080a21f745f0aba0b3e86df9877c9f3130d27505c7bd353e963e5969d9c333c184f9d38936284c590dc89c0de5c175bcf6caa20cb15084b5a6ff370587aa8b79d99cd0422dc086be144cd8a7af3263e44fb5bc2d80d8e7e11ca173cf0e308a59e438770d8ec0712ee3b37934b9535f3d8d8011b4b8482cf4d81fbef152475c9a9ae4e881be2d49434dd513cc893cf6bea20916e84e09135b9867f686167c05382dc697a0c2baada3901cd457a4c55731b86cefc30cc6f00ac6c576afe65977d7605b04437405159301661005b04f7e656eec2a011961e93b48f5394de4e3c23e9d85e22bc948632b6a950f75bda604079d95173877108badc890d40fd9b13ace6c8c315351e0d21a264f82dde91bd3b1efa538578897fddd806c37ba25b69392a3d16e46c97cd3cf989d56fb75b5c65a0c7126015dd7d12fdbf011d5ec4e51204e58f1ca7e473035f668b87d8fe37a3bb7d6df3ae62310a5ca9190f9ab8f53fc668d66d36f2377e48482ac76248eb32c50435afa440b327381918c15bbca2a46eef7002d723a1e0691caadde878ead03ce0128435b5acb40ca00dc2e0bd9bd56672526ee600392c5ea4230991b8921d3bae305ea66626c7247db377f14e108b32dd70e10461339cf8104c9e06df0cd04ab5bb233da2bae3dac4899230a087820f98aa3ac29502be2c9fd0951ec534d8b8da110f0355f55c78ea70ff91deacc9ab5f47016d7dc50b2a517d366259c63827111a5b78a591946391314e4a1971948692b51c5d5a56cae05e50f2373555e1513ab1758959f05e9273bbab4aaf27509705efb52d44ae95ba9f5c1f41026e9076dc11afca9a1db78ef6299f4968aaab744f35225d7e4961e3d297dd41f5cf688ebf8740443375433f6a2f72c10075acdba11d8812456c8660de9ef5b8f975951845481e9424bab0b9a3349ca5d5b9ddf0650a5e69c8e3cfa5d7b90a53c10f7e2abf3ed32e9cf962326c6ff01325e768a0802bef7167cca4488af9e4da465e98ec8020b58e12d0549203d688b9b150c788c085a5da0f47d9e0873de5ef42ae78c5af57513b9626b6fa27ab05a7614653206a2560a5ffb8c525b3e08b622e54d2141583489c20455e552535f3e942946121c1762f2335b05e50f91d1b45504bb87c6c1baef04b79206b6dcf392b95d2161392c18ce0a832983c9886a43c0c8110b097356099af183a6cb393030261ff1b7317b44f018e046bebad739efcb94adf0eb52b1a746dc7c4f5f6b28829ac000eec6dfd5af362614dd56227e7a66d62efa477e8834da2a811f6eaa833c8ad587fe2b3656f7c8588646db73c8cbc234d391413025d621298f957555bd1d2444fee5ad646b0439f773ae4843b2e9b7ea3253396d18ce57d4589efcd360f51f3bb44232fa2eea506778c6029c4f55a7fe5b72d0ffc92f2f504b7316a487ec1eb8cb32003d6d6adf27a610ee10934488bef545bfcd7d9a2bc202cca2509787edd8cf8c1c47c822221632656f6d8f609f470ebbfa9eebdcaa6365e0b1440ca6c942c75b3fb62320e7689d5734869ff181f26c5018d295fe7e82cea75c51f2f6d5ecbf7871af19670a40f1c7515c865c8b48394a3b105e683e00d9bc2ec76956bf0b45b7b8ca454be08b4f17a278d19beedf75154a9b4379ddcba4dcbed20109feae535abb7b8e9b630e3e304e94054224f7c538be4647b013a25c663c0ca104cf22a0dc08b1bb8d1d1f68bf4e82df6dc74b85f70f74da54a4865aa5e755c26ded84aa8c1f08960b415cb21d39be26d3709a342af1b5ce351fceef3f53c8bcfde63e14173cf0c05c4e7798efc70facfcfc9197bf6ba6b6234e763c61c7656e0a020932b512f5be3bbaa4d00b06e3710f8d4fd0e8f98ab7ce1305305bdb064fdf20fa50e79140ed896424c2cb342f97a720c5a95d614c4e85fc7d8cfa808ffacfff81ebe8fc08ebde9636949df900b017cc4ca76fd02b188e93fc60b7d3d986f0a40b4a77f1e456a3fc53cacfb0a4b5577ba2012dcba5a931d160f42afcf125813534c17dfece4f2d78b285f75ed4b4652dd838afc052bcc48b943db99cea42842f6304e648116d0b13e933d0da43eb5f01d03c26440b1b8c82a1b584804bfb3eb73733a68c9f498e67729ef448f628a134f48ac86b44beadd05370b6f9a64151feb4eb254e357968adfccf3a59cd15fddfe671", + "frame_size": 64509 + }, + { + "data": "e956deb9e36c9330578c9fdd5d9c559e1deedfb7e93ba583802d0ada1e663d99f6770ad37e178d314b59f7a90f2baff35ac290fc0feeb5915ae7592dca0e4a6d595a925bffcdcdd8afa0f0a9bb24af6c3c04f7d9dee245b6fc14add790e65fb63af04935c8bf56424794bc46b572e4cdfdd302af92c467a8c51108767a163f424380ee729d5986c09e1e173e6a3729c4dfecfc99b85dab899c77c89b3fcf0ccbece4de3c3f19e8e7bb99cd8242a31febae4491f14b3014f4fc4c51138d4e9212d2947a6370e6755a9b6b611c742bb74b22300cc6ac891f77ce91d7642835d2f33670f8744f66359ba251d90de3cc705ee07411be399a28e550a7e174c210fc827b49516837c3aeb8ebfee54226100d0d43b8fdfaf2e6074bed8352e9fd7de26aa59012fd139749942b54f5511816c413af89fdfa069f7354b959b3a32e6d78fa07cf342ece4f18294a3d43795e5bc05b8d245b5ced61c97d6d5ef1d73e8d81046f00fad3a687f8b74782550838fcec546a36367846abc7f6774b2e47e3cdd012f4e05d32351c4ccd394e526852fbafd47ba4c48d74edfdd47f285838c61d6828c821179174d78e00d229e1fbecaf403fec69253f455575dcf9da380f37cc14aee8dea911d3afa16dfaa44dab9f60b6a47ddd4f2ba2a1e0cb64e33c56b1a32fe7d00494110ed4dfc7f6ed8feb1ec6c67e922ba070b33ef6f38d3f7bbb5cf0f928bf7948158dbbfed5f0c6782b846eaf3e9bc2079683eab08c0efbcf265051b0009f993f79ec57d520ea6b4a091a5607fde76eacba49206e978795ca67961a44e0d778fde882e0b6bb8863bf529bbf0e8b34f3f2bb8c82047fb1bb0b3fbdc38269e29dd3201091223765d3e28514f9fb4b9f7951533a04908d78ef8791670b2894ff3e04aca025edd81762b13ec4796527d654cb5faa74f5e6623c90254f99ce7e4d0bc1584b41ede4b64018382798e8b93e863f9bc3507286869b19ed9220a6112222ed17985265955e0fa6c70b1e162696af9fa8e494ca48455aaf48ff5371e6fba07705815cc4a86b29fac25a223c93d07354c4b002605a514d66c11e319bc43a1f5e90da7a77f9ccb82b82d22c501415606883d8198a5993f7ef11eeb46113a89692911de1a0ed905176243c1aa033ea0a95f768ab39bbb9c6f702b3ee8808768f3d2c855db47faefa87231ef3ab4754e456067f73e33456ecd747ae7e2ce84200042a9c17b5726088d2d8f819ba33489a3f6068fa3b6e15b1f3c0836cd0fe714c5c49cd89d8bcfe3d9aa3291e98748d4e11da7abe171b704a34707c08caa51dda21c88fdad370665bb36b7db6a2a8b558fe0152a1e51d9243405cd555ae214ef7ff9b3860ae2412aab6053c2f95dd5b785963de2b888b00cf7515990ab3c25e6cbe83349035bb3c745256acd301c767239cebe6d8dd5e8f7eac7cd3ac5826561db4cd7988ea1e07f05dd2b6e3319b506c08018a2c1b5493eabfc0c2c719e1a11c1873a1bf7187d49556ebf7628b6d6513c6359b0029d83058c2a9ac47d6c06050f9647a7b1bc585098eb5fda81e7daa0102b8ab4ba54c31641358a3bf5059d01b139aba42ff817b708276448551f4a5df942e33566d581566c87e8084859e52e2b0fff4d89d7075606fcb5391993523b160fec9f7ee7aad392740f39afa70a9bfe7679d73a0eaf2b572cf096f6a30a8bbac2c5f02eabafc714c93276a266cb6f51e496580c613ec2efc35262f2022d97632ba560873d4d660bc65a417aabee7016ba109153b8839fcc68fc2405c73f9920f055f28f22dc87b652193efe0c1f54244fd6f355409de50d3db0f8ba47023f9927f028aa6838564254ba9ef45c2553a4925dfca1a10a038abb9fafe061b92c35fd067f7bc399c3a0c85d834bda25fc7498b7bc4d39e400bc80c8418a7e267d24b676dc861e54473af1dc9251bf0d697c087850b48faf2b4e7c483098a697dba592d0b52923a1cbaa82d109c2138782e32039f1a08a6726d24e255f51f32be9d31150eae8cab0f93ed76ac8bb2630176056c802e7742df8e513a20cf7024d2a717df47f814d729fbdd7e92bd74260ddc907961ec224475669307a33b75d86b9e4eb008544574f708912222e040b3eee88f7ae6930fcd6cb9c71e36ebf7ab75615dd1951d8dee08c3722cfb9f768a8256edf7be731bee698962cdaae9d496275f09d2315848f0c62e502aff9629b941b619193244d01d2be39e5073601ca47dbf7903c479921cc83cef9b20690a9eeb7c685f85cd12938beded07b77fcd8f791556f6f2820c9f60025774b610f76c87e6ce5a3271a667ed6f3846db5f1f47f47814213d9b67db984b41f21179fc04457b7f22d5f51befa395a23887600c1361ebe74bfed1c08702ad5ede7d52bf6115754fc971c330c9a4afe1728d122121eaefb748aa0d34b788c39681b392801c156ea0a89408fef5686535762e079de8fe4fe95037a9edbf7e1ab34f817572f14d42ea9529b9d58a554eb5cde0d14281de7858e055ff80197a0253ecc8b4c332a94d12e22bc6dddb55358ba1b12dcfce7a4e6918801834eec77b9d304581f324a029a54c12d239ba34f95ebb8793462a592260a91c4808154ba7b77446a378ef59e67cd45d8991b59393301e5a15b73851da8339ea84845e02b358a2c618ef8fc86d338597f45382022734b48091274e18744ca6cad03da57434c9c968a1a9379e0c1c78316f9c6fbce4cb905f79bb784475dbda96c15f32275494e12c7eec010e64ea4bec69ba4a222b6be7063a4b62aa67747f1749870ae62f58762e553514cbed9e26404c09060637987ef2af74a78129082fb17ce7cf9d3de257fd274d60096ea8f22a47b48d9d97680a8fb6ef562f9e06bbd03ddd649296f3ded2144987d87846fd9654281bf141fb7bf85a0efaede379602d0644a3bcff0ad3c1c95a842535377aa2174c2e85d984c2d92d5798371e2ed24c92ca2e3084b67521f5ebb20dcb9621ecba7b9240d587c50cb83f07c0e1265a3f49f773db15992525388076389bbd650ee87ed64115e006b3dd7ad4c6a40f802d11d1817e8d34ae6e6c5c78f985aa8e6434ee4b3b62935ecfddab32ef2209539bc693e14dc32c4141f282ff573c78d4071df40bd6a63523dd44e76ca9cd643fb5145672961547e25ba6488794038c0970aa1ac845cd5007d1ae4a3a90570769e07a4cb28e1421ab825d24c092f77c7aedb7ecec69b1a6cd497c7794ecbd444fccd8fc55fb7af120d448fc23ebc12cc66034af1f97e9b675ffe31f5e84b47f17867b2865176ae5b90338517b4fa8affbdc064588894f421d00b45951f859cd03a1a55662bd57c13b25e842990768ea6afd5b3bfade0c88a90187aa375e156ac08b2aa23fbbb8911420ffc6ed3af604a294eb50074349e5b4ba7a818fd61a5f0ef026f3319ff838f2a262a62c70979af6f64ff31c46d316b9a523aa904b89de6ae6978570780284755a70fbb2e0fe541e593cd6e1810ca5fb33abc1a172e852c3b8bf21c628e79723a1e32c9cd915083b780d0c9fa71bae49cf798fab9661771a550526aaa453e2bdbdb93a0b2ce5bf8943f2a112a8618f19d24049fad733b7839a27c980ce7c8e51d024b310e96ddfc246db611a3a11333a51fc09092ae667f41b5bb196e0a48043baddb1901dcb831db89993fa3d8f875eae59584c912e875b1a3631450d0d938ef064f40937fc05d96705494c8193e1a2ff3e7bedfe298073cbd1b00b7d1ad9efd61bdfc566e990917666a8b287ce7cff406c7c0c3a6cb7ebefe1179d53d6ca663a565bb0a7b59160ff6fb2f7dbf2ea20fc17144b8728e2fa101b212e536292598045488b71ddabe1ba55e31868dbc67a90b96b239bcfcbb58996b992fea4480921e9db651fc08cc2ead6b0882cdc65b1a6f2beb510eeaec0fe8f1c3dbb767da3b07f8a08a8c373b9b98d3a650365c225d61a20504a3a43615b80b46e309be29687c0db27441e692c22cd152d1fe457c1d749147c970dba466970fe308d2675c31a00433134e3f6d461e3fb4bca04e4f230711338b2ad5ce58e5c983dded5eab31da2809e6da840f356e4c5d6d190ed3850dc0ac37b0a10837becc4a56fa0893e45f4164a0ddad37603edeffd0acfc76797ee3dd69703d8c9893d5fc005a01d4e6767864ada36a458a8bda650a7c497c1afb9d5cfdd11abc9bbc9c20c32afa101ac63fcab2675e6bdb5b2a57fa3a6cb6039478092a61ab055851254aef2b319ce14f486b0be59e73d08fae76455bfb926ac10460fc9d50d8cf705fd19a26cf22471a43d0836be18a2dee0702ba28ebf98dea6227bff2aacb0c60272622feb0147aa5c5b150ec97d0da70ebe2a5f84915ceb2a6bb8138755da7b666c81f2f024133594b9a534005acb5355dfec3f86f4d98032b34f6974d616efb4041331d39b9099c5b66f8c9f42d8d2caed6ef41fa856747f9f6b82a25e3db711d3ddd87a9851f9c3c3965423b4fd06cc558f54f383fc06622d5477637857061a4d69ebc4e87564b3c8e9caea5a405e3809d85c7d5c574bfde4b04f6ce69ef54ace0ff957ab950537f0a5f4bf70f6684f6f8d015ad83d97de4e15e4cfbbf579feea8c2f8a5d1112f84b55e3ed4ac2bbfeeb1223ab4ee8b6dc629a1e272ad905879bf518f7c7edcbc887effb3dc94b4723d10719bd0da9aabb249f20b8760b598132df539297fc7fecfd9aa1265f09f637ce89a2a091f87f6596a70076a8504d2e4f154bf6f0c7de72ba9a16d50e4f07220c01854a948198b4e24fcd775aeceae852301f89957b529691ba2657ffa8655d383584b9053745139cb78493fd45ba621a72127652d0654a00d673bcb38753a0c57bef9420241f18ea559cc8a4d0792a18e22975158827d6188f0b70d5be9a65d08da4fc5587c5128160f97e720d0d496771c28b35e12aca7d230b06731b875e898bca312195d3b8cab08efe4917407110c2b988724864fbea609685469cdd3525a425974112a3bcdeb9ec7dd2a9d1dc148536b2adea4eea4599ab0041bddc8b0b3fd8b1356c3ca104dab3d3c49f8a5bbc609700624b383206f7a482442f9ecd3c24acc721e6a02e0b6bec62f84d1a3daeea8a9a446e5c7a816371f4efbcc97bef057e9e0399ee9e143948c002d99a07b976d28e8c238b1cb33105a34cc9ce094eb159112ae7d6dc5dc87d824c41aa69671f111f7e31d668e8181c8b614fd96e5432423c94cffc6b2520f135ca6163bf488d6d6801d71d1c0f12ee2929e01fb3384955c4f177cbba05aa1a10762e4cf021111f2a9e444d138d3fafa838ae43f8ae22a4ef67193f0cb7645defdfd8612fc989093379a898214cf33f80127363fbee2b03016f48ad8a1efa1136638ab59f260b2489258b13cbd91da8093c6b63636c1adc1f02b08d223c2377ef6340a60e77f9cc129a6bebe03ac3e6d760c0f3d4934661462a41e3ee63cb6a4d8e63c12ec510e526b42ea4ea8c3cc06a408d0a6869bdf85c5390730d51222c18c7e5dc4b98058641b2e2f65d8fc67d0dd95dda589dc64c45930a08b66720a4d9978cc51b786384df1b123c9e706428840dd1de82947c14a156d50e4b1d3c69ba65435b91fe9d218c9801810ee06c038827615c0d567d2ad818277764e7f535964539a85767322dead0310677625d3549e39e87270cfeee5833ccfa0f233f1a35a4c61247457a6a1c171673dbdb4305f67f2e32aa31afcec5f52c3eaefb696df5b59d44ea7464b703d3e2eedeffe2845e1aaa2d717bdc41a0a4685471f54872dd04cf0c26c8457c39fd9a7420a6707359c1c1a9d1e975697bfbed6d25949833b43892bde5eb85a2c5a678f1205f9e7e0d697f6e50c331d7d32f4e19e3c1b770290609c5428889e2e14ee38de5aaa57907c6807611a020b5673fdd0dc72707f9da64d84ecb64f49424218df75d9247fb5623eb24a4002783a2f911f52378cc750ca02c52940d5aa9e73ec47a0bf8ac0183c47cb5b4592c092fa2ad6a58a87086b63a9d034f9a756848216cd7e42929b37e56eaeef55e94075c9b484dbd7802132eda2e5dcccb9236f74b59e8ced5eb8565cecbaeb363e03c35868ec316df4f4c6f38b8a27cab2bd5947e124e8763b4c0d096cde9c2df663ed66a8eb86404a55c3ef2d8f1b15074d6ebcc2bfba842e5900227470307b1638c44d491c0c1667305c19205f9428ab6867ac2287ff2f64a3dd44645976bfbb8f82f2f245ef29457db1e992258f038b74269658c93a3a23b65910f20c75ea951762be8e9666cc14b61cde872679903c8604b0ad67af430fe7b16d071584ae8f1c6b279654f6b47b4d7560bd525d1f419888ab80270b661379d958e24310183cc4e36b2800a3e3f1542b4f24504a2e4bf0dc241e4007fa4670aa270dc43281a81d56d5e3d1ff4b58ce8006a02836ad8594b4996b4ad3ffdba1791405a3829da4853ae7f828da01bea491f00f7ab01b7bf10d2c7dfb3cec1db7986b21c64db3e11a6dca7a8c23781fc6f0c4ea31e6be4443b27b900c4c06759edd4e10e9dc05addb93cf7b25f1891d4a5de9263fb8029090d708ffa56006082eade921d9571caffe2752aa11bb9156076d7d4394c3389780d02366aac88c22e68732ff462dd4feb0d3ec5155a2d2d7c6b20ea21c682978f1b5a245208b658897ffcaa3cd6db1a3d04d139becf96d0309aa8dfd3e0cf5ff0973d27c4052d51a278e6e37d85a2497120600b5a20a7238c839e621b201a6928156ac9b7c42ad8ae8585fe3ebf90f5945f7df1cae82fddc6376c13237c2a3e720d3bbd55026d01c242a2368419eee5fbadda2b025932341fccb0b26005d939e62d415be7637b7eaaa82a310afac92163841dcc805ffe6dc781fe107f4dea5d17a597d1a20fd2bdbb07e5a216de0e00dc3d0611f8c162c228d0e08d5a8bb7de9a8b949e4954d050dc5dc6a2f885859b54143825612fdf4e2769e3da1bc8eb6e4f7a0140d5886e6618964f8fdc4d5f5ef77d58a793846f02da1ee80ab98cb2697f261af717a3b57a5a3dad1c49142e455929de51c2de930c7cce388c5db6b71b1a3694d87fbb7814aa400aa419c1d0f34adf4c34123e824141c34f2d0dc358755dc303e49b7a3b86a39e64a356e1214ed3317af7a4ae87389a3f7cfb6260fcd7a391f0b2cf7b7cc0358a6c7f1b3df1604d9338603dee05c3d9579d7706324c75cf9c5ea6183f2800d85d8e010f15237b14129b3629344314bdeb05ca81f93963765765a9c55c9567010e9c61e52299e32196c74c4249076a8c9117a40fab38ed1ed0164304e4dbb5bd4d0a51df3ea1cd4b963102657562ac5611555b19e2b545662bb27d027c08003b87a6d175df538deb1d64131f2f6441b03ccd04f247ab3883d5d68ee8de597553f847ea93380effbe7103994e6b377d8f8e35a8905ac3d9bf720c56c2a82e25a66d6526200732297fb4af183048585e02c6af1e8aeed662677547c790b7f8ddcf8779aade4ed72ef04b4007eef93f342f83481f2ddb62207bc406ba47e8002cfbe74db018223d6687256ff255c8ecee1d7e3513d078998bb04a0a290eb93fb6b99ac9eb7ab91ba0f93cfa3638dcf88f2f228a279bfb0c7ed2d2cc26b0ce79dd1aa5e915f2b538878e32f39f766f811528a9144b3c378a543acd23e2f0da3a3cdb658e82535a877bbae34e05f1d987701703897dfec51bc979296bb299f62a68014a634732f60083896060845078660f77c0b72968697f3d43050447027d1b87491c73b51f1e3cb99ee2ca2971629f5787e50b2c5ce0f9ca861a0d210a695d7329365e9d0f49e4966e6c1187036ddac3178832d6aa13532fcca155e690c3370db7f0bda47786d0fc98300663fcd1cdd1881b28b3693dc1d037dd742f2d9cc607006557220463dc66e35090ed4058d0e800b3bf8f379629bd31b986a4fc545f660f18f1ea52b08dd748f959dd1f62edaeb8ce690a8ab5bb2ec788b29fa51dd76b28186804d69e51b2fd5bab4e463703dad868ae8f8e29a43032c3dcee2115aa539ca3b91593933a0e998948bd3c0eed6d322cc9196e9cfc77fe8243391087c2d234c3de08170c2113cd3900eb182198a3ae2135ed15773a9711ecb3d39caf6b70a7ada75333da6553de4158e4fa37af7d589a9fbaac1424375217a62e0a39a62b8dbe0289d593045b985d661b7bf1f9a66ab7f43bacce321d1267ca59f8ae1b14f20c91bc7a28b146b998476db9fd583ea2348c67a282d952853c2174935feeccc92084cd1562df8d0bb82b02da61dffc3c13b5e83e709c235aef45229982f3292010baf7108bc84e68f35b2f45e81beed87a67394011dc0c3ca8f558d62b8815546866e865b8b8e1654b78c405e4b2a353c001d69a0a871f1214b10fe28c6a7c1b93f1d2e8f9915c88a2967a839cedf76cdac36d525b3eff97e68e0977c2663a9ac543d0738ccb99f80fadac40ecf2541d8e1570c3bc20ac826e990aca7c9df7edf71e2d99639801bc87fcb15c39366fd2b18c98ccb2e8596c26fac6183909f169eef9d888bb8ac700ba689bbcd876a5ea7a3dec4761565e200a97e054b2fa99f6233655cfedf2f908cf1ac968326bbb839a8eab7781cc629dc8b3c8494b6c4d49964b38b66345f5582f7713ebc31e4d4f77e4299e22cce316ab4b04f90518fd4a18b92ad38f1223f8e68236efbda584695126af9c5e81d5194075bc9aff6dcc12bf6785e67936662b693d901ff267c253c480614aaf470a10b78704f3626a2698fb31b9a4794840f522ae3575bab28ee7fac3ef9b660b1950b3874f9513b36b20a1f3a2c896d6c9e9eb2d2db43d323b12924e22b37ebfc5aa490ef6441cdb10b97310b317dac2c40eb4e5722b9c91001eb496174a76a8dcb8198449e7c8435d9c900c90805914a0979e3a5ff3d05416d2e372dbd3a06222ee3e8c7b0c26eeec09674174c9927abaeb379c7fd86e3adbcd1b70ec40010b3a5236a9f905af8f0a57650f60713d271638d6d12faca54e9bcc140aed434f462fb083d97ca775c0f3b54507bd3027c80458ca4a33db7bfda47132940fb1453f55875cc43e5b4be6435d018099b459cef512a28437a5cdd42ce7f495fbc56477d9cf35d88b43c0e4cc5742214bb9019c6f9f05dc5ef754be617da3797a7d618725e450b8c720e1dbe00e81a561c6bcec508d28d3f42593f6ac90bca174f6e40e19c4bfaac181b9b03b444230fdf9916d3116ea3bd71f774054b662784409079dab56f2356d51b4116fe6c144139c9c07e1eeb04c341acd3b96533e1dcc52facaf921dc68532d1637541c9317c081f9c18226e049c3327c74f41bcd73fa9640deee85c2b2da236cdb0bca0daaee856d5f70a32cb8c6020398fd888b93a56a5ae747879933bb33b7f4b3bed8443cb367aec7f83695928f4dac24d39b030ab43bcd2333a6b8554f1f873dd4b33ffde62ada5657c215d7f61ce07717a4ae281640bdff85d60479bdcf45a0fad5f0a40f71497730b25fa9c2acfe2d82d5ed4ae67a3db35bfe7be883030a5a034882548f0acca5dd826d87cceed9e7e3a36f7ed987a8b3d1668310d4c2844951d2f5bf4664e638bc4a2692ee22ac11eee9bc74940f1ed3c493c7615e32b74dc519ac454e7c954c160eb803a9e42f313c56f0c05bc209db92544cfd8394a2afe9eff0d4a05c2b91e119b9343d042259d10bdf03f0cda7160889265a9031923c98467c58f2740bda4ac1bdfbbb9e4dbb424bc7a2c52a2096ba7163bcf67db9934ecf21e2462f3b03e0a4532a0ffc7b0d5d93d9ff1e847a1eb456d323f93d421c5b974291d9d3032dc15666fb64a316bae605adb46a22cc7d45dc586f22482e64897a7b326be1159b0b814c38a714a811e042ef280a620323d31e66ed51fc7ee6db5ef3469aae887c21620398c15a5be0c76ecf00ae2cc0b4954c7917b934861afbd0bc228bce5a514d1b5a0dc9dfc522e8b348ccdebc951d789a8c79af128d34be1d5436e40a806741333a15c0397bbda7fb2d6ed82ac4d325376e4fc6577587c0e2e2549686041834ef5e535edb42d394e3e52add084a8bf338394475da8e0d28e1642edda25c921b3753ca0e388eae118d573ed282a1576ecaa8752b11b36551e9f3ac3a6346f94cf801144fc2f35bd9792d4af32446681e05b198559ab4c27af671740edf2a91a300d5948ad77b211f8138e4425e9796a2d15f3c21c647566fc1b049bdb10802d5a2bd68a0bd121c4d6165ccf43fc88fbf982ec56b79ae7abfd6f5e8cb57e54c00b328d5ec22f72dd0be33c284cc2749ed1989e2bbbc81552355c44014cbba1e9bf6845e7f9ee4a9317fedbfa6b54e104f27c2c6029dbd6067faea336667c1320068e4b890fbec919202f858452f31f68f95d91df550b361a6617f095a3d717a4fa86ee7dba7bce3e6dd7bb142a1f26837d8a4ade07a430494b512f4195f412f73aa11f1d57864366ad28e8165fdc64622e015c6d0af4fd987521697c14b5197ce9599a197acdeb48718b5b1f13a98dda9808320275a49a0c70d6926c4930b5081ab3d9194886085240e0e527b7927a4b7dc45eaa1bd9a30ef9b0e75baf464334f648e2e691219bf42056a6d4198ee3c36888fdca25f844f538e2f4042d010ac59c9d1355116ce44a3b324ec9101af3ef479e0c1fca7105f4b38d7ff29f2ecdbd696538e42fc5df773ec7c182dfcd3715a26856e3efdcb55992f4e1c73c0cb5da79f31a9b19713cac5364b077ff40dc13f409a6581b785278885510a5c1e26fe02b581a6a0e81f0e33214db4a87e41fac07ca9931e29a5a05218715989712f0309749be247a0c7cd50798e5fdabb93aa990dada9f894bd72e2b83847e2d191d4c122f1c5aac6fb2632e319eea7ad04221f9a2bf5b50e69c682e313ac984842c863b8a382863f04c302a70c0cc1459d28e7d1c7e1a06670782e1c0a61deb37c600bb333230099e166bc235053a2e79db37e18091d6fe762a9fb983b78b674a81e7e598cb9166d300b363efdb683b00bafdac350d766d100a873cadba35cf1b32c045d317a9acb4ef54dcda2669cf8b2127f528f8345521493225474903e34b6a9d97066e6b2415fe8c0606f768422010603cffd973d6a6a7373dfe73842cc80750b059b0d97dc362907afdad3742d16200f9312938a2f55327e5cab91a6ff82193627bf4998f3511c7602569aa16061172fd6acc93f73e8f854edc6deedc4c42d20208fa20bccc094563564399505cdb7ac6712ea10a535108279972a5f0d70886837eef6f5efceee10db2fb758a0f64710cc8fdba362f48394e4468a7f844abc308fb306198d074425165ea91a6b9200d789f1d33aa5fd3d1b08066cdec3599af3c710d0e540abda0cfe6a9f2605f211af76c8e87e189896f332a003acb2e515316e72b17355703460ba9902b186d6313ecd2c7e8277141e1f52eefdba2b7a502e50fb16da8076319b92f10c6aa74a7c7a1c369024b8a4496f92b82697d2e066066840ca930a9db060168952f3a277c1bcc6fe6d92ef9ae1caebaf42dd4b538bfdb9a1a40179a9db9ee51a6b72e322b77ef66e7276b2568d4b550905ad11325259f6a817a6c6e45ac3aedc66702befa17adc13baaed362713090a14b8702381f14121472988ac4796ad5a29d39efaf7c73276ad26736907c3a7b8cc164ac5bfdf09ed2f39d98fb137d7b34987c6d5a630c54f3f26ec2defe96e5ea4c87a0b3e74d561d493a455d41a5ee27062d11189e392adb816bdf86ef68f78a346204dd481b690dc0e3aff8e243fc06ae31b1e606e42c1c5bad6e176271111fb8a9aaa0fa47ff893b5b372b6e5e94734e234d9b4beb9707f1522a9f867337886fa1ed59905235132e5c19748a24eb64c76feea5e4624be2ef3c2f23afa83ea378c3ee37d0002edcbbca426c897a991f07623e4faaebd7062117c968bdddbdb907a93d53ee07b67fa8e51f29f3d593bb981865d2b948dc0a402ebfe752c2b4c3cab725864b7b4e5a3930e91cb3c0c48b14177094ecff7c7ab7167cbcf9d9a803e1f8a26a420be6d66d8839284d8c91fbbd1aa3639bed4299932ea0347edba560a59b3d733e2065d3972aa83b02127f0408dfa7cf495a529f08510c5b59a562346ee34701a97bffc6d57d318f04df55f7960c97959c0beea3808c8b01daa536dd95bf2cb7cdeb1cae9bed816b8b41efaf4deba56a88ef6f8ca2141716d36992eb7c5de3d8d7e119a45efe215c40b1e20464bf908117321405e47da193b5268cb4abf6d3bfafe6ce8ed4b2caae160f2e84cabfa6c6421e62ef1afb504bcf42a0339cecc5a2a2bb3e854a6d730984e8caffd25eaa6b4af1d9dace6af0b77755dce8449d0ae2682c687dfcc3a3f764cf0edcca52191dfebcee494ed59792c0d20758cbca89badf89764acf68e8da583802c3fef887cd1dfdbc62846df88a7106368d0e5fa47b581984b3ecd555d02593527e2e53b4086aac67aa5e642df87e6a09b791dd6bcccf57343773f5c92ff0ab4ee9d78d27537ee5e976843fdf15a686501546b070397c6a9ce85dad2ae3e1124cd766e6403b107939508902192ed2bf02472a19de5b9166278e1d45c750b6449b8801d50723a3c6fbad1bb47f71d9f147c14f0c781ec7725568178b58a3381c61c5bccd96059c4a5b45e38296db5e2284bac08b9022bb22de728cb482601662058cf640a5e39c1565089a5a876149fb35466dbe7af2499c3aa5eca14963651c0b6eb40388e04704d416a1a617c66aabd0d82e109f113be10e2fa528973cd86f027d7688f7e09a2444ff127d582b3697f51e36a818b3d7cdf0e3b9e662315f85dc49e01a3dd3aa2b238bac90b6e4c81f24ce3daf7ad8a9b36941dd728fec6247fcdb8808faa8bbb0a5be151ec5a04aa2595372997a8564a5fb5347b09800d27cbbe1c5b7f0cea27dcb6e41f8db14545c1ab00897f35e1fccc114a503d479f1390cd769997035df0e43e3cd34b83a0d91fec348173fe459e0b43078c86c8d10d1a908bdb85f7c16db2f92199554a3cbf4f3d006898c326c6d4f9b303d8b5006d43926ecdd4f983caea55d0438269c113c8a9e1d76e08eb0537db2a0e82adb188673111d3b36a6f0f497421e136828320c9d020ce288eba94b13330a566c56afb43bc34398fffe84d5503eb599eb0104085e082168865dc0a8424e4b762c458e16b4cb2c5d669a1eb358654dc28b4b4116ed1bb0b2bccbb3d2900e92bad5bd3ec514d35f44ed0d6cd8b99b538346d105b6fe04a18a3bb982ccbae6713b838e650e3df21c37155a378df6e233a59e270de3e68da14635d4a465e3d9b528b85157dc3eab0b087502543fc9cc0e86c5a79a35fc934df77f1bb3744f51dbf9cc34d1bca872fb42c79c154950db1fc29e9d344c67c6bec74d904d38b61964b3d5fdf18815e61971ec7c3fa9cbcc25314d5308a9ff5c51ccfd7433f2e8623d1fde760f87f0c71db0662f27134e50587b0140f979ddebb4d8dbafdf2ba52d894306cf7d9dde56f01458cc07293a39519e9bc9eac5b2b5eb429014e9e9738f89b7ccf62cb0f18e4e29e9d1f53c64e0bba24d46ebf99f266f9e32801d504398d6b1156cdae875bd323e96b8f8e71842f4523eddb73e1e5c77ed641a16afdd5d8f3cc2174bb97b45490f2f0a94fd9e33b58a657c73f5a468b6cee324639930b187998651e35ad2cf528da388cb6989f1d294ad44de693685f32af2bed80c2ce9323c367bb087e4508fd7d6c56465017ab5674eb3391874f647887fb3a7c1eb5029cf2c632e70e4b108ac634f34249083fca3e0564a10edf7143d7ba45f0a0b51c457ca7731073b47f60ce17a2349c51f2cad24a4e8679af724f04c24336385976a3ae1ea5d971782e783fb263d354f2e03f715b56fab36c39b3ea3714f25fcb2c2aebdaaa5900cd1c4a52f0f1286183569bdc3a683b6865ccb499626e36f055f7fba48c2794490e90a5b73ab6198062c602e9bb59c465b361100042281c8eff4ac08dda0376e6dfeda2d8963ab49e3b1b00aec3c421f0227f0c0c68eef660eeed1f05a2d8a6e588852598b3f223db16a5feb1a14423d3bb32f58807257908b5d43a12d60b3dfbefca87373967f28ccdb289f685c6e0fad0abf9f56ebcb68868a8c4cca341eaba96b9574f8a23e7e617e2b0402ae9d866b6e651f5b8e0ea8f14bcd89070a8dd02b2cff583f3944ea23704c5ec6c268f0713753dc23f06e6172913893a06d55c8ab73fb9a159d42b58e39102499f366159799759dbc467925de3b9e74121aa8ffac3b8e40ce8c673c9f3f9b5870a10400f6675b0a22dd8a24591a86b24a1c16a080a6a7b6e58daa843fce4360479bf1f22b393d328d285c66650964dac53b05ecfa113520e6007ea5e03f4d84f9f00f7f37be22add5bc4a75bfd606edd5d618e9c5d4cdc613fde508970e6f6795e803315678b2b2b60f05b4c47c244f1c300687667f4ee49dbadc42ed618e628a00ce881e5ed7002ba0a5a70f961a053e1a79b76d53484f77d5245eea2a93b0b15d0e4db7e96f9a441bb8786e48c120531a29985e4d6840fb0ce638b99907789237e9c07874e6659c3bb041f94abf636e5ecec363fe3f3a0055261fb7d50514c499ac2c8786c94be1fc9becf2cfeb330c8464e0c7b27702b8a4e7a32f4d47ab4e9cc8997b06bd7a5205ac630c76c66894971d9ab3ab70673e9b8141507f15943724a5f1c89d5eec749e5f9e60e2217eac2c3d98f6b9c8164345d753ed38a7be7802aa9bc1b2987cb3f098568979a8dad75965211011e2a332a12f3542f26423221aa716351fac97f5188a2c1e85713da56fa78ea2863484bb6fe7bb2157e1e6ee51468b04d9064e46f99b7f38814cf63ec5ff54cceea08cb024728bf366ee490fa1c680e5ef1de09fcdc4628c33acf92585ac02539fe4a79771854b56768895179521a5003cc59806720ab0a517a63361da1fa2fa9b0095194a5fb8c3ce7ce7d3968b3eb04775d040475aaeb53a51ecbd47a91952f734504299c959b486e8d2648ef378e2ef12483f99f4b784a312ef8ff7f5c482a91a6ce23f966b81a7f9b36dacb246c5069ea423f2dd6967495f4d787d4411b83e73bba206dce97588b563d7e53ff66835089fdca25adae85248fcf5ff786a40c992ef57b49a4caf56a67e72dc92eba1864248326404939a7daff2324cb30a8a4307c73c9df4e42af6bff4884999a431780a117b881a7c1cbd0e5b95bce8295056b0997af75822b6e5863029c504d898f93653745e5934ac43ae0167d2da40756a30165477736af9528e568f636e4a0dfcf90931968972d2dc55427d3ad87a0640b71d9fb12f55872661aa8498793c68bd80f085aaf7c23ebc46dcbb49397ee1411bd861b00f52d40c1d1207f3c9c695d6ff46a6ba5ca095a50553b34403f3fc6ebdc0c9aae964768d0df005f3edcbee89d69f907df29aea0db02724f8441129b8aedd43ba9b9fae5e4c435dd033f6d1ebcfd0c133ba4e6280b88ce9b1d531377ec06148859e57f3f1c5a55cc4c9482135e27048103349d49832d5b3aa4b6713f5171f0be6926426c2ef48869104dafc04cbe007f5778d24280b24f89ee1438f452f09f1a445d78277452054e865fe951d63245e5eb8ae6a7868cd28f320d005f44f8438cdd449672947ab86a7fc0009b33ec89dd26fb8ffe882fc3d2592e8696dddea3e5efe56cb33b0d0592f69787dea983970130eb950378e4fe360e80d7f230e3320386216c98313022a239efd7715586022d151428b4dc29fa586e51061737e332eeee0ce3e0465d1a523bb2627b76d8902581ee38ce8dd514a6310e141dc57386e605958fc2f9200372b22bf53c63406ad1cf8a2e5ef242efbbabe7a72b57439674e233e071822c660e5deaed5b99bc60cb8c314506939755d6f0d8c281e3d65a889ea6548880f706de77742c49193eff9c7f96211fcbc69a1ddc2a9eaefa93da008953004400c4ad891e33492ad17dfe99676b133ff6dcdc1d7b0c056f6e3b238b94b7a451d15c649922befd50bb0d6c7e7d3c86fdd1868ba893329ce15bda3c3fc2d7e587b517e4a86db1a9da5013fe5bb021d0124d1758494731c819c83b74f46ace844bbf9535b0e016bf4b87160810e1e5eddcdd62290d667ccc049dce45f8029fe3547f3328916e6baf4ed8384b102d8f610572ddc383fb8b1ac0770ce307c4db6c3b145b8bbb8463f32ab8442e7d770528ff26262d1e795cbcb7cf253cef4ef9bb371110c75c8996e5f689b4aa05b995e2170d339a5b61de58c8eca53f45a91c8fb6e014eddb4748169eaf57428d21dd6579a4e2e3660ba51f8271704f5831802f56c60c2469ccf9e8a8c25937461c91b645f2ccaa693923fca5ef350e6e22160e510349aaa6f47d1946c8eb03430de922781cdac9d0b157605b6bd5edeab37988d0cd68724b231702bbe860ca6544a6acf99278ad0ef02f9afcb2ae86d75fce8259b477c6d5f638b59bed7fd1ce1705093064f307cdd66d9221f9cd08a7dde0ffe647e42707045304ff3a2cb17e37faa4b7451afe24d62af43bff9c175e5648bc923a77b88aac8356f446b82770a5bce46a30a6d506954c1ade703b50115deb98e32973b103b4dc95652dd85c88a147fd360b11d1a845d404078bd462c0a371336ef1cbddf5da0d25c43a715e72d904173ed7b35e193a45b896b167d23f66e565668df93a5f10de00416471ec6d7fb2319e0a157625100492191919f0705cf4cb001aad41c878c175e5a16d2df0edeef8b97a16a45121b812264d44574c51bd13cedfc0793f65564e510ad25a565ce91d540d7f9ff331c074113e12e578e788f87aff283c7ca19f9a83212622f57c80d4274785ac3afd8d271ad0ef7db67ea9798796384b04e0b9a5c8587f773e99d85baeaa52d4bfe31771d474f4aca945b8979c629ea588f8a84f15c7bb32754f69be6af87bd9864d1e33e391a37ce57f36dd1990d2503081c690ce887e3763807810d0cc7c264d688c5c2bde8e524ab1bb9670ea31bb18475e1a988c2baf18904c05d8607473385c4a0f719bdb851896d95d8f6f1c66f1ac061fe5d180eb905d9c17c9eb4b7a750027d9de19322d08039517ab7df9840b65d51127d298d768451765e326078d0a7dabc7ce854dc5423af1d669bce0023fb90e8a96c3f3f8661271e369d10e3a3b5497fb4e8ff1ec4cf9d5164851c3b83a8f3af0deee4b2c014a608ed909b64cd03b1e78a5c79809afc468e80378c2d19cfec6a558a4857df3a9172829b9b5644ad38f08d662da18dbefc771e30056fc0123ec2ce05d9e145d1d029c4642d60de52ecd8bbde6a04cb401596775a5fef5af6a132f468a2315617b490d125ed81b484855fa4a9247dab7076b206575990998f90bb6ed7874016d97528fbc8171b2757c0d509f5f07122867ddde84e3f857f9d7a6cb0728642afb7564ab8f1708b15fbfaf5986dd1fe19d574d071c747e78c2d82429e801591a4d776b02ca389ab5159a40a496eab67f10326af132e1a424f6b1d1f7940c5829d623b69a29996acada022e07e14e168b09ed3ed4050f5908d9ce09c3a0fb64d4c87d427f6e7d7809026b2a4615eae9c77331ece272b6bb1e1ffc505e6aa507dee328276047d9b9590e2ca7fad0c848274afac6fdc5d179c5bc851917a4a62cd3e6099663e51ae15e9cd9e9344c291044e446c7d21bc43665512f2c5d725635b96e38370a8c17b27cf41ff52d4df3257a1eda4e7c8c3b5ab3b82fa828c9bf6ce7656d70839cc257745af7f27f19b3fae73f796c1e90dc65f7c3d0ad735f57e53bf76999d0f77b2a42a978f99d2bd271e6d36d892c9a6bbc117da20d3085ec0b7b1026966b1c9f935201f020ab7085f42365419689a66421964a3e00b6562e3e7d7ebe78ac74996c93ef001fad8a4ea9465fc5b3d1a1d36cad31f3f20cc50c71c907be9c2188613b3723d59d97876df79ef4109e9f5d682aa43df5823ad2ed54bfcde0693726eab2f49118dbd462f41a3d6115fcd74d99e9a55a0c546bd276c923064c58b3656c1bc6f95a9e420f68ccb5a5a771e7256c9c5b99370ffc16b563acc472eb6518e44235368b112b555ccabb82ae6d6cc76afc92b64c31105254bb5b33499ce04ec3aa5b6a4b1299447f6979a2dd97d248ae7df01b668c00972e1cb3d6392b7d87d7a81ab814c6d9327e42b433b5dc723db286fbefd087a7094f387b78c8fc4ce2522731bf8302116a2e608b0b4fd6e7eedcf2a97c92746814f4eb78002fec22d64609cb4b929a7d813c2d6a82e35335adbdaaabf7d0599ae66a822fc7e73bb2021651827a4e070ef0647aab5b7b979427babd70d5a43b16ed6a4577ffa21f7e6dc3709fc2e742f1e241f07a2b2313b48f7b41a79810370bef881cc8ea2da5c8437274622a529afd5cfc5c50fa390151e9f4e10fa3e37033e01a5862879fc69966d006916f478cd2c18b529345d573c41d13ae73c1faae9e30273f8944859e27fa9e8d0a33b337158bf2d141688dc6c17593fb95613a3b0c75c3497d50314b6e5ac7d977c9c33c80c39148f7a48f6949417d76dfe44bb7ed56b482244b0f7e004a921fcc189bbf97857d686dff01e51812df440bcc4b6b121a2e8f1726518912ecb4f6b0aa9ae6101e028ec8939a1de8c3c2aae43e87a1fd3343d74e276d18a0d5b96eb01b15667d4a4f1e2c6d5019d65d5b36b599b3f41ac2d4929e75a172752a7465dd5920e7c811c5d678dfe5f1fb6242f7a2cc1570bb113e83ace12a2f5f8fc186ed3037bd0ddf4dcf016bd16379b633218f364fc2b1ef6099e85dc221b344688ebe2c51bf5aa227587d6115b9c97f56b4cafe56951eb3d07651c082d162b55268cc5ebf7ce314b6f42ff590c994845db84246378a74f4cfcfc2dcf3de228109fa497ebcb6134a120a188756e6b7ab57a3dc1f1b1b9f99ba164c4a35e9c35ab0b96f7b824c78f6078da1e86c66fca06ad94a75b22019498977743d9a63c376faca1fddf649fee3d4550eefb04a4182961bdbee554f19c22eec5f3128e0dedf71d276de72a184b2a4845f0f08ffd6e9e8d750977c7b361ac50245e2571c2b8902973dc3e424b0946eef9438e7acf201c3261f81b395a262fb1d4a5ae157190888780590c5e078a425225e9033b34a5ae5b04cbc75077b29b4439a9285ce58a9068dfd4c9bce89de1edb29888627f91bd255be781f4c517c483ee99d278fcca1e052d7e1a23034093e1eab6407068e272be37ff7947d9e8bf3e10eec79b6541bbc27346a09c28ce580fa816890bfd0804fcd591afd7e0ed9091621a3f67e1d77e6f48ee6917d4dd8ec505bc25b613fb11c84c0e89f2bcb2af3fb85d58166e9a95ff8c32781df9f94d8a67bf8072d227cb1d20785d6d39edebee0d24c61fd98887728cdeea5d00099bea17093d3422a89c575eec2b55fea71122b4ba25ad935564fd86dcda35056536f471a2e8ccf32119adc66895422dba76632352cb580d9eb2901d5e5fc583e6663fe21d380990ff51e98d7ed8aefb6c3ab39b649db4a93ed03d572076c219c23bf2c66bed7af125b0ac3676171e0b595dbee9118a4e779b3337d1861034998e0e7a33bc56076e8dcf33320e6f24e600201b8e0bf741e26eff42bd49c8e9da98fea85771f39561a92e084e465041b25759be3f79b4819c0fee2cb617798aa9dcc9350fac7e94016eb304777be8f4926326c0ea6b9352806033b196326a2a1e5f9c841f4ff30bb3e7315aa3791022c5b3aae6464ff1084078715d51668820256abfc252e6c61f3b7b2b25b0bd5771a82ca86b86ec167f009f2bb273e871ad91ad8801f1c7e2d8daa0fbf38c498da88a54cb6759983db87153af0ff91bc54c9e01f17711fc0e06078f5c10c72c9d1d65106cb017c5368f58c70ffc9825a7dc0f4e71e3be168daf81aa3df76e7914272d91d457f8128cccf0b52d16ff905c9cc74e8e3056bd4a75323ece9723b3015f635858a6cef9b1bf356596a5d70b9886ee42970b2fefc8dcbfcb94d7af1b48832a15d6a270a4912c7a38a0b6d2507d4082d70baae9e6d906d0238fc10c90613e6cbb2321b57786e04d6106373a3c1339c1f06487186aa722a5d141f8ccb20c93b964aaa8bcad69a220b13a27a018c4a5f1301169f1def1f6b4013fe0b26e09c59220c156d07ee3109bea9511f63e10567014f541b6cafb5927df5b4e2d2cbd2480f442e3e56b204632a6f0b37dad9b62aa2764bfcce2eba65583ac63b10db5bc640e5a6145ffc3d78c3d06ef30612efcfce3c395bd645b504d68eaf825f80f1f045c654b8d130c0479707cbfa71ab0053a7136d91556f44cb5106b16098dd472ea3312d4944b5b2fcf157671d291199db71342f632440ad8e1d23058e633e6e0c018f2ee53a6023270205e4d4c061c8f34d10d7ff14f1f66753f52d3eb3a5a0d8ff10b09f734d257002a2cadbaa074ff4331b061b9bce01b97a200abc570641f94c517d6b70587ee21a3a438975876eaa058094e614a74d8edaa942d3b78efe0f77ccae16a580dc7d5eccee2feab405f8be1d63e7cbda607e12507c0cc11628dc73406819354a7d2227c079db0d59bd201003ff59f77d1c4b033f7af12cc48eb9cb21bae2964104756b906189d337259844334d9017115398c34a380763a9172013eec8aad2e2e8548fdd9d1bc73c238be6b80d31612b85ed3c479bd821e8c7d5bf5408ea2d1099dbf10ecf717f1dadf1622b78e3bcc170df02710210de372e9d681594ec57f47844cd36be024dab99ac85a6dc668788cdee826da929f0d5dc72e519f850c91c69a61dc97c673662848c2e7ca73918e9db19ad9aa56d3092bf666656c69efa5f1a214957df05587410b9a680f08ac70a203b44a36e26794a6bcb77f60504434dbd0eff57ba652538c7fbd96d84d816cd551fa987215d8d607b542da87ea0736cdefc988b848f8816a44964a25f91f308303870bb13808566afb205a6a247e0db60f1bbff297925a81f5ea8d53562095a3b193faf9551dc0d97b58b6ec9839e5a10f7e6c435993de7cc493446bda47022afc0b224e925859a54b91f372157d553338bf19d22a549a5b2e111dc476905b032986470bfc23dda55a96a2e16b6627f4f508cd8eb4ac1ebb12994c0cca9586277b0feed6db1aae4f0590af6cac3237854de80cdeccca7bfa7b83f2fc9c9a5be89a3526e39a3951baad7e20a3252ae4f4a1ce4e4d1c5cb27b32b47808a60ce70dceb8f53cc1cad79c5685b1e946d146a0370bdc4c39db43de39089a5717d63f12ab62ff527b0c671acb93b690038736f9023a13f2423b0ce6c483286d1cb53c62c3e777b1f8a11d3328953441017bd84bda53ecf3833f6619b9d49bb266f015320a590db04f79ae50feaddae4f040d9f5724ca2209c2318bd6a4cb20a545c2ce7ecbbbf764b9137fbefe273237f6ed59e77c0ff3e539edbe208fda8e643b3333e98782887ad7fbdd9969a0e33768c0166de99256804c70fdd367bd043ec8ef1cc731417c11889c0ef17cb6c87e6a0b90fdbf5739970374abb6b67f51df51af43842ee47ca12bfd97b5d5b5012d5cf311cbe4e85e20f68d8ce94a956d668aa40bb8c29c327618d3a3331eef29187e82bfc91da5925259a58bbe267af4954ccf4cbe0276cce6d11256a8c572c233430d6fff7facec6d68469d9966260d5653402d8dcadeedc4e0b7755637154cecfdb12ad2099a605e92c6503912f7bb67964e6fc84d7dc3655088d5423756a9b27f46c4aa314be9e3723ecf32a4c6084ded77e02821833757a83a8bbf88b9dac5f219e9db0a420a1af890b725a485f63142f98007ced531854175962ad0c92f7f7e37fd414e3018199e590ff15a11be4b4c315eecedeac650cab0aa820f67f8b16513cfd0dea5790b7b465140dd82a3afcab93588fb813c79724f0e338e0459beac813eae788955b9d217b4f1d3ab882c99bb1a0bf955ccca1b6bd1b9d3133b120cb41473f8aab19914b5655e7661a9d2b974c92905a44267df30912d6a0a52f339290bde2422f44565d4404172f63f0f3b7b0588c26e02b2be3d2c1ea0d8cee67a025232aa523e129973901e116ef62d10432a4f81b7d93e3007408e3c6b252a0565f516edd046590dff579456954564714d4ea5faaae63e75863734f38b241df68846506d774cadca4204849d04a36135a67c0c133830a0c8ebc8f9f2fc56d5726ebce280c5e54adef480b021ab604bbc4d10abc8003b199dd6c4dd79442482b83e4616217d9feca4ba50b03dfbff9a2e8cabe94cc67116dbfbaa9cc19b90bfcdfc93f3b0fe0dff5d1a7d228b157ab922080322e7ed3ef1905b244a2b6ec37641e2ff0d1751be5306565b9b7eb55946842ea1209eeba990150482f8afe72b6c9f3eb303c0724915be41973ade5d787c5d9e24b838a74e691ad49895053fa30283d27342c71a3d172c8d443118a56ba4f96a61bacdfd83c9b118388a443520521174380fd41857593dbea32d61078a7369179054460fe99a020754e7ff0737083e63dd7cf784f40b0b956bf99ecfa5c2408429e2cf0f51753948f62149e68f8e6ea53d5e8bdcb89e21933eb6023d9553cd132dcc29d5b4d8efd501d95a273b68b39ebd67896968b5c9a0db8b919a9cf991adb1e1eb7369eaed88d4078a18fbd47a14d3b0982f2b9b01bbc53e688de55d97f8eeff9645faea72aae4947115c4f8ce85c0c5aa2227a146bd91386fcc452b3d785c93832ea140574316c788e825e6e73903b7581d9ec0bcdb599fc553395ab53b5b2ab43be2751faf4d72f68a9a30f5e5b04a67e632b5fa90614a2f6ad101df790d0c5348c86ca6b78105374f606d72c0697eb92ddbe206e933527b3122f03d687cc52313de5059f7df482d7f51c9a74f0795170cb95a34784f72db97783af3903f6df34f02ff55aa4e5faa565b05831f85bc272d4a4a6c8eda84ee80e6d3168710ce3956e7fd4bb28c5e1b3cd52f17e33c04af4fbaa6a79434a1cc5076e20fb4c3d4d1f9259e9897a5ada54421b9f9494067704ee0dd57d7abb616d03e252e179a23305ca6b5e4495c4d502f47ecf09c6f9f90e883aad7011bd2371cad35494d3485a23691481adf8d276cc388ccb8d2f38636e0f35930836066af2ebbddc9236a780e091eb737600d72004fa208e8ebb49d93e2b884a1354782c8ca694c67c80da2317c8201c3a73690ff99fae18846a7213844597448c37ebbb3af2216eb4822e0bbf22ec721d09aa6b71d3bbb18e0c2828a781cb2a2e7c00d7ff435b5996237ed5f50c1fab7a4a05868927578c6a5b47911dd7cf85ac62e5fd09a716dab0ff376564fef7d8c3503f9fe5f2c13ccba7fa641cc04678e9ee2a25fa8612a4e4791d6f2eb532b131fc409a63bbd336aca1791de26d6d168f5dec1f1cc77f6f90de2826e1de496e3b35c31ac46230af97991bd58b96b920513a56ca26eff914f62a959dcefac2466aec684b5c025ba547d1b56b7df4cc5df8e84e2e13482b40ac7bb33250023e21d911db89a08ac1a9833866fb1fabe3067d75b3c178c5166e2c1c7495a0a4694c74db3540f61aaae4c1b9f9a3c68a269edeb584ab847e6ad03fe6d73fd5b9efdb0b0e3fb08e820ba7e0bf158c694f69e4b26a25e7fe4d0ddc451ef8738e636572d31688b712bd042e46242e0997697f8174032d1d30cf5f8f5b7b587e604800c4a3e7c02f25edeebf47ea5e0a8111f78f3df12501f4a3670c83f8c6ffd6e470cb87d269c35117a38e83510fd6097d22144cd2b908967ed0b04cef26c568555daf6534f5519d6106548fa9a5b0e66a0a92cad196498d17f021e2768c6b8982bbedcece87719e552eaa990c16d0c179275178321500d84e5b912c8a1118b646867e70c4e4f9ab387ba7781ba915a04545e9f0202c8558d49e615b9c8065a61dab695bbe6ed702bd13506ae325e76585ada40d72466431bf89c69a966bd2248639e16932cffe61b8d6ed37978d25b01565bd7e7ce6d0702d4f8d2b28b2173a8234b1b41df9dd11c64b01e069784570dfe77b77a35e929c4e986419a75696fc9b65eb2511f34fd04c8baab453268802985e132bf5c5982731f4eab568913f1a1576d7d1c0fca05a8e47b77e343e213fd759a63352f05e96130da3f55ff38d3ed49e6fbcf7ad301cbc652c3bbbc7520d5b6e1b8be2f2495de829d70544fec4750f61dd8d307431c147cc7bbae85f570fbdaf058e849a0928a61cf811aae4baa608c0248ddb88e23c7d784656ca53c1c72686e6217654284a1e1fd29505a3cc4a5431dda21b5f6e884398d1a10e022856d836bf1e21c433651444f1248c006c2c7653db06ef4f562adb59cb59f8ad23be5b3364d905a62a0abdb3fbc7e06d0cafb09e17524a22252abc835cb12f6dac7a797940a97f559cfbec69d14054f6eb3c8d3f77390ccc3184f9974c8f0f69c10853c29abcb61bf3fc75b94806ef7ada9f76a199a41d80ab2a5206bde325736c71a758386a6495691995d3409c644d2c8d284d19b9717da4fff602c8ea5e0955dcab98dc327f44ac69c4ea4058d534080d20d897d5314ce5894ff691459c1833f76174eb5ec21230b8d4b271e6d7f1bb9e0b21d1670eadcd8d8d49c76ef5f3bde83ae7b398e9aa5040b9e3fe1cf0c8b124494946d834a89dd8dafe252e2033cbe5e46b1447adb8a5241ccce9ff7f43ba05050dd3d346413e5f8f94a2c2a2f8ed9a0b8687fe919c36d3799efe021901eb176a5a81318aa6555b68bb955fb304664056788036e5eeb989e402c8b6c518a4ed1fdcfd3790c64788048b44cd3fcf6578c58b2560fd33021e5946f5c629b06b70328907fc06ba07ba9cd08fb9739f63a869a4a72895bbd18e811c01c8cbde4087c6e0730cacf3b42b0f087cf25efabb3e093f3c673e4b8d701f1bda3af8ab42ca9b22ab3c6b274fb6c1a63dc2fa5183673bc0274dd59ee49579f39ae91a1f70c3f1d906b99e77b55b6382c9ee1207364d3b608052efe50eaf8245fa63adcdffa73fa4e3df06a52b59c13dbeabe8ea6f88595fba2010116ecf276dd211678ea08a5d6c88e589cb948d322c4857406db0f38ff533818b7ebc13dc8cdf39bbb117ad840b4ac7798e7fc05daa8261d1e2a1efbf53c1788cccb8cb8c7baec51198f781e82b0c060a7d9caf12723be0962df84b8d45c317481895a0349cce38aae7bd801b6d06ebb3526931f73d6cca57f44f39a30bd541f058b8cf7cb93dd02bf7bec731df47293eb123579f61d5f2a192a84a0f9af65534d870264ab4ed451ffeadbbd7bf0dbaa5b41facde8163651c6197676278a541a39dc1f95571a5b60a2c1e4327b00e1da5f85990f8453431549d2649d06c0b4ed8587917cbb6e2ed6492a5e9b0035fd9110099ac4190b50517447c5a17338058c2edcbb96b3006c5f8ae175014a00e3e91b3db63126f7d3f76b1d5dc053227b2b1ddc8cb07ce7c1690cdc3e34db07428d651bae62f834e1763132811ecd8a21fcb3d0e545e8b4ad66f90216835c1f6c29e0e3b100df5a9923ff0b017dc83d6214e6f350b90164fac94a45162ef79d818fae6eb2cd941fe6619e4aa21db48a2bc8cb4894602e351471ad1454e1317efbdb41201fccb651a0e791757dc9685e6ae0253a150539eabde54048826608cca8114a60ca3ca974aa75bae23df0441d870d6ce1331c8275b5d367f9512811d5d9c56450bc84dad7a6289a931bdbdfa713d66e4cc023b0b603a4c9a85cb2600c22139c64312ce8c771d6aa847192d7b85b916f891b19c6fa0b4a36ece5a9940a0dd433fec002b81e194eaad7918b02f39054775ddf9188c701ab11cd59405d09ba872e9d91c35220565989a51704750027c80b160ac6c1dd0305d426435608d1fa13089545522d10cd461c5bfae2a461f601e2b16de2e8bccc3c442cc4799a2611f10f88d7512e1b7adfea18c56891fffd77b5492955f42a6eccb1907452e0d95b630e4af5cfcf34ace2b42584ba1c35b8e00a2bd8e431f3c19d7747fb988501b912bc6adaf2f3d63db199acf64562427f8294244d72d12ff70022e36003bf80e0b8a16df9b48b0ac3d51ef85806cd30b34a9c44adc7e6d19cc1c40b6307e8d9c0f46a85d0e8a2bbcf93d27fbcb6e3bb6b5db5e194521727da24eb19cd3b1753d2c2c3ec6a2f920dc6b1001f71501535e04fd82b4d800ab93887f5fb918544ebc9bf7652072d8442361614b87cab4c3a50875937069a83e4554bdaf45666793ee919055b73f6960f6e4e2725d896c51031c82667f5d3beab88a2e214ae110d1853725e493fa39849e6a46512e47c9bce6c014ea5562aada5a1e0e647437ae7c2a05bdb3b41d6122d0126aa60a1d602bf8a76216800abf6b210044d9aac9ba1ed8a6dd4b5cb8b432eda9fa7c764f6bc82e45c1fd1547c2de5bc941bbd70d741b150f41c3ed20f680991b783a0b81b4bc4d1c651113b7da612a49bcfad025eeac312c89073d9ab7c7ea79b5ef25272ad114d680771a96fa28b9b90a2481670f0882147c09b213cbbc5b7599d057e33e68d923c34c1c369c83569880e085bbcf018213b366ecb05e57dec4674cc76aa78cb22517168a3032c70e307820b65ba924a17128d9b1ae6422bce0f065d17a803c446a8c71af67130cc76f7fec84accf6085167de1b4197c96f0cc4658c4955286eed9896ec3d0a5fc0daea8728e8290ac29ec4bf5c87874799e0dd0ae554b98830d6be0f7285b1a3c4b56519371b74a7ec591c2d4c85987f8fe0ee5c3787c5f6375aef08b7ed8254baf7ad5bed989368603e1fbba57f756041cf8aa646be3f1a34be3fe77c7299ebc0a83408bbfa83b8d39a51b746c0c30fa3d0cf5f8aea9d9c997d3948eb4766d4c3865152c389708fc94a1630fffdd370511b8588304c677ad2c2bdf65e66f579a027ad5f6bf61b779904f5943deda1478ccb050cfb08d70b85754a99ea05e5c4cf05f38f76cf72556f776316a448f7eeb98b6dd54140241c90b49eec28066b0e2e484a133f314397425bcdebfa80c23d840e9e9d4ae2969ca18c0f50e88b50f0aab2d9ee28dab0ba620624fbc44462ce9cec183364a353d3511f52d07b53cf9f2e041b8a999f54cf4f8e9c694719a1da126dff4d23e845826fd0ff80eb5e66aa51a06745c9799373005659664fdb2aeeb3240d90f23c69da2df273df9f4065e2c5dd4082e12565167a4de515f55c943283db2bedef882338f7ae7d3ff5be8428fa8607bce1401cce8e9403eb598cd9c4c27f46adf4465b5fefd4601056696795d851cf589c84b53f65a48f5d6d0af12b56b4fb16d5d1d9816c04393625c58c82f2bc0dece43153d62f4cbc27472aee3b5cfb0144c0d8dcec4e1734efcf12fea3858ef3dd44a77ac9d50d46780c795ea79b7d7429f0f402207f354e95a1c37675655a2daac0ad88bb4756ba9dadf73f7d4799a0d47bb18e2cdf2e14f6d54045107bdf6994ef1bfc832d282bddf37e10fe96f4fc2ac7232fe451e15e3c67dcc9b3d38281d67bb01a2ede203e3107f819127525d43a0709277ddf173edfba0a7486866ba04ced421193e7793f3aaacf2bada42270242bc3949b3af628f3add9c86cc13e69ad3f057826d71ebf725850954b4373b1aa4710f978d103660b6f332885f0f40593d1e633a7f8bbb12c183f5adcdba62ff18077893abe03f1448bf1fa1c3374b506b9c5dfdaa4cae347df95457c77765043c1a308690649397a01ed01f3498eee202cd71eefb96a8a21fa0d5e59f5300bcfbb7c950fd1841821d0b47856aead495fdfa93aa9ab0c257dcfe78cb7cf6dba504447dd5dd46e852342d6d0108dab9c1129d6e1131ee190226bd1b2d233a90c4dcc89e611c3be265b1c666865a87d8cfbf160790be47a1f0b5ec545369880c8610096d0b604bc7f9b6c3513b5e1fff8264034dc4fafbc1e81b148f721944b38b444ed1d383cbe04da04214cd79873372708c32176f6d6f37f985596e41ba1b8d4776fcb8ce5a116eb91864e317431566f8ef339df6147314227980978e5af5aa817196f6e2ae7ec8b2cb8cea0b1df21e482a52c915e5aad4fe6764b1a2940ba82aaf028b1049e257c03d6319fffc7a9100c36e1d6d40a1ce9cd44bfc1e26a70ae3fbef842b8d43fd60b9c4e01bc5dd7deec3e67b2c663f0a4a44c60e95f9c7e1d5a4e2dd57aa8dee886fde227e1c70fca0af3aa525b903895c6a898a7bf98454e1f19c236be936e9dec59bf9813dfc420143362d8847b3fcfc69f3e4a9157605f9d864fc6009c1c7c9f00597e2302263bd30ee4c4b8a291209ee20943ec18f51205ddac6377c17f7a4c8a8585fcb5a8ef414040e1959ef48a2eabc3ccf4609497047fab2b1197fde2fe3e4fe06fe1b090a81c4d2072eb9c7072f38e96b5f4b7607e02ff404f04641c625e1210d09626dda6cb09863d434f615b1e2c0d9555fed3ce1d4c4a93509087d6f2ff35a2b367545c951d7f0f7dc3075593d362090a0f527d09b1db306a902e56132f1747831d0cb63be7ae7083a9e728369ff0e3ed1c3ab2c55d4548b775e5c8a58bca94afb3f24cdd147d775eb442117ff6b6ca66cd6ceb29aee5f676acac1111b3eead2541a9ce360e2953cda623d5ca751a4e754cc4f24e776383ed01483539d8e2d00371df157d9ecc87c7fa8c2eb802b56c2bde92ac56b9c99ddf6cb5106a67a0fd7481fe45f911529ef668a47119f8bb3490e4685e064841de4714b1473108ad464ea8291c96b40c8082e9193df35ed98cc0533a709a83a88931a53fea1fdb94bd8315e24ccbb57c5c418355272dc909ef302bcbff3299f746c3e3f3ce3f5799bd3a911bd36d06f9d578c891eecc3a03369cfe759999d9fa9ad467d529bbb61725066fc9325761b771abb13671074f5aade66a696a2ea2fbe3c39b24d808b2a72186e8fc051666354d6782e670adb2af70a5d0fdb8b43f44f9a87a0e6210fa71e80e3b2423ed3797178ec523a1df861d562a658cac092c29be8c563b00428c1333f535ad68453445cae534bce4c8720f621e87f9d4dacd8bd9796e9363f42895fb8fc93396ea177cc7a880a024486c8289350dede39f257e5413af631196182fdd6c30cc840a2a67c7fb8127dc299739b430d2267ba676544d3302057d5f51651ce2f7b5b683555cecede408a5fd2b34bd0e29f4ff5ba548c5a8da32d2992787893684947d8efeba8f4b9ef17f2bca2d3efa9aeca2d65f75a75b4c82a2fc379b2d6870afa2a40c89e3104d5101d7849efad728584a86689372abd92e4063149a3517158599284f71c8e62a4281b3adb967d8001c79e82c6389597c317d0839c4f7bcae47e2f8a8e36f2442259dd32a2262c9913b0bf6d4689afb95ec99114764b3db9d673f46ff76577db67267a14eb91f052a120b00b82229d239ab3f351d64dfd4d4efab75ee1542693c03f7feb6e30a0ab458722d5a3c63f0e6137277893cebf2d9340b14aabf6be794a55d78cb0a625b32afd0cdc3e31b57cc4bb3f4eb5da9f06273e1881a53d04eb3b26bd5ef433a7198008cd41e864366be88edfe07c63414f669256e1a665cb0ba8b3febb3448da3c4189424cc0df2e511efeebf9ee47cbe53fb1d77a7e74b98ccc1b15401f2acd4baa8332d8a3bb1c4f41ebe72c18697fbdceb48fcd55fd5cef97024f4d832d575db13c0b3a60af9f388db367b620ac0434defb0de88531af870b85551faaa1cd5f633ed32da05f91a3c52eca6728a97fd107790fed2ee5379c8ce8dec29d3db9c6c6a9f3cbab22ae87636d0b5a80217138b9edd7c4af81cf654091308e31f3be7aaca5ed13d0d2fec3d1133c900bcea9e3322b33b93f0a1f356124d981e8b53b26d57c27d3aa1cb671956a0674fab6e2689a02babcad6d317a936adf148cf6693721f53df1bcf342e1eec1eea59d0818b39ff2e95507a3251da5e1d0b42e59ba41858eebbcca6a9301a20423ee16919e0b0ab55ee39ffa7d3f9ca4a48d6a36254e4f848886558819b53089d72477aeb7b66a2ca4f09180348ce2674af68a8e7a787aff76fababa5ae773736726c4ec097bce39b3404afa528260cb7be02e38cbf04979aa50758d9e240d855b7b09a00a69c15bb46c2eb4ce19fa918685b19dfbe68a0e813ab8dd69efa31dc360f127b9b637b2c532decea0fb8d0cb9b32df8f30a28b2befb59c0575b096537be4fbda581ba182610db051f8c96c5d063d488907c5085576afe59117f43454c7f607096932e23e3ad3b4bc2f26fc417efb3599ec5a259f18c8d429492641403ca527ccbcdf26bc813238d87faff008e294f8dc4eb9a83bad55c019bc70ae39b2925ad47acf1071d9f46bb5adb24259ffe00fcf9f43caeff62db9900403c8cebf3b860ce5afe704fe78262b47efecdc8edb901ee89f747cbc12168de631bc62525282ccf99599d54859fb1fcf7971295e68316524c783f1a76235d1ab460f7792f1536e7194469fe966aa176c6df9ed0088fee4d57610679f99d743f121fb4566ef1535d04f9f54ac8c027f432734dc7fa5ff17d3d887302b5a032382156940f876e5b13f06f86b32ba7833cfd9a6ee221111abe057a2f1d44861d33aba6a1c0787c7384320e948c4bebfc95313b94ca6102c841c6c8da4604e988136a6aedfb5e07e34eb13f3708ab5adead82fb5cb9780321ae9f3d3c6b0d21cf32092d444417835f85ddb4d298913dbd9056a0594ec56848a617d31c888a48389b0742abc3b655c06aefed1ada91c493f7939a8a9b421eeea84e1217a8873c71f1a124504f9b167a12b415db8beb8cbe337fb249be876c512d7af8c80f13b4726f38cb0867a9e9f82a863c7120a71520dbed7e398997ef6cb4278b7d05b0c4136c5de673a33b88a2a63f1b79677ca6065697c5d2b7de73dc952f39168f78b9319a3934d892bcbc420404e8f2461d2ac4339c470f4cbe0d8801ca6205dfa285a70556bf97d80cc53beb37ccff662f184cc756e4e91708de8f9bc68e1ee578f12dbe695898c88049de36d723199a923080e92c484941ae3cdaf1334d3cfde52499d0eacda23d9d1769665597a3b7389ca6d17c9a484509d888f31349f6e45b4db399c4eedb62b7624c9585a3414f3a9976b2075b23acf904e36ab91826fdcc6984acf3ea29575e87d91cce007f69a85e5a4d3880ac397cfc63d68ab0d451219acdd1adec4cb53efd9f60fe1d4cb6f486fd7a7756687d7cf0a953a6b6f02d0833830146bbb14e582773c2522455fdaeb802d315639a2aaaa485d146d842705edf547df76ebdac18c31d29042fdf7d58c6e71cce5fa9dd43e40b2f186a82d1585e3f5a4e1430d548ce8dea79ff52635e9e22dbd1e6bf3e54ef7b5b91b0ab928486e881ede14592f5ca2022299777f2dbb412b9753fccc61c5babe0255ad6cb476389c1c8bdde0c410847bfe4585572343213e5220a978dbf8d06e9d1770024d6896235c353513b0c78ba66cfafab9efc6382102a7d230612b6dde26215586f3ac8e76d0fab343d3b08dce33a714d594f8ae3a8eaa4b8d874e6835dee51479947a634c69752d1ae3a488e5bcbb1d63b8a8bf11ecbff20987dfb233f340d61b86c0fd07639f1094c4168270b75d4800fc5ad7a1525945bcdbdf69cd2799b72bd6f1be64fee30f25536b7e9f23fcc94b7a64746ff478d1c2394c38935bff0de7034c44d24652fb3e13881bb5cc1839bde388df66e37b5f67a5cb0723b255d742894f9f0eed3e531be5ab7010c42f62b7db2503a3b2d646d5278b1a5688e659e63716f2ebd948a237d1b1ebdfca8d6c0b7b086a2a223216dcbb55a2d2d6dc0cbd1ef79c2e5b1c0a804b9442638b7eb370993b1d654c002117dba5925be577dfe39e9afd5f97b5c222b4b8dc622365fae55c8f93fe7fa199b890e678b90fb0ad3b19c1fddb04dac53f5a79bf5f6bc644185fc782acd3a13171d4322e9d87457456943b4b40b3db800f8a39e5991272854e5fd462828a2d7ec61ea7a5d839aa42d974d578c443a07ba88e99a8853d4aca7b63790bd058f76c14256c8bef93a1fe42306439b00308ceeb2ab85d2c013e1928b2d41dbc04c84fdeb887173fe025b308e66a5f064f4ad7ecd63d6aa10ebdf68abea26c65a57f8c833efd5b15fa26408d0fe9683883e2b2d1d23546f215c556cd80e487691075f82dcd9e50c32c84c03fcba4c7309834ffe1200ea2cf7170613e7f9796f22f4ed785d38e4f30eb40b9e79cd2352cdc228ab8e9919a78f7fe0c09eada6d81262dd39e29cea8559ec4ec0bdbdaac17ccb0df2e76b79ede1a3dee743a2c79b6274d92174199050ef02ea50ad5eb0bdf6d070f358af1306c67b7303f2465a38eae68c17374c3ee05822d36fe497c365fec1b642e918ab12722164aaabb290d3ed1a64f97454f20ed1a84404e740c0e7813136861a958d69d0b2ce217ecf79fbe7bbd6728aaa634a3970ee1b43cc9b5340823632c8effef7bde2d939142f547affeab84faac3e84a1c3c23184f5d3ce8a428b894649f720d94f5d60e0d53c071a8e3b781a202212835b31cca49740a6017e815c3371a3f16b5d5d130d3f4e3fa41bc7df65e7880faf22a3c08e30726e2bf861819fee4ccc1a5780dd44009a70cfd915540d44e43f3eaaee4b7f5f18d7f03838cb636492f30dca4dff51925c9aeab0cef1c73d1a77e2da773142759596ddf07ffd66ff98bd6af8ea7458806b94f2e82d8f3465682c965288a4db5a1681f9eec77e2afab11a2978c3d4ab15905c37964580d27daba0b638f66f9c6a95ef7ff55dd6a3e8711fcabc48b48b7aa977fbbfbf7dae6897f48afecac000ffeb445dc7c28ea2f7e3e8cc949f0d58903de2691458258ab66e438b3feca716ed87b7d9c5f06cd1fa05559de4692587db9bcf5a6a22a58325f7ad0b5af37c13fd8097448229faa9e207dc4846cbc6a34dff93d835e29b501d65de0d6ecb663a70eb4be72be4e780e9a0344c9d0668d05cdc46761bb4a7e553e8655c5898bf2cc2d052d76be5e564cea382746acc204c35966b324101cb7c75c56c3f1dd8cf487281f8dc8045a8feb590a4ce850d40edae98309ffbdf66b85cc39f996fd7c1304dd0aef93a6317d0bc4fd1f14e57637bbac0163a75898edd34b3a6c453d646d8980d2bf2b060f3c1ed958f50297051baa26a9de4f3f3713c333d43b4e65f7ef178db5bcff7371f692fd06dc3f4ab1318c86fbea7856a2c7b11b7621b8cdd3e00d6c5d197323ba983f613412c9c2a230836c957809482b462852ff27c361f04567447f2876bd754aacfa21d718a2c25d069b81325eec4a88bdb0eab84be86962fd5748a2d3cbc0b8a00302d9d9869c6296bd5c6cb0502e855fcf2ef5761d047cd8e3fdcf2c3a85d94415d51c16359af47a24cb55b8644b86ada170ffb77596606307f2d821ad0a811f51bd7bffb51a9785657d2a8bef6a774c2d4d8ac872f8c3005d70bcf901ad189fc3bb86b50b42fbffc9510a6938ffc38922cd5c631679dd129730cba035e74c634da697eb15f10625bc0afdb2365114abe8373e2202411b36178e77ea9e41fbb46c417304116afb2b0021567042775d84da6c52af113731c8acb8d11ddbbeab4cd2d604e25a248721cf90377f60b026da73cc3fc9f592ae6cf50be100af026dd0c6815b3165683730ad3d882c14e9d9b4da9b35f879a0cc95d657a5ac458677f970f26a5888de3dcafb2e78b176a14358d52159d612b0aaf34b5ad4551ab2a82df5adae9189c18a5274558f74f2ff1b481d15e14b65fd78f81d8684270e020dee80d6c42eec3b0f70ddb966ecb87efee2129604c24d7ac8e638e04a926bd00b5fa4902364370e01e44ad709a868d39134696761e2aa19df124d76c4f1219424e598f5519189ea9cc934132fb646df02db4d8ab17a3e773890730e2b57ac449c953d56cb29818aec933e598f2ed95e39439c945d7690be387db444f51681763ac1564d47a220f10ae257fb508d209eb4cd36f7b49415df6737f6400305b121beafedf07884d374ef75f0059518988542162f6fbc65513e69296107d525b50189663c8bed6cfd4ddaa0e72a1221626dc58d8a2311ad62faf8e08bc97c5b555a5ffc5db348f9dc6e6ea217bfd64de47c5fcc9b1e1cfdde37b5795f377356f78db5e576d47803d2bd0815472dcc091666796404eee55d144a2d20a98a1edcb052c5351a9f69f637013bc294a7a2cf5a25e59c2f088a019d4fc3d166cb05fb73ba9ce9c249c8c621f33845b50628db4aff5e01db7f9238cd5a287cbaef763ba181b0340be9656347e3d175ba116e92d05055bd18ee3603e021c1c43fff2a39ba52da844972aed29e7ae28cc1d0fefaf9145f2c82fce784f92cf8117f4a29452092194b94d7df748cf67370cd0466007c89fe8fd8b0b6e06441afac5fdc8498aa685b252fe5566e1841871807a9ee437b8bf5f3b026c1a41ffbc7dee291632a7e1e4eba594570a730e77f3738cbd25e56a59e833697df07833aa6cb2e4c08d978f651127d7fb35c2e10f861d0980f4a015443cabae077f4237ec8e7f8b0800d2f2e7bf8cc17d7c193f6de609d0ead35bddd36bef8eca96b1fc62d69d0a34a826bae68dd1f6d0458d9cfd0e7cbb99ab0fc5fa4b7263e76bf95ed345f493d9e570431d554248251c406cb0d36caac2ab95c611fe27f5d74ef25c8a758e593ba57b6cd57df43bcf7c3107c4a531b8b1d8e81fa67a872932c8dc669e026e0d8d1c282b59db48331d6e057311a2472e3417cf0001c02064acf3bb8d1260dd1184fe595c8a3f4e5dfb3b1203ac68c17adadfb6970d6e136a30daa7cfb0bc4dfef0e477c9aca6241da67b24e245caececd6a30df7bafb34744695724c46681ffc910ef7dc9d3624373eb0cead7abef80077a7bb6ec85f9d548017b026a05c1c69f8eed11e5398e1fb1f68c71398cb9e58cb48acc26c37c8b6ab1e0f42f19fc40bfff5bf8c48ed92fe42b73e86b12af642fadb0fc998059aec2cae8de2394befca90f4466f243e8c43855bee7efa5625cacbc53e19b8e9481bc92ac440432a62c2f669a08ddd502d0a42701e03ee4504594db9b05628514813187f37c23b940e7dfe9f61450ca9af4fd09afda4052486b0576b2ac449ea2a9429fa009656be7657d1799305657095630656cbaf6ce0283ad062286d03ff869cf9a27c75f3ac4dd3c6672ec90aaa0b3966c59a8c9e663a7a0c13ffd1fbbdd76b01ab4906b5d446014924126e19f6637804d5bc287309e7560525b2fedc58bd1e2c1b2d34c784b3f4be001afec4ba6a9f89da6032e707347bbeb836465c90233e89812516c87cc636c7c57dd82f52c6277e31c6eae2269aefccfe2b967645a428bcd3b52fbeac1bb2c398caf37751b36c9341be42b72079f87125593c95ba8922369b294084801b71e22935cade664b0fafbb230a82d09e02e1864aa426133d03e033fe3e370ae50ac81070fadcc410cab79d1fe1dacec5379a6369c2ca88f2c00e32e71a7bbdf45f12ef426f5972ddfa6964109714fd6f5f389a744d2d18b2d7e3a3f27bb59380fbb507921d412101275e681cbd1c1ae8bd919581c73e3015c50bed0f7d217d9fd79dccb73ca8394049b50d69c39dd3f75535bbbad80f3c6b9bcdcc814968d73c7bf3a44a4d95fe313ffa50aca3f8efd2d1d33b4386819b0bbbd189749545e13109df13e8d1f09ab0ba3d95bceab216db2156630cd2deea626eaa168addcb60573f04cc0648fda85bea344f158f8a8f67573128adffc03219059a19c8fb8cb358ec730afb4727f89ee100f6806c3ada3c93f242d9baaadbcd4185058db5b51fe1575060989286162ab606621aaf484999ca42c22cbaf92856f7d385c0112e9babcc2c904b344d91d3cda24693deb7b50f74e5087fff6d6d415db32a2f6a60702a846584ebd584416a3b7efd3491dc29d358bebb88db21e6aefb07758ba5a9868379515375708cbde144c84827b0eead61e6bca52ddfc09940b832a1cd5484bb03b547a3b179e69d8f81d9d1371bf79001be5f5e96f78a10f8f5c9e17de5079a2f11e3a0561cd8164644be74c73630eb76377e6983948687f98f4fdc43eac444fd4b8f07ad42f737a945e41ed8e07e16c72f9f0e91f6002d86636646c3ce549ab3b64a7bf03ecfdcb1b084ad4925990d1ad52982596188ab638ecd2dedf0680f1c0bbd3b216bd94c74191e74900cad3a7fe305f443a899c577d37e44f4c8f4bd41b2946be008bbe3e3f750682f5b89260c9ff91f7d13faa0c8bb8e45eef696cede7b77c7d93f8497026054d00d7ccf28e533ebd932c7a8f2e2962ec5c9172a44862a50559debc766b2d739daf56ae779ad95f0e2c9c9b24fb187bc83eb0b8ee741a44743f7f4cb9cd8c1d54332e3306a228a3c69cd2bee3ec1f80f63d47fcbf4eaf584fc181049350ace7d5afa457b08f513a7dfed10817a46759cb30c0eda75ef1d9f5c2f9d7ec773fcbf489d740fde71d80b0e57114e80864f35f02ce15344284371b26f1cd34bda59b0d1f51130d2ce7fdc8b3f328913a38710859f6a9675cc1c01ba0c0334bb9de8aaed4ba7b084d13eab2438bbe88eb56adc6f9a0d8ee5d5908e4de1a9aeb7ba7fae5a18c806eea761feb8a7b661fb60e203595ed2010426c54b3a09f5d6d811dfef6cb2e2b76d2b3d9253cb77e3456ed64424600fd3c622cca9441f1f42d6eea58aa1ab58571e9a864b85f02d1c1fa6d9d88d015de686078bfacc501bd12e75a6b594d7fb6fce218c586c4e02a1c916f266c6aeac4c64475b95ffb8f9af566a3a3c6ae24a2e27dad8643e8ce83f7da626d23a2f8b392afeb6a369b356a2c192af5cfc1918e93d195237f2ed21996ebb3c0acf40c6a4ca596eab1868ab318fa8b52e79f2a9347586bb259eaa2abb84fefd1e146af57be2660312589b6db90eaa5978ff11b62bbd7988edd4ec148245cae6869e600b7b3cbde205943ef7a24685be57c28f672473ce8801c84ce475950f55b7f1ae3e9c2d5beaf8fc249072334693ff5916d62550c1991b82e942b6c99905f49843f201395e1e0b31f17143d3a42e05aa18c2dea1da8805acfd01758e2b68bea7b7fd25e3e63c52b2cf1fe11c2baa8dd49afbcfa9ca799b51915d8970890e931224d0b007ba546e60fac976d7ac1d194692583de992aae1d6955933850e04f0775c7005acc2de1075ea99f8d5e7eb478dc41a59be9ae2592de2fb975f64a2c60b9802be8db85ade2ff57f31de1daa8165a86096f697677b57fe24669091215e57c64ef72eca80ba74e288335bd69c61c2eeca7d38927977c22731125d1254b02f5552f886de1f09cacc151904763e7639b9849db033881378c8cd8c2da990bede5c60616f288d2e34b73774b108edf06431aca035bb04651181394ca12b426f605af79016806be263aeb43304b3b6cff89acb1f1eed63f3ac920c33d43703854f5610b236e12c38345b5c7f3b82424b3e0ce00287716dfa87fc8ed82b4e253054f5921caa29050d7988f3d7c429808e5309581a514cd01db6de17c97137413caa2ad537fa408d50df2710c76872ea5fdb645733d1ae7157f3e202f570867258251cba6f8b4de9c8c6351810dbe446da88beb542738768f485548fb975900b36a373bfb7ebadaf84b1771d8d1dffa021c991106c5009add63291e2faf7580333ee81adab7a4c358e9d0f73c3edf976d7ae67c761590e3b95c55a071a80b521a4c04053a7d6fc29f887447764d5a2d915cd5647f6d45d6f87467df16f3763bbbd88075028318c5a8ef632b8410c9a95ae721a51f9e7ab50c035f523d4068dce5260305d73034ef1ac9187ec719d60b9238b73a0ae1aee91022cb3ae5b93dcd1d0d818c357746dc797e80a6da5171780233feae7c0815cc5f9289e9fc4d05bf1e14858547f4dfa74410d56507195e891e786550814531446eb959bea0372df6b1cfe36c841c06ee53a4533e2cb9bd59815f38049ec6be0073e9d78c13f4ccbf3bb3cc8e443bc6164c08910865517401b7d878153027057c74badd9e931a1fe78547194615dbfd6d59dfff13c51d3cfd105f91df3fde2eac985d0917d784702d39d36f653e4fa724d3cb1db6871d6cca8b1c9d5fa133f7b9dfa59345e3abe5a06e0234e82eedfea2ea524a05d642c0a4420ae4182563c988a13806f583ed28965812f16c2d6e6db238f33870c7c9479d18b74a49f89092d38e827efe2e82874187da846408a460a44266e710f0fd8f53c59e2a12ab0682c1222a615a3e1d3bc9ab56a765d7e681f51efcb8fb153a026a0146d785be701d72a1194826cd29d228b7c223c06263a363fe1421165f08f9685ef6979c51daca81017685dd0d7b179ac92128e733218c3f71b960d5916c2b26c94c7ddae0158be1351a7fe3c22da8bf379163eebc38af326c9ddf3d78e61c706041166394307193897b324035b95e91cd8d90bce73118497183ee075cbfea10d7d2145e28a82d4dd18e58962fc0a3d4a40ffb7185fc3bde90ca97ca032f226244967d4eb0aa307507a6cde17549a0851134d24c42482a381fefb4ec9178b7a96f4bf5d3cc1e57cc573f412847e0a0071e6a180cd00bc1667fd0f334f80ee3a2d29ac500e5c68fc2618a519bca3992f8c6612f4dc738819fe36a4d36db14f2e8c21d3a04d2eb26a140f28ab08819d08650f34c4d8a6f805af40668283c713fd2b93907b260af4ce1c40cef98c7b921281252766e767bb79014c1b06920ad3963df450c5719729d12ab7fc92f059692f9633d7cd64325d8c286e00229b208dbf8bd735b39cf97c60d1ef6655d033f4700bd15e5fa575c691f3da99dc4e5d212ddb993495f686a25318e604243393234b4bd054cfa75cf46860bc12b32c56f98bbb13834159d34a25825417eb6de2072d4cd929970b63a36b1ab04748649275722a39f12601c01152db3094ba1132f15dabec04364f60c6764b7256b6dbb57011281d58684021264ede8c1288d010b1babb106e117ff0325c904b3ff049fbd63d5272164221c72e33fbbe707eb941fc5bc81fc22a5ca50a6a9771fea5e1f107cdb27b4525b776261c813ced7c7b3d8d6c659bf1e2ba56616e0848182c4f85a806fd08317f2d75a130f6c3529897ddd2fe3ce54622a2c74cacf59ca8709de7c541cec47ed66e4daa25bd1a6f706a00c0529ea59fd916ea9717ae6f16f878ee0c820e51cc22ca3576047f8b63063052b5422229a23e1d01c094d0caabaa8a0cc8d3310be0ada9d5f79e299ba00fce5a6ff69609ab533afa9e0c7846e0a58ad1e5e3437563d26d1a5b79a058f58e1a786bacc454ef786bcce888304849410fc033c6b76e72e053884fc390cab907f59a20c6664b439e8661e11781f5af94b0ecc292538a3562ecc0abc53b54793da59e400727d8a0caf14a1d52201e07107680f5dd5f071cada123aee4b05eae397e4c05780b1abaf37bc3c05620d3e17ba3d3603d684e545c0fb4386c699efe9d1e3f8fc167290fcaefceed79729d7310312c01e62d5f0868a763ee286cbafe6be46af651ce1eb58ca03acbf2f34d85ad0be3cc0b10202c489796f0098dff341c24264af8437465788edb90f3612968b716beead029a5c5b0662d728df2eab61009b312614507f55d27d10c9716fcd673566bbe8ccf67f6360f75f17f2de0b8a73325d8d918383a6a111446882c00690dbd636f23faef4622624886e2071d0c4eaee5b7d0f089335b8f183ce6b2ebcf6ed79f68588deef34780b0b8c45549054b605f19138cdcfe81d56c708188b0ef9f863076944c4460a7f88d9b40f299d605820de7b8568ae335929ffe9c591154c98a9a4ef47710f01e5b2992c7002d0a9e9209bcb78897ca044190de33cd7be7322e1200e8f5037b3895f62e9bee7729bd1f67264fdc7837f4aa5184f8984163e4e805dbb8fbfe503168073f4b192d5d78a4665f9c715841cfd758fba86e0fbe82545c970c7b51a01223b74ac19594846d97f890aea688009ac17c59ec2345095a6bd5126410fe4de63358ce767c5c7ddf961200a723582dcc25dba40fcc35028e648374b350112b318cbd2555916e2b93b5a3f3710bef3643a03438c235a08e0edd86fbf122614d408fc9c3ddc3180e38af6734bf30449df82d0594da8b691c7e532a35d409812d07abdcfd4434b9a6cf777569554aab68c09add648e0d92f29766afcde758b079ee5a9ef4529fed74ea0b798f9dbf26fa7b8a7c034db9dd62bc4340b09beb8e31ef653f8ad25e7873a46ea76d257488286c8699c71fe5c6188fec9cc867630a354719f25cc138a590859b1f33c1c53970b3fe2a59d37a57597a954e44070bed80cdf18aa3217e90722730e3da8001257db8699d3e5f4fe64caafa9482f630973977ca3514861d550ab4201a8cf7389a01586035517fe631d25895233a8444354156ca324db1e9d3a9d4c3ced9a0c2aac58aeaff69ae352ee8810f1d3b28a0ba69dde6a6e34d1642eff16ff39fb1a579d87f586d023655bb2a684095985d2edc5e9bed493362f7f0574ef3c83fb91b7794cd4e22744d0c9020b837547ff7c71010a3e4492303881f4ad8e5e511ac418ee5fd4f504378255b6d001bb024a25545b72d09de7553edd350aa6552804843117917a78df3dbfc545222f673084c1d92346225156b4258d1a32c8959534afebf4a7ceee6ba459a8e32de52a3976614c3dd53618db28b286a24cd82d39c4d303492c99e708158acdb1e0b7021778fb31613271e254aab407c596c5e2336cb2ca9ba480c895abdcb4c20d7dbd38be73cdeb865063a8cfce55de08ef3f49a10a531e28d47af3d7a910c19bdabeb113041e4801971e2ae14577ba9e4f5db37623c51cb35e75e2ac7773fe474027fdd36eee057f5d4bf8ca5d7adb645382cafb10a38d4f6c341ec4579266484c9166ecdb79d32b166f007fb1e22d8360bb8eaadf7065a722ffe9e4e39fa7656724e0927e8bdd02c3a8528478b64344ddcd3e296c960ca7a06df49b98d5ab8fc56049dec197b6069d9195385010609e660ce8e86be94b4abe7da3ef4e49dfb0fc77d4354a6811ac789ef54043e1b450178fd836a81c595a20bb28c8e3d85db740b229afb879626ffaac1fce09bff8b80fa9cbd63dcb37058b0f85b3a86d5df2289c5bd4bd1c08cd4bfee6d0854d45a3354b7768566abe6db5885cac96d71749de79594954f7e9cce179ccbdea2a6dde1e508a7e7a025f1860b6e2e7c7d6aea3fb117f42630219e00e157cba58b765828b131e4856df38fd2dd82e7cf4e865e2d9f74c4c5f1bcb77b75b5b07aba28a9b9c05a3d905a15bdc577c48b6e04dfa674254cdcd11cc98034e8995628e1d0d6824e0126915b189d1f1451acc36a1228a6b288912b3c47fbe7e216753fa349c29301c8672a2b68fc518673dbfb150a6f6e31f552d3bce515def5e767f069989dfac943274c48c373b572507ba0470abe14ad022bb3ca60c330b90c49d33e047aed5820f9bd300680dfa100961eec40aab24e2cd27ddd1b1bbac3a1ffb00fd2d663c4e7aedc845e6a3738cfc50e5c677cfb63006f14be804a0c580983069fee6e10e48d885b721654e2db45a2610122a819ca20478897ce4fce143562f1873d7279c477086cc50be26f1db469242ced912afcc47471d74ecce3ce566f37baacedd429377298c20d0097eac432240e01ea6bf0fa5ed8193ccb32eea73cecb75d1006d6aae1c15e1e57a3995b4fbe907c69dea208db714505bfd6d795953a94df8e0b57a34a625bcff77647f601119cf0279eef801cd0d3da41a9c27717bdccdf8840a1adfad116fd2f2539a958d9cfebca3cefe711ced01a25da6315d663317f333c1664d7b19fb56f9d26c228dfe15030bfb6a8718b035b3b132cf193eb1ec6b274fb1d7072762824f2d107b2ad104f653b13d1cf619b60cca32dd3e19a376923d1696ace2363d90efa31fe4b596e969fedbb50d3e960509efcb47144f7cd3608a01a9422511b224d83b05f8a637c1e7c1b648bb27dd79388cadc5bfd8589a0577ae929c5c80cc59ac75af666b6428ac9c6b64af435ab2f855eb6ac11c7f110ade3750f9348a1ddec2afe8f5b15b8d66f68b96378edd52dfed4261b44eec3358db216ed7c9a678ec8d16079dec6343b0847de259363e55d0322f353c0c89c6914ec0d167b9fb73ccf00b938af5d4587f755e082cf993b4462b54c111d2c5f23c245c495ee1218c29f173118eaec8015404655c0046fe95c6118c534bea26861dea436851157ce2157b6830a5f24da8e38932aedd2d7d5e0fedc2d670d31b66e56ee992cba725313a56c42c957098c701f52d078cd244be9b252bf640415510d4c3d03f3ab8b553ac01302987f83628b9757a4d1be02c6680c765595b0a86ca0699749372c6549c31517bf52e5eb7f20601671dbaf71f9313fcb5a876977b76c390585497c9e0bf4e7128a37584c1d57fba802568384f11aeabbe706336b4c0be05a50b1df28999bc5b4dec2a8f8ec07a574e6f6faa7ed47c4cb641040b0e5e27877fb3ff315aadab43982cdcba23b931117738dac60f782b0c8e597ed5a1516b60c825c1e43bef711f2e6402ac7e727a74b3e9e11b73b62f93949a216a7cea48a1c34fd5b996b9ad4f5dad44ae0ba677d7c762afc31829ec9baa5772129840f44ae5fb55a7162163f5f66035b73ef32bb573a2ada699b878ef30962dad0958811e9559e8d7324c73f7023be748397e5cc6438b37309d03b64f8435303ac7ce963cc8ceb626978411b8c26f723eefc53a4aee381f5d58ac2733ac963373f7f25fa7e0ebdac06d6a08ab90d3319df1582c894cb050b163bb7ce9ab2781f421cf9e57a9f4a623dbf1d6354854dcc54e80e452fc178f794fc90725da10b98a1c602b563405a654d6053d22b087dcc1db6de0b805bd95883add8d5f6dd3c44a6daa16e25a972c77afa96d603141863ef57f1d528a6b4ffe011ed1180ad5584a6cfc4d19d13bd5ab206bf50d1b31410bee207fd26f10b550ebb09e88bb727e82d270f64c2d667ff7816e5c37ba7d6d6155d0448fd664ea5ded3313f9808a257fd6bacf01a537cdb72f07335702b67ea12b9ad2b6c710f2a03155812cbc5c410135363caa90761bf46653e49f5e611e326b39bfcf866fce762e94a7e792408d1a9cd734fed21eb95f23cc978209877c0fd7a1c5ac1f94190b820eb83de26e4656810ead98bcf569b97a478f54cf17d108814c7e4c1a6d8e1f3d1cf63e87e6624f09c85d4a1e64810437b53efcd441a0899609a6a43b0a9fa27fe58372f1ff9c34a28345c4d983668af49e8e93ed37678264650b407b3cd92689e4475d431e5e0381b14ee9e210b3b11d6f09004117d77eb0778833a5ffa1320e5a8faef857c3552249962f7945789bd1a08e53cd595ece4919217f79ef437ae43296de29694d4e451084c8f4224efe0c85a47d07963841ce845a460232c94ef3f219a2aa209a37d68756d23763f138b014ead922efdf33b7c233b407dbe6677f4737d90db188f9bd82bf216804cd176ac473f76c1fb4c339a5243f4248390f80f9321fda00034180d8b3dda7b4ae90ffdb2ddeb20ea1d3e47ed422b386bd9bc96ea409b53d5c2c039f3f8de2051ac016bae84c007c497533d38c388c884950f0f129d46b7434ab5a5c3ed5074c44024f2d1291a71aff21d10b42a8b65f5e08636e4f350a7309e64718dc1b610e057212151899dd43644c2be9d1fd9c87fd304cb9facad37912147678ebfd4decdd67a85dd7e6794c901306548c5d09041b135b34aea72bd09d298520f427777bd4e72784814367f00301853f503340c8960fdf0b5be6c6abc0635880158f53b3e996b2ae0e547225efd0ba01745e4f45d30ff843592627089a3bd5c25b96dd4cb6eaf1ea17c2b85f4b7336457ce05cf500b8dc3092de38f821d58d134c7dff2cd711ea8ff6e9f8180c41bc7bc3f89ab866773f98ed59ddf2fad7ed4ee5a7f409979a40c6985fc08bc1b029198acbc8e76e7fcad66c33187238a0246263e49d3ccb5effdb1326b880220cb72ab95bc1804b827b7e141e6b70cdbbc6965eee5c7ad6dfab6adf340d58919f88f5240477db655b2cbddfffcdc88c10394114cc1f32f113ce791a4651f24bfaf028edb54dc7fbd4f36327ffe44f5a394dff8e82a4c98a7ac95425390b1bfc76721914c3889ee8d8f37123707a68313462d5457edc1a656ece7417bdc88a9166e2e4b79e896edc63b41d8ac3024bdfb5de38525ccdeafa8c190a4ad0d87668c276304a8108caeb558b2e157a9c75e0e7b74df4f494b07584903a4e113e289b29f8f7787ec6c5be22a9ecac7e329af237b520a4ed64ab02439dc57d93e46a9ad4c3f49635c831927d34ec48ff8ee4961d7437e2ab6158db5889dc50d2e4f94e29831b0ba90fd41f92109c5fc6dc00f2bd92b09b5cf4ad88c45eafeb2b1b409f0c4aa0f3589c89bf6e47b9cea7110c6cadd408c0ff181381aa9a85b8cb36244fd0ec0068e4812fbfdf7e0725f183933909773f745fb76b8b69ec7b25dd3fb160d1af350cdb93eb01a1d87dc1a94e9143fc2b0f365e95dfdcb949d002c50556c62bc4b07f0246d54e099b4448f03d7dbfbe3fa72bfec578304056af17ff865c2fd826484e67defcad9d31dc447bf96dc79b6a8d474480af6c6acd6c60bbf40bcf1135ab40dc6db98b7a2cde61c50972ec27011ec97cb9949f27563f6b902af511c30481b1a9c2ac7cd4ca16fd30ccb4db3170a0d6e56792905136e75e010fa8c98c380fcfabeb554d7e14f96a5581fa42d03a730753d3c6c0cd83f5d536966a8263acd38fcb588b8879c253f1522dda0ecb9e6969e398880380031a588e23be33d11e226ccf4c9f5648c8a0a04498512d51d698cbd35fbc3e90d8ad16878df7bf9749506d23743bb872cdeca673f3aca4c676e1af2e9674357e6b842f949dd51fa902b3c6041e551a84cfb049b3b4e69081833027719b0de5416e9e61aee553566f8343f33cdf4dc6ff60f0c3d2b9efe93a752ec49f9d1dc9bb28d8dcae3a7776737157ae351330f999988888d04c8e835884213b27a4feb4e1acdcea5f39964760d18b5ee999585b58014e6bc4a44a844def44357f859094866ac8286f1634871a8952afbae4f30aadf16fe63bfeb76311c9c5a537664377ebb79cafeb983a4b285c6a24f57813f0e3f5260194dfb766e5e5d3c2283004318683b9f7d927f43f4c0200cc3ff8f6f4a81bd96585c7e6d35dfe7a065bcc65267c027edfa7f9995800089794b0a6afcb9a5f097631eca8506d1c880df7fe649f9d64f29f2d091b3b86efe7106d33717aeb035cd1786eb3988a994601e03c9c0109d3698211432fafcad4e2011aa637189addad6be7a350f63edf0b54cbca020adc5e6623f5848ccb567c157f54953387b26eb0cb825f97c8dce215639a809d880bb368f9646489814c5d797889d37a25a3c0bb9581e2ae2c0e5a7fb1becbf0dc2b0d76228d0f1422d76edc017ceab73c1b6570056a16f3774c5f4e0536e928610ec7cd631894c0ae03b74305c436bc2f7e4426d52f0d784ba8de8736d7a95342eeb539f3ca1dac33ac13074a3e9285d1743a0b29c4d28ae4d6f6bdde7dc71e989dfc73061d5eba9ab9ba6a6dfced32f4cc8ccbdb7656e8911c12f64edbcf950a931c9e0e1cad30ff90b728549b8667de6a6632106a59e080472af59f8e385e6ea185847bcb8df70f317694316baaca9bdebc47cbeec2c51a4521e6a392d3152b563b54f59c949a8ca4ca7d1a272d41216d4945e57219aee30c4fb8a0eaed1112f90468ef3495d985a2cd07b5a2dd45ec53bef24760f91815d849f7ce3cab481b1e2159be7129c6d32a60eb386e42c6fead04231398f475805f1da15a10203b152d9616f347ffd505a33545f4a73af72357da7d90cfe88d29f42260b2919e9a1f28ed2aabdf39bb7bae60eb7b3c0fbef38daaef4e834e4b57676a77d11b50d9a7e12112717e139cce4e9142c429b97071e9207169140cf4e774bd01e11e1b88a334b8b4aa1eb08b2399e0e83f5a1d72d5013b2560be0d855960a30dc55d20fd8dcfd5bd6794c392d28b56c8ed56a7e4ea7eedb466700af427d5855f9611bacbe618275b17ff0aadc5f5149569ad005262dfc3f09e0f8a943f950ed9e78d305541850e4b787d9cb75211852860100863401ecb62550aa0554028785a0ca014902a2065cd2efd8854d824528cfd89e344d4df3b2bc7156acf08e936533f17917f233c6edd5b39b4be0f378af456b2407aab8e8f2d89849ac11868eafc883fe51d08d17ac4f548bd6c3e4ee0c9b3c1a87e078b416f9352ccb80a0e15d56865277a8c469d40b9dde417bf6bee83fd64eaa021eef417749cdadbb5b2938fe626e6e30aed5450c9ea3d88a7587f691e825e5549e83ff083324edd4385524d1c2e3f3abf9c6ef1169fd6aafd8916bc91e13eeb9d9030c7bb2ab6edd77c2cb0106ee044d1e0f76d6e4d82fc6a2e40a21db88af7c089e8069ab0e4a9d630819994d18059fe450d1795b72e24ebb31fee4f8e27feafabf31ebc99c60b585b28bce74b54cb38080b4541018e2d3706d895b7610d09f224579060951c56d4ecc58ff63919e3c18712f507d0e70385ccc73bc0b25134dda8b320ad151712cbc070f0ee5edd1cb39c937f943049b99c2ccc17c5c7778b8b90b046612b00ddecdf10b79c90e59347e24109ab3eafaa779a8821cc3e156b1b2c5c2075550a21acac6c4bea2495a6532ad74a0ab73f494924b8d8ec6a9daa7d2c718f316d8af919332ad5755538026f0635b90bbf458956fdd3d0df40910d39d602d6d017ef3caf9c34651b1d2fa4b744a37b7cb26ae50ccbd4ed963f78d8a1ec38c24d40ce10a4db9ac0949756b99802a1a21bcc96baea9da319bc86e26b042de56c27197470ab832da84abf93bd0eecc0c06bf41fb8fd2501aeddd3e5b69928134ed09912b2cb59da4d6597c61c62ac6dfe1839820922b4aa2ba90f2b8f82cd1f8f6b000e10fe8abdc0f05daaf7a268a8f2e9631e234d72d1f8e490a9b94d87d8afc48d0153c3d6cc8f5d646e9f856c15413de660c113ea4de34721a4a878eb9b4e08878818ab2f242f284c744e312d3e3c32c81eb1921e527557ccd248e35466ea40f6447148c7bdd21077f3a9e89776f8a43d48f5c1b39b700278faf2d45e773f429b9369d2d69f75899c48f671bba89439602b82709c8f415627520a8da599aa6dabe982d159f4ef077b4c75d8c8a92c7070a02c419d06761a816c355a0c93730646c1a61692ec49915ffc7c37cff1603db2e486d7f212a9fd831b12f71d854416756bd27bb5395f8609be2b4213bfd20b6c1acb39b340ef2b6c9d9f59bf976ecf57e82b1a1792a11a4ba21eb2f1f813f95ef9d01c39f57b554181f13e68cf50a3fbaf8d96b21398850e63c3619a3670ab2bd3f1b8714b44e943ba8a1d39787ce3c333f75839f4f91a0cec99c54eed44baebc855042f1b2327bc276a26c1f3c989e63dd3a6da9b7187641b4d794a6476f8163b25d331db3ff5e9d503ca8dcab6ba4b427db2a1549e6f1d3644736e859204e78d8459d105cdcdc095df222ad9bb324ad32ff4f6f8266aca475b40b20f83cb7d875dcb62836b4722dcb8b35ce657a3920412b206027ac00446883fca2c3fdf81a3da15cc9ebdceb1217cb3e5d645d8b68ee3970e92bca903e7e8ae0862b06a6116e16aa322ce4a3ec1c02fc73a203f9f67eba9b6c254eed619a195c1357dc22adf48118a998f808b74354ccd532cf3105a03d38416bb707f5e309b2d455d0277f5e17488e00ed63ba2836569562f7ded505ed2f4e3459ade9ae7dae3afa2c3ffd5ee103bb839ac3b809631d33f748709b2fad0f6325e319a873175d68a44add96ea5624334973c48e65a718d064d75ea86fff0b0cde64bfef23f345676d9caeb2bff0eff02565a81bbc1fd96ccfc89ae1ab69d059a31eaf09ead76d8e1eb639388284f2ed334ffd0c6e17b460d82ee7705aaf30e837e5132810c76b41d23d4170067ba373b5f44f3bd17a666ab5ae1a69ddbd6bd5b4dfbb820f439c3509d42403bff4169cc0abb1adeedf4bc6fe5c659c6b27052234bc34d9fe9e328bf109a068d69395c9b1e63606669c2b0fd7f9a37252d91f14b39f4b8a474a4fae400bb51a8e087d61985e2968e1992e96bb904d7928056da5d6a2da0cd4c7bd880afd35d9f69813c23aed4a3bd44e250df6aea5862071107039b1addfcdd482e40c6f202525bb9a4daf768bb82431439dcd3a100af2b068044e5cf199fcac7aaa8b13766d2340961defefff64e1b87e591c6f52cb2dc846d37de54c985d8c972c5b71c3f241b24c0df8ce004a00d3e6b653eca9bbd95d198b5f45fcd7aeabcc46c34ea0632d3fb42ff251f0160a521de204132b26b2e1be7cc036636a75928ae63b9e6d8537c04f588af7c51f4427b211184b4c22b997515844cbb04cc3d7200d0865e23a1d20017370cbbb5c483d4e579b783b0eaf23ec034532e2467bb9fd71742654d6e515e5dc25810df38716e2b69447358e376edf46ed0d69baeac52a0d7f4fd11ca9541f32614edb9f1ea9569cfca27bc49503d80b38857a181411bd8fe62b101921386286d34a3e2914e85ea2ba44385b13c6d12ede1fb8729a881a4826758099903fbd4980e818d2f9a7de76a237ae9b5bfcf6f01ea0aeb1ff5e06a1e529f6dc8786284e70643d11bf5b7d84b8b244cd31550b52af361989961f74dd70675c90933ca9fd0a71109f269b8aa0d1bb2d88a3ea6134326b53dfe8b9460b92201e4b7ce3e6a3ca6079f9e34c4b60ca91040197ef19b773205f128c682b66a3211621d8a08b2c44ebbad07fc3d147c7bb04fd35fa5050a35f7edff1b328746aebdf8b9fbc96d9539e55662e59a34f3d335091a1505c0f9836b786371449190c871aa2619e5f57311cbbb1392bf02fa7e944fa31af9de15b555075cf45e09189069032543e356f92f28c0b54793cdaf9006ebfc3a73909dd4ec577fae41738de656391a118688cf7faa34da5d475e36246071f3fc97f7b9604dcf6bf3a392b9070cd6957df7088abbccb93e10cffecdd316758761641b045a362f4ad5bd4f611d16934c2bded6c2a37b2c3a074645c83883e6664da68ea81800fb6004d959893078f6dbc92c71bb70343e2b4617c92ba6ae08a5e365c6aaef19b669ff271b234fe915ed35164ff969f3722624c61a66d7872111d9c8f0542cadb38beef0828fd7ff80e5b04bd9b2a0441d753d59185977c2ba1174514234cede859e4cb9b2d4954fb6c368920e30f7231b104641363452563762ee592b3e8000eb493cd9b2858365ccbba060cb0f23f80f71af29c10d685191990d82a345aded31f317390e8fd0a3c041efa6b49ddeabfd8193f66f172347f24f5217ce4367fb554867a421843ff51890b8ba7a5d3744308a77db8858b8edc1ba44c0ec06c7aa5990fc1243b9737afa3e5bbbb00ed30f9b8773cd3889c727c4302f6dc324c7eae2a795d1a4388e2f8422225728ace457e68dbd1b05b28e14dfe2d0fef80f4ae762c8c173f8a6f26a885244ffde35bd0cd074c7258b7646e2804149d55a09ab2af373502068fc404d8ff77bc0f8fa9ebd4a8a3a35e2be40a5eda2dfbee1c4410051ede12156366c44dac641fdd43d195c084ea29d17b1cbd7c386f8e75847bf82fcdeff7014340a46972d3bd0b2636ea3f09efabb0450dbdc22d57553442d0f26bbad3ae47dd7d8f81077bfc4228288e92fd89a816f815cd7c5adef8aa8eccc708e991d6b008a163d41dea729f5fd012b75b8c77f1ec52ff2b970cdbdd5108311ccbffb82144dfb4f07e78908a6b7402b71760750d78470ee75198f196187c6551454ccd7908276cb16a8d55e687dc231bdcee72f9693b7553f26fc0c86defac6d3ec8c5a279095e9a838f5b58ceef219753ec9e26b8c15e56f1ac41193c1c6b96ff668afb970c432f1e1c479db416710b497737f5bbba187f3358bd8bdfbb5b75c6636fbcb54f99d6e81eac98dabcf2f8410e1c4eacd6bacb8a19392a8e13765842a22f435106312e8609ca45124ce86231a8327f2e676b5a38bcbdcd07b8b3ab6db50e48d6b61bb091794d73e954a061769d1534ade742fe1956df4dcfea0ea8e8c248417b2b80e1438d8a1931b33b9dc42f086ccad79e16c67cff5f5292f0e8acdb7042b7b045cc7a7bb2b72613a06c1b8a9853cf09a793a00937442bcbc84f3c3c7e9a5b5153d377b13a251e97bdea966eb9bf22ebdd9a1a48d4449a45e2e30ab1e8ff571831722e2d7094e8ddde1b8820cbdea37cd42fdb602421d32b4b17ddcf786c9c7ba8cd6c66a90fd55beded8df86f1a07e76e92c045abe99aac256de844a6ad2c8e26059c470183da78f4027abdc4a36cec57f8511a188aaae7f971ec0ee46f675cce6510d51d6bb906b45ed8c36739f95851d4b0abd4c89ee9bf65572918839009c19e510eeae3f4ab7319a58f098820c9b40e1f117a5593b102e96565281e843ee841abb8710dc5e0ae87e868632ad56f2e3499fe24ce23c30416e1d2ffc355cace80df36d8d0af62af6f7d2aeb822fe621fa2eb492143af8316f2c0840a2c05195103addae46fe366e66f7e905267f0cad80675ecc138c7689e49ca576d35c5d686c6eb56816dec9e5ef405bc3951a908cbde46408b47639804cf4450ea35dbe67df99a0252b44135adf04c44d59ec21d1d6b79825f6a99a3a63229f63d3a31d4029db8d9cee5eb6f5632377fea9323bfb0de2caec9a990047ba4eaba521bc3a5b54777d334147182651c681299cfb15a5a5992e766124a10bbc779ee580ed47bc0a55d76428e83e07a678b650ebe5aa8d749028eecbee2f149e9780ef7bd70475412aa65a56253f3e2acbc6011e43d8604fddac565eb7151987a1a4b2fb18b58005b1c865fb4240ac04f8e14db633668b199d784ac80765c3f8584cc256c37d37da8f9b790c0d8e8c755f5fde7f374d30f284bef1db351f58c421246ae95ff1275093debee54b7fd0fbcb450903f27f416b733c30f60dab2ef9e59e646c8d0ef4b3043c5d8eb005b4c868a61d199a02c57acc123f50b03009a171f3ef38fc02a2f8074da26a06c46426a6df678ee56b6c350dc1a6f42f7daf10d6fa929e04ebbc6629a216e15ef8e1dda0f6ac8368f2c8133c3f0dd49f962fec6bfa45da6c237e16e1e9bb95eaf811304e6c9ea8ee7bf9d189f1e7be79a2af12868a9b4899dce782fca361db2660401ed360065a39581091b16e8c4914713925edf7c3a1c5815072cfabd750d697ae5206f87f7bdfb7a22547d09022a3110a0ff3065e8271b55761ff947fb960a714501e758d6ac6f4f494080861c2cda7661abd669316dbc75065b72fced024d9fb4c9e6b0a1dfb5b06cce4e555860dfa23534500cfd4f5efb46b1af6ba83d987cad800409aab3b73ec7f28464dfd7b4907d1652469189ba586f798b4087a67d89af5216900286fa6c4bf9831b8c7762914802e143c58a0951f4045bb117889bff29ba99ef183b6a834aa7744cd8d099c51a0e1412adbe3bc0892addd2923cf5eb0b6b7c2e9164d12ea760ddecb9ed266f96fb3b88f624817fb5d582f6d8a90182cde4d30d9e3288ea4540f6d25503dd3801ae12b8661012d1df43bd5f9f53d0311714d85f9837ed82f4be6370bfd9ca11d5d7a7e1fe113d62a2d87f68583af0e5a1efe642f63419cddf3be1531bd6e011dd8fbc0e8c31e70b8ab1e88614cb12ffb431b1af2a1713d27ad1c80d58bfe8276ab1702c499c6c026608ef3b9b5f3031d27e069a9b02be5a7662880a8026e0886e0f141d5b58ea8dcbb83d51a6f97d6ff125acf343fd246a6aae8124036b89016ae194dc246dffd5368fe21a4459233baa85c2a189df5c06a75ae1b0e7d139ca113695faf3de24989a30243f498c942dd4bd38afae30438521654ba724dee856379233960f643d16ea651e8a50ce9dc53c538110522c8fa38ff15d8b29a7d4f6c38521b1a192c8c883ebfbf8d6743d1118524f7940c33ac54313d3ec660decf843a1b3153326eace1566a7097adb447f87bffe66fe67a3be4757adf1ea3a088fe4b1af6da4f90557b13c8bfc70d1e4ba5f1ea467f6d1913223525433f58a77c651f293a6a2e5c63d41a88e368b7d64f75e171da939692472b680d6ba47cddc906b9daa00d4366b1140d47acf5734164cc5af4ead06043247769cc041a6d938677f7b5df3ce943b78fd3e88253a58d1bf03a393de45ef63337d24c17524292f1187f3e27b18a918f862f02b40d375e691a23d290ae4205a3ffd446e4b72d8b7e4d394205d7cc29e8f26cea5c6243d0fad26ced02b717327f5f564ec0bc9c9bd81fc0da61196892a98f218d7a03651b48222daf13c3f0726ae3b35f88341b234f3e575dd9e39e07f5e9f382890f93d254d31496568d558c9edf88bd4488b8816a81455296dbde67215ba45c534cbc645c62f14ac7adfbd1d4340f162d6846111feb9e1cc1554e9a26aafd34a5a2aad1386f8113c83a1c0cce3fc228fb29530c33572eb52b8a580e7a8ab6a5a870b8ba05451c1a7d4ef82e1e9c3e2c1e69378ff2a35a99701092dfbaa8bb97acc78259047b3b5615b23cfb1c984d33f7ec6ffe6fdab76fd47652b673c314907021046e1970f8c3e85f944b48f3ca37386bc2e7c09a7c2ef53ec51d64ea35af286f940d233ef7dca32134889d5639a8f7d58ce9580cc86651c5d14edc2340373439b8e10b073ca5052a2b698a13ab168cd82215e7bd99c5c7ef597f28cb4c051755eca32912109251df13dbf5bb12ae560904cfb5ed80ba2cb8db5d3e825bebdc2b9cd1b00664e092ffd82e680b88b94e7ed93c9c122c0bb474be1dc653223c6c39979186e96281be0f22315793ec226fd0891b875b4d5365138d0b51c5c23ce5bc55937377dd81a1f5ac8059bb7a9f4bf5952163731e57701eeffa8de4b93323fc4afa9ec44211cad78543bab9a0d959219cc61985aa3aa2f63f03ee72e01f38582d91ea95e89c44dd5678cad50c7f2eec9b3a3a33e2de5574dff59d4649dba0186d64c54c81d060e65a15d232a5111c6e1364f9112448736849ba301585bb0051f898b7940b573773cf9f1cb0e43d795553bd3685b097398e31570824bc2c8dc2f104407ead3921be6d5319476f045267f162dedb8d27354bfd6f18274a55531a3837cf8501d8224d3fe8b41ed5f1a0e741b5f63a4774fe5bc2b6ec26521bb2b1a230b40903ecb489d15ac75f5f3443ad4320a70b6a30e65cb7b90762576c8733f74e8c158884012a1fd4373e550723b958652898677d2df2ad7ef377d6792ee6e4244cafbdfaa07589e2b705910af09bb332c0bc4648cad61563d853e99da8a7714f7205ac79ff35420991574d5dc51900ace584ef41005da2d28639840fc66dce63663f13722ff5da1b61bc558a2e6e68b784d1214f97717198e2fb59b11187ec6aac4370288758bb53b16c67c95ff08b9fbcf69c3ad956b8eb12571452d3b7c348aee936029f810cfa3388946894a4212d7b8ee29e97c25a0fee8d1d8f164e6a107a36f9761d728c7e00320baaec69011214172b92900e68cacf2c1937d8325a1bfd5f471a791e09237b7bb919bac292ae72520d15f8df3af7687b4b0191be9d6d3182a7bd0159c323abd0909ab166c3392425a3d706453cf23e65e5bab88255e36f7b7884b0192582e19942dd792cc3563e2f3ccebadb595db6d887bac8d7cda7bf7322b109984012075022e710b73ffba8049e2f0bade7ff434ce4e61b9f35ba7038aeeb8e2e61f73397084441dacf78beab93169437d44a05f4cdd94505d8e60458851ce397dca6ee1540e6b0ba2a3f71bd54ff74ae15320591d89b75de295841cf7b4378ec79964f4705e2b637ca9706e7ca51833c6e55d6d02479c0e6dc43b0e82bd839dc080a2161770ab2bc21028f361fd389f6d81c87566efc40fd920ddb67302a878c80b103822bf00ceff60ba7ce84fe8206c2db974f759434350ffef0eae1c4c111fa01694859104faa9ee24c77c798b559c8c964b6c11007f7a67ab0913a972d2eba8a8389923b08bd814292daa43d5fa65eb136e8a053972d68cbd2b90ff28df08f13003331a7ac3497cadc24d51daa4d6b5a5fbe06ce5d98332515bd21e42cd2cbb6693eef497ccf8cdf29c64389e48907dee24b1a4a9f62f3b94fd7774f8a919ac8c0c1569309a1ca0714ff315680a1e69551f8dffe22d21b72e5c0e25b43b9462569be3431f59278a597c4bc7389561cc2e427a4a81ef342396115741c393c26b56226c73b3c3c41d36b2aff6e15e0450fdf3a619bbdb1a68c544143e8c9fa382b9b99b1925534573772d46d4a2bb8960208dfc94bd3934c9b73bf4752fdef83b19b1183e89a70e4abb90f96ca146b607ec8b98644154dedb178148ac70e70b570312500a19d9bc5e8ad2befbce2e02121437ea31834d5c83747adaac5a3ee6cbcce4818622566071ebfb611ff5b4ee9a4213d61c60c4a9539e4cd9400f5b073b6f4e632e85bff6e280519d4de1d301148fdee29dd3860e91808232de1ff7b5bb176b1b0f3a77b184344a31e44f2665771a5abd390d8aefd203579218d06fcdb3333a1283887e1c879169253b0ab90ea0a69553bc2fdf4ea987e999fec93de09786379bed1c0f158a12869f0fd71b9ff5f89d26ca98069c33b209745889f1692164f4fcc27a8caf693fe9ee44e94918a2d8db7e59b3923098e0f9af9101f02e3290b9aee192d9e62c6f2320e97dbc88c1157bca56b90677a7ed1712bc36d18bb5822efe640ff24955a1dd80144d99bfcaf366257b555cd7e935efbaa24232ce34ea6ad0cd6b8f5b5569ce13523beba17444830f1e80e8cf028f4298d1f1cc4386fc94da524b2b0f81b6c0a5f602de732a6acc72a6f35e227a432c5dafea7f9c4ed204c11f829e98429ff15173f3c20ded2b141d195c8894f91153e04f22aa32df855b99c707325c51199ad5f280962802da570052d76795e99b5af1e9157898fc06dfdb4a8f36cb7f22276220ccd134553069683c0a51f1ccac15eaec5fce4cb5807a37c0580ff2ee731206a9f6c0421d7ddb83b521f1502fd08c7ae2b28517009c7c2774fc12649de41a88ec77cb4666bdf4a984d2a81bf1755bf307c9e0ec5d7f844fb402f869c17516ea02ca3aaeff1e1ba4efeaea93b421a85d4e9d9f916af928ac24c3c04a7dc0ea4f6b66536c586a8312d761f2a5ee98a44c80735e60fe26e3b12ebfdff3efa4162be0d70a5e137b116addad68cf1ae1d59074285882591a785ddb506c158c4ae53ef237a29383cfb0c56308710a78aecfa0cc2cf1dce629588071b9c45c5503fcb641f7ebad20de2750b4c770d8d649a5be38f1f2b7e7e91b183e2ddc5e2d20e0789339a148af8aefeeaab9372b24ea4006610eb2b2a11a07299a899a6ffddf39755714e6a606f8a562e22917ee22ae91bf25d7c948de20783e9738be9286a0f6d9f467e1f2d1f46ad2790181ac01bf316977feac9e3cc5ced91ee13b0406dcc74b76934049b848ce8b66cde258ae055c3f36d32db9f5da778fadaa7f45dbd8fd230fa55c5f08b310809a0ac0739fb2ca8d65dbc767bc46b5ddd9a9f2ffc5723242ab2b6c9cd783c665d3e7020e9279ce6d22039b7c384228540f7e37e7216450739f5a40ce3cea5780ee0679c487e44accfb8f43978f3304316a8ae485fedbd047a6260dbfdee8ce2b95122a9c01744e9063efaf6468ab841d331b28dc19eab1a4fa80ab7c041dd59c80420056d00d3d3d1b5da0b70fa4dbc455f38187238bf404814808695058afedca50c82f07de69b05525c68e66072bd02259a049a8c3e3a09b74aec958440ca7a66c2883d7840dadd9a40985c5e693f63719acdb3e43e17d93f464b700c6c917128f34a5c59ad90b5a766283f334e52dfbe5e80a504df6bd87f8caf69d7ba420e70794d198222811acee675cd15aa9a033aad4f5553b85608c51f7b07d2fabc72494a26099db5b84aa80e6bbf9a4efd8d7ce09ba69e37be419008356168185c6e8c7fb072c8d2bbc54a36f4ab1652df44ecf0e1655822c4fec45fca1d17600ae0707e5d15827107fe63bb926f94e2e60d5bdee0a9e6a3e817a562b6eddf3842413af196f466e5ac5b0a73b2b70fb8da4ad9007823da96e2e9bef6655fded235d23fb5fa6f93cf8f79aee9a833489323600035b743ae7bf842da6334af6f7f240575833797112805831b382992dcfe591b537be34ab15d1885ca7f9fc52f81607a1e09e1cea84fb5ec16e9d00d10abce40559a559e40b37ed05ea46f75402523cc26e1aef08b19feae1736b89ffd3d9d4466c174b83c856c69dac1766b8d16078376a9ce72e68586b418b8ed7e0b5c526eff593c79f0bda2b3d3b2eb4175d62db680ee60920e8f71d74f15fb1c3d125c27f5e2ae14a04ebbaa4b4f0a49c5f0cad2e5315a3d97fe463ec969840ae8efdb220062a1907fd02139d1897de78a676a95054d7ae75377f5c727818d4004b6415d7e2fa6f3265bf6dfe6849d262f9567f141295f7b5de39ce0e7c3a6ffd23424ec5f41ad91d95962909f963ec9e48a648bd5f2bfe5448d834857e9a4570bbb90a56b83815d2982223cbdadd210ee3f02c1f87a6321de13f756c7ad54856edae826089898b7acb9e5b36877b2e2f41d9cf1c100516927a7fb73624ae8ee124269520eb491a010ed33dfb85c27a561ca6296ec679565b9486878d4f3286360f66341d7cef28330790e2f2830130b966ac32f043193a5acccc0e9de8aad149779ed8259dbf6eee5791757436ece01dd3dc4f4d908bba0707c155770c2176ef18345a6ab038e8f7aeb9ece11fb1474eb3874b8722eae5c275de286891c708a730ba29470c350432d39116986d858c64b5e2e8686d12a246c500a195f46283f27fac44214194397ae1ed4a133bb6b89054045a0d747ccce88b2d1ad4febbea47de3fefeac5e923acd416be123a76268437598368741903bcc34a7668f800f86cca08784adf6a9de4a9b146fab6932faaf20509bd39ba8aaf7d2d3aa81d403dc7ab933b0344a0dfedc21d1f05d5fbd6187a72f1531cebb1f6688907c60bc5aafb4734976f5475caf3b5819f82bfa51b1bf6fcfe927be58c40deb065c3768233b322bcae751a12782dab171cd2a7dee153ea1554d0ede1e342c19d294f93c75a32416995341dd8a2e89e10f874b43b4886bd487bb6f6ebf81d05bb52204ab9f2fcba3c683ec020afb99b14fd253e89d81101fa4ae2ac336ddf25eb6037de409cbdced10b1f282f65d2d04db8ae098d36eb2dd23bfbe534005cd04c547b78cdb935f32bbf5645da897552c1f6d65109ede4b78d9322b16df24f2d257efab32f6e413c420588c53995693495c1f9ca92046cdf5bc6ed86700c1f704daeef60df0057e35fd6fe834e9f99d8c0f68173b427af0e5f96f426964e295699618ceda0d615fa486222795a3e8e35debf7af0a0bef7460571a49265b9bf059c2bc0808f59d93668acca656af9d7bdb465bcba1cd29b50e74745b910a9f5accb5c784b80cdcd8a61d9a5d5d6acbc9e782d28e61f4ebe32a931528d1aa14849e24058034b14a674b2de00a669356383fd6c9988d8acbd18c6ee39238d82ad2296125037a17e9f10225f1c2a555a5d24094202ba369c8e4e4c36f4e5c5a88eb056dfb975b906699213f81c7e8d67705bcbe550171733807ff2030e4f40ac66921ef58e91dd38662127a8007e38782c9f556d6799d579caf17cc0d79a317247f9e3b32ee27446494c1a84e3551db99f0695a510485e61788356115f5e8cd8892040b66127d23dfc3632aa266f2c4056e3234f62393c4731b7e5ef2a58183f533e46e97a2ef5a568336334e1a48a5f2d9e384fe0c7ee71fb8f17af91a06a970725a086d6d5354557936a2f3829211b91e57c600acb2768e6673f2c9e5398e6e4d741c0f6c022255067cfcb4baf880f6e10f9b79ff3660a6ce10f18b6ecb09dcecd385aa2424a7f7d9502b9fad2afd612a67b72fe029483c6bffe7520032ec7a9138b0ce911a79aae3409328841ef22b1758c529b15667e3dd2a30b8ff7bc4c9f6db46b64bd804da37f6383ec9129d3804016c66cd61fbe7426cbe3c9149669b67f390baf2ad612a3a0f6ce19f815243a839192a1316f965316c93c73d363aec08b235f99248efb773e3d22d59b506aaad6c7fca11fbefd817bd0aa51997aca2dac886ef829fd9cc32077dea89524c92d22baa1199274bcb23bec7644e09003bae41e4826c196fbfe74d4241e0a73fb04ebcccc5c7b0baa42fe4aafb927eab746f02d4729f61dc87ec1212485657f9b6189b8d1cced882ca895994ec7d217574620fd1d4b53168f44e67046ce02a6545c77313420880934b3267f994912626ee88bf1a4c060a80e26c901df73c74b6a5f18baed3a268b9761094826e4bb2c0c434d27c8bf51e59e27d5f2aa37f216164c685d5c109eeeb8758f43f3d431988987a78b076a18e75bd44dedf28db892cb6e24e3110500f26bcddd06ff409a8bb3ea4a8065f2fefba183807efeeb369cb1575c700d74229cea0b055a9889242a77008da8890aeabfa196e83b799d8e960add3e786c1b3401dd98baac33d10af0633428fd1e9aa803e12970dad0450e15a1e487bbe860bda5d9d2af4d42720f5e6aeb67da5b27b6b394d5f036306efd07f7c5cea2bff36402e5be663ae5dbfcf665136626d36db1bad08c9c468d94111f1139240c1a4d298f9ec3d0251261a117de9c15410e1fe9c54cefcc7d71402fca094f47c2964ca07bcb3418f397532c6105bc028f1132707a5172b04ee7bf667ba1c54d38bd86140d033c0cd091194856ed1dfa046207561764c0029ceded3fb6dd79ee6aa27b94552d4cddc37f096ab9b647bcdba0029bc7a08b4d7e918f8dc863e649dd8b9d942415331576106123a464f2312ba4cb0110a82121750846cb1952bdc0315e94f17b0e9f7bb9cb10aa1a0551c242ceaed6b4e75c6242d3599a77c47e6501a45eb88d7bc66ce6476e60490dbd552e71bbe3c2be3257620ba2da1a9b560599618894e1729a6209fcdd0908f6e87a7cd1c90dda82073168412731bcccf3c8e8c051bcba43a51789c4fac145aef751e215fa37b9f5e292621e50a66dad0f020734f3f445af1ce6d0914b9f40d82f65d4bdb2fd48ff650501c7bf3282afeaaf4edb98518eebcc2a46f2ad5ded06bc91bcc2701318c060a002d24b0827649ee25647251de30367712bd2ea20c04c5de465224fc8470b59580c2f701c5cba5500f23969e7dbeb4de302a6a70e2571cf28a0c3066de8178c1483d1ca7df297d9c112e82ae92c6490d8828f17775d06e586d70c5c9f638a26ad2453777bf94f898382c61cbdac40748ab8599c6d7c7234f73646ed8859b537d94be028cfa048821c7c427d182fe450e37f3cde4c28fc9c26ca9b063228b998fd8478818f7ba5fa4ea0de21e126a6d0a6cc7cbf77fbfa40c43ed3ec72b6811ed43650226ab2ea4e8042efbd6d688dc1ae1884a1a131c6bb9e31707b96b31ebe7b4e2a1afeb3e7c7244a83b6c07428332a06fc9bb228c0aaa97b759b5e6e6cae789cd1eb7a51016910b695ade3724d13927c40e642ffb5f21b580dd9885bc1d39d198d4c678b6ace8de0c90f452d2e3c49b38d23788c0c0c63cbdaeaa75f0307ceb78f4d727cbe1a9b6a75631c9a70f399d6aba16cb68c2096fae80420a0830b8396abb69caf878692eae9672a11f42018ccc18041a46082d22f5c9cf45cfeb2e7aea2bd3f045817dd77e510ee91220790a2dab5992f1ae00703d92d10148aba6a95578032571929f53c7d8314e10dafd2980c8d8c9c4d05024fa797f0ccb87e5d93218859c217f10f9837a6dc8b121be4e8949a15fbc30096089b38258e0c92878b86b32105d910fcf6d46d10e7976e3705d1bf8c2431147c09a9efba0e07ac027eef653c6367a81199d88ab9c35014661828290a92369b921a63fcdc58e05a7e185fe1e9061a80fdf869e2f5bf065dc39b30939a71cba279d7ddfe7ed7723c4fa946505ef0f3e523d312a891001d2a9ce0079c5b2cecc9d9993cab6a1d7e015152fad795df4bb27ea684f44ec36c64d51a2df0bed817411ea046611ccc2e332a2f74ae1e671184f1ba1d023f949f8be1d773c267314b947ecced4d4e848bc13c87eb2bf98a5f1307c00ea037a91bc98b6843a0be846e82127c99e1df8e90e928842daac158006a161caaf1f689db80a17d9a38f375db2c5540ab7cc06b6490f96e1be9c726faf17804fbb347ff8392e8d8369577452fd25318b63fbd9df1f598c7729e3144df5e84b248de761a67d35caa0e0541c9bf09153ad7f1d28ec76d34089b312015f92271d8570125f383bea8b8404f368520f8f035bf0a845b064f82aead2e60965ebd5bd1d7dcbf9c2716a57dbfea6587ecfb472c3a9663a1887f5cba80c2da229e70c0cdd69ab94697e945d01d12ec0da87962943414621eddab199478bb83809233a257e9afd93f5c1fb0b1ad7591c8efe36ec6e0a91fb4a2931c65d8063b049b460f02865598201959715e7326c1a8454eb4fa257298517c2d1aecadefaa58647a8035a999ed7ca2e57a8c6d04cadf2a8e81c9bfcb6d71774607624191d49f3a6c2e3c54c6c06320369cca287b3f69da554f24eaa84dce260745bf7bca0ca7f3b51e3f8e9ef06b3d072126c8da6b81d6543eaf8427b1b46e9c3caea7f82c9c485736c4affb7bd030c8ecf35c4c74156a7f2d361c81ceddd4bce41218ee134d3c0d4a05b6a6e9f319ee3f5df4cbda7620400ffc1e642a6cfe2f366d375bdda7d5a1edbfadf9a6124bc438f157f38bdef21a277fefbdb376555dc621103b8c559f0c6dbb59c6b0ea718cb8bc7c9be5ce7bfe4650e6ea97b7eb50e940de4eca4415ced18e739c607eb9b02263b1becedea7fdd60e358e23821cf7495a0ce93cc08f0be7119135d8fcdb97442b682b337cfc39be94e02494e5364a5e8a84d05b7d5215eb7bcfe00fee9c8341964c87ae3e10187379e9e69a2de7cc5774ada8de1d39e455e481e703bd8a6068d4a1d069b1d933568908bb20917ad9add6cbfa3e77e59f8da0b0104cf1e45a88eb23eeacfe9e30ee299fab035ae48533280f09076b56678ca5de436ddcb73930fa014050eceffb87930a76a27c95a11297e85a5c1d7cdb1929459df0da364c11888b641610b2b851692b9ae9da3c12fcf4f47473dd88eeff8d5409eba635fd5f09abcc3c8b0f2444395a86124e2fefda6c165248229b38862e8e2f8ad48b9d60a3308bdfa394580ecfa77dcbed89c62f6f64a70fedeb4fa0155defe05acfb8e7e111ba8592db083728181903d461b362ee575d4b13439d9ff239d545f37b3217963c119b12737c19a584bfca3580a43a7ed58313c602a7ec0d03087d6bbcc0dcf00635d0f4011f0280c5ec5d9bb49b82c1f0eddbd1117e4cc88162b657c054cdb7597086629de7f88857a04f29c2bf7d0f1d74ee42073227acbe94773c2134e480f92eecaaa7362274a8addc3c953a927351c4b3a3c6f57d8390f160aa48872e197024f3a1cce7d120c268cae34d2e8a52d56ef4224d7a327e045b44348d24414330e22ee27e90d9e553766cb83fc09fd08bac5b6de8e60657ff089d7f1aa770343e97a584b8093f5343d76bc633737440e39b0cbf01e86cd59da62f7beb5d71bc8645085a300f362829ad8eb9c5283143ed50d671381e25f77dae93692bc595663b56afb7a255cb6b5068bbc310ad4a127e0d8a6dbed7b3926bfca8bd35b8576ffdf9d8f41646daf4f2c386bebb17cda97e03d89ddaf67db425abcd96dab878bded20422dbbb94688c472baca270151a0d0945b802fbc7bd207411a409dd4e62c4dbfd3b89ec75ad25faec10aa6df2ec8b50fa066490e85b929c3869124ebd2918911513910c1b00b6d759a1093039fde0fd0d06c37e0f7591c8a2b82df19c9a6aedb96e786936965e22dce50d7da8a025f7028051fbca44bee8739e03718c1de0db5213c3d5d1a594f66156dfbaebba3b0a6dd45c127fcf2f410c943c65afceade03ca267bb2bb99933d50076a3dc0599fbff0619691122f31cbabe4d98fcf72fb014cc08a886b0b4e775f35674f118299576e6f7658ee6308635ed5a375a099cc85a248f6a92051f3eb2a0b507460462b27e65ee7b835c3f51da2c50106615a05a74278e489d4931bef431623878002171897cbc09f53953a6331048b45c085e2582257765784494681aecc7a0330eb2f740194659ef0a18cabe568935ed49e3ef824c52fb65d2f7e07a7cb7a9eb15531eb433d0efa6c4caf876652826974fa18db683e22047e8d10262682a82160da1a0c20ab789f6a586dd8d61c43d3be129bc94d1245892709dec12060f9f5b3ec8d2a9ed9b322a47acaa14cdce26939943699fb2b1b475a553022cd4101ed1dcfd43ddc36f08a9ff2aedd6434f3c91e97ae14219a2d82505b2a3bfcb04f76c8339974d6572d849a998de9bf9916088b9261a27c8850b905db2ebd255095e785024159879199ab56b63308d27280787ef33b62693646ac183100606e6fa0655a9e6d0b8452d264619c367e0c2d99dea0fb40a25e98082b433f6f13d34e9017511df0ea811e8d444aa6d29759f866e74e58c7d8eb7b8edc83364657fa96c5a2d31ccda410e3910451f81006a93ada9148a05b0d6c66c279d4aa434ac08905dea51ad35b487d77a8900a7b8b197a0108562d8979212ba2f9ed1b69706a4c15e57f2c8558632f7b36c29e88a3282c97ad00b5cb7e2eae729b4de31fbbdfcd80251fd11ddcf8af370806ede7af659ea6ce441832bc97783d82595adf4a212fbf2f27de99d0d8ce145c535d2d31689951c2cfd4e5edf0ed7ade6b9449901ee37d0b52bcc41d28db3862801f64ec28ef360f0415e35bc9962c0a06ca750199751f5496e7c80e1fc6148e3269387e2583479e087de813f8304956e622988a8d18ce30511d312a8d5aadfb12d5e75993948016a1c75c24786a21fb1e56d1d81646c1b4542fd6320085b9f01262367340c04abcbed0159962fd874ab811c65536f855c13c02fb205b9aba2b24c2b596ab297945a0722d84083c1f8870e1206a746a30814419e95db1443688854bf04596cbcc56c409152358001c974881547af99147a6f2566fadfaaddc806d324b6ae8d2d15ace8bd0123208179fdee28718423f5106791a3711794df4e3061a68ad0134e7590871511262c01939afb9310a89013a065fa5387cd1171deaccf78e91f07cf63280d4f52578699177558da2cfde34474ea363558c68b672649b0b58f652203abb18137e162c2edac32eff52a09c0d918724326b23eab112631c9204df0949670dda03c61aee99b27d49b2cfac9178ade33bfa4e0729e9f00d26b5878f37268965b07aac579d4d2b3143c5afec69cbe704b0014bd7365c54db041ee1ed2a33f1062fcd78d9f587909eeab549387b08f9ea450b347982a9e8d31cf3a4d13d54c2ce602fe799a7515945cb1e5e875fc6cfd2cee5942f97f259a78360cdf72e6d57df317c5498cbe15042a39b7b26edc03063f1516b74d6b3b154bbd6171a9605ebb823529c732364c5752a1bbca65289445c5bbbb014c502441d3eb2b1385cca38a0e7cf662d3177dff61feb4210377701f52b1f5359f165a6211d7fdda7f758434343dfbaedc7e4289beee4d337b40c3895669f780b4075422a17cd7899f94e380cd6970aac2d48fde69ab5eba9874e4a6b037251e6a7fc4c2210cd6cfb96c58be41fca0bc4515e272b74e334d4f25b89ca2e3b9454ab7d7111c3e8d0ee5e657630de8c66e4ed5c203f16a5f1ba55b9687003397d7938e1527731d2ba8dc480c71aa23b5efccea4df536e56b1db12898adedf52bbf38eaffed6a406ebb3d77e7b0bb9348be0b73d3a0d1facdb5c456b990951cd37d5b893943cd3334f778f8acade054ebda31be1350b5c5fd12e5583388d2bf90dda60636ec37cd00ea8a3e3284576e9561c8c69dfdc36985cb2193f2d8f9489669a7c845be4bafcc70a2c6b1a3b3f1cbf24056fe69cc8a048503ce3e9fe70929af40e5df477e23df1e3e688600f80557e9322a0a91a0bb37b604fe1e9cd073acfb3503964fb8dd92641215eb3bb165021f288cdcc7a7066afb73f6b37cd7a54d6622e40cdcd48c97af23c9889454f84cd8f87faa2f59eb30f93da9ade33627636d8598b8dc0f7f6e396e28629696a9a99593c3bb47883b2aff26c26bb7743e05af7345f5edd6d33e83f80a9709c768abb9a37191f708e45743fa0e7c7fcc567c3dc9184be228ff39892acfd8c5bd9c649e82b56e85f606b1cac76475e7008e742cea44469a0e81fba571ae5bc7463447598e829d7f4ed2a034387cbf7f1552c3fe9a618ab7f5b929719ac2cda1e82a0aac4428892cc0342b9f91e45b90624a59395ab0fb1e9ec246f318df9db3d456e3d7ad9bb4d0b425eed164262300cf40b5e9c279216f1eeec2bb43d0dc945d1d41c4518a2a2cb491fbdba9e7183176f2bf9c32cd526673624dfa6fa84887ba97614d3e1e400078eecfac1c5741de2948099335af4432bbb9de4adc30891ac266dccfd11bebc43df5d8f0d389f34c0e3f69be8d622395726f2e7ea372613461e5079a2af980f30ec7c3031ba8153d1f2058c11307178c57409b0e9e297d9255dd004dcde3c17bc2d6c1e31849392684839dcf1096636eead19d7a9dc9844184372ea18ba96c8f3bcf044e9956014e0163ba968c227ac5e8a60cdae8fafba24c4965aeefbe12d6ebe7e7b47091815d6a6dd991c3167a1f15fe2cca04d73791ed853b10da9fde8d17da99fc049d35df7134709b59274ee2a77d984eef6b0ca824241ef68918f701cf8c1664d991a3331b9b6b6e6f7805e58a9579cafbc5a127725af79aef3995d55c5b11536fead512c2631f6ffbd7c33382aeb422c5e7486f1ebc1700db60ab925cd62ccd2983e2ccf8347986cc2ac58e0e59e6c477ba381e082ffaf7989e71763e19906fa375e62d0c1b5bd238a3cf9d688e7a57798ae6996e4491fbab70dcca0808965f449db2b420ea622cee2e245e4b901d539efdaec52084af9b594c8924923f568f9e04195204151bfce207f7b9565f6299a8ad7bbcc201e3585c3c96009ee694427e371db9bcf61af74a5051a938e5ec161bd327c65676e06f4c445c37e435801178a0bd0a17b236759400220320aa19ff7be4c7d5b8f7ede0551a6434f4d98e0983f1a4a68cc27ff5a3149cb34ef098ffc8716c77eedea4cd4761744f14d06e9f36261d40ed77b74848f55394842901060ba048d3a826b20d0c62a46b69cf572faa8224cb51da8e217d636159dd85c3274b543a2b9815ccd6287edbabd56fbf6a47bec16f0de9a75c05b0d72d1d443fae6c5e408e282f28eec1c47f39deda478705262446a5ffdb59867b17cbbebecb8b1630e4327ab2cd38ed2a3d2e06465efc6122404f74fbf3f8abf6338e1bc4624128ebea5b049d63b8b794cde91d76cad808a49bb16cc8a489e930e36019e8de5505769f6dc8630a5b6283073bab8469cbd5dd73013962b4c921b8bee204b8f9f830f6ce01b4156923605e7073d4ba8d74dd6d6ba3a2726821b99f24e2662d21ea09830ac72e06f22f1eb3f0e519756c3b53077edec3a8c1c356136160da3965d3056ff82a682ddbbe80881e972a428a17170ad83740e57494571d2572526ed135b4372d29f1cdae054b2bd66ea0b99800a81e9644165ed1e866d05995de1903328f92efa4b777e914b94155cc01f780b432d53b68f6b9202baa54bb4c58c178f6edfdfa74de5802ec1cffbaea581b2f21378dd82417688e0a2c29a1b4452c6a58bd8226601488ebc6563812e6b35ec0c91717baa2701eff13fcb4f3a7c186416200d7b3811220e9d0a07a71544fcc26b756506752b9ee69d685d65de644a80a52350064c8b8aaf60f5a67bc59472a002233925826e0e0cdf0e193bd487ea90d489866ce79a7eee81f6966bd99934c2fa19dee54fbf49e57db8d7f1498774dcdf7efca00a75901e23afbeec1fae4223ca025d356bd95350e83feffe296d5c9c067e36330da8034494554c82d1f2c5ff22f784f4ba509571a8cc73d49e701d5640621c3a6afab32d44c8b1d050051661a2b52326b3ec162a6b1181701b30cac763f0194cd4753d78d00ace371c3b7cb916ed9e4828e3c87b86b19080802ba46117d6735d114596b3b41545eaf18975a73254cc68ebd8fca51ab9860925cea89084885da6cbec850747ddd9e4bee115c657112f36455e4e39361f779f1ca0a4afff70ec98fa7e7809b2c82b4b9e9c543760702166f59a9ae7904d5c797d6cd1665f8ade5afb54cb181a08e1a204eace0d02c24e35ced72060471da058170764feb931880078be4c084d8ef9c3eadb3b200c1e1e9a099d6e966ba875ee8ed2f87969d970b4aeed0654f75f81d722a10c73cf98c1cd8bbcdd4dc913dea36e73176a252fd48ff2e268ecb8c0108f3b7edf766bef2e6c378545a8dd6352c8340ec2b486b50002ff513a60b9a17dc1c40af7e6d404ac02bdaaade1998a5aaa9bb007b068f3fd33d4efa468dad4f56253cf1149bef307a8b8f50659c8859e11e42f826458ff3f5229323efb1c779a0455cdd8b01974578ea06e71aee07ca334d24c40a6e14664b8f58048ebcbc12cd12cb178d9f070565c397fb9d909602a0b5187b7ddff819f7125b6ad83980e1f3a47cd73578f7c88f26353a9ed780442abf1caea82c0c087a535e001c50d18f3cf166b178ca1773b68b7a937a5c19073e810c4993d395472f56414eb439320d04dbb6e52b12a54735ee3324733a87d25967939de971f4a6b789961802ebcf4bad03fa2ff81f68c737b48e6334e8d660789aab84ec804c0af75b81ba7a702e8b5c6593016c80fcacdfea50070bcd6e3bedf1bc99e2197b5dbdfe99c1e2e02658a561755d3ed6fc51ed0b403dff24ed63e27b572646e8ffc5fbf0743d9366b8a41622e93395b417cb006ef71c9e233c90885dcc21032773fdc6deae0249c23f41b408e19be0890ce18d304096d87ece7ff85eaf25bf0ed813651dfd92e9d30e2512e7e99942007a8e3115d79e80a2e672e9564dbef38de4c348eaf88731e34ce5ad66f02d24e68f5b8a728043507ae79ddc2654313c72d6b5c8467aebc9218717d32a74e60e626931fcb63fe7e6659bcc073d36d93447eddccaadcd99bcdbb9680a82869b59e1c3f0691b9426f4f025247780423e4117c5add033b9f1eb16c6d131edbc59391308a03256a28fd716d54fb938c93e9ac969a1531e785e39a4e48fd58fafcd96c12503984397804681a8163c03cf2e35bd8bb25f983f29e45b6370a0b1aa33fac3db769429811e1db390949c21ff12e8a8da11340304745fb734b738a2d77dd5e20fef39fbfbd480881d0452eb6241af783ae6bd9b4716c4c7166fc6481822335d0078026efe76327c82704e95d27b6085109dbe1e191537c0b10e314bae4f4252b4b85dfa9206373e1de1c4512e386cf8e3e6c5d8405d8c28704706a0eb16b36d7d30c30e5730b2060489efe209ba5774a12e843d7a9402cc1f9fff6cbde09ff45832fb30483098d3a313800dd18f1f1a6afe828b2bde2b26d66b377b67f7d1769ec96064d5b60e765ee653312fb1f62d9fbdc4ea84419b776a2231fecb92b5de466a15c9ccf2ec55098524a16b38641b10da81f57fe2d73f97b14d869d62e31270b96698a3c686a8609d85abb614aac289a8e9a01c758305e3e26e5ba68d0ab1b76a6484f4677f3fcb6b821b1cb1fb76c2be4779dee73869c8da4a2178464428c4de00df63021960e712ef7bb2971745bb7bcf1cd1c16b8c4e232b53a03c1f2be825004b8ef70c1d27ccca9f8bf82d2fd40bb39ba669b7d6b0bfd018a50b9a376e563db326c16f8a32d34b6d601d09729443de0e6574d593fd502b8da034ef4c67bc946e25c1f177325f3ab31dd1b0eda1ed4aeef3bcb2951971256c55f5dc968ecffbbd38184325a0108da72f9c107206bbe3dd348e2deff749e5bbd84631c13301625a889bc4b293506fc3f61b465a1c101586091022707e9b325f5dbe9246251546d7d74d7083813b9694554064a379966e9b2bbf718a039c301396370f68bec8c4398a0f7f9d8317dccd2d8d3769d152e05941f2118db5dfdfd9e75dc879299c956e7be99f6bc01b6984097e1b3ab91d62a989b67b8cc94f9df4b97d3c3023292363b3874a97a0cf4f9143c5d6bffce90d62d50f62959d1947e0628f4c82065c4d38e75da6651fdf5a69523f97b43d3043e8fa08bf2ed9f3378b7f1ee0971832577f6ee431414a6233e74ab2a88e18aa4d8d40732377972f8dc174c857744de36a986f48b973adb7e3fb333554a8fd4d7e856c2b12656c8963595190d1fa0d08dce6599ddf337cea1d372d37d3584d0321d3a5abf5c9801f06540492cf45624b1343f1139faf686cdd1219cd9e1c93bef4cd7b3fc62cf95c17d30640840545901bbb52c6331adce32416bcd16f4bb54c32e291ca682c4f8a8219a3283290b94778e8220d8a19a28271a7d69f939caa20721390564a5e253c990044e7049af77bb7ae875ad7789f616a5ed5c4054a260c531301386b2b35a4f1d24b1d2447f1ec97875f50c0e44e8393f3bfc6a25946a6287c8109c0c00cc7b71bea952495d3f277021f82ba052e14e7e2d0210a1181abfdcef6dd73a88004facb870e91325c0c491fc634bc84d7b547e9970ca29a047fe4f691d2149d88615d5337aee100280f3d44efb984c3c40781a5c5da4f14c493b947670a7c370678b713ab2fb6b967408281544af56c6adb0c6e69825350dc31f0e32a95a9e97e0cc176d0988ff119db07f33820df750e7f373315b31832a96d2e4b71b529d44290381e09d47324c7efba3eaf2cc843137b70b2323b8b8e39469da887581c07788562a47738355fd2982cffc7f2b8fec2d14ceebbd4024540be2019a9d4d1293a3e8b9b28bf6458e1af7097530fb86b6bb073913a3265718f0a0ee51595108ffe27087a5e0edd13c42579f380f0bf10441eae2aeae2efa6710839dc2f0c4443ece0c882dc482c27fbc266b2383b87b16597f81832273d064c46afb35711dd76122a8a47b19e5e0f8bc7cd2540136c351c853ff1daf7235e29962963ccb46aec932aa269116bdf8a4683d316f13019179857ad108789fbbbd01d122ed422f47d7d069c042d9dcc0d258d630ef04c8350e891d6bbd8367e1c27f3f3325f2b0c10859fcfcdffda60c27c7555fc8449ac568fdb91cbc76193ed9b92a3d8ac5e7eb616d69e0e1b1e0729f4616e53e2698adf04a0eeb2a34e0e9b5398b665e4a68000bfae1cbfc30606091fc87cb1f663a7e26bb4a10c27871f6dd99d3580d4dd6cc8daf238cd9ab9a0bba5bb1a7a434130415f5844ef7c704a86d8e95ea3b3a7ec1f2454bae21f533f457507f97b94ad115c3dfa70968a9db4e86c53ce0fc5488ee8a1e40282f7d4d34b9e5afd78e8865e10e71eb48c9a996e11a2cd4e4abaa8eaf7f71b330a6cbed9a2aa8bdd0ba4281ba5db13b8405dd208f06e272d9adb9d709f53a83c46d98441d280fa9246db96e303b83fbc200bbc65bea3d9ff56cc38c1702114d9591892e5218137c18f811c4190f0c780b559432bc18acad3134fc0e8e4668065a3479e64a863ac73af9b33c25c827b5a78124b171ad8b2780bc5780add8c6532b513c271b0b6fe07fdff26aa246faf3f1207ee421148ecb5650dc96cee42c9fbd2dab372091edf2654223c527b36abfbb661c2822299a57607f6f2d34cf2933b6a5e545d582b37a44514c37972cf133c7b822a002447a8bd09e8b1f246591fc8d2ca8e5ed5ebd04b5bc690456b1c5f70d9b42810eccee043cef03a63b4e02aa6c792da3f8ed04ba3c20b38edd62c6dd39ee68435d569af78255834b62934e939d14cfee01948f980332284310c2ae7e2230943c3765ffe99498abf6f02c37dd08acee24b46b9a9fc8d590829211056a7ececf7122d185f052840a3f38d5a37432bbcc8eab7fe21c007c16b7a2a9e2940738703e42bef37f6a79d5f49a79993e5294d6129f494a4e96d35e7021c1c211df81810761680da927e0cfd518685c93c6283139bd390bd772bf8abb2ce20baa63c8dcf1b3a180a7db6981923ef2989b7f2abe08a027fdee9d4f26da0211c1107c35834cd14fb87a1365820c39e6b8f12ccdf05abafe70d53dd7fe2d2f9877d038896e4119503643e1aa206a3232d92a6a4b4681cbdea7a4eca8578e09e13d12716dec3f0a272285d38a037701c4e69d89a5028857104cee3237d92690f742acb999c238f5cb14c8d247948ea6d2c3665c32cb8ed349fb059663a6e88a05cdc71cbd8b24840884a5e874492e398026428c15669fc40ac0fac6d089a717d5b0820c38c86d296b2e073534edc9365c96f64d6508964481a7c5e9bcd50c5667246b82a1f1043d6f0ae2d84ab335002bd1aad88752d3a536d1d5d55137c3a1226aae46e744c2f7d2e59770dfa8cee89818717449282893d7de040cc14b16956e247d015273341ba740839f581b2d11be31704e9f11189ec7896c03d12e23e143b7c780666eda07582ddcfbcec11172a2a591f7feb5175ab839697fbbd4d2fca5b0db00c7eb44b77fa9b0c931db7c7c5092e43a271ca79b2a336de5c90a050f4ec2a01c69ced482590cd7ae017344d56097a49ba798a5e99aa6fd2fd4e949d38d6522e31f2a6440022fe97fb73a671e9db088c0cea49abca10566a6a49bdfb425a7fb510420e3345cd694b906bd3fffaa10755f1aca312461c2e0094eb2c878d95f57747b108ff0a5c0ac37a1e5b9bc5f234db77628b97e380ea9296489fa330983136f961ee131e5319022b226ecb9db0ee192094b4383750f215931b3acd966f9493a8b6ca7e41e1a3a317f30470e819ebd57fbfc4878482c35eb4f9527b3294f0be29ce0707efe587fc8437d09ab2b109f435b8295504f76d5982d77ca3a8b18fba5c5fb4a6723ce4e0f80a4a3bcfac6859563f47dd24c4212705f69a35e16855e598774d0574df6baf4d64c90ba7338e2c3ef78d6101414d2be0425c2f39153a68f1e6eb838568cd44f7d0fca7184a93e953348ef59361d9aff4a19f2a14c4c0c962f4dbd5e5d2a4876a2ac0237843bc1c04d73380150dfef4dd9190e8bf4c1e8ca7b65b49c365ab31e85e5d37ceb3b94b49c23e3589567926ac98bc15c5e2f71ad244719987aa8a3b38df8206f37aec2ec4c2c937130935e09d4145b8e4e8adfbb4b460b8382929a0fa1755e8445e3d9d64f40a696cd8d1aae4e55f633eadfd4d4041216d242acd342475c09e54dc92731e48b3f4b3328179e136432117eb90c43b76c1aebd2be74bc05eb8386a26d7fcdab6c7d4cbec82c94233aed6dc46a5a3851925d066c07dc2c2f1739ed83d3b4995013e6fdd5849c6e0ac19372e4bb9e7a7b1590db846b29098f56f418c98ae40d418ec72163d5118b66e25b06daecf046cc00838c32673eff8a9294b32c24d48778c03d5a38af9af62a22de70f8b02a4938803b2859442c08f8715b8d59c2b19de13201c8a3508fe4271a6dcedb4c3721d3fe488234cc5612f6c41dcef92259073a340839675b8c2076dc59a69aeed3872f283944de8696da9ad7f60b409e9bc5bd0768f0925c57eec69101932f6b0fb23172d7917883fa8e8f030f2e098b3072af109df796c25f1609c494356d9a640c44e0f6f411224fa1506af884f5b1ebacffbd532b5c9110d5021b2dd06ccf7688c3e8a7e6e227fb3ee5716ef3b27dc03c6982ea08ebee6a85ffddaab35ba58188fb6323bfa35e3e787665cf3fda19ee41e34d05d26798af90e75a370c652b000e1a3987bbf427b0e993dd085af7d6d910ea9a024526d8e9a4239c5839269b08425f363ce238387e0f4e77e81cfbaa8e304cfeb77b4dcdde337ef078b1b989ae948dd8354160e3d8db918fac3e2bf8b8522a67d6086acc88aaf45f8488a18d0610e643d9ec960c6105fe8f43ee2be2e26e90fdbe7e63a9feee57c518b8bf536983cd966341f155eb5bb30fcd9ebfdd0f6661e7e87e4a9d9a84f82e4153464bbf99fb14e2e244e819cb131c897ac9721758e438a9d8c3085acedfb7b910062e82aa19f9c93865663be9bd240bbad3528a8bd140f479ce7574d80612af5367218a167b977296027be1aefb1e3476353eed256cb9b63ca6c3fa01819895ded96ba838dd5dfabae13308def5d3a931699bfdce45cdc04457b3305b2c66d09393b1d3d7dbf09a59f388e4c9d3645ce53f69268af06b1a880f428fbbcb8cda50890b4e9c285c6330f163f3f0d3f006b53a93bb902617d146963fe4b19591a46a484ca9cb0f64df0be824978cc064ea6dbb901f80ad21be18f9ea5a3c4f6137136f0a0da885c087fde48a2aab09d41cbc46ff38c845eca040fdfac5ad04d1a49aa8adda087702e13eeb724262dc47e2f268efc31ac5404754b20be4ce1153cc79d9705c5fef6087d9e5d51fc39e19a33a12aab8d8559a6d99fbe8f919de7454ca30fc380b8a62b91a76b53abd3d06cf0c6fce96fcab10d6cac62922153ab7be1960b98ef5e2dc9bea11a02387291c4c985c62e9a42ca5333b8b3032c18c965feb1fa2114d86081e109851b33d279a5e8e1897b929b918b85f13c8c07f9bdce311d3ece8fceede180602a94bde187467047340108ffcb0d4521b81809676c85727cabc1adf4b9bf8ff63f675ee8580c2afabd9b127e782e1952b9706ab081de6d38a395dbd152e70b8d078b1c4693163f3d6dfe931c0492122b8cd266fa53131c931da11fa9af21d4eefee37e7d24874fc9c2118374923ec191149ecaa4a4317324d8042c037cc4186e4ee5bbbf8e0ea560bf83a30b5f54fd66369642d803f7046ead307c98ddb975da9644d31abe88d02af071481764ac080727dd4e7454f830b6670a5d55d5c5cfc20d21564072f5bd2887f074c85ce025d415b9f25dc03df3e91d6a4485d8d79697d9360cd33f5fda1d87fb7aef5501829759f30b265853702e1482b77faf20ccbb397f4a7391f7bb5131ab011d7c896534432ba10a0a384f33369ca7081f635f264b216133397b8af9004dcb4d713315fbdf30d3833d83aa9d77489ac169e2d63d5fd30ff2e5b9f9769cdcf806642c7ced4780aeabf77f4a082cdd7069bc979edd56af9bc2bff573b9d36ba2e0e0d451458e75062cc2ee00f0e11e65b313b4614a8e202b7f35339e0a68460e39fd94a5d37e28ebe57565d74e03155773a98c4f892e48d7f6efb1dcd579315742f5add74278cc8532462e5da15a3aff24662d76804944d2ecfa0afb38b013ba26cac6fa7c77bc91fa2f58b51d959445dd04e1e2316928404fbf6469b7180af96434cd174a7bcb25c0a633ae4304a039771edaa1c550f5008223b16b212a737dcf770aaa97b5575634f89aba27b79afe69a47052fbdc17f6447190d336b837cb622ea6bccd2c468b412bd672671789e6defd5de37f264d531d28742af0c32f01cd6d403c9c37e81fa5cbcb8228c089f3781629072468c7f29c4ed6eea87b7257ffa0613cad29d4053e3f6a5c91ea4b630ab880cb710f80a7f788b6d3ed6ec59741790cbc879297e21cdb30888f9532cc0ab0157602d7a1ec15d5825225620ae50566253d15398005c3734b7c180803c06091c2fc8a82a278c7f2d707d22ab8e11e0233e03053716ece6104d176fe95dba2cf4afc3410c6f90b4e75a5cfd3cfb4af5e32fbd4ef5cc9758c06d396b769083ffeb6158bae4f4c2a3ad51485c5099258032d7fa01dff8f3dc2fb159f4e1f7d709722ef6e12410021641a89c51cc0830f6d8a99e006b5695d994b0bb5f5f6eb7b506af2b500b1c46af1f8f78b6f26bc65745e4192d0e92b26afd19ba3079fb20654c123bc09192733b7dcb1da0ce01bc20419c79ab07e5e6e70b47f99075d3b75aa00ad1596be444ebfd6ecf9fb264d62adbf50f2bc68781939cc7252e224972cb3be07cb2daf99ea80d2d14112a04c6da0cd147c268c79c400b56d1f0c75a3217796c8e5ce1546813c714148f25e33c2d18ca441e298c3798ce9600606c1a60ff32882f10d08e8d33edf45d74b07703d6788d75886341ccf114ac02662e0ac1fac1447b60ece12e0650f80921341e167aaa9aba2d861ba467c4c8bc77031a001852d05d1aab854e7ab1b6c09a71dd76cc73fd44672922d46c5dc6211e0e287e80466d9fe05ceb2c817b5c7ffbc20cc15361928930c072c018619fef9e3b7306c6b1925cf229126978b376c408878d92c54a565cd932480b343848815538835b1c32b9f4cafc21984b5e4d948b8cbf4cb6437bfe6c7c6c916f8f899200e980e7b1b80fc567eb10dab8eeaba806ac471e1f732c03ceea289f85c9d777e9ebcfc6e2d4194c47e4ba50e46a4b628d20731966c11b646a1792a2492d38b2f2e8ad02104a065e6cddf160c1b66a8aec5ab9805f3c172a878029a1ada8dc04d67fb066038fa6a921c8b14ee0111e94eda295c9c5a7765f0f32d38fdebe091e2c01c1619144fe78278f8040d447f5112308b9837ae25024e20a37a3029abfe24efac7e19655083d388dfc7a6380fa393c70cca5a78716487c5294c737aa219ff03a483483a03a1acd5ff184bf69782259f22c938cab75497566b8520177fece08914725bcb0f16dc3ed20763a52c997482245ef51bfdab0219a15b1ec90ed82bcd58cd920e34b6481d092a61e222c5a628de34af99eefa745728417a405db6af20c41945fb319968882b910690f6e69a3a058706ba2b498e7f748a304b42bf8182572312a3eb7cb00fdcc59327ef9531ee31ac60c9d70ef6b4f52fad703306dae20ea45be6bb2ccc7be5eb7e0f2ed6cbe63ae1746d412116017cb26b95491f323515a3cca891fc39316f4533017c09b689c8b25bf173c83acd79b5bccbd46a56409c1f9ddb2fb8bc77c2525d94bbc5f4171c4077f60d996de3ef0154285c77ed3b29a5178997989e2b8ae4bf57699d83e122fbb20fed76aa861693caf8a9cc21b9d24645c86592a402447c6cf8e49dc7825bd48451548537e1610954568a8651fa1b4ec95cbb5afe29a0ff9a3737eaf3564ebaebeddeb322b578c9af2f263aeb43b4696504c763c5ab26b06938331bda4524bb81cdeedd50ab113fe921d607687a40d54bf5555be26ce6cfa75cab94d269ea206e13bc383b64456b58ae698e00ebd1f22c23e4a6c34ee419331a698bab045eb151928e736cdd00337581f1e9b1fb3eded3e09ef7b0b9d62d015282ebed69e55b775c481908f31974a94e7a89c74d963fa1455a9476edaf3fd2a57ee70453ef9e957850aade1bd9191565b604e363898a8341d88b6d093eb67e844e2302565773f97688a5b95aa0136aadd952652538895c2a415e62dfaf3071fea6bdb4fa6bff44852f7588c821be04d894701513a8a5a88af99f8017d07a47ecfa8918616a90112b3851941daf8b66f61c29980ee0b22fd903c9bc2d276725d514d0f7662c8c80ea3a801bdee7144fa08ca0c844cd77eda20241475efd8a2aadc5f05628e86f365a5759be39148518e0117a25a52dedb2bdf8fbe9307df0a4da34a7667caf39db2ad042cb87903224b3fe215a7ab95f61fe4fc25e923f368bfa463276e89a765a6cf01d99dc277649c4c2bd9e1b7ed4d215f77a85940e3786c84480c74a7c19f04fcd32e60d065c83fc58a93ef4596ca4d0a7fc07fe29d06775f65a6f4eac95e43fbeb65204be25424450538bcf8fcfaa7fb237e078763f3cdb89e2eedfe931491271ee5ef0e3c662888e1aed45ef538e3dd577c6380262ca6e70ebaf232c78f492baa16fd9a91d5bb9d84500134f6c169d2b975004b3d9b09be3f300b3a03dbf1f708b3f3383cc43b85d5035a1808d2b5d347a9cc96d711fbe171b7172e7d34296a9f67e2c2acc3944906969b9709fb5a1e274033ff7dec7483ee5f6b1ba1eefdae135e2e2fe274cc719ad17464855e1ea9cc834cd56886e1d508b78934ed06b12f94a464c98f44310b62f313c0f368633e0503843fcc1ee96ae127c2dbc65cb05c6e14a9f8de56e15d3dcb818f95e881f627465c8a785b01de8205ee656174b5edd0e7da740680fbac415e3d747d923cc4b1fbe660abb5711afef461c01bd11410bed2a486f8420d36699d498f5855746ea355b1944c2769a24e7109d036d1e66bbf9678aa51d3be6fb52216ccb619c697c043004efbea6781c44874cec84fe7f09938398602879b6eb30b3dfa729e4f261849708475d0666311d13bd3f431bf37ec28c5c881c69fd977aba26a032c69b79a5853d1833ccf17c8c018d71e90af9c47d8606bb2bc92840e54a9084d94e655e50c64f0fa30b18ce490459fa3bcc20c03ad7e77808f2fc80759f674212ed4d57bc34ae7c5371924dda420030bab261d7897e89d12fc131198acf5297bde814fe9ceb0cf52d6d4cddbd22ce79748ad9840bfa70426e8d0679d338418967bbfb0126bbe42a281b30a0ff795653d87ee6d25e8a8fec754fd788671954f4f1395660833c4f0f7ca956cfcd0f35cc9fc37e89c127f0f67e3c8d01f7ea43cf7559640026d8bc7ab55270e01ce8a6a3055714ce2db52066d85f6f76a86b2b7783065c984a8759c5446e9b344da00df729562fda088a6b99fd2edce6e0b36470b02dd110f658c678177d664c47d3d2616891d840180c71056ebc702b3df645225afd9c55ae3d0145bb064af2e4ea638a37c80ae1ddacf8b5031d8fb90de29a459ec15e53f10a9de06918c355235976a9d375ef8c24fd5a74e235a0093516b76e992f3e57e25b9d9b772166e34e7faf37f9f31dade795d13474f6012e37cda73a55336029662ffbfb62a2a07bfd1ae0f19106a5be1970084aac57a1b669e652842eaa332c4e5bd7f9cc45668c28f644c2fa65f2fa9203bc569cf4b37f279a229925f277e0699c8be53f1f8e6350847cec549d90db6f97f37bfb8c781b22061ee90a99678dd42030e7cbc5e69b03418362ae5d8074aa1e92177cfd2580c95a58d4fa341969af9849f307e5aed29bea8ff1554bda640a0b41d64f18addc379e532e244ee82d1ada62e8b2ba355e64d12bd6cf0be666a68e7f9e9dda6a3d2e08c2b67b2f3ea8af9fd47a56242d3d20a49aaf5d0ddf9918c9fff89aeede813a3501b5cc4eb07757e067d18f1489407aa1d72e483465c1ea8ebc3ac8860d6b7fb7a64ae728fc2573460ab00c805ea1bc736aa51969c7dfa16f12d25584db264662e32e37e1242e552ef0d40b42af60c11f4a2fecdc719b357ee54c4570bb3724ac0d00b7b974c0de9f6b301291bf4460b186d7bbc16fdcb6460570455650c1ff1570b3ca67e6acc95452bfbcca417ceb3f44626545b6efa8a425bc242d84169fe6a1b01bc4a793c633e3dc21b6ee2cefd73af01c20d2b0071746de89a13ccb0b9c094bbdf97747669f710b84cec3cb36dd6b8391002ba7ebeb0d609eaed60029ad057358a54f95b564b40645a91b3710285672a1c5b94f43d8355b52ae3a74b00fb3b96535db66fc80608542331a8766f472b3da0218c69b8eb3f9db27821dbef16cbbbc616b80e92444ee26d0d9c5d6d5e59c4aa7c4714c88a664b001f01d18a294bd3799e7a8c824a448e36322639e81a03ffd18bb0a7120c96cd4e19c238a0b7ea8242ecd4247eca0e9183c6f1353c29cf0f9db44ef5eb04d0a1a9ac28967e3edc574c4f17f249c5f6d2622ee8eaa32b4b2bb7527559cd3305b072eb4f41cfb079fc4fa530e12c1bc0612204c8fbbbfa70c2abcaec44548fa78eda9167352405f37008f2fd4823192ceb5a5375deb4142a60e92285f8cc15a6186102447012873e056f5faa10c827db70c6f99457f1c049c8bd157c8e57399610970a6969439e3aabfcee54de658877a1a2cfcb0ca6f1e1d39c38da3f55ff745b5a6ec7c2db7cd2b455fe9ece713ae0d1dcf7b6b4d92e152f823b74408d515c744d2cd8a1faabf23d544a7a3b348d91d4f09566734cc7621cd3563410aa1f6546bbc54250b1e0b21be2e9c2e31810186f0ca7ee43d63274e30e683d068fd719a8b698f73a5d4f3ea8271a5c19227c0266250b21ce7b2146a1c5ec71eba0cefd1d5e0516c2f783bc1322208f93c92258d4c40e3a591301fff37a5b33ec3e2b7bf612ee72070303ee1c77d2b86163f27dc7eb573185d724e5dd6b65a39badd8cec0e00ec338ad18d54a0e19b4d0011ec08ba51867144cf34f2f0fbd93adf91947e3b280cce4ed8c8dd718e3e55170e859f5fc89b6f9f1f1bbcb71d6764469f890cdfe1b948310114af9c1d4a453990e2157cef475e7b8a9ad75b1806c8dc2a5c2d68fdc437f73e0b5576006cd2281772150b4883309311595a25d20c9b7eccb5d80713d39e3e17355533e3958ee402bb165cee066ed65756d0223c3b25a4363decc52f17e2e625f6bf412c68d59689a014e02f1fe35609f7582a98f9c9a24b746b5e4be1a38e5af419bcb0350bc5a4da5541ab45d9bb88b7c629b4304133fc24567a24bc082dc7dd8e4a1fca163273b3af13d10563e89c32ee92a4d289fe76b1334038e7dec472e6c9346239108e1d5ebfa12caeca21bb62a77b97a24e3f4ed6173596b68b088b96a1d38290a1136a401fcf149ea8281df7f1ad4809b3e8997255556f41c34cd2f5e420550d5087912dec4d235512082e647cb2941790ec5d9514f9c5ef4228a2cac41375032b7f5a2cdfda2be1642495f01f19d64f688b967945cfe3b8b8910b694e08d104b9899f7de4d4325ea9f1b54b5cbf211e264591baa3956f1c0bb7827b564de561f235dc6ce170896d544c7710891bc9c31a8f0fa2de0fd188e3f9cafe54bf75e2a2d6d2ffaeaccc81454e990221a816680cf648251dc4892d6d3bff0a333764045b79127b75ff76267cae1ba3ee9def91377dd4c4fabd079b7c52678df5578f67e0e0e7e3342cb412a3591ec1c6890b262959977c69cdcc5db90552cceadb85f43a5ac106d84fa2b806315665abab7c732490ac723d8e27276eb5064f32e7d9cf0e10e87a57df57d883f6ecc9ef7e77ee5a3c664dbb2e7cfb3a42a9c551c297693d15a69a7908b605db0ed7fdb4cc11fb941eccb4161393e1cf5c2c513e00033fde0c77c47b0420494a6bff70c6d3628015fd845b543d06c69862a6efa8b0c579cafbf7d6aed63c5334090cf39caa4aeec5686391f77470343e8169c3bba6e359a0df06b16eca6d5f8d5f1add8b4493fdc3a84f77f5b45360b5321d04535d8ab0f0f74f3954c744b161cd7c35b594177e3eabab4e0e367957d065731983881a8790c7e2dc9b6b270a6a0a32e3d36b8382f6bdd350de811893b8b825022ce89a3a841cf1c8cba435cd712bdc14bb27810d1f2c539ae18e1ee856309cd01d5d51eff9fc509076d4fe35c726736e3f20038acf9c905b60e1653ba823e2b5b42307ce477e6c3e21b0df80685fe6906bd38297cc5948d76be0411c66fb24047a5d9aa422bd128fbd47bd0489fbe5c3b08b374c0806bfc33cb20729b9b63b5c13d54794f69b3b331aca1eb97f05d6c1e129fc8d502bd7eadaa1aa7d0150a32cc99db972821dbaca00449e39d8c8a40825ece730398cdc465d7e02a1fb49badba75fde84c1813b131c23c101aba6be9fd867a4ca64f50c1739dd535f59b6912df5c390ec7ca9e75f5755b050dbb9f6af88bef727995a175c87d2d49cccd50852759a151ff3309d36ae0268646edcd2805f8955ff44a71b59286003d07c0fc930951120f694c0a403369704d895e66b3d4a6de31fce5afbb42a39e0e256bfb0c4c3795b485b531452be0f1e0cd9c8c0ab6d31abde5fef56cd71451a31b810e76b5fe1f0365852f2fca2f762c6e906fa6ea09e507331873d160e6f598ffcaf4da6c8d0fcb67fe9a34a051033394dfd11ff9284472d41b0978e3b6bd81f0d3146f66e13af5f4ceae72c990498c0d5601104cdfb8fcf001fac89226693fde2db6aff96c7870586dd5c0dcdb96c453d4adabd2ec7fa3f2c00e8f2e616b285193b23b2ed2e20a701025b5fc601993474b343e16ff35fff81593be331f4da7b2eb3cfe8fc2a98f29627368b0a703e04831426d8b5921f9f709dd5a025c589ca718ddd51ed8d23e643195394478f8d4f5fcdd170e5b2d1f8301ea54ccef6b71cd99facab8736e4448efb77a506feb94ef0e0f494b40d04e4322432dbab38a16ea691878521e528992696bd4881f0ae82321bec3ac62401430a75a8a79ccc6631b0ee0dd8d3a6004f1b8a44dfeb52a8d145bc42229bf977c1063dd23fa85bec3b3ed8956fdb08c2544cf2c41bb3623dbf44c53f1737cfeb5e2e26cee92135daca4f55cc8020ed550640519b2cb03a26c886291956b7dcbaa77943963443cb8157aa3f0a8aa8b27d83b2d27fc6db6e8fd74f946536f10c677684818c3aa54957acffd319e9e78ad50dc508bdde2d1e807b1f25f93a118bdaeaf0cce4f18ea966f85f5af996c33e734846cdf563fd2947ff81dcf3e8d97d9035fce10498322b5824c081cbe9bd7abab5903f83e153e5b0d8e785cd5817644b0151135b81f56e811a7b0417d674a01b7b97b5c3ff3e1ab5338038daa2ad45b598c0ab9f2ea793d18d99181a14fd06dcad9703b81edab02f447f42a4d3f1dc8d5de90190be222e09f16678f12ac997a2ea418390834b790c4a899e3f4236d00ac37937d7201b873238ecfcb1aadd4338db8aa8daa3b87b9dde46ee46d3eac3febf1c63ff36d4c1b969188f95edb1114e19859b84daa7a9038c7248de2daeb2c5ccae169854451e1c1d2aa2a5699c5a641ae6903d59d36a579b600da3815a7c8037de2a9e98db7161b0bbcfa7dc9658da2e8644744c730a98064b7c27c5f15b9f9c57f1329871fe38ce8e0e03076ba6d740c079d0029eba14acdbd4f08f85d5db7229a3b5ae5d203122171567ef060e4d99d2f96577407875081729c089084e288d5029d601ed7c97d098a33114076142fb139cabfa97bc1a96365ff9bcd1af0184cb219486b02e452bb60e542b341054a72c4adcc0432f982730a1d0e6f0dd515194517ad5ce3bc7cf30ee839bb206b0fa4e59d25c5b988e847c5657570b5fb8ea3b0f9941d1629b19755c516b7110f7da4d6cdb5bca15004a4ca168ae7b67873e85dc3b21fc9d54f3ab38c6158aa2154ad71a952a9dd9fa05a9938e819a26a27a6cf815aa0183a1037ffe1eb2ddfb710d6c5eccc1e8abea22525b08b85236046ec307c1b99fc686366f3555663ba1ec5f71e7f2419593204bddb686f9b1d106a1a5edeb5c141e344b16f965af390401da5d6f7bfc4453540c96f306c3733fd5776481c89532bddc601e26248cafc895f1f396c4e4a54dedc1df03f2bfe8522f8f51cdf5609ab5b6c802ab01f54d47eeb0065c9e0a58bcf0228111a19e816769fa51cfdc8123c40850c4cb39074387f77160589f9f14cf18df3f4112efcce0912ff2129f52a1203aa0648e3c5fec2545d96ca831766b2dea9c6350940ad23bb7523b98bac25fea2f38d8e4d6c9cfe94c3319873cb824bd33cd19aea6230ca7eab9e89cfb1387fde5027603019657e71c545adb258cb96ece5f2ccbd7dfabb54d34f3d436408ca1279e784647e78170317ecb5ca52032c350a2cb00fff33b4c866a402f41888e675052587757ed33a0158b0ed2572565ecdc0f3899a4439e09de490a9fb5476d05d95959e9fb9e9b70bd56c703825ff12dee4362b318d227dd3be53ff2abb236de301688983350a257bee0cc3f2818fe8b51f49629781b98b2672d3e154ea0a67d0777d442bc31a3e2e48bffdb918b2479152f39052ef0bd3acb492b5351d1a5a289ea5fac7da453f09b8b9d4b05667112512cae87a292fc459554bb4c7873f36ba3444801eaefae1dd30cd793b841039075055ece4bacdbb1fc0c74c84116f16ddc47ac2fc349c07cc094be79868a12bc9ebd8a2002eb468bf64de09dde0d0dbdfc1ed3c528d5b66788d925da28bd113d05adc19c547283b0cb290779fb3ad72b41adbf1c13059769a11fed162d8eb220ff92998efd7101596bf9714e94f9f275f75f89a4c855f256d6dfb7559772cd2d742a19bb5aedf0eff45229d9985d5f47abd3e729e2ab66fa684caded36703fc6994dd3c624e5ddd0f3194bb76e1cd6f394e0ff7b41123f787bf5152858ca2ccdc0923bd9aa5be06fd81bed350348b5aeedee8d938183896cbf691aed523b83ec7da98c9814c5b387893e6c515a5b4c688336e6c7f28fded30dfed5e4cbb80e73e105b32cde481fe2e7bbd6b746b33f478b078a57464b4078a74ce0e79f195607427bb73bc4e27a70aef93d948791948ac12637799156bc9ded4ce5d918e4df7b56c0ec83a4984d7fea61766941c55313fc1ccfc66ab0b6c790a38df8f67f5a14f8ccc239ccdb35b6693b6626fe16a139f864b8cfd996c872bc1132eb3b92d8906a07b58454de893d4e8146f04e23cd768a5b67281d0b235412e5fb499869e575459a6ef5628ea55654dfa149defb27b376ba184ac7fc47a467a6cf6bee1dcbf25fa9d32fc23c913ec198315f0f0a9aadc0dd3148d4cfe601681f28f4fb09242ab5c746aa8827182ff3506cbf021321f8bcc554deb21352f0eb2c59d0f568eb5097eb3e3204cfd0386dcb26ec51fe1c0db1d9fde8d1848af7a0529eb1ad0192757cddb5b9d46fbc2cba67c47adfd3c5216bd0138f456e996eef819219e43a8ebd03c543a51e9700555042d7518b2f3e1fab123efe87240d85660c581c5738d185a6b06191b5ce938b4387af9d787dbc1eb7cd24f75491e3a0edee0efd54c94605f43276a0dfa1cb9b5b31119abf77fdf83175ad7d55f59cb7e732d55e3985d1bac6d21f6057c59f1add543f8da42a4a8de51e41b057f068f01ee66a877219384428eb173704876bd5bac7b7042bd95f8ade2ba95e0fa0a8ee94bb5e9dafa015a223dd40a839c810cac0e11eb6625eb5948f4afd87a5be811eea9b2d48269a534dbbb824c088478f48ead26c46d07e03a551884cb90abeba1d6bb19104d5c13509865f9ba0dc468c0f0ff2584b1653deb9a90423ed31dc46c210dd594167d2aec321c9e3f3ba57eb3a0b48e762c188f090361e803678677508cd01dcad48b7f1d7b6b01599b970fbcbad2ac40bf318e602bac66e14456a0e0fc676b44a62bd1432922c5102545f12405a939cfe3f6a3e998c550a2386a1147601c06d73e321d0770e11abf251461594861a0c2729e4a2805f74512c56eb45abea5e158ed1b5f21f81ef124456af24c1998b5723b56b646add01fced3988ba3494933afa328f0341c967cc8322e8d77e15d469cecd5e31aa354e605a6ea03bcc89422ba4f5752ecfbad5d4ce061e5ba2de6434dc356c69e1c2c9db43153e0d7986b89b99c5dde0e825087af913b753566dadab29922316937665c2e7f3e05a79e31ba1fa4781622d2e3ce89ba9bc04c0851ce336ee9df66c7848ac25d6ff585321642c40abe3dc4c26e0761c6ad0bb57711d10d0f1a74c68d8fcca5cf78d33d2286b35e74ecf053db669bf6de00eb99b0afd02068becb1cc73aa279a87744b0549a7c6249a75e1c0a8fba331becef2729e528e7afbf314e08b23257b6bf5b2fe96747e38259848a49af7c1b0445d80e2603ae0a558e6f9f4b41d7eb7099e8b578e3fb9f84639e4417afe2ff2ab4229d9a4606cbf6423af2119872e5a66d6d5354a5cf4eae1784fafe96e7903d0f0fff30fcc0740e739769774082bc86e9c25f6dcc93f84000b22c67390661b1d2da68e5916dac181a623135719efa426c0d8813926491853bd47a2291a91fde48126c85a7e7606e705c6ac579f8ba34795e40c8de57bac68fefd0f9a7b0d56f457d635b264a3aa318d977677575af47df5b98ad0cf043c1a591c89776a5d458776aee18b2aa091fb3f7e6212ea2049e548ba79844eff49e99af206db0e91daf3c7aa6232f72d4f98efd7611b3cbf4b8a3a534c3c4e1843c9496f39868ed688e76c979dca0dfb4648ef16b25e1251decc87d0d854a63a34b18a877ca3427f25689899b50fc79c404ed6ac3ad75fda6af52b27b8dc061da702f2cb152e490613d1f58170d3367424ba557f87174e68d12ec7476219fe230e6b6a5123e23b18cb2525a55ec7f8e9bd75510bb84f746ce9f62675a37efa820a70622e8810b6cfea6112a521669b52d053e55ba646bad02fb621bb5045278e2957ba09ab5537d86f78b433e8ec34ca1884d8235afb023d26555fa43cdb2efae7e76c5b88650c08536d0d34a2345d6fb130df3fc303bce9eede0fe8949dba31d90c4cd6af1b3e833efe3dd82752fdc885e7f07cf747c6be1c178ab09dd46d107be501689ec16c52ce02b15240c29d3160b762a5d7aebb6f35cf6b3896bf7d3172fd16f56b2c77634d7808108dba83bf7d40b1692a532b571636cd38b485b8357ae4fab9707cb1ffab5e9abca8edd98273942746d11a3a0f47ee6082ce1a599032061064cce386b1b92dcb1411689a75a1a69d011bec0b4996abe6cb4f7b4f455fccfe070bf0a0071e210db2a09f30e7ed5388b40985323f1bef90a1a340384144b7f8c1ceb6e1ca2d83300fe9376f4e86e2d88412e3ec349b132039bc7313b4ebe2c7f94c0f0cc3c4bf41630981863aa601ae313eae593f7093e36ea2c68f291761276d2470aa7e5b5ca3f8ca37cb4a88d32e23716ad3d8289f26210302db7803676c003d03551cee0b363b439fe4014dcbbf0438fe22d5b29506e512fe50c521e79c0cc755fbeb28b3ec281713b149443ffcf6b28f094347100375a5bd1946ba6587987f7c39376387162aa102383bdb69e01f8c25dd5ccb22135978f94e9487a2a63698cf3428c8cbfd7d061a80190d4c9dc6041008944648336fc5342b6b27dc929b17de0fa030a63ab6a9b4a39e61ab6895ee89fa1a5596ad3b32eb2383fdba389921a85e635214f4d8b105efec1f57444df25d8371f2fe1e4f2b1dc1c94627881440d9f29a354d3dd1bb2c99ce04254c56324b75643b05d9bad375fdafd44f786c440f60e09c3866c5eb0828c62dfc6f1763a18060e373436ff50b0e6d36f0af3120e1127856fa3a20697ba22823608dae817b6c6724fcdeca9d732c18e76de14e9780812f2a2ab83c0114b0d6e63aac7f661c1bb76742e3711995fb68fd3eb8014b5c8e5a9870c3fd40eb139f93c5f256db7bc8ffe01d2b18398a16dd3f1ea9e247b9587947b1ece300ef267c31f10bed8b3faa56e5b6053691bf58863fc96f7b12503dc746fbfac4ed2cdb36d8249f1eeaf839adc3a5c1507243eb4a5647e9c67a8c9cd3b95a1a0c33f0dd7e8fec3646bebca14870ded8555118f4a30098a257985e96eab50319e8e234c5ff66d4c86e97ba3d510f2027ce28590171ec3526102536d5acf9fa5c76080195326df66cd7df7e8bf375740a94175c9fc3dd3d11db62f673c8dbfb29e0804960d247b2b5207f35e58667a6aa6d242528dbbc697678aaac4fa3cd5ab8dd118f3411f4771d0f4beb417bc6c621617fc7c4d0a568ddc52ccf2584f104a88b8cf42913deff8a009ea280464f1b55ce53c3817f69ee990daf2bdd5202cc01c79455079a929990469bbb99876a89f497c73d6889fd4ccb616906cc9ec4e63347d5a42f160a774b5f2e2b9ad2439f3a0b56d16517a55bbe38fe79e33e945c1dcd5f21921577b8e0f150515c3fdf406fddc91a0ef19eb144b22e9b4c215088cf80b4fc3230f9b29fbd05e2d8d0034871201005ded97760e13916a695ec804034c22051bc2535b461793ae41476ba6c9cb90c8bac50f5627c1000066e0a6ef9d2df55309587a4f79922fe57289a547d3d492cfd7e71117cff3d0672f3a5c43f4ea9167442befa23a8d5749b11024eeff98cb73e4f90e3c94b84f9ebedb2e5fe20614c883814a62842faecd398f76d54ad3f6cf2dc0a19c0d6c8a33678200c1f354562bdc489637e399ad8b3ca039c01f92cd10f8d5f208c741d3086d8bea8de633f4103442bb8778486d516e8af20019d48a6ba72298f2b7bfb027f5c6c59499ffb4aded63af59eae5ed942ea2a1d5e866aa63a9564b5ed33013401e22f75f9df3e8bc9d4c135a37a49f3596527856f9e725582281d316f8ddfe2cefb5f240507f5abd7ed3388cae0344738effbd341c369d3da08832f8af488f4a8a8549c18cfd7a5c6279168267799f8673def75a4d4b360a2ccc332642e38eb294d2467159caca3a200fd48cd39d698ea2edbbd646f051b96efe72744f640692bbed2ddded8cf05ce89288a9bb30950f89b6277f3637fdb4e5a960227cfe100ee309168abb92925f38314f85bbd588571d92b5ab9946d26312ef80ae202c38f90eb9e67123083449aacfc3aee23c244adad3a22ab25c2ea8b739537badb793b90279020f47642a0c926b3f21a12e1bcad489fb43acb8a53ec3adab692850d4323e0a837f0935e8cb3bce0c975d768328b09e6ae6d41319dacf717fcc5c495357572711a744c581da8acc54695dd058b225c97970146099bccfd38d4bc4d8101d3e6a3c5c63c81d43b3c14b1d23fce7568d4f620d57359d244c563fd049f2522b7da60243d1f5ea9436cb36f4be581bbbd669ba9bceabb5ffdc7c7855e17bd734407f370ef90718a8d0e38ef926af5fb6873c71effcdb26d9bebac66f9945182b0af31f411ffea76735c4664d81ea7ef6b76956ce6b3de92ee8c9e5dfbe539f797f6566ef248c63f7bf26a82ef72f7fb1644fea971bd011f3102110b6426a848335166284a0f85e328adb9b4f03c994fd7dbaccd9bfcc0b96d16ff134e5e95d5ac3eaa426924f015bac3bce4c53ae7c92aab0072be317c32e6a9ce1aed5ba06f1cec89e53d3cdf4923e796def36e6a8c8853dd30e3a40bea66a45b3926aca640ce603bedbf14eb7f42feace283fdb97ad4ab3044b6bdc64c42f46b373211b2889f18c335d28338a8f6592e1e6110f51460255877b94f327f2714fc5f2f6a2010656e8b070fc833fc463e534de52605eca60b91bd157b09176d50e226b8e87b0dc0cab3541b0f133e661385eb99eb4fc54608cf93687d8ca25566b5707df74565b9e20e21c553494d0c584a6e888689ee055bec915982edb57956887049111dd536137a3c0eddba28fe64099c4776e54d632779d14dd9014e3507861583b653c348c207654a714ccf249f3d25c8b6c091a92d2ecc6714fa667d4d0aa59677706157d152cbb88f4983f5473800c9718d6fb92d782dbff562618321933c60c5c0221f887cd642261e09bc46459d7c8d68ac87da89e40ffe21baee1ce7d76f4d0a16fb73086bc567056eb8b652f63605d9f1ef7f70d5df28136eee143fcb148242c75ae61523c907d2655e1d99f2f34cd7cc93804d7f02635e291d6788a3a3039343755164524347aeab36202c5f57562f85d58140763128756c72a8337035ca48f76ebff2f9d02051b8f0d7a49c9b1da217df409b3ca5e1640f5bb8d3568d19af8feeb4f8d540f617a177abff73c1743492a3315ef266462a4f764d12c13142f90551ed4cb0cfd6d56a9a60eb374baec9715eecc0d7d0ca1f04349b1625a5ae7c5422cdec43d224c0b2aff6438f7c2b5fa8ec9cf24e016712f871b5cde40ed2efbc32ff07c25452710ae8b2d671f7c51500cc3e5cc05ccba06bc28d9295415e7977e272e7d553212f17eab957e4cac23b95d27ae255bfbc419d5683c35181a4bd94437e17f44e3f7664fa3ee727c57b93f7985639adf3a26479a2e3d54ad023398a736318f0bcace79d7410cc545e018312e9bf220798eba787bc1c0c0986269307a0bca9525c0f358818f11815ead7aa461e83ea8da332aea35aa33bf182213e39457c6408ae394f9bd4ae56265f0024113111257c75143efb55268f2fa82c541690dcca64beb85c32c4179faaf41f4cd5562c296b8030c4aff84a056f24ad398980f1c834731e70f94b3bb54b3e7b6007e91acd627bebbf514db88fe6cfe91356e51fd9eda82e6b9b0a272566b01e9fda23aca4aeceeafabd374326ae0e5ac8d13905fa438931577935f32ff20e26bf02e5a0186a47a65f3d4a4f14b40d5d3cb9f4fd710aa4bd5c0c54bdf199dfbfd533a63378a79e818c7b0de4e2081054aaf9efe9e463edec8b2dd46e59ab03a0cebd2c3e975b52e126e43b7f282a8b9521276e0aa0a2004d7eac1a4026f0973e8f57ddbeffa5ebc65d2e9681be259b5c33dcf8e2833fd620b2ad7d10dfb1f130a51ea049e374d7b8ac1946cc7297518b9b069cf72aca0fa86f0c00da0e5dcd9cafc6ceceb5743984f72a2ae621f2f515e124e9d32e124a14b08af20906c76a85c8a6453a248c750017f4f1a917f5c29fd3bfac1b4186ade696392b2fe552bb8e96c0c69be4b23702c93aac2795538acdf9c6c30b860e76cc5d552cce11a0c12231ff43377ce9e2f1108f6c5e217e10941bc7d8de778044886fac3f2e3e6a3762d77391abaad64e7898e45a200f8863f03110a174352fdf057cad23f83e02686c0785bfe7049354c40a51ce683ecb5b50c305e2c0e119aa67f72f6e5a37ca407ae73145339e11b90b61e5e2c3aa5bb3b915ec641340026f81036cded99ad5af6cc80a1ae07ebc1e34f33313b131a6ad95aaa0dddf1bc8047e2d4bb23edf48c0d8f9b2247f55fd49917453ce43bfb96bb9cee3cc93c0336342d37c9b7d51a85628f5c6599123ebfd204c1f3975c366017d00fd67bc9d816bc277a040b361117dfd60c378bb5562fb4b77ac931cab92cb3c3c5060a464b05da6d0695abc8326bd9c8773500d7944e450f8017b2cd9fc18098f69fddbe1d8580b06fc78fd8a1241cfbc1b34f927d4623a3ca56f7dbb488e8eb44b8d189e0dd0673c4d71e11104f0f7d9e8895856f6584b59eb0756e63f688e0e586281b4bce6f8414bb1db57b643ce29bee859f04491d0a77c77f0682033bf356825abe4ab38897a839276b11cb4f6bbf69afb45d6b20b509ab1ab57b30dedf9a5ffdabbcb2b001f910572b6647b0f328347c0db2c131e491f09f967e3b6ccc5b44236a12c474e672bdb0585d8d2481544e51cf4471bd1481bbd4523054b645747b3dfbbe49477735173b8eb1b2cca241cea98dd3ed795b2a79a17d99bc232fb967b19b0a354d1ed977ac8f2ea32167504ee845304c873ebdca5c50da56430b53e26a69230e9c991f0365d830006f35163aad874dfeccd5acbe6081876caa8770499d16cb09dc46064f77eed06b24fe6e84e686c7e10df846e4ac96c8169e3416c7e403db0c505a9ef1dce93c65b7a16bee19af56fb9365e9af4e0cb6911708bf88eba41866cb1c84a130aab47d817041509c5371a8d3ed6f6a2921926b8b2bb531925ba528642f65b4e087d7b9490dcd83734263948de2956950812c9613be14d5c1dd437d5af8e2b2b0f49fcc1dfeaa5693c4191451b656a3a32dfa1657985b08f9b9b8f3054e97a7f8f9b7afba504b020cc1ba141e7832251a4b64bffbe1321515956c606d09104f4248a7420ac2bbaa2c30141273d02612db88ee3fbfaf2478d05670bb92ffd166bce9413e857a84d91cfc583b0a0a806134805e7f52d627f3f8077eee23efbf71830337202de38d40bbe39651472121aca82fe4e2da618d5985c33232908dafb2888acb8e220dabb82452beb0e4b37f09447c85219b3e56a84ea15ad5ca2763b875c7029dd7f7f2318566f66621fd8e3ab7660eaf78e5e201ac3326c4c2e41cf9d92a8c770346dd7c249ac7e2624ab15eaf34c3ad88cdc0fcb4d844b6dd6b75976f8a7f7031dc09a57c6439e3d925a1147b9cd37bdf156ece56ff1433d2b98d79240ee491d2e62a46f139d37917569edc091825520144ebfa71c24866e2eb9ae96e65fea82cdd574f5dd2e2c8dc6099f483cce33f8d567c037cea1ed21222f6c1bcfb2935d3ba2c0e423bde6c08c971186dd18b59f600545d8deb58aa3ec13a88a85a3ba394593587a0f9d981c8bf836cc6cf6b2527bf187112111f66a26c3dcb5c5bb84fbea41b2ae94a083833912f37c99631d17fc8954b21576c63e83c6146b5d84ed5c9a6115ad85201e859e1571c3b24c66e33c2d26b792750008f1abe2c8a292e62a3a8b0e3c3ad6dcefc624452f1900151a9d1730759f1fa520172f6712a067b21c64c5cb5f3a792c781cee30e7bdfcfdefaf01ce92ed614f387b49f699e1f8f638b823bde9c07b07763d4c999aa611782388e594b314249ccfc9438c95629533e4b3bf1ea73976d403b1f7d2ede9ac05ac76a597bd0eeacd7faf2af024e2c71e8fa0f90a469f3138b3110ac8d24f63df4d9fb3c74a41476813a985c56e44f7977c81c84d5097c69ec9d73372a0cd18ea220054140352406e885ce748a45adf57eef8c91753354a2bfebcb568b7862304716fbb589ec267d17e282f34bbe5a58e434f4a365263eeffdffe42b8fda820319b3275d60a90f9176cb9f87922fa8238e44d07b290afe717c3c04782e5698870d3071851bae95b04f7df99375121d035dbd4ad92738dacfa4416ab23267894886c0cf3a467ab3178dba8c7a1e1e38a7dd579d0de4a71c3ac1cf1d12cbab998d9f6f29a5a3e1e1bb3bcd48fb65fcdb5724a24fdd910e7b93cce8ba76f95cce4d132d8767d2ab0a517c42842520846cb06e98d82e6549540db3b25a49dab507b5c266d4ba957a00e07ed5040f41b7f6bb501d255f8c6b70286be553f7e4b8c7b182ceaac149562ac322f7e0f81200a31cd4fa93f04fb3addc2537a379c5a242acfd4f710941c51c8d723617820e17d22825f6a8d78f730f91a0640cabb75f6ddec8de13584cdb3c84c0f919cf260f91c4841c7f643a9c2124f17c00f83774eae1d0fdc37acc73e314788a8af4adfc55ca5f6d9ad96360ede3f44f0f72a45942ecef95d617383fa45d1c80effb15c30c2f9588a94e7edfb6bcc0775666b7f619ab12e26d85d3a838edad1fea02a28d29a54504dc02771beee3358d269381967dab8f56e8ec1bd7eb7bfa84805bbdda08de595c75d12d9896c53c30e26eddb02b830d6cd4a94b6bd948da7b325bf4beff04109fdbd40d159c4284e70c57b8ba53ad2104409aa0db48084a4708512373a482777d26e0b5d54165cc1d8e957eb3d4b02a0d2dd2af5d22496963bea06495e1b4c9242aeff114a4283aa6c21f19dc7c3097c7c51753b0dcfc8fbb2973efd2e51f0446f5969a6d18c182ebedbea179c2fe79b3dd25526d441d6d86ec1c11587123c18989af3512f1380bc892d530032762b1ded4cac9e432ab69f0ff870c6a28711b40789fefa13cce5a17e320927297046bf7a01ce250fab39c1c9f6e86a88939c1c7eeaadddf75d277b154c9f51848e623d51009e5ec36978a29f6e0a546a9c17e57378e0eeb82db8c6030a6d05adee39752be0ae3cd1b47fc9d97797b488f975a28ad2608d3e1449a0d471ecca14b724f09ca20e21e2145c75222b285bd5272c11d83312267aad765fe2ec86d0394fe4beb9f2b58c63898cd15b6f469051ff2fc8209b7c5135e7dd4b6653243c1b4f07149fc4dbc42027afef88c4aa90fe0ccdf327f1742c79b0f60d4e0933d7d55458754332edaa4747c9f3d35d1526ab69f2a9a648ec1fb8478d95600fd8b296f1f921520d150fbc5e74ee7b6379bd2d83e0fe3354f44733534d2b8063d686ed538ff9a13da55f14ee3c8ff8bb5508e52eeb373ce1f0c174c86c3b37586be0c34d9de63e20b308e7ad38c4cc3437653b320826fdb929878ea0b68c98d3ae96253349d1985f463e27a7ed95491488174af241bbd5cc1dbafe0712e3cd5e1a49dc80f8abad09e765747075f906816425b3ef5590cbb0ee1fe92b50038710b05e7592fa9c046ced68eaa109fbd7aad1d6398dea32b2cef7ef4092b8d11e4c8bc3f4c70dad157e22608032bff4dd0b741b40c65dd59a2e023fda79877763968d0e5d9be8342b983f58c1ac22f754de5fe54ac580f30c991b733e4501d48088babfa52965ab741b797a65c77b46b96c0505334ce25f162d898e73ec639ca65b9f76c0d07af732feff38b034dcb489be67b4f143f0e480c2f66c4884390166bf6eaf5bde4fb0e32e890d6ff6feda5b33af527a57f9010b390474155fe6ba255a0313c229263203509ce7cd248a3c99b61af929c35b4c62fc80390ad35575b4aef7f3d1129d065c1f65af438883fa6c6a2999fb941d12946038e204bf67ef002e9703f005d740d5bc4bd5d54baed9b62bdbc7d81dd920d65e6a44c0d12e35021eacad026e52b3ddb48144f5b4036261369889622f0930859308df3610b0df27d5ea56ce8d37027b708212f95584bd64fdfcb1542bba26e6fa778a3b1608e970d39f940292051dba564cf4d345493aeabaabf262444c6abc738bfe259f52b0bdd833d760de5028b7e33997b9130da8864e99f9c0ef4a1372e819d171ff0d65b45a9def11665c5d2976fd303d2facbccfba0d898974ff874478432ea40c4c08aba6361fa36b38b52bcd41399f98a1de866fa9e7c5e337df9e6b3e2df3c68ef9d7f15e5942459174dabc9f2c832e3ae888d114e6f50d9e0cb856c0f5da74c061072448019ffb487e52b00e8455bf8e9d6a1bfe6c70cc6c6d948994caeb78d496045a46735422ebac35b3b9695dfb639a87065fbed0203820e9970d2c78afe0be7a936db0dfb8ef17a5bf0b89815d68810bc898624cf3774a055a94d39ff796e6987489dcab78236118046e3cac0a13c72a23e671da40524c6fa0e58c0825588a3bd6e02906ba48c30f117f6f8e5d837697012e17039b5efaecd16eba1006503eda6fbb80a46e033d0a2086ace43f53fc672aa600a4eb84799d46f4db490873f6e19155b4b808cf51b8040ec16067dd4f86a090e1a30cc9b3ad9682d37467f9b91b989baf182a55fb362c96fdd792195bc90ebd94480b7fc12b4338e5e729a72a4b098d5ddff9b12b289e002a40f947505bbb50b864ae07170ec720bbc305aacf335f5fe2daaf64bb5b15359720b084829d02171db7e6cef380d83b6303a7cc06ebe9c7dc385f075d1232d4e8b348d68fd7ce041915d8c850d60c62817a98eaa6f4219b8d3bbf74cb708abe579ad610206736b90b941d3425e80041c70e2833389da4988cf3c67bdbdacfa096f4aaf11dab6e66d4ce5bb5c47e93f4c18c89ba5d3c6f030dd9f5db54e0c81633316d2144461b2e758645f6b4ed86b2a150995d34525c99b1caf8ad4be7a3643ac110dc6daa88bd28f615ece774ec5466795cfb87991c595436916ff0e5f37fe5bf27e7f9857426088203e064ee1d9e41bc3de221fdec9abd99bab7e2e26aa6e246429228c8c30759257983e704ab97f05bd3994880bb3728908ca8ae79802b56f229d0c5110879f5769d47b885461425dbf7b4fc508c7cb9c096469dea66534c7b9d736eec54894b35a7c29071301cdde44e17bd84a644834847427e7259081bbdf5bc90d7c49dd2b39613387d47c796f4e925304dab14c309da6a16aff9bcd943c814583451ce1ab969907a71c61edbbcb1371da74ff4e320d45e6d7ea9e9960318c7b4a4626149696c61aea410521cff5629967536fb44bd785b8c96772e8ec0c20fc892495eb89130f19040be9bbc42fe98c7bc8cdb6075407aec20c97aa51cc1863e3bc7428390559d6ca6488191c1203190e4033d2fa0367ace59f813d509fb23bfe6adb203c2171dc584aade11a5b26ebe9a9b998061c82b766337b0234c08c6fc5f89a53d672d37a25b82f14c77eadc8da600b703c2e36ebf7d623db8aaa874f89ce7b2a3b11665947bfe604d6eb1e6aff1a4a0838a328882a0fe313021bc836a6f30f20bce6e14a47437fc4a2435aa39065aede713517ee90e6ef69c61074fa3de535fa372277b4945f0579ae4a340eaab67906410c8006ae1ce2bd2cd080cff7e8ce37dd45e3b834fb7f7e1e5d05bcfa1bdc808452275d54fb08d786980e9514aab29851b3eb96cc7fe77296f595166c8e3303ae1e2621264ce5ff52ddd54b2a93433670660b1815866a68cb539489df8fec4b1f3945911ac14861b644ce7655ffe3f6fe1c45e1447a587cb44fbd3b83bb3afaf329baaf979d0c2d7f12a626487576c0dc1e4bcdbc940bd5c35f00cb648da6dab4fd693a682d617554229481361d47481464dc99e9d9b1b0270637c14046f77583d805bb9e4dd6e3eb4c1abd23495c87553e6b75895368b880420f2dad909d827728d8a40cbe11e4c5473dd217b3bfef4d1383b233edc448f0bb57ce2d2b81bd09c55f6d0b8cc2e9db1cddc3d92083aeffed6ee0b7deb7e031fd734cb7109fe0368a62216b6239a01d4aacd188d2462539ff36b1135194b796f06c4f5f7fac8a7cd9cb300b418b469d4e3cebb8df782fcd0aaaa753d2d76bddf0b07ac681f477bb02c48ba0f852c54f45357be4e94dc89147bf5101f5dec6b112f5d6eac89ce02d3b8e69fa13c063bc8c6b5464f26f0e5542ae7246ac425895e3cf300fc3c84dddad58f55e06ca8df5fa3aa5146de6b4d8dd2bc54536dd7536d40bb5863a42939fdf772d99c4a80cd40c102869965d03ac9c4c884e25a2042b8d3ea2a04e526c9fb0f09af70e305d43b1954338ab93407adb729d58fb7190a19acc548e4299b6174a38cdb5615104a650346230dade6da00ff87a334e5aa095d28e35eedc6fe5ed0feb5cbc5ed4d039d0a2af65d3c11f6d3901b296fad100c02e486def3dc0d8ab6553714fbc7df80f547317f319e00f03272e6c96eb3d38a0acb8220eb7b728d6b2c4223d276dd51fa132d06de21ae5342be4829db6dbbd50d8bb3972c42b692b5ff4a3d2c298bf8ca65be92bd00feabf41cb83a74143faea5a3397a39f79f1b2727baac0c3e1c25343951e0a43deec3f0d848f08f8a847d3fb37974d23be5d3e23874f7b3c4bb7fd66ad5ddbb3b2f1b9fb062869f5d5177b9c95b0bc5f18f7b1613beb9d5d61702b81bdcec2ff9037ac1134f2f7ce25005002574df3c8523ae08f129bdc090dafe9ac3eabce32cd8ad14bce290db22060890aca433e8ae243501421db483fd1144cab80b73fb20887448f536139f16ab50acdbf4e9caf679a512bda9675a1abcfcbf300d67fc61d0ad2c607c61f589beb719611b52d64b3f01c7cb64b3ad15be3de5f2a5f4222fbcd39eabb70772006e6b24f6e090cb0f43e4436dc9f303abbbeed5c4e25b7dce6c5d9f24594f0f69df29a4998f8effbcdc1d883ac7f7b28efcd2ce8874ed7ed29cd7d1e48290d631ff9ba12d98a36c32c17df8abd352f0b743c9b7886bf8a4b2278b29d96384daf2e4975640fecb44e76e45d26046187a933d98e0c18cd7fa67bd9a6a5291a70a99aeb3ef52360b2b5163e7925144d4b5f507fd2689621b6244a249038bf488e931520327c92fa3d3dc3fe5137d75c390b86221bd4789fc08c6a38030b5c27def71bbf17f7781a6ef48a2f19473fa070e545f2b7f8deb8eca9e209cca80931604e8101d11d4b2bc11d117ae261482ff8953375750e036da6203aec66c5840072c03dc8b7e58a7a138a7cb23a5fe692c7b5d8ca652e4ef6d42fbeb839c03ff7b112e3f5d069b1612bc6e53f0d9069edcdd0547126d231c74932c243945e247277af0faec6724af3c7653dcee027d686d314ea23fada30fb780e38785915b2bf3d43e10d2efbe0e0e7acec8140331fd73065402de325c8d223a155e522d56859d350e5162be2909fc56685cfc63a8510d93a936947103199d9917d7e7d1750016462773aae358ea1500ddb9598206467e56d4449351c170b49116225899259a92d4f2cd57a233c94e897ed3fc587e80d4582fdc3528f61f4e7cb9cd38d3c7d5ccc3bc702dd4dcd4f0866473be668a2555d5f9b06e97acd00e9dd8da6099ed3b86b7ce2ef1f302f92888a3434b2cfbc7ca569a34229ef7d094a359a97efa862f16d69a1bda0012468708e44ab3998681d145b028105211eeb7150c9b5e636e6bbe25d8a9a3c2981869a61faf94b4d3bcac31bc3a54dcff4d13786075f17f63c5f9fbfa2f39d81c07161c27b8d22ececfefad11f6afd91bc010d91e84bbf583ce7b33fc4fe893f64f2c8cdc52438f28cb9d921db7cce3f10a3df3a4577a3a1c2ae3cb2e89bbad2123c90cc87a9667aa984a5fdf9b65a0030c094645d940df2792ed6aceabcc67fe75a815544104536373b8b9b13dd96e6fdeb04ddd256ff5412ad312a12df809428cebc669b4caaabac54dd492c16cf1a34a7c49cb1968928a4b8d73654c1a75b6c722fb14b873d1a43749383cb95bbe868def78aef147d63131fc4712f596127fa43fc4275e466b20dfe7072ebd671d3cee34f5e45b56f9c9dd78988aac6fe1d29a435698cb94fe9d4418eef9a7af5d690e97a10f3b3bc222eafc17e687cc8203350795cdd75309c22db861a847ba1e784da6ae838fb46dd6688369d11673c1ce671102f030079fb25c305398803d2eae552381026acde003e139327e177212b6b8dfdec0e3ae2f4618b6b11fb85051e19f5d2aba5f9635bdff57a282ed1c0a5b5634339f85b637e701c80752bb02c94e7f060d194e6dff3f6c8bd307be85da83de349c900b81d7e0c197dd6ad02897b69d000b2a4a12d6c6013a6620d194c7677ead8fe85b1c2c5f93f50a1b7052e7e7c1e765ed54072b896054e6b54b4b0d31ec41fbcf187ad68fb4f0149099c088c529921fadb6fd899797a50935e7d4b024b5ce44eea2a00751e7cf0c0f4fda05bd5cb3312ac9ee1ccda58ffb04141635358bcea67e93b182205848d31ce300bb6d59f778579723839009338d58cffe11e21d9bdb8dca068c59308e6dcf986b516a30d49958638605a5729553ceddf1953bc5c41bed6b7f57bb37bfce5d9022ab54cbcb9e2a3db4c1f4a59b1e2cd0572f701d68be65c707c6c95fd1eaef2b12a8d210a39fd89c8de0f2477f6248fdb7cf2431ff1dad8b67c9e05d83a01c6a4398340c6eddeb3efb2be170f57f78bccfc8fad6c1cee94c3e92c3c81dc49b90806dc2396933d82f8db7e66f0bcd1d0a3c72b21a5c225c5f619d873b25365102af1dd84945ab37bbdcfbe6796f84e9d689d483d4fe26cda526c74a13f1cc1c8eca025e60beefa472f584988dc8979def18b3481c26deec52117a613d1bd182964840710db0a8a578857503d66b4ee0352ef1b06d534005f544919b8f1de7760b7f8585718ded0c17ed8863e7d641f9e2f1696a65eec394bddb0cadb2a93dce62778cefe70e3cae9569b53c66973a4ae747a3fdd4b5d2802360ad3dd0bdaae426826a07cdf3a65e0821873be1131e5f596f1b2cfd09cc8586fb375063c89b73c07d26986f5d117d2c570a5dfa6c6d7b5c6ad9e2fdbf1e35b7eae71bba52b95c5a3aaea437ca960db6fd01a8ea2a5ac98819d57ab25f7d24da6913977ab07333f575d2d771b03dc29c9e41f261cf465d3f9c94ef890c2e563ee9bb2b58ecf32a8f0053a6b8480d2fa63d908c5bd69616264f0b3a258bd97f9c44e843315a4dce21f454de3116876762f9003d08ed47ad280fa48144d706be1beb3ce4b3695a977a44198650ca599f79afe4fd6d90ad9595f418fec30086a1019141e910b43a94bb24c9ab9b2fc7eb700a8598317da80c0bfbf25a05062a8b1f67e98b8c13cae09891f69412a9e6bbe957d71756aa0b94b8eb1b801e7051202fe2a759b60dc7a76b6be2994a3876b6c401b55dc45695de543ad514cd52946d5a531f7f5f6124047a5187d2dda34b8bb1aa1896e041a47cece43d05f7a160ae5aba7ffd382489cc9071b38977bc586cb12b295d313a482d36bf56aa635da0741fd207afb8fd221fd1b92518f2787fcf3c05407efa13e32adbf0e9ac846d24b5163f3e77c46283ce21ada161117d3a1aceda04da55b3ab09e94cc9a8ea899f3798f4aa7d50830cda471c30ffb50b295404bca82dd8817a4034a94585bba1eb01ec9bddca9ef3a5f23f4243c30b900fd8fc7dbde1b57ad55fb5ecdb4954074150a6186a4c0eb7f4fc0ff9c3eb2dab44656a4780520be9b1e2bcc71c4cc3f07f0e786fed011ef187ba7b36118fe1f58233825313629863baa25609b1fab432d98708b9ab414ac77309c3bad23595da8e863d607a1955adeb0ae3ef9bfc32cb7bac8ee9ec6e25ef5aa23a5534f3dacf2051fa7bd1ea9978524a580a990b0fca36a912086a0d46aff56046a4d79fe3a1c5ebe359f66989dcc92f16e31278b15fd69d909fd928b2e19cc44a37279c2e4f718945e115c949d3f6d202c1708079d9aa7a91a7a0781b290eec3656c9b2506dbd7a9af852cf88136ed6f61c6f57508d00c7b5fad818d581a7890ef5a3a4e1a4cf5f390829506a8eacefa12c7511fd6e7ad295afbae618a4ae7eec67ce43fb32fd946b7cf7f938e6e3956a97af87ba73d6f653203822e9de726bc2f43989cf5aa379935e61e45dc72a1d78aa885137721723dbf3a98aa7a12ed8f36b7a17e1dbba2ddd392d2e499002384a674ca13d4d1bb4b5f7accdc0cc9b154245f026ecff8e38759da294d51673dafe83b01cd69412f523f59e01e1d17185f962faec9661020302dc8a27edc64e4826eb9c007e0cd8089dcae69f657c5e7028bc0841a961d4c0355375a6f3d167b7643820ea6a70413f29c52e5acbc57b1dce376dc74e447618f1584e3b1febe9dc3ed03a332cea762cba654c515f08220baf319720dbdfebf92401ca9f39cf3aabf3250d8eb70b3b193e7d3eee31e499da2e28b70a7708170caf1a8643ac4718b48c17a6b0e94f98d5e105d886ed80b15e969a52440ebb585f10d0fa9a0c640d315eaf0fd77c2aa0bf26fc8712f5167d4b9d6fd7ded6183b894dcf5b4d5770d6b5a0d52d4b9618cb32873b11cdf2c92ff802009150991ccde3406a027d064c590d4b4e1f4dd0c3fe171bf22c41820ef19c118c3d7cfecc77bbd1a2f38e49d558826b5df5ad742975b494b45cc754b923d8e39911279cb78219209be969820f3415a0a9c90aaf2202da599ac708ca19d7b10fad1e821fbbc55e875bbf55f014672cadeacc405676103021dd1ff2052974cf12a85468081655a6825502488b6d41863d12c2efbe417aca1a04453866e2ba552dbd5cca66be14c991a3fea2e0a7092cf4d40cd481ce63dcd8bcf9ebaace1cf75f4232025b69895533a444f1aeb85ea4372185d5b35999442a14847da3af5f1316be0f487ecafbc3a2865eaef1de184cc8680f2b3955c5ce841b02b04c2f99522e09789c505804f252dc188249b3ba7dea542f600c51d3404c11d46400bc45d068f6b58857593eb391bfa47f554de2c79c44ca15c359695b73d25a01c73fdbcfc86ea506616fc88b5946c57298500318f0e73f3af31db39f1ef684d58f54266441f9aa5809e28eaacc35f3c74ff501c72b7630d37231d5023a7467e502155e27d205e51edea5dc83f806ab2f9a890c8f0640678048e13eba47eab4dbecba09bde64551e384e6e42d07f49ee53cce7200e40979e82b4baf7c025a406c81fe8950dc6c56c29f79cdf5c6c551ce3ee51d81499e4559bd6b55af43dfa717e5fe3c1e9e7c1901affad603c631a82eafd26e6c7475db36a12329da5faab4c1cb26ed98a37bdd5c2cdd40ca22951fb481f79a0096f1b3eba722ede2503628551c1a0b17e1e552df9df8b3152217b025dff2e7d495f6d610ff3b9a26ac53cafaa90d5ed0369a7a2bd728800edecd710523a3fc2d09966c982d24d3a2858a1cc1a399133b750a38ff48d2397b9611bb728ac5802dbae76ead8b4ccee2bc79b168db9e028ac6117fbe3c94229141159480c76e256f1d776971c54fb0f8c399f8e2256d37926bc685285167cde9f0b459f39af5c4c46b7734e54106d4aed8997a5274ccfaeb7ad40e3ccdba0365858c4837b0bd78c6006c5ffa4455825abed324a2cf2d0cae20735bf1dd65ef5a1e6f0b03c5c7cc4e98fed3e7cd6dd70c989bfc509467039ae86c1d34109e9fefd900b211f64ded5cb5471f3cb0724297e1d482c70340f676eb6cf9055fe20f688ebb2b7d5e5c629d0ac94ef4165a874c714683481840104d8af5fe0dfd690bd34a51802a5581c195a871eac62469d07c975bfb3eccf3b064d5b1e904c70f439f4b1d38474685f0040974569bd7b10b39021308f74fd67763b8a7218c7a9d25d458d172005ffa7cd67dbdd8c49740094d1acfc0f3b39eeb7a16bd8a14fefadc3c828c5921b94acf2ebf9432ca298e40c7b14b95a6e7ac5e84d8fadf4defa2688162977ae7d34b7d89352217c064694ef2b1b06cd20fa163e180b19e20c081a4c1df6613c1c5ef4eaf9fd93d13b26ae4035378bc10121ad7681f49440b45ef206b6b1be6d146ba66df729646cb65be2e056515bc185b2b573830b8d183626e1336f6877dce032fbba75bb5c0b049c076f55bb43ae62dc3c814c9cabb0f2c03efe19bd0248748e35408ed22fe2a2bfe3e3232af8b6ff8303b9cbb44de1b411219e6728e7f486558f7d2cdc5ec6b482c52dfa7435f1ae2bf32a4b78277eb0dda7b15e1dca1b46e066fc0128d61b3f4a7de70f3a3f4584020ae8a5320fcb25c4b99011d8ddf5b3ee354788908d6db865398b8fef88ab8dc1ba5038b09ac77279d99d032dab3b91bf945a5b1b180c0e0b15cbe143b69cbda1db8b389c6112ce21bd80589213942a9214888b36c94e192d171ac84ced0b6cd3363fc07a5db60cf300f4646ac255ccc8775b0c72ff414b79b899fa3a3635975e9b0d90f571838e52d63b42fec0ab5c306648498b845bca137dedce1184f65013510da95241ca574bc901b8ad3e4bc7d08388cb20215cdf1ddc9c470712f397c9d466f008575f33f552ee8d20d6b84ddfc6c76df7f71ffa5f21d30dfbadc37804f73bcb80c82f1cd87db4207006aacde8e449117bb11968ed9194a0506e6782cdebd614cdbee428c1ca0680af5265bcd2b609c20d7bd0c94c07aa654e76d866d875caf34b6cc3ff555313c795f9f84d98833702090d328ac64405afb0437b6d34b5e91ecce4bb5328118883bd3e723e041c2cf35dca20c483b71cb6bf9929d96005a34427a2008a5139285844f0b174d75695b28f8f0a99a697db3efcd70ebf19f3d2d03038d1b892c97b6652105f3e74e37e8a3370ca521dc7aab1187a7fde6f69a50915dac4deb3cdfd09a44fbd492bc6c10eef633cb146d9f32e9f1df973cb2155d7bd9140240f3fdd310a1590526f68470207a1bf15d74ba2046dc3e03c93affeadbd0d978f6c77d5b59c04f4e1e1d0a80c6dae427aa619db10c60922df8de8b90bf1b2fe32966ea64a5f6cf223248e63136887012c3017bc482fa3e6930c9001e18a8e4a0bbf5b1f02c1d65124d19093112469da464c71fc344181200cda456514e948aa45c1c54ce8e1582bb49f22b63823b2b0cfcdcd180294a82af027b71ee08ab99edbfb8b971ba4bd44de68c8193a7d70f001dff2d948193a285589d1c13b962eb1b1df6c44021e92e96aa1249b27eee72b01bc3954e600f60ff3279ef35b4534b4766db3eac3fdd8f48337a5b6cbf7bbf7767a59b9f523f34cdfc99e7a3c3abca454d0bb0cfa7433207c8e2949749abdf90805b63686db3b0b7d501a4c4b37bf5632feb8efbe071794616b55a852e57247acc2f7e67c61a06fda7be35680c738f027a730e04f5952ae993fb74ace8d5dfc6d120f2314a12fdb052a1cbc81c0256c4447ed27c21e3cc4559750a2dd232ea55395a8c5273d5bb2bd24e4c9582eeae2b3e805315fb35b24057b8337190a4367abae0fb2e011ac9e19b935e3a3312e60a984acf27ba9130409f89f12ccffa3ee1c033de17c439c360d0b06ecab47ddd163af67de47029da1d8d8b6a4e1f47147331ac09b53e6e97ed7bde8eb0bf6f08a4e8fced617416ddb7344cbcea3c3f9772a85f5cb17a672519138b85e992a2b54011ac7d80e211bfc1291cb24f6dd98906426c78481a3f1ab832d632f204fd1b5fc119b359d29f89870d454f0558cf3dffc358f024124542888065ed508eb2102014763827a45a438915210b3a406cf9147b9567a27f9505e4f624f73526610b124916aa77656ab97883cacf120529887c9a91f49de5b53a0e76383a99f6e9105f5aae4c931a6fdfb9fc155299c460c3b4ac331455cec54ff5bf9b06c5fa3418cd8297aad31ea2da53920027c6d140a13123968abdd35d9932e1571f0a65a78f427b2587546ff396fa37d98edb916f63a9393fdab060ee3535cda5c813018ccb23fe2ad5661de1db9bdedd5c8b57729c9c3de34e32d3567231320220adad9699f8e929e50027d87f1cbd18bcbf5992964bca7ef748e725024480faf1ad81e95cf567d0b1eedf80275e00a053e6e1d5f4377af3772c50268623e53e66b0d9db31b7ddcf247b015c275429bfba71d607002579b870ab5810bad90b22595ff731a04e5afe1f88c7e726f1fd358625d8e948822aea73f020b80a11150b70ee58d84137c89dd0e09563ae4805659bd890f76f060a2139a66a481a608a98096bacb2571a5218c077584027ef29ca9503961e9ef2da2710cbe96e940d3e0262a9c73000c18717617835a935d583ca55a11200a9d997ad9d7cbda4196a181738aa8dc261348d517a768b1fc12a73525a864c8b6f904dc2d168e4b9ab8afa51da4a9ddc399b50ebde18fffe02f93af67651f9aa2cbd0ef3b94da8e5a48ec5da450d8241c03582bfec683dc3c9412ba7c92007a409bacd97dc7845ec6d41282f1eb1022b176a28b0449d3b33731072b33280bb391d5158ade3a92945aa5d05559f434703745aa15ef5065855af93851019dd028449b53f36bdec1e91cdf53f45ed4b9ed60f63d4ad752edc0f8fd3772018870596b03dc233140d5738469bd4d0e941d7a752e515a1c0629651b47b9165efe30086aada8f75ef5f7c28e838993fefbc833b7c54e0665d370036ed670df4233f1fd0241e176c302ddc32b5309b325159786d16d58fc57424b3e8afb4124322812bb07c3725a94c07893a4baefaa2f1f7e78a214aabcadd5d9051914ea25c8d6626f8b6d8c52875dfffc9d5902531ff8a7acf19c7d91850a581136d0fae45b4883a0a8b3e346193dd36cc3511dc3083a120e4a9a2b18e2e17ff98856d12ed1561a27b5c6283aaade0a64d43e80cd570884183486f165af541760fcb004c5390bbd5a8ee517bfa9d11066e2d82b2211ab65f36d1013d86bb01b6ace1f794748bcf528205d1acf6f28ffd77db840b363d2a3cc3d58c488873f958b721dcf64c1a4c6f735493262804bcab6bc93960d8d7c58bac0041b9d685bbe0b783cae3ebfb86907cfd0fa86dec53901d8f697328dc6d95fb759d9176f668699550407aaf4bf7446bb4e300e00fa8b402d9f553f6e968075ea76be7b6a39327b484c615e3f311d3f5d1152c3ff64090d69b21e1065c7d633e3828c89fcf9fb290321067a0a556408fdb9355d2ca065b04422a0d75ea0c99fd83a5155fcb71584595284580ca934546d5a3bd799dca7182e9b178d42cca86a50ec81b60c499f22611e0a87482291c1bd187a095a51e43c236e2f78e4d9f4ea13d593f557e534f68e35e7c5b96b09ec6e438e4001d9c029bad7e7d4c4f2edd5ba7f2d85ce7f2d58ce56e3348d04e8152b13947824b8fc53e17799f13c973a314b3c61e7b56c51961eef56f9c21cfaaf1046373fa9a4b2d5bdaa1af56876c1c81b795efabecfa2a18ab3d45874342ebced4b25329fb4f63c094300c760e8175754f95993815d024137f2f19b2603e51938c1d511d7e92a1ba66db159cbdd28f20b7af0c047e81365a17c4fe716810b4e7435155a5c5b20b1cb754a75984fb14ec24e9f26d2cd170524c6085badb8c3b8a2258c625b78b43b92027053c2ae31055f17978fa7fe36b940dd0cb67c2011ee1c96471a616d01d5d2c9dff7224c3ec8be951f457f7579afac09110849c448c770d1daf060f628e41a0b853f43fcff865fc12da0bd3dfd7e3c62eab2c8a39ce70fb3ea82efa5872b4c1612db4baf8323c975d54ec8e815fe262ff7cc11ff6976a41317675480af6d4438e1fc7fe4d226b6616d50dfaaa969b6ac229fbbf22d75491415bf11820a806f5e85aa4bda4885e2628909de0338d121daa7897deec492c81b6c269b253a210236ce5f92dbab790f3b159c69f7052e4019f68d24c80bf536b7756a7a750b404f87ff17e44a464c6383a1660b50cd2d2f9c8cc6243899de140604732b420c896b078f8bfe971e731f986014d8e5665dda7dae8dcf4624edbcf50a839eb0e31f3ddf4104cdab643897598d9086a3c55376b5d9527b1827a2ed971bb8b7313965aec4f807b38bd0c6c378553a983c813ac56b19094f0bc54509e978e1e42781ac5b5f5a2bb1b184ba344619d1ee903bc7505d15826a505a92af20ed8bce14b6b9cab3467fe76c301c8db892379daa83bd61e803254f424062037132ae1b08ab010dd19de598178c8fe9ee05a05422ba7cfe05aa1e1ae0a139d7cb4d42dc0c9df91f2fbf89ca7d02169cb6cbee23a16aa580cebc0cc655aa54c515913dbc45ca4f2ff7c06aa75d74b351660f5fb0fd420fea54fde74fe73fe5c7b863ee6d99246ccbf7e993a55112c2c771f37195e4471fbe617eaaddcddc8a1794c58d46b85fad4cd44d6fdc3f5f3f76b32ba3b6460fd27675389bec220d87d61194b7f00becd3f278a9e65f2da229d69b618c57f7c1df877b5b44b624bc0361785d43f67cc838ec6a3d394e274a3e05a9f3897be00d9141ef7b6406d0708fbeb63e2785f6505be85ceefbf4fa18b78785d00eba6d4defe81264c2d0d331a546a766d03528989bcd99a9a309f0b98d18e9ac21b2a136022d27a569b10557691285ea139de6d3b8325f157a939709b592307a3cb6f11958ce51de1e14f795fd57b67da993dffb110ba07c18922eafd8e56f2eae63f41c89075d7b50311907f3cb1dd13abb60c213a0c1dbed8a0822cf96ba6974e6943c63c39aa62f5b464b4c9250a07a14049a524b930165f0afcec9bd7fb46dfa4940fd770ac6f48f65a48e52b1d3f280fee052a5bdddd91c682e373494ad01dfb300e1a011fbd6110c4e2bb75d671ceb5458aed15699cfef9f605cb2e386ea017ed17e848b5ee3f8b99fcd0816da7aa1e7c50e7ce9c36bb1742bcea9795455f8585df02572be185be4ab35036fc2bb00ad2be982d48b28dcea9fe8ace75e7f042963d9b773e104556a5063407b19b2762c36855cd359cc3c90f6521abedaad2a193b4225de8a0619d5aa22c83c3b2c9be2c8bfe6956e44cfe86603c99ca6f2795bc9e4f0419ad807e5bbb23e5b4a970791c2e98fd7c9d835d859a5d783b9d8f64ce3fdf76e398098d4d686ac121a60ced1564d7d68d49a99cb51869f6b77bd4a4774c6001d0acde1c30429518365eedd8ac8ee987b10162cecbd8cf15259a82d01041d6e1a0b1b3b06af458fc9eb23e7e8232bd4f1bc360333b53a9cb73c73d806b9e2f82ce69837195aea738cafa8f202bbff1795cfd927ab57a84eb0bdf5d45fdb29adbfad45b060086c6dc8f8da475099e2e083a542d9fe1f715e99f67d7246c39e99ad3313eae93db26cbc8bab637bc61316e961912f6b60db022e87541158498d77af836f6f000e968e6adb406f8329a2ed607fa6f0d728319df0447072f2cc544bbcf3a42994810dd37e4e947da362353700c7b488cec030ee9d9169e89d1e9a34b6a29a880e9474b7d5527550c9787180a5f489974e0c7c746d3759ed889573ecd60a3cc6b79ced182841ba7196b574bb9320b6f5448cf2ad4634901cba9540f8994000dcf24da3731a5d5ec28f20dad0ac430f96be5f3550f527c1a06b1663e783dc9ccd8435836f41fe964b7e1dccfd804cf606688ef5c4b883d34b8e65353898aa4f8e86d8fbc087fa713811a2f772ac7db9602e41ad631d67f4fcee73c1755223c0174ba0b5dd5b783f5861fe106808f610cf9ee9ec6643534822e4ba23ec94d0774ce59abcf647ad91fc094f70191453a47c15121c326738a68e2f46799e1b281da8bea2014b58e69cdd0e53da52d39fd8ee408c07bc5398f18755bca1e181e0171ab661e06e3b91e9288c4e15edc7fae002f69c9cb1bf24039da128d97c10dd32454c65f4285ef5b49fa5d93e3ab4a2138bd728c154c5b569fd71cca329eda2ee1b400a1f9dd25a302c4d39b07c8e5d8b360f8a55863c44ff9db954c746eb067ec071fa4037666e47504aea2667f14c655ab0dee5ed3c8c35e874f67039bdb39dce2d1e08148ac0e0f912c7c50ab3954aa5d4f076ab83af6abff776ce4044a33062c81db253a3eda4e9ce4bb813e7eba7d89bb57ba0bbbe08d8b9d4375917a5b784b18c97de6704720877bd826fa22c1f1797e45b00c3c424a5fb4eaf898c748bfd909c612e48edcb2000468215b20887d39e7e20e6d6c9caa7bc02aac867fcbeea71c397fea07183a1128d1eb55ecbf0687641ba24dca02de874e98ff1f7d642123205ec5199f21ad40b293e9626229c5262f9c42f2c5969db1bdd6b45e32ed316c78fb843b69a3316f29e30141d2791e709a7b6930bc75143746bf2fb0ad330ea1b421282fe0aaae77ab83c433c960833abc7542b0ed155f6376bbf1a1eaaa20338518eeb4b718c044e6423178becc7079d8fa77e5fea42c47bb62ef4dc46d2065bdb2f628a6e15b0e1b1c6601a0b29d045c1ed500541ba5fb258509c692301312df9d165b04e9c6b396fa1e64332a8e19c4b5b52d0c1cfc8fdb0747ce8f5971aeac6553bdd0189c2903d46d76a4899efbc2090647aa93460bd6f0a5d31ccf486dde9cd55aafc3ace173e32275487f3530680f6782fb472fbd8893088f601bb2a71e4bf2d032259e8be160b157152cb9033294f24e4cac8924d898b7f306c3c9906fa31097cab942859aec10ad252c81881777f9bf9537224703aaa41efca6cfeb55b98195484246798f497c35543b96b792d2b24e972ff7e8e6382cdba507607dcd13a7cceb67bc3baa6603a26897925203d2155c5523e0af1f8ddec036f7364be670a0aaa5316d6ee79184ebd65c9c978c9edf77708c003de2da257f4c1e531766cc93ed2c07777a4d4450decadfde1953b9b0f9e24d0602ccbbcc88785828aad50e02dc5872f73a901ee574d22d411493e84590f9f7aa32cb513a3cfbe16991e32548e38b014a47098b726f4aac0ca8307328222724c98df8068286bdf6226f39f6f5b0dc229f9a7e61dd005b5d25581a9ba06ba575d708980da66c0c4707916468d5cb2d89b15bbfdee03117814d205d37170d21beb0e78806bc39dcc94ed952733f60574c527ca91b3f5a5e2ef440daab50418402975b4d44a6a90292aa9d277aec877d96e9f46ddc68b142f8ebd17573fed8e30f95e6db5588a7068fdd3aa1214dd46f3cbd6b9ca66b6bc7caefc84adb3325f0e5ff5fe3f2b0a6734d5723aa9ecae5580f3f7c3152e66c47c9d180c82de4508eb1f7de06fca68d9000edcbc37a3b524de22127dc5e5118a53c9fa4d048c5eb539dda5de5a5d4b418841f282bbd84b22a0fb6fa0795e0fff1c23a5369e7ba6d8c4368d29da0abd719270b6f7b9f8b943b092056178a830223ca5468c74cafac8c7202d1df888a722956e78636195187116a13c14bbd75b3e7cd6fd085f1d5dc461eb896c5814733cc4046109b2bb9a16a19fed6db1f277c59bc4a2712e1492c0486cb424e15ec6d1b55a04e2fe1971a8c3464be5029a2fe83850fec712c76deb4903352bf45f515f77f398f3019499cb51221cb71d7d8e9d7e044fe3639b1e683a1286458ce56c58e87d5ddbabb14040c46dde24dc0cb534ecb528fedbe977e89f9ded9c2b031c224812d9d96a2b14d910a4baedf508b8edec3804aad7fead3e5b2ef93fba1b8386eeeef816bbfbaf6ec51f92ab8c963df0341f7f41d71db9de72257c431ec8eda2c87984c052526e64bbe1c0236f6d44430108dd9231648bbf44e4531c58f65bb5c57bf9195989e0cf39a6c5e9d62f441927dc757f339c47d79e7d9f852fddb1f43cd2ac5e4247a016bfb3ebaab91c4fafffee4cde7c74db290817c2ca8cd355d65856b119e89ba909471a0c2347cad595ea57c48aadfc4e22f52f45bc376539112ad84a1aab185d0df0352fca31ec17a1258b8416b33450b1764053e5d5048f839b2ef413675e455f87ec95f694e2c9038d126d09a39a842a7e9e8295380c6ac783da8d5201e672350af2ff6c9f01a2a44f5220ff6d274ed658bc006d68d9e02a78847490d37aab953323667510777ef9bf76c31f4719140594df701c5946a0a90a88f35e4ec99f29bcd8877a1e0388564ab0bff912bce5483e2da204a0303c8221823eb9859e2cda09f9b907c5178017c714d29ac69f39559c68a3547877a0dc6daa7bb65974dbd56d5cf9af6998b6db86a05fbf3beeb05384bc13bfb609b654730fd6aa6d449186223513183b6cf7dafe150d2ffae76469a5f42282cf02f87bb58fcafe797113e2870b50979164d93669d553701f859561ad0955fd16eda99a0426a11f8f46a11d5ffb224b29bc71728dd0356a9744c5eb42946e76f04cda4c71fb55e74d3862017063b1154764d32b1db04cd50f9458317deef3327b6f2b4ebcbbdaffca1800ac10ac4a6ebfbe2a8c87a1463081af204405fe92926153f49eb67eb305afc96bc35e49ae6fddefcef92f94d56415963f2a0e1c11796726c5c3b721a34d3f79844ad6bb268715e8017a26879b35e545523a0d065e8dfb9b13e072deafe199aba29e1bb7a69e3cf242ec7a2fcfed5024a7b3763ddf8766f9e5407ee1ec9d7168dd5aaa5060e4111b7180e78f308634a3123e88c28558f2f64428ab0e96ef1140117d6fbc5879cf538eee2593b5f164108e1f3cbf1d86e31e1ef0008b832cdad604db95949a90d380c038f619c0014cc77f78f95c319041fdfabebfe8499ac903c0e23cd19b6bde8daf0a733c63ff81af5a73c34a78eb892428bbf2e58c915c925f0066f7e71d90577c93826427d04931a82f3fec52c3f9454632c3af308e2b6139f1423d9ab7f5bb70dfaa5671cc8a07f06a584754edc53d2a59e295e04a29ec3328f4fbd2fa42025e1bc8fdab3cac2efea3094fe13119e76fbda2b537cab6541710999071c28d157c703c815679e4905a4b3185f217edbf6ba62adb3f5b3a6a7e351bd4f6db22eced0f01e31f4c7198f35eabdaf92f1a9b601199d09b54bede125c98faf180014731cfd2ef70e99d8d8b93fb2999bbfaba3c03c9107a60c0b87fbcf31993569f11635e11b324ad7b47d7196c7f7ac5e1fe93b23aecca1d148e37d71829ddf8d03149d0d72fe27c66f68ba724ccd311a067253620f24d91b3702784a99e3111215e09bfc72b88347bcab8264fc133dc0653fd183d029752885b865e1f75f9277cca75bf0f47fdf065a595dfb9ae93b286efe29f24d0ee5407c79567ea6c38bef500d386dd00e3ee4fb190cf0530fd2043bb5dfd93ade5e4305b8fde133a5e493480f6c5d50a0793a161bb3d5e0aa341c0ccbe9f8ce4613a0119469f11807e5cbfc9b65a6a8a22f1279f82e2a5dc31a0412f68baf756b409f789af82d5f65d45f173cf2238d59ccffd6a19050359692eaad478905b1e45bf3ebb6ec41e1dece8458d5093cd52470b5d4560a064428cdfbcd32968f2f936ef0586a5d601d65f8f913722193a49c3c450a225216bb6acdc87e3e066bd16c2e2692e033b88fd1d2583194f7eec623d806d7059b88fd060465ad7f0fcdf7ad849723c37ff2e6acdde3743045212a8dac10633ccbfe33de80d66d45b503aaf864780f48221ec56d146ba6c8f3fd5bbc3fbd19bbbb6265783d854823eb675fa9d997c76d55154c9ef3b9bbfbf367c1a035df050cc6cb7eb77152dc5c39c882d2e84d5ee50b95cce326518cbe7653c9f9c9cd900fdd203353dabffc9211930ab8257fd462357bb6a9cf40c97bcba468c35b78fe6ad4d5920849e58cf310e7da4b551d35e3438de315d9529df8016f9d0a197c5614350027a733d13f0efa87edf68ccbb2d397e399a79e3b183f14d33a5b48681eaf323add54d9b6941c00e72d7ff89b0f725c78641598a3ceeb70af673e0c88cac8028117f0c9ef259d1832d944015f974876688d2420edea7c011334951631c5122fc0fc3b228f0de56fa89dc19ef906773955872c335c6a36b11f120fd6dc697b7af0a106468475733a3ce82c51c8feb9ed6e80aa5c7308fa6872e05eebd01ad1bf5b645e20c5a2d9eff4f1afa85291764955bfc1c62b5963243d61565a26a5989635968fb8d31b641ff66e59dcf7dd8f5f355e946b29254db6300e156ef3efc238bd4559427075dbd5768bba1244392f93e167b9d94f81553220fc4fef1ec8b0144302d4cea2bf93abe7b1713441d64394abb812d7737955ad40f2dda413a05ac175f389df8dd3199d969c2ebd475f456a8b0a607814268200b06196b91551505d762933018db5cc8aaafd5ef068c96086bfab4f6df8ff30d705168f25a97321797de7d2ff029ead5edd10597c9b2a0a085567314b94c60159a524fd365fe106c72432f0aa50ea85a9ed5ebc5f9647d0e31608d7f66e49101009df474c4dd0836736ab077810b399adaa30cb608266fb35b699ba0c84d049819fbba5e34d3c48b06186603e908283e1446509b434a16a2e582e808678489d00f3909c5ea2cf55196e4ced009d35a62cb6e0569afc747813a7810a6097f753f54602d6364d34ccef27791b056b450ea7d4058794540fa97888bb6d8daf0f6c968be0b5f5e9f6c6e1e0137800754eb956b58fc3e49f6fe72a55aa46733976a8bd6569353c4c713f20169a17021cb900c7d5c2765ac85027ce8fc0e22c936c82f814b08ef946f422aac68e65836b8b45e50295afd6e6bfacae08beb1ad848fb557cec41ca7c6447d867581d4222f4b0312dc28ccb4771851e94ccf673dad8265a34ac4654cfa4fa92875ee0cace218e57a9893f9bce80e743c5b9de8f561c34f181cc839101c51ea515784851bea52b6c9e21774d0a4644d116dda8c9d539684222de509739f0ea819e0b473d0594dd808874f21e4df98d62fbb2fa46df0ef7a845c8f21d274e8169d258c7b94549219f1ed46bf7abc0d3a347c6d1156f5a3f1cda3c1307eda04a7faecd810b9f61fd81c6866af91b2274202bcc91030932176ec03cac422aea688f79f9a03846f7aaaaded18e47c44ce9106d5394e88e035796e5ca08d2727de3872de5b205568b9c5b2b726a1e73e8b4373039ef7baa7d27fce91337df0705374280457ea0050d1d023a4c684ad07ea9ba37fdc76c26a450aec6be98d2d4278b5cd66e73e81c90242ac207b3cd4f25b80f10e1a1605556d97a53f98090ac66fa658526d9c5484ff869ef38397e596d97388a399f6a0cb62044ffca64e31375a96412ef2d546f4576ec9d469da9fbb361dd7a811f8e1fca158a5519f164f35652ddec13df64d74fb6532508468e48369a3447b6496d8e05d98efc07f2aa34dbe590be115a18021230033f75a1dbae1d420b6118ea86360897328ab52e3c9d7d0e8f53099e89e7cbe6aba41665892b0e55aeea5c8b51cf619fe2739fc11f14ec706154d7d74f84d32a08c30400b9a59522ebbc622b8c16285a9fe97990acb4b115d1789bb6f2e4b41b759ad74f3f5ed72a714c548d3f9d3ba6166420683894812cb4cc0e1004ef057ccea166d3a6e29a56b7135dddf89a6545ec1cd2dfa04c84f9035663af955af4e86c6bcc69fb7b9457d36ab5f76e448d2a16485e0de979366a4bcd1d465927ab6da784fa95af836d3b20ffda9902ada4e0b44df56a47a689d145722403fcc58e4454614b84ac392610ae45ac3d5a7254810f8fb35eb7493611692bb6736fd7df1b3393a1615b9891bb4e8c1c4b4f7f5b4d090b64e91625b3d7e555868b805510471f13a5fd9e1751ed3d27cad580b04e952f3d4019161caf1f18df9e7f4282852195bcf6d9f20682a7c5f8f3ab6c76903472f7d8f39c8e17f39b6bbeb6c94ae970e7a07acae1e5bafbf9f60292000bca7c1de827d9460163cc2cb396ad48224b0849b7d91594b58b464d44998ed41500525e6f798daaa88a78149a26a2c840d952b04d71c0e74f7682d1fc3739957687e34830d31b9418a088c5dc3bee96a99b45d898eb890e5326c927f04edc2a8fdf28b9fe4e592c3b839205f46a9f73053f01fbf030a6805d4576add4814afc972dab7c3fac89a31180ea254ceaeab53c7a4f3d5035c2c1c730feb58eaadacfe444930c7ae9d38a47106383934da616c6821eebad8e78a51ce3d34bcecb3010a22600b9a245b2bef7b1aaaf14d94e6496443f5ef9bdc809f7780c10a16a13cc8bc334cf141f2f1ef283e1a9894cca06a65676a5a83e2f2ef1126aa70358c6430c2f393679002ce23234139c286500a14c4754ff6425aa271e2e6e224ab9a9b222349819e03b088e1778e2fa1a11d404d26e76fd121269e3bf185d9d2a3631e99cba9e4da01f7e11fa20f053365f74e6fb21a0604033accc8e6e6da1d48c3fb347a693036f01af62812b3e860b759cc0383e9a82bf8d4da358df2f863d2f4cd0e5b177ab51c69fd3379043be995d9cbd0f884c41f68faf769fd5e13d45d2c188d1ca171a4ec6012665fb32a1d196eaf3cee74933cd596680bc65b43da43eba2373c5a9f3c9af58f33268035b61751c517b601643939d7bf3564ca669260839274b6c34c6e85db041ee4251fa98b1e22f86be5d0370bc41ac8cc08f4cdead8d84d5c4dfb1ee3236a1cd40c11bf230eec6109e3fbfc7fa5a0598c58207407040e4b4ae1f1c76dd83af9ec230b1d3a057d34a1a59f8a5009857df0bb0c60df22ff625b71af42f161a63b1868f9b5da1136e9f772f16c9674071df05c0ca01d771fa1c53c7763ddf964c80717ba903262cface16561ba42a08053162b665a4b61cf231c3ade9fb81e01b0520f0f781b11ef39b41a361a18df6a7a55378f2436ff7a9203e7973c81741b7c12c4e3aca44a7bdd2e88b277d9a3e1b5e912e54db0b1dc0a0d71bb4e9c7a6dc94b09bdb530211a5077036c51b2417162639791f8d4e2322b62b95b98075f0d3a7a25aa94d26201c87811300fa811810c74527f4e7d1c945b2126219ac5e6c937abe0a473482749fc7767021f4dd36b96af5c48bad391c4aab54b5cfb2eabe59d09867e790793609199f19c42f19b714e6391617abd40a3150ac10334b7bca9fbf4c669e66e096f95bf939143dcd1975219763b6016518b2e7ef5e3a1f9640f871b4193d04d14de1aaacb9b2a918f6f41d98cd04987380846391157385f5511808392a34cd7154a4f8071fb6c7256a6c26919b84d4e307fd0df1a4062b3ca3393eb2df01a1c910aecad39f7698defd02a9fc62374efb9084e45b3b13bd01e3107f7e23bc28ea9e42f27f54746e582c1a94936535edd1e7d39383b1f84b4bab82b8871877cea0a40e530efccf2ec6030750a454c7a0d72618c8d8bae58b3958e8deb65234f27bb1be8653d18c325933e5cd3f5986921c2a5e3d7b3477b5579ef21253cfba96e81b97b6ad25ee40b07fb3efa7aa9657c03567943502378e67aaa0495a7900d5959e684629378fc5d0951e32ee552d5d60f152f4637bf3ada893a526fdc0b16996075a202a116da21de3f6ab28ca31b9ed222c679025f04c5350c6e38445df66a304e0a3e5688b194ad8ea55f6d437360d064cdf7a6277a5bb0c8b9bc42780d38442e55e451c210c791ad1a03870406022f975e00c5d6e43db24b817f6d07232785fdb7d236eb3bc98c8b17c8853708ce1c8659f65fb16b4c2bb7d5ee046e1c5b94fb1dda1e79922495bac385b0fbd40d0ddc0825810c7b6e7f383192e695043a29227458bc96960ff987c63f566cc03eacc0e89672212278a73bcaf7a65d5620750741c7cc2ec17a0fe6cadacb952a9120c8a79821ef39cc7178177ff8c98265425d8999bfc65cbe15816b3773c177d856aa5d17cd8a988116b27793d9f5bcbff", + "frame_size": 80296 + }, + { + "data": "a8210aad1ed9b3eb0351be41fe10e8154d2875cae4ce1e44439293c3a44575a1eca27df05bf6b6ade3f5a40d7cbbe2728cce0207501fe8c03c6240ee0cc23bb9fab805f5f255658504c4184be836867cc2f08f132be21630de01981571f31deaa2d059e4d1f41e2424b34312bc5f94e2066da386ea99dce79c5a1f9df448ab5d57260484731ad3355b6363187cde38cbe5c69567a005217b3bcfe5bc56f851a5ee097043453f26b16737dea708be9c1fd8c95d26f22373faa22648c850063931e39b67397986017de8b1799f020131eabd4358fb2f63b67886e977369430799da490974902599b311e72e1b1cd6712e0c71ba461125adefee9e51049e99995cc7bad75c262602c7d5e14e4d6c96d305fa10c6c2088b89e26ec1fc213527a642f19df423f49e5e69c6324706315cbdbc691d90b5fb6c31f811e387040d02da9aa4f45ac78e2f83bd56a416ddc80c959d95009c1cf6a1c45f7680543d146e26f7d5f4590ce83e9d8cded3efb223002cb53ffeab1e32f43e8b332c5ba1d3f9952ed34893ee7a94261817a40e0cb6c0a4df72889b08bf96f8a69647096d7788e0f16b1c5f24bd44b600b1e8eeafcace9fe7a79ac0a6e22f8cd84ccb9af0f561ff2c3a042acef95be1fa71aebc18694af103cae687ba056f8e2e87b8ad3c766c4bb9895f9a12e167be2401041c06390356c8f2ddb44d886098474bc2d5574de8390c99f3b0d8cafe061b6090640c653a3112cb62a6119a1e949adffca64824166f492ec2c16cca9cb651a337b4f16f160d77bf6716cb9843ed87d382f49621cec47c30ad82b8d4a9b010213bfb83be8b98f2b6f4bed3e5f669ec641f3064fb8f65507b42673c85ae8a7707e2b369b5d3243eb01a970819a327d4de7d0ac7b1f14dfe766dad2f56dbacbae54f96513581e9f5ac359459e42d6272259a31d21ed1b8c48e9798d581fc1cb6c01b87e8e9e043e0dfd34f9c62a07c434cbdba540d689dc2ca5d174d407f4221fdbdd8b6753e3bc3547f23c560b44b5133a44dd0d4d4d2aab6a0aaf3a9c27c6756e33973f3c4731e38f9ac45edc87307e5eaa47800cb978235ce92f89f0f5b8cd2f6ecfb8c8c6346f427f6a15078c74b9fe6ebf4cc082db0203f5a704a9ee3fad73826a651bc64b65e1fd9e57911dfa76656e6a638f505bca165d7f76ce8989ca7f3a2a7d3c9095c49bc666559c49036894d49a50fd73c78000ba1ee2b268e3336b1121029ea0dee564e4352a18943d6614276b18e5336d18c479e37b36c016469ba46d6cd516cc598eb4fcfc386b3548ad5ec90ef12ac514620cb05c9c70c32c10ae733310d864046c9b9b31300356ffbd6019a5501f73d4e321ed799347eb0419377096e6bd73f779c20cc75fd3967326716bfba0d75366bad45cb1375bc93a060ad42d6cd9790b7fbf4525fb29dce7ccd2baf4bb465494c48f6e7a86d048f9c39cd58b998fe75c8c7d810bcc0ea788b2ff8c026ef0e1e178baa50737dca1455aa9efa7754cb54c1c55eb81d0b920644ecb270a2cca51785e9d670e00cc369e6107a978855749c4bd3438a9e9e00e53bef88bc2442c17f3047135f572d86100c46f76641acadb093b009f0db3104015a3da0531cfd53af065ced28679093653b79a468f164d24ed37bfb0472a6286f82e4489a99aecb2e91f31c3c12d8cab48067f32cee9c42ad0b60fb4825cad476a87b732eb8336810eb257c17f0747dbe96c886c7afd3043a2103d7b6f3d078f32a36166025f17231d537eaa88483f5b2e696c31cea213615417a7c518640722d00625173e3cd817e8b4bc26728c8ed4b79145b9e0a50969020a1c4f89821a4a2ee6134b20ae95a05f33af2356e8fc8b4c285d4fc98776be19b985494f4de3566eaabd66a8bc53890da9758fdb2cb9693a2003615705f2b88b4952fc3a072927685065801a9cd7acacb9fbd8f7978794b08bc5171efbafbefd1ece6e7110705f5c4ed3df88ce87b461a3ae65797ac8be1f2a13382db3ae27499a1e93fdc766dddba9f5e5b70d61634301f103038fdf1b14e01c48b0f8c1e8b87c694642ba9841ea6aa4ac8679198e4213a6b9abc134a8e1c9952dd670f1ec174e7f5dd9c9d3a47cce722593db7b5c2d759e52d8255c5ce6b3994ba5ced4eb6652297c31435a22b52b247e6e64b9c1749c344e04c07d20fbfd5f7c98feed32c136da4270c91ea3a1cd3f01b1b5ef0da3491475cebbf9aa0260b05259a492423929c22f548be98ce2f1b12e3cc4471639a302af70c4d221076a25e3d33de526870c312cf3d510adc1179b1811e04da705dc0500a3b703fd683669b3f5d51c14a22fb548d58e8c3ae26092ea19b41d7595acd689b58e7f788942eddf36e16aabfe4014f3f79f67274ae80f8183c8c332f18168da5a6d3d3e3e8931717451c6d52d50f1629f9396e8f69e6dc27e2604e5bcd3f640d09bf489a26433d97a321e0ca72caad15484165ea013ed3fe3d431f4ab241029d6343c6ef1dda7f1a64a94977ebcb04fd6c9eb83be08e01985f635b7b4023a05c4ae75632242413291bfe12fddef315b51f44203ad9f20003787a20d511af1a922f902bb189c39494ca9b082bf65d839ce14d972b4f1830c7ce2567f80235905bf891f66f648679961a7cf6d95aa6ab047fcdf61224f0f528c2289603c8fc4a032fd95b4b2839d1e9220a6fa7b36aaac360120835ce07808000f3dbaac3041eb448e11c68f451c9cd03ce385e38b71b8d0486bbd6078dc91c2cd2eb0243947714116a00bf5ee38c923783f4136df0a388a8c006db12a663375e300c8b8133c65ac5fc347a29487c67dd31ec2584fc5d17badcb89e6279ebdc17a99fa83c9a4c185d3f2e756af6ed3a5c359baf1047b24e41534b5f2bc613a1d39fddf2387ce2991b1e6fa2f8f84839332e5d71f94ad9557d4ae6d2568f78a6a951c5832b3a44b0e76c281faec6d7e35ae4dc685591e65a617e17a25fc6de2f5a6fb4e104d8930c441b22bb2e6b63f9b38fa0695e7997b148d8d263f2093dbc289425c1e8b92c331e23779ed2fb7c6c8648ddb9554445df1bdc767abf0c46d9d2ee63e84694c00d6e91d33b890543f6845f51acc1a9530f81fbc211b5b590f2cf0cebd281e9abadbade2cff3146fa1b837c7eadee8359965d61bd565ade9a7f55eb50308097654cc33abd206b7ac50ef15e679a0cf5db5c59181bba8e31b85af8e90ee8e1464291f343cd8dc3cadca5983a12d5c7cfb19585f1a77cb315363c1521751a289d98cfaff39b5ccd312089507c98008a8aebf881a1d1a8a09245721dfada68f6a81503a3c7632a26864424ee158e1b5ad3d71f5e003f826b5a99f6b20ee6cfa99003fcb2ce9d37aeed1bd11c2e1168ff076ab14997196be24954b65e8fa80d200dabafd45683204fe0de9424aeb1bf7c98fdef43efbc37989fd443a516fc6a8801aa5b905320c662c6c21f45b0844942efc7b60c8318958e97ca4a3ee81d4c1181d1910badc46beb040fcb9ff6f701f1cf38526f2ae8a06de62dde4e5504212cf9505c144bfd61b2babd07abef716cbe77611c4c65e89787c9ced7a36824c6740ab7c51548a1604b6ecd5ea936536a3cc8823a0dc901c27826cafb22539da15c5bae77428b36944f20fb53a2c28d22aedadd1810a54339f76459ba80a82fb9057c30848ce7e6d08d588af9c8b40b7e215e86bc10fa3720caa6ace195f48b74c4efbe0793c24f3679a73fd4adb4cb09d5a9826136ef3b476245278fd82ca90c8c0d7967e68585dae54ed03f4bad24b9ac63a5ab6e1772b59ea22ba97178612e6c8143d697a536768670fbb980a780122b311ddc902a72e29b9059c45c9ef1941a10457153fe92a46af66d526a6ca2e580a1d30a0784c8b5094acc01c510fd4f763fbb738c9f369c92bd497300b6e8ac9a41ccc64d9ee1057d702829fe6eca99cf250488d4e58d9b7a4dc3894edd3aa6ea3a32e2f319718070ba4209fd53e47f465f3ad2c607ba4b20ceb2d994570f0d25958aad2efc7d7868edb712d8d06e367f42dad5362f6de4cc0f8a9ffc40091de491dc425e0dd3bd8ea5657f3a14006b2bc45b70f1ac2c8873f5b3c9efc6269f6e5e68f9349926783fb95a9f3488ff6074e38aaa53e438e5acd990653bf7f297335ab14f6725a5e6a506765b926167ed3b4764dbcbf219b58506e280ce29e9cec13ad10a922c29c7f5c8f879d2ded36fc99904aa30130509ccdf17798f3a6afb9958c0c34b9d14237629a069a79cf917fd597ba54b68abf55299556bc88019725259c9a4388c6ab28b5beed1f8238a6ab3758fc47ed7b0118235dd3bf02ab9043e274397a5c9587b53626e12abf524200fae137ac17bd24d4bc94cc338e3397646e69eb79f21b7905b845688f4c59d6a4d4e378a926f31ece69524f695f8e0b646e584acd54ac5f6ee70bf35c64da77c53c2daf9c00d011d92c047ae93e5fbb0dc99238c161d980e289285dabdc5157235fcf77125a18e929dd2bdc71696df7c95e9e8c4b4a0c3d118a3117c19b8a6f333dc12190a60cff364fe5c3f6b2ac040378df7e0dadaf306863eecc0c2073bc49b60a106aa9446ea8e6b3876ca0fb643d09f7405fd5f1ecd29e48feb6291387bc9c413f782b590e077f447013396b118f988f0593bf9af6e004994c54b709d7ce07e35c15a6c36361037f6f5661450a4351d9f50bd4ee0b004584a910e372cf93d123f95344acc65ffd6774804fb3a254cf76ba0b8926b31ff3b724ad8483cdf1f1751247c67d67b91022ac94a8f9ef579f97510e7416a9ded1abba417c7fd71b4427f0bf603b53215c444fa6e55747c144cdc0c18115f9d6676a827f2404407cc75dc2c6236c5e69a216bdba3c57756692e3407a27b4685c1ac093046c08e4f52c692166884f2510d2ddaf0589caff3a4950fbf227e90330bd2113cb8c592cee6cad5f2585d3777b583519c1cc2d35abb630ae2874cb9246dcf6e23b3d8f52ba47fef317de2787a3e423632db45f265a080ad7eba4d7985b4fcd1fdfffd5cfe2a640dc41cf03aab2758aaf6dc6fcae2815883401c4a41d1001f23a4388a765de74887be05008fd636a4d36fa37d0b9a54d903a11060d3ce345b9b3f19063525cc71242bc4eaf540857d8fc9c813e592a7b4e33854b2962519e87a7e4548a9180609f00d7ad4da0a9ce289b295dfba59c3fdc105129ad46e3fa895990bf7e1e2502d87bc3e130bdce5f1b3fbbb3cc0e941a3eb1e20f91cf86043c2dc701551135d29b3293484dfb084a7e7348db1153db99773b50ffc4b934989e936f5a1b2c450e7efc283b3868df8211c88059f1d86ba69ee67745ea985ad9862c0868ecdd95326d1c17e6e20cb250f2680be373f021938932ff17f10a2e8b7fb86823d6f0b09b83ec50848d9ac6359b5d4ad8746765947d59fbe36726015ce4f660c48bd6175e997f4a81b5b5f3f2e8a871c723005675ac6404b43392b61b9a56da79b7861729bfeb5d66cc5a0c129a3e75aced8df9e21005658d0ed470459dcccd59664ac447bb59f941de58f3469ee754a01c49e1d679dd50a435b0160522919bd0027a5c27c4d8f661732fc69683d81369631f7d2a8fa989ae716ceafbd883de810d6ee91b0e2740680935a33ddb9bef7ef47885f8e205ff25b94ea090ae2bd75a5b84dbc66dd9cb0fae0c92e2676df39f8114a1a647ad78374b791c605c854452c8abe549805539937c7b78674230038c2edc49d0f12bf3138000d4b055ae9c3bab81a87d5e52f742bed642884b71266bfb05a0e45266a0015604a957863ca201e512728357553842ccd9d5430d37e9a035f2c3b185bee57482cee7f549bf8c41c01df1c799a8be7e1362611496ee8d740e690b915747bee5f7457429bc9fdd32ace308b327b723c6d9c03d10ef298cf4334d856feefffe96ae11da37baeb5375ef731ab4ba958389d9bc08193b85d7c05880b1e16346757205f8e9407ba79b879c7d1de5409f11c6ce7c6523ba0f22048f1eb3dccc23ff0fea5356194f43189b8d742da46397e693c3d49d60a921447d60babf543abaf421ab2e125fe64c4f5878a6c5f1897965d5dbcc1712e9d2e1b34be0fce8cbbf6975951a338ccaf710defc5f36fae80f6f8ddf644f00e2e281e9ae72267cd311d059a9664b99d6a5f0f6f82a685df3bc19dfa201efa1b72b0b6817acdf1418d5370265a3a2de48e2e7e69483a18a47139e26b3dba8b97bfac6f1eced6b59f59e8b6d5e9eb278cb31beafb49998b4c998ba51d7484e68741f421c41384129c5afa882e48dd646773c8d5fdf065675344a7f6b0950a0f99d02fcbd38cb22eb156438c85bad0e1b75c91d694f13c8a684401223cc26c489d574d32fe53d99f44e920475395235e3eabb8339128ed588aded339809d45852ae30ec16f9a792db527ab24771373483d72d50c9994515fa5e1cf63e13b3ffddd2334f3edc2c67435375496a937e9caada5b7301d2fc53a88695165d3e91480d59a50967cf33ef584ccde2d12e53aefad17bea563fc17493cb93c120528ecbe564f117165962ec79651cc0413ca080c0f4f81e627b0d6e4a0d23d549d6201e3b4af7adcfdb89060c253bda9e731bd53175545f79b50dbed4d479274136a9c94ca1d565aac6b3987c1cadc9b1015d917aa1111a8de9d59612aa7eaae391410a61ef49cea58da0792ce29ccbeb7a289538626472b2d07373d1e1332752b6014348a5da87be7fdb87657387ec27c07cb65a1136819b36d15b819ca866d96980377f8ea08ce4f49f3ef7723ae4f90f1675808796ae2fe5423a85372c01e6c1f60213c020f90b403dd8d7edfe96bbd18fefe12eff8eae8e1982db7737560ce213ba2f6b9ae4cf8ea09422066b63f24497a238e0e0bf237413158bf3c0cc4f9580649def279af7c861665e45ecae50b8daf63ee7b8440da948a745f6d2767fa5508edf4648013456e64c81a8383034074612068cea438955d5dfc9c3f455ac299d8f751d49ee082760d7f36919a5946c4b5a9bf1586935ce4d68232f9fa14764f81ced6df5a81e0a9ba1e752a7821107153b4bbf2a8fce84aa0559389cb009c7b068f29dc5894058d40b2b3f35119f1ced3fe0acc13f357101d3fddb369d0b58da80d6e9407ad3446e267825e81f5ac6408fd110c66633d079289d9606287f250b6aa1f5d6b3c91bae05ae3dddf9ae0de088259ad9fb6aed3d4c9dc17bfaac8b6bb5e7eff1713b6a5cdc9e03c1db891d2cb556c8cc372c7e0c88b9afd98b8e441205dce4ddaf43b2bdad3a73db901c59af9440a69814a1c81174f56b450c7fccd55e4c9bc2da61077767a16a73cda38b28c62b915ffe8f6a310b6b33d05f85298377cfbaccba6e10fa5e7e0e70c2fb3579cb3b3e87e15aa288bb7f3cb5b37f5417ace0e1b6d430d355fff1bc49c522b4b8dace04a78abfc9d58cb15eddd8a108e889bd14b88213627d1c594fd2be09ad47e87654a9fa0121914bfc490474452d0eb57bf4a459def43de8f129fc13983c68a6c83f824148776deb5ec0e54ece747d8e825c90502d0ece136deb75d04123ffec2e8905f8543bd3d65268440cd82e8f21c1f84b3dfe8bc593ad5f1716327cff6646c1a4b4083607b769408389685cc59e4a8b8aff5faf131eeccbbc8cdbc3ccb36b81b041d9886df9346f8a4eacbcf27509f9cc667f5551b585208cf9050afdbbae22834cadeccd24e4602bd81e6f9f4d7fa6887052f3ee563e8588462a6f9462ef8b13a05290500db0fe454bb49e7cd979fb2b6d1be8e5883ac38dc67a28665bc7eea6bc15bf3f7be4e84e9b5864a5ac775841533e017dee3fa009e811f72b67d1df390b6f379312ab6dbcbd1754305c1f893cbb42b1d4a64e93ea101aea02de48aad928a9040357a41bc9a260d3e597587010eb4b06e081acf052258b692766a754075b309cf0aa423cd31f3a95aecd831aaf311cbce786c393f5cdb94cd010b4a849f8cd8c7d023730828a877e0a72d7166cb29d89034d0580e04a1ad2e92cb2265bf14bc939fbcc505f59bc3e528be7e22cde013add67c602a02a68411c4da808f1443b082f30c98043cd50a811b7a62b0c4a3f1f9a41b948d8ad41f192543415faa64c21b606875e83bdbda40379d58924f54a4b703188b0fc2ad714a45cea367af19c4e92db604820a779cd081f476552f14c3f0b52d5ee94f934a266a4019d7e6416bc0aff76c5bd7eea32ff130d3bba26c0f6f7ce99f6e26aa5d3ca4374845103bf16c60485adb5c478de8b4547f3491c06a0306f19bb120bab567352082d6e08ae61304b1a7639e31402b4d5ce7fb7bdcafba5cc61082a334381fe75439e9e5445f5798a3e19e8badcde346d5fb2251539232bd74a09d3c0abac936c57e53959dfe2316fbf9290689d95b7329cef1b4b09ccd36f0bc006ba3d8d88f265414e1c10f561cc78fd5b43f0d77ea8b89a0f0ec9a9a1941016a23f892fda1f3258e3804d0c950d7618df4762ab2e088e98089a0fe0e3df797b14db986496b4c2fb7c9d7699d30136a1201319f5be4032c67a108384d8dc4357e68a849e6844dd658398757fd86256c82989ffad829c074c8f4cab191298628d30292d5ada7735b26662ed48f963c628cd5f6fdb283fcea0dcf3160cc04a1265f92232fb6d38f1b60eafe747f0f3e8aaadc1ae69138ac0b206c396349af5c085453b1a9a5edf36dbc6b7b43bf5f42e9b10469e702dcfa448375c3848c65ae3affaac9e2de2a8ec11da8b411eb145fb73031babfbaf3731f222a3b5996627766cbe3fb8e95942c568632da17bd3a54ba15c448a0e4279bf2ee3955c07b1ff6dea98b06843238e1923372f6cab9353b333322556543bb4513615d95ea3eb090264b020fb7e48e9eea65f91d720d943002135c0fe5d27d8e170abedcf96cd6be1bfee63c9ace9314229645eda751b54d38eab8f3756c6221e89470825584144c12aeff697e2a100f41548234dbb0b2fc20e0af22575db306879030d306ba96ba1822a1f29ba7ca20af69c587075ec8198d033d5270c94bf4bbcd02fb62ec7a5f6b2f1b924b8d2f1649ae14cd4b975b2f5d64e5fe86a47554c8cc7cb0e72b7df2d88e68ef464dd2b526915a89444cf813abf34a473b0911aea558098d6b8a774f4e3647d2113c6b812ad2e2713c5e9ed8e562c2d12dff3a38f418a83f299af37c7fb0fba362469f324db73e681054610d0b512e5d880361f215e31132823ebc015f4a765c9c46ea6674721dc246bfa0fe4c9431d4a5b61eb5e83fb37344bd185344e0f5f8ec146f846ecfa1fe56044abfa62d11ddc08e4c879bdc18d6944553fba6790195c60f29b9e53469a4f73747a400c0ebb3b73dfabee6a0e5acae2fc9611af2d4fc443c0fbf97d6f005ad0c6adf02f9f6a3d6801203e9b9feb5d044ea27a605da2f1730295d055d7c2a8bb7c876e3260352d212381be27738ff06123c0b3c8ba03f0c8da9da5981daf6e375d66d5cd6b7149d3bc4c2d5597b0128d6811faa7402cd61ee61295c2d6e3b43f9d3e362042f475b2a9e8f9b18b815b0f9f14a556aa424415ee6c66f1587d475e996f9b6ec53ac921cf113cbc19c3e6d8fa40902c9bf18496e78a19f4b05896b00073e85c66b3407942122629c708f13e8539a2fc4ca197db722628928d0486bb0b084472069fd6f9b804d83e03da2821835378f06e09484a4aadf93eb275f943b997a478766ba811db486c11512973762f2cfe1c7c355b6bc2cb760a05502b8fc408b2bc13a4dda29fe9146af9b3735b52c61e8740868a69bc4f38f39d1d97f867ea36ca5cd74b70442c7ddd1653c87b94e71a722861cb7666a25120ea262f102510fa7ce10c4f04ace82dcd6803acf10ca47039a45412e893682006975c75c71b00cdec3840354d810c28cfa35d6b5372fa8fcfdae60f09e047fd7d251ca37b0b2631223507a1d63f00031eaa44c3da61b1bb57b4be8091a739e9a4e86df25decb26955b8a938003f36764bb3981d48fe3e15d34b01246546579a315b9e798d1f9acbf0b254ee23b9a834b0488aa06bb4b232f7c1a888f3a3d8267c2ee46db338221b163148fca0b4257be4b50b17a3ca2c9b99f4b68a0269607226ef08ebd5a72c550a134b92185294ea6bc3c786a5fff3d8274c16d9473d64b8100fc3df7c0c80ce72287c03f3020f91fbb0a15737ca3f114ae25b452d52757436cc3f46ca952cb3dec36408f82029f59ccae1dd4105b9ca168f8d96114b67a3a173e3732f78ea5ac05b38ef2dd36ecaa2c66cd58bf944105cd7869048866b1db9402744e54470c3e708f8bdb87b16d44b20703456a9f4c5a83c513a6c397850bd4be00fa857c6ddb060bb5cf70d7d9bfb09c718c5d16058f726ce4c5af75adb86a2887a1d3f16475479d11b68d3757bb2a46a815f3eeed03f2711589e8742b953a23f1a515db11b7a888f423a0d25e96fc3b97b2faf95bc4e18d2ed3b987a98a5a3193ea9dcb7ae5e4dc4be02089eb5566c3f8aa26a46f84129e936c91e4d5c2ceeeb32c573d9de9c8da7a4f590ee77301a7717db697bfe60ce52d561f6f78f60f8aadddea37e75de0387401d92f1d625aafad8b3097ddf931d25135f3b12b421b8996ade18517ad9045d39e8788ef87148f85c24a56aad8f3ecc902abd377f1becf185b61845333f78c76afde3776808c93015bf9b20a94a218273dc861dafa2122369eb86ca883b60922dee61c330a5a1a638d866e97421095c047f0509fe4a64e1dd853765ca25a7fc6c455b7a4d776e65145a828a329bb597d916de93a16b1af22f69a96509dde4df83c8db4a1f7be57169193f7670203ea81d971b2edba3ebac77c4cb130394b31eb9d65308952272ce8fa54c165b8efdee0bcda3dc084fda280e54c225f50846cb9d2ca3253bd2e7e702ffc7671cb2fad0f4f036c6aae4f632e51715a22dfbeb71c1958ce823379b6a05aefb973735c6d64a84611732d400657f76fa9494188fa1bc38dd844bdbbf527c110be87ed2f608aca327071717a5d4875bafd7b9b81aba706495e3cee221174ca2085a1d6b85f7161101b357b62f4b8dfef40abfdc800e09b353b21dee93519b36ac98a4b333220f59c6f93fdcd16a4f0fb1b65c61222ed8c2b981519fa2e4cde5b5b08524f1861d83e95924689a6ff66611575504fe4b76de3a36ed38e4a1f47f8bfefe789fc581599c9748fe63e17742ff65a01425169527937bbb2c41c3ee509a71d457fe5b3ac34ec95937744fe5cd895083aa1f5d881793e0232eb883c09b8351aea37dbf67d59b3a09936a98ffbaaf316dec8bf041bdb91cd3a8d0bcf71b4c4ec258b7744140377c74b8673f3370cbfa21afe3fd69893b24d99b2c9dd4a416088e744185d21fad8b2625af977a3ed0a88732353eb261c64dd49f2755731be6bfdbf955b7b46e060eb6b739a1aa10389fc563d8b2467ec28d83b9a6d63596c69391bee92d45b83d2b11aac9598164804f30b082e8f9ebd26e410e16ef10823613db9df53caec9e6b432754802c1feef52c551ef93f66bbfc1d166fcf7a55379299b157c3ae3ade8a135145d3ecab42d9ff465a71981049c92b531a4a9fed08d2d48e3f224593ef603afd9cf680305fbde95f722af3cc22f2fc8761fc5448530e23c93b4fb7a1ff53debe83018bb2b8c59235d2abed64529792bea04bd3a4c2c092a2077da832bb872846bce0f9861792e584658bbbe919f4a53cc71e18f4ad0699a3ca2219306a88d7c8adada8fc29538a08429f2273a4eb428197fa3297bae6eb05aec389b7d0a97052d1b03bcdfcf53b4b04d3c03fe80779367022839b2ec6d43449913388e3d41f64753f2ffa4c4c8a0f5126862829aa5a16df1c4f6f3e1cbab7b8adc55ef8b6740130d2ca1c58c53c807bb99db923068edbd6dcfb257544201aa28ef024c9775f1b379e73ac63990c79eea1374eb4a7b348611859d4644d618d1fec862b748b6eb2f94c6964e6168d753025bb3388e0c645b16ef69277242ecc370d68d75fb6ac4e90fb13bab0364729c61f639bbb2325640f6f0fbb11895d61efbcb14977bdc2899cbe48af952a423f1a820c873e84b9649488724acab6583b896ed1961cb2359b731467e7f52145b8629027b709daf8c0043a24057a709336f0cf270cd98d1524b010ab3bcd6e9057b2ee98f4ba59884e1a8982efe48c31db42b71139f227a0c3c348523be3943a85bab72d17da3ba033ff48836a7291d64014c95dadda378cde36e805a93bcae61158f2a33e06f61da88308fdd6774b81ce9b54d51eacd685a49f31fc4b371bafa0d71aaeca0d7baecccc7bf8b1c314b1b5adadd141e90b2f7f49b741142d1ae9715ee1d0e609252ec524f844ca31d5414bf9daacd6de5525dfe9b98efd4f94094ad1c3732c573bb4bc7f2c219fcad67f90d1278a7c18e68f315ebe4feae4e7ceb5c486bffdfb8f8c13f31137080e77a3b853c72cb1e3b8466ffab7906390c27f74735d636cf7f5ddf17f039b5cff46a7f6e18abdcd8159fa903fa194a7820e44d245201134d9c133f49f197c143838dc281604c093f042006d4382d5b0c024523e29f1b6ae7a8edaa4bd5f00e27a0b0550dbdd863db16329b6d2a831e4bf21c7fa62df3210f9bdac71fffdfe47bd1837f45821330526ad007b274808f6f064876d6e433d8891a0b0a3f4a4270d1ffa1d093d48c5f04907fec3f739631e2992055b02417f9d4f2d0fda9d579001e9e154d408e7569b7e5cc5619c4f26c987e4aeaeb0dcb4c5ca8f8218059e2cb2556977a6e7c5da6f2a8a38721626bd4b0090bbbf95775d0d27307fda2f86d7a5731fb8284ffdd0e752fb942ba9addd4eae6e7de5ba5c51698fe2fd435f72b39587441a148652f829fdabe9c13d28ddfcca34f44400ddcea97629b0b58c3cf1e1210a575529f337af0b78b3779f1d0a737e6ec0c819db948a19e06e04cc537ebe884a40d2ad7cea0c9e25dd176a51d5c993d8a7a4a4d963ffe9bb35a8af7896c38401d832f07db719bd0a56b5cf9ce9e2f8975f35300d36d2384cee61ea4cd61772f157aa418977586e7522a771f4b819b7af9fb04f6d3cdfdaab75af2c43212d2191101ffbf2a36ff1438b78a1bb8fb0d626e9c5724a1ef340142f890980d9e6072d6d1e995f821eeb0d189553782132eb939e82a14a23ff21e3f8bed1589210af8c600662a0e71159363b96b2888b3ee7be635be1d9e9068196dac521eb784923602061b21d263653b9f2db093419d51f2ee4033f1021be202aa1a986f31c2580cf32590c168b0e9e5e3154d85996c90286d40e45efbd344ba27ae9df2bc24d482f23a0b30fec4238088dd62f33d75d51bfd358ddfc7f95aaeaf66462e750e8818fa7e0ade9db2583f1214be84999f721b33ec84ac98f86929fc94751365878c07fbde40e239f1cd92bf3dad746b16a4b20359605ad8bf9e03c66cdf1efcb78fadf8fd898cca6c0f65746b49ca23a6f414952aa7c1f66f9f136813e16bc031a2cfd669b370b7289128ea635c005596767478d74b7983e26549878e39d488cce08734b8c24c2c104ae746e41ffcfed0ca0419a77e573ef1377d4ddb093aacddf23b4a9d6d7e86e0209c1f306b7eb7e9fd8aec322d11715dc1ea98159c85a487abed7cd859a37a10f30afc76aee48dc77dc726c4f6dd5745e5a0f3e3a18c50571c0c2d006469ce286e7f62f76662ead5677c97b184a00c6e42b2e452357b0135900ec2ef0f3bd783507a1d12a11a3a2dfa7d664db957f657eb1ef32197db33644428b5eadfe07ea607c7449794bce53e825f4492f9097ef964d2346b1002811e2903d764a9e9153030d27a70c04bc95fa2f7a626eb2524815ff639083c6fa683a15d0f2d00972adcb111a74813a499548c4e3adb1de9808ad727571057f53435d14e21a09cc6f6fcbf779eec9e9969b4a302256ef5283c8eef6ba9878d80d7c4f0850aac0024cf831c520972eed7a51d4638a37a1c2cd87eeedd3d6fbffeb1c11d64eea081d28e804c7e534e6f45e04355a05ff8a1ae95d54c9070e83b100808c04450344b460fb559803d245682243da44e43cc17531751ce7f1b642b72d7c5d414a9c312abd1301b5f9a8236224b5b0cdb6fc168cd70196ff216334ad4d423a85760052ca68d984e3dfe4b13387874a0f7f10920358995eeb77141dd7de917f2232006d21942846937707d6dc01081ab084db9b3369fa54eec3c43ced921b9d381ca01897156a83c105211b6784a8d15b4b2f19acb5a1a1927714b9e1bf4980737bff88009b4b110875ff5db512dc8c3634f986b7497c9d16388ca918cb2eea0eecee9cffbde9b4d3562fe40cf36d1bbbe2b7ceace97b833a04b76abeb1e503f128a7101eb37f49ed06e8f593c328358cacbee0ba6c365630c32f2c1092c77334e240a656d6a1fbcd9556a1f6a4827ef68a755bd4227f7e6ceaef8c5956e562293663cbc8ca8495ee46d9128fc44ddbae826ff7be8a1076a49aa9a846d0542fbe8fafd3b36dd5ab075310fd384088d9845c2cad39098d943e8236beb4f691cd79cb6aa1dd47ca87cb9eda261bdb2f40229d51fbe1074056eb919fd4f4fd4d1656fe7aa6b9af6d3a50b84c56df9e8d4f8e3990e3cc544a48931b39cabc03a3458403cc901b9f91712dd0d775706a26c40b9732f4c257c2c2acee79d8d61628f36e4cade7aa9405b4f62f184ae045d5f72e44feaed7915dc0a1e93fc08ab986c021ee0fdc6830f0b0b688551faf83c8bb5407f92446ad4bb5f3ff4e6dd6e29bc15fb972e25f5e361e513e34524b1643013481649535eda9469d6cb966ae1a752b5120d0531e29a239ac35343e3f20d81abe0f489e530bdd32829147208e2065d1724e0a9f40845c9f626e3c89da15bc9f85e0c124e2fd48980d033b688760342ba64c5ffc799b746e4f88e392aaee454f9865c1817c2c0e3085ed41a200e6e0e433367b3567d2c870f6b415eac97b28ff0e5ca079d6e2b86e0237eea3c6569a6c08ce13a8d7f87db0094427cf453785e10413b0434d48c0797ba1472106ddd52590e42449073dbdd7636456d22fbfaee8c7dd9a166880a3771f49bf2e1a04e5bd9d98fcc5093b6fa9e302564ee05f384b558044879332f91de3832ef0b61147619e6305f339e4829507688d9df24176f462ba3811c515654d403007b4d031632e79c6bd3e435df9324ff9719fe754e8bb177121f7c94c6a562494997d43478e28efe48efa2b28210f2c61d9d2aea401bdbb4d91b5d77e84b1ffd6d8a085ac0404fb1d352598d6d19d5f5b508ff866818366c97d6aae39247d2fd90c3a0af9f0d30a091582dcf9c520418570841fabe36dce896f12b21a396d093399d7c148dc7773f64", + "frame_size": 10769 + } + ] +} diff --git a/ethereum/p2p/src/test/resources/peer2.json b/ethereum/p2p/src/test/resources/peer2.json new file mode 100755 index 00000000000..72ba456f6bb --- /dev/null +++ b/ethereum/p2p/src/test/resources/peer2.json @@ -0,0 +1,18 @@ +{ + "aes_secret": "aa48695c0d5b67c46cb76584d4a8dd93f4c25546c20d0718ddaa5ebc82469138", + "mac_secret": "306aeb31b7db738ce8f4f6d68817ecbe1646e5eae5186f4c8590c695afec3e62", + "egress_gen": [ + "5ea10e6d57516718031868bd7c34493ee93c60d67435096fadf437ae4f6fc5f2", + "01ab042bc9f27dc8720d8c1ff86677582c8a8a3f06ce10ab6991f4d938ca354fae77d045b394fd3162f2856f0fabcd1c058412a9f117c73c4f25a23c3b302eacc127a96ab5992fcda5e812fa4cdc8a4e62c737dd2ef996ee67a470b535d27f3e109201c50145412202ddb9977839cd698a3b677198018936a53235b7ea5074501de43fcf683f7c7de662729e34aa8a084356dbee03450579934490692e8ffebd387ca78f20a2dfe16d2669e24032621fca234382142693258a9f037d09b95726dfd6c6a79df06c2550e73cadba3ff0bfa5b85c54a291bf05ebdc45581891414fdb7b41cbf60386e01fee4e4304d5e19f04f4fa3469b38e8e95d729c04de57656329e3e46e65f75555aa03c10c524051e4c25e0a865d856f4594d9d605aa25defb4d062673eb65451fa9068de42328332860ca373e2348e96a302337178abbd6250a2dc1aaf11897e52f7873788dbaeb88ffa52e94af66748ceacd60a6dd674291ae5e69568da29e7d19bb83ceaf25e608ce53d2de5ae8eed14659844b64f2251eb1863cbe4e5d8add741337875fd181005ada324cc5a0ee34ea446efd0a7916504b389f137b2697b84e7d38d97" + ], + "ingress_gen": [ + "cef25a9d9d843122d781fae138a286b1f15ab4ad0a0ab78ea491aa6e5501b9a6", + "01d3048e9d352f83959917ce282ed69956add5bf7ce505d37fefe70869f12ae74ec681f336d20217c5f41871f3d3e1bedeee808ec4c2f91eeecac92300864f750f5c8efd005218d88de03f11b58d80fe64ed20c3b3b854b4eb7dfccd44c1a68b72a314348ac23f9af981f29565d1e13a74f2490561412ba655c5150b11d5c72cfed0e167cbbff32c6516ca42c322f72810636ec2fe819d32225a73730829405bcd6c7e735277418a4caa2a2b87626467b6bdb289e6a192812274ec75802d26be7eb2a055ec6f09c2ef1a1b0545076d42efd742b3e855c3f4014957456c428a14f3f689a27e29f3ba048c080625abc2d97555dae693a2481fd16a261d542692de30b4edad8111a4ba80438d646ad9cb327b53bea7eb3196fc4b045788d652d2cc15db2d396988104622d5805250340ec0934718edae4e4eef5cf06c482c08ca401958765b85e3df1b1cf6d2684db5c38db9d531cd27d9ab8b5b5090fea8436c175ab779c0d6e780225516149a8921b53833df08f688ded93bede82ffe1ff5033b174b140e1d2267948a5feda19c830346d3ee7845ad23333bc91987723e320459e7c964d6962edd20452f74f3e99e8db28a5c1399659dab37236f722ab24df5998ab1e1133eeffeab3876e8214542e7abff693e993f" + ], + "messages": [ + { + "data": "1457c49af1e24c56adbe54609331478a922eb4c9fbc6f28af816f445745829dac4dfcde63f6e39b3abd78b20b6c144061259882009155806d0a0454169a8dc81e1b0ef2502bb34b095e51a7348208417c8e221fe2006a4bdef83c6dc669fedea0eca44ddb48d9d5c94a3bef29d67d1f2d0800c88c01a50eae5463d10a558fa2586f464a782e5fbc813bd9c84aa03b3b03af5807ce90626873ba30e0e9e0bb86c07614ad1ea4d545f29afafa57c1764fa9c7bfdc56727f5580b0b8513843a9389", + "frame_size": 132 + } + ] +} diff --git a/ethereum/p2p/src/test/resources/udp.pcap b/ethereum/p2p/src/test/resources/udp.pcap new file mode 100755 index 0000000000000000000000000000000000000000..a9a44f6f02cb88073e4b54ddeb9e81b5819b0a44 GIT binary patch literal 396844 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

Z+5%+^6BPY&0;jE;?J@)U^+As=}e#| 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�PtYoDQ_DWE1*8vt|YNAKHqyJ)XU_}`9u)o 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 literal 0 HcmV?d00001 diff --git a/ethereum/referencetests/build.gradle b/ethereum/referencetests/build.gradle new file mode 100755 index 00000000000..5c49c16d0ab --- /dev/null +++ b/ethereum/referencetests/build.gradle @@ -0,0 +1,10 @@ +spotless { groovyGradle { paddedCell() + } } + +sourceSets { + test { resources { include '*Tests/**/*.json' } } +} + +configurations { testOutput } + +dependencies { testOutput sourceSets.test.output } diff --git a/ethereum/rlp/build.gradle b/ethereum/rlp/build.gradle new file mode 100755 index 00000000000..712bda07f9c --- /dev/null +++ b/ethereum/rlp/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-ethereum-rlp' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + api project(':util') + + implementation 'com.google.guava:guava' + implementation 'io.vertx:vertx-core' + + compileOnly 'org.openjdk.jmh:jmh-generator-annprocess' + + jmh project(':util') + + testImplementation project(':testutil') + testImplementation project(path:':ethereum:referencetests', configuration: 'testOutput') + + testImplementation 'com.fasterxml.jackson.core:jackson-databind' + testImplementation 'junit:junit' +} diff --git a/ethereum/rlp/src/jmh/java/net/consensys/pantheon/ethereum/rlp/RLPBench.java b/ethereum/rlp/src/jmh/java/net/consensys/pantheon/ethereum/rlp/RLPBench.java new file mode 100755 index 00000000000..38222eef1d2 --- /dev/null +++ b/ethereum/rlp/src/jmh/java/net/consensys/pantheon/ethereum/rlp/RLPBench.java @@ -0,0 +1,65 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +@State(Scope.Benchmark) +public class RLPBench { + private static Object generate(final int depth, final int width, final int size) { + final byte[] bytes = new byte[size]; + for (int i = 0; i < size; i++) { + bytes[i] = (byte) ((100 + i) * i); + } + return generateAndRecurse(BytesValue.wrap(bytes), depth, width); + } + + private static Object generateAndRecurse( + final BytesValue value, final int depth, final int width) { + if (depth == 0) { + return value; + } + + final List l = new ArrayList<>(width); + for (int i = 0; i < width; i++) { + l.add(i % 3 == 0 ? value : generateAndRecurse(value, depth - 1, width)); + } + return l; + } + + @Param({"1", "3", "8"}) + public int depth; + + @Param({"4", "8"}) + public int width; + + @Param({"4", "100"}) + public int size; + + volatile Object toEncode; + volatile BytesValue toDecode; + + @Setup(Level.Trial) + public void prepare() { + toEncode = generate(depth, width, size); + toDecode = RLP.encode(toEncode); + } + + @Benchmark + public BytesValue getBenchmarkEncoding() { + return RLP.encode(toEncode); + } + + @Benchmark + public Object getBenchmarkDecoding() { + return RLP.decode(toDecode); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPInput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPInput.java new file mode 100755 index 00000000000..10131b988c8 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPInput.java @@ -0,0 +1,552 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static com.google.common.base.Preconditions.checkState; + +import net.consensys.pantheon.ethereum.rlp.RLPDecodingHelpers.Kind; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytes32; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Value; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.function.Function; + +abstract class AbstractRLPInput implements RLPInput { + + private final boolean lenient; + + protected long size; + + // Information on the item the input currently is at (next thing to read). + protected long + currentItem; // Offset in value to the beginning of the item (or value.size() if done) + private Kind currentKind; // Kind of the item. + private long currentPayloadOffset; // Offset to the beginning of the current item payload. + private int currentPayloadSize; // Size of the current item payload. + + // Information regarding opened list. The depth is how many list deep we are, and endOfListOffset + // holds the offset in value at which each list ends (indexed by depth). Allows to know if we're + // at the end of our current list, and if there is any unfinished one. + private int depth; + private long[] endOfListOffset = new long[4]; + + AbstractRLPInput(final boolean lenient) { + this.lenient = lenient; + } + + protected void init(final long inputSize, final boolean shouldFitInputSizeExactly) { + if (inputSize == 0) { + return; + } + + currentItem = 0; + // Initially set the size to the input as prepareCurrentTime() needs it. Once we've prepare the + // top level item, we know where that item ends exactly and can update the size to that more + // precise value (which basically mean we'll throw errors on malformed inputs potentially + // sooner). + size = inputSize; + prepareCurrentItem(); + if (currentKind.isList()) { + size = nextItem(); + } + + // No matter what, if the first item advertise a payload ending after the end of the input, that + // input is corrupted. + if (size > inputSize) { + // Our error message include a snippet of the input and that code assume size is not set + // outside of the input, and that's exactly the case we're testing, so resetting the size + // simply for the sake of the error being properly generated. + final long itemEnd = size; + size = inputSize; + throw corrupted( + "Input doesn't have enough data for RLP encoding: encoding advertise a " + + "payload ending at byte %d but input has size %d", + itemEnd, inputSize); + } + + if (shouldFitInputSizeExactly && inputSize > size) { + throwMalformed( + "Input has extra data after RLP encoding: encoding ends at byte %d but " + + "input has size %d", + size, inputSize); + } + + validateCurrentItem(); + } + + protected abstract byte inputByte(long offset); + + protected abstract BytesValue inputSlice(long offset, int length); + + protected abstract Bytes32 inputSlice32(long offset); + + protected abstract String inputHex(long offset, int length); + + protected abstract BigInteger getUnsignedBigInteger(long offset, int length); + + protected abstract int getInt(long offset); + + protected abstract long getLong(long offset); + + /** + * Sets the input to the item provided (an offset to the beginning of an item) and check this is + * valid. + * + * @param item the value to which the current item is to be set. + */ + protected void setTo(final long item) { + currentItem = item; + if (currentItem >= size) { + // Setting somewhat safe values so that multiple calls to setTo(nextItem()) don't do anything + // even when at the end. + currentKind = null; + currentPayloadOffset = item; + currentPayloadSize = 0; + return; + } + prepareCurrentItem(); + validateCurrentItem(); + } + + private void prepareCurrentItem() { + // Sets the kind of the item, the offset at which his payload starts and the size of this + // payload. + final int prefix = inputByte(currentItem) & 0xFF; + currentKind = Kind.of(prefix); + switch (currentKind) { + case BYTE_ELEMENT: + currentPayloadOffset = currentItem; + currentPayloadSize = 1; + break; + case SHORT_ELEMENT: + currentPayloadOffset = currentItem + 1; + currentPayloadSize = prefix - 0x80; + break; + case LONG_ELEMENT: + final int sizeLengthElt = prefix - 0xb7; + currentPayloadOffset = currentItem + 1 + sizeLengthElt; + currentPayloadSize = readLongSize(currentItem, sizeLengthElt); + break; + case SHORT_LIST: + currentPayloadOffset = currentItem + 1; + currentPayloadSize = prefix - 0xc0; + break; + case LONG_LIST: + final int sizeLengthList = prefix - 0xf7; + currentPayloadOffset = currentItem + 1 + sizeLengthList; + currentPayloadSize = readLongSize(currentItem, sizeLengthList); + break; + } + } + + private void validateCurrentItem() { + if (currentKind == Kind.SHORT_ELEMENT) { + // Validate that a single byte SHORT_ELEMENT payload is not <= 0x7F. If it is, is should have + // been written as a BYTE_ELEMENT. + if (currentPayloadSize == 1 + && currentPayloadOffset < size + && (payloadByte(0) & 0xFF) <= 0x7F) { + throwMalformed( + "Malformed RLP item: single byte value 0x%s should have been " + + "written without a prefix", + hex(currentPayloadOffset, currentPayloadOffset + 1)); + } + } + + if (currentPayloadSize > 0 && currentPayloadOffset >= size) { + throw corrupted( + "Invalid RLP item: payload should start at offset %d but input has only " + "%d bytes", + currentPayloadOffset, size); + } + if (size - currentPayloadOffset < currentPayloadSize) { + throw corrupted( + "Invalid RLP item: payload starting at byte %d should be %d bytes long, but input " + + "has only %d bytes from that offset", + currentPayloadOffset, currentPayloadSize, size - currentPayloadOffset); + } + } + + /** The size of the item payload for a "long" item, given the length in bytes of the said size. */ + private int readLongSize(final long item, final int sizeLength) { + // We will read sizeLength bytes from item + 1. There must be enough bytes for this or the input + // is corrupted. + if (size - (item + 1) < sizeLength) { + throw corrupted( + "Invalid RLP item: value of size %d has not enough bytes to read the %d " + + "bytes payload size", + size, sizeLength); + } + + // That size (which is at least 1 byte by construction) shouldn't have leading zeros. + if (inputByte(item + 1) == 0) { + throwMalformed("Malformed RLP item: size of payload has leading zeros"); + } + + final int res = RLPDecodingHelpers.extractSizeFromLong(this::inputByte, item + 1, sizeLength); + + // We should not have had the size written separately if it was less than 56 bytes long. + if (res < 56) { + throwMalformed("Malformed RLP item: written as a long item, but size %d < 56 bytes", res); + } + + return res; + } + + private long nextItem() { + return currentPayloadOffset + currentPayloadSize; + } + + @Override + public boolean isDone() { + // The input is done if we're out of input, but also if we've called leaveList() an appropriate + // amount of times. + return currentItem >= size && depth == 0; + } + + private String hex(final long start, final long taintedEnd) { + final long end = Math.min(taintedEnd, size); + final long size = end - start; + if (size < 10) { + return inputHex(start, Math.toIntExact(size)); + } else { + return String.format("%s...%s", inputHex(start, 4), inputHex(end - 4, 4)); + } + } + + private void throwMalformed(final String msg, final Object... params) { + if (!lenient) throw new MalformedRLPInputException(errorMsg(msg, params)); + } + + private CorruptedRLPInputException corrupted(final String msg, final Object... params) { + throw new CorruptedRLPInputException(errorMsg(msg, params)); + } + + private RLPException error(final String msg, final Object... params) { + throw new RLPException(errorMsg(msg, params)); + } + + private RLPException error(final Throwable cause, final String msg, final Object... params) { + throw new RLPException(errorMsg(msg, params), cause); + } + + private String errorMsg(final String message, final Object... params) { + final long start = currentItem; + final long end = Math.min(size, nextItem()); + final long realStart = Math.max(0, start - 4); + final long realEnd = Math.min(size, end + 4); + return String.format( + message + " (at bytes %d-%d: %s%s[%s]%s%s)", + concatParams( + params, + start, + end, + realStart == 0 ? "" : "...", + hex(realStart, start), + hex(start, end), + hex(end, realEnd), + realEnd == size ? "" : "...")); + } + + private static Object[] concatParams(final Object[] initial, final Object... others) { + final Object[] params = Arrays.copyOf(initial, initial.length + others.length); + System.arraycopy(others, 0, params, initial.length, others.length); + return params; + } + + private void checkElt(final String what) { + if (currentItem >= size) { + throw error("Cannot read a %s, input is fully consumed", what); + } + if (isEndOfCurrentList()) { + throw error("Cannot read a %s, reached end of current list", what); + } + if (currentKind.isList()) { + throw error("Cannot read a %s, current item is a list", what); + } + } + + private void checkElt(final String what, final int expectedSize) { + checkElt(what); + if (currentPayloadSize != expectedSize) + throw error( + "Cannot read a %s, expecting %d bytes but current element is %d bytes long", + what, expectedSize, currentPayloadSize); + } + + private void checkScalar(final String what) { + checkElt(what); + if (currentPayloadSize > 0 && payloadByte(0) == 0) { + throwMalformed("Invalid scalar, has leading zeros bytes"); + } + } + + private void checkScalar(final String what, final int maxExpectedSize) { + checkScalar(what); + if (currentPayloadSize > maxExpectedSize) + throw error( + "Cannot read a %s, expecting a maximum of %d bytes but current element is %d bytes long", + what, maxExpectedSize, currentPayloadSize); + } + + private byte payloadByte(final int offsetInPayload) { + return inputByte(currentPayloadOffset + offsetInPayload); + } + + private BytesValue payloadSlice() { + return inputSlice(currentPayloadOffset, currentPayloadSize); + } + + @Override + public void skipNext() { + setTo(nextItem()); + } + + @Override + public long readLongScalar() { + checkScalar("long scalar", 8); + long res = 0; + int shift = 0; + for (int i = 0; i < currentPayloadSize; i++) { + res |= ((long) payloadByte(currentPayloadSize - i - 1) & 0xFF) << shift; + shift += 8; + } + if (res < 0) { + error("long scalar %s is not non-negative", res); + } + setTo(nextItem()); + return res; + } + + @Override + public int readIntScalar() { + checkScalar("int scalar", 4); + int res = 0; + int shift = 0; + for (int i = 0; i < currentPayloadSize; i++) { + res |= (payloadByte(currentPayloadSize - i - 1) & 0xFF) << shift; + shift += 8; + } + setTo(nextItem()); + return res; + } + + @Override + public BigInteger readBigIntegerScalar() { + checkScalar("arbitrary precision scalar"); + final BigInteger res = getUnsignedBigInteger(currentPayloadOffset, currentPayloadSize); + setTo(nextItem()); + return res; + } + + private Bytes32 readBytes32Scalar() { + checkScalar("32-bytes scalar", 32); + final MutableBytes32 res = MutableBytes32.create(); + payloadSlice().copyTo(res, res.size() - currentPayloadSize); + setTo(nextItem()); + return res; + } + + @Override + public UInt256 readUInt256Scalar() { + return readBytes32Scalar().asUInt256(); + } + + @Override + public > T readUInt256Scalar(final Function bytesWrapper) { + final Bytes32 bytes = readBytes32Scalar(); + try { + return bytesWrapper.apply(bytes); + } catch (final Exception e) { + throw error(e, "Problem decoding UInt256 scalar"); + } + } + + @Override + public byte readByte() { + checkElt("byte", 1); + final byte b = payloadByte(0); + setTo(nextItem()); + return b; + } + + @Override + public short readShort() { + checkElt("2-byte short", 2); + final short s = (short) ((payloadByte(0) << 8) | (payloadByte(1) & 0xFF)); + setTo(nextItem()); + return s; + } + + @Override + public int readInt() { + checkElt("4-byte int", 4); + final int res = getInt(currentPayloadOffset); + setTo(nextItem()); + return res; + } + + @Override + public long readLong() { + checkElt("8-byte long", 8); + final long res = getLong(currentPayloadOffset); + setTo(nextItem()); + return res; + } + + @Override + public InetAddress readInetAddress() { + checkElt("inet address"); + if (currentPayloadSize != 4 && currentPayloadSize != 16) { + throw error( + "Cannot read an inet address, current element is %d bytes long", currentPayloadSize); + } + final byte[] address = new byte[currentPayloadSize]; + for (int i = 0; i < currentPayloadSize; i++) { + address[i] = payloadByte(i); + } + setTo(nextItem()); + try { + return InetAddress.getByAddress(address); + } catch (final UnknownHostException e) { + // InetAddress.getByAddress() only throws for an address of illegal length, and we have + // validated that length already, this this genuinely shouldn't throw. + throw new AssertionError(e); + } + } + + @Override + public BytesValue readBytesValue() { + checkElt("arbitrary bytes value"); + final BytesValue res = payloadSlice(); + setTo(nextItem()); + return res; + } + + @Override + public Bytes32 readBytes32() { + checkElt("32 bytes value", 32); + final Bytes32 res = inputSlice32(currentPayloadOffset); + setTo(nextItem()); + return res; + } + + @Override + public T readBytesValue(final Function mapper) { + final BytesValue res = readBytesValue(); + try { + return mapper.apply(res); + } catch (final Exception e) { + throw error(e, "Problem decoding bytes value"); + } + } + + @Override + public RLPInput readAsRlp() { + if (currentItem >= size) { + throw error("Cannot read current element as RLP, input is fully consumed"); + } + final long next = nextItem(); + final RLPInput res = RLP.input(inputSlice(currentItem, Math.toIntExact(next - currentItem))); + setTo(next); + return res; + } + + @Override + public int enterList() { + return enterList(false); + } + + /** + * Enters the list, but does not return the number of item of the entered list. This prevents + * bouncing all around the file to read values that are probably not even used. + * + * @see #enterList() + * @param skipCount true if the element count is not required. + * @return -1 if skipCount==true, otherwise, the number of item of the entered list. + */ + public int enterList(final boolean skipCount) { + if (currentItem >= size) { + throw error("Cannot enter a lists, input is fully consumed"); + } + if (!currentKind.isList()) { + throw error("Expected current item to be a list, but it is: " + currentKind); + } + + ++depth; + if (depth > endOfListOffset.length) { + endOfListOffset = Arrays.copyOf(endOfListOffset, (endOfListOffset.length * 3) / 2); + } + // The first list element is the beginning of the payload. It's end is the end of this item. + final long listStart = currentPayloadOffset; + final long listEnd = nextItem(); + + if (listEnd > size) { + throw corrupted( + "Invalid RLP item: list payload should end at offset %d but input has only %d bytes", + listEnd, size); + } + + endOfListOffset[depth - 1] = listEnd; + int count = -1; + + if (!skipCount) { + // Count list elements from first one. + count = 0; + setTo(listStart); + while (currentItem < listEnd) { + ++count; + setTo(nextItem()); + } + } + + // And lastly reset on the list first element before returning + setTo(listStart); + return count; + } + + @Override + public void leaveList() { + leaveList(false); + } + + @Override + public void leaveList(final boolean ignoreRest) { + checkState(depth > 0, "Not within an RLP list"); + + if (!ignoreRest) { + final long listEndOffset = endOfListOffset[depth - 1]; + if (currentItem < listEndOffset) throw error("Not at the end of the current list"); + } + + --depth; + } + + @Override + public boolean nextIsList() { + return currentKind != null && currentKind.isList(); + } + + @Override + public boolean nextIsNull() { + return currentKind == Kind.SHORT_ELEMENT && currentPayloadSize == 0; + } + + @Override + public int nextSize() { + return currentPayloadSize; + } + + @Override + public boolean isEndOfCurrentList() { + return depth > 0 && currentItem >= endOfListOffset[depth - 1]; + } + + @Override + public void reset() { + setTo(0); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPOutput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPOutput.java new file mode 100755 index 00000000000..bba9caf4bf3 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/AbstractRLPOutput.java @@ -0,0 +1,177 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static com.google.common.base.Preconditions.checkState; +import static net.consensys.pantheon.ethereum.rlp.RLPEncodingHelpers.elementSize; +import static net.consensys.pantheon.ethereum.rlp.RLPEncodingHelpers.listSize; +import static net.consensys.pantheon.ethereum.rlp.RLPEncodingHelpers.writeElement; +import static net.consensys.pantheon.ethereum.rlp.RLPEncodingHelpers.writeListHeader; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +abstract class AbstractRLPOutput implements RLPOutput { + /* + * The algorithm implemented works as follows: + * + * Values written to the output are accumulated in the 'values' list. When a list is started, it + * is indicated by adding a specific marker in that list (LIST_MARKER). + * While this is gathered, we also incrementally compute the size of the payload of every list of + * that output. Those sizes are stored in 'payloadSizes': when all the output has been added, + * payloadSizes[i] will contain the size of the (encoded) payload of the ith list in 'values' + * (that is, the list that starts at the ith LIST_MARKER in 'values'). + * + * With that information gathered, encoded() can write its output in a single walk of 'values': + * values can encoded directly, and every time we read a list marker, we use the corresponding + * payload size to write the proper prefix and continue. + * + * The main remaining aspect is how the values of 'payloadSizes' are computed. Computing the size + * of a list without nesting inside is easy: simply add the encoded size of any newly added value + * to the running size. The difficulty is with nesting: when we start a new list, we need to + * track both the sizes of the previous list and the new one. To deal with that, we use the small + * stack 'parentListStack': it stores the index in 'payloadSizes' of every currently "open" lists. + * In other words, payloadSises[parentListStack[stackSize - 1]] corresponds to the size of the + * current list, the one to which newly added value are currently written (until the next call + * to 'endList()' that is, while payloadSises[parentListStack[stackSize - 2]] would be the size + * of the parent list, .... + * + * Note that when a new value is added, we add its size only the currently running list. We should + * add that size to that of any parent list as well, but we do so indirectly when a list is + * finished: when 'endList()' is called, we add the size of the full list we just finished (and + * whose size we have now have completely) to its parent size. + * + * Side-note: this class internally and informally use "element" to refer to a non list items. + */ + + private static final BytesValue LIST_MARKER = BytesValue.wrap(new byte[0]); + + private final List values = new ArrayList<>(); + // For every value i in values, rlpEncoded.get(i) will be true only if the value stored is an + // already encoded item. + private final BitSet rlpEncoded = new BitSet(); + + // First element is the total size of everything (the encoding may be a single non-list item, so + // this handle that case more easily; we need that value to size out final output). Following + // elements holds the size of the payload of the ith list in 'values'. + private int[] payloadSizes = new int[8]; + private int listsCount = 1; // number of lists current in 'values' + 1. + + private int[] parentListStack = new int[4]; + private int stackSize = 1; + + private int currentList() { + return parentListStack[stackSize - 1]; + } + + @Override + public void writeBytesValue(final BytesValue v) { + checkState( + stackSize > 1 || values.isEmpty(), "Terminated RLP output, cannot add more elements"); + values.add(v); + payloadSizes[currentList()] += elementSize(v); + } + + @Override + public void writeRLPUnsafe(final BytesValue v) { + checkState( + stackSize > 1 || values.isEmpty(), "Terminated RLP output, cannot add more elements"); + values.add(v); + // Mark that last value added as already encoded. + rlpEncoded.set(values.size() - 1); + payloadSizes[currentList()] += v.size(); + } + + @Override + public void startList() { + values.add(LIST_MARKER); + ++listsCount; // we'll add a new element to payloadSizes + ++stackSize; // and to the list stack. + + // Resize our lists if necessary. + if (listsCount > payloadSizes.length) { + payloadSizes = Arrays.copyOf(payloadSizes, (payloadSizes.length * 3) / 2); + } + if (stackSize > parentListStack.length) { + parentListStack = Arrays.copyOf(parentListStack, (parentListStack.length * 3) / 2); + } + + // The new current list size is store in the slot we just made room for by incrementing + // listsCount + parentListStack[stackSize - 1] = listsCount - 1; + } + + @Override + public void endList() { + checkState(stackSize > 1, "LeaveList() called with no prior matching startList()"); + + final int current = currentList(); + final int finishedListSize = listSize(payloadSizes[current]); + --stackSize; + + // We just finished an item of our parent list, add it to that parent list size now. + final int newCurrent = currentList(); + payloadSizes[newCurrent] += finishedListSize; + } + + /** + * Computes the final encoded data size. + * + * @return The size of the RLP-encoded data written to this output. + * @throws IllegalStateException if some opened list haven't been closed (the output is not valid + * as is). + */ + public int encodedSize() { + checkState(stackSize == 1, "A list has been entered (startList()) but not left (endList())"); + return payloadSizes[0]; + } + + protected void writeEncoded(final MutableBytesValue res) { + // Special case where we encode only a single non-list item (note that listsCount is initially + // set to 1, so listsCount == 1 really mean no list explicitly added to the output). + if (listsCount == 1) { + // writeBytesValue make sure we cannot have more than 1 value without a list + assert values.size() == 1; + final BytesValue value = values.get(0); + + int finalOffset; + // Single non-list value. + if (rlpEncoded.get(0)) { + value.copyTo(res, 0); + finalOffset = value.size(); + } else { + finalOffset = writeElement(value, res, 0); + } + checkState( + finalOffset == res.size(), + "Expected single element RLP encode to be of size %s but was of size %s.", + res.size(), + finalOffset); + return; + } + + int offset = 0; + int listIdx = 0; + for (int i = 0; i < values.size(); i++) { + final BytesValue value = values.get(i); + if (value == LIST_MARKER) { + final int payloadSize = payloadSizes[++listIdx]; + offset = writeListHeader(payloadSize, res, offset); + } else if (rlpEncoded.get(i)) { + value.copyTo(res, offset); + offset += value.size(); + } else { + offset = writeElement(value, res, offset); + } + } + + checkState( + offset == res.size(), + "Expected RLP encoding to be of size %s but was of size %s.", + res.size(), + offset); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInput.java new file mode 100755 index 00000000000..487ef709aee --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInput.java @@ -0,0 +1,60 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.math.BigInteger; + +/** An {@link RLPInput} that reads RLP encoded data from a {@link BytesValue}. */ +public class BytesValueRLPInput extends AbstractRLPInput { + + // The RLP encoded data. + private final BytesValue value; + + public BytesValueRLPInput(final BytesValue value, final boolean lenient) { + super(lenient); + this.value = value; + init(value.size(), true); + } + + @Override + protected byte inputByte(final long offset) { + return value.get(Math.toIntExact(offset)); + } + + @Override + protected BytesValue inputSlice(final long offset, final int length) { + return value.slice(Math.toIntExact(offset), length); + } + + @Override + protected Bytes32 inputSlice32(final long offset) { + return Bytes32.wrap(value, Math.toIntExact(offset)); + } + + @Override + protected String inputHex(final long offset, final int length) { + return value.slice(Math.toIntExact(offset), length).toString().substring(2); + } + + @Override + protected BigInteger getUnsignedBigInteger(final long offset, final int length) { + return BytesValues.asUnsignedBigInteger(value.slice(Math.toIntExact(offset), length)); + } + + @Override + protected int getInt(final long offset) { + return value.getInt(Math.toIntExact(offset)); + } + + @Override + protected long getLong(final long offset) { + return value.getLong(Math.toIntExact(offset)); + } + + @Override + public BytesValue raw() { + return value; + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutput.java new file mode 100755 index 00000000000..ae185e06f14 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutput.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +/** An {@link RLPOutput} that writes RLP encoded data to a {@link BytesValue}. */ +public class BytesValueRLPOutput extends AbstractRLPOutput { + /** + * Computes the final encoded data. + * + * @return A value containing the data written to this output RLP-encoded. + */ + public BytesValue encoded() { + final int size = encodedSize(); + if (size == 0) { + return BytesValue.EMPTY; + } + + final MutableBytesValue output = MutableBytesValue.create(size); + writeEncoded(output); + return output; + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/CorruptedRLPInputException.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/CorruptedRLPInputException.java new file mode 100755 index 00000000000..7889cd104ba --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/CorruptedRLPInputException.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.rlp; + +/** Exception thrown if an RLP input is corrupted and cannot be decoded properly. */ +public class CorruptedRLPInputException extends RLPException { + CorruptedRLPInputException(final String message) { + super(message); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/FileRLPInput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/FileRLPInput.java new file mode 100755 index 00000000000..5a94e432e66 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/FileRLPInput.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** An {@link RLPInput} that reads RLP encoded data from a {@link File}. */ +public class FileRLPInput extends AbstractRLPInput { + + // The RLP encoded data. + private final FileChannel file; + + public FileRLPInput(final FileChannel file, final boolean lenient) throws IOException { + super(lenient); + checkNotNull(file); + checkArgument(file.isOpen()); + this.file = file; + + init(file.size(), false); + } + + @Override + protected byte inputByte(final long offset) { + try { + final ByteBuffer buf = ByteBuffer.wrap(new byte[1]); + + file.read(buf, offset); + final byte b = buf.get(0); + return b; + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected BytesValue inputSlice(final long offset, final int length) { + try { + final byte[] bytes = new byte[length]; + final ByteBuffer buf = ByteBuffer.wrap(bytes); + file.read(buf, offset); + return BytesValue.of(bytes); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected Bytes32 inputSlice32(final long offset) { + return Bytes32.wrap(inputSlice(offset, 32), 0); + } + + @Override + protected String inputHex(final long offset, final int length) { + return inputSlice(offset, length).toString().substring(2); + } + + @Override + protected BigInteger getUnsignedBigInteger(final long offset, final int length) { + return BytesValues.asUnsignedBigInteger(inputSlice(offset, length)); + } + + @Override + protected int getInt(final long offset) { + return inputSlice(offset, Integer.BYTES).getInt(0); + } + + @Override + protected long getLong(final long offset) { + return inputSlice(offset, Long.BYTES).getLong(0); + } + + @Override + public BytesValue raw() { + throw new UnsupportedOperationException("raw() not supported on a Channel"); + } + + /** @return Offset of the current item */ + public long currentOffset() { + return currentItem; + } + + @Override + public void setTo(final long item) { + super.setTo(item); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/MalformedRLPInputException.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/MalformedRLPInputException.java new file mode 100755 index 00000000000..37c27df3091 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/MalformedRLPInputException.java @@ -0,0 +1,11 @@ +package net.consensys.pantheon.ethereum.rlp; + +/** + * Exception thrown if an RLP input is strictly malformed, but in such a way that can be processed + * by a lenient RLP decoder. + */ +public class MalformedRLPInputException extends RLPException { + MalformedRLPInputException(final String message) { + super(message); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLP.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLP.java new file mode 100755 index 00000000000..0fdf8084315 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLP.java @@ -0,0 +1,263 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static java.lang.String.format; +import static net.consensys.pantheon.ethereum.rlp.RLPDecodingHelpers.extractSize; +import static net.consensys.pantheon.ethereum.rlp.RLPEncodingHelpers.elementSize; +import static net.consensys.pantheon.ethereum.rlp.RLPEncodingHelpers.isSingleRLPByte; +import static net.consensys.pantheon.ethereum.rlp.RLPEncodingHelpers.writeElement; + +import net.consensys.pantheon.ethereum.rlp.RLPDecodingHelpers.Kind; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import io.vertx.core.buffer.Buffer; + +/** Static methods to work with RLP encoding/decoding. */ +public abstract class RLP { + private RLP() {} + + /** The RLP encoding of a single empty value, also known as RLP null. */ + public static final BytesValue NULL = encodeOne(BytesValue.EMPTY); + + public static final BytesValue EMPTY_LIST; + + static { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.endList(); + EMPTY_LIST = out.encoded(); + } + + /** + * Creates a new {@link RLPInput} suitable for decoding the provided RLP encoded value. + * + *

The created input is strict, in that exceptions will be thrown for any malformed input, + * either by this method or by future reads from the returned input. + * + * @param encoded The RLP encoded data for which to create a {@link RLPInput}. + * @return A newly created {@link RLPInput} to decode {@code encoded}. + * @throws MalformedRLPInputException if {@code encoded} doesn't contain a single RLP encoded item + * (item that can be a list itself). Note that more deeply nested corruption/malformation of + * the input will not be detected by this method call, but will be later when the input is + * read. + */ + public static RLPInput input(final BytesValue encoded) { + return new BytesValueRLPInput(encoded, false); + } + + /** + * Creates a new {@link RLPInput} suitable for decoding an RLP value encoded in the provided + * Vert.x {@link Buffer}. + * + *

The created input is strict, in that exceptions will be thrown for any malformed input, + * either by this method or by future reads from the returned input. + * + * @param buffer A buffer containing the RLP encoded data to decode. + * @param offset The offset in {@code encoded} at which the data to decode starts. + * @return A newly created {@link RLPInput} to decode RLP data in {@code encoded} from {@code + * offset}. + * @throws MalformedRLPInputException if {@code encoded} doesn't contain a properly encoded RLP + * item. Note that this only detect malformation on the main item at {@code offset}, but more + * deeply nested corruption/malformation of the input will not be detected by this method + * call, but only later when the input is read. + */ + public static VertxBufferRLPInput input(final Buffer buffer, final int offset) { + return new VertxBufferRLPInput(buffer, offset, false); + } + + /** + * Fully decodes a RLP encoded value. + * + *

This method is mostly intended for testing as it is often more convenient and + * efficient to use a {@link RLPInput} (through {@link #input(BytesValue)}) instead. + * + * @param value The RLP encoded value to decode. + * @return The output of decoding {@code value}. It will be either directly a {@link BytesValue}, + * or a list whose elements are either {@link BytesValue}, or similarly composed sub-lists. + * @throws RLPException if {@code value} is not a properly formed RLP encoding. + */ + public static Object decode(final BytesValue value) { + return decode(input(value)); + } + + private static Object decode(final RLPInput in) { + if (!in.nextIsList()) { + return in.readBytesValue(); + } + + final int size = in.enterList(); + final List l = new ArrayList<>(size); + for (int i = 0; i < size; i++) l.add(decode(in)); + in.leaveList(); + return l; + } + + /** + * Fully RLP encode an object consisting of recursive lists of {@link BytesValue}. + * + *

This method is mostly intended for testing as it is often more convenient and + * efficient to use a {@link RLPOutput} (through {@link #encode(Consumer)} for instance) instead. + * + * @param obj An object that must be either directly a {@link BytesValue}, or a list whose + * elements are either {@link BytesValue}, or similarly composed sub-lists. + * @return The RLP encoding corresponding to {@code obj}. + * @throws IllegalArgumentException if {@code obj} is not a valid input (not entirely composed + * from lists and {@link BytesValue}). + */ + public static BytesValue encode(final Object obj) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + encode(obj, out); + return out.encoded(); + } + + private static void encode(final Object obj, final RLPOutput out) { + if (obj instanceof BytesValue) { + out.writeBytesValue((BytesValue) obj); + } else if (obj instanceof List) { + final List l = (List) obj; + out.startList(); + for (final Object o : l) encode(o, out); + out.endList(); + } else { + throw new IllegalArgumentException( + format("Invalid input type %s for RLP encoding", obj.getClass())); + } + } + + /** + * Creates a {@link RLPOutput}, pass it to the provided consumer for writing, and then return the + * RLP encoded result of that writing. + * + *

This method is a convenience method that is mostly meant for use with class that have a + * method to write to an {@link RLPOutput}. For instance: + * + *

{@code
+   * class Foo {
+   *   public void writeTo(RLPOutput out) {
+   *     //... write some data to out ...
+   *   }
+   * }
+   *
+   * Foo f = ...;
+   * // RLP encode f
+   * BytesValue encoded = RLPs.encode(f::writeTo);
+   * }
+ * + * @param writer A method that given an {@link RLPOutput}, writes some data to it. + * @return The RLP encoding of the data written by {@code writer}. + */ + public static BytesValue encode(final Consumer writer) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + writer.accept(out); + return out.encoded(); + } + + /** + * Encodes a single binary value into RLP. + * + *

This is equivalent (but possibly more efficient) to: + * + *

+   * {
+   *   @code
+   *   BytesValueRLPOutput out = new BytesValueRLPOutput();
+   *   out.writeBytesValue(value);
+   *   return out.encoded();
+   * }
+   * 
+ * + * So note in particular that the value is encoded as is (and so not as a scalar in particular). + * + * @param value The value to encode. + * @return The RLP encoding containing only {@code value}. + */ + public static BytesValue encodeOne(final BytesValue value) { + if (isSingleRLPByte(value)) return value; + + final MutableBytesValue res = MutableBytesValue.create(elementSize(value)); + writeElement(value, res, 0); + return res; + } + + /** + * Decodes an RLP-encoded value assuming it contains a single non-list item. + * + *

This is equivalent (but possibly more efficient) to: + * + *

{@code
+   * return input(value).readBytesValue();
+   * }
+ * + * So note in particular that the value is decoded as is (and so not as a scalar in particular). + * + * @param encodedValue The encoded RLP value. + * @return The single value encoded in {@code encodedValue}. + * @throws RLPException if {@code encodedValue} is not a valid RLP encoding or if it does not + * contains a single non-list item. + */ + public static BytesValue decodeOne(final BytesValue encodedValue) { + if (encodedValue.size() == 0) { + throw new RLPException("Invalid empty input for RLP decoding"); + } + + final int prefix = encodedValue.get(0) & 0xFF; + final Kind kind = Kind.of(prefix); + if (kind.isList()) { + throw new RLPException(format("Invalid input: value %s is an RLP list", encodedValue)); + } + + if (kind == Kind.BYTE_ELEMENT) { + return encodedValue; + } + + int offset; + int size; + if (kind == Kind.SHORT_ELEMENT) { + offset = 1; + size = prefix - 0x80; + } else { + final int sizeLength = prefix - 0xb7; + if (1 + sizeLength > encodedValue.size()) { + throw new RLPException( + format( + "Malformed RLP input: not enough bytes to read size of " + + "long item in %s: expected %d bytes but only %d", + encodedValue, sizeLength + 1, encodedValue.size())); + } + offset = 1 + sizeLength; + size = extractSize(encodedValue::get, 1, sizeLength); + } + if (offset + size != encodedValue.size()) { + throw new RLPException( + format( + "Malformed RLP input: %s should be of size %d according to " + + "prefix byte but of size %d", + encodedValue, offset + size, encodedValue.size())); + } + return encodedValue.slice(offset, size); + } + + /** + * Validates that the provided value is a valid RLP encoding. + * + * @param encodedValue The value to check. + * @throws RLPException if {@code encodedValue} is not a valid RLP encoding. + */ + public static void validate(final BytesValue encodedValue) { + final RLPInput in = input(encodedValue); + while (!in.isDone()) { + if (in.nextIsList()) { + in.enterList(); + } else if (in.isEndOfCurrentList()) { + in.leaveList(); + } else { + // Skip does as much validation as can be done in general, without allocating anything. + in.skipNext(); + } + } + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPDecodingHelpers.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPDecodingHelpers.java new file mode 100755 index 00000000000..e253ae3f05d --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPDecodingHelpers.java @@ -0,0 +1,73 @@ +package net.consensys.pantheon.ethereum.rlp; + +import java.util.function.IntUnaryOperator; +import java.util.function.LongUnaryOperator; + +/** + * Helper static methods to facilitate RLP decoding within this package. Neither this class + * nor any of its method are meant to be exposed publicly, they are too low level. + */ +class RLPDecodingHelpers { + + /** The kind of items an RLP item can be. */ + enum Kind { + BYTE_ELEMENT, + SHORT_ELEMENT, + LONG_ELEMENT, + SHORT_LIST, + LONG_LIST; + + static Kind of(final int prefix) { + if (prefix <= 0x7F) { + return Kind.BYTE_ELEMENT; + } else if (prefix <= 0xb7) { + return Kind.SHORT_ELEMENT; + } else if (prefix <= 0xbf) { + return Kind.LONG_ELEMENT; + } else if (prefix <= 0xf7) { + return Kind.SHORT_LIST; + } else { + return Kind.LONG_LIST; + } + } + + boolean isList() { + switch (this) { + case SHORT_LIST: + case LONG_LIST: + return true; + default: + return false; + } + } + } + + /** Read from the provided offset a size of the provided length, assuming this is enough bytes. */ + static int extractSize(final IntUnaryOperator getter, final int offset, final int sizeLength) { + int res = 0; + int shift = 0; + for (int i = 0; i < sizeLength; i++) { + res |= (getter.applyAsInt(offset + (sizeLength - 1) - i) & 0xFF) << shift; + shift += 8; + } + return res; + } + + /** Read from the provided offset a size of the provided length, assuming this is enough bytes. */ + static int extractSizeFromLong( + final LongUnaryOperator getter, final long offset, final int sizeLength) { + long res = 0; + int shift = 0; + for (int i = 0; i < sizeLength; i++) { + res |= (getter.applyAsLong(offset + (sizeLength - 1) - i) & 0xFF) << shift; + shift += 8; + } + try { + return Math.toIntExact(res); + } catch (final ArithmeticException e) { + final String msg = + "unable to extract size from long at offset " + offset + ", sizeLen=" + sizeLength; + throw new RLPException(msg, e); + } + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPEncodingHelpers.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPEncodingHelpers.java new file mode 100755 index 00000000000..7dc97079d69 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPEncodingHelpers.java @@ -0,0 +1,94 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +/** + * Helper static methods to facilitate RLP encoding within this package. Neither this class + * nor any of its method are meant to be exposed publicly, they are too low level. + */ +class RLPEncodingHelpers { + private RLPEncodingHelpers() {} + + static boolean isSingleRLPByte(final BytesValue value) { + return value.size() == 1 && value.get(0) >= 0; + } + + static boolean isShortElement(final BytesValue value) { + return value.size() <= 55; + } + + static boolean isShortList(final int payloadSize) { + return payloadSize <= 55; + } + + /** The encoded size of the provided value. */ + static int elementSize(final BytesValue value) { + if (isSingleRLPByte(value)) return 1; + + if (isShortElement(value)) return 1 + value.size(); + + return 1 + sizeLength(value.size()) + value.size(); + } + + /** The encoded size of a list given the encoded size of its payload. */ + static int listSize(final int payloadSize) { + int size = 1 + payloadSize; + if (!isShortList(payloadSize)) size += sizeLength(payloadSize); + return size; + } + + /** + * Writes the result of encoding the provided value to the provided destination (which must be big + * enough). + */ + static int writeElement( + final BytesValue value, final MutableBytesValue dest, final int destOffset) { + final int size = value.size(); + if (isSingleRLPByte(value)) { + dest.set(destOffset, value.get(0)); + return destOffset + 1; + } + + if (isShortElement(value)) { + dest.set(destOffset, (byte) (0x80 + size)); + value.copyTo(dest, destOffset + 1); + return destOffset + 1 + size; + } + + final int offset = writeLongMetadata(0xb7, size, dest, destOffset); + value.copyTo(dest, offset); + return offset + size; + } + + /** + * Writes the encoded header of a list provided its encoded payload size to the provided + * destination (which must be big enough). + */ + static int writeListHeader( + final int payloadSize, final MutableBytesValue dest, final int destOffset) { + if (isShortList(payloadSize)) { + dest.set(destOffset, (byte) (0xc0 + payloadSize)); + return destOffset + 1; + } + + return writeLongMetadata(0xf7, payloadSize, dest, destOffset); + } + + private static int writeLongMetadata( + final int baseCode, final int size, final MutableBytesValue dest, final int destOffset) { + final int sizeLength = sizeLength(size); + dest.set(destOffset, (byte) (baseCode + sizeLength)); + int shift = 0; + for (int i = 0; i < sizeLength; i++) { + dest.set(destOffset + sizeLength - i, (byte) (size >> shift)); + shift += 8; + } + return destOffset + 1 + sizeLength; + } + + private static int sizeLength(final int size) { + final int zeros = Integer.numberOfLeadingZeros(size); + return 4 - (zeros / 8); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPException.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPException.java new file mode 100755 index 00000000000..5b264e2ff2e --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPException.java @@ -0,0 +1,11 @@ +package net.consensys.pantheon.ethereum.rlp; + +public class RLPException extends RuntimeException { + public RLPException(final String message) { + this(message, null); + } + + RLPException(final String message, final Throwable throwable) { + super(message, throwable); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPInput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPInput.java new file mode 100755 index 00000000000..26deb73ede6 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPInput.java @@ -0,0 +1,346 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; +import net.consensys.pantheon.util.uint.UInt256Value; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * An input used to decode data in RLP encoding. + * + *

An RLP "value" is fundamentally an {@code Item} defined the following way: + * + *

+ *   Item ::= List | Bytes
+ *   List ::= [ Item, ... , Item ]
+ *   Bytes ::= a binary value (comprised of an arbitrary number of bytes).
+ * 
+ * + * In other words, RLP encodes binary data organized in arbitrary nested lists. + * + *

A {@link RLPInput} thus provides methods to decode both lists and binary values. A list in the + * input is "entered" by calling {@link #enterList()} and left by calling {@link #leaveList()}. + * Binary values can be read directly with {@link #readBytesValue()} ()}, but the {@link RLPInput} + * interface provides a wealth of convenience methods to read specific types of data that are in + * specific encoding. + * + *

Amongst the methods to read binary data, some methods are provided to read "scalar". A scalar + * should simply be understood as a positive integer that is encoded with no leading zeros. In other + * word, a method like {@link #readLongScalar()} does not expect an encoded value of exactly 8 bytes + * (by opposition to {@link #readLong}), but rather one that is "up to" 8 bytes. + * + * @see BytesValueRLPInput for a {@link RLPInput} that decode an RLP encoded value stored in a + * {@link BytesValue}. + */ +public interface RLPInput { + + /** + * Whether the input has been already fully decoded (has no more data to read). + * + * @return {@code false} if the input has more data to read, {@code true} otherwise. + */ + boolean isDone(); + + /** + * Whether the next element to read from this input is a list. + * + * @return {@code true} if the input is not done and the next item to read is a list. + */ + boolean nextIsList(); + + /** + * Whether the next element to read from this input is an RLP "null" (that is, {@link + * BytesValue#EMPTY}). + * + * @return {@code true} if the input is not done and the next item to read is an empty value. + */ + boolean nextIsNull(); + + /** + * Returns the payload size of the next item + * + * @return the payload size of the next item + */ + int nextSize(); + + /** + * Whether the input is at the end of a currently entered list, that is if {@link #leaveList()} + * should be the next method called. + * + * @return Whether all elements of the current list have been read but said list haven't been + * "left" yet. + */ + boolean isEndOfCurrentList(); + + /** + * Skips the next item to read in the input. + * + *

Note that if the next item is a list, the whole list is skipped. + */ + void skipNext(); + + /** + * If the next item to read is a list, enter that list, placing the input on the first item of + * that list. + * + * @return The number of item of the entered list. + * @throws RLPException if the next item to read from this input is not a list, or the input is + * corrupted. + */ + int enterList(); + + /** + * Exits the current list after all its items have been consumed. + * + *

This method is equivalent to calling {@link #leaveList(boolean)} with value false. + * + *

Note that this method technically doesn't consume any input but must be called after having + * read the last element of a list. This allow to ensure the structure of the input is indeed the + * one expected. + * + * @throws RLPException if the current list is not finished (it has more items). + */ + void leaveList(); + + /** + * Exits the current list, allowing the caller to ignore any remaining unconsumed elements. + * + *

Note that this method technically doesn't consume any input but must be called after having + * read the last element of a list. This allow to ensure the structure of the input is indeed the + * one expected. + * + * @param ignoreRest Whether to ignore any remaining elements in the list. If elements remain and + * this parameter is false, an exception will be thrown. + * @throws RLPException if the current list is not finished (it has more items), if + * ignoreRest is false. + */ + void leaveList(boolean ignoreRest); + + /** + * Reads a scalar from the input and return is as a long value. + * + * @return The next scalar item of this input as a long value. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to + * fit a long or has leading zeros. + */ + long readLongScalar(); + + /** + * Reads a scalar from the input and return is as an int value. + * + * @return The next scalar item of this input as an int value. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to + * fit a long or has leading zeros. + */ + int readIntScalar(); + + /** + * Reads a scalar from the input and return is as a {@link BigInteger}. + * + * @return The next scalar item of this input as a {@link BigInteger}. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item has leading zeros. + */ + BigInteger readBigIntegerScalar(); + + /** + * Reads a scalar from the input and return is as a {@link UInt256}. + * + * @return The next scalar item of this input as a {@link UInt256}. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to + * fit a {@link UInt256} or has leading zeros. + */ + UInt256 readUInt256Scalar(); + + /** + * Reads a scalar of maximum 32 bytes from the input and pass it to the provided value to create a + * corresponding {@link UInt256Value}. + * + *

Note that for convenience, any exception thrown by the provided method will be wrapped in a + * {@link RLPException} (it is considered as a "decoding error"). + * + * @param bytesWrapper A function that provided a 32 bytes value creates a specific 32 bytes + * unsigned integer value. + * @param Type of the value created, which must be 32 bytes unsigned integer variant. + * @return The value created from applying {@code bytesWrapper} to the scalar read from that input + * (eventually padded to fit 32 bytes). + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is either too big to + * fit a {@link UInt256} or has leading zeros. + */ + > T readUInt256Scalar(Function bytesWrapper); + + /** + * Reads the next item of this input (which must be exactly 1 byte) as a byte. + * + * @return The byte corresponding to the next item of this input. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is not a single byte + * long. + */ + byte readByte(); + + /** + * Reads the next item of this input (which must be exactly 2-bytes) as a (signed) short. + * + * @return The short corresponding to the next item of this input. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is not 2-bytes. + */ + short readShort(); + + /** + * Reads the next item of this input (which must be exactly 4-bytes) as a (signed) int. + * + * @return The int corresponding to the next item of this input. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is not 4-bytes. + */ + int readInt(); + + /** + * Reads the next item of this input (which must be exactly 8-bytes) as a (signed) long. + * + * @return The long corresponding to the next item of this input. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is not 8-bytes. + */ + long readLong(); + + /** + * Reads the next item of this input (which must be exactly 1 byte) as an unsigned byte. + * + * @return The value of the next item interpreted as an unsigned byte. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is not a single byte + * long. + */ + default int readUnsignedByte() { + return readByte() & 0xFF; + } + + /** + * Reads the next item of this input (which must be exactly 2-bytes) as an unsigned short. + * + * @return The value of the next item interpreted as an unsigned short. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is not 2-bytes. + */ + default int readUnsignedShort() { + return readShort() & 0xFFFF; + } + + /** + * Reads the next item of this input (which must be exactly 4-bytes) as an unsigned int. + * + * @return The value of the next item interpreted as an unsigned int. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is not 4-bytes. + */ + default long readUnsignedInt() { + return (readInt()) & 0xFFFFFFFFL; + } + + /** + * Reads an inet address from this input. + * + * @return The inet address corresponding to the next item of this input. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or if the next item is neither 4 nor 16 + * bytes. + */ + InetAddress readInetAddress(); + + /** + * Reads the next item of this input (assuming it is not a list). + * + * @return The next item read of this input. + * @throws RLPException if the next item to read is a list or the input is at the end of its + * current list (and {@link #leaveList()} hasn't been called). + */ + BytesValue readBytesValue(); + + /** + * Reads the next item of this input (assuming it is not a list) that must be exact 32 bytes. + * + * @return The next item read of this input. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or the next element is not exactly 32 + * bytes. + */ + Bytes32 readBytes32(); + + /** + * Reads the next iterm of this input (assuming it is not a list) and transform it with the + * provided mapping function. + * + *

Note that the only benefit of this method over calling the mapper function on the result of + * {@link #readBytesValue()} is that any error thrown by the mapper will be wrapped by a {@link + * RLPException}, which can make error handling more convenient (having a particular decoded value + * not match what is expected is not fundamentally different from trying to read an unsigned short + * from an item with strictly more or less than 2 bytes). + * + * @param mapper The mapper to apply to the read value. + * @param The type of the result. + * @return The next item read from this input, mapped through {@code mapper}. + * @throws RLPException if the next item to read is a list, the input is at the end of its current + * list (and {@link #leaveList()} hasn't been called) or {@code mapper} throws an exception + * when applied. + */ + T readBytesValue(Function mapper); + + /** + * Returns the current element as a standalone RLP element. + * + *

This method is useful to extract self-contained RLP elements from a list, so they can be + * processed individually later. + * + * @return The current element as a standalone RLP input element. + */ + RLPInput readAsRlp(); + + /** + * Returns a raw {@link BytesValue} representation of this RLP. + * + * @return The raw RLP. + */ + BytesValue raw(); + + /** Resets this RLP input to the start. */ + void reset(); + + /** + * Reads a full list from the input given a method that knows how to read its elements. + * + * @param valueReader A method that can decode a single list element. + * @param The type of the elements of the decoded list. + * @return The next list of this input, where elements are decoded using {@code valueReader}. + * @throws RLPException is the next item to read is not a list, of if any error happens when + * applying {@code valueReader} to read elements of the list. + */ + default List readList(final Function valueReader) { + final int size = enterList(); + final List res = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + try { + res.add(valueReader.apply(this)); + } catch (final Exception e) { + throw new RLPException( + String.format( + "Error applying element decoding function on " + "element %d of the list", i), + e); + } + } + leaveList(); + return res; + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPOutput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPOutput.java new file mode 100755 index 00000000000..b4e874484bb --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RLPOutput.java @@ -0,0 +1,269 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; +import net.consensys.pantheon.util.bytes.MutableBytesValue; +import net.consensys.pantheon.util.uint.UInt256Value; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.util.Collection; +import java.util.function.BiConsumer; + +/** + * An output used to encode data in RLP encoding. + * + *

An RLP "value" is fundamentally an {@code Item} defined the following way: + * + *

+ *   Item ::= List | Bytes
+ *   List ::= [ Item, ... , Item ]
+ *   Bytes ::= a binary value (comprised of an arbitrary number of bytes).
+ * 
+ * + * In other words, RLP encodes binary data organized in arbitrary nested lists. + * + *

A {@link RLPOutput} thus provides methods to write both lists and binary values. A list is + * started by calling {@link #startList()} and ended by {@link #endList()}. Lists can be nested in + * other lists in arbitrary ways. Binary values can be written directly with {@link + * #writeBytesValue(BytesValue)}, but the {@link RLPOutput} interface provides a wealth of + * convenience methods to write specific types of data with a specific encoding. + * + *

Amongst the methods to write binary data, some methods are provided to write "scalar". A + * scalar should simply be understood as a positive integer that is encoded with no leading zeros. + * In other word, if an integer is written with a "Scalar" method variant, that number will be + * encoded with the minimum number of bytes necessary to represent it. + * + *

The {@link RLPOutput} only defines the interface for writing data meant to be RLP encoded. + * Getting the finally encoded output will depend on the concrete implementation, see {@link + * BytesValueRLPOutput} for instance. + */ +public interface RLPOutput { + + /** Starts a new list. */ + void startList(); + + /** + * Ends the current list. + * + * @throws IllegalStateException if no list has been previously started with {@link #startList()} + * (or any started had already be ended). + */ + void endList(); + + /** + * Writes a new value. + * + * @param v The value to write. + */ + void writeBytesValue(BytesValue v); + + /** + * Writes a RLP "null", that is an empty value. + * + *

This is a shortcut for {@code writeBytesValue(BytesValue.EMPTY)}. + */ + default void writeNull() { + writeBytesValue(BytesValue.EMPTY); + } + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + * @throws IllegalArgumentException if {@code v < 0}. + */ + default void writeIntScalar(final int v) { + writeLongScalar(v); + } + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + * @throws IllegalArgumentException if {@code v < 0}. + */ + default void writeLongScalar(final long v) { + checkArgument(v >= 0, "Invalid negative value %s for scalar encoding", v); + writeBytesValue(BytesValues.toMinimalBytes(v)); + } + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + * @throws IllegalArgumentException if {@code v} is a negative integer ({@code v.signum() < 0}). + */ + default void writeBigIntegerScalar(final BigInteger v) { + checkArgument(v.signum() >= 0, "Invalid negative integer %s for scalar encoding", v); + + final byte[] bytes = v.toByteArray(); + // BigInteger will not include leading zeros by contract, but it always include at least one + // bit of sign (a zero here since it's positive). What that mean is that if the first 1 of the + // resulting number is exactly on a byte boundary, then the sign bit constraint will make the + // value include one extra byte, which will be zero. In other words, they can be one zero bytes + // in practice we should ignore, but there should never be more than one. + writeBytesValue( + bytes.length > 1 && bytes[0] == 0 + ? BytesValue.wrap(bytes, 1, bytes.length - 1) + : BytesValue.wrap(bytes)); + } + + /** + * Writes a scalar (encoded with no leading zeroes). + * + * @param v The scalar to write. + */ + default void writeUInt256Scalar(final UInt256Value v) { + writeBytesValue(BytesValues.trimLeadingZeros(v.getBytes())); + } + + /** + * Writes a single byte value. + * + * @param b The byte to write. + */ + default void writeByte(final byte b) { + writeBytesValue(BytesValue.of(b)); + } + + /** + * Writes a 2-bytes value. + * + *

Note that this is not a "scalar" write: the value will be encoded with exactly 2 bytes. + * + * @param s The 2-bytes short to write. + */ + default void writeShort(final short s) { + final byte[] res = new byte[2]; + res[0] = (byte) (s >> 8); + res[1] = (byte) s; + writeBytesValue(BytesValue.wrap(res)); + } + + /** + * Writes a 4-bytes value. + * + *

Note that this is not a "scalar" write: the value will be encoded with exactly 4 bytes. + * + * @param i The 4-bytes int to write. + */ + default void writeInt(final int i) { + final MutableBytesValue v = MutableBytesValue.create(4); + v.setInt(0, i); + writeBytesValue(v); + } + + /** + * Writes a 8-bytes value. + * + *

Note that this is not a "scalar" write: the value will be encoded with exactly 8 bytes. + * + * @param l The 8-bytes long to write. + */ + default void writeLong(final long l) { + final MutableBytesValue v = MutableBytesValue.create(8); + v.setLong(0, l); + writeBytesValue(v); + } + + /** + * Writes a single byte value. + * + * @param b A value that must fit an unsigned byte. + * @throws IllegalArgumentException if {@code b} does not fit an unsigned byte, that is if either + * {@code b < 0} or {@code b > 0xFF}. + */ + default void writeUnsignedByte(final int b) { + writeBytesValue(BytesValues.ofUnsignedByte(b)); + } + + /** + * Writes a 2-bytes value. + * + * @param s A value that must fit an unsigned 2-bytes short. + * @throws IllegalArgumentException if {@code s} does not fit an unsigned 2-bytes short, that is + * if either {@code s < 0} or {@code s > 0xFFFF}. + */ + default void writeUnsignedShort(final int s) { + writeBytesValue(BytesValues.ofUnsignedShort(s)); + } + + /** + * Writes a 4-bytes value. + * + * @param i A value that must fit an unsigned 4-bytes integer. + * @throws IllegalArgumentException if {@code i} does not fit an unsigned 4-bytes int, that is if + * either {@code i < 0} or {@code i > 0xFFFFFFFFL}. + */ + default void writeUnsignedInt(final long i) { + writeBytesValue(BytesValues.ofUnsignedInt(i)); + } + + /** + * Writes the byte representation of an inet address (so either 4 or 16 bytes long). + * + * @param address The address to write. + */ + default void writeInetAddress(final InetAddress address) { + writeBytesValue(BytesValue.wrap(address.getAddress())); + } + + /** + * Writes a list of values of a specific class provided a function to write values of that class + * to an {@link RLPOutput}. + * + *

This is a convenience method whose result is equivalent to doing: + * + *

{@code
+   * startList();
+   * for (T v : values) {
+   *   valueWriter.accept(v, this);
+   * }
+   * endList();
+   * }
+ * + * @param values A list of value of type {@code T}. + * @param valueWriter A method that given a value of type {@code T} and an {@link RLPOutput}, + * writes this value to the output. + * @param The type of values to write. + */ + default void writeList( + final Collection values, final BiConsumer valueWriter) { + startList(); + for (final T v : values) { + valueWriter.accept(v, this); + } + endList(); + } + + /** + * Writes an already RLP encoded item to the output. + * + *

This method is the functional equivalent of decoding the provided value entirely (to an + * fully formed Java object) and then re-encoding that result to this output. It is however a lot + * more efficient in that it saves most of that decoding/re-encoding work. Please note however + * that this method does validate that the input is a valid RLP encoding. If you can + * guaranteed that the input is valid and do not want this validation step, please have a look at + * {@link #writeRLPUnsafe(BytesValue)}. + * + * @param rlpEncodedValue An already RLP encoded value to write as next item of this output. + */ + default void writeRLP(final BytesValue rlpEncodedValue) { + RLP.validate(rlpEncodedValue); + writeRLPUnsafe(rlpEncodedValue); + } + + /** + * Writes an already RLP encoded item to the output. + * + *

This method is equivalent to {@link #writeRLP(BytesValue)}, but is unsafe in that it does + * not do any validation of the its input. As such, it is faster but can silently yield invalid + * RLP output if misused. + * + * @param rlpEncodedValue An already RLP encoded value to write as next item of this output. + */ + void writeRLPUnsafe(BytesValue rlpEncodedValue); +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RlpUtils.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RlpUtils.java new file mode 100755 index 00000000000..8e6dc356c00 --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/RlpUtils.java @@ -0,0 +1,132 @@ +package net.consensys.pantheon.ethereum.rlp; + +import java.nio.ByteBuffer; + +public final class RlpUtils { + + public static final int RLP_ZERO = 0x80; + + private RlpUtils() { + // Utility Class + } + + public static int encode(final byte[] buffer, final long value) { + if (value == 0L) { + buffer[0] = (byte) RLP_ZERO; + return 1; + } else { + final int resultBytes = 8 - Long.numberOfLeadingZeros(value) / 8; + int shift = 0; + for (int i = 0; i < resultBytes; i++) { + buffer[resultBytes - i - 1] = (byte) (value >> shift & 0xFF); + shift += 8; + } + return resultBytes; + } + } + + /** + * Decodes the offset of an RLP encoded element. + * + * @param buffer Buffer that has an RLP element starting at index {@code start} + * @param start the index into the bytebuffer from which to start decoding. + * @return Length of the RLP element + */ + public static int decodeOffset(final ByteBuffer buffer, final int start) { + int len = 0; + final int offset; + final int first = buffer.get(start) & 0xff; + if (first <= 0x7f) { + offset = 0; + } else if (first <= 0xb7) { + offset = 1; + } else if (first <= 0xbf) { + final int lenOfLen = first - 0xb7; + for (int i = 0; i < lenOfLen; ++i) { + len = (len << 8) + (buffer.get(start + 1 + i) & 0xff); + } + offset = 1 + lenOfLen; + } else if (first < 0xf8) { + offset = 1; + } else { + final int lenOfLen = first - 0xf7; + for (int i = 0; i < lenOfLen; ++i) { + len = (len << 8) + (buffer.get(start + 1 + i) & 0xff); + } + offset = 1 + lenOfLen; + } + return offset; + } + + /** + * Decodes the offset of an RLP encoded element. TODO: Don't wrap in buffer, use array directly + * + * @param buffer Buffer that has an RLP element starting at index {@code start} + * @param start the index into buffer from which to start decoding. + * @return Length of the RLP element + */ + public static int decodeOffset(final byte[] buffer, final int start) { + return decodeOffset(ByteBuffer.wrap(buffer), start); + } + + /** + * Decodes the length of an RLP encoded element starting at the beginning of the given buffer. + * + * @param buffer Buffer that has an RLP element starting at index {@code start} + * @param start the index into buffer from which to determine the length of an RLP element. + * @return Length of the RLP element + */ + public static int decodeLength(final ByteBuffer buffer, final int start) { + int len = 0; + final int offset; + final int first = buffer.get(start) & 0xff; + if (first <= 0x7f) { + len = 1; + offset = 0; + } else if (first <= 0xb7) { + len = first - RLP_ZERO; + offset = 1; + } else if (first <= 0xbf) { + final int lenOfLen = first - 0xb7; + for (int i = 0; i < lenOfLen; ++i) { + len = (len << 8) + (buffer.get(start + 1 + i) & 0xff); + } + offset = 1 + lenOfLen; + } else if (first < 0xf8) { + len = first - 0xc0; + offset = 1; + } else { + final int lenOfLen = first - 0xf7; + for (int i = 0; i < lenOfLen; ++i) { + len = (len << 8) + (buffer.get(start + 1 + i) & 0xff); + } + offset = 1 + lenOfLen; + } + return len + offset; + } + + /** + * Decodes the length of an RLP encoded element starting at the beginning of the given buffer. + * + *

TODO: Don't wrap in buffer, use array directly + * + * @param buffer Buffer that has an RLP element starting at index {@code start} + * @param start the index into buffer from which to start decoding an RLP element length. + * @return Length of the RLP element + */ + public static int decodeLength(final byte[] buffer, final int start) { + return decodeLength(ByteBuffer.wrap(buffer), start); + } + + public static long readLong(final int offset, final int length, final byte[] buffer) { + long num = 0; + for (int i = decodeOffset(buffer, offset); i < length; ++i) { + num = (num << 8) + (buffer[offset + i] & 0xff); + } + return num; + } + + public static int nextOffset(final byte[] buffer, final int offset) { + return offset + decodeLength(buffer, offset); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPInput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPInput.java new file mode 100755 index 00000000000..fd4daa3ad3e --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPInput.java @@ -0,0 +1,84 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.math.BigInteger; + +import io.vertx.core.buffer.Buffer; + +/** A {@link RLPInput} that decode RLP encoded data stored in a Vert.x {@link Buffer}. */ +public class VertxBufferRLPInput extends AbstractRLPInput { + + // The RLP encoded data. + private final Buffer buffer; + // Offset in buffer from which to read. + private final int bufferOffset; + + /** + * A new {@link RLPInput} that decodes data from the provided buffer. + * + * @param buffer The buffer from which to read RLP data. + * @param bufferOffset The offset in {@code buffer} in which the data to decode starts. + * @param lenient Whether the created decoded should be lenient, that is ignore non-fatal + * malformation in the input. + */ + public VertxBufferRLPInput(final Buffer buffer, final int bufferOffset, final boolean lenient) { + super(lenient); + this.buffer = buffer; + this.bufferOffset = bufferOffset; + init(buffer.length(), false); + } + + /** + * The total size of the encoded data in the {@link Buffer} wrapped by this object. + * + * @return The total size of the encoded data that this input decodes (note that this value never + * changes, it is not the size of data remaining to decode, but the size to decode at creation + * time). + */ + public int encodedSize() { + return Math.toIntExact(size); + } + + @Override + protected byte inputByte(final long offset) { + return buffer.getByte(Math.toIntExact(bufferOffset + offset)); + } + + @Override + protected BytesValue inputSlice(final long offset, final int length) { + return BytesValue.wrapBuffer(buffer, Math.toIntExact(bufferOffset + offset), length); + } + + @Override + protected Bytes32 inputSlice32(final long offset) { + return Bytes32.wrap(inputSlice(offset, Bytes32.SIZE), 0); + } + + @Override + protected String inputHex(final long offset, final int length) { + return inputSlice(offset, length).toString().substring(2); + } + + @Override + protected BigInteger getUnsignedBigInteger(final long offset, final int length) { + return BytesValues.asUnsignedBigInteger(inputSlice(offset, length)); + } + + @Override + protected int getInt(final long offset) { + return buffer.getInt(Math.toIntExact(bufferOffset + offset)); + } + + @Override + protected long getLong(final long offset) { + return buffer.getLong(Math.toIntExact(bufferOffset + offset)); + } + + @Override + public BytesValue raw() { + return BytesValue.wrap(buffer.getBytes()); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPOutput.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPOutput.java new file mode 100755 index 00000000000..a3794744bab --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/VertxBufferRLPOutput.java @@ -0,0 +1,28 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import io.vertx.core.buffer.Buffer; + +/** + * A {@link RLPOutput} that writes/appends the result of RLP encoding to a Vert.x {@link Buffer}. + */ +public class VertxBufferRLPOutput extends AbstractRLPOutput { + /** + * Appends the RLP-encoded data written to this output to the provided Vert.x {@link Buffer}. + * + * @param buffer The buffer to which to append the data to. + */ + public void appendEncoded(final Buffer buffer) { + final int size = encodedSize(); + if (size == 0) { + return; + } + + // We want to append to the buffer, and Buffer always grows to accommodate anything writing, + // so we write the last byte we know we'll need to make it resize accordingly. + final int start = buffer.length(); + buffer.setByte(start + size - 1, (byte) 0); + writeEncoded(MutableBytesValue.wrapBuffer(buffer, start, size)); + } +} diff --git a/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/package-info.java b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/package-info.java new file mode 100755 index 00000000000..e751123a53e --- /dev/null +++ b/ethereum/rlp/src/main/java/net/consensys/pantheon/ethereum/rlp/package-info.java @@ -0,0 +1,12 @@ +/** + * Recursive Length Prefix (RLP) encoding and decoding. + * + *

This package provides encoding and decoding of data with the RLP encoding scheme. Encoding is + * done through writing data to a {@link net.consensys.pantheon.ethereum.rlp.RLPOutput} (for + * instance {@link net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput}, which then exposes the + * encoded output as a {@link net.consensys.pantheon.util.bytes.BytesValue} through {@link + * net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput#encoded()}). Decoding is done by wrapping + * encoded data in a {@link net.consensys.pantheon.ethereum.rlp.RLPInput} (using, for instance, + * {@link net.consensys.pantheon.ethereum.rlp.RLP#input}) and reading from it. + */ +package net.consensys.pantheon.ethereum.rlp; diff --git a/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java new file mode 100755 index 00000000000..9a178581f64 --- /dev/null +++ b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java @@ -0,0 +1,436 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class BytesValueRLPInputTest { + + private static BytesValue h(final String hex) { + return BytesValue.fromHexString(hex); + } + + private static String times(final String base, final int times) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < times; i++) sb.append(base); + return sb.toString(); + } + + @Test + public void empty() { + final RLPInput in = RLP.input(BytesValue.EMPTY); + assertTrue(in.isDone()); + } + + @Test + public void singleByte() { + final RLPInput in = RLP.input(h("0x01")); + assertFalse(in.isDone()); + assertEquals((byte) 1, in.readByte()); + assertTrue(in.isDone()); + } + + @Test + public void singleByteLowerBoundary() { + final RLPInput in = RLP.input(h("0x00")); + assertFalse(in.isDone()); + assertEquals((byte) 0, in.readByte()); + assertTrue(in.isDone()); + } + + @Test + public void singleByteUpperBoundary() { + final RLPInput in = RLP.input(h("0x7f")); + assertFalse(in.isDone()); + assertEquals((byte) 0x7f, in.readByte()); + assertTrue(in.isDone()); + } + + @Test + public void singleShortElement() { + final RLPInput in = RLP.input(h("0x81FF")); + assertFalse(in.isDone()); + assertEquals((byte) 0xFF, in.readByte()); + assertTrue(in.isDone()); + } + + @Test + public void singleBarelyShortElement() { + final RLPInput in = RLP.input(h("0xb7" + times("2b", 55))); + assertFalse(in.isDone()); + assertEquals(h(times("2b", 55)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleBarelyLongElement() { + final RLPInput in = RLP.input(h("0xb838" + times("2b", 56))); + assertFalse(in.isDone()); + assertEquals(h(times("2b", 56)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleLongElement() { + final RLPInput in = RLP.input(h("0xb908c1" + times("3c", 2241))); + + assertFalse(in.isDone()); + assertEquals(h(times("3c", 2241)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleLongElementBoundaryCase_1() { + final RLPInput in = RLP.input(h("0xb8ff" + times("3c", 255))); + assertFalse(in.isDone()); + assertEquals(h(times("3c", 255)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleLongElementBoundaryCase_2() { + final RLPInput in = RLP.input(h("0xb90100" + times("3c", 256))); + assertFalse(in.isDone()); + assertEquals(h(times("3c", 256)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleLongElementBoundaryCase_3() { + final RLPInput in = RLP.input(h("0xb9ffff" + times("3c", 65535))); + assertFalse(in.isDone()); + assertEquals(h(times("3c", 65535)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleLongElementBoundaryCase_4() { + final RLPInput in = RLP.input(h("0xba010000" + times("3c", 65536))); + assertFalse(in.isDone()); + assertEquals(h(times("3c", 65536)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleLongElementBoundaryCase_5() { + final RLPInput in = RLP.input(h("0xbaffffff" + times("3c", 16777215))); + assertFalse(in.isDone()); + assertEquals(h(times("3c", 16777215)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void singleLongElementBoundaryCase_6() { + // A RLPx Frame can have a maximum length of 0xffffff, so boundary above this + // will be not be real world scenarios. + final RLPInput in = RLP.input(h("0xbb01000000" + times("3c", 16777216))); + assertFalse(in.isDone()); + assertEquals(h(times("3c", 16777216)), in.readBytesValue()); + assertTrue(in.isDone()); + } + + @Test + public void assertLongScalar() { + // Scalar should be encoded as the minimal byte array representing the number. For 0, that means + // the empty byte array, which is a short element of zero-length, so 0x80. + assertLongScalar(0L, h("0x80")); + + assertLongScalar(1L, h("0x01")); + assertLongScalar(15L, h("0x0F")); + assertLongScalar(1024L, h("0x820400")); + } + + @Test(expected = RLPException.class) + public void longScalar_NegativeLong() { + assertLongScalar(-1L, h("0xFFFFFFFFFFFFFFFF")); + } + + private void assertLongScalar(final long expected, final BytesValue toTest) { + final RLPInput in = RLP.input(toTest); + assertFalse(in.isDone()); + assertEquals(expected, in.readLongScalar()); + assertTrue(in.isDone()); + } + + @Test + public void intScalar() { + // Scalar should be encoded as the minimal byte array representing the number. For 0, that means + // the empty byte array, which is a short element of zero-length, so 0x80. + assertIntScalar(0, h("0x80")); + + assertIntScalar(1, h("0x01")); + assertIntScalar(15, h("0x0F")); + assertIntScalar(1024, h("0x820400")); + } + + private void assertIntScalar(final int expected, final BytesValue toTest) { + final RLPInput in = RLP.input(toTest); + assertFalse(in.isDone()); + assertEquals(expected, in.readIntScalar()); + assertTrue(in.isDone()); + } + + @Test + public void emptyList() { + final RLPInput in = RLP.input(h("0xc0")); + assertFalse(in.isDone()); + assertEquals(0, in.enterList()); + assertFalse(in.isDone()); + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleShortList() { + final RLPInput in = RLP.input(h("0xc22c3b")); + + assertFalse(in.isDone()); + assertEquals(2, in.enterList()); + assertEquals((byte) 0x2c, in.readByte()); + assertEquals((byte) 0x3b, in.readByte()); + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleIntBeforeShortList() { + final RLPInput in = RLP.input(h("0x02c22c3b")); + + assertFalse(in.isDone()); + assertEquals(2, in.readIntScalar()); + assertEquals(2, in.enterList()); + assertEquals((byte) 0x2c, in.readByte()); + assertEquals((byte) 0x3b, in.readByte()); + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleShortListUpperBoundary() { + final RLPInput in = RLP.input(h("0xf7" + times("3c", 55))); + assertFalse(in.isDone()); + assertEquals(55, in.enterList()); + for (int i = 0; i < 55; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleLongListLowerBoundary() { + final RLPInput in = RLP.input(h("0xf838" + times("3c", 56))); + assertFalse(in.isDone()); + assertEquals(56, in.enterList()); + for (int i = 0; i < 56; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleLongListBoundaryCase_1() { + final RLPInput in = RLP.input(h("0xf8ff" + times("3c", 255))); + assertFalse(in.isDone()); + assertEquals(255, in.enterList()); + for (int i = 0; i < 255; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleLongListBoundaryCase_2() { + final RLPInput in = RLP.input(h("0xf90100" + times("3c", 256))); + assertFalse(in.isDone()); + assertEquals(256, in.enterList()); + for (int i = 0; i < 256; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleLongListBoundaryCase_3() { + final RLPInput in = RLP.input(h("0xf9ffff" + times("3c", 65535))); + assertFalse(in.isDone()); + assertEquals(65535, in.enterList()); + for (int i = 0; i < 65535; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleLongListBoundaryCase_4() { + final RLPInput in = RLP.input(h("0xfa010000" + times("3c", 65536))); + assertFalse(in.isDone()); + assertEquals(65536, in.enterList()); + for (int i = 0; i < 65536; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleLongListBoundaryCase_5() { + final RLPInput in = RLP.input(h("0xfaffffff" + times("3c", 16777215))); + assertFalse(in.isDone()); + assertEquals(16777215, in.enterList()); + for (int i = 0; i < 16777215; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleLongListBoundaryCase_6() { + // A RLPx Frame can have a maximum length of 0xffffff, so boundary above this + // will be not be real world scenarios. + final RLPInput in = RLP.input(h("0xfb01000000" + times("3c", 16777216))); + assertFalse(in.isDone()); + assertEquals(16777216, in.enterList()); + for (int i = 0; i < 16777216; i++) { + assertEquals((byte) 0x3c, in.readByte()); + } + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleListwithBytesValue() { + final RLPInput in = RLP.input(h("0xc28180")); + assertFalse(in.isDone()); + assertEquals(1, in.enterList()); + assertEquals(h("0x80"), in.readBytesValue()); + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void simpleNestedList() { + final RLPInput in = RLP.input(h("0xc52cc203123b")); + + assertFalse(in.isDone()); + assertEquals(3, in.enterList()); + assertEquals((byte) 0x2c, in.readByte()); + assertEquals(2, in.enterList()); + assertEquals((byte) 0x03, in.readByte()); + assertEquals((byte) 0x12, in.readByte()); + in.leaveList(); + assertEquals((byte) 0x3b, in.readByte()); + in.leaveList(); + assertTrue(in.isDone()); + } + + @Test + public void readAsRlp() { + // Test null value + final BytesValue nullValue = h("0x80"); + final RLPInput nv = RLP.input(nullValue); + assertEquals(nv.raw(), nv.readAsRlp().raw()); + nv.reset(); + assertTrue(nv.nextIsNull()); + assertTrue(nv.readAsRlp().nextIsNull()); + + // Test empty list + final BytesValue emptyList = h("0xc0"); + final RLPInput el = RLP.input(emptyList); + assertEquals(emptyList, el.readAsRlp().raw()); + el.reset(); + assertEquals(0, el.readAsRlp().enterList()); + el.reset(); + assertEquals(0, el.enterList()); + + final BytesValue nestedList = + RLP.encode( + out -> { + out.startList(); + out.writeByte((byte) 0x01); + out.writeByte((byte) 0x02); + out.startList(); + out.writeByte((byte) 0x11); + out.writeByte((byte) 0x12); + out.startList(); + out.writeByte((byte) 0x21); + out.writeByte((byte) 0x22); + out.endList(); + out.endList(); + out.endList(); + }); + + final RLPInput nl = RLP.input(nestedList); + final RLPInput compare = nl.readAsRlp(); + assertEquals(nl.raw(), compare.raw()); + nl.reset(); + nl.enterList(); + nl.skipNext(); // 0x01 + + // Read the next byte that's inside the list, extract it as raw RLP and assert it's its own + // representation. + assertEquals(h("0x02"), nl.readAsRlp().raw()); + // Extract the inner list. + assertEquals(h("0xc51112c22122"), nl.readAsRlp().raw()); + // Reset + nl.reset(); + nl.enterList(); + nl.skipNext(); + nl.skipNext(); + nl.enterList(); + nl.skipNext(); + nl.skipNext(); + + // Assert on the inner list of depth 3. + assertEquals(h("0xc22122"), nl.readAsRlp().raw()); + } + + @Test + public void raw() { + final BytesValue initial = h("0xc80102c51112c22122"); + final RLPInput in = RLP.input(initial); + assertEquals(initial, in.raw()); + } + + @Test + public void reset() { + final RLPInput in = RLP.input(h("0xc80102c51112c22122")); + for (int i = 0; i < 100; i++) { + assertEquals(3, in.enterList()); + assertEquals(0x01, in.readByte()); + assertEquals(0x02, in.readByte()); + assertEquals(3, in.enterList()); + assertEquals(0x11, in.readByte()); + assertEquals(0x12, in.readByte()); + assertEquals(2, in.enterList()); + assertEquals(0x21, in.readByte()); + assertEquals(0x22, in.readByte()); + in.reset(); + } + } + + @Test + public void ignoreListTail() { + final RLPInput in = RLP.input(h("0xc80102c51112c22122")); + assertEquals(3, in.enterList()); + assertEquals(0x01, in.readByte()); + in.leaveList(true); + } + + @Test(expected = RLPException.class) + public void leaveListEarly() { + final RLPInput in = RLP.input(h("0xc80102c51112c22122")); + assertEquals(3, in.enterList()); + assertEquals(0x01, in.readByte()); + in.leaveList(false); + } +} diff --git a/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java new file mode 100755 index 00000000000..986da0e5ca3 --- /dev/null +++ b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/BytesValueRLPOutputTest.java @@ -0,0 +1,295 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class BytesValueRLPOutputTest { + + private static BytesValue h(final String hex) { + return BytesValue.fromHexString(hex); + } + + private static String times(final String base, final int times) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < times; i++) sb.append(base); + return sb.toString(); + } + + @Test + public void empty() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + assertEquals(BytesValue.EMPTY, out.encoded()); + } + + @Test + public void singleByte() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeByte((byte) 1); + + // Single byte should be encoded as itself + assertEquals(h("0x01"), out.encoded()); + } + + @Test + public void singleByteLowerBoundary() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeByte((byte) 0); + assertEquals(h("0x00"), out.encoded()); + } + + @Test + public void singleByteUpperBoundary() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeByte((byte) 0x7f); + assertEquals(h("0x7f"), out.encoded()); + } + + @Test + public void singleShortElement() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeByte((byte) 0xFF); + + // Bigger than single byte: 0x80 + length then value, where length is 1. + assertEquals(h("0x81FF"), out.encoded()); + } + + @Test + public void singleBarelyShortElement() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("2b", 55))); + + // 55 bytes, so still short: 0x80 + length then value, where length is 55. + assertEquals(h("0xb7" + times("2b", 55)), out.encoded()); + } + + @Test + public void singleBarelyLongElement() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("2b", 56))); + + // 56 bytes, so long element: 0xb7 + length of value size + value, where the value size is 56. + // 56 is 0x38 so its size is 1 byte. + assertEquals(h("0xb838" + times("2b", 56)), out.encoded()); + } + + @Test + public void singleLongElement() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("3c", 2241))); + + // 2241 bytes, so long element: 0xb7 + length of value size + value, where the value size is + // 2241, + // 2241 is 0x8c1 so its size is 2 bytes. + assertEquals(h("0xb908c1" + times("3c", 2241)), out.encoded()); + } + + @Test + public void singleLongElementBoundaryCase_1() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("3c", 255))); + assertEquals(h("0xb8ff" + times("3c", 255)), out.encoded()); + } + + @Test + public void singleLongElementBoundaryCase_2() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("3c", 256))); + assertEquals(h("0xb90100" + times("3c", 256)), out.encoded()); + } + + @Test + public void singleLongElementBoundaryCase_3() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("3c", 65535))); + assertEquals(h("0xb9ffff" + times("3c", 65535)), out.encoded()); + } + + @Test + public void singleLongElementBoundaryCase_4() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("3c", 65536))); + assertEquals(h("0xba010000" + times("3c", 65536)), out.encoded()); + } + + @Test + public void singleLongElementBoundaryCase_5() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("3c", 16777215))); + assertEquals(h("0xbaffffff" + times("3c", 16777215)), out.encoded()); + } + + @Test + public void singleLongElementBoundaryCase_6() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeBytesValue(h(times("3c", 16777216))); + assertEquals(h("0xbb01000000" + times("3c", 16777216)), out.encoded()); + } + + @Test(expected = IllegalStateException.class) + public void multipleElementAddedWithoutList() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeByte((byte) 0); + out.writeByte((byte) 1); + } + + @Test + public void longScalar() { + // Scalar should be encoded as the minimal byte array representing the number. For 0, that means + // the empty byte array, which is a short element of zero-length, so 0x80. + assertLongScalar(h("0x80"), 0); + + assertLongScalar(h("0x01"), 1); + assertLongScalar(h("0x0F"), 15); + assertLongScalar(h("0x820400"), 1024); + } + + private void assertLongScalar(final BytesValue expected, final long toTest) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeLongScalar(toTest); + assertEquals(expected, out.encoded()); + } + + @Test + public void emptyList() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.endList(); + + assertEquals(h("0xc0"), out.encoded()); + } + + @Test(expected = IllegalStateException.class) + public void unclosedList() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.encoded(); + } + + @Test(expected = IllegalStateException.class) + public void closeUnopenedList() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.endList(); + } + + @Test + public void simpleShortList() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.writeByte((byte) 0x2c); + out.writeByte((byte) 0x3b); + out.endList(); + + // List with payload size = 2 (both element are single bytes) + // so 0xc0 + size then payloads + assertEquals(h("0xc22c3b"), out.encoded()); + } + + @Test + public void simpleShortListUpperBoundary() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 55; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xf7" + times("3c", 55)), out.encoded()); + } + + @Test + public void simpleLongListLowerBoundary() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 56; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xf838" + times("3c", 56)), out.encoded()); + } + + @Test + public void simpleLongListBoundaryCase_1() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 255; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xf8ff" + times("3c", 255)), out.encoded()); + } + + @Test + public void simpleLongListBoundaryCase_2() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 256; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xf90100" + times("3c", 256)), out.encoded()); + } + + @Test + public void simpleLongListBoundaryCase_3() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 65535; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xf9ffff" + times("3c", 65535)), out.encoded()); + } + + @Test + public void simpleLongListBoundaryCase_4() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 65536; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xfa010000" + times("3c", 65536)), out.encoded()); + } + + @Test + public void simpleLongListBoundaryCase_5() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 16777215; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xfaffffff" + times("3c", 16777215)), out.encoded()); + } + + @Test + public void simpleLongListBoundaryCase_6() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < 16777216; i++) { + out.writeByte((byte) 0x3c); + } + out.endList(); + assertEquals(h("0xfb01000000" + times("3c", 16777216)), out.encoded()); + } + + @Test + public void simpleNestedList() { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.writeByte((byte) 0x2c); + // Nested list has 2 simple elements, so will be 0xc20312 + out.startList(); + out.writeByte((byte) 0x03); + out.writeByte((byte) 0x12); + out.endList(); + out.writeByte((byte) 0x3b); + out.endList(); + + // List payload size = 5 (2 single bytes element + nested list of size 3) + // so 0xc0 + size then payloads + assertEquals(h("0xc52cc203123b"), out.encoded()); + } +} diff --git a/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTest.java b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTest.java new file mode 100755 index 00000000000..6e8f56c3fc1 --- /dev/null +++ b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTest.java @@ -0,0 +1,37 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.testutil.JsonTestParameters; + +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** The Ethereum reference RLP tests. */ +@RunWith(Parameterized.class) +public class InvalidRLPRefTest { + + private static final String[] TEST_CONFIG_FILES = { + // TODO: upstream these additional tests to the ethereum tests repo + "net/consensys/pantheon/ethereum/rlp/invalidRLPTest.json", "RLPTests/invalidRLPTest.json" + }; + + private final InvalidRLPRefTestCaseSpec spec; + + public InvalidRLPRefTest(final String name, final InvalidRLPRefTestCaseSpec spec) { + this.spec = spec; + } + + @Parameters(name = "Name: {0}") + public static Collection getTestParametersForConfig() { + return JsonTestParameters.create(InvalidRLPRefTestCaseSpec.class).generate(TEST_CONFIG_FILES); + } + + /** Test RLP decoding. */ + @Test(expected = RLPException.class) + public void decode() throws Exception { + RLP.decode(spec.getRLP()); + } +} diff --git a/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTestCaseSpec.java b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTestCaseSpec.java new file mode 100755 index 00000000000..f9b01093492 --- /dev/null +++ b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/InvalidRLPRefTestCaseSpec.java @@ -0,0 +1,23 @@ +package net.consensys.pantheon.ethereum.rlp; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties({"in"}) +public class InvalidRLPRefTestCaseSpec { + + /** The rlp data to analyze. */ + private final BytesValue rlp; + + @JsonCreator + public InvalidRLPRefTestCaseSpec(@JsonProperty("out") final String out) { + this.rlp = BytesValue.fromHexStringLenient(out); + } + + public BytesValue getRLP() { + return rlp; + } +} diff --git a/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTest.java b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTest.java new file mode 100755 index 00000000000..73f80895fbc --- /dev/null +++ b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTest.java @@ -0,0 +1,40 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static org.junit.Assert.assertEquals; + +import net.consensys.pantheon.testutil.JsonTestParameters; + +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** The Ethereum reference RLP tests. */ +@RunWith(Parameterized.class) +public class RLPRefTest { + + private static final String TEST_CONFIG_FILES = "RLPTests/rlptest.json"; + + private final RLPRefTestCaseSpec spec; + + public RLPRefTest(final String name, final RLPRefTestCaseSpec spec) { + this.spec = spec; + } + + @Parameters(name = "Name: {0}") + public static Collection getTestParametersForConfig() { + return JsonTestParameters.create(RLPRefTestCaseSpec.class).generate(TEST_CONFIG_FILES); + } + + @Test + public void encode() { + assertEquals(spec.getOut(), RLP.encode(spec.getIn())); + } + + @Test + public void decode() { + assertEquals(spec.getIn(), RLP.decode(spec.getOut())); + } +} diff --git a/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTestCaseSpec.java b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTestCaseSpec.java new file mode 100755 index 00000000000..9873b8c65f9 --- /dev/null +++ b/ethereum/rlp/src/test/java/net/consensys/pantheon/ethereum/rlp/RLPRefTestCaseSpec.java @@ -0,0 +1,83 @@ +package net.consensys.pantheon.ethereum.rlp; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.Lists; + +/** + * A RLP reference test case specification. + * + *

Note: this class will be auto-generated with the JSON test specification. + */ +public class RLPRefTestCaseSpec { + + /** Prefix for integer-encoded string. */ + private static final String BIG_INT_PREFIX = "#"; + + /** The test input. */ + private final Object in; + + /** The expected output. */ + private final BytesValue out; + + @SuppressWarnings("unchecked") + private static Object parseIn(final Object in) { + if (in instanceof String && ((String) in).startsWith(BIG_INT_PREFIX)) { + return BytesValue.wrap(new BigInteger(((String) in).substring(1)).toByteArray()); + } else if (in instanceof String) { + return BytesValue.wrap(((String) in).getBytes(UTF_8)); + } else if (in instanceof Integer) { + return BytesValues.toMinimalBytes((Integer) in); + } else if (in instanceof List) { + return Lists.transform((List) in, RLPRefTestCaseSpec::parseIn); + } else if (in instanceof Object[]) { + return Arrays.stream((Object[]) in) + .map(RLPRefTestCaseSpec::parseIn) + .collect(Collectors.toList()); + } else { + throw new IllegalArgumentException(); + } + } + + /** + * Public constructor. + * + * @param in The test input. + * @param out The expected output. + */ + @JsonCreator + public RLPRefTestCaseSpec( + @JsonProperty("in") final Object in, @JsonProperty("out") final String out) { + // Check if the input is an integer-encoded string. + this.in = parseIn(in); + this.out = BytesValue.fromHexString(out); + } + + /** + * Returns the test input. + * + * @return The test input. + */ + public Object getIn() { + return in; + } + + /** + * Returns the expected output. + * + * @return The expected output. + */ + public BytesValue getOut() { + return out; + } +} diff --git a/ethereum/rlp/src/test/resources/net/consensys/pantheon/ethereum/rlp/invalidRLPTest.json b/ethereum/rlp/src/test/resources/net/consensys/pantheon/ethereum/rlp/invalidRLPTest.json new file mode 100755 index 00000000000..7ca65892474 --- /dev/null +++ b/ethereum/rlp/src/test/resources/net/consensys/pantheon/ethereum/rlp/invalidRLPTest.json @@ -0,0 +1,90 @@ +{ + "declaredPayloadLengthOverflowsRLP_shortByteArray": { + "in": "INVALID", + "out": "82FF" + }, + "declaredPayloadLengthOverflowsRLP_longByteArray": { + "in": "INVALID", + "out": "B83A0102" + }, + "declaredPayloadLengthOverflowsRLP_shortList": { + "in": "INVALID", + "out": "C201" + }, + "declaredPayloadLengthOverflowsRLP_longList": { + "in": "INVALID", + "out": "F83A0102" + }, + "lengthByteStringHasLeadingZeros_longByteArray": { + "in": "INVALID", + "out": "B9003A000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839" + }, + "lengthByteStringHasLeadingZeros_longList": { + "in": "INVALID", + "out": "F9003A000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839" + }, + "longPayloadLengthTruncated_longByteArray": { + "in": "INVALID", + "out": "B901" + }, + "longPayloadLengthTruncated_longList": { + "in": "INVALID", + "out": "F901" + }, + "listContainsTruncatedPayload": { + "in": "INVALID", + "out": "C181" + }, + "trailingBytes_shortList": { + "in": "INVALID", + "out": "C10102" + }, + "trailingBytes_longList": { + "in": "INVALID", + "out": "F839000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233343536373839" + }, + "longElement_invalidLength_lowerBoundary": { + "in": "INVALID", + "out": "0xb800" + }, + "longElement_invalidLength_middle": { + "in": "INVALID", + "out": "0xb80101" + }, + "longElement_invalidLength_upperBoundary": { + "in": "INVALID", + "out" : "0xb83701010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" + }, + "longList_invalidLength_lowerBoundary": { + "in": "INVALID", + "out": "0xf800" + }, + "longList_invalidLength_middle": { + "in": "INVALID", + "out": "0xf80101" + }, + "longList_invalid_Length_upperBoundary": { + "in": "INVALID", + "out" : "0xf83701010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" + }, + "invalid_string_lowerBoundary": { + "in": "INVALID", + "out": "8100" + }, + "invalid_string_middle": { + "in": "INVALID", + "out": "8105" + }, + "invalid_string_upeerBoundary": { + "in": "INVALID", + "out": "817f" + }, + "list_contains_invalidString": { + "in": "INVALID", + "out": "c2817f" + }, + "list_contains_invalidList": { + "in": "INVALID", + "out": "c3c2817f" + } +} diff --git a/ethereum/trie/build.gradle b/ethereum/trie/build.gradle new file mode 100755 index 00000000000..9d3ab8e5fec --- /dev/null +++ b/ethereum/trie/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon-trie' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + implementation project(':crypto') + implementation project(':ethereum:rlp') + implementation project(':services:kvstore') + + implementation 'com.google.guava:guava' + implementation 'org.bouncycastle:bcprov-jdk15on' + + testImplementation project(path: ':ethereum:referencetests', configuration: 'testOutput') + testImplementation project(':testutil') + + testImplementation 'com.fasterxml.jackson.core:jackson-databind' + testImplementation 'junit:junit' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.assertj:assertj-core' +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/BranchNode.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/BranchNode.java new file mode 100755 index 00000000000..cd99630c097 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/BranchNode.java @@ -0,0 +1,209 @@ +package net.consensys.pantheon.ethereum.trie; + +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +class BranchNode implements Node { + public static final byte RADIX = CompactEncoding.LEAF_TERMINATOR; + + @SuppressWarnings("rawtypes") + private static final Node NULL_NODE = NullNode.instance(); + + private final ArrayList> children; + private final Optional value; + private final NodeFactory nodeFactory; + private final Function valueSerializer; + private WeakReference rlp; + private SoftReference hash; + private boolean dirty = false; + + BranchNode( + final ArrayList> children, + final Optional value, + final NodeFactory nodeFactory, + final Function valueSerializer) { + assert (children.size() == RADIX); + this.children = children; + this.value = value; + this.nodeFactory = nodeFactory; + this.valueSerializer = valueSerializer; + } + + @Override + public Node accept(final PathNodeVisitor visitor, final BytesValue path) { + return visitor.visit(this, path); + } + + @Override + public void accept(final NodeVisitor visitor) { + visitor.visit(this); + } + + @Override + public BytesValue getPath() { + return BytesValue.EMPTY; + } + + @Override + public Optional getValue() { + return value; + } + + public Node child(final byte index) { + return children.get(index); + } + + @Override + public BytesValue getRlp() { + if (rlp != null) { + final BytesValue encoded = rlp.get(); + if (encoded != null) { + return encoded; + } + } + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + for (int i = 0; i < RADIX; ++i) { + out.writeRLPUnsafe(children.get(i).getRlpRef()); + } + if (value.isPresent()) { + out.writeBytesValue(valueSerializer.apply(value.get())); + } else { + out.writeNull(); + } + out.endList(); + final BytesValue encoded = out.encoded(); + rlp = new WeakReference<>(encoded); + return encoded; + } + + @Override + public BytesValue getRlpRef() { + final BytesValue rlp = getRlp(); + if (rlp.size() < 32) { + return rlp; + } else { + return RLP.encodeOne(getHash()); + } + } + + @Override + public Bytes32 getHash() { + if (hash != null) { + final Bytes32 hashed = hash.get(); + if (hashed != null) { + return hashed; + } + } + final Bytes32 hashed = keccak256(getRlp()); + hash = new SoftReference<>(hashed); + return hashed; + } + + @Override + public Node replacePath(final BytesValue newPath) { + return nodeFactory.createExtension(newPath, this); + } + + public Node replaceChild(final byte index, final Node updatedChild) { + final ArrayList> newChildren = new ArrayList<>(children); + newChildren.set(index, updatedChild); + + if (updatedChild == NULL_NODE) { + if (value.isPresent() && !hasChildren()) { + return nodeFactory.createLeaf(BytesValue.of(index), value.get()); + } else if (!value.isPresent()) { + final Optional> flattened = maybeFlatten(newChildren); + if (flattened.isPresent()) { + return flattened.get(); + } + } + } + + return nodeFactory.createBranch(newChildren, value); + } + + public Node replaceValue(final V value) { + return nodeFactory.createBranch(children, Optional.of(value)); + } + + public Node removeValue() { + return maybeFlatten(children).orElse(nodeFactory.createBranch(children, Optional.empty())); + } + + private boolean hasChildren() { + for (final Node child : children) { + if (child != NULL_NODE) { + return true; + } + } + return false; + } + + private static Optional> maybeFlatten(final ArrayList> children) { + final int onlyChildIndex = findOnlyChild(children); + if (onlyChildIndex >= 0) { + // replace the path of the only child and return it + final Node onlyChild = children.get(onlyChildIndex); + final BytesValue onlyChildPath = onlyChild.getPath(); + final MutableBytesValue completePath = MutableBytesValue.create(1 + onlyChildPath.size()); + completePath.set(0, (byte) onlyChildIndex); + onlyChildPath.copyTo(completePath, 1); + return Optional.of(onlyChild.replacePath(completePath)); + } + return Optional.empty(); + } + + private static int findOnlyChild(final ArrayList> children) { + int onlyChildIndex = -1; + assert (children.size() == RADIX); + for (int i = 0; i < RADIX; ++i) { + if (children.get(i) != NULL_NODE) { + if (onlyChildIndex >= 0) { + return -1; + } + onlyChildIndex = i; + } + } + return onlyChildIndex; + } + + @Override + public String print() { + final StringBuilder builder = new StringBuilder(); + builder.append("Branch:"); + builder.append("\n\tRef: ").append(getRlpRef()); + for (int i = 0; i < RADIX; i++) { + final Node child = child((byte) i); + if (!Objects.equals(child, NullNode.instance())) { + final String branchLabel = "[" + Integer.toHexString(i) + "] "; + final String childRep = child.print().replaceAll("\n\t", "\n\t\t"); + builder.append("\n\t").append(branchLabel).append(childRep); + } + } + builder.append("\n\tValue: ").append(getValue().map(Object::toString).orElse("empty")); + return builder.toString(); + } + + @Override + public boolean isDirty() { + return dirty; + } + + @Override + public void markDirty() { + dirty = true; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CommitVisitor.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CommitVisitor.java new file mode 100755 index 00000000000..bb2cf8e462a --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CommitVisitor.java @@ -0,0 +1,61 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.BytesValue; + +class CommitVisitor implements NodeVisitor { + + private final NodeUpdater nodeUpdater; + + public CommitVisitor(final NodeUpdater nodeUpdater) { + this.nodeUpdater = nodeUpdater; + } + + @Override + public void visit(final ExtensionNode extensionNode) { + if (!extensionNode.isDirty()) { + return; + } + + final Node child = extensionNode.getChild(); + if (child.isDirty()) { + child.accept(this); + } + + maybeStoreNode(extensionNode); + } + + @Override + public void visit(final BranchNode branchNode) { + if (!branchNode.isDirty()) { + return; + } + + for (byte i = 0; i < BranchNode.RADIX; ++i) { + final Node child = branchNode.child(i); + if (child.isDirty()) { + child.accept(this); + } + } + + maybeStoreNode(branchNode); + } + + @Override + public void visit(final LeafNode leafNode) { + if (!leafNode.isDirty()) { + return; + } + + maybeStoreNode(leafNode); + } + + @Override + public void visit(final NullNode nullNode) {} + + private void maybeStoreNode(final Node node) { + final BytesValue nodeRLP = node.getRlp(); + if (nodeRLP.size() >= 32) { + this.nodeUpdater.store(node.getHash(), nodeRLP); + } + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CompactEncoding.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CompactEncoding.java new file mode 100755 index 00000000000..57186abfed6 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/CompactEncoding.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.ethereum.trie; + +import static com.google.common.base.Preconditions.checkArgument; + +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.MutableBytesValue; + +abstract class CompactEncoding { + private CompactEncoding() {} + + static final byte LEAF_TERMINATOR = 0x10; + + public static BytesValue bytesToPath(final BytesValue bytes) { + final MutableBytesValue path = MutableBytesValue.create(bytes.size() * 2 + 1); + int j = 0; + for (int i = 0; i < bytes.size(); i += 1, j += 2) { + final byte b = bytes.get(i); + path.set(j, (byte) ((b >>> 4) & 0x0f)); + path.set(j + 1, (byte) (b & 0x0f)); + } + path.set(j, LEAF_TERMINATOR); + return path; + } + + public static BytesValue pathToBytes(final BytesValue path) { + checkArgument(!path.isEmpty(), "Path must not be empty"); + checkArgument(path.get(path.size() - 1) == LEAF_TERMINATOR, "Path must be a leaf path"); + final MutableBytesValue bytes = MutableBytesValue.create((path.size() - 1) / 2); + int bytesPos = 0; + for (int pathPos = 0; pathPos < path.size() - 1; pathPos += 2, bytesPos += 1) { + final byte high = path.get(pathPos); + final byte low = path.get(pathPos + 1); + if ((high & 0xf0) != 0 || (low & 0xf0) != 0) { + throw new IllegalArgumentException("Invalid path: contains elements larger than a nibble"); + } + bytes.set(bytesPos, (byte) (high << 4 | low)); + } + return bytes; + } + + public static BytesValue encode(final BytesValue path) { + int size = path.size(); + final boolean isLeaf = size > 0 && path.get(size - 1) == LEAF_TERMINATOR; + if (isLeaf) { + size = size - 1; + } + + final MutableBytesValue encoded = MutableBytesValue.create((size + 2) / 2); + int i = 0; + int j = 0; + + if (size % 2 == 1) { + // add first nibble to magic + final byte high = (byte) (isLeaf ? 0x03 : 0x01); + final byte low = path.get(i++); + if ((low & 0xf0) != 0) { + throw new IllegalArgumentException("Invalid path: contains elements larger than a nibble"); + } + encoded.set(j++, (byte) (high << 4 | low)); + } else { + final byte high = (byte) (isLeaf ? 0x02 : 0x00); + encoded.set(j++, (byte) (high << 4)); + } + + while (i < size) { + final byte high = path.get(i++); + final byte low = path.get(i++); + if ((high & 0xf0) != 0 || (low & 0xf0) != 0) { + throw new IllegalArgumentException("Invalid path: contains elements larger than a nibble"); + } + encoded.set(j++, (byte) (high << 4 | low)); + } + + return encoded; + } + + public static BytesValue decode(final BytesValue encoded) { + final int size = encoded.size(); + checkArgument(size > 0); + final byte metadata = encoded.get(0); + checkArgument((metadata & 0xc0) == 0, "Invalid compact encoding"); + + final boolean isLeaf = (metadata & 0x20) != 0; + + final int pathLength = ((size - 1) * 2) + (isLeaf ? 1 : 0); + MutableBytesValue path; + int i = 0; + + if ((metadata & 0x10) != 0) { + // need to use lower nibble of metadata + path = MutableBytesValue.create(pathLength + 1); + path.set(i++, (byte) (metadata & 0x0f)); + } else { + path = MutableBytesValue.create(pathLength); + } + + for (int j = 1; j < size; j++) { + final byte b = encoded.get(j); + path.set(i++, (byte) ((b >>> 4) & 0x0f)); + path.set(i++, (byte) (b & 0x0f)); + } + + if (isLeaf) { + path.set(i, LEAF_TERMINATOR); + } + + return path; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/DefaultNodeFactory.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/DefaultNodeFactory.java new file mode 100755 index 00000000000..4deb6351797 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/DefaultNodeFactory.java @@ -0,0 +1,57 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Function; + +class DefaultNodeFactory implements NodeFactory { + @SuppressWarnings("rawtypes") + private static final Node NULL_NODE = NullNode.instance(); + + private final Function valueSerializer; + + DefaultNodeFactory(final Function valueSerializer) { + this.valueSerializer = valueSerializer; + } + + @Override + public Node createExtension(final BytesValue path, final Node child) { + return new ExtensionNode<>(path, child, this); + } + + @SuppressWarnings("unchecked") + @Override + public Node createBranch( + final byte leftIndex, final Node left, final byte rightIndex, final Node right) { + assert (leftIndex <= BranchNode.RADIX); + assert (rightIndex <= BranchNode.RADIX); + assert (leftIndex != rightIndex); + + final ArrayList> children = + new ArrayList<>(Collections.nCopies(BranchNode.RADIX, (Node) NULL_NODE)); + if (leftIndex == BranchNode.RADIX) { + children.set(rightIndex, right); + return createBranch(children, left.getValue()); + } else if (rightIndex == BranchNode.RADIX) { + children.set(leftIndex, left); + return createBranch(children, right.getValue()); + } else { + children.set(leftIndex, left); + children.set(rightIndex, right); + return createBranch(children, Optional.empty()); + } + } + + @Override + public Node createBranch(final ArrayList> children, final Optional value) { + return new BranchNode<>(children, value, this, valueSerializer); + } + + @Override + public Node createLeaf(final BytesValue path, final V value) { + return new LeafNode<>(path, value, this, valueSerializer); + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/ExtensionNode.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/ExtensionNode.java new file mode 100755 index 00000000000..1f0b5fd2170 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/ExtensionNode.java @@ -0,0 +1,131 @@ +package net.consensys.pantheon.ethereum.trie; + +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.bytes.BytesValues; + +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.Optional; + +class ExtensionNode implements Node { + private final BytesValue path; + private final Node child; + private final NodeFactory nodeFactory; + private WeakReference rlp; + private SoftReference hash; + private boolean dirty = false; + + ExtensionNode(final BytesValue path, final Node child, final NodeFactory nodeFactory) { + assert (path.size() > 0); + assert (path.get(path.size() - 1) != CompactEncoding.LEAF_TERMINATOR) + : "Extension path ends in a leaf terminator"; + this.path = path; + this.child = child; + this.nodeFactory = nodeFactory; + } + + @Override + public Node accept(final PathNodeVisitor visitor, final BytesValue path) { + return visitor.visit(this, path); + } + + @Override + public void accept(final NodeVisitor visitor) { + visitor.visit(this); + } + + @Override + public BytesValue getPath() { + return path; + } + + @Override + public Optional getValue() { + throw new UnsupportedOperationException(); + } + + public Node getChild() { + return child; + } + + @Override + public BytesValue getRlp() { + if (rlp != null) { + final BytesValue encoded = rlp.get(); + if (encoded != null) { + return encoded; + } + } + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.writeBytesValue(CompactEncoding.encode(path)); + out.writeRLPUnsafe(child.getRlpRef()); + out.endList(); + final BytesValue encoded = out.encoded(); + rlp = new WeakReference<>(encoded); + return encoded; + } + + @Override + public BytesValue getRlpRef() { + final BytesValue rlp = getRlp(); + if (rlp.size() < 32) { + return rlp; + } else { + return RLP.encodeOne(getHash()); + } + } + + @Override + public Bytes32 getHash() { + if (hash != null) { + final Bytes32 hashed = hash.get(); + if (hashed != null) { + return hashed; + } + } + final BytesValue rlp = getRlp(); + final Bytes32 hashed = keccak256(rlp); + hash = new SoftReference<>(hashed); + return hashed; + } + + public Node replaceChild(final Node updatedChild) { + // collapse this extension - if the child is a branch, it will create a new extension + return updatedChild.replacePath(BytesValues.concatenate(path, updatedChild.getPath())); + } + + @Override + public Node replacePath(final BytesValue path) { + if (path.size() == 0) { + return child; + } + return nodeFactory.createExtension(path, child); + } + + @Override + public String print() { + final StringBuilder builder = new StringBuilder(); + builder.append("Extension:"); + builder.append("\n\tRef: ").append(getRlpRef()); + builder.append("\n\tPath: " + CompactEncoding.encode(path)); + final String childRep = getChild().print().replaceAll("\n\t", "\n\t\t"); + builder.append("\n\t").append(childRep); + return builder.toString(); + } + + @Override + public boolean isDirty() { + return dirty; + } + + @Override + public void markDirty() { + dirty = true; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/GetVisitor.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/GetVisitor.java new file mode 100755 index 00000000000..3d628db071f --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/GetVisitor.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.BytesValue; + +class GetVisitor implements PathNodeVisitor { + private final Node NULL_NODE_RESULT = NullNode.instance(); + + @Override + public Node visit(final ExtensionNode extensionNode, final BytesValue path) { + final BytesValue extensionPath = extensionNode.getPath(); + final int commonPathLength = extensionPath.commonPrefixLength(path); + assert commonPathLength < path.size() + : "Visiting path doesn't end with a non-matching terminator"; + + if (commonPathLength < extensionPath.size()) { + // path diverges before the end of the extension, so it cannot match + return NULL_NODE_RESULT; + } + + return extensionNode.getChild().accept(this, path.slice(commonPathLength)); + } + + @Override + public Node visit(final BranchNode branchNode, final BytesValue path) { + assert path.size() > 0 : "Visiting path doesn't end with a non-matching terminator"; + + final byte childIndex = path.get(0); + if (childIndex == CompactEncoding.LEAF_TERMINATOR) { + return branchNode; + } + + return branchNode.child(childIndex).accept(this, path.slice(1)); + } + + @Override + public Node visit(final LeafNode leafNode, final BytesValue path) { + final BytesValue leafPath = leafNode.getPath(); + if (leafPath.commonPrefixLength(path) != leafPath.size()) { + return NULL_NODE_RESULT; + } + return leafNode; + } + + @Override + public Node visit(final NullNode nullNode, final BytesValue path) { + return NULL_NODE_RESULT; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/KeyValueMerkleStorage.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/KeyValueMerkleStorage.java new file mode 100755 index 00000000000..c65f4322b5b --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/KeyValueMerkleStorage.java @@ -0,0 +1,53 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class KeyValueMerkleStorage implements MerkleStorage { + + private final KeyValueStorage keyValueStorage; + private final Map pendingUpdates = new HashMap<>(); + + public KeyValueMerkleStorage(final KeyValueStorage keyValueStorage) { + this.keyValueStorage = keyValueStorage; + } + + @Override + public Optional get(final Bytes32 hash) { + final Optional value = + pendingUpdates.containsKey(hash) + ? Optional.of(pendingUpdates.get(hash)) + : keyValueStorage.get(hash); + return value; + } + + @Override + public void put(final Bytes32 hash, final BytesValue value) { + pendingUpdates.put(hash, value); + } + + @Override + public void commit() { + if (pendingUpdates.size() == 0) { + // Nothing to do + return; + } + final KeyValueStorage.Transaction kvTx = keyValueStorage.getStartTransaction(); + for (final Map.Entry entry : pendingUpdates.entrySet()) { + kvTx.put(entry.getKey(), entry.getValue()); + } + kvTx.commit(); + + pendingUpdates.clear(); + } + + @Override + public void rollback() { + pendingUpdates.clear(); + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/LeafNode.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/LeafNode.java new file mode 100755 index 00000000000..96a029efaad --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/LeafNode.java @@ -0,0 +1,122 @@ +package net.consensys.pantheon.ethereum.trie; + +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.Optional; +import java.util.function.Function; + +class LeafNode implements Node { + private final BytesValue path; + private final V value; + private final NodeFactory nodeFactory; + private final Function valueSerializer; + private WeakReference rlp; + private SoftReference hash; + private boolean dirty = false; + + LeafNode( + final BytesValue path, + final V value, + final NodeFactory nodeFactory, + final Function valueSerializer) { + this.path = path; + this.value = value; + this.nodeFactory = nodeFactory; + this.valueSerializer = valueSerializer; + } + + @Override + public Node accept(final PathNodeVisitor visitor, final BytesValue path) { + return visitor.visit(this, path); + } + + @Override + public void accept(final NodeVisitor visitor) { + visitor.visit(this); + } + + @Override + public BytesValue getPath() { + return path; + } + + @Override + public Optional getValue() { + return Optional.of(value); + } + + @Override + public BytesValue getRlp() { + if (rlp != null) { + final BytesValue encoded = rlp.get(); + if (encoded != null) { + return encoded; + } + } + + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + out.writeBytesValue(CompactEncoding.encode(path)); + out.writeBytesValue(valueSerializer.apply(value)); + out.endList(); + final BytesValue encoded = out.encoded(); + rlp = new WeakReference<>(encoded); + return encoded; + } + + @Override + public BytesValue getRlpRef() { + final BytesValue rlp = getRlp(); + if (rlp.size() < 32) { + return rlp; + } else { + return RLP.encodeOne(getHash()); + } + } + + @Override + public Bytes32 getHash() { + if (hash != null) { + final Bytes32 hashed = hash.get(); + if (hashed != null) { + return hashed; + } + } + final Bytes32 hashed = keccak256(getRlp()); + hash = new SoftReference<>(hashed); + return hashed; + } + + @Override + public Node replacePath(final BytesValue path) { + return nodeFactory.createLeaf(path, value); + } + + @Override + public String print() { + return "Leaf:" + + "\n\tRef: " + + getRlpRef() + + "\n\tPath: " + + CompactEncoding.encode(path) + + "\n\tValue: " + + getValue().map(Object::toString).orElse("empty"); + } + + @Override + public boolean isDirty() { + return dirty; + } + + @Override + public void markDirty() { + dirty = true; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerklePatriciaTrie.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerklePatriciaTrie.java new file mode 100755 index 00000000000..c9636e54624 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerklePatriciaTrie.java @@ -0,0 +1,63 @@ +package net.consensys.pantheon.ethereum.trie; + +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.Map; +import java.util.Optional; + +/** An Merkle Patricial Trie. */ +public interface MerklePatriciaTrie { + + Bytes32 EMPTY_TRIE_ROOT_HASH = keccak256(RLP.NULL); + + /** + * Returns an {@code Optional} of value mapped to the hash if it exists; otherwise empty. + * + * @param key The key for the value. + * @return an {@code Optional} of value mapped to the hash if it exists; otherwise empty + */ + Optional get(K key); + + /** + * Updates the value mapped to the specified key, creating the mapping if one does not already + * exist. + * + * @param key The key that corresponds to the value to be updated. + * @param value The value to associate the key with. + */ + void put(K key, V value); + + /** + * Deletes the value mapped to the specified key, if such a value exists (Optional operation). + * + * @param key The key of the value to be deleted. + */ + void remove(K key); + + /** + * Returns the KECCAK256 hash of the root node of the trie. + * + * @return The KECCAK256 hash of the root node of the trie. + */ + Bytes32 getRootHash(); + + /** + * Commits any pending changes to the underlying storage. + * + * @param nodeUpdater used to store the node values + */ + void commit(NodeUpdater nodeUpdater); + + /** + * Retrieve up to {@code limit} storage entries beginning from the first entry with hash equal to + * or greater than {@code startKeyHash}. + * + * @param startKeyHash the first key hash to return. + * @param limit the maximum number of entries to return. + * @return the requested storage entries as a map of key hash to value. + */ + Map entriesFrom(Bytes32 startKeyHash, int limit); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorage.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorage.java new file mode 100755 index 00000000000..ea54c84b99e --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorage.java @@ -0,0 +1,36 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +/** Storage for use in a {@link StoredMerklePatriciaTrie}. */ +public interface MerkleStorage { + + /** + * Returns an {@code Optional} of the content mapped to the hash if it exists; otherwise empty. + * + * @param hash The hash for the content. + * @return an {@code Optional} of the content mapped to the hash if it exists; otherwise empty + */ + Optional get(Bytes32 hash); + + /** + * Updates the content mapped to the specified hash, creating the mapping if one does not already + * exist. + * + *

Note: if the storage implementation already contains content for the given hash, it will + * replace the existing content. + * + * @param hash The hash for the content. + * @param content The content to store. + */ + void put(Bytes32 hash, BytesValue content); + + /** Persist accumulated changes to underlying storage. */ + void commit(); + + /** Throws away any changes accumulated by this store. */ + void rollback(); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorageException.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorageException.java new file mode 100755 index 00000000000..2a04a41d842 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/MerkleStorageException.java @@ -0,0 +1,16 @@ +package net.consensys.pantheon.ethereum.trie; + +/** + * This exception is thrown when there is an issue retrieving or decoding values from {@link + * MerkleStorage}. + */ +public class MerkleStorageException extends RuntimeException { + + public MerkleStorageException(final String message) { + super(message); + } + + public MerkleStorageException(final String message, final Exception cause) { + super(message, cause); + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/Node.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/Node.java new file mode 100755 index 00000000000..ddff301dd74 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/Node.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +interface Node { + + Node accept(PathNodeVisitor visitor, BytesValue path); + + void accept(NodeVisitor visitor); + + BytesValue getPath(); + + Optional getValue(); + + BytesValue getRlp(); + + BytesValue getRlpRef(); + + Bytes32 getHash(); + + Node replacePath(BytesValue path); + + /** Marks the node as needing to be persisted */ + void markDirty(); + + /** @return True if the node needs to be persisted. */ + boolean isDirty(); + + String print(); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeFactory.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeFactory.java new file mode 100755 index 00000000000..1adfa22e0c9 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeFactory.java @@ -0,0 +1,17 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Optional; + +interface NodeFactory { + + Node createExtension(BytesValue path, Node child); + + Node createBranch(byte leftIndex, Node left, byte rightIndex, Node right); + + Node createBranch(ArrayList> newChildren, Optional value); + + Node createLeaf(BytesValue path, V value); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeLoader.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeLoader.java new file mode 100755 index 00000000000..a07251b7874 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeLoader.java @@ -0,0 +1,10 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +public interface NodeLoader { + Optional getNode(Bytes32 hash); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeUpdater.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeUpdater.java new file mode 100755 index 00000000000..91839e13e0e --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeUpdater.java @@ -0,0 +1,8 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +public interface NodeUpdater { + void store(Bytes32 hash, BytesValue value); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeVisitor.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeVisitor.java new file mode 100755 index 00000000000..e5c16d4e4fd --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NodeVisitor.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.ethereum.trie; + +interface NodeVisitor { + + void visit(ExtensionNode extensionNode); + + void visit(BranchNode branchNode); + + void visit(LeafNode leafNode); + + void visit(NullNode nullNode); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NullNode.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NullNode.java new file mode 100755 index 00000000000..9da10bde41f --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/NullNode.java @@ -0,0 +1,78 @@ +package net.consensys.pantheon.ethereum.trie; + +import static net.consensys.pantheon.crypto.Hash.keccak256; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +class NullNode implements Node { + private static final Bytes32 HASH = keccak256(RLP.NULL); + + @SuppressWarnings("rawtypes") + private static final NullNode instance = new NullNode(); + + private NullNode() {} + + @SuppressWarnings("unchecked") + static NullNode instance() { + return instance; + } + + @Override + public Node accept(final PathNodeVisitor visitor, final BytesValue path) { + return visitor.visit(this, path); + } + + @Override + public void accept(final NodeVisitor visitor) { + visitor.visit(this); + } + + @Override + public BytesValue getPath() { + return BytesValue.EMPTY; + } + + @Override + public Optional getValue() { + return Optional.empty(); + } + + @Override + public BytesValue getRlp() { + return RLP.NULL; + } + + @Override + public BytesValue getRlpRef() { + return RLP.NULL; + } + + @Override + public Bytes32 getHash() { + return HASH; + } + + @Override + public Node replacePath(final BytesValue path) { + return this; + } + + @Override + public String print() { + return "[NULL]"; + } + + @Override + public boolean isDirty() { + return false; + } + + @Override + public void markDirty() { + // do nothing + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PathNodeVisitor.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PathNodeVisitor.java new file mode 100755 index 00000000000..0708715a8f5 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PathNodeVisitor.java @@ -0,0 +1,14 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.BytesValue; + +interface PathNodeVisitor { + + Node visit(ExtensionNode extensionNode, BytesValue path); + + Node visit(BranchNode branchNode, BytesValue path); + + Node visit(LeafNode leafNode, BytesValue path); + + Node visit(NullNode nullNode, BytesValue path); +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PutVisitor.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PutVisitor.java new file mode 100755 index 00000000000..b77bfab9588 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/PutVisitor.java @@ -0,0 +1,94 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.BytesValue; + +class PutVisitor implements PathNodeVisitor { + private final NodeFactory nodeFactory; + private final V value; + + PutVisitor(final NodeFactory nodeFactory, final V value) { + this.nodeFactory = nodeFactory; + this.value = value; + } + + @Override + public Node visit(final ExtensionNode extensionNode, final BytesValue path) { + final BytesValue extensionPath = extensionNode.getPath(); + + final int commonPathLength = extensionPath.commonPrefixLength(path); + assert commonPathLength < path.size() + : "Visiting path doesn't end with a non-matching terminator"; + + if (commonPathLength == extensionPath.size()) { + final Node newChild = extensionNode.getChild().accept(this, path.slice(commonPathLength)); + return extensionNode.replaceChild(newChild); + } + + // path diverges before the end of the extension - create a new branch + + final byte leafIndex = path.get(commonPathLength); + final BytesValue leafPath = path.slice(commonPathLength + 1); + + final byte extensionIndex = extensionPath.get(commonPathLength); + final Node updatedExtension = + extensionNode.replacePath(extensionPath.slice(commonPathLength + 1)); + final Node leaf = nodeFactory.createLeaf(leafPath, value); + final Node branch = + nodeFactory.createBranch(leafIndex, leaf, extensionIndex, updatedExtension); + + if (commonPathLength > 0) { + return nodeFactory.createExtension(extensionPath.slice(0, commonPathLength), branch); + } else { + return branch; + } + } + + @Override + public Node visit(final BranchNode branchNode, final BytesValue path) { + assert path.size() > 0 : "Visiting path doesn't end with a non-matching terminator"; + + final byte childIndex = path.get(0); + if (childIndex == CompactEncoding.LEAF_TERMINATOR) { + return branchNode.replaceValue(value); + } + + final Node updatedChild = branchNode.child(childIndex).accept(this, path.slice(1)); + return branchNode.replaceChild(childIndex, updatedChild); + } + + @Override + public Node visit(final LeafNode leafNode, final BytesValue path) { + final BytesValue leafPath = leafNode.getPath(); + final int commonPathLength = leafPath.commonPrefixLength(path); + + // Check if the current leaf node should be replaced + if (commonPathLength == leafPath.size() && commonPathLength == path.size()) { + return nodeFactory.createLeaf(leafPath, value); + } + + assert commonPathLength < leafPath.size() && commonPathLength < path.size() + : "Should not have consumed non-matching terminator"; + + // The current leaf path must be split to accommodate the new value. + + final byte newLeafIndex = path.get(commonPathLength); + final BytesValue newLeafPath = path.slice(commonPathLength + 1); + + final byte updatedLeafIndex = leafPath.get(commonPathLength); + + final Node updatedLeaf = leafNode.replacePath(leafPath.slice(commonPathLength + 1)); + final Node leaf = nodeFactory.createLeaf(newLeafPath, value); + final Node branch = + nodeFactory.createBranch(updatedLeafIndex, updatedLeaf, newLeafIndex, leaf); + if (commonPathLength > 0) { + return nodeFactory.createExtension(leafPath.slice(0, commonPathLength), branch); + } else { + return branch; + } + } + + @Override + public Node visit(final NullNode nullNode, final BytesValue path) { + return nodeFactory.createLeaf(path, value); + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/RemoveVisitor.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/RemoveVisitor.java new file mode 100755 index 00000000000..ac72f8633b4 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/RemoveVisitor.java @@ -0,0 +1,49 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.BytesValue; + +class RemoveVisitor implements PathNodeVisitor { + private final Node NULL_NODE_RESULT = NullNode.instance(); + + @Override + public Node visit(final ExtensionNode extensionNode, final BytesValue path) { + final BytesValue extensionPath = extensionNode.getPath(); + final int commonPathLength = extensionPath.commonPrefixLength(path); + assert commonPathLength < path.size() + : "Visiting path doesn't end with a non-matching terminator"; + + if (commonPathLength == extensionPath.size()) { + final Node newChild = extensionNode.getChild().accept(this, path.slice(commonPathLength)); + return extensionNode.replaceChild(newChild); + } + + // path diverges before the end of the extension, so it cannot match + + return extensionNode; + } + + @Override + public Node visit(final BranchNode branchNode, final BytesValue path) { + assert path.size() > 0 : "Visiting path doesn't end with a non-matching terminator"; + + final byte childIndex = path.get(0); + if (childIndex == CompactEncoding.LEAF_TERMINATOR) { + return branchNode.removeValue(); + } + + final Node updatedChild = branchNode.child(childIndex).accept(this, path.slice(1)); + return branchNode.replaceChild(childIndex, updatedChild); + } + + @Override + public Node visit(final LeafNode leafNode, final BytesValue path) { + final BytesValue leafPath = leafNode.getPath(); + final int commonPathLength = leafPath.commonPrefixLength(path); + return (commonPathLength == leafPath.size()) ? NULL_NODE_RESULT : leafNode; + } + + @Override + public Node visit(final NullNode nullNode, final BytesValue path) { + return NULL_NODE_RESULT; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java new file mode 100755 index 00000000000..ea04b315178 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java @@ -0,0 +1,73 @@ +package net.consensys.pantheon.ethereum.trie; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.consensys.pantheon.ethereum.trie.CompactEncoding.bytesToPath; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * An in-memory {@link MerklePatriciaTrie}. + * + * @param The type of values stored by this trie. + */ +public class SimpleMerklePatriciaTrie implements MerklePatriciaTrie { + private final PathNodeVisitor getVisitor = new GetVisitor<>(); + private final PathNodeVisitor removeVisitor = new RemoveVisitor<>(); + private final DefaultNodeFactory nodeFactory; + + private Node root; + + /** + * Create a trie. + * + * @param valueSerializer A function for serializing values to bytes. + */ + public SimpleMerklePatriciaTrie(final Function valueSerializer) { + this.nodeFactory = new DefaultNodeFactory<>(valueSerializer); + this.root = NullNode.instance(); + } + + @Override + public Optional get(final K key) { + checkNotNull(key); + return root.accept(getVisitor, bytesToPath(key)).getValue(); + } + + @Override + public void put(final K key, final V value) { + checkNotNull(key); + checkNotNull(value); + this.root = root.accept(new PutVisitor<>(nodeFactory, value), bytesToPath(key)); + } + + @Override + public void remove(final K key) { + checkNotNull(key); + this.root = root.accept(removeVisitor, bytesToPath(key)); + } + + @Override + public Bytes32 getRootHash() { + return root.getHash(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + getRootHash() + "]"; + } + + @Override + public void commit(final NodeUpdater nodeUpdater) { + // Nothing to do here + } + + @Override + public Map entriesFrom(final Bytes32 startKeyHash, final int limit) { + return StorageEntriesCollector.collectEntries(root, startKeyHash, limit); + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StorageEntriesCollector.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StorageEntriesCollector.java new file mode 100755 index 00000000000..25cd5991f0a --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StorageEntriesCollector.java @@ -0,0 +1,44 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.ethereum.trie.TrieIterator.State; +import net.consensys.pantheon.util.bytes.Bytes32; + +import java.util.Map; +import java.util.TreeMap; + +public class StorageEntriesCollector implements TrieIterator.LeafHandler { + + private final Bytes32 startKeyHash; + private final int limit; + private final Map values = new TreeMap<>(); + + public StorageEntriesCollector(final Bytes32 startKeyHash, final int limit) { + this.startKeyHash = startKeyHash; + this.limit = limit; + } + + public static Map collectEntries( + final Node root, final Bytes32 startKeyHash, final int limit) { + final StorageEntriesCollector entriesCollector = + new StorageEntriesCollector<>(startKeyHash, limit); + final TrieIterator visitor = new TrieIterator<>(entriesCollector); + root.accept(visitor, CompactEncoding.bytesToPath(startKeyHash)); + return entriesCollector.getValues(); + } + + private boolean limitReached() { + return limit <= values.size(); + } + + @Override + public State onLeaf(final Bytes32 keyHash, final Node node) { + if (keyHash.compareTo(startKeyHash) >= 0) { + node.getValue().ifPresent(value -> values.put(keyHash, value)); + } + return limitReached() ? State.STOP : State.CONTINUE; + } + + public Map getValues() { + return values; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java new file mode 100755 index 00000000000..506c1611b29 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java @@ -0,0 +1,109 @@ +package net.consensys.pantheon.ethereum.trie; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.consensys.pantheon.ethereum.trie.CompactEncoding.bytesToPath; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +/** + * A {@link MerklePatriciaTrie} that persists trie nodes to a {@link MerkleStorage} key/value store. + * + * @param The type of values stored by this trie. + */ +public class StoredMerklePatriciaTrie implements MerklePatriciaTrie { + private final GetVisitor getVisitor = new GetVisitor<>(); + private final RemoveVisitor removeVisitor = new RemoveVisitor<>(); + private final StoredNodeFactory nodeFactory; + + private Node root; + + /** + * Create a trie. + * + * @param nodeLoader The {@link NodeLoader} to retrieve node data from. + * @param valueSerializer A function for serializing values to bytes. + * @param valueDeserializer A function for deserializing values from bytes. + */ + public StoredMerklePatriciaTrie( + final NodeLoader nodeLoader, + final Function valueSerializer, + final Function valueDeserializer) { + this(nodeLoader, MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH, valueSerializer, valueDeserializer); + } + + /** + * Create a trie. + * + * @param nodeLoader The {@link NodeLoader} to retrieve node data from. + * @param rootHash The initial root has for the trie, which should be already present in {@code + * storage}. + * @param valueSerializer A function for serializing values to bytes. + * @param valueDeserializer A function for deserializing values from bytes. + */ + public StoredMerklePatriciaTrie( + final NodeLoader nodeLoader, + final Bytes32 rootHash, + final Function valueSerializer, + final Function valueDeserializer) { + this.nodeFactory = new StoredNodeFactory<>(nodeLoader, valueSerializer, valueDeserializer); + this.root = + rootHash.equals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH) + ? NullNode.instance() + : new StoredNode<>(nodeFactory, rootHash); + } + + @Override + public Optional get(final K key) { + checkNotNull(key); + return root.accept(getVisitor, bytesToPath(key)).getValue(); + } + + @Override + public void put(final K key, final V value) { + checkNotNull(key); + checkNotNull(value); + this.root = root.accept(new PutVisitor<>(nodeFactory, value), bytesToPath(key)); + } + + @Override + public void remove(final K key) { + checkNotNull(key); + this.root = root.accept(removeVisitor, bytesToPath(key)); + } + + @Override + public void commit(final NodeUpdater nodeUpdater) { + final CommitVisitor commitVisitor = new CommitVisitor<>(nodeUpdater); + root.accept(commitVisitor); + // Make sure root node was stored + if (root.isDirty() && root.getRlpRef().size() < 32) { + nodeUpdater.store(root.getHash(), root.getRlpRef()); + } + // Reset root so dirty nodes can be garbage collected + final Bytes32 rootHash = root.getHash(); + this.root = + rootHash.equals(MerklePatriciaTrie.EMPTY_TRIE_ROOT_HASH) + ? NullNode.instance() + : new StoredNode<>(nodeFactory, rootHash); + } + + @Override + public Map entriesFrom(final Bytes32 startKeyHash, final int limit) { + return StorageEntriesCollector.collectEntries(root, startKeyHash, limit); + } + + @Override + public Bytes32 getRootHash() { + return root.getHash(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + getRootHash() + "]"; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNode.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNode.java new file mode 100755 index 00000000000..a135361c437 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNode.java @@ -0,0 +1,89 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Optional; + +class StoredNode implements Node { + private final StoredNodeFactory nodeFactory; + private final Bytes32 hash; + private Node loaded; + + StoredNode(final StoredNodeFactory nodeFactory, final Bytes32 hash) { + this.nodeFactory = nodeFactory; + this.hash = hash; + } + + /** @return True if the node needs to be persisted. */ + @Override + public boolean isDirty() { + return false; + } + + /** Marks the node as being modified (needs to be persisted); */ + @Override + public void markDirty() { + throw new IllegalStateException( + "A stored node cannot ever be dirty since it's loaded from storage"); + } + + @Override + public Node accept(final PathNodeVisitor visitor, final BytesValue path) { + final Node node = load(); + return node.accept(visitor, path); + } + + @Override + public void accept(final NodeVisitor visitor) { + final Node node = load(); + node.accept(visitor); + } + + @Override + public BytesValue getPath() { + return load().getPath(); + } + + @Override + public Optional getValue() { + return load().getValue(); + } + + @Override + public BytesValue getRlp() { + // Getting the rlp representation is only needed when persisting a concrete node + throw new UnsupportedOperationException(); + } + + @Override + public BytesValue getRlpRef() { + // If this node was stored, then it must have a rlp larger than a hash + return RLP.encodeOne(hash); + } + + @Override + public Bytes32 getHash() { + return hash; + } + + @Override + public Node replacePath(final BytesValue path) { + return load().replacePath(path); + } + + private Node load() { + if (loaded == null) { + loaded = nodeFactory.retrieve(hash); + } + + return loaded; + } + + @Override + public String print() { + final String value = load().print(); + return value; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNodeFactory.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNodeFactory.java new file mode 100755 index 00000000000..3390d9be686 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/StoredNodeFactory.java @@ -0,0 +1,216 @@ +package net.consensys.pantheon.ethereum.trie; + +import static java.lang.String.format; + +import net.consensys.pantheon.ethereum.rlp.RLP; +import net.consensys.pantheon.ethereum.rlp.RLPException; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; + +class StoredNodeFactory implements NodeFactory { + @SuppressWarnings("rawtypes") + private static final NullNode NULL_NODE = NullNode.instance(); + + private final NodeLoader nodeLoader; + private final Function valueSerializer; + private final Function valueDeserializer; + + StoredNodeFactory( + final NodeLoader nodeLoader, + final Function valueSerializer, + final Function valueDeserializer) { + this.nodeLoader = nodeLoader; + this.valueSerializer = valueSerializer; + this.valueDeserializer = valueDeserializer; + } + + @Override + public Node createExtension(final BytesValue path, final Node child) { + return handleNewNode(new ExtensionNode<>(path, child, this)); + } + + @SuppressWarnings("unchecked") + @Override + public Node createBranch( + final byte leftIndex, final Node left, final byte rightIndex, final Node right) { + assert (leftIndex <= BranchNode.RADIX); + assert (rightIndex <= BranchNode.RADIX); + assert (leftIndex != rightIndex); + + final ArrayList> children = + new ArrayList<>(Collections.nCopies(BranchNode.RADIX, (Node) NULL_NODE)); + + if (leftIndex == BranchNode.RADIX) { + children.set(rightIndex, right); + return createBranch(children, left.getValue()); + } else if (rightIndex == BranchNode.RADIX) { + children.set(leftIndex, left); + return createBranch(children, right.getValue()); + } else { + children.set(leftIndex, left); + children.set(rightIndex, right); + return createBranch(children, Optional.empty()); + } + } + + @Override + public Node createBranch(final ArrayList> children, final Optional value) { + return handleNewNode(new BranchNode<>(children, value, this, valueSerializer)); + } + + @Override + public Node createLeaf(final BytesValue path, final V value) { + return handleNewNode(new LeafNode<>(path, value, this, valueSerializer)); + } + + private Node handleNewNode(final Node node) { + node.markDirty(); + return node; + } + + public Node retrieve(final Bytes32 hash) throws MerkleStorageException { + return nodeLoader + .getNode(hash) + .map( + rlp -> { + final Node node = decode(rlp, () -> format("Invalid RLP value for hash %s", hash)); + // recalculating the node.hash() is expensive, so we only do this as an assertion + assert (hash.equals(node.getHash())) + : "Node hash " + node.getHash() + " not equal to expected " + hash; + return node; + }) + .orElseThrow(() -> new MerkleStorageException("Missing value for hash " + hash)); + } + + private Node decode(final BytesValue rlp, final Supplier errMessage) + throws MerkleStorageException { + try { + return decode(RLP.input(rlp), errMessage); + } catch (final RLPException ex) { + throw new MerkleStorageException(errMessage.get(), ex); + } + } + + private Node decode(final RLPInput nodeRLPs, final Supplier errMessage) { + final int nodesCount = nodeRLPs.enterList(); + try { + switch (nodesCount) { + case 1: + return decodeNull(nodeRLPs, errMessage); + + case 2: + final BytesValue encodedPath = nodeRLPs.readBytesValue(); + BytesValue path; + try { + path = CompactEncoding.decode(encodedPath); + } catch (final IllegalArgumentException ex) { + throw new MerkleStorageException( + errMessage.get() + ": invalid path " + encodedPath, ex); + } + + final int size = path.size(); + if (size > 0 && path.get(size - 1) == CompactEncoding.LEAF_TERMINATOR) { + return decodeLeaf(path, nodeRLPs, errMessage); + } else { + return decodeExtension(path, nodeRLPs, errMessage); + } + + case (BranchNode.RADIX + 1): + return decodeBranch(nodeRLPs, errMessage); + + default: + throw new MerkleStorageException( + errMessage.get() + format(": invalid list size %s", nodesCount)); + } + } finally { + nodeRLPs.leaveList(); + } + } + + private Node decodeExtension( + final BytesValue path, final RLPInput valueRlp, final Supplier errMessage) { + final RLPInput childRlp = valueRlp.readAsRlp(); + if (childRlp.nextIsList()) { + final Node childNode = decode(childRlp, errMessage); + return new ExtensionNode<>(path, childNode, this); + } else { + final Bytes32 childHash = childRlp.readBytes32(); + final StoredNode childNode = new StoredNode<>(this, childHash); + return new ExtensionNode<>(path, childNode, this); + } + } + + @SuppressWarnings("unchecked") + private BranchNode decodeBranch(final RLPInput nodeRLPs, final Supplier errMessage) { + final ArrayList> children = new ArrayList<>(BranchNode.RADIX); + for (int i = 0; i < BranchNode.RADIX; ++i) { + if (nodeRLPs.nextIsNull()) { + nodeRLPs.skipNext(); + children.add(NULL_NODE); + } else if (nodeRLPs.nextIsList()) { + final Node child = decode(nodeRLPs, errMessage); + children.add(child); + } else { + final Bytes32 childHash = nodeRLPs.readBytes32(); + children.add(new StoredNode<>(this, childHash)); + } + } + + Optional value; + if (nodeRLPs.nextIsNull()) { + nodeRLPs.skipNext(); + value = Optional.empty(); + } else { + value = Optional.of(decodeValue(nodeRLPs, errMessage)); + } + + return new BranchNode<>(children, value, this, valueSerializer); + } + + private LeafNode decodeLeaf( + final BytesValue path, final RLPInput valueRlp, final Supplier errMessage) { + if (valueRlp.nextIsNull()) { + throw new MerkleStorageException(errMessage.get() + ": leaf has null value"); + } + final V value = decodeValue(valueRlp, errMessage); + return new LeafNode<>(path, value, this, valueSerializer); + } + + @SuppressWarnings("unchecked") + private NullNode decodeNull(final RLPInput nodeRLPs, final Supplier errMessage) { + if (!nodeRLPs.nextIsNull()) { + throw new MerkleStorageException(errMessage.get() + ": list size 1 but not null"); + } + nodeRLPs.skipNext(); + return NULL_NODE; + } + + private V decodeValue(final RLPInput valueRlp, final Supplier errMessage) { + BytesValue bytes; + try { + bytes = valueRlp.readBytesValue(); + } catch (final RLPException ex) { + throw new MerkleStorageException( + errMessage.get() + ": failed decoding value rlp " + valueRlp, ex); + } + return deserializeValue(errMessage, bytes); + } + + private V deserializeValue(final Supplier errMessage, final BytesValue bytes) { + V value; + try { + value = valueDeserializer.apply(bytes); + } catch (final IllegalArgumentException ex) { + throw new MerkleStorageException( + errMessage.get() + ": failed deserializing value " + bytes, ex); + } + return value; + } +} diff --git a/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/TrieIterator.java b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/TrieIterator.java new file mode 100755 index 00000000000..8bf30190dd8 --- /dev/null +++ b/ethereum/trie/src/main/java/net/consensys/pantheon/ethereum/trie/TrieIterator.java @@ -0,0 +1,96 @@ +package net.consensys.pantheon.ethereum.trie; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +public class TrieIterator implements PathNodeVisitor { + + private final Deque paths = new ArrayDeque<>(); + private final LeafHandler leafHandler; + private State state = State.SEARCHING; + + public TrieIterator(final LeafHandler leafHandler) { + this.leafHandler = leafHandler; + } + + @Override + public Node visit(final ExtensionNode node, final BytesValue searchPath) { + BytesValue remainingPath = searchPath; + if (state == State.SEARCHING) { + final BytesValue extensionPath = node.getPath(); + final int commonPathLength = extensionPath.commonPrefixLength(searchPath); + remainingPath = searchPath.slice(commonPathLength); + } + + paths.push(node.getPath()); + node.getChild().accept(this, remainingPath); + paths.pop(); + return node; + } + + @Override + public Node visit(final BranchNode node, final BytesValue searchPath) { + byte iterateFrom = 0; + BytesValue remainingPath = searchPath; + if (state == State.SEARCHING) { + iterateFrom = searchPath.get(0); + if (iterateFrom == CompactEncoding.LEAF_TERMINATOR) { + return node; + } + remainingPath = searchPath.slice(1); + } + paths.push(node.getPath()); + for (byte i = iterateFrom; i < BranchNode.RADIX && state.continueIterating(); i++) { + paths.push(BytesValue.of(i)); + node.child(i).accept(this, remainingPath); + paths.pop(); + } + paths.pop(); + return node; + } + + @Override + public Node visit(final LeafNode node, final BytesValue path) { + paths.push(node.getPath()); + state = State.CONTINUE; + state = leafHandler.onLeaf(keyHash(), node); + paths.pop(); + return node; + } + + @Override + public Node visit(final NullNode node, final BytesValue path) { + state = State.CONTINUE; + return node; + } + + private Bytes32 keyHash() { + final Iterator iterator = paths.descendingIterator(); + BytesValue fullPath = iterator.next(); + while (iterator.hasNext()) { + fullPath = BytesValue.wrap(fullPath, iterator.next()); + } + return fullPath.isZero() + ? Bytes32.ZERO + : Bytes32.wrap(CompactEncoding.pathToBytes(fullPath), 0); + } + + public interface LeafHandler { + + State onLeaf(Bytes32 keyHash, Node node); + } + + public enum State { + SEARCHING, + CONTINUE, + STOP; + + public boolean continueIterating() { + return this != STOP; + } + } +} diff --git a/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/CompactEncodingTest.java b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/CompactEncodingTest.java new file mode 100755 index 00000000000..60835672560 --- /dev/null +++ b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/CompactEncodingTest.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.ethereum.trie; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.util.Random; + +import org.junit.Test; + +public class CompactEncodingTest { + + @Test + public void bytesToPath() { + final BytesValue path = CompactEncoding.bytesToPath(BytesValue.of(0xab, 0xcd, 0xff)); + assertThat(path).isEqualTo(BytesValue.of(0xa, 0xb, 0xc, 0xd, 0xf, 0xf, 0x10)); + } + + @Test + public void shouldRoundTripFromBytesToPathAndBack() { + final Random random = new Random(282943948928429484L); + for (int i = 0; i < 1000; i++) { + final Bytes32 bytes = Hash.keccak256(UInt256.of(Math.abs(random.nextInt())).getBytes()); + final BytesValue path = CompactEncoding.bytesToPath(bytes); + assertThat(CompactEncoding.pathToBytes(path)).isEqualTo(bytes); + } + } + + @Test + public void encodePath() { + assertThat(CompactEncoding.encode(BytesValue.of(0x01, 0x02, 0x03, 0x04, 0x05))) + .isEqualTo(BytesValue.of(0x11, 0x23, 0x45)); + assertThat(CompactEncoding.encode(BytesValue.of(0x00, 0x01, 0x02, 0x03, 0x04, 0x05))) + .isEqualTo(BytesValue.of(0x00, 0x01, 0x23, 0x45)); + assertThat(CompactEncoding.encode(BytesValue.of(0x00, 0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10))) + .isEqualTo(BytesValue.of(0x20, 0x0f, 0x1c, 0xb8)); + assertThat(CompactEncoding.encode(BytesValue.of(0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10))) + .isEqualTo(BytesValue.of(0x3f, 0x1c, 0xb8)); + } + + @Test + public void decode() { + assertThat(CompactEncoding.decode(BytesValue.of(0x11, 0x23, 0x45))) + .isEqualTo(BytesValue.of(0x01, 0x02, 0x03, 0x04, 0x05)); + assertThat(CompactEncoding.decode(BytesValue.of(0x00, 0x01, 0x23, 0x45))) + .isEqualTo(BytesValue.of(0x00, 0x01, 0x02, 0x03, 0x04, 0x05)); + assertThat(CompactEncoding.decode(BytesValue.of(0x20, 0x0f, 0x1c, 0xb8))) + .isEqualTo(BytesValue.of(0x00, 0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10)); + assertThat(CompactEncoding.decode(BytesValue.of(0x3f, 0x1c, 0xb8))) + .isEqualTo(BytesValue.of(0x0f, 0x01, 0x0c, 0x0b, 0x08, 0x10)); + } +} diff --git a/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java new file mode 100755 index 00000000000..5df8f462f72 --- /dev/null +++ b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java @@ -0,0 +1,276 @@ +package net.consensys.pantheon.ethereum.trie; + +import static junit.framework.TestCase.assertFalse; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.nio.charset.Charset; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +public class SimpleMerklePatriciaTrieTest { + private SimpleMerklePatriciaTrie trie; + + @Before + public void setup() { + trie = + new SimpleMerklePatriciaTrie<>( + value -> + (value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null); + } + + @Test + public void emptyTreeReturnsEmpty() { + assertFalse(trie.get(BytesValue.EMPTY).isPresent()); + } + + @Test + public void emptyTreeHasKnownRootHash() { + assertThat(trie.getRootHash().toString()) + .isEqualTo("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + } + + @Test(expected = NullPointerException.class) + public void throwsOnUpdateWithNull() { + trie.put(BytesValue.EMPTY, null); + } + + @Test + public void replaceSingleValue() { + final BytesValue key = BytesValue.of(1); + final String value1 = "value1"; + trie.put(key, value1); + assertThat(trie.get(key)).isEqualTo(Optional.of(value1)); + + final String value2 = "value2"; + trie.put(key, value2); + assertThat(trie.get(key)).isEqualTo(Optional.of(value2)); + } + + @Test + public void hashChangesWhenSingleValueReplaced() { + final BytesValue key = BytesValue.of(1); + final String value1 = "value1"; + trie.put(key, value1); + final Bytes32 hash1 = trie.getRootHash(); + + final String value2 = "value2"; + trie.put(key, value2); + final Bytes32 hash2 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash2); + + trie.put(key, value1); + assertThat(trie.getRootHash()).isEqualTo(hash1); + } + + @Test + public void readPastLeaf() { + final BytesValue key1 = BytesValue.of(1); + trie.put(key1, "value"); + final BytesValue key2 = BytesValue.of(1, 3); + assertFalse(trie.get(key2).isPresent()); + } + + @Test + public void branchValue() { + final BytesValue key1 = BytesValue.of(1); + final BytesValue key2 = BytesValue.of(16); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void readPastBranch() { + final BytesValue key1 = BytesValue.of(12); + final BytesValue key2 = BytesValue.of(12, 54); + + final String value1 = "value1"; + trie.put(key1, value1); + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue key3 = BytesValue.of(3); + assertFalse(trie.get(key3).isPresent()); + } + + @Test + public void branchWithValue() { + final BytesValue key1 = BytesValue.of(5); + final BytesValue key2 = BytesValue.EMPTY; + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void extendAndBranch() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); + } + + @Test + public void branchFromTopOfExtend() { + final BytesValue key1 = BytesValue.of(0xfe, 1); + final BytesValue key2 = BytesValue.of(0xfe, 2); + final BytesValue key3 = BytesValue.of(0xe1, 1); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final String value3 = "value3"; + trie.put(key3, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); + assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); + assertFalse(trie.get(BytesValue.of(2, 4)).isPresent()); + assertFalse(trie.get(BytesValue.of(3)).isPresent()); + } + + @Test + public void splitBranchExtension() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue key3 = BytesValue.of(1, 9, 1); + + final String value3 = "value3"; + trie.put(key3, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); + } + + @Test + public void replaceBranchChild() { + final BytesValue key1 = BytesValue.of(0); + final BytesValue key2 = BytesValue.of(1); + + final String value1 = "value1"; + trie.put(key1, value1); + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + + final String value3 = "value3"; + trie.put(key1, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value3)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void inlineBranchInBranch() { + final BytesValue key1 = BytesValue.of(0); + final BytesValue key2 = BytesValue.of(1); + final BytesValue key3 = BytesValue.of(2); + final BytesValue key4 = BytesValue.of(0, 0); + final BytesValue key5 = BytesValue.of(0, 1); + + trie.put(key1, "value1"); + trie.put(key2, "value2"); + trie.put(key3, "value3"); + trie.put(key4, "value4"); + trie.put(key5, "value5"); + + trie.remove(key2); + trie.remove(key3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); + assertFalse(trie.get(key2).isPresent()); + assertFalse(trie.get(key3).isPresent()); + assertThat(trie.get(key4)).isEqualTo(Optional.of("value4")); + assertThat(trie.get(key5)).isEqualTo(Optional.of("value5")); + } + + @Test + public void removeNodeInBranchExtensionHasNoEffect() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final Bytes32 hash = trie.getRootHash(); + + trie.remove(BytesValue.of(1, 4)); + assertThat(trie.getRootHash()).isEqualTo(hash); + } + + @Test + public void hashChangesWhenValueChanged() { + final BytesValue key1 = BytesValue.of(1, 5, 8, 9); + final BytesValue key2 = BytesValue.of(1, 6, 1, 2); + final BytesValue key3 = BytesValue.of(1, 6, 1, 3); + + final String value1 = "value1"; + trie.put(key1, value1); + final Bytes32 hash1 = trie.getRootHash(); + + final String value2 = "value2"; + trie.put(key2, value2); + final String value3 = "value3"; + trie.put(key3, value3); + final Bytes32 hash2 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash2); + + final String value4 = "value4"; + trie.put(key1, value4); + final Bytes32 hash3 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash3); + assertThat(hash2).isNotEqualTo(hash3); + + trie.put(key1, value1); + assertThat(trie.getRootHash()).isEqualTo(hash2); + + trie.remove(key2); + trie.remove(key3); + assertThat(trie.getRootHash()).isEqualTo(hash1); + } +} diff --git a/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java new file mode 100755 index 00000000000..46e0e426ce1 --- /dev/null +++ b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java @@ -0,0 +1,410 @@ +package net.consensys.pantheon.ethereum.trie; + +import static junit.framework.TestCase.assertFalse; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import net.consensys.pantheon.services.kvstore.KeyValueStorage; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.nio.charset.Charset; +import java.util.Optional; +import java.util.function.Function; + +import org.junit.Before; +import org.junit.Test; + +public class StoredMerklePatriciaTrieTest { + private KeyValueStorage keyValueStore; + private MerkleStorage merkleStorage; + private Function valueSerializer; + private Function valueDeserializer; + private StoredMerklePatriciaTrie trie; + + @Before + public void setup() { + keyValueStore = new InMemoryKeyValueStorage(); + merkleStorage = new KeyValueMerkleStorage(keyValueStore); + valueSerializer = + value -> (value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null; + valueDeserializer = bytes -> new String(bytes.getArrayUnsafe(), Charset.forName("UTF-8")); + trie = new StoredMerklePatriciaTrie<>(merkleStorage::get, valueSerializer, valueDeserializer); + } + + @Test + public void emptyTreeReturnsEmpty() { + assertFalse(trie.get(BytesValue.EMPTY).isPresent()); + } + + @Test + public void emptyTreeHasKnownRootHash() { + assertThat(trie.getRootHash().toString()) + .isEqualTo("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + } + + @Test(expected = NullPointerException.class) + public void throwsOnUpdateWithNull() { + trie.put(BytesValue.EMPTY, null); + } + + @Test + public void replaceSingleValue() { + final BytesValue key = BytesValue.of(1); + final String value1 = "value1"; + trie.put(key, value1); + assertThat(trie.get(key)).isEqualTo(Optional.of(value1)); + + final String value2 = "value2"; + trie.put(key, value2); + assertThat(trie.get(key)).isEqualTo(Optional.of(value2)); + } + + @Test + public void hashChangesWhenSingleValueReplaced() { + final BytesValue key = BytesValue.of(1); + final String value1 = "value1"; + trie.put(key, value1); + final Bytes32 hash1 = trie.getRootHash(); + + final String value2 = "value2"; + trie.put(key, value2); + final Bytes32 hash2 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash2); + + trie.put(key, value1); + assertThat(trie.getRootHash()).isEqualTo(hash1); + } + + @Test + public void readPastLeaf() { + final BytesValue key1 = BytesValue.of(1); + trie.put(key1, "value"); + final BytesValue key2 = BytesValue.of(1, 3); + assertFalse(trie.get(key2).isPresent()); + } + + @Test + public void branchValue() { + final BytesValue key1 = BytesValue.of(1); + final BytesValue key2 = BytesValue.of(16); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void readPastBranch() { + final BytesValue key1 = BytesValue.of(12); + final BytesValue key2 = BytesValue.of(12, 54); + + final String value1 = "value1"; + trie.put(key1, value1); + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue key3 = BytesValue.of(3); + assertFalse(trie.get(key3).isPresent()); + } + + @Test + public void branchWithValue() { + final BytesValue key1 = BytesValue.of(5); + final BytesValue key2 = BytesValue.EMPTY; + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void extendAndBranch() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); + } + + @Test + public void branchFromTopOfExtend() { + final BytesValue key1 = BytesValue.of(0xfe, 1); + final BytesValue key2 = BytesValue.of(0xfe, 2); + final BytesValue key3 = BytesValue.of(0xe1, 1); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final String value3 = "value3"; + trie.put(key3, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); + assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); + assertFalse(trie.get(BytesValue.of(2, 4)).isPresent()); + assertFalse(trie.get(BytesValue.of(3)).isPresent()); + } + + @Test + public void splitBranchExtension() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue key3 = BytesValue.of(1, 9, 1); + + final String value3 = "value3"; + trie.put(key3, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); + } + + @Test + public void replaceBranchChild() { + final BytesValue key1 = BytesValue.of(0); + final BytesValue key2 = BytesValue.of(1); + + final String value1 = "value1"; + trie.put(key1, value1); + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + + final String value3 = "value3"; + trie.put(key1, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value3)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void inlineBranchInBranch() { + final BytesValue key1 = BytesValue.of(0); + final BytesValue key2 = BytesValue.of(1); + final BytesValue key3 = BytesValue.of(2); + final BytesValue key4 = BytesValue.of(0, 0); + final BytesValue key5 = BytesValue.of(0, 1); + + trie.put(key1, "value1"); + trie.put(key2, "value2"); + trie.put(key3, "value3"); + trie.put(key4, "value4"); + trie.put(key5, "value5"); + + trie.remove(key2); + trie.remove(key3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); + assertFalse(trie.get(key2).isPresent()); + assertFalse(trie.get(key3).isPresent()); + assertThat(trie.get(key4)).isEqualTo(Optional.of("value4")); + assertThat(trie.get(key5)).isEqualTo(Optional.of("value5")); + } + + @Test + public void removeNodeInBranchExtensionHasNoEffect() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue hash = trie.getRootHash(); + + trie.remove(BytesValue.of(1, 4)); + assertThat(trie.getRootHash()).isEqualTo(hash); + } + + @Test + public void hashChangesWhenValueChanged() { + final BytesValue key1 = BytesValue.of(1, 5, 8, 9); + final BytesValue key2 = BytesValue.of(1, 6, 1, 2); + final BytesValue key3 = BytesValue.of(1, 6, 1, 3); + + final String value1 = "value1"; + trie.put(key1, value1); + final Bytes32 hash1 = trie.getRootHash(); + + final String value2 = "value2"; + trie.put(key2, value2); + final String value3 = "value3"; + trie.put(key3, value3); + final Bytes32 hash2 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash2); + + final String value4 = "value4"; + trie.put(key1, value4); + final Bytes32 hash3 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash3); + assertThat(hash2).isNotEqualTo(hash3); + + trie.put(key1, value1); + assertThat(trie.getRootHash()).isEqualTo(hash2); + + trie.remove(key2); + trie.remove(key3); + assertThat(trie.getRootHash()).isEqualTo(hash1); + } + + @Test + public void canReloadTrieFromHash() { + final BytesValue key1 = BytesValue.of(1, 5, 8, 9); + final BytesValue key2 = BytesValue.of(1, 6, 1, 2); + final BytesValue key3 = BytesValue.of(1, 6, 1, 3); + + // Push some values into the trie and commit changes so nodes are persisted + final String value1 = "value1"; + trie.put(key1, value1); + final Bytes32 hash1 = trie.getRootHash(); + trie.commit(merkleStorage::put); + + final String value2 = "value2"; + trie.put(key2, value2); + final String value3 = "value3"; + trie.put(key3, value3); + final Bytes32 hash2 = trie.getRootHash(); + trie.commit(merkleStorage::put); + + final String value4 = "value4"; + trie.put(key1, value4); + final Bytes32 hash3 = trie.getRootHash(); + trie.commit(merkleStorage::put); + + // Check the root hashes for 3 tries are all distinct + assertThat(hash1).isNotEqualTo(hash2); + assertThat(hash1).isNotEqualTo(hash3); + assertThat(hash2).isNotEqualTo(hash3); + // And that we can retrieve the last value we set for key1 + assertThat(trie.get(key1)).isEqualTo(Optional.of("value4")); + + // Create new tries from root hashes and check that we find expected values + trie = + new StoredMerklePatriciaTrie<>( + merkleStorage::get, hash1, valueSerializer, valueDeserializer); + assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); + assertThat(trie.get(key2)).isEqualTo(Optional.empty()); + assertThat(trie.get(key3)).isEqualTo(Optional.empty()); + + trie = + new StoredMerklePatriciaTrie<>( + merkleStorage::get, hash2, valueSerializer, valueDeserializer); + assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); + assertThat(trie.get(key2)).isEqualTo(Optional.of("value2")); + assertThat(trie.get(key3)).isEqualTo(Optional.of("value3")); + + trie = + new StoredMerklePatriciaTrie<>( + merkleStorage::get, hash3, valueSerializer, valueDeserializer); + assertThat(trie.get(key1)).isEqualTo(Optional.of("value4")); + assertThat(trie.get(key2)).isEqualTo(Optional.of("value2")); + assertThat(trie.get(key3)).isEqualTo(Optional.of("value3")); + + // Commit changes to storage, and create new tries from roothash and new storage instance + assertThat(keyValueStore.entries().count()).isEqualTo(0); + merkleStorage.commit(); + assertThat(keyValueStore.entries().count()).isGreaterThan(0); + final MerkleStorage newMerkleStorage = new KeyValueMerkleStorage(keyValueStore); + trie = + new StoredMerklePatriciaTrie<>( + newMerkleStorage::get, hash1, valueSerializer, valueDeserializer); + assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); + assertThat(trie.get(key2)).isEqualTo(Optional.empty()); + assertThat(trie.get(key3)).isEqualTo(Optional.empty()); + + trie = + new StoredMerklePatriciaTrie<>( + newMerkleStorage::get, hash2, valueSerializer, valueDeserializer); + assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); + assertThat(trie.get(key2)).isEqualTo(Optional.of("value2")); + assertThat(trie.get(key3)).isEqualTo(Optional.of("value3")); + + trie = + new StoredMerklePatriciaTrie<>( + newMerkleStorage::get, hash3, valueSerializer, valueDeserializer); + assertThat(trie.get(key1)).isEqualTo(Optional.of("value4")); + assertThat(trie.get(key2)).isEqualTo(Optional.of("value2")); + assertThat(trie.get(key3)).isEqualTo(Optional.of("value3")); + } + + @Test + public void shouldRetrieveStoredExtensionWithInlinedChild() { + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + final MerkleStorage merkleStorage = new KeyValueMerkleStorage(keyValueStorage); + final StoredMerklePatriciaTrie trie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, b -> b, b -> b); + + // Both of these can be inlined in its parent branch and the branch + // itself can be inlined into its parent extension. + trie.put(BytesValue.fromHexString("0x0400"), BytesValue.of(1)); + trie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(2)); + trie.commit(merkleStorage::put); + + // Ensure the extension branch can be loaded correct with its inlined child. + final Bytes32 rootHash = trie.getRootHash(); + final StoredMerklePatriciaTrie newTrie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, rootHash, b -> b, b -> b); + newTrie.get(BytesValue.fromHexString("0x0401")); + } + + @Test + public void shouldInlineNodesInParentAcrossModifications() { + // Misuse of StorageNode allowed inlineable trie nodes to end + // up being stored as a hash in its parent, which this would fail for. + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + final MerkleStorage merkleStorage = new KeyValueMerkleStorage(keyValueStorage); + final StoredMerklePatriciaTrie trie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, b -> b, b -> b); + + // Both of these can be inlined in its parent branch. + trie.put(BytesValue.fromHexString("0x0400"), BytesValue.of(1)); + trie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(2)); + trie.commit(merkleStorage::put); + + final Bytes32 rootHash = trie.getRootHash(); + final StoredMerklePatriciaTrie newTrie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, rootHash, b -> b, b -> b); + + newTrie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(3)); + newTrie.get(BytesValue.fromHexString("0x0401")); + trie.commit(merkleStorage::put); + + newTrie.get(BytesValue.fromHexString("0x0401")); + } +} diff --git a/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieIteratorTest.java b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieIteratorTest.java new file mode 100755 index 00000000000..ae0214010de --- /dev/null +++ b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieIteratorTest.java @@ -0,0 +1,132 @@ +package net.consensys.pantheon.ethereum.trie; + +import static net.consensys.pantheon.ethereum.trie.CompactEncoding.bytesToPath; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.Hash; +import net.consensys.pantheon.ethereum.trie.TrieIterator.LeafHandler; +import net.consensys.pantheon.ethereum.trie.TrieIterator.State; +import net.consensys.pantheon.util.bytes.Bytes32; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.nio.charset.StandardCharsets; +import java.util.NavigableSet; +import java.util.Random; +import java.util.TreeSet; +import java.util.function.Function; + +import org.junit.Test; +import org.mockito.InOrder; + +public class TrieIteratorTest { + + private static final Bytes32 KEY_HASH1 = + Bytes32.fromHexString("0x5555555555555555555555555555555555555555555555555555555555555555"); + private static final Bytes32 KEY_HASH2 = + Bytes32.fromHexString("0x5555555555555555555555555555555555555555555555555555555555555556"); + private static final BytesValue PATH1 = bytesToPath(KEY_HASH1); + private static final BytesValue PATH2 = bytesToPath(KEY_HASH2); + + @SuppressWarnings("unchecked") + private final LeafHandler leafHandler = mock(LeafHandler.class); + + private final Function valueSerializer = + value -> BytesValue.wrap(value.getBytes(StandardCharsets.UTF_8)); + private final DefaultNodeFactory nodeFactory = new DefaultNodeFactory<>(valueSerializer); + private final TrieIterator iterator = new TrieIterator<>(leafHandler); + + @Test + public void shouldCallLeafHandlerWhenRootNodeIsALeaf() { + final Node leaf = nodeFactory.createLeaf(bytesToPath(KEY_HASH1), "Leaf"); + leaf.accept(iterator, PATH1); + + verify(leafHandler).onLeaf(KEY_HASH1, leaf); + } + + @Test + public void shouldNotNotifyLeafHandlerOfNullNodes() { + NullNode.instance().accept(iterator, PATH1); + + verifyZeroInteractions(leafHandler); + } + + @Test + public void shouldConcatenatePathAndVisitChildOfExtensionNode() { + final Node leaf = nodeFactory.createLeaf(PATH1.slice(10), "Leaf"); + final Node extension = nodeFactory.createExtension(PATH1.slice(0, 10), leaf); + extension.accept(iterator, PATH1); + verify(leafHandler).onLeaf(KEY_HASH1, leaf); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldVisitEachChildOfABranchNode() { + when(leafHandler.onLeaf(any(Bytes32.class), any(Node.class))).thenReturn(State.CONTINUE); + final Node root = + NullNode.instance() + .accept(new PutVisitor<>(nodeFactory, "Leaf 1"), PATH1) + .accept(new PutVisitor<>(nodeFactory, "Leaf 2"), PATH2); + root.accept(iterator, PATH1); + + final InOrder inOrder = inOrder(leafHandler); + inOrder.verify(leafHandler).onLeaf(eq(KEY_HASH1), any(Node.class)); + inOrder.verify(leafHandler).onLeaf(eq(KEY_HASH2), any(Node.class)); + verifyNoMoreInteractions(leafHandler); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldStopIteratingChildrenOfBranchWhenLeafHandlerReturnsStop() { + when(leafHandler.onLeaf(any(Bytes32.class), any(Node.class))).thenReturn(State.STOP); + final Node root = + NullNode.instance() + .accept(new PutVisitor<>(nodeFactory, "Leaf 1"), PATH1) + .accept(new PutVisitor<>(nodeFactory, "Leaf 2"), PATH2); + root.accept(iterator, PATH1); + + verify(leafHandler).onLeaf(eq(KEY_HASH1), any(Node.class)); + verifyNoMoreInteractions(leafHandler); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldIterateArbitraryStructureAccurately() { + Node root = NullNode.instance(); + final NavigableSet expectedKeyHashes = new TreeSet<>(); + final Random random = new Random(-5407159858935967790L); + Bytes32 startAtHash = Bytes32.ZERO; + Bytes32 stopAtHash = Bytes32.ZERO; + final int totalNodes = Math.abs(random.nextInt(1000)); + final int startNodeNumber = random.nextInt(Math.max(1, totalNodes - 1)); + final int stopNodeNumber = random.nextInt(Math.max(1, totalNodes - 1)); + for (int i = 0; i < totalNodes; i++) { + final Bytes32 keyHash = Hash.keccak256(UInt256.of(Math.abs(random.nextLong())).getBytes()); + root = root.accept(new PutVisitor<>(nodeFactory, "Value"), bytesToPath(keyHash)); + expectedKeyHashes.add(keyHash); + if (i == startNodeNumber) { + startAtHash = keyHash; + } else if (i == stopNodeNumber) { + stopAtHash = keyHash; + } + } + + final Bytes32 actualStopAtHash = + stopAtHash.compareTo(startAtHash) >= 0 ? stopAtHash : startAtHash; + when(leafHandler.onLeaf(any(Bytes32.class), any(Node.class))).thenReturn(State.CONTINUE); + when(leafHandler.onLeaf(eq(actualStopAtHash), any(Node.class))).thenReturn(State.STOP); + root.accept(iterator, bytesToPath(startAtHash)); + final InOrder inOrder = inOrder(leafHandler); + expectedKeyHashes + .subSet(startAtHash, true, actualStopAtHash, true) + .forEach(keyHash -> inOrder.verify(leafHandler).onLeaf(eq(keyHash), any(Node.class))); + verifyNoMoreInteractions(leafHandler); + } +} diff --git a/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTest.java b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTest.java new file mode 100755 index 00000000000..cff87675844 --- /dev/null +++ b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTest.java @@ -0,0 +1,46 @@ +package net.consensys.pantheon.ethereum.trie; + +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.testutil.JsonTestParameters; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.Collection; +import java.util.function.Function; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TrieRefTest { + + private static final String[] TEST_CONFIG_FILES = {"TrieTests/trietest.json"}; + + private final TrieRefTestCaseSpec spec; + + public TrieRefTest(final String name, final TrieRefTestCaseSpec spec) { + this.spec = spec; + } + + @Parameters(name = "Name: {0}") + public static Collection getTestParametersForConfig() { + return JsonTestParameters.create(TrieRefTestCaseSpec.class).generate(TEST_CONFIG_FILES); + } + + @Test + public void rootHashAfterInsertionsAndRemovals() { + final SimpleMerklePatriciaTrie trie = + new SimpleMerklePatriciaTrie<>(Function.identity()); + for (final BytesValue[] pair : spec.getIn()) { + if (pair[1] == null) { + trie.remove(pair[0]); + } else { + trie.put(pair[0], pair[1]); + } + } + + assertThat(spec.getRoot()).isEqualTo(trie.getRootHash()); + } +} diff --git a/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTestCaseSpec.java b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTestCaseSpec.java new file mode 100755 index 00000000000..810ce14d80d --- /dev/null +++ b/ethereum/trie/src/test/java/net/consensys/pantheon/ethereum/trie/TrieRefTestCaseSpec.java @@ -0,0 +1,81 @@ +package net.consensys.pantheon.ethereum.trie; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A Trie reference test case specification. + * + *

Note: this class will be auto-generated with the JSON test specification. + */ +public class TrieRefTestCaseSpec { + + /** The set of inputs to insert into the Trie. */ + private final BytesValue[][] in; + + /** The expected root hash of the Trie after all inputs have been entered. */ + private final BytesValue root; + + /** + * Public constructor. + * + * @param inAsObj The set of inputs to insert into the Trie. + * @param root The expected root hash of the Trie after all inputs have been entered. + */ + @JsonCreator + public TrieRefTestCaseSpec( + @JsonProperty("in") final Object inAsObj, @JsonProperty("root") final String root) { + if (inAsObj instanceof ArrayList) { + @SuppressWarnings("unchecked") + final ArrayList> in = (ArrayList>) inAsObj; + + this.in = new BytesValue[in.size()][2]; + + for (int i = 0; i < in.size(); ++i) { + final String key = in.get(i).get(0); + final String value = in.get(i).get(1); + + this.in[i][0] = stringParamToBytes(key); + this.in[i][1] = stringParamToBytes(value); + } + } else { + throw new RuntimeException("in has unknown structure."); + } + + this.root = BytesValue.fromHexStringLenient(root); + } + + private BytesValue stringParamToBytes(final String s) { + if (s == null) { + return null; + } + if (s.startsWith("0x")) { + return BytesValue.fromHexString(s); + } + return BytesValue.wrap(s.getBytes(UTF_8)); + } + + /** + * Returns the set of inputs to insert into the Trie. + * + * @return The set of inputs to insert into the Trie. + */ + public BytesValue[][] getIn() { + return in; + } + + /** + * Returns the expected root hash of the Trie after all inputs have been entered. + * + * @return The expected root hash of the Trie after all inputs have been entered. + */ + public BytesValue getRoot() { + return root; + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100755 index 00000000000..fad0c094005 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1g diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle new file mode 100755 index 00000000000..d9cd632779b --- /dev/null +++ b/gradle/check-licenses.gradle @@ -0,0 +1,162 @@ +/** + * Check that the licenses of our 3rd parties are in our acceptedLicenses list. + * + * run it with "gradle checkLicenses" + * + * To add new accepted licenses you need to update this script. + * Some products may be available with multiple licenses. In this case you must update + * this script to add it in the downloadLicenses#licenses. + */ + +// Some parts of this code comes from Zipkin/https://github.com/openzipkin/zipkin/pull/852 +// Zipkin itself is under Apache License. + +/** + * The lists of the license we accept. + */ +ext.acceptedLicenses = [ + 'BSD License', + 'BSD 3-Clause', + 'Eclipse Public License - v 1.0', + 'Eclipse Public License 1.0', + 'MIT License', + 'Apache License, Version 2.0', + 'Bouncy Castle Licence', + 'Public Domain', + 'Mozilla Public License 1.0', + 'Mozilla Public License Version 1.1', + 'Mozilla Public License Version 2.0', + 'CC0 1.0 Universal License', + 'Common Development and Distribution License 1.0', + 'Unicode/ICU License', + 'No license found',// for temporary use with Picocli jar + 'TODO to be fixed' // use it for temporary states when a license needs investigation. +]*.toLowerCase() + +/** + * This is the configuration we need for our licenses plugin: 'com.github.hierynomus.license' + * This plugin generates a list of dependencies. + */ +downloadLicenses { + includeProjectDependencies = true + reportByDependency = false + reportByLicenseType = true + dependencyConfiguration = 'testCompile' + + ext.apache = license('Apache License, Version 2.0', 'http://opensource.org/licenses/Apache-2.0') + ext.mit = license('MIT License', 'http://www.opensource.org/licenses/mit-license.php') + ext.bsd = license('BSD License', 'http://www.opensource.org/licenses/bsd-license.php') + ext.bsd3Clause = license('BSD 3-Clause', 'http://opensource.org/licenses/BSD-3-Clause') + ext.mpl = license('Mozilla Public License', 'http://www.mozilla.org/MPL') + ext.mpl1_1 = license('Mozilla Public License Version 1.1', 'http://www.mozilla.org/MPL/1.1/') + ext.mpl2_0 = license('Mozilla Public License, Version 2.0', 'http://www.mozilla.org/MPL/2.0/') + ext.cddl = license('Common Development and Distribution License 1.0', 'http://opensource.org/licenses/CDDL-1.0') + ext.cddl1_1 = license('Common Development and Distribution License 1.0', 'http://oss.oracle.com/licenses/CDDL-1.1') + ext.todoToBeFixed = license('TODO to be fixed', 'TODO to be fixed') + aliases = [ + (apache) : [ + 'The Apache Software License, Version 2.0', + 'The Apache Software License, version 2.0', + 'Apache License Version 2.0', + 'Apache License, Version 2.0', + 'The Apache License, Version 2.0', + 'Apache 2', + 'Apache 2.0', + 'Apache License 2.0', + 'Apache-2.0', + license('Apache License', 'http://www.apache.org/licenses/LICENSE-2.0'), + license('Apache Software Licenses', 'http://www.apache.org/licenses/LICENSE-2.0.txt'), + license('Apache', 'http://www.opensource.org/licenses/Apache-2.0') + ], + (mit) : ['The MIT License'], + (bsd) : [ + 'BSD', + 'BSD licence', + 'The BSD License', + 'Berkeley Software Distribution (BSD) License', + license('New BSD License', 'http://www.opensource.org/licenses/bsd-license.php') + ], + (bsd3Clause): [ + 'BSD 3-Clause', + 'BSD 3-Clause "New" or "Revised" License (BSD-3-Clause)', + license('BSD 3-clause', 'http://opensource.org/licenses/BSD-3-Clause'), + license('BSD 3-Clause', 'http://www.scala-lang.org/license.html') + ], + (mpl): [ + 'MPL', + 'Mozilla Public License', + 'Mozilla Public License 1.0', + license('Mozilla Public License', 'http://www.mozilla.org/MPL') + ], + (mpl1_1): [ + 'MPL 1.1', + 'Mozilla Public License Version 1.1', + license('Mozilla Public License Version 1.1', 'http://www.mozilla.org/media/MPL/1.1/index.0c5913925d40.txt') + ], + (mpl2_0): [ + 'MPL 2.0', + 'Mozilla Public License Version 2.0', + license('Mozilla Public License 2.0', 'http://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt') + ], + (cddl): [ + 'CDDL', + 'Common Development and Distribution License 1.0', + 'CDDL + GPLv2 with classpath exception', + 'Dual license consisting of the CDDL v1.1 and GPL v2' + ], + (cddl1_1): [ + 'CDDL 1.1', + 'COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1', + ] + + ] + + licenses = [ + (group('pantheon')) : apache, + (group('pantheon.ethereum')) : apache, + (group('pantheon.services')) : apache, + (group('pantheon.consensus')) : apache, + + // https://checkerframework.org/manual/#license + // The more permissive MIT License applies to code that you might want + // to include in your own program, such as the annotations and run-time utility classes. + (group('org.checkerframework')): mit, + // RocksDB is dual licensed under Apache v2.0 and GPL 2 licenses + // Explicitly declare that we are using the Apache v2.0 license + (group('org.rocksdb')): apache, + /// Explicilitly declare Apache 2.0 license for javassist + (group('org.javassist')): apache, + /// Explicilitly declare Apache 2.0 license for javassist + (group('javax.ws.rs')): cddl1_1, + (group('org.glassfish.jersey.core')): apache, + (group('org.glassfish.jersey.bundles.repackaged')): apache, + (group('org.glassfish.jersey.connectors')): apache + ] +} + + +task checkLicenses { + description "Verify that all dependencies use white-listed licenses." + dependsOn ':downloadLicenses' + + def bads = "" + doLast { + def xml = new XmlParser().parse('build/reports/license/license-dependency.xml') + xml.each { license -> + if (!acceptedLicenses.contains((license.@name).toLowerCase())) { + def depStrings = [] + license.dependency.each { depStrings << it.text() } + bads = bads + depStrings + " => ${license.@name} \n" + } + } + if (bads != "") { + throw new GradleException("Some 3rd parties are using licenses not in our accepted licenses list:\n" + + bads + + "If it's a license acceptable for us, add it in the file check-licenses.gradle\n"+ + "Be careful, some 3rd parties may accept multiple licenses.\n" + + "In this case, select the one you want to use by changing downloadLicenses.licenses\n" + ) + } + } +} +check.dependsOn checkLicenses diff --git a/gradle/eclipse-java-google-style.xml b/gradle/eclipse-java-google-style.xml new file mode 100755 index 00000000000..34e5ecb0d8c --- /dev/null +++ b/gradle/eclipse-java-google-style.xml @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/formatter.properties b/gradle/formatter.properties new file mode 100755 index 00000000000..163a2e52981 --- /dev/null +++ b/gradle/formatter.properties @@ -0,0 +1,51 @@ +#Whether to use 'space', 'tab' or 'mixed' (both) characters for indentation. +#The default value is 'tab'. +org.eclipse.jdt.core.formatter.tabulation.char=space + +#Number of spaces used for indentation in case 'space' characters +#have been selected. The default value is 4. +org.eclipse.jdt.core.formatter.tabulation.size=2 + +#Number of spaces used for indentation in case 'mixed' characters +#have been selected. The default value is 4. +org.eclipse.jdt.core.formatter.indentation.size=1 + +#Whether or not indentation characters are inserted into empty lines. +#The default value is 'true'. +org.eclipse.jdt.core.formatter.indent_empty_lines=false + +#Number of spaces used for multiline indentation. +#The default value is 2. +groovy.formatter.multiline.indentation=1 + +#Length after which list are considered too long. These will be wrapped. +#The default value is 30. +groovy.formatter.longListLength=30 + +#Whether opening braces position shall be the next line. +#The default value is 'same'. +groovy.formatter.braces.start=same + +#Whether closing braces position shall be the next line. +#The default value is 'next'. +groovy.formatter.braces.end=next + +#Remove unnecessary semicolons. The default value is 'false'. +groovy.formatter.remove.unnecessary.semicolons=false + +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line + +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert diff --git a/gradle/versions.gradle b/gradle/versions.gradle new file mode 100755 index 00000000000..fee4d3d5dcc --- /dev/null +++ b/gradle/versions.gradle @@ -0,0 +1,49 @@ +dependencyManagement { + dependencies { + + dependency('com.google.guava:guava:23.6-jre') + dependency('commons-cli:commons-cli:1.4') + + dependency('org.apache.logging.log4j:log4j-api:2.10.0') + dependency('org.apache.logging.log4j:log4j-core:2.10.0') + dependency('org.apache.logging.log4j:log4j-slf4j-impl:2.10.0') + + dependency('org.bouncycastle:bcprov-jdk15on:1.58') + + dependency('junit:junit:4.12') + dependency('io.vertx:vertx-core:3.5.0') + dependency('io.vertx:vertx-unit:3.5.0') + dependency('io.vertx:vertx-web:3.5.0') + dependency('io.vertx:vertx-codegen:3.5.0') + + dependency('org.assertj:assertj-core:3.9.0') + + dependency('org.mockito:mockito-core:2.21.0') + + dependency('com.fasterxml.jackson.core:jackson-databind:2.9.7') + + dependency('com.squareup.okhttp3:okhttp:3.9.1') + + dependency('org.awaitility:awaitility:3.0.0') + + dependency('io.pkts:pkts-core:3.0.2') + + dependency('org.xerial.snappy:snappy-java:1.1.7.1') + + dependency('com.github.docker-java:docker-java:3.0.14') + + // Disable picocli while the 3.6 release with default enhancement is published. + // The jar is directly inserted until then. + // dependency('info.picocli:picocli:3.3.0') + dependency('net.consensys.cava:cava-toml:0.3.1') + + dependency('org.openjdk.jmh:jmh-core:1.21') + dependency('org.openjdk.jmh:jmh-generator-annprocess:1.21') + + dependency('org.web3j:core:3.5.0') + dependency("com.google.errorprone:error_prone_check_api:2.3.1") + dependency("com.google.errorprone:error_prone_core:2.3.1") + dependency("com.google.errorprone:error_prone_annotation:2.3.1") + dependency("com.google.errorprone:error_prone_test_helpers:2.3.1") + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..91ca28c8b802289c3a438766657a5e98f20eff03 GIT binary patch literal 54413 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 00000000000..e95643d6a2c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pantheon/build.gradle b/pantheon/build.gradle new file mode 100755 index 00000000000..bebede0f9b7 --- /dev/null +++ b/pantheon/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'java-library' + +jar { + baseName 'pantheon' + manifest { + attributes('Implementation-Title': baseName, + 'Implementation-Version': project.version) + } +} + +dependencies { + implementation project(':crypto') + implementation project(':consensus:common') + implementation project(':consensus:clique') + implementation project(':consensus:ibft') + implementation project(':consensus:clique') + implementation project(':ethereum:eth') + implementation project(':ethereum:client') + implementation project(':ethereum:core') + implementation project(':ethereum:rlp') + implementation project(':ethereum:p2p') + implementation project(':ethereum:jsonrpc') + implementation project(':services:kvstore') + + implementation 'com.google.guava:guava' + // Disable picocli while the 3.6 release with default enhancement is published. + // The jar is directly inserted until then. + // implementation 'info.picocli:picocli' + compile files('libs/picocli-3.6.0-SNAPSHOT.jar') + implementation 'net.consensys.cava:cava-toml' + implementation 'io.vertx:vertx-core' + implementation 'io.vertx:vertx-web' + implementation 'org.apache.logging.log4j:log4j-api' + + runtime 'org.apache.logging.log4j:log4j-core' + + testImplementation project(':testutil') + testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts') + + testImplementation 'com.squareup.okhttp3:okhttp' + testImplementation 'junit:junit' + testImplementation 'org.awaitility:awaitility' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' +} + +task writeInfoFile(type: ProjectPropertiesFile) { + destPackage = "net.consensys.pantheon" + addString("version", "$rootProject.name/$rootProject.version") +} + +compileJava.dependsOn(writeInfoFile) diff --git a/pantheon/libs/picocli-3.6.0-SNAPSHOT.jar b/pantheon/libs/picocli-3.6.0-SNAPSHOT.jar new file mode 100755 index 0000000000000000000000000000000000000000..ecd4506258d1715326e86c8693afa96d59b8373a GIT binary patch literal 240631 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%0Ary->A7 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%0A!o7N$AX{29=Q}p!W_vYdi8Kw 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 e@kl;o?n`bK#>*iggHAd8#{zE jsonRpc; + private final Optional websocketRpc; + + private final PantheonController pantheonController; + private final Path dataDir; + + Runner( + final Vertx vertx, + final NetworkRunner networkRunner, + final Optional jsonRpc, + final Optional websocketRpc, + final PantheonController pantheonController, + final Path dataDir) { + this.vertx = vertx; + this.networkRunner = networkRunner; + this.jsonRpc = jsonRpc; + this.websocketRpc = websocketRpc; + this.pantheonController = pantheonController; + this.dataDir = dataDir; + } + + public void execute() { + try { + LOGGER.info("Starting Ethereum main loop ... "); + networkRunner.start(); + pantheonController.getSynchronizer().start(); + jsonRpc.ifPresent(service -> service.start().join()); + websocketRpc.ifPresent(service -> service.start().join()); + LOGGER.info("Ethereum main loop is up."); + writePantheonPortsToFile(); + networkRunner.awaitStop(); + } catch (final InterruptedException e) { + LOGGER.debug("Interrupted, exiting", e); + Thread.currentThread().interrupt(); + } catch (final Exception ex) { + LOGGER.error("Exception in main loop:", ex); + throw new IllegalStateException(ex); + } + } + + @Override + public void close() throws Exception { + networkRunner.stop(); + exec.shutdown(); + try { + jsonRpc.ifPresent(service -> service.stop().join()); + websocketRpc.ifPresent(service -> service.stop().join()); + } finally { + try { + exec.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + } finally { + try { + vertx.close(); + } finally { + pantheonController.close(); + } + } + } + } + + private void writePantheonPortsToFile() { + final Properties properties = new Properties(); + if (networkRunner.getNetwork().isListening()) { + properties.setProperty("discovery", String.valueOf(getP2pUdpPort())); + properties.setProperty("p2p", String.valueOf(getP2pTcpPort())); + } + if (getJsonRpcPort().isPresent()) { + properties.setProperty("json-rpc", String.valueOf(getJsonRpcPort().get())); + } + if (getWebsocketPort().isPresent()) { + properties.setProperty("ws-rpc", String.valueOf(getWebsocketPort().get())); + } + + final File portsFile = new File(dataDir.toFile(), "pantheon.ports"); + portsFile.deleteOnExit(); + + try (FileOutputStream fileOutputStream = new FileOutputStream(portsFile)) { + properties.store( + fileOutputStream, + "This file contains the ports used by the running instance of Pantheon. This file will be deleted after the node is shutdown."); + } catch (final Exception e) { + LOGGER.warn("Error writing ports file", e); + } + } + + public Optional getJsonRpcPort() { + return jsonRpc.map(service -> service.socketAddress().getPort()); + } + + public Optional getWebsocketPort() { + return websocketRpc.map(service -> service.socketAddress().getPort()); + } + + public int getP2pUdpPort() { + return networkRunner.getNetwork().getDiscoverySocketAddress().getPort(); + } + + public int getP2pTcpPort() { + return networkRunner.getNetwork().getSelf().getPort(); + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/net/consensys/pantheon/RunnerBuilder.java new file mode 100755 index 00000000000..df8e406f1fa --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/RunnerBuilder.java @@ -0,0 +1,278 @@ +package net.consensys.pantheon; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.jsonrpc.CliqueJsonRpcMethodsFactory; +import net.consensys.pantheon.consensus.ibft.IbftContext; +import net.consensys.pantheon.consensus.ibft.jsonrpc.IbftJsonRpcMethodsFactory; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.chain.Blockchain; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcHttpService; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import net.consensys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketRequestHandler; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketService; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.methods.WebSocketMethodsFactory; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.SubscriptionManager; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.blockheaders.NewBlockHeadersSubscriptionService; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.logs.LogsSubscriptionService; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.pending.PendingTransactionSubscriptionService; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.subscription.syncing.SyncingSubscriptionService; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.NetworkRunner; +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.config.DiscoveryConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.NetworkingConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.RlpxConfiguration; +import net.consensys.pantheon.ethereum.p2p.config.SubProtocolConfiguration; +import net.consensys.pantheon.ethereum.p2p.discovery.internal.PeerRequirement; +import net.consensys.pantheon.ethereum.p2p.netty.NettyP2PNetwork; +import net.consensys.pantheon.ethereum.p2p.peers.PeerBlacklist; +import net.consensys.pantheon.ethereum.p2p.wire.Capability; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.base.Preconditions; +import io.vertx.core.Vertx; + +public class RunnerBuilder { + + public Runner build( + final Vertx vertx, + final PantheonController pantheonController, + final boolean discovery, + final Collection bootstrapPeers, + final String discoveryHost, + final int listenPort, + final int maxPeers, + final JsonRpcConfiguration jsonRpcConfiguration, + final WebSocketConfiguration webSocketConfiguration, + final Path dataDir) { + + Preconditions.checkNotNull(pantheonController); + + final DiscoveryConfiguration discoveryConfiguration; + if (discovery) { + final Collection bootstrap; + if (bootstrapPeers == null) { + bootstrap = DiscoveryConfiguration.MAINNET_BOOTSTRAP_NODES; + } else { + bootstrap = bootstrapPeers; + } + discoveryConfiguration = + DiscoveryConfiguration.create() + .setBindPort(listenPort) + .setAdvertisedHost(discoveryHost) + .setBootstrapPeers(bootstrap); + } else { + discoveryConfiguration = DiscoveryConfiguration.create().setActive(false); + } + + final KeyPair keyPair = pantheonController.getLocalNodeKeyPair(); + + final SubProtocolConfiguration subProtocolConfiguration = + pantheonController.subProtocolConfiguration(); + + final ProtocolSchedule protocolSchedule = pantheonController.getProtocolSchedule(); + final ProtocolContext context = pantheonController.getProtocolContext(); + + final List subProtocols = subProtocolConfiguration.getSubProtocols(); + final List protocolManagers = subProtocolConfiguration.getProtocolManagers(); + final Set supportedCapabilities = + protocolManagers + .stream() + .flatMap(protocolManager -> protocolManager.getSupportedCapabilities().stream()) + .collect(Collectors.toSet()); + + final NetworkingConfiguration networkConfig = + new NetworkingConfiguration() + .setRlpx(RlpxConfiguration.create().setBindPort(listenPort).setMaxPeers(maxPeers)) + .setDiscovery(discoveryConfiguration) + .setClientId(PantheonInfo.version()) + .setSupportedProtocols(subProtocols); + + final NetworkRunner networkRunner = + NetworkRunner.builder() + .protocolManagers(protocolManagers) + .subProtocols(subProtocols) + .network( + caps -> + new NettyP2PNetwork( + vertx, + keyPair, + networkConfig, + caps, + PeerRequirement.aggregateOf(protocolManagers), + new PeerBlacklist())) + .build(); + + final Synchronizer synchronizer = pantheonController.getSynchronizer(); + final TransactionPool transactionPool = pantheonController.getTransactionPool(); + final MiningCoordinator miningCoordinator = pantheonController.getMiningCoordinator(); + + Optional jsonRpcHttpService = Optional.empty(); + if (jsonRpcConfiguration.isEnabled()) { + final Map jsonRpcMethods = + jsonRpcMethods( + context, + protocolSchedule, + pantheonController, + networkRunner, + synchronizer, + transactionPool, + miningCoordinator, + supportedCapabilities, + jsonRpcConfiguration.getRpcApis()); + jsonRpcHttpService = + Optional.of(new JsonRpcHttpService(vertx, jsonRpcConfiguration, jsonRpcMethods)); + } + + Optional webSocketService = Optional.empty(); + if (webSocketConfiguration.isEnabled()) { + final Map webSocketsJsonRpcMethods = + jsonRpcMethods( + context, + protocolSchedule, + pantheonController, + networkRunner, + synchronizer, + transactionPool, + miningCoordinator, + supportedCapabilities, + webSocketConfiguration.getRpcApis()); + + final SubscriptionManager subscriptionManager = + createSubscriptionManager(vertx, context.getBlockchain(), transactionPool); + + createLogsSubscriptionService( + context.getBlockchain(), context.getWorldStateArchive(), subscriptionManager); + + createNewBlockHeadersSubscriptionService( + context.getBlockchain(), context.getWorldStateArchive(), subscriptionManager); + + createSyncingSubscriptionService(synchronizer, subscriptionManager); + + webSocketService = + Optional.of( + createWebsocketService( + vertx, webSocketConfiguration, subscriptionManager, webSocketsJsonRpcMethods)); + } + + return new Runner( + vertx, networkRunner, jsonRpcHttpService, webSocketService, pantheonController, dataDir); + } + + private Map jsonRpcMethods( + final ProtocolContext context, + final ProtocolSchedule protocolSchedule, + final PantheonController pantheonController, + final NetworkRunner networkRunner, + final Synchronizer synchronizer, + final TransactionPool transactionPool, + final MiningCoordinator miningCoordinator, + final Set supportedCapabilities, + final Collection jsonRpcApis) { + final Map methods = + new JsonRpcMethodsFactory() + .methods( + PantheonInfo.version(), + String.valueOf(pantheonController.getGenesisConfig().getChainId()), + networkRunner.getNetwork(), + context.getBlockchain(), + context.getWorldStateArchive(), + synchronizer, + transactionPool, + protocolSchedule, + miningCoordinator, + supportedCapabilities, + jsonRpcApis); + + if (context.getConsensusState() instanceof CliqueContext) { + // This is checked before entering this if branch + @SuppressWarnings("unchecked") + final ProtocolContext cliqueProtocolContext = + (ProtocolContext) context; + methods.putAll(new CliqueJsonRpcMethodsFactory().methods(cliqueProtocolContext)); + } + + if (context.getConsensusState() instanceof IbftContext) { + // This is checked before entering this if branch + @SuppressWarnings("unchecked") + final ProtocolContext ibftProtocolContext = + (ProtocolContext) context; + methods.putAll(new IbftJsonRpcMethodsFactory().methods(ibftProtocolContext)); + } + return methods; + } + + private SubscriptionManager createSubscriptionManager( + final Vertx vertx, final Blockchain blockchain, final TransactionPool transactionPool) { + final SubscriptionManager subscriptionManager = new SubscriptionManager(); + final PendingTransactionSubscriptionService pendingTransactions = + new PendingTransactionSubscriptionService(subscriptionManager); + transactionPool.addTransactionListener(pendingTransactions); + vertx.deployVerticle(subscriptionManager); + + return subscriptionManager; + } + + private LogsSubscriptionService createLogsSubscriptionService( + final Blockchain blockchain, + final WorldStateArchive worldStateArchive, + final SubscriptionManager subscriptionManager) { + final LogsSubscriptionService logsSubscriptionService = + new LogsSubscriptionService( + subscriptionManager, new BlockchainQueries(blockchain, worldStateArchive)); + + blockchain.observeBlockAdded(logsSubscriptionService); + + return logsSubscriptionService; + } + + private SyncingSubscriptionService createSyncingSubscriptionService( + final Synchronizer synchronizer, final SubscriptionManager subscriptionManager) { + return new SyncingSubscriptionService(subscriptionManager, synchronizer); + } + + private NewBlockHeadersSubscriptionService createNewBlockHeadersSubscriptionService( + final Blockchain blockchain, + final WorldStateArchive worldStateArchive, + final SubscriptionManager subscriptionManager) { + final NewBlockHeadersSubscriptionService newBlockHeadersSubscriptionService = + new NewBlockHeadersSubscriptionService( + subscriptionManager, new BlockchainQueries(blockchain, worldStateArchive)); + + blockchain.observeBlockAdded(newBlockHeadersSubscriptionService); + + return newBlockHeadersSubscriptionService; + } + + private WebSocketService createWebsocketService( + final Vertx vertx, + final WebSocketConfiguration configuration, + final SubscriptionManager subscriptionManager, + final Map jsonRpcMethods) { + final WebSocketMethodsFactory websocketMethodsFactory = + new WebSocketMethodsFactory(subscriptionManager, jsonRpcMethods); + final WebSocketRequestHandler websocketRequestHandler = + new WebSocketRequestHandler(vertx, websocketMethodsFactory.methods()); + + return new WebSocketService(vertx, configuration, websocketRequestHandler); + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandler.java b/pantheon/src/main/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandler.java new file mode 100755 index 00000000000..8aec0f2cd0f --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandler.java @@ -0,0 +1,56 @@ +package net.consensys.pantheon.cli; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import picocli.CommandLine; +import picocli.CommandLine.AbstractParseResultHandler; +import picocli.CommandLine.DefaultExceptionHandler; +import picocli.CommandLine.ExecutionException; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.ParseResult; + +class ConfigOptionSearchAndRunHandler extends AbstractParseResultHandler> { + + private final AbstractParseResultHandler> resultHandler; + private final DefaultExceptionHandler> exceptionHandler; + private final String configFileOptionName; + + ConfigOptionSearchAndRunHandler( + final AbstractParseResultHandler> resultHandler, + final DefaultExceptionHandler> exceptionHandler, + final String configFileOptionName) { + this.resultHandler = resultHandler; + this.exceptionHandler = exceptionHandler; + this.configFileOptionName = configFileOptionName; + // use the same output as the regular options handler to ensure that outputs are all going + // the in the same place. No need to do this for the exception handler as we reuse it directly. + this.useOut(resultHandler.out()); + } + + @Override + protected List handle(final ParseResult parseResult) throws ExecutionException { + final CommandLine commandLine = parseResult.asCommandLineList().get(0); + if (parseResult.hasMatchedOption(configFileOptionName)) { + final OptionSpec configFileOption = parseResult.matchedOption(configFileOptionName); + File configFile; + try { + configFile = configFileOption.getter().get(); + } catch (final Exception e) { + throw new ExecutionException(commandLine, e.getMessage(), e); + } + final TomlConfigFileDefaultProvider tomlConfigFileDefaultProvider = + new TomlConfigFileDefaultProvider(commandLine, configFile); + commandLine.setDefaultValueProvider(tomlConfigFileDefaultProvider); + } + commandLine.parseWithHandlers( + resultHandler, exceptionHandler, parseResult.originalArgs().toArray(new String[0])); + return new ArrayList<>(); + } + + @Override + protected ConfigOptionSearchAndRunHandler self() { + return this; + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/ExportPublicKeySubCommand.java b/pantheon/src/main/java/net/consensys/pantheon/cli/ExportPublicKeySubCommand.java new file mode 100755 index 00000000000..29f6720d066 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/ExportPublicKeySubCommand.java @@ -0,0 +1,55 @@ +package net.consensys.pantheon.cli; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +// Export of the public key takes a file as parameter to export directly by writing the key in the +// file. A direct output of the key to sdt out is not done because we don't want the key value +// to be polluted by other information like logs that are in KeyPairUtil that is inevitable. +@Command( + name = "export-pub-key", + description = "This exports node public key in a file.", + mixinStandardHelpOptions = true +) +class ExportPublicKeySubCommand implements Runnable { + + private static final Logger LOGGER = LogManager.getLogger(); + + @Parameters(arity = "1..1", paramLabel = "PATH", description = "File to write public key to") + private final File publicKeyExportFile = null; + + @SuppressWarnings("unused") + @ParentCommand + private PantheonCommand parentCommand; // Picocli injects reference to parent command + + @Override + public void run() { + + final PantheonController controller = parentCommand.buildController(); + final KeyPair keyPair = controller.getLocalNodeKeyPair(); + + // this publicKeyExportFile can never be null because of Picocli arity requirement + //noinspection ConstantConditions + final Path path = publicKeyExportFile.toPath(); + + try (BufferedWriter fileWriter = Files.newBufferedWriter(path, UTF_8)) { + fileWriter.write(keyPair.getPublicKey().toString()); + } catch (final IOException e) { + LOGGER.error("An error occurred while trying to write the public key", e); + } + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/ImportBlockchainSubCommand.java b/pantheon/src/main/java/net/consensys/pantheon/cli/ImportBlockchainSubCommand.java new file mode 100755 index 00000000000..af0d19cb5cc --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/ImportBlockchainSubCommand.java @@ -0,0 +1,105 @@ +package net.consensys.pantheon.cli; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.util.BlockImporter; + +import java.io.IOException; +import java.nio.file.Path; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExecutionException; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +@Command( + name = "import-blockchain", + description = + "This command imports blocks from a file into the database. WARNING: This import is ALPHA and does not include comprehensive validations yet.", + mixinStandardHelpOptions = true +) +class ImportBlockchainSubCommand implements Runnable { + + private static final Logger LOGGER = LogManager.getLogger(ImportBlockchainSubCommand.class); + + @ParentCommand + private PantheonCommand parentCommand; // Picocli injects reference to parent command + + @Parameters(arity = "1..1", paramLabel = "PATH", description = "File containing blocks to import") + private final Path blocksImportPath = null; + + @CommandLine.Option( + names = {"--skip-header-validation"}, + description = "WARNING: Set only if the import file is pre-validated." + ) + private final Boolean isSkipHeaderValidation = false; + + @CommandLine.Option( + names = {"--metrics-interval-sec"}, + description = "seconds between logging progress metrics.", + defaultValue = "30" + ) + private final Integer metricsIntervalSec = 30; + + @CommandLine.Option( + names = {"--account-commit-interval"}, + description = "commit account state every n accounts.", + defaultValue = "100000" + ) + private final Integer accountCommitInterval = 100_000; + + @CommandLine.Option( + names = {"--skip-blocks"}, + description = "skip processing the blocks in the import file", + defaultValue = "false" + ) + private final Boolean isSkipBlocks = Boolean.FALSE; + + @CommandLine.Option( + names = {"--skip-accounts"}, + description = "skip processing the accounts in the import file", + defaultValue = "false" + ) + private final Boolean isSkipAccounts = Boolean.FALSE; + + @CommandLine.Option( + names = {"--start-of-world-state"}, + description = + "file offset for the starting byte of the world state. Only relevant in combination with --skip-blocks." + ) + private final Long worldStateOffset = null; + + public ImportBlockchainSubCommand() {} + + @Override + public void run() { + LOGGER.info("Runs import sub command with blocksImportPath : {}", blocksImportPath); + + checkNotNull(parentCommand); + + checkNotNull(isSkipHeaderValidation); + checkNotNull(isSkipBlocks); + checkNotNull(isSkipAccounts); + + try { + final BlockImporter.ImportResult result = + parentCommand.blockchainImporter.importBlockchain( + blocksImportPath, + parentCommand.buildController(), + isSkipHeaderValidation, + metricsIntervalSec, + accountCommitInterval, + isSkipBlocks, + isSkipAccounts, + worldStateOffset); + System.out.println(result); + } catch (final IOException e) { + throw new ExecutionException( + new CommandLine(this), + String.format("Unable to import blocks from $1%s", blocksImportPath)); + } + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/ImportSubCommand.java b/pantheon/src/main/java/net/consensys/pantheon/cli/ImportSubCommand.java new file mode 100755 index 00000000000..4fc1476eeaf --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/ImportSubCommand.java @@ -0,0 +1,56 @@ +package net.consensys.pantheon.cli; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.util.BlockImporter; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ExecutionException; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; + +@Command( + name = "import", + description = "This command imports blocks from a file into the database.", + mixinStandardHelpOptions = true +) +class ImportSubCommand implements Runnable { + private static final Logger LOGGER = LogManager.getLogger(); + + @ParentCommand + private PantheonCommand parentCommand; // Picocli injects reference to parent command + + @Parameters(arity = "1..1", paramLabel = "PATH", description = "File containing blocks to import") + private final Path blocksImportPath = null; + + private final BlockImporter blockImporter; + + ImportSubCommand(final BlockImporter blockImporter) { + this.blockImporter = blockImporter; + } + + @Override + public void run() { + LOGGER.info("Runs import sub command with blocksImportPath : {}", blocksImportPath); + + checkNotNull(parentCommand); + checkNotNull(blockImporter); + + try { + blockImporter.importBlockchain(blocksImportPath, parentCommand.buildController()); + } catch (final FileNotFoundException e) { + throw new ExecutionException( + new CommandLine(this), "Could not find file to import: " + blocksImportPath); + } catch (final IOException e) { + throw new ExecutionException( + new CommandLine(this), "Unable to import blocks from " + blocksImportPath, e); + } + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/net/consensys/pantheon/cli/PantheonCommand.java new file mode 100755 index 00000000000..5386f7eac5c --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/PantheonCommand.java @@ -0,0 +1,516 @@ +package net.consensys.pantheon.cli; + +import static com.google.common.base.Preconditions.checkNotNull; + +import net.consensys.pantheon.Runner; +import net.consensys.pantheon.RunnerBuilder; +import net.consensys.pantheon.cli.custom.CorsAllowedOriginsProperty; +import net.consensys.pantheon.controller.MainnetPantheonController; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.eth.sync.SyncMode; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration.Builder; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.RpcApis; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.util.BlockImporter; +import net.consensys.pantheon.util.BlockchainImporter; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.google.common.net.HostAndPort; +import io.vertx.core.Vertx; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import picocli.CommandLine; +import picocli.CommandLine.AbstractParseResultHandler; +import picocli.CommandLine.Command; +import picocli.CommandLine.DefaultExceptionHandler; +import picocli.CommandLine.ExecutionException; +import picocli.CommandLine.ITypeConverter; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +@SuppressWarnings("FieldCanBeLocal") // because Picocli injected fields report false positives +@Command( + description = "This command runs the Pantheon Ethereum client full node.", + abbreviateSynopsis = true, + name = "pantheon", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class, + header = "Usage:", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + optionListHeading = "%nOptions:%n", + footerHeading = "%n", + footer = "Pantheon is licensed under the Apache License 2.0" +) +public class PantheonCommand implements Runnable { + private static final Logger LOGGER = LogManager.getLogger(); + + private static final int DEFAULT_MAX_PEERS = 25; + + // Default should be FAST for the next release + // but we use FULL for the moment as Fast is still in progress + private static final SyncMode DEFAULT_SYNC_MODE = SyncMode.FULL; + + private static final String PANTHEON_HOME_PROPERTY_NAME = "pantheon.home"; + private static final String DEFAULT_DATA_DIR_PATH = "./build/data"; + + private static final String MANDATORY_HOST_AND_PORT_FORMAT_HELP = ""; + private static final String MANDATORY_PATH_FORMAT_HELP = ""; + private static final String MANDATORY_INTEGER_FORMAT_HELP = ""; + private static final String MANDATORY_MODE_FORMAT_HELP = ""; + + private static final Wei DEFAULT_MIN_TRANSACTION_GAS_PRICE = Wei.of(1000); + private static final BytesValue DEFAULT_EXTRA_DATA = BytesValue.EMPTY; + + private static final String CONFIG_FILE_OPTION_NAME = "--config"; + + public static class RpcApisEnumConverter implements ITypeConverter { + @Override + public RpcApis convert(final String s) throws RpcApisEnumConvertionException { + try { + return RpcApis.valueOf(s.trim().toUpperCase()); + } catch (final IllegalArgumentException e) { + throw new RpcApisEnumConvertionException("Invalid value: " + s); + } + } + } + + public static class RpcApisEnumConvertionException extends Exception { + RpcApisEnumConvertionException(final String s) { + super(s); + } + } + + private final BlockImporter blockImporter; + final BlockchainImporter blockchainImporter; + + private final PantheonControllerBuilder controllerBuilder; + private final Builder synchronizerConfigurationBuilder; + private final RunnerBuilder runnerBuilder; + + // Public IP stored to prevent having to research it each time we need it. + private InetAddress autoDiscoveredDefaultIP = null; + + // CLI options defined by user at runtime. + // Options parsing is done with CLI library Picocli https://picocli.info/ + + @Option( + names = {CONFIG_FILE_OPTION_NAME}, + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "TOML config file (default: none)" + ) + private final File configFile = null; + + @Option( + names = {"--datadir"}, + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "the path to Pantheon data directory (default: ${DEFAULT-VALUE})" + ) + private final Path dataDir = getDefaultPantheonDataDir(); + + // Genesis file path with null default option if the option + // is not defined on command line as this default is handled by Runner + // to use mainnet json file from resources + // NOTE: we have no control over default value here. + @Option( + names = {"--genesis"}, + paramLabel = MANDATORY_PATH_FORMAT_HELP, + description = "The path to genesis file (default: Pantheon embedded mainnet genesis file)" + ) + private final File genesisFile = null; + + // Boolean option to indicate if peers should NOT be discovered, default to false indicates that + // the peers should be discovered by default. + // + // This negative option is required because of the nature of the option that is true when + // added on the command line. You can't do --option=false, so false is set as default + // and you have not to set the option at all if you want it false. + // This seems to be the only way it works with Picocli. + // Also many other software use the same negative option scheme for false defaults + // meaning that it's probably the right way to handle disabling options. + @Option( + names = {"--no-discovery"}, + description = "Disable p2p peer discovery (default: ${DEFAULT-VALUE})" + ) + private final Boolean noPeerDiscovery = false; + + // A list of bootstrap nodes can be passed + // and a hardcoded list will be used otherwise by the Runner. + // NOTE: we have no control over default value here. + @Option( + names = {"--bootnodes"}, + paramLabel = "", + description = + "Comma separated enode URLs for P2P discovery bootstrap. " + + "Default is a predefined list.", + split = ",", + arity = "1..*" + ) + private final Collection bootstrapNodes = null; + + @Option( + names = {"--max-peers"}, + paramLabel = MANDATORY_INTEGER_FORMAT_HELP, + description = + "Maximium p2p peer connections that can be established (default: ${DEFAULT-VALUE})" + ) + private final Integer maxPeers = DEFAULT_MAX_PEERS; + + @Option( + names = {"--max-trailing-peers"}, + paramLabel = MANDATORY_INTEGER_FORMAT_HELP, + description = + "Maximum p2p peer connections for peers that are trailing behind our chain head (default: unlimited)" + ) + private final Integer maxTrailingPeers = Integer.MAX_VALUE; + + @Option( + names = {"--sync-mode"}, + paramLabel = MANDATORY_MODE_FORMAT_HELP, + description = + "Synchronization mode (Value can be one of ${COMPLETION-CANDIDATES}, default: ${DEFAULT-VALUE})" + ) + private final SyncMode syncMode = DEFAULT_SYNC_MODE; + + // Boolean option to indicate if the client have to sync against the ottoman test network + // (see https://github.com/ethereum/EIPs/issues/650). + @Option( + names = {"--ottoman"}, + description = + "Synchronize against the Ottoman test network, only useful if using an iBFT genesis file" + + " - see https://github.com/ethereum/EIPs/issues/650 (default: ${DEFAULT-VALUE})" + ) + private final Boolean syncWithOttoman = false; + + @Option( + names = {"--p2p-listen"}, + paramLabel = MANDATORY_HOST_AND_PORT_FORMAT_HELP, + description = "Host and port for p2p peers discovery to listen on (default: ${DEFAULT-VALUE})", + arity = "1" + ) + private final HostAndPort p2pHostAndPort = getDefaultHostAndPort(DefaultPeer.DEFAULT_PORT); + + @Option( + names = {"--network-id"}, + paramLabel = MANDATORY_INTEGER_FORMAT_HELP, + description = "P2P network identifier (default: ${DEFAULT-VALUE})", + arity = "1" + ) + private final Integer networkId = MainnetPantheonController.MAINNET_NETWORK_ID; + + @Option( + names = {"--rpc-enabled"}, + description = "Set if the JSON-RPC service should be started (default: ${DEFAULT-VALUE})" + ) + private final Boolean isJsonRpcEnabled = false; + + @Option( + names = {"--rpc-listen"}, + paramLabel = MANDATORY_HOST_AND_PORT_FORMAT_HELP, + description = "Host and port for JSON-RPC to listen on (default: ${DEFAULT-VALUE})", + arity = "1" + ) + private final HostAndPort rpcHostAndPort = + getDefaultHostAndPort(JsonRpcConfiguration.DEFAULT_JSON_RPC_PORT); + + // A list of origins URLs that are accepted by the JsonRpcHttpServer (CORS) + @Option( + names = {"--rpc-cors-origins"}, + description = "Comma separated origin domain URLs for CORS validation (default: none)", + converter = CorsAllowedOriginsProperty.CorsAllowedOriginsPropertyConverter.class + ) + private final CorsAllowedOriginsProperty rpcCorsAllowedOrigins = new CorsAllowedOriginsProperty(); + + @Option( + names = {"--rpc-api"}, + paramLabel = "", + split = ",", + arity = "1..*", + converter = RpcApisEnumConverter.class, + description = "Comma separated APIs to enable on JSON-RPC channel. default: ${DEFAULT-VALUE}" + ) + private final Collection rpcApis = Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + + @Option( + names = {"--ws-enabled"}, + description = + "Set if the WS-RPC (WebSocket) service should be started (default: ${DEFAULT-VALUE})" + ) + private final Boolean isWsRpcEnabled = false; + + @Option( + names = {"--ws-listen"}, + paramLabel = MANDATORY_HOST_AND_PORT_FORMAT_HELP, + description = "Host and port for WS-RPC (WebSocket) to listen on (default: ${DEFAULT-VALUE})", + arity = "1" + ) + private final HostAndPort wsHostAndPort = + getDefaultHostAndPort(WebSocketConfiguration.DEFAULT_WEBSOCKET_PORT); + + @Option( + names = {"--ws-api"}, + paramLabel = "", + split = ",", + arity = "1..*", + converter = RpcApisEnumConverter.class, + description = "Comma separated APIs to enable on WebSocket channel. default: ${DEFAULT-VALUE}" + ) + private final Collection wsApis = Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3); + + @Option( + names = {"--dev-mode"}, + description = + "set during development to have a custom genesis with specific chain id " + + "and reduced difficulty to enable CPU mining (default: ${DEFAULT-VALUE})." + ) + private final Boolean isDevMode = false; + + @Option( + names = {"--miner-enabled"}, + description = "set if node should perform mining (default: ${DEFAULT-VALUE})" + ) + private final Boolean isMiningEnabled = false; + + @Option( + names = {"--miner-coinbase"}, + description = + "the account to which mining rewards are to be paid, must be specified if " + + "mining is enabled.", + arity = "1" + ) + private final Address coinbase = null; + + @Option( + names = {"--miner-minTransactionGasPriceWei"}, + description = + "the minimum price offered by a transaction for it to be included in a mined " + + "block (default: ${DEFAULT-VALUE}).", + arity = "1" + ) + private final Wei minTransactionGasPrice = DEFAULT_MIN_TRANSACTION_GAS_PRICE; + + @Option( + names = {"--miner-extraData"}, + description = + "a hex string representing the (32) bytes to be included in the extra data " + + "field of a mined block. (default: ${DEFAULT-VALUE}).", + arity = "1" + ) + private final BytesValue extraData = DEFAULT_EXTRA_DATA; + + public PantheonCommand( + final BlockImporter blockImporter, + final BlockchainImporter blockchainImporter, + final RunnerBuilder runnerBuilder, + final PantheonControllerBuilder controllerBuilder, + final Builder synchronizerConfigurationBuilder) { + this.blockImporter = blockImporter; + this.blockchainImporter = blockchainImporter; + this.runnerBuilder = runnerBuilder; + this.controllerBuilder = controllerBuilder; + this.synchronizerConfigurationBuilder = synchronizerConfigurationBuilder; + } + + public void parse( + final AbstractParseResultHandler> resultHandler, + final DefaultExceptionHandler> exceptionHandler, + final String... args) { + + final CommandLine commandLine = new CommandLine(this); + + final ImportSubCommand importSubCommand = new ImportSubCommand(blockImporter); + final ImportBlockchainSubCommand importBlockchainSubCommand = new ImportBlockchainSubCommand(); + commandLine.addSubcommand("import", importSubCommand); + commandLine.addSubcommand("import-blockchain", importBlockchainSubCommand); + commandLine.addSubcommand("export-pub-key", new ExportPublicKeySubCommand()); + + commandLine.registerConverter(HostAndPort.class, HostAndPort::fromString); + commandLine.registerConverter(SyncMode.class, SyncMode::fromString); + commandLine.registerConverter(Address.class, Address::fromHexString); + commandLine.registerConverter(BytesValue.class, BytesValue::fromHexString); + commandLine.registerConverter(Wei.class, (arg) -> Wei.of(Long.parseUnsignedLong(arg))); + + // Create a handler that will search for a config file option and use it for default values + // and eventually it will run regular parsing of the remaining options. + final ConfigOptionSearchAndRunHandler configParsingHandler = + new ConfigOptionSearchAndRunHandler( + resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME); + commandLine.parseWithHandlers(configParsingHandler, exceptionHandler, args); + } + + @Override + public void run() { + //noinspection ConstantConditions + if (isMiningEnabled && coinbase == null) { + System.out.println( + "Unable to mine without a valid coinbase. Either disable mining (remove --miner-enabled)" + + "or specify the beneficiary of mining (via --miner-coinbase
)"); + return; + } + synchronize( + buildController(), + noPeerDiscovery, + bootstrapNodes, + maxPeers, + p2pHostAndPort, + jsonRpcConfiguration(), + webSocketConfiguration()); + } + + PantheonController buildController() { + try { + return controllerBuilder.build( + buildSyncConfig(syncMode), + genesisFile, + dataDir, + syncWithOttoman, + new MiningParameters(coinbase, minTransactionGasPrice, extraData, isMiningEnabled), + isDevMode, + networkId); + } catch (final IOException e) { + throw new ExecutionException(new CommandLine(this), "Invalid path", e); + } + } + + private JsonRpcConfiguration jsonRpcConfiguration() { + final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); + jsonRpcConfiguration.setEnabled(isJsonRpcEnabled); + jsonRpcConfiguration.setHost(rpcHostAndPort.getHost()); + jsonRpcConfiguration.setPort(rpcHostAndPort.getPort()); + jsonRpcConfiguration.setCorsAllowedDomains(rpcCorsAllowedOrigins.getDomains()); + jsonRpcConfiguration.setRpcApis(rpcApis); + return jsonRpcConfiguration; + } + + private WebSocketConfiguration webSocketConfiguration() { + final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); + webSocketConfiguration.setEnabled(isWsRpcEnabled); + webSocketConfiguration.setHost(wsHostAndPort.getHost()); + webSocketConfiguration.setPort(wsHostAndPort.getPort()); + webSocketConfiguration.setRpcApis(rpcApis); + return webSocketConfiguration; + } + + private SynchronizerConfiguration buildSyncConfig(final SyncMode syncMode) { + checkNotNull(syncMode); + synchronizerConfigurationBuilder.syncMode(syncMode); + synchronizerConfigurationBuilder.maxTrailingPeers(maxTrailingPeers); + return synchronizerConfigurationBuilder.build(); + } + + // Blockchain synchronisation from peers. + private void synchronize( + final PantheonController controller, + final boolean noPeerDiscovery, + final Collection bootstrapNodes, + final int maxPeers, + final HostAndPort discoveryHostAndPort, + final JsonRpcConfiguration jsonRpcConfiguration, + final WebSocketConfiguration webSocketConfiguration) { + + checkNotNull(runnerBuilder); + + // BEWARE: Peer discovery boolean must be inverted as it's negated in the options ! + final Runner runner = + runnerBuilder.build( + Vertx.vertx(), + controller, + !noPeerDiscovery, + bootstrapNodes, + discoveryHostAndPort.getHost(), + discoveryHostAndPort.getPort(), + maxPeers, + jsonRpcConfiguration, + webSocketConfiguration, + dataDir); + + runner.execute(); + } + + // Used to discover the default IP of the client. + // Loopback IP is used by default as this is how smokeTests require it to be + // and it's probably a good security behaviour to default only on the localhost. + private InetAddress autoDiscoverDefaultIP() { + + if (autoDiscoveredDefaultIP != null) { + return autoDiscoveredDefaultIP; + } + + autoDiscoveredDefaultIP = InetAddress.getLoopbackAddress(); + + return autoDiscoveredDefaultIP; + } + + private HostAndPort getDefaultHostAndPort(final int port) { + return HostAndPort.fromParts(autoDiscoverDefaultIP().getHostAddress(), port); + } + + private Path getDefaultPantheonDataDir() { + // this property is retrieved from Gradle tasks or Pantheon running shell script. + final String pantheonHomeProperty = System.getProperty(PANTHEON_HOME_PROPERTY_NAME); + Path pantheonHome; + + // If prop is found, then use it + if (pantheonHomeProperty != null) { + try { + pantheonHome = Paths.get(pantheonHomeProperty); + } catch (final InvalidPathException e) { + throw new ParameterException( + new CommandLine(this), + String.format( + "Unable to define default data directory from %s property.", + PANTHEON_HOME_PROPERTY_NAME), + e); + } + } else { + // otherwise use a default path. + // That may only be used when NOT run from distribution script and Gradle as they all define + // the property. + try { + final String path = new File(DEFAULT_DATA_DIR_PATH).getCanonicalPath(); + pantheonHome = Paths.get(path); + } catch (final IOException e) { + throw new ParameterException( + new CommandLine(this), "Unable to create default data directory."); + } + } + + // Try to create it, then verify if the provided path is not already existing and is not a + // directory .Otherwise, if it doesn't exist or exists but is already a directory, + // Runner will use it to store data. + try { + Files.createDirectories(pantheonHome); + } catch (final FileAlreadyExistsException e) { + // Only thrown if it exist but is not a directory + throw new ParameterException( + new CommandLine(this), + String.format( + "%s: already exists and is not a directory.", pantheonHome.toAbsolutePath()), + e); + } catch (final Exception e) { + throw new ParameterException( + new CommandLine(this), + String.format("Error creating directory %s.", pantheonHome.toAbsolutePath()), + e); + } + return pantheonHome; + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/PantheonControllerBuilder.java b/pantheon/src/main/java/net/consensys/pantheon/cli/PantheonControllerBuilder.java new file mode 100755 index 00000000000..0fd96394b13 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/PantheonControllerBuilder.java @@ -0,0 +1,54 @@ +package net.consensys.pantheon.cli; + +import static net.consensys.pantheon.controller.KeyPairUtil.loadKeyPair; + +import net.consensys.pantheon.controller.MainnetPantheonController; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class PantheonControllerBuilder { + + public PantheonController build( + final SynchronizerConfiguration synchronizerConfiguration, + final File genesisFile, + final Path homePath, + final boolean syncWithOttoman, + final MiningParameters miningParameters, + final boolean isDevMode, + final int networkId) + throws IOException { + + // instantiate a controller with mainnet config if no genesis file is defined + // otherwise use the indicated genesis file + final KeyPair nodeKeys = loadKeyPair(homePath); + if (genesisFile == null) { + final GenesisConfig genesisConfig = + isDevMode ? GenesisConfig.development() : GenesisConfig.mainnet(); + return MainnetPantheonController.init( + homePath, + genesisConfig, + synchronizerConfiguration, + miningParameters, + networkId, + nodeKeys); + } else { + return PantheonController.fromConfig( + synchronizerConfiguration, + new String(Files.readAllBytes(genesisFile.toPath()), StandardCharsets.UTF_8), + homePath, + syncWithOttoman, + networkId, + miningParameters, + nodeKeys); + } + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProvider.java b/pantheon/src/main/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProvider.java new file mode 100755 index 00000000000..b438929ac44 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProvider.java @@ -0,0 +1,123 @@ +package net.consensys.pantheon.cli; + +import net.consensys.cava.toml.Toml; +import net.consensys.cava.toml.TomlArray; +import net.consensys.cava.toml.TomlParseError; +import net.consensys.cava.toml.TomlParseResult; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import picocli.CommandLine; +import picocli.CommandLine.IDefaultValueProvider; +import picocli.CommandLine.Model.ArgSpec; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.ParameterException; + +public class TomlConfigFileDefaultProvider implements IDefaultValueProvider { + + private final CommandLine commandLine; + private final File configFile; + private TomlParseResult result; + + TomlConfigFileDefaultProvider(final CommandLine commandLine, final File configFile) { + this.commandLine = commandLine; + this.configFile = configFile; + } + + @Override + public String defaultValue(final ArgSpec argSpec) { + loadConfigurationFromFile(); + + // only options can be used in config because a name is needed for the key + // so we skip default for positional params + return argSpec.isOption() ? getConfigurationValue(((OptionSpec) argSpec)) : null; + } + + private String getConfigurationKey(final OptionSpec optionSpec) { + // remove leading dashes on option name as we can have "--" or "-" options + return optionSpec.longestName().replaceFirst("^-+", ""); + } + + private String getConfigurationValue(final OptionSpec optionSpec) { + final String optionKey = getConfigurationKey(optionSpec); + String + defaultValue; // Convert values to the right string representation for default string value + if (optionSpec.type().equals(Boolean.class)) { + defaultValue = getBooleanEntryAsString(optionKey); + } else if (optionSpec.isMultiValue()) { + defaultValue = getListEntryAsString(optionKey); + } else if (optionSpec.type().equals(Integer.class)) { + defaultValue = getIntegerEntryAsString(optionKey); + } else { // else will be treated as String + defaultValue = getEntryAsString(optionKey); + } + return defaultValue; + } + + private String getEntryAsString(final String optionKey) { + return result.getString(optionKey); + } + + private String getListEntryAsString(final String optionKey) { + final TomlArray tomlArray = result.getArray(optionKey); + if (tomlArray != null) { + final List items = + tomlArray.toList().stream().map(e -> (String) e).collect(Collectors.toList()); + return String.join(",", items); + } + return null; + } + + private String getBooleanEntryAsString(final String optionKey) { + final Boolean booleanValue = result.getBoolean(optionKey); + if (booleanValue != null) { + return !booleanValue ? "false" : "true"; + } + return null; + } + + private String getIntegerEntryAsString(final String optionKey) { + if (result.get(optionKey) != null) { + return String.valueOf(result.get(optionKey)); + } + return null; + } + + private void checkConfigurationValidity() { + if (result == null || result.isEmpty()) + throw new ParameterException( + commandLine, String.format("Unable to read TOML configuration file %s", configFile)); + } + + private void loadConfigurationFromFile() { + + if (result == null) { + try { + final TomlParseResult result = Toml.parse(configFile.toPath()); + + if (result.hasErrors()) { + final String errors = + result + .errors() + .stream() + .map(TomlParseError::toString) + .collect(Collectors.joining("%n")); + ; + throw new ParameterException( + commandLine, String.format("Invalid TOML configuration : %s", errors)); + } + + this.result = result; + + } catch (final IOException e) { + throw new ParameterException( + commandLine, "Unable to read TOML configuration, file not found."); + } + } + + checkConfigurationValidity(); + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/VersionProvider.java b/pantheon/src/main/java/net/consensys/pantheon/cli/VersionProvider.java new file mode 100755 index 00000000000..136fa6282af --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/VersionProvider.java @@ -0,0 +1,12 @@ +package net.consensys.pantheon.cli; + +import net.consensys.pantheon.PantheonInfo; + +import picocli.CommandLine; + +public class VersionProvider implements CommandLine.IVersionProvider { + @Override + public String[] getVersion() { + return new String[] {PantheonInfo.version()}; + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/cli/custom/CorsAllowedOriginsProperty.java b/pantheon/src/main/java/net/consensys/pantheon/cli/custom/CorsAllowedOriginsProperty.java new file mode 100755 index 00000000000..7b8eed25abb --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/cli/custom/CorsAllowedOriginsProperty.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.cli.custom; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import com.google.common.collect.Lists; +import picocli.CommandLine.ITypeConverter; + +public class CorsAllowedOriginsProperty { + + private List domains = Collections.emptyList(); + + public CorsAllowedOriginsProperty(final List domains) { + this.domains = domains; + } + + public CorsAllowedOriginsProperty() {} + + public List getDomains() { + return domains; + } + + public static class CorsAllowedOriginsPropertyConverter + implements ITypeConverter { + + @Override + public CorsAllowedOriginsProperty convert(final String value) throws IllegalArgumentException { + List domains; + if (value != null && !value.isEmpty()) { + domains = new ArrayList<>(Arrays.asList(value.split("\\s*,\\s*"))); + } else { + throw new IllegalArgumentException("Property can't be null/empty string"); + } + + if (domains.contains("none")) { + if (domains.size() > 1) { + throw new IllegalArgumentException("Value 'none' can't be used with other domains"); + } else { + return new CorsAllowedOriginsProperty(Collections.emptyList()); + } + } + + if (domains.contains("all") || domains.contains("*")) { + if (domains.size() > 1) { + throw new IllegalArgumentException("Value 'all' can't be used with other domains"); + } else { + return new CorsAllowedOriginsProperty(Lists.newArrayList("*")); + } + } + + try { + final StringJoiner stringJoiner = new StringJoiner("|"); + domains.stream().filter(s -> !s.isEmpty()).forEach(stringJoiner::add); + Pattern.compile(stringJoiner.toString()); + } catch (final PatternSyntaxException e) { + throw new IllegalArgumentException("Domain values result in invalid regex pattern", e); + } + + if (domains.size() > 0) { + return new CorsAllowedOriginsProperty(domains); + } else { + return new CorsAllowedOriginsProperty(); + } + } + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/controller/CliquePantheonController.java b/pantheon/src/main/java/net/consensys/pantheon/controller/CliquePantheonController.java new file mode 100755 index 00000000000..6a70ffb56df --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/controller/CliquePantheonController.java @@ -0,0 +1,163 @@ +package net.consensys.pantheon.controller; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.consensus.clique.CliqueContext; +import net.consensys.pantheon.consensus.clique.CliqueVoteTallyUpdater; +import net.consensys.pantheon.consensus.clique.VoteTallyCache; +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.sync.DefaultSynchronizer; +import net.consensys.pantheon.ethereum.eth.sync.SyncMode; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.eth.transactions.TransactionPoolFactory; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.config.SubProtocolConfiguration; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.RocksDbKeyValueStorage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.logging.log4j.Logger; + +public class CliquePantheonController implements PantheonController { + + private static final Logger LOG = getLogger(); + private final GenesisConfig genesisConfig; + private final ProtocolContext context; + private final Synchronizer synchronizer; + private final ProtocolManager ethProtocolManager; + private final KeyPair keyPair; + private final TransactionPool transactionPool; + private final Runnable closer; + + CliquePantheonController( + final GenesisConfig genesisConfig, + final ProtocolContext context, + final ProtocolManager ethProtocolManager, + final Synchronizer synchronizer, + final KeyPair keyPair, + final TransactionPool transactionPool, + final Runnable closer) { + + this.genesisConfig = genesisConfig; + this.context = context; + this.ethProtocolManager = ethProtocolManager; + this.synchronizer = synchronizer; + this.keyPair = keyPair; + this.transactionPool = transactionPool; + this.closer = closer; + } + + public static PantheonController init( + final Path home, + final GenesisConfig genesisConfig, + final SynchronizerConfiguration taintedSyncConfig, + final int networkId, + final KeyPair nodeKeys) + throws IOException { + final RocksDbKeyValueStorage kv = + RocksDbKeyValueStorage.create(Files.createDirectories(home.resolve(DATABASE_PATH))); + final ProtocolSchedule protocolSchedule = genesisConfig.getProtocolSchedule(); + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + final MutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisConfig.getBlock(), kv, blockHashFunction); + final KeyValueStorageWorldStateStorage worldStateStorage = + new KeyValueStorageWorldStateStorage(kv); + final WorldStateArchive worldStateArchive = new WorldStateArchive(worldStateStorage); + genesisConfig.writeStateTo(worldStateArchive.getMutable(Hash.EMPTY_TRIE_HASH)); + + final EpochManager epochManger = new EpochManager(30_000); + final ProtocolContext protocolContext = + new ProtocolContext<>( + blockchain, + worldStateArchive, + new CliqueContext( + new VoteTallyCache( + blockchain, new CliqueVoteTallyUpdater(epochManger), epochManger), + new VoteProposer())); + + final SynchronizerConfiguration syncConfig = taintedSyncConfig.validated(blockchain); + final boolean fastSyncEnabled = syncConfig.syncMode().equals(SyncMode.FAST); + final EthProtocolManager ethProtocolManager = + new EthProtocolManager( + protocolContext.getBlockchain(), + genesisConfig.getChainId(), + fastSyncEnabled, + networkId); + final Synchronizer synchronizer = + new DefaultSynchronizer<>( + syncConfig, protocolSchedule, protocolContext, ethProtocolManager.ethContext()); + + final TransactionPool transactionPool = + TransactionPoolFactory.createTransactionPool( + protocolSchedule, protocolContext, ethProtocolManager.ethContext()); + + return new CliquePantheonController( + genesisConfig, + protocolContext, + ethProtocolManager, + synchronizer, + nodeKeys, + transactionPool, + kv::close); + } + + @Override + public ProtocolContext getProtocolContext() { + return context; + } + + @Override + public GenesisConfig getGenesisConfig() { + return genesisConfig; + } + + @Override + public Synchronizer getSynchronizer() { + return synchronizer; + } + + @Override + public SubProtocolConfiguration subProtocolConfiguration() { + return new SubProtocolConfiguration().withSubProtocol(EthProtocol.get(), ethProtocolManager); + } + + @Override + public KeyPair getLocalNodeKeyPair() { + return keyPair; + } + + @Override + public TransactionPool getTransactionPool() { + return transactionPool; + } + + @Override + public MiningCoordinator getMiningCoordinator() { + return null; + } + + @Override + public void close() { + closer.run(); + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/controller/IbftPantheonController.java b/pantheon/src/main/java/net/consensys/pantheon/controller/IbftPantheonController.java new file mode 100755 index 00000000000..d8d5d9a5d27 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/controller/IbftPantheonController.java @@ -0,0 +1,226 @@ +package net.consensys.pantheon.controller; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.consensus.common.EpochManager; +import net.consensys.pantheon.consensus.common.VoteProposer; +import net.consensys.pantheon.consensus.common.VoteTally; +import net.consensys.pantheon.consensus.ibft.IbftContext; +import net.consensys.pantheon.consensus.ibft.IbftEventQueue; +import net.consensys.pantheon.consensus.ibft.IbftProcessor; +import net.consensys.pantheon.consensus.ibft.IbftProtocolSchedule; +import net.consensys.pantheon.consensus.ibft.IbftStateMachine; +import net.consensys.pantheon.consensus.ibft.VoteTallyUpdater; +import net.consensys.pantheon.consensus.ibft.protocol.IbftProtocolManager; +import net.consensys.pantheon.consensus.ibft.protocol.IbftSubProtocol; +import net.consensys.pantheon.consensus.ibft.protocol.Istanbul64Protocol; +import net.consensys.pantheon.consensus.ibft.protocol.Istanbul64ProtocolManager; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.sync.DefaultSynchronizer; +import net.consensys.pantheon.ethereum.eth.sync.SyncMode; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.eth.transactions.TransactionPoolFactory; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.config.SubProtocolConfiguration; +import net.consensys.pantheon.ethereum.p2p.wire.SubProtocol; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.RocksDbKeyValueStorage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.json.JsonObject; +import org.apache.logging.log4j.Logger; + +public class IbftPantheonController implements PantheonController { + + private static final int DEFAULT_ROUND_EXPIRY_MILLISECONDS = 10000; + private static final Logger LOG = getLogger(); + private final GenesisConfig genesisConfig; + private final ProtocolContext context; + private final Synchronizer synchronizer; + private final SubProtocol ethSubProtocol; + private final ProtocolManager ethProtocolManager; + private final IbftProtocolManager ibftProtocolManager; + private final KeyPair keyPair; + private final TransactionPool transactionPool; + private final IbftProcessor ibftProcessor; + private final Runnable closer; + + IbftPantheonController( + final GenesisConfig genesisConfig, + final ProtocolContext context, + final SubProtocol ethSubProtocol, + final ProtocolManager ethProtocolManager, + final IbftProtocolManager ibftProtocolManager, + final Synchronizer synchronizer, + final KeyPair keyPair, + final TransactionPool transactionPool, + final IbftProcessor ibftProcessor, + final Runnable closer) { + + this.genesisConfig = genesisConfig; + this.context = context; + this.ethSubProtocol = ethSubProtocol; + this.ethProtocolManager = ethProtocolManager; + this.ibftProtocolManager = ibftProtocolManager; + this.synchronizer = synchronizer; + this.keyPair = keyPair; + this.transactionPool = transactionPool; + this.ibftProcessor = ibftProcessor; + this.closer = closer; + } + + public static PantheonController init( + final Path home, + final GenesisConfig genesisConfig, + final SynchronizerConfiguration taintedSyncConfig, + final boolean ottomanTestnetOperation, + final JsonObject ibftConfig, + final int networkId, + final KeyPair nodeKeys) + throws IOException { + final RocksDbKeyValueStorage kv = + RocksDbKeyValueStorage.create(Files.createDirectories(home.resolve(DATABASE_PATH))); + final ProtocolSchedule protocolSchedule = genesisConfig.getProtocolSchedule(); + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + final MutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisConfig.getBlock(), kv, blockHashFunction); + final KeyValueStorageWorldStateStorage worldStateStorage = + new KeyValueStorageWorldStateStorage(kv); + final WorldStateArchive worldStateArchive = new WorldStateArchive(worldStateStorage); + genesisConfig.writeStateTo(worldStateArchive.getMutable(Hash.EMPTY_TRIE_HASH)); + + final EpochManager epochManager = + new EpochManager(IbftProtocolSchedule.getEpochLength(Optional.of(ibftConfig))); + + final VoteTally voteTally = + new VoteTallyUpdater(epochManager).buildVoteTallyFromBlockchain(blockchain); + + final VoteProposer voteProposer = new VoteProposer(); + + final ProtocolContext protocolContext = + new ProtocolContext<>( + blockchain, worldStateArchive, new IbftContext(voteTally, voteProposer)); + + final SynchronizerConfiguration syncConfig = taintedSyncConfig.validated(blockchain); + final boolean fastSyncEnabled = syncConfig.syncMode().equals(SyncMode.FAST); + EthProtocolManager ethProtocolManager; + SubProtocol ethSubProtocol; + if (ottomanTestnetOperation) { + LOG.info("Operating on Ottoman testnet."); + ethSubProtocol = Istanbul64Protocol.get(); + ethProtocolManager = + new Istanbul64ProtocolManager( + protocolContext.getBlockchain(), networkId, fastSyncEnabled, 1); + } else { + ethSubProtocol = EthProtocol.get(); + ethProtocolManager = + new EthProtocolManager(protocolContext.getBlockchain(), networkId, fastSyncEnabled, 1); + } + final Synchronizer synchronizer = + new DefaultSynchronizer<>( + syncConfig, protocolSchedule, protocolContext, ethProtocolManager.ethContext()); + + final IbftEventQueue ibftEventQueue = new IbftEventQueue(); + + final IbftStateMachine ibftStateMachine = new IbftStateMachine(); + final IbftProcessor ibftProcessor = + new IbftProcessor( + ibftEventQueue, + ibftConfig.getInteger("requestTimeout", DEFAULT_ROUND_EXPIRY_MILLISECONDS), + ibftStateMachine); + final ExecutorService processorExecutor = Executors.newSingleThreadExecutor(); + processorExecutor.submit(ibftProcessor); + + final Runnable closer = + () -> { + ibftProcessor.stop(); + processorExecutor.shutdownNow(); + try { + processorExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (final InterruptedException e) { + LOG.error("Failed to shutdown ibft processor executor"); + } + kv.close(); + }; + + final TransactionPool transactionPool = + TransactionPoolFactory.createTransactionPool( + protocolSchedule, protocolContext, ethProtocolManager.ethContext()); + + return new IbftPantheonController( + genesisConfig, + protocolContext, + ethSubProtocol, + ethProtocolManager, + new IbftProtocolManager(ibftEventQueue), + synchronizer, + nodeKeys, + transactionPool, + ibftProcessor, + closer); + } + + @Override + public ProtocolContext getProtocolContext() { + return context; + } + + @Override + public GenesisConfig getGenesisConfig() { + return genesisConfig; + } + + @Override + public Synchronizer getSynchronizer() { + return synchronizer; + } + + @Override + public SubProtocolConfiguration subProtocolConfiguration() { + return new SubProtocolConfiguration() + .withSubProtocol(ethSubProtocol, ethProtocolManager) + .withSubProtocol(IbftSubProtocol.get(), ibftProtocolManager); + } + + @Override + public KeyPair getLocalNodeKeyPair() { + return keyPair; + } + + @Override + public TransactionPool getTransactionPool() { + return transactionPool; + } + + @Override + public MiningCoordinator getMiningCoordinator() { + return null; + } + + @Override + public void close() { + closer.run(); + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/controller/KeyPairUtil.java b/pantheon/src/main/java/net/consensys/pantheon/controller/KeyPairUtil.java new file mode 100755 index 00000000000..91bf1a0f6cc --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/controller/KeyPairUtil.java @@ -0,0 +1,33 @@ +package net.consensys.pantheon.controller; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.crypto.SECP256K1; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import org.apache.logging.log4j.Logger; + +public class KeyPairUtil { + private static final Logger LOGGER = getLogger(); + + public static SECP256K1.KeyPair loadKeyPair(final Path home) throws IOException { + final File keyFile = home.resolve("key").toFile(); + final SECP256K1.KeyPair key; + if (keyFile.exists()) { + key = SECP256K1.KeyPair.load(keyFile); + LOGGER.info( + "Loaded key {} from {}", key.getPublicKey().toString(), keyFile.getAbsolutePath()); + } else { + key = SECP256K1.KeyPair.generate(); + key.getPrivateKey().store(keyFile); + LOGGER.info( + "Generated new key key {} and stored it to {}", + key.getPublicKey().toString(), + keyFile.getAbsolutePath()); + } + return key; + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/controller/MainnetPantheonController.java b/pantheon/src/main/java/net/consensys/pantheon/controller/MainnetPantheonController.java new file mode 100755 index 00000000000..02007a68cb7 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/controller/MainnetPantheonController.java @@ -0,0 +1,207 @@ +package net.consensys.pantheon.controller; + +import static net.consensys.pantheon.controller.KeyPairUtil.loadKeyPair; +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.DefaultBlockScheduler; +import net.consensys.pantheon.ethereum.blockcreation.EthHashMinerExecutor; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.BlockHashFunction; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.db.DefaultMutableBlockchain; +import net.consensys.pantheon.ethereum.db.WorldStateArchive; +import net.consensys.pantheon.ethereum.eth.EthProtocol; +import net.consensys.pantheon.ethereum.eth.manager.EthProtocolManager; +import net.consensys.pantheon.ethereum.eth.sync.DefaultSynchronizer; +import net.consensys.pantheon.ethereum.eth.sync.SyncMode; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.eth.transactions.TransactionPoolFactory; +import net.consensys.pantheon.ethereum.mainnet.MainnetBlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.p2p.api.ProtocolManager; +import net.consensys.pantheon.ethereum.p2p.config.SubProtocolConfiguration; +import net.consensys.pantheon.ethereum.worldstate.KeyValueStorageWorldStateStorage; +import net.consensys.pantheon.services.kvstore.RocksDbKeyValueStorage; +import net.consensys.pantheon.util.time.SystemClock; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.Logger; + +public class MainnetPantheonController implements PantheonController { + + private static final Logger LOG = getLogger(); + public static final int MAINNET_NETWORK_ID = 1; + + private final GenesisConfig genesisConfig; + private final ProtocolContext protocolContext; + private final ProtocolManager ethProtocolManager; + private final KeyPair keyPair; + private final Synchronizer synchronizer; + + private final TransactionPool transactionPool; + private final MiningCoordinator miningCoordinator; + private final Runnable close; + + public MainnetPantheonController( + final GenesisConfig genesisConfig, + final ProtocolContext protocolContext, + final ProtocolManager ethProtocolManager, + final Synchronizer synchronizer, + final KeyPair keyPair, + final TransactionPool transactionPool, + final MiningCoordinator miningCoordinator, + final Runnable close) { + this.genesisConfig = genesisConfig; + this.protocolContext = protocolContext; + this.ethProtocolManager = ethProtocolManager; + this.synchronizer = synchronizer; + this.keyPair = keyPair; + this.transactionPool = transactionPool; + this.miningCoordinator = miningCoordinator; + this.close = close; + } + + public static PantheonController mainnet(final Path home) throws IOException { + final MiningParameters miningParams = new MiningParameters(null, null, null, false); + final KeyPair nodeKeys = loadKeyPair(home); + return init( + home, + GenesisConfig.mainnet(), + SynchronizerConfiguration.builder().build(), + miningParams, + MAINNET_NETWORK_ID, + nodeKeys); + } + + public static PantheonController init( + final Path home, + final GenesisConfig genesisConfig, + final SynchronizerConfiguration taintedSyncConfig, + final MiningParameters miningParams, + final int networkId, + final KeyPair nodeKeys) + throws IOException { + final RocksDbKeyValueStorage kv = + RocksDbKeyValueStorage.create(Files.createDirectories(home.resolve(DATABASE_PATH))); + final ProtocolSchedule protocolSchedule = genesisConfig.getProtocolSchedule(); + final BlockHashFunction blockHashFunction = + ScheduleBasedBlockHashFunction.create(protocolSchedule); + final MutableBlockchain blockchain = + new DefaultMutableBlockchain(genesisConfig.getBlock(), kv, blockHashFunction); + final WorldStateArchive worldStateArchive = + new WorldStateArchive(new KeyValueStorageWorldStateStorage(kv)); + genesisConfig.writeStateTo(worldStateArchive.getMutable(Hash.EMPTY_TRIE_HASH)); + + final ProtocolContext protocolContext = + new ProtocolContext<>(blockchain, worldStateArchive, null); + + final SynchronizerConfiguration syncConfig = taintedSyncConfig.validated(blockchain); + final boolean fastSyncEnabled = syncConfig.syncMode().equals(SyncMode.FAST); + final EthProtocolManager ethProtocolManager = + new EthProtocolManager( + protocolContext.getBlockchain(), + genesisConfig.getChainId(), + fastSyncEnabled, + syncConfig.downloaderParallelism()); + final Synchronizer synchronizer = + new DefaultSynchronizer<>( + syncConfig, protocolSchedule, protocolContext, ethProtocolManager.ethContext()); + + final TransactionPool transactionPool = + TransactionPoolFactory.createTransactionPool( + protocolSchedule, protocolContext, ethProtocolManager.ethContext()); + + final ExecutorService minerThreadPool = Executors.newCachedThreadPool(); + final EthHashMinerExecutor executor = + new EthHashMinerExecutor( + protocolContext, + minerThreadPool, + protocolSchedule, + transactionPool.getPendingTransactions(), + miningParams, + new DefaultBlockScheduler( + MainnetBlockHeaderValidator.MINIMUM_SECONDS_SINCE_PARENT, + MainnetBlockHeaderValidator.TIMESTAMP_TOLERANCE_S, + new SystemClock())); + + final MiningCoordinator miningCoordinator = + new MiningCoordinator(protocolContext.getBlockchain(), executor); + miningCoordinator.addMinedBlockObserver(ethProtocolManager); + if (miningParams.isMiningEnabled()) { + miningCoordinator.enable(); + } + + return new MainnetPantheonController( + genesisConfig, + protocolContext, + ethProtocolManager, + synchronizer, + nodeKeys, + transactionPool, + miningCoordinator, + () -> { + miningCoordinator.disable(); + minerThreadPool.shutdownNow(); + try { + minerThreadPool.awaitTermination(5, TimeUnit.SECONDS); + } catch (final InterruptedException e) { + LOG.error("Failed to shutdown miner executor"); + } + kv.close(); + }); + } + + @Override + public ProtocolContext getProtocolContext() { + return protocolContext; + } + + @Override + public GenesisConfig getGenesisConfig() { + return genesisConfig; + } + + @Override + public Synchronizer getSynchronizer() { + return synchronizer; + } + + @Override + public SubProtocolConfiguration subProtocolConfiguration() { + return new SubProtocolConfiguration().withSubProtocol(EthProtocol.get(), ethProtocolManager); + } + + @Override + public KeyPair getLocalNodeKeyPair() { + return keyPair; + } + + @Override + public TransactionPool getTransactionPool() { + return transactionPool; + } + + @Override + public MiningCoordinator getMiningCoordinator() { + return miningCoordinator; + } + + @Override + public void close() { + close.run(); + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/controller/PantheonController.java b/pantheon/src/main/java/net/consensys/pantheon/controller/PantheonController.java new file mode 100755 index 00000000000..691ac572ee5 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/controller/PantheonController.java @@ -0,0 +1,86 @@ +package net.consensys.pantheon.controller; + +import net.consensys.pantheon.consensus.clique.CliqueProtocolSchedule; +import net.consensys.pantheon.consensus.ibft.IbftProtocolSchedule; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.blockcreation.MiningCoordinator; +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.core.Synchronizer; +import net.consensys.pantheon.ethereum.core.TransactionPool; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.p2p.config.SubProtocolConfiguration; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; + +import io.vertx.core.json.JsonObject; + +public interface PantheonController extends Closeable { + + String DATABASE_PATH = "database"; + + static PantheonController fromConfig( + final SynchronizerConfiguration syncConfig, + final String configContents, + final Path pantheonHome, + final boolean ottomanTestnetOperation, + final int networkId, + final MiningParameters miningParameters, + final KeyPair nodeKeys) + throws IOException { + + final JsonObject config = new JsonObject(configContents); + final JsonObject configOptions = config.getJsonObject("config"); + + if (configOptions.containsKey("ethash")) { + return MainnetPantheonController.init( + pantheonHome, + GenesisConfig.fromConfig(config, MainnetProtocolSchedule.fromConfig(configOptions)), + syncConfig, + miningParameters, + networkId, + nodeKeys); + } else if (configOptions.containsKey("ibft")) { + return IbftPantheonController.init( + pantheonHome, + GenesisConfig.fromConfig(config, IbftProtocolSchedule.create(configOptions)), + syncConfig, + ottomanTestnetOperation, + configOptions.getJsonObject("ibft"), + networkId, + nodeKeys); + } else if (configOptions.containsKey("clique")) { + return CliquePantheonController.init( + pantheonHome, + GenesisConfig.fromConfig(config, CliqueProtocolSchedule.create(configOptions, nodeKeys)), + syncConfig, + networkId, + nodeKeys); + } else { + throw new IllegalArgumentException("Unknown consensus mechanism defined"); + } + } + + default ProtocolSchedule getProtocolSchedule() { + return getGenesisConfig().getProtocolSchedule(); + } + + ProtocolContext getProtocolContext(); + + GenesisConfig getGenesisConfig(); + + Synchronizer getSynchronizer(); + + SubProtocolConfiguration subProtocolConfiguration(); + + KeyPair getLocalNodeKeyPair(); + + TransactionPool getTransactionPool(); + + MiningCoordinator getMiningCoordinator(); +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/management/RestfulRouteBuilder.java b/pantheon/src/main/java/net/consensys/pantheon/management/RestfulRouteBuilder.java new file mode 100755 index 00000000000..c41d4295668 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/management/RestfulRouteBuilder.java @@ -0,0 +1,140 @@ +package net.consensys.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/main/java/net/consensys/pantheon/util/BlockImporter.java b/pantheon/src/main/java/net/consensys/pantheon/util/BlockImporter.java new file mode 100755 index 00000000000..37ce9743ba5 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/util/BlockImporter.java @@ -0,0 +1,122 @@ +package net.consensys.pantheon.util; + +import static org.apache.logging.log4j.LogManager.getLogger; + +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.util.RawBlockIterator; +import net.consensys.pantheon.util.uint.UInt256; + +import java.io.IOException; +import java.nio.file.Path; + +import com.google.common.base.MoreObjects; +import org.apache.logging.log4j.Logger; + +/** Pantheon Block Import Util. */ +public class BlockImporter { + private static final Logger LOG = getLogger(); + /** + * Imports blocks that are stored as concatenated RLP sections in the given file into Pantheon's + * block storage. + * + * @param blocks Path to the file containing the blocks + * @param pantheonController the PantheonController that defines blockchain behavior + * @param the consensus context type + * @return the import result + * @throws IOException On Failure + */ + public BlockImporter.ImportResult importBlockchain( + final Path blocks, final PantheonController pantheonController) throws IOException { + final ProtocolSchedule protocolSchedule = pantheonController.getProtocolSchedule(); + final ProtocolContext context = pantheonController.getProtocolContext(); + final GenesisConfig genesis = pantheonController.getGenesisConfig(); + + try (final RawBlockIterator iterator = + new RawBlockIterator( + blocks, + rlp -> + BlockHeader.readFrom( + rlp, ScheduleBasedBlockHashFunction.create(protocolSchedule)))) { + final MutableBlockchain blockchain = context.getBlockchain(); + int count = 1; + BlockHeader previousHeader = null; + while (iterator.hasNext()) { + final Block block = iterator.next(); + final BlockHeader header = block.getHeader(); + if (header.getNumber() == genesis.getBlock().getHeader().getNumber()) { + continue; + } + if (header.getNumber() % 100 == 0) { + LOG.info("Import at block {}", header.getNumber()); + } + if (blockchain.contains(header.getHash())) { + continue; + } + if (previousHeader == null) { + previousHeader = lookupPreviousHeader(blockchain, header); + } + final ProtocolSpec protocolSpec = protocolSchedule.getByBlockNumber(header.getNumber()); + final BlockHeaderValidator blockHeaderValidator = protocolSpec.getBlockHeaderValidator(); + final boolean validHeader = + blockHeaderValidator.validateHeader( + header, previousHeader, context, HeaderValidationMode.FULL); + if (!validHeader) { + throw new IllegalStateException( + "Invalid header at block number " + header.getNumber() + "."); + } + final net.consensys.pantheon.ethereum.core.BlockImporter blockImporter = + protocolSpec.getBlockImporter(); + final boolean blockImported = + blockImporter.importBlock(context, block, HeaderValidationMode.NONE); + if (!blockImported) { + throw new IllegalStateException( + "Invalid block at block number " + header.getNumber() + "."); + } + ++count; + previousHeader = header; + } + return new BlockImporter.ImportResult(blockchain.getChainHead().getTotalDifficulty(), count); + } finally { + pantheonController.close(); + } + } + + private BlockHeader lookupPreviousHeader( + final MutableBlockchain blockchain, final BlockHeader header) { + return blockchain + .getBlockHeader(header.getParentHash()) + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Block %s does not connect to the existing chain. Current chain head %s", + header.getNumber(), blockchain.getChainHeadBlockNumber()))); + } + + public static final class ImportResult { + + public final UInt256 td; + + public final int count; + + ImportResult(final UInt256 td, final int count) { + this.td = td; + this.count = count; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("td", td).add("count", count).toString(); + } + } +} diff --git a/pantheon/src/main/java/net/consensys/pantheon/util/BlockchainImporter.java b/pantheon/src/main/java/net/consensys/pantheon/util/BlockchainImporter.java new file mode 100755 index 00000000000..411a51fce05 --- /dev/null +++ b/pantheon/src/main/java/net/consensys/pantheon/util/BlockchainImporter.java @@ -0,0 +1,558 @@ +package net.consensys.pantheon.util; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.chain.MutableBlockchain; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockBody; +import net.consensys.pantheon.ethereum.core.BlockHeader; +import net.consensys.pantheon.ethereum.core.Hash; +import net.consensys.pantheon.ethereum.core.MutableAccount; +import net.consensys.pantheon.ethereum.core.MutableWorldState; +import net.consensys.pantheon.ethereum.core.Transaction; +import net.consensys.pantheon.ethereum.core.TransactionReceipt; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.core.WorldUpdater; +import net.consensys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.mainnet.ScheduleBasedBlockHashFunction; +import net.consensys.pantheon.ethereum.rlp.FileRLPInput; +import net.consensys.pantheon.ethereum.rlp.RLPInput; +import net.consensys.pantheon.util.bytes.BytesValue; +import net.consensys.pantheon.util.uint.UInt256; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.Strings; + +/** + * Pantheon Blockchain Import Util. + * + *

Expected File RLP Format: + * + *

Snapshot: BlockhainSnapshot || WorldStateSnapshot + * + *

BlockchainSnapshot: N || BlockWithReceipts[0] || BlockWithReceipts[1] || ... || + * BlockWithReceipts[N] + * + *

BlockWithReceipts[n]: Block[n] || Receipts[n] + * + *

Block[n]: Header[n] || Transactions[n] || OmmerHeaders[n] + * + *

Transactions[n]: [ Transaction[0] || Transaction[1] || ... || Transaction[T] ] + * OmmerHeaders[n]: [ OmmerHeader[0] || OmmerHeader[1] || ... || OmmerHeader[O] ] Receipts[n]: [ + * Receipt[0] || Receipt[1] || ... || Receipt[T] ] + * + *

WorldStateSnapshot: AccountSnapshot[0] || AccountSnapshot[1] || ... || AccountSnapshot[A] + * AccountSnapshot[a]: AccountAddress[a] || AccountState[a] || AccountCode[a] || + * AccountStorageSnapshot[a] AccountStorageSnapshot[a]: AccountStorageEntry[0] || + * AccountStorageEntry[1] || ... || AccountStorageEntry[E] AccountStorageEntry[e]: + * AccountStorageKey[e] || AccountStorageValue[e] + * + *

N = number of blocks T = number of transactions in block O = number of ommers in block A = + * number of accounts in world state E = number of storage entries in the account || = concatenation + */ +public class BlockchainImporter extends BlockImporter { + private static final Logger LOG = LogManager.getLogger(); + private static final Logger METRICS_LOG = LogManager.getLogger(LOG.getName() + "-metrics"); + + Boolean isSkipHeaderValidation = false; + + /** + * Imports blockchain from file as concatenated RLP sections + * + * @param the consensus context type + * @param dataFilePath Path to the file containing the dataFilePath + * @param pantheonController the PantheonController that defines blockchain behavior + * @param isSkipHeaderValidation if true, header validation is skipped. This must only be used + * when the source data is fully trusted / guaranteed to be correct. + * @param metricsIntervalSec seconds between logging progress metrics + * @param accountCommitInterval commit account state every n accounts + * @param isSkipBlocks true if blocks in the import file should be skipped over. + * @param isSkipAccounts true if accounts in the import file should be skipped over. + * @param worldStateOffset file offset for the starting byte of the world state. Only relevant in + * combination with isSkipBlocks + * @return the import result + * @throws IOException On Failure + */ + public BlockImporter.ImportResult importBlockchain( + final Path dataFilePath, + final PantheonController pantheonController, + final boolean isSkipHeaderValidation, + final int metricsIntervalSec, + final int accountCommitInterval, + final boolean isSkipBlocks, + final boolean isSkipAccounts, + final Long worldStateOffset) + throws IOException { + checkNotNull(dataFilePath); + checkNotNull(pantheonController); + this.isSkipHeaderValidation = isSkipHeaderValidation; + final long startTime = System.currentTimeMillis(); + + checkNotNull(dataFilePath); + try (final FileChannel file = FileChannel.open(dataFilePath, StandardOpenOption.READ)) { + final FileRLPInput rlp = new FileRLPInput(file, true); + LOG.info("Import started."); + + BlockchainImporter.ImportResult blockImportResults; + blockImportResults = + importBlockchain( + pantheonController, rlp, isSkipBlocks, metricsIntervalSec, worldStateOffset); + + if (!isSkipAccounts) { + Hash worldStateRootHash; + worldStateRootHash = + importWorldState(pantheonController, rlp, metricsIntervalSec, accountCommitInterval); + validateWorldStateRootHash(pantheonController, worldStateRootHash); + } + + final long totalRunningSec = (System.currentTimeMillis() - startTime) / 1000; + final double totallRunningHours = totalRunningSec / (60.0 * 60); + + final String message = + format( + "Import finished in %,d seconds (%,1.2f hours).", + totalRunningSec, totallRunningHours); + METRICS_LOG.info(message); + + return blockImportResults; + } catch (final Exception e) { + final String message = format("Unable to import from file '%s'", dataFilePath.toString()); + throw new RuntimeException(message, e); + } finally { + pantheonController.close(); + } + } + + /** + * Imports the blockchain section of the file + * + * @param the consensus context type + * @param pantheonController the PantheonController that defines blockchain behavior + * @param rlp RLP Input File + * @param isSkipBlocks true if blocks in the import file should be skipped over. + * @param metricsIntervalSec seconds between logging progress metrics + * @param worldStateOffset file offset for the starting byte of the world state. Only relevant in + * combination with isSkipBlocks + * @return the import result + */ + private BlockImporter.ImportResult importBlockchain( + final PantheonController pantheonController, + final FileRLPInput rlp, + final Boolean isSkipBlocks, + final int metricsIntervalSec, + final Long worldStateOffset) { + final ProtocolSchedule protocolSchedule = pantheonController.getProtocolSchedule(); + final ProtocolContext context = pantheonController.getProtocolContext(); + final GenesisConfig genesis = pantheonController.getGenesisConfig(); + checkNotNull(isSkipBlocks); + + final long startTime = System.currentTimeMillis(); + long lapStartTime = startTime; + final long metricsIntervalMS = + 1_000L * metricsIntervalSec; // Use Millis here to make math easier + long nextMetricsTime = startTime + metricsIntervalMS; + long itemStartingOffset = 0; + final String logAction = isSkipBlocks ? "Skipped" : "Imported"; + + final long totalBlockCount = rlp.readLongScalar(); + + LOG.info( + format( + "Import file contains %,d blocks, starting at file offset %,d.", + totalBlockCount, rlp.currentOffset())); + + if (isSkipBlocks && worldStateOffset != null) { + // Skip blocks. Offset was given, so we don't even have to parse through the blocks + logFinalMetrics("Skipped", "block", startTime, Math.toIntExact(totalBlockCount)); + rlp.setTo(worldStateOffset); + return new BlockchainImporter.ImportResult(UInt256.ZERO, 0); + } + + final Function headerReader = + rlp2 -> BlockHeader.readFrom(rlp2, ScheduleBasedBlockHashFunction.create(protocolSchedule)); + + BlockHeader previousHeader = genesis.getBlock().getHeader(); + int blockCount = 0; + int lapCount = 0; + BlockHeader header = null; + BlockBody body = null; + List receipts = null; + try { + while (blockCount < totalBlockCount) { + header = null; // Reset so that if an error occurs, we log the correct data. + body = null; + receipts = null; + + blockCount++; + lapCount++; + itemStartingOffset = rlp.currentOffset(); + + if (isSkipBlocks && !(blockCount == totalBlockCount - 1)) { + // Skip block, unless it is the last one. If it's the last one, it will get parsed + // printed into the log, but not stored. This is for ops & dev debug purposes. + rlp.skipNext(); + rlp.skipNext(); + } else { + rlp.enterList(true); + header = headerReader.apply(rlp); + body = new BlockBody(rlp.readList(Transaction::readFrom), rlp.readList(headerReader)); + rlp.leaveList(); + receipts = rlp.readList(TransactionReceipt::readFrom); + final ProtocolSpec protocolSpec = + protocolSchedule.getByBlockNumber(header.getNumber()); + + if (!isSkipHeaderValidation) { + // Validate headers here because we already have the previous block, and can avoid + // an unnecessary lookup compared to doing the validation in the BlockImporter below. + final BlockHeaderValidator blockHeaderValidator = + protocolSpec.getBlockHeaderValidator(); + final boolean validHeader = + blockHeaderValidator.validateHeader( + header, previousHeader, context, HeaderValidationMode.FULL); + if (!validHeader) { + final String message = + format( + "Invalid header block number %,d at file position %,d", + header.getNumber(), itemStartingOffset); + throw new IllegalStateException(message); + } + } + + if (blockCount == 1) { + // Log the first block for ops & dev debug purposes. + LOG.info( + format( + "First Block, file offset=%,d\nHeader=%s\n\nBody=%s\n\n", + itemStartingOffset, header, body)); + } + + if (blockCount == totalBlockCount) { + // Log the last block for ops & dev debug purposes. + LOG.info( + format( + "Last Block, file offset=%,d\nHeader=%s\n\nBody=%s\n\n", + itemStartingOffset, header, body)); + } + + if (LOG.isTraceEnabled()) { + final String receiptsStr = + receipts == null ? null : Strings.join(receipts.iterator(), ','); + LOG.trace( + format( + "About to import block from file offset %,d with header=%s, body=%s, receipts=%s", + itemStartingOffset, header, body, receiptsStr)); + } + + net.consensys.pantheon.ethereum.core.BlockImporter blockImporter; + blockImporter = protocolSpec.getBlockImporter(); + + if (!isSkipBlocks) { + // Do not validate headers here. They were already validated above, since we already + // have the previous block on-hand, we avoid the extra lookup BlockImporter would do + final boolean blockImported = + blockImporter.fastImportBlock( + context, new Block(header, body), receipts, HeaderValidationMode.NONE); + if (!blockImported) { + final String message = + format( + "Invalid header block number %,d at file position %,d", + header.getNumber(), itemStartingOffset); + throw new IllegalStateException(message); + } + } + } + + if (System.currentTimeMillis() >= nextMetricsTime) { + logLapMetrics(logAction, "block", startTime, blockCount, lapStartTime, lapCount); + lapCount = 0; + lapStartTime = System.currentTimeMillis(); + nextMetricsTime = lapStartTime + metricsIntervalMS; + } + previousHeader = header; + } + } catch (final RuntimeException e) { + final String receiptsStr = receipts == null ? null : Strings.join(receipts.iterator(), ','); + final String message = + format( + "Error importing block from file offset %,d with header=%s, body=%s, receipts=%s", + itemStartingOffset, header, body, receiptsStr); + throw new RuntimeException(message, e); + } + + logFinalMetrics(logAction, "block", startTime, blockCount); + return new BlockchainImporter.ImportResult( + context.getBlockchain().getChainHead().getTotalDifficulty(), blockCount); + } + + /** + * Imports the worldstate section of the file + * + * @param pantheonController the PantheonController that defines blockchain behavior + * @param rlp RLP Input File + * @param metricsIntervalSec seconds between logging progress metrics + * @param accountCommitInterval commit account state every n accounts + * @param the consensus context type + * @return root hash of the world state + */ + private Hash importWorldState( + final PantheonController pantheonController, + final FileRLPInput rlp, + final int metricsIntervalSec, + final int accountCommitInterval) { + final ProtocolContext context = pantheonController.getProtocolContext(); + final MutableWorldState worldState = context.getWorldStateArchive().getMutable(); + WorldUpdater worldStateUpdater = worldState.updater(); + + final long startTime = System.currentTimeMillis(); + long lapStartTime = startTime; + final long metricsIntervalMS = + 1_000L * metricsIntervalSec; // Use Millis here to make math easier + long nextMetricsTime = startTime + metricsIntervalMS; + long itemStartingOffset = 0; + + int count = 0; + int lapCount = 0; + Address address = null; + MutableAccount account = null; + Long nonce = null; + Wei balance = null; + Hash storageRoot = null; + Hash codeHash = null; + + LOG.info(format("Starting Account Import at file offset %,d", rlp.currentOffset())); + LOG.info("Initial world state root hash: {}", worldState.rootHash()); + try { + while (!rlp.isDone() && rlp.nextSize() == 20) { + address = null; // reset to null here so we can log useful info if error occurs + account = null; + nonce = null; + balance = null; + storageRoot = null; + codeHash = null; + + count++; + lapCount++; + itemStartingOffset = rlp.currentOffset(); + + address = Address.readFrom(rlp); + + rlp.enterList(true); + nonce = rlp.readLongScalar(); + balance = rlp.readUInt256Scalar(Wei::wrap); + storageRoot = Hash.wrap(rlp.readBytes32()); + codeHash = Hash.wrap(rlp.readBytes32()); + rlp.leaveList(); + + if (LOG.isTraceEnabled()) { + LOG.trace( + format( + "About to import account from file offset %,d with address=%s, nonce=%s, balance=%s", + itemStartingOffset, address, nonce, balance)); + } + + account = worldStateUpdater.createAccount(address, nonce, balance); + + final BytesValue code = rlp.readBytesValue(); + account.setCode(code); + + // hash code and compare to codehash + verifyCodeHash(address, code, codeHash); + + while (!rlp.isDone() && rlp.nextSize() == 32) { + // Read an Account Storage Entry. We know the key is 32 bytes, vs 20 bytes if we started + // with the next Account + final UInt256 key = rlp.readUInt256Scalar(); + final UInt256 value = rlp.readUInt256Scalar(); + account.setStorageValue(key, value); + } + + // Add verification for each account's storage root hash here, if debugging state root + // becomes a problem + // Functionally, the single check at the end is enough, but if that fails, it doesn't give + // any + // indication about which account started the mismatch. That check can go here if it + // becomes necessary. + + if (count == 1) { + // Log the first account for ops & dev debug purposes. + LOG.info( + format( + "Importing first account number %d at file offset %,d. address=%s, account=%s, account nonce=%s, account balance=%s, account storage root=%s, account code hash=%s", + count, + itemStartingOffset, + address, + account, + nonce, + balance, + storageRoot, + codeHash)); + } + + if (count % accountCommitInterval == 0) { + worldStateUpdater.commit(); + + worldStateUpdater = + worldState.updater(); // Get a new updater, so the old one can GC itself. + } + if (count % accountCommitInterval == 0) { + worldState.persist(); + } + if (System.currentTimeMillis() >= nextMetricsTime) { + logLapMetrics("Imported", "account", startTime, count, lapStartTime, lapCount); + lapCount = 0; + lapStartTime = System.currentTimeMillis(); + nextMetricsTime = lapStartTime + metricsIntervalMS; + } + } + + // Log the last account for ops & dev debug purposes. + LOG.info( + format( + "Importing last account number %d at file offset %,d. address=%s, account=%s, account nonce=%s, account balance=%s, account storage root=%s, account code hash=%s", + count, itemStartingOffset, address, account, nonce, balance, storageRoot, codeHash)); + + // Do a final commit & persist. + worldStateUpdater.commit(); + worldState.persist(); + } catch (final Exception e) { + final String message = + format( + "Error importing account number %d at file offset %,d. address=%s, account=%s, account nonce=%s, account balance=%s, account storage root=%s, account code hash=%s", + count, itemStartingOffset, address, account, nonce, balance, storageRoot, codeHash); + throw new RuntimeException(message, e); + } + logFinalMetrics("Imported", "account", startTime, count); + LOG.info("Final world state root hash: {}", worldState.rootHash()); + return worldState.rootHash(); + } + + /** + * Verifies the account code against it's stated hash + * + * @param address Address of the account being checked + * @param code Code to be verified + * @param codeHashFromState stated hash of the code to verify + */ + private void verifyCodeHash( + final Address address, final BytesValue code, final Hash codeHashFromState) { + final Hash myHash = Hash.hash(code); + if (!myHash.equals(codeHashFromState)) { + final String message = + format( + "Code hash does not match for account %s. Expected %s, but got %s for code %s", + address, codeHashFromState, myHash, code); + throw new RuntimeException(message); + } + } + + /** + * Verifies the calculated world state's root hash against the stated value in the blockain's head + * block + * + * @param pantheonController the PantheonController that defines blockchain behavior + * @param worldStateRootHash calculated world state's root hash + * @param the consensus context type + */ + private void validateWorldStateRootHash( + final PantheonController pantheonController, final Hash worldStateRootHash) { + final ProtocolContext context = pantheonController.getProtocolContext(); + final MutableBlockchain blockchain = context.getBlockchain(); + final Optional header = blockchain.getBlockHeader(blockchain.getChainHeadHash()); + + if (!header.isPresent()) { + final String message = + "Can not get header for blockchain head, using hash " + blockchain.getChainHeadHash(); + throw new IllegalStateException(message); + } + final Hash blockStorageHash = header.get().getStateRoot(); + if (!blockStorageHash.equals(worldStateRootHash)) { + final String message = + format( + "Invalid block: state root mismatch (expected=%s, actual=%s)", + blockStorageHash, worldStateRootHash); + throw new RuntimeException(message); + } + } + + /** + * logs progress metrics for each 'lap' of execution. The total stats are also logged. Laps are + * defined as number of seconds between logging progress metrics. + * + * @param action Action being performed on itemName. (eg "Imported", "Skipped") + * @param itemName Item being tracked. (eg "account", "block"). identifier for what is being + * tracked + * @param startTime timestamp for when the overall process started + * @param totalItemCount total items processed + * @param lapStartTime timestamp for when the current lap processing started + * @param lapItemCount items processed this lap + */ + private void logLapMetrics( + final String action, + final String itemName, + final long startTime, + final int totalItemCount, + final long lapStartTime, + final int lapItemCount) { + final long curTime = System.currentTimeMillis(); + long lapRunningSec = (curTime - lapStartTime) / 1000; + long totalRunningSec = (curTime - startTime) / 1000; + lapRunningSec = lapRunningSec > 0 ? lapRunningSec : 1; // Set min time to 1 sec. + totalRunningSec = totalRunningSec > 0 ? totalRunningSec : 1; // Set min time to 1 sec. + final long lapItemPerSec = lapItemCount / lapRunningSec; + final long totalItemPerSec = totalItemCount / totalRunningSec; + final String message = + format( + "%s %,7d %ss in %3d seconds (%,5d %ss/sec). Totals: %,7d %ss in %3d seconds (%,5d %ss/sec).", + action, + lapItemCount, + itemName, + lapRunningSec, + lapItemPerSec, + itemName, + totalItemCount, + itemName, + totalRunningSec, + totalItemPerSec, + itemName); + METRICS_LOG.info(message); + } + + /** + * logs the final metrics for this process. + * + * @param action Action being performed on itemName. (eg "Imported", "Skipped") + * @param itemName Item being tracked. (eg "account", "block"). identifier for what is being + * tracked + * @param startTime timestamp for when the overall process started + * @param totalItemCount total items processed + */ + private void logFinalMetrics( + final String action, final String itemName, final long startTime, final int totalItemCount) { + final long curTime = System.currentTimeMillis(); + long totalRunningSec = (curTime - startTime) / 1000; + totalRunningSec = totalRunningSec > 0 ? totalRunningSec : 1; // Set min time to 1 sec. + final long totalItemPerSec = totalItemCount / totalRunningSec; + final String message = + format( + "%s %,d %ss in %,d seconds (%,d %ss/sec).", + action, totalItemCount, itemName, totalRunningSec, totalItemPerSec, itemName); + METRICS_LOG.info(message); + } +} diff --git a/pantheon/src/main/resources/log4j2.xml b/pantheon/src/main/resources/log4j2.xml new file mode 100755 index 00000000000..af051170155 --- /dev/null +++ b/pantheon/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + INFO + + + + + + + + + + + + diff --git a/pantheon/src/test/java/net/consensys/pantheon/RunnerTest.java b/pantheon/src/test/java/net/consensys/pantheon/RunnerTest.java new file mode 100755 index 00000000000..c56e8b5c7d0 --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/RunnerTest.java @@ -0,0 +1,253 @@ +package net.consensys.pantheon; + +import static net.consensys.pantheon.controller.KeyPairUtil.loadKeyPair; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.controller.MainnetPantheonController; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.ProtocolContext; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.core.Block; +import net.consensys.pantheon.ethereum.core.BlockImporter; +import net.consensys.pantheon.ethereum.core.BlockSyncTestUtils; +import net.consensys.pantheon.ethereum.core.MiningParametersTestBuilder; +import net.consensys.pantheon.ethereum.eth.sync.SyncMode; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import net.consensys.pantheon.ethereum.mainnet.HeaderValidationMode; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSpec; +import net.consensys.pantheon.ethereum.p2p.peers.DefaultPeer; +import net.consensys.pantheon.util.uint.UInt256; + +import java.net.InetAddress; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.assertj.core.api.Assertions; +import org.awaitility.Awaitility; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link Runner}. */ +public final class RunnerTest { + + private static final int NETWORK_ID = 10; + @Rule public final TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void fullSyncFromGenesis() throws Exception { + syncFromGenesis(SyncMode.FULL); + } + + @Test + public void fastSyncFromGenesis() throws Exception { + syncFromGenesis(SyncMode.FAST); + } + + private void syncFromGenesis(final SyncMode mode) throws Exception { + final Path dbAhead = temp.newFolder().toPath(); + final int blockCount = 500; + final KeyPair aheadDbNodeKeys = loadKeyPair(dbAhead); + final SynchronizerConfiguration fastSyncConfig = + SynchronizerConfiguration.builder() + .syncMode(mode) + // TODO: Disable switch from fast to full sync via configuration for now, set pivot to + // realistic value when world state persistence is added. + // .fastSyncPivotDistance(blockCount / 2).build(); + .fastSyncPivotDistance(0) + .build(); + + // Setup state with block data + try (final PantheonController controller = + MainnetPantheonController.init( + dbAhead, + GenesisConfig.mainnet(), + fastSyncConfig, + new MiningParametersTestBuilder().enabled(false).build(), + NETWORK_ID, + aheadDbNodeKeys)) { + setupState(blockCount, controller.getProtocolSchedule(), controller.getProtocolContext()); + } + + // Setup Runner with blocks + final PantheonController controllerAhead = + MainnetPantheonController.init( + dbAhead, + GenesisConfig.mainnet(), + fastSyncConfig, + new MiningParametersTestBuilder().enabled(false).build(), + NETWORK_ID, + aheadDbNodeKeys); + final String listenHost = InetAddress.getLoopbackAddress().getHostAddress(); + final ExecutorService executorService = Executors.newFixedThreadPool(2); + final JsonRpcConfiguration aheadJsonRpcConfiguration = jsonRpcConfiguration(); + final WebSocketConfiguration aheadWebSocketConfiguration = wsRpcConfiguration(); + final RunnerBuilder runnerBuilder = new RunnerBuilder(); + final Runner runnerAhead = + runnerBuilder.build( + Vertx.vertx(), + controllerAhead, + true, + Collections.emptyList(), + listenHost, + 0, + 3, + aheadJsonRpcConfiguration, + aheadWebSocketConfiguration, + dbAhead); + try { + + executorService.submit(runnerAhead::execute); + final JsonRpcConfiguration behindJsonRpcConfiguration = jsonRpcConfiguration(); + final WebSocketConfiguration behindWebSocketConfiguration = wsRpcConfiguration(); + + // Setup runner with no block data + final Path dbBehind = temp.newFolder().toPath(); + final KeyPair behindDbNodeKeys = loadKeyPair(dbBehind); + final PantheonController controllerBehind = + MainnetPantheonController.init( + temp.newFolder().toPath(), + GenesisConfig.mainnet(), + fastSyncConfig, + new MiningParametersTestBuilder().enabled(false).build(), + NETWORK_ID, + behindDbNodeKeys); + final Runner runnerBehind = + runnerBuilder.build( + Vertx.vertx(), + controllerBehind, + true, + Collections.singletonList( + new DefaultPeer( + aheadDbNodeKeys.getPublicKey().getEncodedBytes(), + listenHost, + runnerAhead.getP2pUdpPort(), + runnerAhead.getP2pTcpPort())), + listenHost, + 0, + 3, + behindJsonRpcConfiguration, + behindWebSocketConfiguration, + dbBehind); + + executorService.submit(runnerBehind::execute); + final Call.Factory client = new OkHttpClient(); + Awaitility.await() + .ignoreExceptions() + .atMost(5L, TimeUnit.MINUTES) + .untilAsserted( + () -> { + final String baseUrl = + String.format("http://%s:%s", listenHost, runnerBehind.getJsonRpcPort().get()); + try (final Response resp = + client + .newCall( + new Request.Builder() + .post( + RequestBody.create( + MediaType.parse("application/json; charset=utf-8"), + "{\"jsonrpc\":\"2.0\",\"id\":" + + Json.encode(7) + + ",\"method\":\"eth_syncing\"}")) + .url(baseUrl) + .build()) + .execute()) { + + assertThat(resp.code()).isEqualTo(200); + + final int currentBlock = + UInt256.fromHexString( + new JsonObject(resp.body().string()) + .getJsonObject("result") + .getString("currentBlock")) + .toInt(); + assertThat(currentBlock).isEqualTo(blockCount); + } + }); + + final Future future = Future.future(); + final HttpClient httpClient = Vertx.vertx().createHttpClient(); + httpClient.websocket( + runnerBehind.getWebsocketPort().get(), + WebSocketConfiguration.DEFAULT_WEBSOCKET_HOST, + "/", + ws -> { + ws.write( + Buffer.buffer( + "{\"id\": 1, \"method\": \"eth_subscribe\", \"params\": [\"syncing\"]}")); + ws.handler( + buffer -> { + final boolean matches = + buffer.toString().equals("{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":\"0x0\"}"); + if (matches) { + future.complete(); + } else { + future.fail("Unexpected result"); + } + }); + }); + Awaitility.await() + .catchUncaughtExceptions() + .atMost(5L, TimeUnit.MINUTES) + .until(future::isComplete); + } finally { + executorService.shutdownNow(); + if (!executorService.awaitTermination(2L, TimeUnit.MINUTES)) { + Assertions.fail("One of the two Pantheon runs failed to cleanly join."); + } + } + } + + private JsonRpcConfiguration jsonRpcConfiguration() { + final JsonRpcConfiguration configuration = JsonRpcConfiguration.createDefault(); + configuration.setPort(0); + configuration.setEnabled(true); + return configuration; + } + + private WebSocketConfiguration wsRpcConfiguration() { + final WebSocketConfiguration configuration = WebSocketConfiguration.createDefault(); + configuration.setPort(0); + configuration.setEnabled(true); + return configuration; + } + + private static void setupState( + final int count, + final ProtocolSchedule protocolSchedule, + final ProtocolContext protocolContext) { + final List blocks = BlockSyncTestUtils.firstBlocks(count + 1); + + for (int i = 1; i < count + 1; ++i) { + final Block block = blocks.get(i); + final ProtocolSpec protocolSpec = + protocolSchedule.getByBlockNumber(block.getHeader().getNumber()); + final BlockImporter blockImporter = protocolSpec.getBlockImporter(); + final boolean result = + blockImporter.importBlock(protocolContext, block, HeaderValidationMode.FULL); + if (!result) { + throw new IllegalStateException("Unable to import block " + block.getHeader().getNumber()); + } + } + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/net/consensys/pantheon/cli/CommandTestAbstract.java new file mode 100755 index 00000000000..a0546785101 --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/cli/CommandTestAbstract.java @@ -0,0 +1,93 @@ +package net.consensys.pantheon.cli; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.Runner; +import net.consensys.pantheon.RunnerBuilder; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import net.consensys.pantheon.util.BlockImporter; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import picocli.CommandLine.DefaultExceptionHandler; +import picocli.CommandLine.Help.Ansi; +import picocli.CommandLine.RunLast; + +@RunWith(MockitoJUnitRunner.class) +public abstract class CommandTestAbstract { + + private final Logger LOGGER = LogManager.getLogger(); + + final ByteArrayOutputStream commandOutput = new ByteArrayOutputStream(); + private final PrintStream outPrintStream = new PrintStream(commandOutput); + + final ByteArrayOutputStream commandErrorOutput = new ByteArrayOutputStream(); + private final PrintStream errPrintStream = new PrintStream(commandErrorOutput); + + @Mock RunnerBuilder mockRunnerBuilder; + @Mock Runner mockRunner; + @Mock PantheonControllerBuilder mockControllerBuilder; + @Mock SynchronizerConfiguration.Builder mockSyncConfBuilder; + @Mock SynchronizerConfiguration mockSyncConf; + @Mock PantheonController mockController; + @Mock BlockImporter mockBlockImporter; + + @Captor ArgumentCaptor> stringListArgumentCaptor; + @Captor ArgumentCaptor pathArgumentCaptor; + @Captor ArgumentCaptor fileArgumentCaptor; + @Captor ArgumentCaptor stringArgumentCaptor; + @Captor ArgumentCaptor intArgumentCaptor; + @Captor ArgumentCaptor jsonRpcConfigArgumentCaptor; + @Captor ArgumentCaptor wsRpcConfigArgumentCaptor; + + @Before + public void initMocks() throws Exception { + // doReturn used because of generic PantheonController + Mockito.doReturn(mockController) + .when(mockControllerBuilder) + .build(any(), any(), any(), anyBoolean(), any(), anyBoolean(), anyInt()); + + when(mockSyncConfBuilder.build()).thenReturn(mockSyncConf); + } + + // Display outputs for debug purpose + @After + public void displayOutput() { + LOGGER.info("Standard output {}", commandOutput.toString()); + LOGGER.info("Standard error {}", commandErrorOutput.toString()); + } + + void parseCommand(final String... args) { + + final PantheonCommand pantheonCommand = + new PantheonCommand( + mockBlockImporter, null, mockRunnerBuilder, mockControllerBuilder, mockSyncConfBuilder); + + // parse using Ansi.OFF to be able to assert on non formatted output results + pantheonCommand.parse( + new RunLast().useOut(outPrintStream).useAnsi(Ansi.OFF), + new DefaultExceptionHandler>().useErr(errPrintStream).useAnsi(Ansi.OFF), + args); + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java b/pantheon/src/test/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java new file mode 100755 index 00000000000..b9050834ff7 --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/cli/ConfigOptionSearchAndRunHandlerTest.java @@ -0,0 +1,95 @@ +package net.consensys.pantheon.cli; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import picocli.CommandLine; +import picocli.CommandLine.AbstractParseResultHandler; +import picocli.CommandLine.DefaultExceptionHandler; +import picocli.CommandLine.Help.Ansi; +import picocli.CommandLine.IDefaultValueProvider; +import picocli.CommandLine.Model.IGetter; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.ParseResult; +import picocli.CommandLine.RunLast; + +@RunWith(MockitoJUnitRunner.class) +public class ConfigOptionSearchAndRunHandlerTest { + + private static final String CONFIG_FILE_OPTION_NAME = "--config"; + @Rule public final TemporaryFolder temp = new TemporaryFolder(); + + @Rule public ExpectedException exceptionRule = ExpectedException.none(); + + private final ByteArrayOutputStream commandOutput = new ByteArrayOutputStream(); + private final ByteArrayOutputStream commandErrorOutput = new ByteArrayOutputStream(); + private final PrintStream outPrintStream = new PrintStream(commandOutput); + private final PrintStream errPrintStream = new PrintStream(commandErrorOutput); + + private final AbstractParseResultHandler> resultHandler = + new RunLast().useOut(outPrintStream).useAnsi(Ansi.OFF); + private final DefaultExceptionHandler> exceptionHandler = + new DefaultExceptionHandler>().useErr(errPrintStream).useAnsi(Ansi.OFF); + private final ConfigOptionSearchAndRunHandler configParsingHandler = + new ConfigOptionSearchAndRunHandler(resultHandler, exceptionHandler, CONFIG_FILE_OPTION_NAME); + + @Mock ParseResult mockParseResult; + @Mock CommandLine mockCommandLine; + @Mock OptionSpec mockConfigOptionSpec; + @Mock IGetter mockConfigOptionGetter; + + @Before + public void initMocks() { + final List commandLines = new ArrayList<>(); + commandLines.add(mockCommandLine); + when(mockParseResult.asCommandLineList()).thenReturn(commandLines); + final List originalArgs = new ArrayList<>(); + originalArgs.add(CONFIG_FILE_OPTION_NAME); + when(mockParseResult.originalArgs()).thenReturn(originalArgs); + when(mockParseResult.matchedOption(CONFIG_FILE_OPTION_NAME)).thenReturn(mockConfigOptionSpec); + when(mockParseResult.hasMatchedOption(CONFIG_FILE_OPTION_NAME)).thenReturn(true); + when(mockConfigOptionSpec.getter()).thenReturn(mockConfigOptionGetter); + } + + @Test + public void handle() throws Exception { + when(mockConfigOptionGetter.get()).thenReturn(temp.newFile()); + final List result = configParsingHandler.handle(mockParseResult); + verify(mockCommandLine).setDefaultValueProvider(any(IDefaultValueProvider.class)); + verify(mockCommandLine).parseWithHandlers(eq(resultHandler), eq(exceptionHandler), anyString()); + assertThat(result, is(empty())); + } + + @Test + public void handleShouldRaiseExceptionIfNoFileParam() throws Exception { + exceptionRule.expect(Exception.class); + final String error_message = "an error occurred during get"; + exceptionRule.expectMessage(error_message); + when(mockConfigOptionGetter.get()).thenThrow(new Exception(error_message)); + configParsingHandler.handle(mockParseResult); + } + + @Test + public void selfMustReturnTheHandler() { + assertThat(configParsingHandler.self(), is(configParsingHandler)); + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/cli/ExportPublicKeySubCommandTest.java b/pantheon/src/test/java/net/consensys/pantheon/cli/ExportPublicKeySubCommandTest.java new file mode 100755 index 00000000000..38cffd17245 --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/cli/ExportPublicKeySubCommandTest.java @@ -0,0 +1,48 @@ +package net.consensys.pantheon.cli; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; + +import java.io.File; + +import org.junit.Test; + +public class ExportPublicKeySubCommandTest extends CommandTestAbstract { + + @Test + public void callingExportPublicKeySubCommandWithoutPathMustDisplayErrorAndUsage() { + parseCommand("export-pub-key"); + final String expectedErrorOutputStart = "Missing required parameter: PATH"; + assertThat(commandErrorOutput.toString()).startsWith(expectedErrorOutputStart); + } + + @Test + public void callingExportPublicKeySubCommandHelpMustDisplayImportUsage() { + parseCommand("export-pub-key", "--help"); + final String expectedOutputStart = "Usage: pantheon export-pub-key [-hV] PATH"; + assertThat(commandOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingExportPublicKeySubCommandWithFilePathMustWritePublicKeyInThisFile() + throws Exception { + + final KeyPair keyPair = KeyPair.generate(); + + when(mockController.getLocalNodeKeyPair()).thenReturn(keyPair); + + final File file = File.createTempFile("public", "key"); + parseCommand("export-pub-key", file.getPath()); + + assertThat(contentOf(file)) + .startsWith(keyPair.getPublicKey().toString()) + .endsWith(keyPair.getPublicKey().toString()); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/cli/ImportSubCommandTest.java b/pantheon/src/test/java/net/consensys/pantheon/cli/ImportSubCommandTest.java new file mode 100755 index 00000000000..36b82141aca --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/cli/ImportSubCommandTest.java @@ -0,0 +1,41 @@ +package net.consensys.pantheon.cli; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.Test; + +public class ImportSubCommandTest extends CommandTestAbstract { + + @Test + public void callingImportSubCommandWithoutPathMustDisplayErrorAndUsage() { + parseCommand("import"); + final String expectedErrorOutputStart = "Missing required parameter: PATH"; + assertThat(commandErrorOutput.toString()).startsWith(expectedErrorOutputStart); + } + + @Test + public void callingImportSubCommandHelpMustDisplayImportUsage() { + parseCommand("import", "--help"); + final String expectedOutputStart = "Usage: pantheon import [-hV] PATH"; + assertThat(commandOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingImportSubCommandWithPathMustImportBlocksWithThisPath() throws Exception { + final Path path = Paths.get("."); + parseCommand("import", path.toString()); + + verify(mockBlockImporter).importBlockchain(pathArgumentCaptor.capture(), any()); + + assertThat(pathArgumentCaptor.getValue()).isEqualByComparingTo(path); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/net/consensys/pantheon/cli/PantheonCommandTest.java new file mode 100755 index 00000000000..facced0ec1d --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/cli/PantheonCommandTest.java @@ -0,0 +1,783 @@ +package net.consensys.pantheon.cli; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNotNull; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import net.consensys.pantheon.PantheonInfo; +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.core.Address; +import net.consensys.pantheon.ethereum.core.Wei; +import net.consensys.pantheon.ethereum.eth.sync.SyncMode; +import net.consensys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; +import net.consensys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; +import net.consensys.pantheon.util.bytes.BytesValue; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Optional; + +import com.google.common.collect.Lists; +import com.google.common.io.Resources; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; + +public class PantheonCommandTest extends CommandTestAbstract { + + @Rule public final TemporaryFolder temp = new TemporaryFolder(); + + @Override + @Before + public void initMocks() throws Exception { + super.initMocks(); + + when(mockRunnerBuilder.build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + any(), + any(), + any())) + .thenReturn(mockRunner); + } + + @Test + public void callingHelpSubCommandMustDisplayUsage() { + parseCommand("--help"); + final String expectedOutputStart = String.format("Usage:%n%npantheon [OPTIONS] [COMMAND]"); + assertThat(commandOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingVersionDisplayPantheonInfoVersion() { + parseCommand("--version"); + assertThat(commandOutput.toString()).isEqualToIgnoringWhitespace(PantheonInfo.version()); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + // Testing default values + @Test + public void callingPantheonCommandWithoutOptionsMustSyncWithDefaultValues() throws Exception { + parseCommand(); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + eq(true), + isNull(), + eq("127.0.0.1"), + eq(30303), + eq(25), + eq(JsonRpcConfiguration.createDefault()), + eq(WebSocketConfiguration.createDefault()), + any()); + + final ArgumentCaptor miningArg = + ArgumentCaptor.forClass(MiningParameters.class); + verify(mockControllerBuilder) + .build(any(), isNull(), isNotNull(), eq(false), miningArg.capture(), eq(false), anyInt()); + + verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FULL)); + + assertThat(commandErrorOutput.toString()).isEmpty(); + assertThat(miningArg.getValue().getCoinbase()).isEqualTo(Optional.empty()); + assertThat(miningArg.getValue().getMinTransactionGasPrice()).isEqualTo(Wei.of(1000)); + assertThat(miningArg.getValue().getExtraData()).isEqualTo(BytesValue.EMPTY); + } + + // Testing each option + @Test + public void CallingWithConfigOptionButNoConfigFileShouldDisplayHelp() { + + parseCommand("--config"); + + final String expectedOutputStart = "Missing required parameter for option '--config' ()"; + assertThat(commandErrorOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void CallingWithConfigOptionButNonExistingFileShouldDisplayHelp() throws IOException { + final File tempConfigFile = temp.newFile("an-invalid-file-name-without-extension"); + parseCommand("--config", tempConfigFile.getPath()); + + final String expectedOutputStart = "Unable to read TOML configuration file " + tempConfigFile; + assertThat(commandErrorOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void CallingWithConfigOptionButTomlFileNotFoundShouldDisplayHelp() { + + parseCommand("--config", "./an-invalid-file-name-sdsd87sjhqoi34io23.toml"); + + final String expectedOutputStart = "Unable to read TOML configuration, file not found."; + assertThat(commandErrorOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandOutput.toString()).isEmpty(); + } + + @Test + public void CallingWithConfigOptionButInvalidContentTomlFileShouldDisplayHelp() throws Exception { + + // We write a config file to prevent an invalid file in resource folder to raise errors in + // code checks (CI + IDE) + final File tempConfigFile = temp.newFile("invalid_config.toml"); + try (Writer fileWriter = Files.newBufferedWriter(tempConfigFile.toPath(), UTF_8)) { + + fileWriter.write("."); // an invalid toml content + fileWriter.flush(); + + parseCommand("--config", tempConfigFile.getPath()); + + final String expectedOutputStart = + "Invalid TOML configuration : Unexpected '.', expected a-z, A-Z, 0-9, ', \", a table key, " + + "a newline, or end-of-input (line 1, column 1)"; + assertThat(commandErrorOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandOutput.toString()).isEmpty(); + } + } + + @Test + public void CallingWithConfigOptionButInvalidValueTomlFileShouldDisplayHelp() throws Exception { + + // We write a config file to prevent an invalid file in resource folder to raise errors in + // code checks (CI + IDE) + final File tempConfigFile = temp.newFile("invalid_config.toml"); + try (Writer fileWriter = Files.newBufferedWriter(tempConfigFile.toPath(), UTF_8)) { + + fileWriter.write("tester===========......."); // an invalid toml content + fileWriter.flush(); + + parseCommand("--config", tempConfigFile.getPath()); + + final String expectedOutputStart = + "Invalid TOML configuration : Unexpected '=', expected ', \", ''', \"\"\", a number, " + + "a boolean, a date/time, an array, or a table (line 1, column 8)"; + assertThat(commandErrorOutput.toString()).startsWith(expectedOutputStart); + assertThat(commandOutput.toString()).isEmpty(); + } + } + + @Test + public void OverrideDefaultValuesIfKeyIsPresentInConfigFile() throws IOException { + final String configFile = Resources.getResource("complete_config.toml").getFile(); + + final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); + jsonRpcConfiguration.setEnabled(false); + jsonRpcConfiguration.setHost("5.6.7.8"); + jsonRpcConfiguration.setPort(5678); + jsonRpcConfiguration.setCorsAllowedDomains(Collections.emptyList()); + jsonRpcConfiguration.setRpcApis(JsonRpcConfiguration.DEFAULT_JSON_RPC_APIS); + + final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); + webSocketConfiguration.setEnabled(false); + webSocketConfiguration.setHost("9.10.11.12"); + webSocketConfiguration.setPort(9101); + webSocketConfiguration.setRpcApis(WebSocketConfiguration.DEFAULT_WEBSOCKET_APIS); + + parseCommand("--config", configFile); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + eq(false), + stringListArgumentCaptor.capture(), + eq("1.2.3.4"), + eq(1234), + eq(42), + eq(jsonRpcConfiguration), + eq(webSocketConfiguration), + any()); + + final String[] nodes = {"enode://001@123:4567", "enode://002@123:4567", "enode://003@123:4567"}; + assertThat(stringListArgumentCaptor.getValue().toArray()).isEqualTo(nodes); + + verify(mockControllerBuilder) + .build( + any(), + eq(new File("~/genesys.json")), + eq(Paths.get("~/pantheondata")), + eq(false), + any(), + anyBoolean(), + anyInt()); + + verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FAST)); + + assertThat(commandErrorOutput.toString()).isEmpty(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void NoOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() throws IOException { + final String configFile = Resources.getResource("partial_config.toml").getFile(); + + parseCommand("--config", configFile); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + eq(true), + isNull(), + eq("127.0.0.1"), + eq(30303), + eq(25), + eq(JsonRpcConfiguration.createDefault()), + eq(WebSocketConfiguration.createDefault()), + any()); + + verify(mockControllerBuilder) + .build(any(), eq(null), any(), eq(false), any(), eq(false), anyInt()); + + verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FULL)); + + assertThat(commandErrorOutput.toString()).isEmpty(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void dataDirOptionMustBeUsed() throws Exception { + final Path path = Paths.get("."); + + parseCommand("--datadir", path.toString()); + + verify(mockControllerBuilder) + .build( + any(), + isNull(), + pathArgumentCaptor.capture(), + anyBoolean(), + any(), + anyBoolean(), + anyInt()); + + assertThat(pathArgumentCaptor.getValue()).isEqualByComparingTo(path); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void genesisPathOptionMustBeUsed() throws Exception { + final Path path = Paths.get("."); + + parseCommand("--genesis", path.toString()); + + verify(mockControllerBuilder) + .build( + any(), + fileArgumentCaptor.capture(), + any(), + anyBoolean(), + any(), + anyBoolean(), + anyInt()); + + assertThat(fileArgumentCaptor.getValue().toPath()).isEqualByComparingTo(path); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void discoveryOptionMustBeUsed() { + parseCommand("--no-discovery"); + // Discovery stored in runner is the negative of the option passed to CLI + // So as passing the option means noDiscovery will be true, then discovery is false in runner + + verify(mockRunnerBuilder) + .build( + any(), any(), eq(false), any(), anyString(), anyInt(), anyInt(), any(), any(), any()); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void callingWithBootnodesOptionButNoValueMustDisplayErrorAndUsage() { + parseCommand("--bootnodes"); + assertThat(commandOutput.toString()).isEmpty(); + final String expectedErrorOutputStart = + "Missing required parameter for option '--bootnodes' at index 0 ()"; + assertThat(commandErrorOutput.toString()).startsWith(expectedErrorOutputStart); + } + + @Test + public void bootnodesOptionMustBeUsed() { + final String[] nodes = {"enode://001@123:4567", "enode://002@123:4567", "enode://003@123:4567"}; + parseCommand("--bootnodes", String.join(",", nodes)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + stringListArgumentCaptor.capture(), + anyString(), + anyInt(), + anyInt(), + any(), + any(), + any()); + + assertThat(stringListArgumentCaptor.getValue().toArray()).isEqualTo(nodes); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void p2pHostAndPortOptionMustBeUsed() { + + final String host = "1.2.3.4"; + final int port = 1234; + parseCommand("--p2p-listen", String.format("%1$s:%2$s", host, port)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + stringArgumentCaptor.capture(), + intArgumentCaptor.capture(), + anyInt(), + any(), + any(), + any()); + + assertThat(stringArgumentCaptor.getValue()).isEqualTo(host); + assertThat(intArgumentCaptor.getValue()).isEqualTo(port); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void maxpeersOptionMustBeUsed() { + + final int maxPeers = 123; + parseCommand("--max-peers", String.valueOf(maxPeers)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + intArgumentCaptor.capture(), + any(), + any(), + any()); + + assertThat(intArgumentCaptor.getValue()).isEqualTo(maxPeers); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void syncModeOptionMustBeUsed() { + + parseCommand("--sync-mode", "FAST"); + verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FAST)); + + parseCommand("--sync-mode", "FULL"); + verify(mockSyncConfBuilder).syncMode(ArgumentMatchers.eq(SyncMode.FULL)); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void maxTrailingPeersMustBeUsed() { + parseCommand("--max-trailing-peers", "3"); + verify(mockSyncConfBuilder).maxTrailingPeers(3); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcEnabledPropertyDefaultIsFalse() { + parseCommand(); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + jsonRpcConfigArgumentCaptor.capture(), + any(), + any()); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().isEnabled()).isFalse(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcEnabledPropertyMustBeUsed() { + parseCommand("--rpc-enabled"); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + jsonRpcConfigArgumentCaptor.capture(), + any(), + any()); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().isEnabled()).isTrue(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcHostAndPortOptionMustBeUsed() { + + final String host = "1.2.3.4"; + final int port = 1234; + parseCommand("--rpc-listen", String.format("%1$s:%2$s", host, port)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + jsonRpcConfigArgumentCaptor.capture(), + any(), + any()); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(jsonRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcCorsOriginsTwoDomainsMustBuildListWithBothDomains() { + final String[] origins = {"http://domain1.com", "https://domain2.com"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + jsonRpcConfigArgumentCaptor.capture(), + any(), + any()); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) + .isEqualTo(origins); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcCorsOriginsWithWildcardMustBuildListWithWildcard() { + final String[] origins = {"*"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + jsonRpcConfigArgumentCaptor.capture(), + any(), + any()); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains().toArray()) + .isEqualTo(origins); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcCorsOriginsWithAllMustBuildListWithWildcard() { + final String[] origins = {"all"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + jsonRpcConfigArgumentCaptor.capture(), + any(), + any()); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()) + .isEqualTo(Lists.newArrayList("*")); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcCorsOriginsWithNoneMustBuildEmptyList() { + final String[] origins = {"none"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + jsonRpcConfigArgumentCaptor.capture(), + any(), + any()); + + assertThat(jsonRpcConfigArgumentCaptor.getValue().getCorsAllowedDomains()).isEmpty(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void jsonRpcCorsOriginsNoneWithAnotherDomainMustFail() { + final String[] origins = {"http://domain1.com", "none"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .contains("Value 'none' can't be used with other domains"); + } + + @Test + public void jsonRpcCorsOriginsAllWithAnotherDomainMustFail() { + final String[] origins = {"http://domain1.com", "all"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .contains("Value 'all' can't be used with other domains"); + } + + @Test + public void jsonRpcCorsOriginsWildcardWithAnotherDomainMustFail() { + final String[] origins = {"http://domain1.com", "*"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .contains("Value 'all' can't be used with other domains"); + } + + @Test + public void jsonRpcCorsOriginsInvalidRegexShouldFail() { + final String[] origins = {"**"}; + parseCommand("--rpc-cors-origins", String.join(",", origins)); + + verifyZeroInteractions(mockRunnerBuilder); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .contains("Domain values result in invalid regex pattern"); + } + + @Test + public void wsRpcEnabledPropertyDefaultIsFalse() { + parseCommand(); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + any(), + wsRpcConfigArgumentCaptor.capture(), + any()); + + assertThat(wsRpcConfigArgumentCaptor.getValue().isEnabled()).isFalse(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void wsRpcEnabledPropertyMustBeUsed() { + parseCommand("--ws-enabled"); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + any(), + wsRpcConfigArgumentCaptor.capture(), + any()); + + assertThat(wsRpcConfigArgumentCaptor.getValue().isEnabled()).isTrue(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void wsRpcHostAndPortOptionMustBeUsed() { + final String host = "1.2.3.4"; + final int port = 1234; + parseCommand("--ws-listen", String.format("%1$s:%2$s", host, port)); + + verify(mockRunnerBuilder) + .build( + any(), + any(), + anyBoolean(), + any(), + anyString(), + anyInt(), + anyInt(), + any(), + wsRpcConfigArgumentCaptor.capture(), + any()); + + assertThat(wsRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(wsRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void pantheonDoesNotStartInMiningModeIfCoinbaseNotSet() throws Exception { + parseCommand("--miner-enabled"); + + final ArgumentCaptor miningArg = + ArgumentCaptor.forClass(MiningParameters.class); + + verifyZeroInteractions(mockControllerBuilder); + } + + @Test + public void miningIsEnabledWhenSpecified() throws Exception { + final String coinbaseStr = String.format("%020x", 1); + parseCommand("--miner-enabled", "--miner-coinbase=" + coinbaseStr); + + final ArgumentCaptor miningArg = + ArgumentCaptor.forClass(MiningParameters.class); + + verify(mockControllerBuilder) + .build(any(), any(), any(), anyBoolean(), miningArg.capture(), anyBoolean(), anyInt()); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + assertThat(miningArg.getValue().isMiningEnabled()).isTrue(); + assertThat(miningArg.getValue().getCoinbase()) + .isEqualTo(Optional.of(Address.fromHexString(coinbaseStr))); + } + + @Test + public void miningParametersAreCaptured() throws Exception { + final Address requestedCoinbase = Address.fromHexString("0000011111222223333344444"); + final String extraDataString = + "0x1122334455667788990011223344556677889900112233445566778899001122"; + parseCommand( + "--miner-coinbase=" + requestedCoinbase.toString(), + "--miner-minTransactionGasPriceWei=15", + "--miner-extraData=" + extraDataString); + + final ArgumentCaptor miningArg = + ArgumentCaptor.forClass(MiningParameters.class); + + verify(mockControllerBuilder) + .build(any(), any(), any(), anyBoolean(), miningArg.capture(), anyBoolean(), anyInt()); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + assertThat(miningArg.getValue().getCoinbase()).isEqualTo(Optional.of(requestedCoinbase)); + assertThat(miningArg.getValue().getMinTransactionGasPrice()).isEqualTo(Wei.of(15)); + assertThat(miningArg.getValue().getExtraData()) + .isEqualTo(BytesValue.fromHexString(extraDataString)); + } + + @Test + public void devModeOptionMustBeUsed() throws Exception { + parseCommand("--dev-mode"); + verify(mockControllerBuilder) + .build(any(), any(), any(), anyBoolean(), any(), eq(true), anyInt()); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProviderTest.java b/pantheon/src/test/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProviderTest.java new file mode 100755 index 00000000000..7dae34ad654 --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/cli/TomlConfigFileDefaultProviderTest.java @@ -0,0 +1,139 @@ +package net.consensys.pantheon.cli; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.util.Collection; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import picocli.CommandLine; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.ParameterException; + +@RunWith(MockitoJUnitRunner.class) +public class TomlConfigFileDefaultProviderTest { + @Mock CommandLine mockCommandLine; + + @Rule public final TemporaryFolder temp = new TemporaryFolder(); + + @Rule public ExpectedException exceptionRule = ExpectedException.none(); + + @Test + public void defaultValueIsNullIfNoMatchingKeyFoundOtherwiseTheValue() throws IOException { + final File tempConfigFile = temp.newFile("config.toml"); + try (Writer fileWriter = Files.newBufferedWriter(tempConfigFile.toPath(), UTF_8)) { + + fileWriter.write("an-option='123'"); + fileWriter.flush(); + + final TomlConfigFileDefaultProvider providerUnderTest = + new TomlConfigFileDefaultProvider(mockCommandLine, tempConfigFile); + + // this option won't be found in config + assertThat(providerUnderTest.defaultValue(OptionSpec.builder("myoption").build())).isNull(); + + // this option must be found in config + assertThat(providerUnderTest.defaultValue(OptionSpec.builder("an-option").build())) + .isEqualTo("123"); + } + } + + @Test + public void defaultValueForOptionMustMatchType() throws IOException { + final File tempConfigFile = temp.newFile("config.toml"); + try (BufferedWriter fileWriter = Files.newBufferedWriter(tempConfigFile.toPath(), UTF_8)) { + + fileWriter.write("a-boolean-option=true"); + fileWriter.newLine(); + fileWriter.write("a-multy-value-option=[\"value1\", \"value2\"]"); + fileWriter.newLine(); + fileWriter.write("an-int-value-option=123"); + fileWriter.newLine(); + fileWriter.write("an-string-value-option='my value'"); + fileWriter.flush(); + + final TomlConfigFileDefaultProvider providerUnderTest = + new TomlConfigFileDefaultProvider(mockCommandLine, tempConfigFile); + + assertThat( + providerUnderTest.defaultValue( + OptionSpec.builder("a-boolean-option").type(Boolean.class).build())) + .isEqualTo("true"); + + assertThat( + providerUnderTest.defaultValue( + OptionSpec.builder("a-multy-value-option").type(Collection.class).build())) + .isEqualTo("value1,value2"); + + assertThat( + providerUnderTest.defaultValue( + OptionSpec.builder("an-int-value-option").type(Integer.class).build())) + .isEqualTo("123"); + + assertThat( + providerUnderTest.defaultValue( + OptionSpec.builder("an-string-value-option").type(String.class).build())) + .isEqualTo("my value"); + } + } + + @Test + public void configFileNotFoundMustThrow() { + + exceptionRule.expect(ParameterException.class); + + final File nonExistingFile = new File("doesnt.exit"); + exceptionRule.expectMessage("Unable to read TOML configuration, file not found."); + + final TomlConfigFileDefaultProvider providerUnderTest = + new TomlConfigFileDefaultProvider(mockCommandLine, nonExistingFile); + + providerUnderTest.defaultValue(OptionSpec.builder("an-option").type(String.class).build()); + } + + @Test + public void invalidConfigMustThrow() throws IOException { + + exceptionRule.expect(ParameterException.class); + exceptionRule.expectMessage("Unable to read TOML configuration file"); + + final File tempConfigFile = temp.newFile("config.toml"); + + final TomlConfigFileDefaultProvider providerUnderTest = + new TomlConfigFileDefaultProvider(mockCommandLine, tempConfigFile); + + providerUnderTest.defaultValue(OptionSpec.builder("an-option").type(String.class).build()); + } + + @Test + public void invalidConfigContentMustThrow() throws IOException { + + exceptionRule.expect(ParameterException.class); + exceptionRule.expectMessage( + "Invalid TOML configuration : Unexpected '=', expected ', \", ''', " + + "\"\"\", a number, a boolean, a date/time, an array, or a table (line 1, column 19)"); + + final File tempConfigFile = temp.newFile("config.toml"); + try (BufferedWriter fileWriter = Files.newBufferedWriter(tempConfigFile.toPath(), UTF_8)) { + + fileWriter.write("an-invalid-syntax=======...."); + fileWriter.flush(); + + final TomlConfigFileDefaultProvider providerUnderTest = + new TomlConfigFileDefaultProvider(mockCommandLine, tempConfigFile); + + providerUnderTest.defaultValue(OptionSpec.builder("an-option").type(String.class).build()); + } + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/util/BlockImporterTest.java b/pantheon/src/test/java/net/consensys/pantheon/util/BlockImporterTest.java new file mode 100755 index 00000000000..68f091a8991 --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/util/BlockImporterTest.java @@ -0,0 +1,71 @@ +package net.consensys.pantheon.util; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.controller.MainnetPantheonController; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.core.MiningParametersTestBuilder; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.testutil.BlockTestUtil; +import net.consensys.pantheon.util.uint.UInt256; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import com.google.common.io.Resources; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link BlockImporter}. */ +public final class BlockImporterTest { + + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + BlockImporter blockImporter = new BlockImporter(); + + @Test + public void blockImport() throws IOException { + final Path source = folder.newFile().toPath(); + final Path target = folder.newFolder().toPath(); + BlockTestUtil.write1000Blocks(source); + final BlockImporter.ImportResult result = + blockImporter.importBlockchain(source, MainnetPantheonController.mainnet(target)); + assertThat(result.count).isEqualTo(1000); + assertThat(result.td).isEqualTo(UInt256.of(21991996248790L)); + } + + @Test + public void ibftImport() throws IOException { + final Path source = folder.newFile().toPath(); + final Path target = folder.newFolder().toPath(); + final String config = Resources.toString(Resources.getResource("ibft_genesis.json"), UTF_8); + + try { + Files.write( + source, + Resources.toByteArray(Resources.getResource("ibft.blocks")), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (final IOException ex) { + throw new IllegalStateException(ex); + } + + final PantheonController controller = + PantheonController.fromConfig( + SynchronizerConfiguration.builder().build(), + config, + target, + false, + 10, + new MiningParametersTestBuilder().enabled(false).build(), + KeyPair.generate()); + final BlockImporter.ImportResult result = blockImporter.importBlockchain(source, controller); + + assertThat(result.count).isEqualTo(959); + } +} diff --git a/pantheon/src/test/java/net/consensys/pantheon/util/BlockchainImporterTest.java b/pantheon/src/test/java/net/consensys/pantheon/util/BlockchainImporterTest.java new file mode 100755 index 00000000000..c7b69e61dd4 --- /dev/null +++ b/pantheon/src/test/java/net/consensys/pantheon/util/BlockchainImporterTest.java @@ -0,0 +1,73 @@ +package net.consensys.pantheon.util; + +import static net.consensys.pantheon.controller.KeyPairUtil.loadKeyPair; +import static org.assertj.core.api.Assertions.assertThat; + +import net.consensys.pantheon.controller.MainnetPantheonController; +import net.consensys.pantheon.controller.PantheonController; +import net.consensys.pantheon.crypto.SECP256K1.KeyPair; +import net.consensys.pantheon.ethereum.blockcreation.MiningParameters; +import net.consensys.pantheon.ethereum.chain.GenesisConfig; +import net.consensys.pantheon.ethereum.core.MiningParametersTestBuilder; +import net.consensys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import net.consensys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import net.consensys.pantheon.ethereum.mainnet.ProtocolSchedule; +import net.consensys.pantheon.util.uint.UInt256; + +import java.io.File; +import java.net.URL; +import java.nio.file.Path; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Tests for {@link BlockchainImporter}. */ +public final class BlockchainImporterTest { + + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + BlockchainImporter blockImporter = new BlockchainImporter(); + + @Test + public void blockImport() throws Exception { + final URL importFileURL = + getClass() + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/json-rpc-test.bin"); + assertThat(importFileURL).isNotNull(); + + final Path source = new File(importFileURL.toURI()).toPath(); + final Path target = folder.newFolder().toPath(); + + final URL genesisJsonUrl = + getClass() + .getClassLoader() + .getResource("net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json"); + assertThat(genesisJsonUrl).isNotNull(); + final String genesisJson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + final KeyPair keyPair = loadKeyPair(target); + + final ProtocolSchedule protocolSchedule = MainnetProtocolSchedule.create(); + final MiningParameters miningParams = new MiningParametersTestBuilder().enabled(false).build(); + final GenesisConfig genesisConfig = GenesisConfig.fromJson(genesisJson, protocolSchedule); + final PantheonController ctrl = + MainnetPantheonController.init( + target, + genesisConfig, + SynchronizerConfiguration.builder().build(), + miningParams, + 10, + keyPair); + final BlockchainImporter.ImportResult result = + blockImporter.importBlockchain(source, ctrl, true, 1, 1, false, false, null); + System.out.println(source); + System.out.println(target); + + System.out.println(result); + assertThat(result.count).isEqualTo(33); + assertThat(result.td).isEqualTo(UInt256.of(4357120)); + } +} diff --git a/pantheon/src/test/resources/complete_config.toml b/pantheon/src/test/resources/complete_config.toml new file mode 100755 index 00000000000..1824f6099ff --- /dev/null +++ b/pantheon/src/test/resources/complete_config.toml @@ -0,0 +1,21 @@ +# this is a valid TOML config file + +datadir="~/pantheondata" # Path + +#invalid-option=true + +# network +no-discovery=true # true=no discovery of peers or false=discover peers please +bootnodes=["enode://001@123:4567", "enode://002@123:4567", "enode://003@123:4567"] +p2p-listen="1.2.3.4:1234" # IP:port +max-peers=42 +rpc-listen="5.6.7.8:5678" # IP:port +ws-listen="9.10.11.12:9101" # IP:port + +# chain +genesis="~/genesys.json" # Path +sync-mode="fast"# should be FAST or FULL (or fast or full) +ottoman=false # true means using ottoman testnet if genesys file uses iBFT + +#mining +miner-coinbase="0x0000000000000000000000000000000000000002" \ No newline at end of file diff --git a/pantheon/src/test/resources/ibft.blocks b/pantheon/src/test/resources/ibft.blocks new file mode 100755 index 0000000000000000000000000000000000000000..a0f05ccb81ace52b2723e869b5465c228888aea9 GIT binary patch literal 865389 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

HZiUIhv4DpvV>ibMn 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%0AwXAk3t5kN9is+gv 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ĊJ#djLzJ~K?x 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 literal 0 HcmV?d00001 diff --git a/pantheon/src/test/resources/ibft_genesis.json b/pantheon/src/test/resources/ibft_genesis.json new file mode 100755 index 00000000000..7d0824af26a --- /dev/null +++ b/pantheon/src/test/resources/ibft_genesis.json @@ -0,0 +1,40 @@ +{ + "config": { + "chainId": 2017, + "homesteadBlock": 1, + "eip150Block": 2, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 3, + "eip158Block": 3, + + "ibft": { + "epochLength": 30000, + "blockPeriodSeconds" : 1, + "requestTimeout": 10000 + } + }, + "nonce": "0x0", + "timestamp": "0x5b3c3d18", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f89af85494c332d0db1704d18f89a590e7586811e36d37ce049424defc2d149861d3d245749b81fe0e6b28e04f31943814f17bd4b7ce47ab8146684b3443c0a4b2fc2c942a813d7db3de19b07f92268b6d4125ed295cbe00b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", + "gasLimit": "0x47b760", + "difficulty": "0x1", + "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "24defc2d149861d3d245749b81fe0e6b28e04f31": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "2a813d7db3de19b07f92268b6d4125ed295cbe00": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "3814f17bd4b7ce47ab8146684b3443c0a4b2fc2c": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + }, + "c332d0db1704d18f89a590e7586811e36d37ce04": { + "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/pantheon/src/test/resources/log4j2.xml b/pantheon/src/test/resources/log4j2.xml new file mode 100755 index 00000000000..82f9c0b4cb2 --- /dev/null +++ b/pantheon/src/test/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + INFO + + + + + + + + + + + + diff --git a/pantheon/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/json-rpc-test.bin b/pantheon/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/json-rpc-test.bin new file mode 100755 index 0000000000000000000000000000000000000000..02bae0b8da4f83d258ae550f55ec3b1481a5a4f1 GIT binary patch literal 36860 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 z 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 F{|{{?!pHys literal 0 HcmV?d00001 diff --git a/pantheon/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json b/pantheon/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json new file mode 100755 index 00000000000..0a51e83fa5f --- /dev/null +++ b/pantheon/src/test/resources/net/consensys/pantheon/ethereum/jsonrpc/jsonRpcTestGenesis.json @@ -0,0 +1,20 @@ +{ + "config": { + "chainId": 1, + "ethash": { + } + }, + "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase" : "0x8888f1f195afa192cfee860698584c030f4c9db1", + "difficulty" : "0x020000", + "gasLimit" : "0x2fefd8", + "timestamp" : "0x54c98c81", + "extraData" : "0x42", + "mixHash" : "0x2c85bcbce56429100b2108254bb56906257582aeafcbd682bc9af67a9f5aee46", + "nonce" : "0x78cc16f7b4f65485", + "alloc" : { + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance" : "0x09184e72a000" + } + } +} \ No newline at end of file diff --git a/pantheon/src/test/resources/partial_config.toml b/pantheon/src/test/resources/partial_config.toml new file mode 100755 index 00000000000..3de87f7931a --- /dev/null +++ b/pantheon/src/test/resources/partial_config.toml @@ -0,0 +1,4 @@ +# this is a valid prtial TOML config file + +#mining +miner-coinbase="0x0000000000000000000000000000000000000002" \ No newline at end of file diff --git a/quickstart/README.md b/quickstart/README.md new file mode 100755 index 00000000000..47b3f267cdd --- /dev/null +++ b/quickstart/README.md @@ -0,0 +1 @@ +For complete and up-to-date documentation, please see [Docker quick-start, on the Pantheon wiki](https://github.com/ConsenSys/pantheon/wiki/Docker-Quickstart). diff --git a/quickstart/docker-compose.yml b/quickstart/docker-compose.yml new file mode 100755 index 00000000000..0054360d286 --- /dev/null +++ b/quickstart/docker-compose.yml @@ -0,0 +1,53 @@ +version: '2.2' +services: + bootnode: + build: + context: ../ + dockerfile: quickstart/pantheon/Dockerfile + image: quickstart/pantheon:latest + entrypoint: /opt/pantheon/bootnode_start.sh --dev-mode + volumes: + - public-keys:/opt/pantheon/public-keys + minernode: + image: quickstart/pantheon:latest +# The address 0xfe3b557e8fb62b89f4916b721be55ceb828dbd73 is one of the addresses in the +# dev mode genesys file + command: ["--dev-mode", + "--miner-enabled", + "--miner-coinbase=0xfe3b557e8fb62b89f4916b721be55ceb828dbd73"] + volumes: + - public-keys:/opt/pantheon/public-keys + depends_on: + - bootnode + node: + image: quickstart/pantheon:latest + command: ["--dev-mode"] + volumes: + - public-keys:/opt/pantheon/public-keys + depends_on: + - bootnode + rpcnode: + image: quickstart/pantheon:latest + command: ["--dev-mode", + "--rpc-enabled", + "--rpc-listen=0.0.0.0:8545", + "--ws-enabled", + "--ws-listen=0.0.0.0:8546", + "--rpc-cors-origins=*"] + volumes: + - public-keys:/opt/pantheon/public-keys + depends_on: + - bootnode + ports: + - "8545" + - "8546" + explorer: + build: + context: explorer + image: ethereum-explorer:latest + depends_on: + - rpcnode + ports: + - "3000" +volumes: + public-keys: \ No newline at end of file diff --git a/quickstart/explorer/App.js.patch b/quickstart/explorer/App.js.patch new file mode 100755 index 00000000000..59a3d0e16a2 --- /dev/null +++ b/quickstart/explorer/App.js.patch @@ -0,0 +1,35 @@ +diff --git a/src/components/App.js b/src/components/App.js +index c39c931..16c3bb5 100644 +--- a/src/components/App.js ++++ b/src/components/App.js +@@ -4,7 +4,6 @@ import { Link } from 'react-router-dom'; + import { getIsUsingFallback } from '@/adapters/web3/init'; + + import Error from './Error'; +-import Logo from '@/svgs/Logo'; + import Search from './Search'; + import Switcher from './ReactRouter/Switcher'; + +@@ -16,7 +15,7 @@ const App = () => ( +

Block Explorer

+ +

Connected to {getIsUsingFallback() +- ? 'MIX (rpc2.mix-blockchain.org)' ++ ? 'Pantheon Ethereum private Docker network (rpcHostPlaceHolder)' + : 'your Web3 browser extension (e.g. Metamask)'} +

+ +@@ -24,13 +23,6 @@ const App = () => ( + + + +-
+-
+-

powered by

+- +-

MIX Blockchain

+-
+-
+ + ); + diff --git a/quickstart/explorer/Dockerfile b/quickstart/explorer/Dockerfile new file mode 100755 index 00000000000..ac29fc918b8 --- /dev/null +++ b/quickstart/explorer/Dockerfile @@ -0,0 +1,20 @@ +FROM node:8-alpine + +RUN apk update && apk upgrade && \ + apk add --no-cache git sed python make g++ + +RUN git clone https://github.com/mix-blockchain/block-explorer.git explorer + +WORKDIR explorer/ + +RUN npm install + +COPY *.patch ./ +COPY favicon.png public/favicon.png + +RUN git apply *.patch + +CMD npm start + +# List Exposed Ports +EXPOSE 3000 diff --git a/quickstart/explorer/favicon.png b/quickstart/explorer/favicon.png new file mode 100755 index 0000000000000000000000000000000000000000..07a0edb8573ac460b3794efe21355fe790f3e30d GIT binary patch literal 14901 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 literal 0 HcmV?d00001 diff --git a/quickstart/explorer/index.html.patch b/quickstart/explorer/index.html.patch new file mode 100755 index 00000000000..5598aa41b75 --- /dev/null +++ b/quickstart/explorer/index.html.patch @@ -0,0 +1,13 @@ +diff --git a/public/index.html b/public/index.html +index e1df6b2..a403d3e 100644 +--- a/public/index.html ++++ b/public/index.html +@@ -4,7 +4,7 @@ + + + +- MIX Block Explorer ++ Block Explorer + + +

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 z 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%0AT3O^V35M(KqV!R56mY3 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�Abx=1D}0U0tWSh6~R|BD1fk&e}bEg#SN= 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