diff --git a/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java b/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java index 48b5c856995..b5014618bae 100644 --- a/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java +++ b/rskj-core/src/main/java/co/rsk/cli/tools/ExecuteBlocks.java @@ -64,7 +64,7 @@ private void executeBlocks(String[] args, BlockExecutor blockExecutor, BlockStor Block block = blockStore.getChainBlockByNumber(n); Block parent = blockStore.getBlockByHash(block.getParentHash().getBytes()); - BlockResult blockResult = blockExecutor.execute(block, parent.getHeader(), false, false, true); + BlockResult blockResult = blockExecutor.execute(null, 0, block, parent.getHeader(), false, false, true); Keccak256 stateRootHash = stateRootHandler.translate(block.getHeader()); if (!Arrays.equals(blockResult.getFinalState().getHash().getBytes(), stateRootHash.getBytes())) { diff --git a/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java b/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java index ee5e933898b..85ace573f5d 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/BlockChainImpl.java @@ -249,7 +249,7 @@ private ImportResult internalTryToConnect(Block block) { long saveTime = System.nanoTime(); logger.trace("execute start"); - result = blockExecutor.execute(block, parent.getHeader(), false, noValidation, true); + result = blockExecutor.execute(null, 0, block, parent.getHeader(), false, noValidation, true); logger.trace("execute done"); diff --git a/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java b/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java index 6228b70b746..6fb07560714 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/BlockExecutor.java @@ -32,6 +32,7 @@ import org.ethereum.config.blockchain.upgrades.ConsensusRule; import org.ethereum.core.*; import org.ethereum.vm.DataWord; +import org.ethereum.vm.GasCost; import org.ethereum.vm.PrecompiledContracts; import org.ethereum.vm.program.ProgramResult; import org.ethereum.vm.trace.ProgramTraceProcessor; @@ -119,20 +120,20 @@ public static byte[] calculateLogsBloom(List receipts) { * @param parent The parent of the block. */ public BlockResult executeAndFill(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, true, false, false); + BlockResult result = executeForMining(block, parent, true, false, false); fill(block, result); return result; } @VisibleForTesting public void executeAndFillAll(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, false, true, false); + BlockResult result = executeForMining(block, parent, false, true, false); fill(block, result); } @VisibleForTesting public void executeAndFillReal(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, false, false, false); + BlockResult result = executeForMining(block, parent, false, false, false); if (result != BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT) { fill(block, result); } @@ -149,6 +150,7 @@ private void fill(Block block, BlockResult result) { header.setGasUsed(result.getGasUsed()); header.setPaidFees(result.getPaidFees()); header.setLogsBloom(calculateLogsBloom(result.getTransactionReceipts())); + header.setTxExecutionListsEdges(result.getTxEdges()); block.flushRLP(); profiler.stop(metric); @@ -163,7 +165,7 @@ private void fill(Block block, BlockResult result) { */ @VisibleForTesting public boolean executeAndValidate(Block block, BlockHeader parent) { - BlockResult result = execute(block, parent, false, false, false); + BlockResult result = execute(null, 0, block, parent, false, false, false); return this.validate(block, result); } @@ -258,13 +260,12 @@ private boolean validateLogsBloom(BlockHeader header, BlockResult result) { return Arrays.equals(calculateLogsBloom(result.getTransactionReceipts()), header.getLogsBloom()); } - @VisibleForTesting - BlockResult execute(Block block, BlockHeader parent, boolean discardInvalidTxs) { - return execute(block, parent, discardInvalidTxs, false, true); - } - - public BlockResult execute(Block block, BlockHeader parent, boolean discardInvalidTxs, boolean ignoreReadyToExecute, boolean saveState) { - return executeInternal(null, 0, block, parent, discardInvalidTxs, ignoreReadyToExecute, saveState); + public BlockResult executeForMining(Block block, BlockHeader parent, boolean discardInvalidTxs, boolean ignoreReadyToExecute, boolean saveState) { + if (block.getHeader().getTxExecutionListsEdges() != null) { + return executeForMiningAfterRSKIP144(block, parent, discardInvalidTxs, ignoreReadyToExecute, saveState); + } else { + return executePreviousRSKIP144(null, 0, block, parent, discardInvalidTxs, ignoreReadyToExecute, saveState); + } } /** @@ -277,12 +278,30 @@ public void traceBlock( BlockHeader parent, boolean discardInvalidTxs, boolean ignoreReadyToExecute) { - executeInternal( + execute( Objects.requireNonNull(programTraceProcessor), vmTraceOptions, block, parent, discardInvalidTxs, ignoreReadyToExecute, false ); } - private BlockResult executeInternal( + public BlockResult execute( + @Nullable ProgramTraceProcessor programTraceProcessor, + int vmTraceOptions, + Block block, + BlockHeader parent, + boolean discardInvalidTxs, + boolean acceptInvalidTransactions, + boolean saveState + ) { + boolean rskip144Active = activationConfig.isActive(ConsensusRule.RSKIP144, block.getHeader().getNumber()); + + if (rskip144Active && block.getHeader().getTxExecutionListsEdges() != null) { + return executeParallel(programTraceProcessor, vmTraceOptions, block, parent, discardInvalidTxs, acceptInvalidTransactions, saveState); + } else { + return executePreviousRSKIP144(programTraceProcessor, vmTraceOptions, block, parent, discardInvalidTxs, acceptInvalidTransactions, saveState); + } + } + + private BlockResult executePreviousRSKIP144( @Nullable ProgramTraceProcessor programTraceProcessor, int vmTraceOptions, Block block, @@ -290,12 +309,126 @@ private BlockResult executeInternal( boolean discardInvalidTxs, boolean acceptInvalidTransactions, boolean saveState) { + boolean vmTrace = programTraceProcessor != null; + logger.trace("Start executeInternal."); + logger.trace("applyBlock: block: [{}] tx.list: [{}]", block.getNumber(), block.getTransactionsList().size()); - if (block.getHeader().getTxExecutionListsEdges() != null) { - return executeParallel(programTraceProcessor, vmTraceOptions, block, parent, discardInvalidTxs, acceptInvalidTransactions, saveState); - } else { - return executeSequential(programTraceProcessor, vmTraceOptions, block, parent, discardInvalidTxs, acceptInvalidTransactions, saveState); + // Forks the repo, does not change "repository". It will have a completely different + // image of the repo, where the middle caches are immediately ignored. + // In fact, while cloning everything, it asserts that no cache elements remains. + // (see assertNoCache()) + // Which means that you must commit changes and save them to be able to recover + // in the next block processed. + // Note that creating a snapshot is important when the block is executed twice + // (e.g. once while building the block in tests/mining, and the other when trying + // to conect the block). This is because the first execution will change the state + // of the repository to the state post execution, so it's necessary to get it to + // the state prior execution again. + Metric metric = profiler.start(Profiler.PROFILING_TYPE.BLOCK_EXECUTE); + + Repository track = repositoryLocator.startTrackingAt(parent); + + maintainPrecompiledContractStorageRoots(track, activationConfig.forBlock(block.getNumber())); + + int i = 1; + long totalGasUsed = 0; + Coin totalPaidFees = Coin.ZERO; + List receipts = new ArrayList<>(); + List executedTransactions = new ArrayList<>(); + Set deletedAccounts = new HashSet<>(); + LongAccumulator remascFees = new LongAccumulator(Long::sum, 0); + + int txindex = 0; + + for (Transaction tx : block.getTransactionsList()) { + logger.trace("apply block: [{}] tx: [{}] ", block.getNumber(), i); + + TransactionExecutor txExecutor = transactionExecutorFactory.newInstance( + tx, + txindex++, + block.getCoinbase(), + track, + block, + totalGasUsed, + vmTrace, + vmTraceOptions, + deletedAccounts, + remascFees); + boolean transactionExecuted = txExecutor.executeTransaction(); + + if (!acceptInvalidTransactions && !transactionExecuted) { + if (discardInvalidTxs) { + logger.warn("block: [{}] discarded tx: [{}]", block.getNumber(), tx.getHash()); + continue; + } else { + logger.warn("block: [{}] execution interrupted because of invalid tx: [{}]", + block.getNumber(), tx.getHash()); + profiler.stop(metric); + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + } + } + + executedTransactions.add(tx); + + if (this.registerProgramResults) { + this.transactionResults.put(tx.getHash(), txExecutor.getResult()); + } + + if (vmTrace) { + txExecutor.extractTrace(programTraceProcessor); + } + + logger.trace("tx executed"); + + // No need to commit the changes here. track.commit(); + + logger.trace("track commit"); + + long gasUsed = txExecutor.getGasUsed(); + totalGasUsed += gasUsed; + Coin paidFees = txExecutor.getPaidFees(); + if (paidFees != null) { + totalPaidFees = totalPaidFees.add(paidFees); + } + + deletedAccounts.addAll(txExecutor.getResult().getDeleteAccounts()); + + TransactionReceipt receipt = new TransactionReceipt(); + receipt.setGasUsed(gasUsed); + receipt.setCumulativeGas(totalGasUsed); + + receipt.setTxStatus(txExecutor.getReceipt().isSuccessful()); + receipt.setTransaction(tx); + receipt.setLogInfoList(txExecutor.getVMLogs()); + receipt.setStatus(txExecutor.getReceipt().getStatus()); + + logger.trace("block: [{}] executed tx: [{}]", block.getNumber(), tx.getHash()); + + logger.trace("tx[{}].receipt", i); + + i++; + + receipts.add(receipt); + + logger.trace("tx done"); } + + addFeesToRemasc(remascFees, track); + saveOrCommitTrackState(saveState, track); + + BlockResult result = new BlockResult( + block, + executedTransactions, + receipts, + null, + totalGasUsed, + totalPaidFees, + vmTrace ? null : track.getTrie() + + ); + profiler.stop(metric); + logger.trace("End executeInternal."); + return result; } private BlockResult executeParallel( @@ -420,8 +553,9 @@ private BlockResult executeParallel( saveOrCommitTrackState(saveState, track); BlockResult result = new BlockResult( block, - new LinkedList(executedTransactions.values()), - new LinkedList(receipts.values()), + new LinkedList<>(executedTransactions.values()), + new LinkedList<>(receipts.values()), + new short[0], totalGasUsed.longValue(), Coin.valueOf(totalPaidFees.longValue()), vmTrace ? null : track.getTrie() @@ -438,17 +572,16 @@ private void addFeesToRemasc(LongAccumulator remascFees, Repository track) { } } - private BlockResult executeSequential( - @Nullable ProgramTraceProcessor programTraceProcessor, - int vmTraceOptions, + private BlockResult executeForMiningAfterRSKIP144( Block block, BlockHeader parent, boolean discardInvalidTxs, boolean acceptInvalidTransactions, boolean saveState) { - boolean vmTrace = programTraceProcessor != null; + logger.trace("Start executeInternal."); - logger.trace("applyBlock: block: [{}] tx.list: [{}]", block.getNumber(), block.getTransactionsList().size()); + List transactionsList = block.getTransactionsList(); + logger.trace("applyBlock: block: [{}] tx.list: [{}]", block.getNumber(), transactionsList.size()); // Forks the repo, does not change "repository". It will have a completely different // image of the repo, where the middle caches are immediately ignored. @@ -463,21 +596,25 @@ private BlockResult executeSequential( // the state prior execution again. Metric metric = profiler.start(Profiler.PROFILING_TYPE.BLOCK_EXECUTE); - Repository track = repositoryLocator.startTrackingAt(parent); + ReadWrittenKeysTracker readWrittenKeysTracker = new ReadWrittenKeysTracker(); + Repository track = repositoryLocator.startTrackingAt(parent, readWrittenKeysTracker); maintainPrecompiledContractStorageRoots(track, activationConfig.forBlock(block.getNumber())); int i = 1; - long totalGasUsed = 0; + long gasUsedInBlock = 0; Coin totalPaidFees = Coin.ZERO; - List receipts = new ArrayList<>(); - List executedTransactions = new ArrayList<>(); + Map receiptsByTx = new HashMap<>(); Set deletedAccounts = new HashSet<>(); LongAccumulator remascFees = new LongAccumulator(Long::sum, 0); + short buckets = 2; + + //TODO(Juli): Is there a better way to calculate the bucket gas limit? + ParallelizeTransactionHandler parallelizeTransactionHandler = new ParallelizeTransactionHandler(buckets, GasCost.toGas(block.getGasLimit())); int txindex = 0; - for (Transaction tx : block.getTransactionsList()) { + for (Transaction tx : transactionsList) { logger.trace("apply block: [{}] tx: [{}] ", block.getNumber(), i); TransactionExecutor txExecutor = transactionExecutorFactory.newInstance( @@ -486,11 +623,11 @@ private BlockResult executeSequential( block.getCoinbase(), track, block, - totalGasUsed, - vmTrace, - vmTraceOptions, + parallelizeTransactionHandler.getGasUsedInSequential(), + false, + 0, deletedAccounts, - remascFees); + remascFees); //TODO(Juli): Check how to differ this behavior between RSKIPs boolean transactionExecuted = txExecutor.executeTransaction(); if (!acceptInvalidTransactions && !transactionExecuted) { @@ -505,14 +642,29 @@ private BlockResult executeSequential( } } - executedTransactions.add(tx); + Optional bucketGasAccumulated; + if (tx.isRemascTransaction(txindex, transactionsList.size())) { + bucketGasAccumulated = parallelizeTransactionHandler.addRemascTransaction(tx, txExecutor.getGasUsed()); + } else { + bucketGasAccumulated = parallelizeTransactionHandler.addTransaction(tx, readWrittenKeysTracker.getTemporalReadKeys(), readWrittenKeysTracker.getTemporalWrittenKeys(), txExecutor.getGasUsed()); + } - if (this.registerProgramResults) { - this.transactionResults.put(tx.getHash(), txExecutor.getResult()); + if (!acceptInvalidTransactions && !bucketGasAccumulated.isPresent()) { + if (discardInvalidTxs) { + logger.warn("block: [{}] discarded tx: [{}]", block.getNumber(), tx.getHash()); + continue; + } else { + logger.warn("block: [{}] execution interrupted because of invalid tx: [{}]", + block.getNumber(), tx.getHash()); + profiler.stop(metric); + return BlockResult.INTERRUPTED_EXECUTION_BLOCK_RESULT; + } } - if (vmTrace) { - txExecutor.extractTrace(programTraceProcessor); + readWrittenKeysTracker.clear(); + + if (this.registerProgramResults) { + this.transactionResults.put(tx.getHash(), txExecutor.getResult()); } logger.trace("tx executed"); @@ -522,7 +674,8 @@ private BlockResult executeSequential( logger.trace("track commit"); long gasUsed = txExecutor.getGasUsed(); - totalGasUsed += gasUsed; + gasUsedInBlock += gasUsed; + Coin paidFees = txExecutor.getPaidFees(); if (paidFees != null) { totalPaidFees = totalPaidFees.add(paidFees); @@ -532,7 +685,13 @@ private BlockResult executeSequential( TransactionReceipt receipt = new TransactionReceipt(); receipt.setGasUsed(gasUsed); - receipt.setCumulativeGas(totalGasUsed); + + if (bucketGasAccumulated.isPresent()) { + receipt.setCumulativeGas(bucketGasAccumulated.get()); + } else { + //This line is used for testing only when acceptInvalidTransactions is set. + receipt.setCumulativeGas(parallelizeTransactionHandler.getGasUsedIn(buckets)); + } receipt.setTxStatus(txExecutor.getReceipt().isSuccessful()); receipt.setTransaction(tx); @@ -545,20 +704,29 @@ private BlockResult executeSequential( i++; - receipts.add(receipt); + receiptsByTx.put(tx, receipt); logger.trace("tx done"); } addFeesToRemasc(remascFees, track); saveOrCommitTrackState(saveState, track); + + List executedTransactions = parallelizeTransactionHandler.getTransactionsInOrder(); + short[] bucketOrder = parallelizeTransactionHandler.getTransactionsPerBucketInOrder(); + List receipts = new ArrayList<>(); + + for (Transaction tx : executedTransactions) { + receipts.add(receiptsByTx.get(tx)); + } BlockResult result = new BlockResult( block, executedTransactions, receipts, - totalGasUsed, + bucketOrder, + gasUsedInBlock, totalPaidFees, - vmTrace ? null : track.getTrie() + track.getTrie() ); profiler.stop(metric); logger.trace("End executeInternal."); diff --git a/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java b/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java index bb52ec2e364..fd656b6b73b 100644 --- a/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java +++ b/rskj-core/src/main/java/co/rsk/core/bc/BlockResult.java @@ -35,7 +35,7 @@ public class BlockResult { null, Collections.emptyList(), Collections.emptyList(), - 0, + new short[0], 0, Coin.ZERO, null ); @@ -49,11 +49,13 @@ public class BlockResult { // It is for optimizing switching between states. Instead of using the "stateRoot" field, // which requires regenerating the trie, using the finalState field does not. private final Trie finalState; + private final short[] txEdges; public BlockResult( Block block, List executedTransactions, List transactionReceipts, + short[] txEdges, long gasUsed, Coin paidFees, Trie finalState) { @@ -63,12 +65,15 @@ public BlockResult( this.gasUsed = gasUsed; this.paidFees = paidFees; this.finalState = finalState; + this.txEdges = txEdges; } public Block getBlock() { return block; } + public short[] getTxEdges() { return txEdges; } + public List getExecutedTransactions() { return executedTransactions; } public List getTransactionReceipts() { diff --git a/rskj-core/src/main/java/co/rsk/core/bc/IReadWrittenKeysTracker.java b/rskj-core/src/main/java/co/rsk/core/bc/IReadWrittenKeysTracker.java new file mode 100644 index 00000000000..3fb6f21d11d --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/core/bc/IReadWrittenKeysTracker.java @@ -0,0 +1,34 @@ +/* + * This file is part of RskJ + * Copyright (C) 2019 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import org.ethereum.db.ByteArrayWrapper; +import java.util.Set; + +public interface IReadWrittenKeysTracker { + Set getTemporalReadKeys(); + + Set getTemporalWrittenKeys(); + + void addNewReadKey(ByteArrayWrapper key); + + void addNewWrittenKey(ByteArrayWrapper key); + + void clear(); +} diff --git a/rskj-core/src/main/java/co/rsk/core/bc/ParallelizeTransactionHandler.java b/rskj-core/src/main/java/co/rsk/core/bc/ParallelizeTransactionHandler.java new file mode 100644 index 00000000000..d2526872bdc --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/core/bc/ParallelizeTransactionHandler.java @@ -0,0 +1,248 @@ +/* + * This file is part of RskJ + * Copyright (C) 2017 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import co.rsk.core.RskAddress; +import org.ethereum.core.Transaction; +import org.ethereum.db.ByteArrayWrapper; +import org.ethereum.vm.GasCost; + +import java.util.*; + +public class ParallelizeTransactionHandler { + private final HashMap bucketByWrittenKey; + private final HashMap> bucketByReadKey; + private final Map bucketBySender; + private final ArrayList buckets; + + public ParallelizeTransactionHandler(short buckets, long bucketGasLimit) { + this.bucketBySender = new HashMap<>(); + this.bucketByWrittenKey = new HashMap<>(); + this.bucketByReadKey = new HashMap<>(); + this.buckets = new ArrayList<>(); + for (short i = 0; i < buckets; i++){ + this.buckets.add(new TransactionBucket(i, bucketGasLimit, false)); + } + this.buckets.add(new TransactionBucket(buckets, bucketGasLimit, true)); + } + + public Optional addTransaction(Transaction tx, Set newReadKeys, Set newWrittenKeys, long gasUsedByTx) { + TransactionBucket bucketCandidate = getBucketCandidates(tx, newReadKeys, newWrittenKeys); + + if (!bucketHasAvailableGas(tx, bucketCandidate)) { + if (bucketCandidate.isSequential()) { + return Optional.empty(); + } + bucketCandidate = getSequentialBucket(); + + if (!bucketHasAvailableGas(tx, bucketCandidate)) { + return Optional.empty(); + } + } + + bucketCandidate.addTransaction(tx, gasUsedByTx); + addNewKeysToMaps(tx.getSender(), bucketCandidate, newReadKeys, newWrittenKeys); + return Optional.of(bucketCandidate.getGasUsed()); + } + + private boolean bucketHasAvailableGas(Transaction tx, TransactionBucket bucketCandidate) { + return bucketCandidate.hasGasAvailable(GasCost.toGas(tx.getGasLimit())); + } + + public Optional addRemascTransaction(Transaction tx, long gasUsedByTx) { + TransactionBucket sequentialBucket = getSequentialBucket(); + sequentialBucket.addTransaction(tx, gasUsedByTx); + return Optional.of(sequentialBucket.getGasUsed()); + } + + public long getGasUsedIn(Short bucketId) { + + if (bucketId < 0 || bucketId >= buckets.size()) { + throw new NoSuchElementException(); + } + + return this.buckets.get(bucketId).getGasUsed(); + } + + public List getTransactionsInOrder() { + List txs = new ArrayList<>(); + for (TransactionBucket bucket: this.buckets) { + txs.addAll(bucket.getTransactions()); + } + return txs; + } + + public short[] getTransactionsPerBucketInOrder() { + List bucketSizes = new ArrayList<>(); + short bucketEdges = 0; + + for (TransactionBucket bucket: this.buckets) { + if (bucket.getTransactions().isEmpty() || bucket.isSequential()) { + continue; + } + bucketEdges += bucket.getTransactions().size(); + bucketSizes.add(bucketEdges); + } + + short[] bucketOrder = new short[bucketSizes.size()]; + int i = 0; + for (Short size: bucketSizes) { + bucketOrder[i] = size; + i++; + } + + return bucketOrder; + } + + private void addNewKeysToMaps(RskAddress sender, TransactionBucket bucket, Set newReadKeys, Set newWrittenKeys) { + for (ByteArrayWrapper key : newReadKeys) { + Set bucketsAlreadyRead = bucketByReadKey.getOrDefault(key, new HashSet<>()); + bucketsAlreadyRead.add(bucket); + bucketByReadKey.put(key, bucketsAlreadyRead); + } + + if (bucket.isSequential()) { + bucketBySender.put(sender, bucket); + return; + } else { + bucketBySender.putIfAbsent(sender, bucket); + } + + for (ByteArrayWrapper key: newWrittenKeys) { + bucketByWrittenKey.putIfAbsent(key, bucket); + } + } + + private Optional getBucketBySender(Transaction tx) { + return Optional.ofNullable(bucketBySender.get(tx.getSender())); + } + + private Optional getAvailableBucketWithLessUsedGas(long txGasLimit) { + long gasUsed = Long.MAX_VALUE; + Optional bucketCandidate = Optional.empty(); + + for (TransactionBucket bucket : buckets) { + if (!bucket.isSequential() && bucket.hasGasAvailable(txGasLimit) && bucket.getGasUsed() < gasUsed) { + bucketCandidate = Optional.of(bucket); + gasUsed = bucket.getGasUsed(); + } + } + + return bucketCandidate; + } + + + private TransactionBucket getBucketCandidates(Transaction tx, Set newReadKeys, Set newWrittenKeys) { + Optional bucketCandidate = getBucketBySender(tx); + + if (bucketCandidate.isPresent() && bucketCandidate.get().isSequential()) { + return getSequentialBucket(); + } + + // read - written + for (ByteArrayWrapper newReadKey : newReadKeys) { + if (bucketByWrittenKey.containsKey(newReadKey)) { + TransactionBucket bucket = bucketByWrittenKey.get(newReadKey); + + if (bucketCandidate.isPresent() && !bucketCandidate.get().equals(bucket)) { + return getSequentialBucket(); + } else if (!bucketCandidate.isPresent()) { + bucketCandidate = Optional.of(bucket); + } + } + } + + for (ByteArrayWrapper newWrittenKey : newWrittenKeys) { + // written - written, + if (bucketByWrittenKey.containsKey(newWrittenKey)) { + TransactionBucket bucket = bucketByWrittenKey.get(newWrittenKey); + + if (bucketCandidate.isPresent() && !bucketCandidate.get().equals(bucket)) { + return getSequentialBucket(); + } else { + bucketCandidate = Optional.of(bucket); + } + } + // read - written + if (bucketByReadKey.containsKey(newWrittenKey)) { + Set readBuckets = bucketByReadKey.get(newWrittenKey); + + if (readBuckets.size() > 1) { + return getSequentialBucket(); + } + + if (bucketCandidate.isPresent() && !readBuckets.contains(bucketCandidate.get())) { + return getSequentialBucket(); + } else { + bucketCandidate = Optional.of(readBuckets.iterator().next()); + } + } + } + + return bucketCandidate.orElseGet(() -> getAvailableBucketWithLessUsedGas(GasCost.toGas(tx.getGasLimit())).orElseGet(this::getSequentialBucket)); + } + + private TransactionBucket getSequentialBucket() { + return this.buckets.get(this.buckets.size()-1); + } + + public long getGasUsedInSequential() { + return getSequentialBucket().getGasUsed(); + } + + private static class TransactionBucket { + + final Short id; + final long gasLimit; + final boolean isSequential; + final List transactions; + long gasUsedInBucket; + + public TransactionBucket(Short id, long bucketGasLimit, boolean isSequential) { + this.id = id; + this.gasLimit = bucketGasLimit; + this.isSequential = isSequential; + this.transactions = new ArrayList<>(); + this.gasUsedInBucket = 0; + } + + private void addTransaction(Transaction tx, long gasUsedByTx) { + transactions.add(tx); + gasUsedInBucket = gasUsedInBucket + gasUsedByTx; + } + + private boolean hasGasAvailable(long txGasLimit) { + //TODO(JULI): Re-check a thousand of times this line. + long cumulativeGas = GasCost.add(gasUsedInBucket, txGasLimit); + return cumulativeGas <= gasLimit; + } + + public long getGasUsed() { + return gasUsedInBucket; + } + + public List getTransactions() { + return this.transactions; + } + + public boolean isSequential() { + return isSequential; + } + } +} diff --git a/rskj-core/src/main/java/co/rsk/core/bc/ReadWrittenKeysTracker.java b/rskj-core/src/main/java/co/rsk/core/bc/ReadWrittenKeysTracker.java new file mode 100644 index 00000000000..46e1b877816 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/core/bc/ReadWrittenKeysTracker.java @@ -0,0 +1,64 @@ +/* + * This file is part of RskJ + * Copyright (C) 2017 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import org.ethereum.db.ByteArrayWrapper; + +import java.util.HashSet; +import java.util.Set; + +//TODO(JULI): +// * Next step should be to check whether a key is written in the cache but also deleted in the same transaction. This key shouldn't be considered as a written key. + +public class ReadWrittenKeysTracker implements IReadWrittenKeysTracker { + private Set temporalReadKeys; + private Set temporalWrittenKeys; + + public ReadWrittenKeysTracker() { + this.temporalReadKeys = new HashSet<>(); + this.temporalWrittenKeys = new HashSet<>(); + } + + @Override + public Set getTemporalReadKeys(){ + return this.temporalReadKeys; + } + + @Override + public Set getTemporalWrittenKeys(){ + return this.temporalWrittenKeys; + } + + @Override + public void addNewReadKey(ByteArrayWrapper key) { + temporalReadKeys.add(key); + } + + @Override + public void addNewWrittenKey(ByteArrayWrapper key) { + temporalWrittenKeys.add(key); + } + + @Override + public void clear() { + this.temporalReadKeys = new HashSet<>(); + this.temporalWrittenKeys = new HashSet<>(); + + } +} diff --git a/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java b/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java index 3a3eef66fec..7d328039bc5 100644 --- a/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java +++ b/rskj-core/src/main/java/co/rsk/db/RepositoryLocator.java @@ -18,6 +18,7 @@ package co.rsk.db; +import co.rsk.core.bc.IReadWrittenKeysTracker; import co.rsk.crypto.Keccak256; import co.rsk.trie.MutableTrie; import co.rsk.trie.Trie; @@ -77,6 +78,13 @@ public Repository startTrackingAt(BlockHeader header) { .orElseThrow(() -> trieNotFoundException(header)); } + public Repository startTrackingAt(BlockHeader header, IReadWrittenKeysTracker tracker) { + return mutableTrieSnapshotAt(header) + .map(MutableTrieCache::new) + .map(mutableTrieCache -> new MutableRepository(mutableTrieCache, tracker)) + .orElseThrow(() -> trieNotFoundException(header)); + } + private IllegalArgumentException trieNotFoundException(BlockHeader header) { return new IllegalArgumentException(String.format( "The trie with root %s is missing in this store", header.getHash() diff --git a/rskj-core/src/main/java/org/ethereum/core/BlockHeader.java b/rskj-core/src/main/java/org/ethereum/core/BlockHeader.java index 2886c666894..aeb4b2657c8 100644 --- a/rskj-core/src/main/java/org/ethereum/core/BlockHeader.java +++ b/rskj-core/src/main/java/org/ethereum/core/BlockHeader.java @@ -627,4 +627,8 @@ public byte[] getUmmRoot() { } public short[] getTxExecutionListsEdges() { return this.txExecutionListsEdges; } + + public void setTxExecutionListsEdges(short[] txEdges) { + this.txExecutionListsEdges = txEdges; + } } diff --git a/rskj-core/src/main/java/org/ethereum/db/DummyReadWrittenKeysTracker.java b/rskj-core/src/main/java/org/ethereum/db/DummyReadWrittenKeysTracker.java new file mode 100644 index 00000000000..e928147c90e --- /dev/null +++ b/rskj-core/src/main/java/org/ethereum/db/DummyReadWrittenKeysTracker.java @@ -0,0 +1,42 @@ +package org.ethereum.db; + +import co.rsk.core.bc.IReadWrittenKeysTracker; + +import java.util.HashSet; +import java.util.Set; + +public class DummyReadWrittenKeysTracker implements IReadWrittenKeysTracker { + + private final HashSet temporalReadKeys; + private final HashSet temporalWrittenKeys; + + public DummyReadWrittenKeysTracker() { + this.temporalReadKeys = new HashSet<>(); + this.temporalWrittenKeys = new HashSet<>(); + } + + @Override + public Set getTemporalReadKeys() { + return temporalReadKeys; + } + + @Override + public Set getTemporalWrittenKeys() { + return temporalWrittenKeys; + } + + @Override + public void addNewReadKey(ByteArrayWrapper key) { + //Dummy tracker does not store added keys + } + + @Override + public void addNewWrittenKey(ByteArrayWrapper key) { + //Dummy tracker does not store added keys + } + + @Override + public void clear() { + //Dummy tracker does not store added keys + } +} diff --git a/rskj-core/src/main/java/org/ethereum/db/MutableRepository.java b/rskj-core/src/main/java/org/ethereum/db/MutableRepository.java index e56c0df82c7..557740966f4 100644 --- a/rskj-core/src/main/java/org/ethereum/db/MutableRepository.java +++ b/rskj-core/src/main/java/org/ethereum/db/MutableRepository.java @@ -20,6 +20,7 @@ import co.rsk.core.Coin; import co.rsk.core.RskAddress; +import co.rsk.core.bc.IReadWrittenKeysTracker; import co.rsk.core.types.ints.Uint24; import co.rsk.crypto.Keccak256; import co.rsk.db.MutableTrieCache; @@ -46,6 +47,7 @@ public class MutableRepository implements Repository { private final TrieKeyMapper trieKeyMapper; private final MutableTrie mutableTrie; + private final IReadWrittenKeysTracker tracker; public MutableRepository(TrieStore trieStore, Trie trie) { this(new MutableTrieImpl(trieStore, trie)); @@ -54,6 +56,13 @@ public MutableRepository(TrieStore trieStore, Trie trie) { public MutableRepository(MutableTrie mutableTrie) { this.trieKeyMapper = new TrieKeyMapper(); this.mutableTrie = mutableTrie; + this.tracker = new DummyReadWrittenKeysTracker(); + } + + public MutableRepository(MutableTrie mutableTrie, IReadWrittenKeysTracker tracker) { + this.trieKeyMapper = new TrieKeyMapper(); + this.mutableTrie = mutableTrie; + this.tracker = tracker; } @Override @@ -71,13 +80,14 @@ public synchronized AccountState createAccount(RskAddress addr) { @Override public synchronized void setupContract(RskAddress addr) { byte[] prefix = trieKeyMapper.getAccountStoragePrefixKey(addr); - mutableTrie.put(prefix, ONE_BYTE_ARRAY); + internalPut(prefix, ONE_BYTE_ARRAY); } @Override public synchronized boolean isExist(RskAddress addr) { // Here we assume size != 0 means the account exists - return mutableTrie.getValueLength(trieKeyMapper.getAccountKey(addr)).compareTo(Uint24.ZERO) > 0; + byte[] accountKey = trieKeyMapper.getAccountKey(addr); + return internalGetValueLength(accountKey).compareTo(Uint24.ZERO) > 0; } @Override @@ -94,7 +104,9 @@ public synchronized AccountState getAccountState(RskAddress addr) { @Override public synchronized void delete(RskAddress addr) { - mutableTrie.deleteRecursive(trieKeyMapper.getAccountKey(addr)); + byte[] accountKey = trieKeyMapper.getAccountKey(addr); + tracker.addNewWrittenKey(new ByteArrayWrapper(accountKey)); + mutableTrie.deleteRecursive(accountKey); } @Override @@ -137,7 +149,7 @@ public synchronized BigInteger getNonce(RskAddress addr) { @Override public synchronized void saveCode(RskAddress addr, byte[] code) { byte[] key = trieKeyMapper.getCodeKey(addr); - mutableTrie.put(key, code); + internalPut(key, code); if (code != null && code.length != 0 && !isExist(addr)) { createAccount(addr); @@ -152,7 +164,7 @@ public synchronized int getCodeLength(RskAddress addr) { } byte[] key = trieKeyMapper.getCodeKey(addr); - return mutableTrie.getValueLength(key).intValue(); + return internalGetValueLength(key).intValue(); } @Override @@ -167,7 +179,7 @@ public synchronized Keccak256 getCodeHashNonStandard(RskAddress addr) { } byte[] key = trieKeyMapper.getCodeKey(addr); - Optional valueHash = mutableTrie.getValueHash(key); + Optional valueHash = internalGetValueHash(key); //Returning ZERO_HASH is the non standard implementation we had pre RSKIP169 implementation //and thus me must honor it. @@ -187,7 +199,7 @@ public synchronized Keccak256 getCodeHashStandard(RskAddress addr) { byte[] key = trieKeyMapper.getCodeKey(addr); - return mutableTrie.getValueHash(key).orElse(KECCAK_256_OF_EMPTY_ARRAY); + return internalGetValueHash(key).orElse(KECCAK_256_OF_EMPTY_ARRAY); } @Override @@ -202,13 +214,13 @@ public synchronized byte[] getCode(RskAddress addr) { } byte[] key = trieKeyMapper.getCodeKey(addr); - return mutableTrie.get(key); + return internalGet(key); } @Override public boolean isContract(RskAddress addr) { byte[] prefix = trieKeyMapper.getAccountStoragePrefixKey(addr); - return mutableTrie.get(prefix) != null; + return internalGet(prefix) != null; } @Override @@ -233,16 +245,16 @@ public synchronized void addStorageBytes(RskAddress addr, DataWord key, byte[] v // conversion here only applies if this is called directly. If suppose this only occurs in tests, but it can // also occur in precompiled contracts that store data directly using this method. if (value == null || value.length == 0) { - mutableTrie.put(triekey, null); + internalPut(triekey, null); } else { - mutableTrie.put(triekey, value); + internalPut(triekey, value); } } @Override public synchronized DataWord getStorageValue(RskAddress addr, DataWord key) { byte[] triekey = trieKeyMapper.getAccountStorageKey(addr, key); - byte[] value = mutableTrie.get(triekey); + byte[] value = internalGet(triekey); if (value == null) { return null; } @@ -253,7 +265,7 @@ public synchronized DataWord getStorageValue(RskAddress addr, DataWord key) { @Override public synchronized byte[] getStorageBytes(RskAddress addr, DataWord key) { byte[] triekey = trieKeyMapper.getAccountStorageKey(addr, key); - return mutableTrie.get(triekey); + return internalGet(triekey); } @Override @@ -311,7 +323,7 @@ public synchronized Set getAccountsKeys() { // To start tracking, a new repository is created, with a MutableTrieCache in the middle @Override public synchronized Repository startTracking() { - return new MutableRepository(new MutableTrieCache(mutableTrie)); + return new MutableRepository(new MutableTrieCache(mutableTrie), tracker); } @Override @@ -341,7 +353,7 @@ public synchronized byte[] getRoot() { @Override public synchronized void updateAccountState(RskAddress addr, final AccountState accountState) { byte[] accountKey = trieKeyMapper.getAccountKey(addr); - mutableTrie.put(accountKey, accountState.getEncoded()); + internalPut(accountKey, accountState.getEncoded()); } @VisibleForTesting @@ -367,6 +379,27 @@ private synchronized AccountState getAccountStateOrCreateNew(RskAddress addr) { } private byte[] getAccountData(RskAddress addr) { - return mutableTrie.get(trieKeyMapper.getAccountKey(addr)); + byte[] accountKey = trieKeyMapper.getAccountKey(addr); + return internalGet(accountKey); + } + + private void internalPut(byte[] key, byte[] value) { + tracker.addNewWrittenKey(new ByteArrayWrapper(key)); + mutableTrie.put(key, value); + } + + private byte[] internalGet(byte[] key) { + tracker.addNewReadKey(new ByteArrayWrapper(key)); + return mutableTrie.get(key); + } + + private Uint24 internalGetValueLength(byte[] key) { + tracker.addNewReadKey(new ByteArrayWrapper(key)); + return mutableTrie.getValueLength(key); + } + + private Optional internalGetValueHash(byte[] key) { + tracker.addNewReadKey(new ByteArrayWrapper(key)); + return mutableTrie.getValueHash(key); } } diff --git a/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java b/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java index fc37e8f97e5..372f539b81c 100644 --- a/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java +++ b/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java @@ -286,6 +286,7 @@ public Block createChildBlock(Block parent, List txs, List txs = block.getTransactionsList(); + BlockResult blockResult = executor.executeAndFill(block, parent.getHeader()); + + Assertions.assertEquals(txs, blockResult.getExecutedTransactions()); + Assertions.assertEquals(expectedAccumulatedGas, blockResult.getGasUsed()); + Assertions.assertArrayEquals(expectedEdges, blockResult.getTxEdges()); + + List transactionReceipts = blockResult.getTransactionReceipts(); + for (TransactionReceipt receipt: transactionReceipts) { + Assertions.assertEquals(expectedAccumulatedGas, GasCost.toGas(receipt.getCumulativeGas())); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void executeSequentiallyTenIndependentTxsAndThemShouldGoInBothBuckets(boolean activeRskip144) { + if (!activeRskip144) { + return; + } + doReturn(activeRskip144).when(activationConfig).isActive(eq(RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); + long expectedGasUsed = 0L; + long expectedAccumulatedGas = 21000L; + short[] expectedEdges = new short[]{5, 10}; + Block parent = blockchain.getBestBlock(); + Block block = getBlockWithNIndependentTransactions(10, BigInteger.valueOf(expectedAccumulatedGas), false); + List txs = block.getTransactionsList(); + BlockResult blockResult = executor.executeAndFill(block, parent.getHeader()); + + Assertions.assertEquals(txs.size(), blockResult.getExecutedTransactions().size()); + Assertions.assertTrue(txs.containsAll(blockResult.getExecutedTransactions())); + Assertions.assertArrayEquals(expectedEdges, blockResult.getTxEdges()); + Assertions.assertEquals(expectedAccumulatedGas*10, blockResult.getGasUsed()); + + List transactionReceipts = blockResult.getTransactionReceipts(); + long accumulatedGasUsed = 0L; + short i = 0; + short edgeIndex = 0; + for (TransactionReceipt receipt: transactionReceipts) { + if ((edgeIndex < expectedEdges.length) && (i == expectedEdges[edgeIndex])) { + edgeIndex++; + accumulatedGasUsed = expectedGasUsed; + } + + accumulatedGasUsed += expectedAccumulatedGas; + Assertions.assertEquals(accumulatedGasUsed, GasCost.toGas(receipt.getCumulativeGas())); + i++; + } + + Assertions.assertEquals(i, transactionReceipts.size()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void executeBigIndependentTxsSequentiallyTheLastOneShouldGoToSequential(boolean activeRskip144) { + if (!activeRskip144) { + return; + } + doReturn(activeRskip144).when(activationConfig).isActive(eq(RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); + Block parent = blockchain.getBestBlock(); + long blockGasLimit = GasCost.toGas(parent.getGasLimit()); + int gasLimit = 21000; + int transactionNumber = (int) (blockGasLimit /gasLimit); + short[] expectedEdges = new short[]{(short) transactionNumber, (short) (transactionNumber*2)}; + int transactionsInSequential = 1; + + Block block = getBlockWithNIndependentTransactions(transactionNumber*2+transactionsInSequential, BigInteger.valueOf(gasLimit), false); + List transactionsList = block.getTransactionsList(); + BlockResult blockResult = executor.executeAndFill(block, parent.getHeader()); + + Assertions.assertArrayEquals(expectedEdges, blockResult.getTxEdges()); + Assertions.assertEquals(transactionsList.size(), blockResult.getExecutedTransactions().size()); + Assertions.assertTrue(transactionsList.containsAll(blockResult.getExecutedTransactions())); + + List transactionReceipts = blockResult.getTransactionReceipts(); + long accumulatedGasUsed = 0L; + short i = 0; + short edgeIndex = 0; + for (TransactionReceipt receipt: transactionReceipts) { + accumulatedGasUsed += gasLimit; + + if ((edgeIndex < expectedEdges.length) && (i == expectedEdges[edgeIndex])) { + edgeIndex++; + accumulatedGasUsed = gasLimit; + } + Assertions.assertEquals(accumulatedGasUsed, GasCost.toGas(receipt.getCumulativeGas())); + i++; + } + + Assertions.assertEquals(i, transactionReceipts.size()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void executeATxInSequentialAndBlockResultShouldTrackTheGasUsedInTheBlock(boolean activeRskip144) { + if (!activeRskip144) { + return; + } + doReturn(activeRskip144).when(activationConfig).isActive(eq(RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); + Block parent = blockchain.getBestBlock(); + long blockGasLimit = GasCost.toGas(parent.getGasLimit()); + int gasLimit = 21000; + int transactionNumberToFillParallelBucket = (int) (blockGasLimit / gasLimit); + int transactionsInSequential = 1; + int totalTxsNumber = transactionNumberToFillParallelBucket * 2 + transactionsInSequential; + Block block = getBlockWithNIndependentTransactions(totalTxsNumber, BigInteger.valueOf(gasLimit), false); + BlockResult blockResult = executor.executeAndFill(block, parent.getHeader()); + + Assertions.assertEquals(gasLimit*totalTxsNumber, blockResult.getGasUsed()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void withTheBucketsFullTheLastTransactionShouldNotFit(boolean activeRskip144) { + if (!activeRskip144) { + return; + } + doReturn(activeRskip144).when(activationConfig).isActive(eq(RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); + Block parent = blockchain.getBestBlock(); + long blockGasLimit = GasCost.toGas(parent.getGasLimit()); + int gasLimit = 21000; + int transactionNumberToFillParallelBucket = (int) (blockGasLimit / gasLimit); + int totalTxs = (transactionNumberToFillParallelBucket) * 3 + 1; + Block block = getBlockWithNIndependentTransactions(totalTxs, BigInteger.valueOf(gasLimit), false); + BlockResult blockResult = executor.executeAndFill(block, parent.getHeader()); + Assertions.assertEquals(totalTxs, blockResult.getExecutedTransactions().size() + 1); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void withSequentialBucketFullRemascTxShouldFit(boolean activeRskip144) { + if (!activeRskip144) { + return; + } + + doReturn(activeRskip144).when(activationConfig).isActive(eq(RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); + Block parent = blockchain.getBestBlock(); + long blockGasLimit = GasCost.toGas(parent.getGasLimit()); + int gasLimit = 21000; + int transactionNumberToFillABucket = (int) (blockGasLimit / gasLimit); + int expectedNumberOfTx = transactionNumberToFillABucket*3 + 1; + Block block = getBlockWithNIndependentTransactions(transactionNumberToFillABucket*3, BigInteger.valueOf(gasLimit), true); + BlockResult blockResult = executor.executeAndFill(block, parent.getHeader()); + Assertions.assertEquals(expectedNumberOfTx, blockResult.getExecutedTransactions().size()); + } + + @ParameterizedTest @ValueSource(booleans = {true, false}) void executeParallelBlocksWithDifferentSubsets(boolean activeRskip144) { @@ -463,13 +633,14 @@ void executeParallelBlocksWithDifferentSubsets(boolean activeRskip144) { } doReturn(true).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = blockchain.getBestBlock(); Block block1 = getBlockWithTenTransactions(new short[]{2, 4, 6, 8}); - BlockResult result1 = executor.execute(block1, parent.getHeader(), true); + BlockResult result1 = executor.execute(null, 0, block1, parent.getHeader(), true, false, true); Block block2 = getBlockWithTenTransactions(new short[]{5}); - BlockResult result2 = executor.execute(block2, parent.getHeader(), true); + BlockResult result2 = executor.execute(null, 0, block2, parent.getHeader(), true, false, true); Assertions.assertArrayEquals(result2.getFinalState().getHash().getBytes(), result1.getFinalState().getHash().getBytes()); } @@ -482,13 +653,14 @@ void executeParallelBlockAgainstSequentialBlock(boolean activeRskip144) { } doReturn(true).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = blockchain.getBestBlock(); Block pBlock = getBlockWithTenTransactions(new short[]{2, 4, 6, 8}); - BlockResult parallelResult = executor.execute(pBlock, parent.getHeader(), true); + BlockResult parallelResult = executor.execute(null, 0, pBlock, parent.getHeader(), true, false, true); Block sBlock = getBlockWithTenTransactions(null); - BlockResult seqResult = executor.execute(sBlock, parent.getHeader(), true); + BlockResult seqResult = executor.executeForMining(sBlock, parent.getHeader(), true, false, true); Assertions.assertEquals(pBlock.getTransactionsList().size(), parallelResult.getExecutedTransactions().size()); Assertions.assertArrayEquals(seqResult.getFinalState().getHash().getBytes(), parallelResult.getFinalState().getHash().getBytes()); @@ -502,6 +674,7 @@ void executeParallelBlockTwice(boolean activeRskip144) { } doReturn(true).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = blockchain.getBestBlock(); Block block1 = getBlockWithTenTransactions(new short[]{2, 4, 6, 8}); BlockResult result1 = executor.executeAndFill(block1, parent.getHeader()); @@ -524,16 +697,16 @@ void validateStateRootWithRskip126DisabledAndValidStateRoot(boolean activeRskip1 Block block = new BlockGenerator(Constants.regtest(), activationConfig).getBlock(1); block.setStateRoot(trie.getHash().getBytes()); - BlockResult blockResult = new BlockResult(block, Collections.emptyList(), Collections.emptyList(), 0, + BlockResult blockResult = new BlockResult(block, Collections.emptyList(), Collections.emptyList(), new short[0], 0, Coin.ZERO, trie); - RskSystemProperties cfg = spy(CONFIG); +// RskSystemProperties cfg = spy(CONFIG); ActivationConfig activationConfig = spy(cfg.getActivationConfig()); doReturn(false).when(activationConfig).isActive(eq(RSKIP126), anyLong()); doReturn(activationConfig).when(cfg).getActivationConfig(); - BlockExecutor executor = buildBlockExecutor(trieStore, cfg); + BlockExecutor executor = buildBlockExecutor(trieStore, cfg, activeRskip144, false); short[] expectedEdges = activeRskip144 ? new short[0] : null; @@ -551,16 +724,16 @@ void validateStateRootWithRskip126DisabledAndInvalidStateRoot(boolean activeRski Block block = new BlockGenerator(Constants.regtest(), activationConfig).getBlock(1); block.setStateRoot(new byte[] { 1, 2, 3, 4 }); - BlockResult blockResult = new BlockResult(block, Collections.emptyList(), Collections.emptyList(), 0, + BlockResult blockResult = new BlockResult(block, Collections.emptyList(), Collections.emptyList(), new short[0], 0, Coin.ZERO, trie); - RskSystemProperties cfg = spy(CONFIG); +// RskSystemProperties cfg = spy(CONFIG); - ActivationConfig activationConfig = spy(cfg.getActivationConfig()); - doReturn(false).when(activationConfig).isActive(eq(RSKIP126), anyLong()); - doReturn(activationConfig).when(cfg).getActivationConfig(); +// ActivationConfig activationConfig = spy(cfg.getActivationConfig()); + boolean rskip126IsActive = false; +// doReturn(activationConfig).when(cfg).getActivationConfig(); - BlockExecutor executor = buildBlockExecutor(trieStore, cfg); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, rskip126IsActive); short[] expectedEdges = activeRskip144 ? new short[0] : null; @@ -572,12 +745,12 @@ void validateStateRootWithRskip126DisabledAndInvalidStateRoot(boolean activeRski @ValueSource(booleans = {true, false}) void validateBlock(boolean activeRskip144) { doReturn(activeRskip144).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); - TestObjects objects = generateBlockWithOneTransaction(); + TestObjects objects = generateBlockWithOneTransaction(activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = objects.getParent(); Block block = objects.getBlock(); - BlockExecutor executor = buildBlockExecutor(objects.getTrieStore()); + BlockExecutor executor = buildBlockExecutor(objects.getTrieStore(), activeRskip144, RSKIP_126_IS_ACTIVE); - short[] expectedEdges = activeRskip144 ? new short[0] : null; + short[] expectedEdges = activeRskip144 ? new short[]{(short) block.getTransactionsList().size()} : null; Assertions.assertArrayEquals(expectedEdges, block.getHeader().getTxExecutionListsEdges()); Assertions.assertTrue(executor.executeAndValidate(block, parent.getHeader())); @@ -587,14 +760,14 @@ void validateBlock(boolean activeRskip144) { @ValueSource(booleans = {true, false}) void invalidBlockBadStateRoot(boolean activeRskip144) { doReturn(activeRskip144).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); - TestObjects objects = generateBlockWithOneTransaction(); + TestObjects objects = generateBlockWithOneTransaction(activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = objects.getParent(); Block block = objects.getBlock(); - BlockExecutor executor = buildBlockExecutor(objects.getTrieStore()); + BlockExecutor executor = buildBlockExecutor(objects.getTrieStore(), activeRskip144, RSKIP_126_IS_ACTIVE); byte[] stateRoot = block.getStateRoot(); stateRoot[0] = (byte) ((stateRoot[0] + 1) % 256); - short[] expectedEdges = activeRskip144 ? new short[0] : null; + short[] expectedEdges = activeRskip144 ? new short[]{(short) block.getTransactionsList().size()} : null; Assertions.assertArrayEquals(expectedEdges, block.getHeader().getTxExecutionListsEdges()); Assertions.assertFalse(executor.executeAndValidate(block, parent.getHeader())); @@ -604,14 +777,14 @@ void invalidBlockBadStateRoot(boolean activeRskip144) { @ValueSource(booleans = {true, false}) void invalidBlockBadReceiptsRoot(boolean activeRskip144) { doReturn(activeRskip144).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); - TestObjects objects = generateBlockWithOneTransaction(); + TestObjects objects = generateBlockWithOneTransaction(activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = objects.getParent(); Block block = objects.getBlock(); - BlockExecutor executor = buildBlockExecutor(objects.getTrieStore()); + BlockExecutor executor = buildBlockExecutor(objects.getTrieStore(), activeRskip144, RSKIP_126_IS_ACTIVE); byte[] receiptsRoot = block.getReceiptsRoot(); receiptsRoot[0] = (byte) ((receiptsRoot[0] + 1) % 256); - short[] expectedEdges = activeRskip144 ? new short[0] : null; + short[] expectedEdges = activeRskip144 ? new short[]{(short) block.getTransactionsList().size()} : null; Assertions.assertArrayEquals(expectedEdges, block.getHeader().getTxExecutionListsEdges()); Assertions.assertFalse(executor.executeAndValidate(block, parent.getHeader())); @@ -621,13 +794,13 @@ void invalidBlockBadReceiptsRoot(boolean activeRskip144) { @ValueSource(booleans = {true, false}) void invalidBlockBadGasUsed(boolean activeRskip144) { doReturn(activeRskip144).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); - TestObjects objects = generateBlockWithOneTransaction(); + TestObjects objects = generateBlockWithOneTransaction(activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = objects.getParent(); Block block = objects.getBlock(); - BlockExecutor executor = buildBlockExecutor(objects.getTrieStore()); + BlockExecutor executor = buildBlockExecutor(objects.getTrieStore(), activeRskip144, RSKIP_126_IS_ACTIVE); block.getHeader().setGasUsed(0); - short[] expectedEdges = activeRskip144 ? new short[0] : null; + short[] expectedEdges = activeRskip144 ? new short[]{(short) block.getTransactionsList().size()} : null; Assertions.assertArrayEquals(expectedEdges, block.getHeader().getTxExecutionListsEdges()); Assertions.assertFalse(executor.executeAndValidate(block, parent.getHeader())); @@ -637,13 +810,13 @@ void invalidBlockBadGasUsed(boolean activeRskip144) { @ValueSource(booleans = {true, false}) void invalidBlockBadPaidFees(boolean activeRskip144) { doReturn(activeRskip144).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); - TestObjects objects = generateBlockWithOneTransaction(); + TestObjects objects = generateBlockWithOneTransaction(activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = objects.getParent(); Block block = objects.getBlock(); - BlockExecutor executor = buildBlockExecutor(objects.getTrieStore()); + BlockExecutor executor = buildBlockExecutor(objects.getTrieStore(), activeRskip144, RSKIP_126_IS_ACTIVE); block.getHeader().setPaidFees(Coin.ZERO); - short[] expectedEdges = activeRskip144 ? new short[0] : null; + short[] expectedEdges = activeRskip144 ? new short[]{(short) block.getTransactionsList().size()} : null; Assertions.assertArrayEquals(expectedEdges, block.getHeader().getTxExecutionListsEdges()); Assertions.assertFalse(executor.executeAndValidate(block, parent.getHeader())); @@ -653,20 +826,20 @@ void invalidBlockBadPaidFees(boolean activeRskip144) { @ValueSource(booleans = {true, false}) void invalidBlockBadLogsBloom(boolean activeRskip144) { doReturn(activeRskip144).when(activationConfig).isActive(eq(ConsensusRule.RSKIP144), anyLong()); - TestObjects objects = generateBlockWithOneTransaction(); + TestObjects objects = generateBlockWithOneTransaction(activeRskip144, RSKIP_126_IS_ACTIVE); Block parent = objects.getParent(); Block block = objects.getBlock(); - BlockExecutor executor = buildBlockExecutor(objects.getTrieStore()); + BlockExecutor executor = buildBlockExecutor(objects.getTrieStore(), activeRskip144, RSKIP_126_IS_ACTIVE); byte[] logBloom = block.getLogBloom(); logBloom[0] = (byte) ((logBloom[0] + 1) % 256); - short[] expectedEdges = activeRskip144 ? new short[0] : null; + short[] expectedEdges = activeRskip144 ? new short[]{(short) block.getTransactionsList().size()} : null; Assertions.assertArrayEquals(expectedEdges, block.getHeader().getTxExecutionListsEdges()); Assertions.assertFalse(executor.executeAndValidate(block, parent.getHeader())); } - private static TestObjects generateBlockWithOneTransaction() { + private static TestObjects generateBlockWithOneTransaction(Boolean activeRskip144, boolean rskip126IsActive) { TrieStore trieStore = new TrieStoreImpl(new HashMapDB()); Repository repository = new MutableRepository(trieStore, new Trie(trieStore)); @@ -679,7 +852,7 @@ private static TestObjects generateBlockWithOneTransaction() { Assertions.assertFalse(Arrays.equals(EMPTY_TRIE_HASH, repository.getRoot())); - BlockExecutor executor = buildBlockExecutor(trieStore); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, rskip126IsActive); Transaction tx1 = Transaction .builder() @@ -832,6 +1005,53 @@ private Block getBlockWithTenTransactions(short[] edges) { ); } + private Block getBlockWithNIndependentTransactions(int number, BigInteger txGasLimit, boolean withRemasc) { + int nTxs = number; + int nAccounts = nTxs * 2; + Repository track = repository.startTracking(); + List accounts = new LinkedList<>(); + + for (int i = 0; i < nAccounts; i++) { + accounts.add(createAccount("accounttest" + i, track, Coin.valueOf(600000))); + } + track.commit(); + Block bestBlock = blockchain.getBestBlock(); + bestBlock.setStateRoot(repository.getRoot()); + + List txs = new LinkedList<>(); + + for (int i = 0; i < nTxs; i++) { + Transaction tx = Transaction.builder() + .nonce(BigInteger.ZERO) + .gasPrice(BigInteger.ONE) + .gasLimit(txGasLimit) + .destination(accounts.get(i + nTxs).getAddress()) + .chainId(CONFIG.getNetworkConstants().getChainId()) + .value(BigInteger.TEN) + .build(); + tx.sign(accounts.get(i).getEcKey().getPrivKeyBytes()); + txs.add(tx); + } + + if (withRemasc) { + txs.add(new RemascTransaction(1L)); + } + + List uncles = new ArrayList<>(); + + return new BlockGenerator(Constants.regtest(), activationConfig) + .createChildBlock( + bestBlock, + txs, + uncles, + 1, + null, + bestBlock.getGasLimit(), + bestBlock.getCoinbase(), + null + ); + } + public static Account createAccount(String seed, Repository repository, Coin balance) { Account account = createAccount(seed); repository.createAccount(account.getAddress()); @@ -849,32 +1069,36 @@ public static Account createAccount(String seed) { ////////////////////////////////////////////// // Testing strange Txs ///////////////////////////////////////////// - @Test - void executeBlocksWithOneStrangeTransactions1() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void executeBlocksWithOneStrangeTransactions1(Boolean activeRskip144) { // will fail to create an address that is not 20 bytes long - Assertions.assertThrows(RuntimeException.class, () -> generateBlockWithOneStrangeTransaction(0)); + Assertions.assertThrows(RuntimeException.class, () -> generateBlockWithOneStrangeTransaction(0, activeRskip144)); } - @Test - void executeBlocksWithOneStrangeTransactions2() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void executeBlocksWithOneStrangeTransactions2(Boolean activeRskip144) { // will fail to create an address that is not 20 bytes long - Assertions.assertThrows(RuntimeException.class, () -> generateBlockWithOneStrangeTransaction(1)); + Assertions.assertThrows(RuntimeException.class, () -> generateBlockWithOneStrangeTransaction(1, activeRskip144)); } - @Test - void executeBlocksWithOneStrangeTransactions3() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void executeBlocksWithOneStrangeTransactions3(Boolean activeRskip144) { // the wrongly-encoded value parameter will be re-encoded with the correct serialization and won't fail - executeBlockWithOneStrangeTransaction(false, false, generateBlockWithOneStrangeTransaction(2)); + executeBlockWithOneStrangeTransaction(false, false, generateBlockWithOneStrangeTransaction(2, activeRskip144), activeRskip144); } private void executeBlockWithOneStrangeTransaction( boolean mustFailValidation, boolean mustFailExecution, - TestObjects objects) { + TestObjects objects, + Boolean activeRskip144) { Block parent = objects.getParent(); Block block = objects.getBlock(); TrieStore trieStore = objects.getTrieStore(); - BlockExecutor executor = buildBlockExecutor(trieStore); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); Repository repository = new MutableRepository(trieStore, trieStore.retrieve(objects.getParent().getStateRoot()).get()); Transaction tx = objects.getTransaction(); @@ -892,7 +1116,7 @@ private void executeBlockWithOneStrangeTransaction( return; } - BlockResult result = executor.execute(block, parent.getHeader(), false); + BlockResult result = executor.executeForMining(block, parent.getHeader(), false, false, true); Assertions.assertNotNull(result); if (mustFailExecution) { @@ -932,7 +1156,7 @@ private void executeBlockWithOneStrangeTransaction( Assertions.assertEquals(BigInteger.valueOf(30000 - 21000 - 10), accountState.getBalance().asBigInteger()); } - public TestObjects generateBlockWithOneStrangeTransaction(int strangeTransactionType) { + public TestObjects generateBlockWithOneStrangeTransaction(int strangeTransactionType, Boolean activeRskip144) { TrieStore trieStore = new TrieStoreImpl(new HashMapDB()); Repository repository = new MutableRepository(trieStore, new Trie(trieStore)); Repository track = repository.startTracking(); @@ -944,7 +1168,7 @@ public TestObjects generateBlockWithOneStrangeTransaction(int strangeTransaction Assertions.assertFalse(Arrays.equals(EMPTY_TRIE_HASH, repository.getRoot())); - BlockExecutor executor = buildBlockExecutor(trieStore); + BlockExecutor executor = buildBlockExecutor(trieStore, activeRskip144, RSKIP_126_IS_ACTIVE); List txs = new ArrayList<>(); Transaction tx = createStrangeTransaction( @@ -1013,29 +1237,35 @@ private static byte[] sha3(byte[] input) { return digest.digest(); } - private static BlockExecutor buildBlockExecutor(TrieStore store) { - return buildBlockExecutor(store, CONFIG); + private static BlockExecutor buildBlockExecutor(TrieStore store, Boolean activeRskip144, boolean rskip126IsActive) { + return buildBlockExecutor(store, CONFIG, activeRskip144, rskip126IsActive); } - private static BlockExecutor buildBlockExecutor(TrieStore store, RskSystemProperties config) { - StateRootHandler stateRootHandler = new StateRootHandler(config.getActivationConfig(), new StateRootsStoreImpl(new HashMapDB())); + private static BlockExecutor buildBlockExecutor(TrieStore store, RskSystemProperties config, Boolean activeRskip144, Boolean activeRskip126) { + RskSystemProperties cfg = spy(config); + doReturn(activationConfig).when(cfg).getActivationConfig(); + doReturn(activeRskip144).when(activationConfig).isActive(eq(RSKIP144), anyLong()); + doReturn(activeRskip126).when(activationConfig).isActive(eq(RSKIP126), anyLong()); + + + StateRootHandler stateRootHandler = new StateRootHandler(cfg.getActivationConfig(), new StateRootsStoreImpl(new HashMapDB())); Factory btcBlockStoreFactory = new RepositoryBtcBlockStoreWithCache.Factory( - config.getNetworkConstants().getBridgeConstants().getBtcParams()); + cfg.getNetworkConstants().getBridgeConstants().getBtcParams()); BridgeSupportFactory bridgeSupportFactory = new BridgeSupportFactory( - btcBlockStoreFactory, config.getNetworkConstants().getBridgeConstants(), config.getActivationConfig()); + btcBlockStoreFactory, cfg.getNetworkConstants().getBridgeConstants(), cfg.getActivationConfig()); return new BlockExecutor( - config.getActivationConfig(), + cfg.getActivationConfig(), new RepositoryLocator(store, stateRootHandler), new TransactionExecutorFactory( - config, + cfg, null, null, BLOCK_FACTORY, new ProgramInvokeFactoryImpl(), - new PrecompiledContracts(config, bridgeSupportFactory), + new PrecompiledContracts(cfg, bridgeSupportFactory), new BlockTxSignatureCache(new ReceivedTxSignatureCache()) ) ); diff --git a/rskj-core/src/test/java/co/rsk/core/bc/ParallelizeTransactionHandlerTest.java b/rskj-core/src/test/java/co/rsk/core/bc/ParallelizeTransactionHandlerTest.java new file mode 100644 index 00000000000..9f4bf89a356 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/core/bc/ParallelizeTransactionHandlerTest.java @@ -0,0 +1,728 @@ +/* + * This file is part of RskJ + * Copyright (C) 2017 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import co.rsk.test.builders.AccountBuilder; +import co.rsk.test.builders.TransactionBuilder; +import org.ethereum.core.Account; +import org.ethereum.core.Transaction; +import org.ethereum.db.ByteArrayWrapper; +import org.ethereum.vm.GasCost; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.*; + + +public class ParallelizeTransactionHandlerTest { + + private short buckets; + private ParallelizeTransactionHandler handler; + private Transaction tx; + private Transaction tx2; + private Transaction tx3; + private ByteArrayWrapper aWrappedKey; + private ByteArrayWrapper aDifferentWrapperKey; + private Transaction bigTx; + private Transaction bigTx2; + private short sequentialBucketNumber; + + @BeforeEach + public void setup() { + Account sender = new AccountBuilder().name("sender").build(); + Account sender2 = new AccountBuilder().name("sender2").build(); + Account sender3 = new AccountBuilder().name("sender3").build(); + Account sender4 = new AccountBuilder().name("sender4").build(); + Account sender5 = new AccountBuilder().name("sender5").build(); + byte[] aKey = {1, 2, 3}; + byte[] aDifferentKey = {1, 2, 3, 4}; + int blockGasLimit = 6800000; + long gasUsedByTx = 16000; + long biggestGasLimitPossibleInBucket = blockGasLimit - 1; + + aWrappedKey = new ByteArrayWrapper(aKey); + buckets = 2; + sequentialBucketNumber = buckets; + handler = new ParallelizeTransactionHandler(buckets, blockGasLimit); + tx = new TransactionBuilder().nonce(1).sender(sender).value(BigInteger.valueOf(1)).gasLimit(BigInteger.valueOf(gasUsedByTx)).build(); + tx2 = new TransactionBuilder().nonce(1).sender(sender2).value(BigInteger.valueOf(1)).gasLimit(BigInteger.valueOf(gasUsedByTx)).build(); + tx3 = new TransactionBuilder().nonce(1).sender(sender3).value(BigInteger.valueOf(1)).gasLimit(BigInteger.valueOf(gasUsedByTx)).build(); + bigTx = new TransactionBuilder().nonce(1).sender(sender4).gasLimit(BigInteger.valueOf(biggestGasLimitPossibleInBucket)).value(BigInteger.valueOf(1)).build(); + bigTx2 = new TransactionBuilder().nonce(1).sender(sender5).gasLimit(BigInteger.valueOf(biggestGasLimitPossibleInBucket)).value(BigInteger.valueOf(1)).build(); + aDifferentWrapperKey = new ByteArrayWrapper(aDifferentKey); + } + + @Test + void createAHandlerShouldReturnAnEmptyTransactionList() { + int expectedNumberOfTxs = 0; + int expectedNumberOfTxsInBuckets = 0; + + Assertions.assertEquals(expectedNumberOfTxs, handler.getTransactionsInOrder().size()); + Assertions.assertEquals(expectedNumberOfTxsInBuckets, handler.getTransactionsPerBucketInOrder().length); + } + + @Test + void createAHandlerAndGasUsedInBucketShouldBeZero() { + int expectedGasUsed = 0; + for (short i = 0; i < buckets; i++) { + Assertions.assertEquals(expectedGasUsed, handler.getGasUsedIn(i)); + } + } + + @Test + void addTransactionIntoTheHandlerAndShouldBeAddedInTheFirstParallelBucket() { + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), 0); + short[] expectedTransactionEdgeList = new short[]{1}; + long expectedGasUsed = 0; + + Assertions.assertTrue(bucketGasUsed.isPresent()); + Assertions.assertEquals(expectedGasUsed, (long) bucketGasUsed.get()); + + List expectedListOfTxs = new ArrayList<>(); + expectedListOfTxs.add(tx); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void addTransactionIntoTheHandlerAndShouldBeSubtractedGasUsedInTheBucket() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + } + + @Test + void addTwoTransactionsWithTheSameReadKeyAndShouldBeAddedInDifferentBuckets() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{1, 2}; + + Set readKeys = createASetAndAddKeys(aWrappedKey); + + Optional bucketGasUsed = handler.addTransaction(tx, readKeys, new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, readKeys, new HashSet<>(), gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionsWithDifferentReadKeysShouldBeAddedInDifferentBuckets() { + short[] expectedTransactionEdgeList = new short[]{1, 2}; + + HashSet readKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys2 = createASetAndAddKeys(aDifferentWrapperKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, readKeys, new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, readKeys2, new HashSet<>(), gasUsedByTx2); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionsWithSameWrittenKeysShouldBeAddedInTheSameBucket() { + short[] expectedTransactionEdgeList = new short[]{2}; + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys, gasUsedByTx2); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx+gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionsWithDifferentWrittenKeysShouldBeAddedInDifferentBuckets() { + short[] expectedTransactionEdgeList = new short[]{1, 2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet writtenKeys2 = createASetAndAddKeys(aDifferentWrapperKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys2, gasUsedByTx2); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionsWithTheSameWrittenReadKeyShouldBeAddedInTheSameBucket() { + short[] expectedTransactionEdgeList = new short[]{2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys = createASetAndAddKeys(aWrappedKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, readKeys, new HashSet<>(), gasUsedByTx2); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx+gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionsWithTheSameReadWrittenKeyShouldBeAddedInTheSameBucket() { + short[] expectedTransactionEdgeList = new short[]{2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys = createASetAndAddKeys(aWrappedKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, readKeys, new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys, gasUsedByTx2); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx+gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionsWithDifferentReadWrittenKeysShouldBeAddedInDifferentBuckets() { + short[] expectedTransactionEdgeList = new short[]{1,2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys = createASetAndAddKeys(aDifferentWrapperKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, readKeys, new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys, gasUsedByTx2); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionWithDifferentWrittenReadKeyShouldBeAddedInDifferentBuckets() { + short[] expectedTransactionEdgeList = new short[]{1, 2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys = createASetAndAddKeys(aDifferentWrapperKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, writtenKeys, new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, readKeys, new HashSet<>(), gasUsedByTx2); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx2, expectedTransactionEdgeList); + } + + @Test + void addTwoIndependentTxsAndAThirdOneCollidingWithBothAndShouldBeAddedInTheSequential() { + short[] expectedTransactionEdgeList = new short[]{1, 2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet differentWrittenKeys = createASetAndAddKeys(aDifferentWrapperKey); + + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + long gasUsedByTx3 = GasCost.toGas(tx3.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), differentWrittenKeys, gasUsedByTx2); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx3, differentWrittenKeys, writtenKeys, gasUsedByTx3); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent() && bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(gasUsedByTx3, (long) bucketGasUsed3.get()); + Assertions.assertEquals(gasUsedByTx3, handler.getGasUsedIn(sequentialBucketNumber)); + + List expectedListOfTxs = Arrays.asList(tx, tx2, tx3); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void addTwoDependentTxsWithTheSecondInSequentialAndAThirdOneCollidingWithBothAndShouldBeAddedInTheSequential() { + short[] expectedTransactionEdgeList = new short[]{1}; + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + long gasUsedByTx3 = GasCost.toGas(tx3.getGasLimit()); + long totalGasInSequential = gasUsedByTx2 + gasUsedByTx3; + + + Optional bucketGasUsed = handler.addTransaction(bigTx, new HashSet<>(), writtenKeys, gasUsedByBigTx); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys, gasUsedByTx2); + Optional bucketGasUsed3 = handler.addTransaction(tx3, new HashSet<>(), writtenKeys, gasUsedByTx3); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent() && bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(totalGasInSequential, (long) bucketGasUsed3.get()); + Assertions.assertEquals(totalGasInSequential, handler.getGasUsedIn(sequentialBucketNumber)); + + List expectedListOfTxs = Arrays.asList(bigTx, tx2, tx3); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void addABigTransactionAndAnotherWithTheSameWrittenKeyAndTheLastOneShouldGoToSequential() { + short[] expectedTransactionEdgeList = new short[]{1}; + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(bigTx, new HashSet<>(), writtenKeys, gasUsedByBigTx); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed2 = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed2.get()); + Assertions.assertEquals(gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(bigTx, tx, expectedTransactionEdgeList); + } + + @Test + void addABigTxAndAnotherWithTheSameReadWrittenKeyAndShouldGoToSequential() { + short[] expectedTransactionEdgeList = new short[]{1}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys = createASetAndAddKeys(aWrappedKey); + + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(bigTx, readKeys, new HashSet<>(), gasUsedByBigTx); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed2 = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed2.get()); + Assertions.assertEquals(gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(bigTx, tx, expectedTransactionEdgeList); + } + + @Test + void addABigTxAndAnotherWithTheSameWrittenReadKeyAndShouldGoToSequential() { + short[] expectedTransactionEdgeList = new short[]{1}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys = createASetAndAddKeys(aWrappedKey); + + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(bigTx, new HashSet<>(), writtenKeys, gasUsedByBigTx); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed2 = handler.addTransaction(tx, readKeys, new HashSet<>(), gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed2.get()); + Assertions.assertEquals(gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(bigTx, tx, expectedTransactionEdgeList); + } + + @Test + void addTwoTransactionsWithTheSameSenderToTheSequentialBucketAndTheSecondShouldBeAddedCorrectly() { + short[] expectedTransactionEdgeList = new short[]{1,2}; + List expectedListOfTxs = Arrays.asList(bigTx, bigTx2, tx, tx); + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + + handler.addTransaction(bigTx, new HashSet<>(), new HashSet<>(), GasCost.toGas(bigTx.getGasLimit())); + handler.addTransaction(bigTx2, new HashSet<>(), new HashSet<>(), GasCost.toGas(bigTx2.getGasLimit())); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + Assertions.assertTrue(bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed3.get()); + + Optional bucketGasUsed4 = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + Assertions.assertTrue(bucketGasUsed4.isPresent()); + Assertions.assertEquals(2*gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertEquals(2*gasUsedByTx, (long) bucketGasUsed4.get()); + + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void twoTransactionWithTheSameSenderShouldBeInTheSameBucket() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{2}; + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(2*gasUsedByTx, (long) bucketGasUsed2.get()); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx, expectedTransactionEdgeList); + } + + @Test + void ifATxHasTheSameSenderThatAnotherAlreadyAddedIntoTheSequentialShouldGoToTheSequential() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx3 = GasCost.toGas(tx3.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{1,2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet writtenKeys2 = createASetAndAddKeys(aDifferentWrapperKey); + HashSet readKeys = createASetAndAddKeys(aWrappedKey, aDifferentWrapperKey); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys2, gasUsedByTx); + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx3, readKeys, new HashSet<>(), gasUsedByTx3); + Optional bucketGasUsed4 = handler.addTransaction(tx3, new HashSet<>(), new HashSet<>(), gasUsedByTx3); + Assertions.assertTrue(bucketGasUsed3.isPresent() && bucketGasUsed4.isPresent()); + Assertions.assertEquals(gasUsedByTx3*2, handler.getGasUsedIn(sequentialBucketNumber)); + + List expectedListOfTxs = Arrays.asList(tx, tx2, tx3, tx3); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifATxReadTwoDifferentWrittenKeysShouldGoToSequential() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx3 = GasCost.toGas(tx3.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{1,2}; + + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + HashSet writtenKeys2 = createASetAndAddKeys(aDifferentWrapperKey); + HashSet readKeys = createASetAndAddKeys(aWrappedKey, aDifferentWrapperKey); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys2, gasUsedByTx); + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx3, readKeys, new HashSet<>(), gasUsedByTx3); + Assertions.assertTrue(bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByTx3, handler.getGasUsedIn(sequentialBucketNumber)); + + List expectedListOfTxs = Arrays.asList(tx, tx2, tx3); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifATxWritesAKeyAlreadyReadByTwoTxsPlacedInDifferentBucketsShouldGoToTheSequential() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx3 = GasCost.toGas(tx3.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{1,2}; + + HashSet readKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys2 = createASetAndAddKeys(aWrappedKey); + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + + Optional bucketGasUsed = handler.addTransaction(tx, readKeys, new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, readKeys2, new HashSet<>(), gasUsedByTx); + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx3, new HashSet<>(), writtenKeys, gasUsedByTx3); + Assertions.assertTrue(bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByTx3, handler.getGasUsedIn(sequentialBucketNumber)); + + List expectedListOfTxs = Arrays.asList(tx, tx2, tx3); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifATxReadTwoKeysThatAreInDifferentBucketsShouldGoToTheSequential() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx3 = GasCost.toGas(tx3.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{1,2}; + + HashSet readKeys = createASetAndAddKeys(aWrappedKey); + HashSet readKeys2 = createASetAndAddKeys(aDifferentWrapperKey); + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey, aDifferentWrapperKey); + + Optional bucketGasUsed = handler.addTransaction(tx, readKeys, new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, readKeys2, new HashSet<>(), gasUsedByTx); + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx3, new HashSet<>(), writtenKeys, gasUsedByTx3); + Assertions.assertTrue(bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByTx3, handler.getGasUsedIn(sequentialBucketNumber)); + + List expectedListOfTxs = Arrays.asList(tx, tx2, tx3); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifATxCollidesWithAnotherOneThatAlsoHasTheSameSenderShouldGoIntoTheSameBucket() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{2}; + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(2*gasUsedByTx, (long) bucketGasUsed2.get()); + assertTwoTransactionsWereAddedProperlyIntoTheBuckets(tx, tx, expectedTransactionEdgeList); + } + + @Test + void ifATransactionHasAnAlreadyAddedSenderButCollidesWithAnotherTxShouldBeAddedIntoTheSequential() { + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + long gasUsedByTx2 = GasCost.toGas(tx2.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{1,2}; + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + + Optional bucketGasUsed = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + Optional bucketGasUsed2 = handler.addTransaction(tx2, new HashSet<>(), writtenKeys, gasUsedByTx2); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx, new HashSet<>(), writtenKeys, gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent() && bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + + List expectedListOfTxs = Arrays.asList(tx, tx2, tx); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifANewTxComesAndAllThePossibleBucketsAreFullTheTxShouldNotBeAdded() { + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByBigTx2 = GasCost.toGas(bigTx2.getGasLimit()); + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + short[] expectedTransactionEdgeList = new short[]{1,2}; + + List expectedListOfTxs = new ArrayList<>(); + expectedListOfTxs.add(bigTx); + expectedListOfTxs.add(bigTx2); + expectedListOfTxs.add(bigTx); + + Optional bucketGasUsed = handler.addTransaction(bigTx, new HashSet<>(), new HashSet<>(), gasUsedByBigTx); + Optional bucketGasUsed2 = handler.addTransaction(bigTx2, new HashSet<>(), new HashSet<>(), gasUsedByBigTx2); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(bigTx, new HashSet<>(), new HashSet<>(), gasUsedByBigTx); + Optional bucketGasUsed4 = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + + Assertions.assertFalse(bucketGasUsed4.isPresent()); + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent() && bucketGasUsed3.isPresent()); + + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByBigTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed3.get()); + Assertions.assertEquals(gasUsedByBigTx, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifBucketsAreFullAndAnIndependentTxComesShouldBeAddedInTheSequential() { + short[] expectedTransactionEdgeList = new short[]{1,2}; + + List expectedListOfTxs = new ArrayList<>(); + expectedListOfTxs.add(bigTx); + expectedListOfTxs.add(bigTx2); + expectedListOfTxs.add(tx); + + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByBigTx2 = GasCost.toGas(bigTx2.getGasLimit()); + long gasUsedByTx = GasCost.toGas(tx.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(bigTx, new HashSet<>(), new HashSet<>(), gasUsedByBigTx); + Optional bucketGasUsed2 = handler.addTransaction(bigTx2, new HashSet<>(), new HashSet<>(), gasUsedByBigTx2); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), gasUsedByTx); + + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent() && bucketGasUsed3.isPresent()); + + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByBigTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(gasUsedByTx, (long) bucketGasUsed3.get()); + Assertions.assertEquals(gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifAllTheBucketsAreFullTheNewIndependentTxShouldNotBeIncluded() { + short[] expectedTransactionEdgeList = new short[]{1,2}; + List expectedListOfTxs = Arrays.asList(bigTx, bigTx2, bigTx); + + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByBigTx2 = GasCost.toGas(bigTx2.getGasLimit()); + + Optional bucketGasUsed = handler.addTransaction(bigTx, new HashSet<>(), new HashSet<>(), gasUsedByBigTx); + Optional bucketGasUsed2 = handler.addTransaction(bigTx2, new HashSet<>(), new HashSet<>(), gasUsedByBigTx2); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(bigTx, new HashSet<>(), new HashSet<>(), gasUsedByBigTx); + Assertions.assertTrue(bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed3.get()); + Assertions.assertEquals(gasUsedByBigTx, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional emptyBucket = handler.addTransaction(tx, new HashSet<>(), new HashSet<>(), GasCost.toGas(tx.getGasLimit())); + Assertions.assertEquals(gasUsedByBigTx, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertFalse(emptyBucket.isPresent()); + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByBigTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void ifAllTheBucketsAreFullTheNewDependentTxShouldNotBeIncluded() { + short[] expectedTransactionEdgeList = new short[]{1,2}; + List expectedListOfTxs = Arrays.asList(bigTx, bigTx2, bigTx); + + long gasUsedByBigTx = GasCost.toGas(bigTx.getGasLimit()); + long gasUsedByBigTx2 = GasCost.toGas(bigTx2.getGasLimit()); + HashSet writtenKeys = createASetAndAddKeys(aWrappedKey); + + Optional bucketGasUsed = handler.addTransaction(bigTx, new HashSet<>(), writtenKeys, gasUsedByBigTx); + Optional bucketGasUsed2 = handler.addTransaction(bigTx2, new HashSet<>(), new HashSet<>(), gasUsedByBigTx2); + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional bucketGasUsed3 = handler.addTransaction(bigTx, new HashSet<>(), new HashSet<>(), gasUsedByBigTx); + Assertions.assertTrue(bucketGasUsed3.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed3.get()); + Assertions.assertEquals(gasUsedByBigTx, handler.getGasUsedIn(sequentialBucketNumber)); + + Optional emptyBucket = handler.addTransaction(tx, new HashSet<>(), writtenKeys, GasCost.toGas(tx.getGasLimit())); + Assertions.assertEquals(gasUsedByBigTx, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertFalse(emptyBucket.isPresent()); + Assertions.assertTrue(bucketGasUsed.isPresent() && bucketGasUsed2.isPresent()); + Assertions.assertEquals(gasUsedByBigTx, (long) bucketGasUsed.get()); + Assertions.assertEquals(gasUsedByBigTx2, (long) bucketGasUsed2.get()); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } + + @Test + void aRemascTxAddedShouldBeInTheSequentialBucket() { + List expectedListOfTxs = Collections.singletonList(tx); + long gasUsedByTx = GasCost.toGas(bigTx.getGasLimit()); + + Assertions.assertEquals(0, handler.getGasUsedIn(sequentialBucketNumber)); + Optional sequentialBucketGasUsed = handler.addRemascTransaction(tx, gasUsedByTx); + + Assertions.assertTrue(sequentialBucketGasUsed.isPresent()); + Assertions.assertEquals(gasUsedByTx, handler.getGasUsedIn(sequentialBucketNumber)); + Assertions.assertEquals(gasUsedByTx, (long) sequentialBucketGasUsed.get()); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + } + + @Test + void ifItsSequentialTheEdgesListShouldHaveSizeZero() { + handler.addRemascTransaction(tx, GasCost.toGas(bigTx.getGasLimit())); + Assertions.assertEquals(0, handler.getTransactionsPerBucketInOrder().length); + } + + @Test + void callGetGasUsedInWithAnInvalidBucketShouldThrowAnError() { + short invalidBucketId = (short) (buckets+1); + try { + handler.getGasUsedIn(invalidBucketId); + Assertions.fail(); + } catch (NoSuchElementException e) { + Assertions.assertTrue(true); + } + } + + @Test + void callGetGasUsedInWithAnInvalidBucketShouldThrowAnError2() { + short invalidBucketId = -1; + try { + handler.getGasUsedIn(invalidBucketId); + Assertions.fail(); + } catch (NoSuchElementException e) { + Assertions.assertTrue(true); + } + } + private HashSet createASetAndAddKeys(ByteArrayWrapper... aKey) { + return new HashSet<>(Arrays.asList(aKey)); + } + + private void assertTwoTransactionsWereAddedProperlyIntoTheBuckets(Transaction tx, Transaction tx2, short[] expectedTransactionEdgeList) { + List expectedListOfTxs = Arrays.asList(tx, tx2); + Assertions.assertEquals(expectedListOfTxs, handler.getTransactionsInOrder()); + Assertions.assertArrayEquals(expectedTransactionEdgeList, handler.getTransactionsPerBucketInOrder()); + } +} diff --git a/rskj-core/src/test/java/co/rsk/core/bc/ReadWrittenKeysTrackerTest.java b/rskj-core/src/test/java/co/rsk/core/bc/ReadWrittenKeysTrackerTest.java new file mode 100644 index 00000000000..c9103ded52d --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/core/bc/ReadWrittenKeysTrackerTest.java @@ -0,0 +1,138 @@ +/* + * This file is part of RskJ + * Copyright (C) 2017 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.core.bc; + +import org.ethereum.db.ByteArrayWrapper; +import org.ethereum.db.DummyReadWrittenKeysTracker; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + + +public class ReadWrittenKeysTrackerTest { + + private IReadWrittenKeysTracker tracker; + private IReadWrittenKeysTracker dummyTracker; + private ByteArrayWrapper key1; + private ByteArrayWrapper key2; + + + @BeforeEach + void setup() { + this.tracker = new ReadWrittenKeysTracker(); + this.dummyTracker = new DummyReadWrittenKeysTracker(); + this.key1 = new ByteArrayWrapper(new byte[]{1}); + this.key2 = new ByteArrayWrapper(new byte[]{2}); + } + + @Test + void createATrackerShouldHaveEmptyMaps() { + Assertions.assertEquals(0, tracker.getTemporalReadKeys().size()); + Assertions.assertEquals(0, tracker.getTemporalWrittenKeys().size()); + } + + @Test + void addReadKeyToTheTrackerAndShouldBeInReadMap() { + tracker.addNewReadKey(key1); + Set temporalReadKeys = tracker.getTemporalReadKeys(); + assertKeyWasAddedInMap(temporalReadKeys, key1); + } + + @Test + void addReadKeyToTheTrackerAndShouldntBeInWrittenMap() { + tracker.addNewReadKey(key1); + Assertions.assertEquals(0, tracker.getTemporalWrittenKeys().size()); + } + + @Test + void addWrittenKeyToTheTrackerAndShouldBeInWrittenMap() { + tracker.addNewWrittenKey(key1); + Set temporalWrittenKeys = tracker.getTemporalWrittenKeys(); + assertKeyWasAddedInMap(temporalWrittenKeys, key1); + } + + @Test + void addWrittenKeyToTheTrackerAndShouldntBeInReadMap() { + tracker.addNewWrittenKey(key1); + Assertions.assertEquals(0, tracker.getTemporalReadKeys().size()); + } + + @Test + void clearTrackerShouldEmptyTheMaps() { + tracker.addNewWrittenKey(key1); + tracker.addNewReadKey(key1); + tracker.addNewWrittenKey(key2); + tracker.addNewReadKey(key2); + + Assertions.assertEquals(2, tracker.getTemporalReadKeys().size()); + Assertions.assertEquals(2, tracker.getTemporalWrittenKeys().size()); + + tracker.clear(); + + Assertions.assertEquals(0, tracker.getTemporalReadKeys().size()); + Assertions.assertEquals(0, tracker.getTemporalWrittenKeys().size()); + } + + @Test + void createADummyTrackerShouldHaveEmptyMaps() { + Assertions.assertEquals(0, dummyTracker.getTemporalReadKeys().size()); + Assertions.assertEquals(0, dummyTracker.getTemporalWrittenKeys().size()); + } + + @Test + void addReadKeyToTheDummyTrackerShouldDoNothing() { + dummyTracker.addNewReadKey(key1); + Assertions.assertEquals(0, dummyTracker.getTemporalReadKeys().size()); + } + + @Test + void addReadKeyToTheTrackerShouldDoNothing() { + dummyTracker.addNewReadKey(key1); + Assertions.assertEquals(0, dummyTracker.getTemporalWrittenKeys().size()); + } + + @Test + void addWrittenKeyToTheDummyTrackerShouldDoNothing() { + dummyTracker.addNewWrittenKey(key1); + Assertions.assertEquals(0, dummyTracker.getTemporalWrittenKeys().size()); + } + + @Test + void clearDummyTrackerShouldDoNothing() { + dummyTracker.addNewWrittenKey(key1); + dummyTracker.addNewReadKey(key1); + dummyTracker.addNewWrittenKey(key2); + dummyTracker.addNewReadKey(key2); + + Assertions.assertEquals(0, dummyTracker.getTemporalReadKeys().size()); + Assertions.assertEquals(0, dummyTracker.getTemporalWrittenKeys().size()); + + dummyTracker.clear(); + + Assertions.assertEquals(0, dummyTracker.getTemporalReadKeys().size()); + Assertions.assertEquals(0, dummyTracker.getTemporalWrittenKeys().size()); + } + + private void assertKeyWasAddedInMap(Set map, ByteArrayWrapper key) { + Assertions.assertEquals(1, map.size()); + Assertions.assertTrue(map.contains(key)); + } +} diff --git a/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplLogsTest.java b/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplLogsTest.java index 040582ed148..7d121ccab62 100644 --- a/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplLogsTest.java +++ b/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplLogsTest.java @@ -106,7 +106,7 @@ class Web3ImplLogsTest { private static final String ONE_TOPIC = "0000000000000000000000000000000000000000000000000000000000000001"; private static final String INCREMENT_METHOD_SIGNATURE = "371303c0"; private static final String GET_VALUE_METHOD_SIGNATURE = "20965255"; - private static final String TRACKED_TEST_BLOCK_HASH = "0x5fcfdf1c5c83850e4e4094124a1d7a314b8684055ed4577a02abf5bc438096f7"; + private static final String TRACKED_TEST_BLOCK_HASH = "0x1d3137d39f8467053020d1521019d23d33e3a03e92f296f4a0e8ed12b2891ae7"; private static final String UNTRACKED_TEST_BLOCK_HASH = "0xdea168a4f74e51a3eeb6d72b049c4fc7bc750dd51f13a3afa4fee4bece0e85eb"; private final TestSystemProperties config = new TestSystemProperties(); private Blockchain blockChain;