/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library 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.
*
* The ethereumJ library 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 the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.sync;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.ethereum.core.*;
import org.ethereum.crypto.HashUtil;
import org.ethereum.datasource.DataSourceArray;
import org.ethereum.db.DbFlushManager;
import org.ethereum.db.IndexedBlockStore;
import org.ethereum.db.TransactionStore;
import org.ethereum.net.eth.handler.Eth63;
import org.ethereum.net.server.Channel;
import org.ethereum.util.FastByteComparisons;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.CountDownLatch;
/**
* Created by Anton Nashatyrev on 27.10.2016.
*/
@Component
@Scope("prototype")
public class ReceiptsDownloader {
private final static Logger logger = LoggerFactory.getLogger("sync");
@Autowired
SyncPool syncPool;
@Autowired
IndexedBlockStore blockStore;
@Autowired
DbFlushManager dbFlushManager;
@Autowired
TransactionStore txStore;
@Autowired @Qualifier("headerSource")
DataSourceArray<BlockHeader> headerStore;
long fromBlock, toBlock;
Set<Long> completedBlocks = new HashSet<>();
long t;
int cnt;
Thread retrieveThread;
private CountDownLatch stopLatch = new CountDownLatch(1);
public ReceiptsDownloader(long fromBlock, long toBlock) {
this.fromBlock = fromBlock;
this.toBlock = toBlock;
}
public void startImporting() {
retrieveThread = new Thread("FastsyncReceiptsFetchThread") {
@Override
public void run() {
retrieveLoop();
}
};
retrieveThread.start();
}
private List<List<byte[]>> getToDownload(int maxAskSize, int maxAsks) {
List<byte[]> toDownload = getToDownload(maxAskSize * maxAsks);
List<List<byte[]>> ret = new ArrayList<>();
for (int i = 0; i < toDownload.size(); i += maxAskSize) {
ret.add(toDownload.subList(i, Math.min(toDownload.size(), i + maxAskSize)));
}
return ret;
}
private synchronized List<byte[]> getToDownload(int maxSize) {
List<byte[]> ret = new ArrayList<>();
for (long i = fromBlock; i < toBlock && maxSize > 0; i++) {
if (!completedBlocks.contains(i)) {
BlockHeader header = headerStore.get((int) i);
// Skipping download for blocks with no transactions
if (FastByteComparisons.equal(header.getReceiptsRoot(), HashUtil.EMPTY_TRIE_HASH)) {
finalizeBlock(header.getNumber());
continue;
}
ret.add(header.getHash());
maxSize--;
}
}
return ret;
}
private void processDownloaded(byte[] blockHash, List<TransactionReceipt> receipts) {
Block block = blockStore.getBlockByHash(blockHash);
if (block.getNumber() >= fromBlock && validate(block, receipts) && !completedBlocks.contains(block.getNumber())) {
for (int i = 0; i < receipts.size(); i++) {
TransactionReceipt receipt = receipts.get(i);
TransactionInfo txInfo = new TransactionInfo(receipt, block.getHash(), i);
txInfo.setTransaction(block.getTransactionsList().get(i));
txStore.put(txInfo);
}
finalizeBlock(block.getNumber());
}
}
private void finalizeBlock(Long blockNumber) {
synchronized (this) {
completedBlocks.add(blockNumber);
while (fromBlock < toBlock && completedBlocks.remove(fromBlock)) fromBlock++;
if (fromBlock >= toBlock) finishDownload();
cnt++;
if (cnt % 1000 == 0) logger.info("FastSync: downloaded receipts for " + cnt + " blocks.");
}
dbFlushManager.commit();
}
private boolean validate(Block block, List<TransactionReceipt> receipts) {
byte[] receiptsRoot = BlockchainImpl.calcReceiptsTrie(receipts);
return FastByteComparisons.equal(receiptsRoot, block.getReceiptsRoot());
}
private void retrieveLoop() {
List<List<byte[]>> toDownload = Collections.emptyList();
while (!Thread.currentThread().isInterrupted()) {
try {
if (toDownload.isEmpty()) {
toDownload = getToDownload(100, 20);
}
Channel idle = getAnyPeer();
if (idle != null) {
final List<byte[]> list = toDownload.remove(0);
ListenableFuture<List<List<TransactionReceipt>>> future =
((Eth63) idle.getEthHandler()).requestReceipts(list);
if (future != null) {
Futures.addCallback(future, new FutureCallback<List<List<TransactionReceipt>>>() {
@Override
public void onSuccess(List<List<TransactionReceipt>> result) {
for (int i = 0; i < result.size(); i++) {
processDownloaded(list.get(i), result.get(i));
}
}
@Override
public void onFailure(Throwable t) {}
});
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
} catch (Exception e) {
logger.warn("Unexpected during receipts downloading", e);
}
}
}
/**
* Download could block chain synchronization occupying all peers
* Prevents this by leaving one peer without work
* Fallbacks to any peer when low number of active peers available
*/
Channel getAnyPeer() {
return syncPool.getActivePeersCount() > 2 ? syncPool.getNotLastIdle() : syncPool.getAnyIdle();
}
public int getDownloadedBlocksCount() {
return cnt;
}
public void stop() {
retrieveThread.interrupt();
stopLatch.countDown();
}
public void waitForStop() {
try {
stopLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
protected void finishDownload() {
stop();
}
}