Coverage Summary for Class: TransactionPoolImpl (co.rsk.core.bc)
Class |
Class, %
|
Method, %
|
Line, %
|
TransactionPoolImpl |
100%
(1/1)
|
77.5%
(31/40)
|
71.9%
(161/224)
|
1 /*
2 * This file is part of RskJ
3 * Copyright (C) 2017 RSK Labs Ltd.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 package co.rsk.core.bc;
20
21 import co.rsk.config.RskSystemProperties;
22 import co.rsk.core.Coin;
23 import co.rsk.core.TransactionExecutorFactory;
24 import co.rsk.crypto.Keccak256;
25 import co.rsk.db.RepositoryLocator;
26 import co.rsk.db.RepositorySnapshot;
27 import co.rsk.net.TransactionValidationResult;
28 import co.rsk.net.handler.TxPendingValidator;
29 import com.google.common.annotations.VisibleForTesting;
30 import org.ethereum.core.*;
31 import org.ethereum.db.BlockStore;
32 import org.ethereum.listener.EthereumListener;
33 import org.ethereum.util.ByteUtil;
34 import org.ethereum.vm.GasCost;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import java.math.BigInteger;
39 import java.util.*;
40 import java.util.concurrent.Executors;
41 import java.util.concurrent.ScheduledExecutorService;
42 import java.util.concurrent.ScheduledFuture;
43 import java.util.concurrent.TimeUnit;
44
45 import static org.ethereum.util.BIUtil.toBI;
46
47 /**
48 * Created by ajlopez on 08/08/2016.
49 */
50 public class TransactionPoolImpl implements TransactionPool {
51 private static final Logger logger = LoggerFactory.getLogger("txpool");
52
53 private final TransactionSet pendingTransactions = new TransactionSet();
54 private final TransactionSet queuedTransactions = new TransactionSet();
55
56 private final Map<Keccak256, Long> transactionBlocks = new HashMap<>();
57 private final Map<Keccak256, Long> transactionTimes = new HashMap<>();
58
59 private final RskSystemProperties config;
60 private final BlockStore blockStore;
61 private final RepositoryLocator repositoryLocator;
62 private final BlockFactory blockFactory;
63 private final EthereumListener listener;
64 private final TransactionExecutorFactory transactionExecutorFactory;
65 private final SignatureCache signatureCache;
66 private final int outdatedThreshold;
67 private final int outdatedTimeout;
68
69 private ScheduledExecutorService cleanerTimer;
70 private ScheduledFuture<?> cleanerFuture;
71
72 private Block bestBlock;
73
74 private final TxPendingValidator validator;
75
76 public TransactionPoolImpl(
77 RskSystemProperties config,
78 RepositoryLocator repositoryLocator,
79 BlockStore blockStore,
80 BlockFactory blockFactory,
81 EthereumListener listener,
82 TransactionExecutorFactory transactionExecutorFactory,
83 SignatureCache signatureCache,
84 int outdatedThreshold,
85 int outdatedTimeout) {
86 this.config = config;
87 this.blockStore = blockStore;
88 this.repositoryLocator = repositoryLocator;
89 this.blockFactory = blockFactory;
90 this.listener = listener;
91 this.transactionExecutorFactory = transactionExecutorFactory;
92 this.signatureCache = signatureCache;
93 this.outdatedThreshold = outdatedThreshold;
94 this.outdatedTimeout = outdatedTimeout;
95
96 this.validator = new TxPendingValidator(config.getNetworkConstants(), config.getActivationConfig(), config.getNumOfAccountSlots());
97
98 if (this.outdatedTimeout > 0) {
99 this.cleanerTimer = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "TransactionPoolCleanerTimer"));
100 }
101 }
102
103 @Override
104 public void start() {
105 processBest(blockStore.getBestBlock());
106
107 if (this.outdatedTimeout <= 0 || this.cleanerTimer == null) {
108 return;
109 }
110
111 this.cleanerFuture = this.cleanerTimer.scheduleAtFixedRate(this::cleanUp, this.outdatedTimeout, this.outdatedTimeout, TimeUnit.SECONDS);
112 }
113
114 @Override
115 public void stop() {
116 // consider cleaning up cleanerTimer/cleanerFuture
117 }
118
119 public boolean hasCleanerFuture() {
120 return this.cleanerFuture != null;
121 }
122
123 public void cleanUp() {
124 final long timestampSeconds = this.getCurrentTimeInSeconds();
125 this.removeObsoleteTransactions(timestampSeconds - this.outdatedTimeout);
126 }
127
128 public int getOutdatedThreshold() { return outdatedThreshold; }
129
130 public int getOutdatedTimeout() { return outdatedTimeout; }
131
132 public Block getBestBlock() {
133 return bestBlock;
134 }
135
136 @Override
137 public PendingState getPendingState() {
138 return getPendingState(getCurrentRepository());
139 }
140
141 private PendingState getPendingState(RepositorySnapshot currentRepository) {
142 removeObsoleteTransactions(this.outdatedThreshold, this.outdatedTimeout);
143 return new PendingState(
144 currentRepository,
145 new TransactionSet(pendingTransactions),
146 (repository, tx) -> transactionExecutorFactory.newInstance(
147 tx,
148 0,
149 bestBlock.getCoinbase(),
150 repository,
151 createFakePendingBlock(bestBlock),
152 0
153 )
154 );
155 }
156
157 private RepositorySnapshot getCurrentRepository() {
158 return repositoryLocator.snapshotAt(getBestBlock().getHeader());
159 }
160
161 private List<Transaction> addSuccessors(Transaction tx) {
162 List<Transaction> pendingTransactionsAdded = new ArrayList<>();
163 Optional<Transaction> successor = this.getQueuedSuccessor(tx);
164
165 while (successor.isPresent()) {
166 Transaction found = successor.get();
167 queuedTransactions.removeTransactionByHash(found.getHash());
168
169 if (!this.internalAddTransaction(found).pendingTransactionsWereAdded()) {
170 break;
171 }
172
173 pendingTransactionsAdded.add(found);
174
175 successor = this.getQueuedSuccessor(found);
176 }
177
178 return pendingTransactionsAdded;
179 }
180
181 private void emitEvents(List<Transaction> addedPendingTransactions) {
182 if (listener != null && !addedPendingTransactions.isEmpty()) {
183 EventDispatchThread.invokeLater(() -> {
184 listener.onPendingTransactionsReceived(addedPendingTransactions);
185 listener.onTransactionPoolChanged(TransactionPoolImpl.this);
186 });
187 }
188 }
189
190 @Override
191 public synchronized List<Transaction> addTransactions(final List<Transaction> txs) {
192 List<Transaction> pendingTransactionsAdded = new ArrayList<>();
193
194 for (Transaction tx : txs) {
195 TransactionPoolAddResult result = this.internalAddTransaction(tx);
196
197 if (result.pendingTransactionsWereAdded()) {
198 pendingTransactionsAdded.add(tx);
199 pendingTransactionsAdded.addAll(this.addSuccessors(tx));
200 }
201 }
202
203 this.emitEvents(pendingTransactionsAdded);
204
205 return pendingTransactionsAdded;
206 }
207
208 private Optional<Transaction> getQueuedSuccessor(Transaction tx) {
209 BigInteger next = tx.getNonceAsInteger().add(BigInteger.ONE);
210
211 List<Transaction> txsaccount = this.queuedTransactions.getTransactionsWithSender(tx.getSender());
212
213 if (txsaccount == null) {
214 return Optional.empty();
215 }
216
217 return txsaccount
218 .stream()
219 .filter(t -> t.getNonceAsInteger().equals(next))
220 .findFirst();
221 }
222
223 private TransactionPoolAddResult internalAddTransaction(final Transaction tx) {
224 if (pendingTransactions.hasTransaction(tx)) {
225 return TransactionPoolAddResult.withError("pending transaction with same hash already exists");
226 }
227
228 if (queuedTransactions.hasTransaction(tx)) {
229 return TransactionPoolAddResult.withError("queued transaction with same hash already exists");
230 }
231
232 RepositorySnapshot currentRepository = getCurrentRepository();
233 TransactionValidationResult validationResult = shouldAcceptTx(tx, currentRepository);
234
235 if (!validationResult.transactionIsValid()) {
236 return TransactionPoolAddResult.withError(validationResult.getErrorMessage());
237 }
238
239 Keccak256 hash = tx.getHash();
240 logger.trace("add transaction {} {}", toBI(tx.getNonce()), tx.getHash());
241
242 Long bnumber = Long.valueOf(getCurrentBestBlockNumber());
243
244 if (!isBumpingGasPriceForSameNonceTx(tx)) {
245 return TransactionPoolAddResult.withError("gas price not enough to bump transaction");
246 }
247
248 transactionBlocks.put(hash, bnumber);
249 final long timestampSeconds = this.getCurrentTimeInSeconds();
250 transactionTimes.put(hash, timestampSeconds);
251
252 BigInteger currentNonce = getPendingState(currentRepository).getNonce(tx.getSender());
253 BigInteger txNonce = tx.getNonceAsInteger();
254 if (txNonce.compareTo(currentNonce) > 0) {
255 this.addQueuedTransaction(tx);
256 signatureCache.storeSender(tx);
257 return TransactionPoolAddResult.okQueuedTransaction(tx);
258 }
259
260 if (!senderCanPayPendingTransactionsAndNewTx(tx, currentRepository)) {
261 // discard this tx to prevent spam
262 return TransactionPoolAddResult.withError("insufficient funds to pay for pending and new transaction");
263 }
264
265 pendingTransactions.addTransaction(tx);
266 signatureCache.storeSender(tx);
267
268 return TransactionPoolAddResult.okPendingTransaction(tx);
269 }
270
271 @Override
272 public synchronized TransactionPoolAddResult addTransaction(final Transaction tx) {
273 TransactionPoolAddResult internalResult = this.internalAddTransaction(tx);
274 List<Transaction> pendingTransactionsAdded = new ArrayList<>();
275
276 if (!internalResult.transactionsWereAdded()) {
277 return internalResult;
278 } else if(internalResult.pendingTransactionsWereAdded()) {
279 pendingTransactionsAdded.add(tx);
280 pendingTransactionsAdded.addAll(this.addSuccessors(tx)); // addSuccessors only retrieves pending successors
281 }
282
283 this.emitEvents(pendingTransactionsAdded);
284
285 return TransactionPoolAddResult.ok(internalResult.getQueuedTransactionsAdded(), pendingTransactionsAdded);
286 }
287
288 private boolean isBumpingGasPriceForSameNonceTx(Transaction tx) {
289 Optional<Transaction> oldTxWithNonce = pendingTransactions.getTransactionsWithSender(tx.getSender()).stream()
290 .filter(t -> t.getNonceAsInteger().equals(tx.getNonceAsInteger()))
291 .findFirst();
292
293 if (oldTxWithNonce.isPresent()){
294 //oldGasPrice * (100 + priceBump) / 100
295 Coin oldGasPrice = oldTxWithNonce.get().getGasPrice();
296 Coin gasPriceBumped = oldGasPrice
297 .multiply(BigInteger.valueOf(config.getGasPriceBump() + 100L))
298 .divide(BigInteger.valueOf(100));
299
300 if (oldGasPrice.compareTo(tx.getGasPrice()) >= 0 || gasPriceBumped.compareTo(tx.getGasPrice()) > 0) {
301 return false;
302 }
303 }
304 return true;
305 }
306
307 @Override
308 public synchronized void processBest(Block newBlock) {
309 logger.trace("Processing best block {} {}", newBlock.getNumber(), newBlock.getPrintableHash());
310
311 BlockFork fork = getFork(this.bestBlock, newBlock);
312
313 //we need to update the bestBlock before calling retractBlock
314 //or else the transactions would be validated against outdated account state.
315 this.bestBlock = newBlock;
316
317 if(fork != null) {
318 for (Block blk : fork.getOldBlocks()) {
319 retractBlock(blk);
320 }
321
322 for (Block blk : fork.getNewBlocks()) {
323 acceptBlock(blk);
324 }
325 }
326
327 removeObsoleteTransactions(this.outdatedThreshold, this.outdatedTimeout);
328
329 if (listener != null) {
330 EventDispatchThread.invokeLater(() -> listener.onTransactionPoolChanged(TransactionPoolImpl.this));
331 }
332 }
333
334 private BlockFork getFork(Block oldBestBlock, Block newBestBlock) {
335 if (oldBestBlock != null) {
336 BlockchainBranchComparator branchComparator = new BlockchainBranchComparator(blockStore);
337 return branchComparator.calculateFork(oldBestBlock, newBestBlock);
338 }
339 else {
340 return null;
341 }
342 }
343
344 @VisibleForTesting
345 public void acceptBlock(Block block) {
346 List<Transaction> txs = block.getTransactionsList();
347
348 removeTransactions(txs);
349 }
350
351 @VisibleForTesting
352 public void retractBlock(Block block) {
353 List<Transaction> txs = block.getTransactionsList();
354
355 logger.trace("Retracting block {} {} with {} txs", block.getNumber(), block.getPrintableHash(), txs.size());
356
357 this.addTransactions(txs);
358 }
359
360 private void removeObsoleteTransactions(int depth, int timeout) {
361 this.removeObsoleteTransactions(this.getCurrentBestBlockNumber(), depth, timeout);
362 }
363
364 @VisibleForTesting
365 public void removeObsoleteTransactions(long currentBlock, int depth, int timeout) {
366 List<Keccak256> toremove = new ArrayList<>();
367 final long timestampSeconds = this.getCurrentTimeInSeconds();
368
369 for (Map.Entry<Keccak256, Long> entry : transactionBlocks.entrySet()) {
370 long block = entry.getValue().longValue();
371
372 if (block < currentBlock - depth) {
373 toremove.add(entry.getKey());
374 logger.trace(
375 "Clear outdated transaction, block.number: [{}] hash: [{}]",
376 block,
377 entry.getKey());
378 }
379 }
380
381 removeTransactionList(toremove);
382
383 if (timeout > 0) {
384 this.removeObsoleteTransactions(timestampSeconds - timeout);
385 }
386 }
387
388 @VisibleForTesting
389 public synchronized void removeObsoleteTransactions(long timeSeconds) {
390 List<Keccak256> toremove = new ArrayList<>();
391
392 for (Map.Entry<Keccak256, Long> entry : transactionTimes.entrySet()) {
393 long txtime = entry.getValue().longValue();
394
395 if (txtime <= timeSeconds) {
396 toremove.add(entry.getKey());
397 logger.trace(
398 "Clear outdated transaction, hash: [{}]",
399 entry.getKey());
400 }
401 }
402
403 removeTransactionList(toremove);
404 }
405
406 private void removeTransactionList(List<Keccak256> toremove) {
407 for (Keccak256 key : toremove) {
408 pendingTransactions.removeTransactionByHash(key);
409 queuedTransactions.removeTransactionByHash(key);
410
411 transactionBlocks.remove(key);
412 transactionTimes.remove(key);
413 }
414 }
415
416 @Override
417 public synchronized void removeTransactions(List<Transaction> txs) {
418 for (Transaction tx : txs) {
419 Keccak256 khash = tx.getHash();
420 pendingTransactions.removeTransactionByHash(khash);
421 queuedTransactions.removeTransactionByHash(khash);
422
423 logger.trace("Clear transaction, hash: [{}]", khash);
424 }
425 }
426
427 @Override
428 public synchronized List<Transaction> getPendingTransactions() {
429 removeObsoleteTransactions(this.outdatedThreshold, this.outdatedTimeout);
430 return Collections.unmodifiableList(pendingTransactions.getTransactions());
431 }
432
433 @Override
434 public synchronized List<Transaction> getQueuedTransactions() {
435 removeObsoleteTransactions(this.outdatedThreshold, this.outdatedTimeout);
436 List<Transaction> ret = new ArrayList<>();
437 ret.addAll(queuedTransactions.getTransactions());
438 return ret;
439 }
440
441 private void addQueuedTransaction(Transaction tx) {
442 this.queuedTransactions.addTransaction(tx);
443 }
444
445 private long getCurrentTimeInSeconds() {
446 return System.currentTimeMillis() / 1000;
447 }
448
449 private long getCurrentBestBlockNumber() {
450 if (bestBlock == null) {
451 return -1;
452 }
453
454 return bestBlock.getNumber();
455 }
456
457 private Block createFakePendingBlock(Block best) {
458 // creating fake lightweight calculated block with no hashes calculations
459 return blockFactory.newBlock(
460 blockFactory.getBlockHeaderBuilder()
461 .setParentHash(best.getHash().getBytes())
462 .setDifficulty(best.getDifficulty())
463 .setNumber(best.getNumber() + 1)
464 .setGasLimit(ByteUtil.longToBytesNoLeadZeroes(Long.MAX_VALUE))
465 .setTimestamp(best.getTimestamp() + 1)
466 .build()
467 ,
468 Collections.emptyList(),
469 Collections.emptyList()
470 );
471 }
472
473 private TransactionValidationResult shouldAcceptTx(Transaction tx, RepositorySnapshot currentRepository) {
474 AccountState state = currentRepository.getAccountState(tx.getSender(signatureCache));
475 return validator.isValid(tx, bestBlock, state);
476 }
477
478 /**
479 * @param newTx a transaction to be added to the pending list (nonce = last pending nonce + 1)
480 * @param currentRepository
481 * @return whether the sender balance is enough to pay for all pending transactions + newTx
482 */
483 private boolean senderCanPayPendingTransactionsAndNewTx(
484 Transaction newTx,
485 RepositorySnapshot currentRepository) {
486 List<Transaction> transactions = pendingTransactions.getTransactionsWithSender(newTx.getSender());
487
488 Coin accumTxCost = Coin.ZERO;
489 for (Transaction t : transactions) {
490 accumTxCost = accumTxCost.add(getTxBaseCost(t));
491 }
492
493 Coin costWithNewTx = accumTxCost.add(getTxBaseCost(newTx));
494 return costWithNewTx.compareTo(currentRepository.getBalance(newTx.getSender())) <= 0;
495 }
496
497 private Coin getTxBaseCost(Transaction tx) {
498 Coin gasCost = tx.getValue();
499 if (bestBlock == null || getTransactionCost(tx, bestBlock.getNumber()) > 0) {
500 BigInteger gasLimit = BigInteger.valueOf(GasCost.toGas(tx.getGasLimit()));
501 gasCost = gasCost.add(tx.getGasPrice().multiply(gasLimit));
502 }
503
504 return gasCost;
505 }
506
507 private long getTransactionCost(Transaction tx, long number) {
508 return tx.transactionCost(
509 config.getNetworkConstants(),
510 config.getActivationConfig().forBlock(number)
511 );
512 }
513 }