/*
* 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.longrun;
import com.typesafe.config.ConfigFactory;
import org.apache.commons.lang3.mutable.MutableObject;
import org.ethereum.config.CommonConfig;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.AccountState;
import org.ethereum.core.Block;
import org.ethereum.core.BlockSummary;
import org.ethereum.core.Repository;
import org.ethereum.core.Transaction;
import org.ethereum.core.TransactionExecutor;
import org.ethereum.core.TransactionReceipt;
import org.ethereum.db.ContractDetails;
import org.ethereum.db.RepositoryImpl;
import org.ethereum.facade.Ethereum;
import org.ethereum.facade.EthereumFactory;
import org.ethereum.listener.EthereumListener;
import org.ethereum.listener.EthereumListenerAdapter;
import org.ethereum.sync.SyncManager;
import org.ethereum.util.FastByteComparisons;
import org.ethereum.vm.program.invoke.ProgramInvokeFactory;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static java.lang.Thread.sleep;
/**
* Regular sync with load
* Loads ethereumJ during sync with various onBlock/repo track/callback usages
*
* Runs sync with defined config for 1-30 minutes
* - checks State Trie is not broken
* - checks whether all blocks are in blockstore, validates parent connection and bodies
* - checks and validate transaction receipts
* Stopped, than restarts in 1 minute, syncs and pass all checks again.
* Repeats forever or until first error occurs
*
* Run with '-Dlogback.configurationFile=longrun/logback.xml' for proper logging
* Also following flags are available:
* -Dreset.db.onFirstRun=true
* -Doverride.config.res=longrun/conf/live.conf
*/
@Ignore
public class SyncWithLoadTest {
private Ethereum regularNode;
private final static CountDownLatch errorLatch = new CountDownLatch(1);
private static AtomicBoolean isRunning = new AtomicBoolean(true);
private static AtomicBoolean firstRun = new AtomicBoolean(true);
private static final Logger testLogger = LoggerFactory.getLogger("TestLogger");
private static final MutableObject<String> configPath = new MutableObject<>("longrun/conf/ropsten-noprune.conf");
private static final MutableObject<Boolean> resetDBOnFirstRun = new MutableObject<>(null);
// Timer stops while not syncing
private static final AtomicLong lastImport = new AtomicLong();
private static final int LAST_IMPORT_TIMEOUT = 10 * 60 * 1000;
public SyncWithLoadTest() throws Exception {
String resetDb = System.getProperty("reset.db.onFirstRun");
String overrideConfigPath = System.getProperty("override.config.res");
if (Boolean.parseBoolean(resetDb)) {
resetDBOnFirstRun.setValue(true);
} else if (resetDb != null && resetDb.equalsIgnoreCase("false")) {
resetDBOnFirstRun.setValue(false);
}
if (overrideConfigPath != null) configPath.setValue(overrideConfigPath);
statTimer.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// Adds error if no successfully imported blocks for LAST_IMPORT_TIMEOUT
long currentMillis = System.currentTimeMillis();
if (lastImport.get() != 0 && currentMillis - lastImport.get() > LAST_IMPORT_TIMEOUT) {
testLogger.error("No imported block for {} seconds", LAST_IMPORT_TIMEOUT / 1000);
fatalErrors.incrementAndGet();
}
try {
if (fatalErrors.get() > 0) {
statTimer.shutdownNow();
errorLatch.countDown();
}
} catch (Throwable t) {
SyncWithLoadTest.testLogger.error("Unhandled exception", t);
}
if (lastImport.get() == 0 && isRunning.get()) lastImport.set(currentMillis);
if (lastImport.get() != 0 && !isRunning.get()) lastImport.set(0);
}
}, 0, 15, TimeUnit.SECONDS);
}
/**
* Spring configuration class for the Regular peer
*/
private static class RegularConfig {
@Bean
public RegularNode node() {
return new RegularNode();
}
/**
* Instead of supplying properties via config file for the peer
* we are substituting the corresponding bean which returns required
* config for this instance.
*/
@Bean
public SystemProperties systemProperties() {
SystemProperties props = new SystemProperties();
props.overrideParams(ConfigFactory.parseResources(configPath.getValue()));
if (firstRun.get() && resetDBOnFirstRun.getValue() != null) {
props.setDatabaseReset(resetDBOnFirstRun.getValue());
}
return props;
}
}
/**
* Just regular EthereumJ node
*/
static class RegularNode extends BasicNode {
@Autowired
ProgramInvokeFactory programInvokeFactory;
@Autowired
SyncManager syncManager;
/**
* The main EthereumJ callback.
*/
EthereumListener blockListener = new EthereumListenerAdapter() {
@Override
public void onBlock(BlockSummary blockSummary) {
lastImport.set(System.currentTimeMillis());
}
@Override
public void onBlock(Block block, List<TransactionReceipt> receipts) {
for (TransactionReceipt receipt : receipts) {
// Getting contract details
byte[] contractAddress = receipt.getTransaction().getContractAddress();
if (contractAddress != null) {
ContractDetails details = ((Repository) ethereum.getRepository()).getContractDetails(contractAddress);
assert FastByteComparisons.equal(details.getAddress(), contractAddress);
}
// Getting AccountState for sender in the past
Random rnd = new Random();
Block bestBlock = ethereum.getBlockchain().getBestBlock();
Block randomBlock = ethereum.getBlockchain().getBlockByNumber(rnd.nextInt((int) bestBlock.getNumber()));
byte[] sender = receipt.getTransaction().getSender();
AccountState senderState = ((Repository) ethereum.getRepository()).getSnapshotTo(randomBlock.getStateRoot()).getAccountState(sender);
if (senderState != null) senderState.getBalance();
// Getting receiver's nonce somewhere in the past
Block anotherRandomBlock = ethereum.getBlockchain().getBlockByNumber(rnd.nextInt((int) bestBlock.getNumber()));
byte[] receiver = receipt.getTransaction().getReceiveAddress();
if (receiver != null) {
((Repository) ethereum.getRepository()).getSnapshotTo(anotherRandomBlock.getStateRoot()).getNonce(receiver);
}
}
}
@Override
public void onPendingTransactionsReceived(List<Transaction> transactions) {
Random rnd = new Random();
Block bestBlock = ethereum.getBlockchain().getBestBlock();
for (Transaction tx : transactions) {
Block block = ethereum.getBlockchain().getBlockByNumber(rnd.nextInt((int) bestBlock.getNumber()));
Repository repository = ((Repository) ethereum.getRepository())
.getSnapshotTo(block.getStateRoot())
.startTracking();
try {
TransactionExecutor executor = new TransactionExecutor
(tx, block.getCoinbase(), repository, ethereum.getBlockchain().getBlockStore(),
programInvokeFactory, block, new EthereumListenerAdapter(), 0)
.withCommonConfig(commonConfig)
.setLocalCall(true);
executor.init();
executor.execute();
executor.go();
executor.finalization();
executor.getReceipt();
} finally {
repository.rollback();
}
}
}
};
public RegularNode() {
super("sampleNode");
}
@Override
public void run() {
try {
ethereum.addListener(blockListener);
// Run 1-30 minutes
Random generator = new Random();
int i = generator.nextInt(30) + 1;
testLogger.info("Running for {} minutes until stop and check", i);
sleep(i * 60_000);
// Stop syncing
syncPool.close();
syncManager.close();
} catch (Exception ex) {
testLogger.error("Error occurred during run: ", ex);
}
if (syncComplete) {
testLogger.info("[v] Sync complete! The best block: " + bestBlock.getShortDescr());
}
fullSanityCheck(ethereum, commonConfig);
isRunning.set(false);
}
}
private final static AtomicInteger fatalErrors = new AtomicInteger(0);
private static ScheduledExecutorService statTimer =
Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread(r, "StatTimer");
}
});
private static boolean logStats() {
testLogger.info("---------====---------");
testLogger.info("fatalErrors: {}", fatalErrors);
testLogger.info("---------====---------");
return fatalErrors.get() == 0;
}
private static void fullSanityCheck(Ethereum ethereum, CommonConfig commonConfig) {
BlockchainValidation.fullCheck(ethereum, commonConfig, fatalErrors);
logStats();
firstRun.set(false);
}
@Test
public void testDelayedCheck() throws Exception {
runEthereum();
new Thread(new Runnable() {
@Override
public void run() {
try {
while(firstRun.get()) {
sleep(1000);
}
testLogger.info("Stopping first run");
while(true) {
while(isRunning.get()) {
sleep(1000);
}
regularNode.close();
testLogger.info("Run stopped");
sleep(10_000);
testLogger.info("Starting next run");
runEthereum();
isRunning.set(true);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}).start();
errorLatch.await();
if (!logStats()) assert false;
}
public void runEthereum() throws Exception {
testLogger.info("Starting EthereumJ regular instance!");
this.regularNode = EthereumFactory.createEthereum(RegularConfig.class);
}
}