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 }