From 65cc0468398271805bc3764f710d3144210fa230 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Sun, 24 Aug 2014 11:59:47 +0200 Subject: [PATCH] Added functional tests --- src/bitcoinrpc.cpp | 190 +++++++++++++++ src/init.cpp | 8 + src/main.cpp | 56 +++++ src/main.h | 7 + src/rpcwallet.cpp | 3 + src/test/Gemfile | 7 + src/test/Gemfile.lock | 39 +++ src/test/containers/README.md | 43 ++++ src/test/containers/base/Dockerfile | 7 + src/test/containers/base_devel/Dockerfile | 8 + src/test/containers/boot.rb | 6 + src/test/containers/build_base | 9 + src/test/containers/build_net | 99 ++++++++ src/test/containers/build_seed | 57 +++++ src/test/containers/check_net_can_mint | 24 ++ src/test/containers/coin_container.rb | 227 ++++++++++++++++++ src/test/containers/init | 13 + src/test/containers/make | 6 + src/test/containers/node | 7 + .../containers/remove_peercoin_containers | 3 + src/test/containers/rpc | 15 ++ src/test/containers/show_log | 18 ++ src/test/cucumber | 4 + src/test/features/revert_duplicate.feature | 69 ++++++ src/test/features/step_definitions/common.rb | 118 +++++++++ .../step_definitions/revert_duplicate.rb | 7 + src/test/features/support/env.rb | 6 + src/test/features/support/helpers.rb | 28 +++ src/util.cpp | 9 + src/util.h | 4 + 30 files changed, 1097 insertions(+) create mode 100644 src/test/Gemfile create mode 100644 src/test/Gemfile.lock create mode 100644 src/test/containers/README.md create mode 100644 src/test/containers/base/Dockerfile create mode 100644 src/test/containers/base_devel/Dockerfile create mode 100644 src/test/containers/boot.rb create mode 100755 src/test/containers/build_base create mode 100755 src/test/containers/build_net create mode 100755 src/test/containers/build_seed create mode 100755 src/test/containers/check_net_can_mint create mode 100644 src/test/containers/coin_container.rb create mode 100755 src/test/containers/init create mode 100755 src/test/containers/make create mode 100755 src/test/containers/node create mode 100755 src/test/containers/remove_peercoin_containers create mode 100755 src/test/containers/rpc create mode 100755 src/test/containers/show_log create mode 100755 src/test/cucumber create mode 100644 src/test/features/revert_duplicate.feature create mode 100644 src/test/features/step_definitions/common.rb create mode 100644 src/test/features/step_definitions/revert_duplicate.rb create mode 100644 src/test/features/support/env.rb create mode 100644 src/test/features/support/helpers.rb diff --git a/src/bitcoinrpc.cpp b/src/bitcoinrpc.cpp index ad699c4167d..022acca786b 100644 --- a/src/bitcoinrpc.cpp +++ b/src/bitcoinrpc.cpp @@ -281,6 +281,187 @@ Value stop(const Array& params, bool fHelp) +#ifdef TESTING + +Value generatestake(const Array& params, bool fHelp) +{ + if (fHelp || params.size() > 1) + throw runtime_error( + "generatestake []\n" + "generate a single proof of stake block on top of (default: highest block hash)" + ); + + if (GetBoolArg("-stakegen", true)) + throw JSONRPCError(-3, "Stake generation enabled. Won't start another generation."); + + CBlockIndex *parent; + if (params.size() > 1) + { + uint256 parentHash; + parentHash.SetHex(params[1].get_str()); + if (!mapBlockIndex.count(parentHash)) + throw JSONRPCError(-3, "Parent hash not in main chain"); + parent = mapBlockIndex[parentHash]; + } + else + { + parent = pindexBest; + } + + BitcoinMiner(pwalletMain, true, true, parent); + return hashSingleStakeBlock.ToString(); +} + + +extern bool fRequestShutdown; +Value shutdown(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 0) + throw runtime_error( + "shutdown\n" + "close the program" + ); + + fRequestShutdown = true; + + return ""; +} + + +Value timetravel(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 1) + throw runtime_error( + "timetravel \n" + "change relative time" + ); + + nTimeShift += params[0].get_int(); + + return ""; +} + + +unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake); + +Value duplicateblock(const Array& params, bool fHelp) +{ + if (fHelp || params.size() < 1 || params.size() > 2) + throw runtime_error( + "duplicateblock []\n" + "propagate a new block with the same stake as block on top of (default: same parent)" + ); + + uint256 originalHash; + originalHash.SetHex(params[0].get_str()); + if (!mapBlockIndex.count(originalHash)) + throw JSONRPCError(-3, "Original hash not in main chain"); + CBlockIndex *original = mapBlockIndex[originalHash]; + + CBlockIndex *parent; + if (params.size() > 1) + { + uint256 parentHash; + parentHash.SetHex(params[1].get_str()); + if (!mapBlockIndex.count(parentHash)) + throw JSONRPCError(-3, "Parent hash not in main chain"); + parent = mapBlockIndex[parentHash]; + } + else + { + parent = original->pprev; + } + + CWallet *pwallet = pwalletMain; + CReserveKey reservekey(pwalletMain); + bool fProofOfStake = true; + unsigned int nExtraNonce = 0; + + auto_ptr pblock(new CBlock()); + if (!pblock.get()) + throw JSONRPCError(-3, "Unable to allocate block"); + + // Create coinbase tx + CTransaction txNew; + txNew.vin.resize(1); + txNew.vin[0].prevout.SetNull(); + txNew.vout.resize(1); + CPubKey reservepubkey; + if (!reservekey.GetReservedKey(reservepubkey)) + throw JSONRPCError(-3, "Unable to get reserve pub key"); + txNew.vout[0].scriptPubKey << reservepubkey << OP_CHECKSIG; + + // Add our coinbase tx as first transaction + pblock->vtx.push_back(txNew); + + // ppcoin: if coinstake available add coinstake tx + CBlockIndex* pindexPrev = parent; + IncrementExtraNonce(pblock.get(), pindexPrev, nExtraNonce); + + CBlock originalBlock; + originalBlock.ReadFromDisk(original); + CTransaction txCoinStake(originalBlock.vtx[1]); + + pblock->vtx[0].vout[0].SetEmpty(); + pblock->vtx[0].nTime = txCoinStake.nTime; + pblock->vtx.push_back(txCoinStake); + + pblock->nBits = GetNextTargetRequired(pindexPrev, pblock->IsProofOfStake()); + + // Fill in header + pblock->hashPrevBlock = pindexPrev->GetBlockHash(); + pblock->hashMerkleRoot = pblock->BuildMerkleTree(); + if (pblock->IsProofOfStake()) + pblock->nTime = pblock->vtx[1].nTime; //same as coinstake timestamp + pblock->nTime = max(pindexPrev->GetMedianTimePast()+1, pblock->GetMaxTransactionTime()); + pblock->nTime = max(pblock->GetBlockTime(), pindexPrev->GetBlockTime() - nMaxClockDrift); + if (pblock->IsProofOfWork()) + pblock->UpdateTime(pindexPrev); + pblock->nNonce = 0; + + if (fProofOfStake) + { + // ppcoin: if proof-of-stake block found then process block + if (pblock->IsProofOfStake()) + { + if (!pblock->SignBlock(*pwallet)) + { + // We ignore errors to be able to test duplicate blocks with invalid signature + } + + // Found a solution + { + LOCK(cs_main); + + // Remove key from key pool + reservekey.KeepKey(); + + // Track how many getdata requests this block gets + { + LOCK(pwallet->cs_wallet); + pwallet->mapRequestCount[pblock->GetHash()] = 0; + } + + // Process this block the same as if we had received it from another node + // But do not check for errors as this is expected to fail + CValidationState state; + ProcessBlock(state, NULL, pblock.get()); + } + } + else + throw JSONRPCError(-3, "generated block is not a Proof of Stake"); + } + + string result(pblock->GetHash().ToString()); + + pblock.release(); + + return result; +} + +#endif + + // // Call Table // @@ -361,6 +542,12 @@ static const CRPCCommand vRPCCommands[] = { "gettxout", &gettxout, true, false }, { "lockunspent", &lockunspent, false, false }, { "listlockunspent", &listlockunspent, false, false }, +#ifdef TESTING + { "generatestake", &generatestake, true, false }, + { "duplicateblock", &duplicateblock, true, false }, + { "shutdown", &shutdown, true, false }, + { "timetravel", &timetravel, true, false }, +#endif }; CRPCTable::CRPCTable() @@ -1295,6 +1482,9 @@ Array RPCConvertValues(const std::string &strMethod, const std::vector 1) ConvertTo(params[1]); if (strMethod == "importprivkey" && n > 2) ConvertTo(params[2]); +#ifdef TESTING + if (strMethod == "timetravel" && n > 0) ConvertTo(params[0]); +#endif return params; } diff --git a/src/init.cpp b/src/init.cpp index 82ce36a1566..876d2bd652b 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1076,6 +1076,11 @@ bool AppInit2(boost::thread_group& threadGroup) printf("mapWallet.size() = %"PRIszu"\n", pwalletMain->mapWallet.size()); printf("mapAddressBook.size() = %"PRIszu"\n", pwalletMain->mapAddressBook.size()); +#ifdef TESTING + if (mapArgs.count("-timetravel")) + nTimeShift = GetArg("-timetravel", 0); +#endif + StartNode(threadGroup); if (fServer) @@ -1085,6 +1090,9 @@ bool AppInit2(boost::thread_group& threadGroup) GenerateBitcoins(GetBoolArg("-gen", false), pwalletMain); // ppcoin: mint proof-of-stake blocks in the background +#ifdef TESTING + if (GetBoolArg("-stakegen", true)) +#endif MintStake(threadGroup, pwalletMain); // ********************************************************* Step 12: finished diff --git a/src/main.cpp b/src/main.cpp index bb4534be39f..59fe66513f8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -78,6 +78,10 @@ const string strMessageMagic = "PPCoin Signed Message:\n"; double dHashesPerSec = 0.0; int64 nHPSTimerStart = 0; +#ifdef TESTING +uint256 hashSingleStakeBlock; +#endif + // Settings int64 nTransactionFee = MIN_TX_FEE; @@ -1171,7 +1175,11 @@ const CBlockIndex* GetLastBlockIndex(const CBlockIndex* pindex, bool fProofOfSta return pindex; } +#ifdef TESTING +unsigned int GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake) +#else unsigned int static GetNextTargetRequired(const CBlockIndex* pindexLast, bool fProofOfStake) +#endif { if (pindexLast == NULL) return bnProofOfWorkLimit.GetCompact(); // genesis block @@ -3078,12 +3086,21 @@ bool LoadBlockIndex() if (fTestNet) { +#ifdef TESTING + hashGenesisBlock = uint256("00008d0d88095d31f6dbdbcf80f6e51f71adf2be15740301f5e05cc0f3b2d2c0"); + bnProofOfWorkLimit = CBigNum(~uint256(0) >> 15); + nStakeMinAge = 60 * 60 * 24; // test net min age is 1 day + nCoinbaseMaturity = 60; + bnInitialHashTarget = CBigNum(~uint256(0) >> 15); + nModifierInterval = 60 * 20; // test net modifier interval is 20 minutes +#else hashGenesisBlock = hashGenesisBlockTestNet; bnProofOfWorkLimit = CBigNum(~uint256(0) >> 28); nStakeMinAge = 60 * 60 * 24; // test net min age is 1 day nCoinbaseMaturity = 60; bnInitialHashTarget = CBigNum(~uint256(0) >> 29); nModifierInterval = 60 * 20; // test net modifier interval is 20 minutes +#endif } printf("%s Network: genesis=0x%s nBitsLimit=0x%08x nBitsInitial=0x%08x nStakeMinAge=%d nCoinbaseMaturity=%d nModifierInterval=%d\n", @@ -3141,6 +3158,18 @@ bool InitBlockIndex() { block.nNonce = 122894938; } +#ifdef TESTING + CBigNum bnTarget; + bnTarget.SetCompact(block.nBits); + while (block.GetHash() > bnTarget.getuint256()) + { + if (block.nNonce % 1048576 == 0) + printf("n=%dM hash=%s\n", block.nNonce / 1048576, + block.GetHash().ToString().c_str()); + block.nNonce++; + } +#endif + //// debug print uint256 hash = block.GetHash(); printf("%s\n", hash.ToString().c_str()); @@ -5025,7 +5054,11 @@ bool CheckWork(CBlock* pblock, CWallet& wallet, CReserveKey& reservekey) return true; } +#ifdef TESTING +void BitcoinMiner(CWallet *pwallet, bool fProofOfStake, bool fGenerateSingleBlock, CBlockIndex *parent) +#else void BitcoinMiner(CWallet *pwallet, bool fProofOfStake) +#endif { printf("CPUMiner started for proof-of-%s\n", fProofOfStake? "stake" : "work"); SetThreadPriority(THREAD_PRIORITY_LOWEST); @@ -5052,7 +5085,15 @@ void BitcoinMiner(CWallet *pwallet, bool fProofOfStake) // Create new block // unsigned int nTransactionsUpdatedLast = nTransactionsUpdated; +#ifdef TESTING + CBlockIndex* pindexPrev; + if (parent) + pindexPrev = parent; + else + pindexPrev = pindexBest; +#else CBlockIndex* pindexPrev = pindexBest; +#endif auto_ptr pblocktemplate(CreateNewBlock(reservekey, pwallet, fProofOfStake)); if (!pblocktemplate.get()) @@ -5072,9 +5113,20 @@ void BitcoinMiner(CWallet *pwallet, bool fProofOfStake) } strMintWarning = ""; printf("CPUMiner : proof-of-stake block found %s\n", pblock->GetHash().ToString().c_str()); +#ifdef TESTING + SetThreadPriority(THREAD_PRIORITY_NORMAL); + bool fSuccess = CheckWork(pblock, *pwalletMain, reservekey); + SetThreadPriority(THREAD_PRIORITY_LOWEST); + if (fSuccess && fGenerateSingleBlock) + { + hashSingleStakeBlock = pblock->GetHash(); + return; + } +#else SetThreadPriority(THREAD_PRIORITY_NORMAL); CheckWork(pblock, *pwalletMain, reservekey); SetThreadPriority(THREAD_PRIORITY_LOWEST); +#endif } MilliSleep(500); continue; @@ -5212,7 +5264,11 @@ void GenerateBitcoins(bool fGenerate, CWallet* pwallet) minerThreads = new boost::thread_group(); for (int i = 0; i < nThreads; i++) +#ifdef TESTING + minerThreads->create_thread(boost::bind(&BitcoinMiner, pwallet, false, false, (CBlockIndex*)NULL)); +#else minerThreads->create_thread(boost::bind(&BitcoinMiner, pwallet, false)); +#endif } // ppcoin: stake minter thread diff --git a/src/main.h b/src/main.h index adb5f87744d..2ecb3b95624 100644 --- a/src/main.h +++ b/src/main.h @@ -115,6 +115,9 @@ extern bool fBenchmark; extern int nScriptCheckThreads; extern bool fTxIndex; extern unsigned int nCoinCacheSize; +#ifdef TESTING +extern uint256 hashSingleStakeBlock; +#endif // Settings extern int64 nTransactionFee; @@ -196,7 +199,11 @@ bool IsInitialBlockDownload(); std::string GetWarnings(std::string strFor); uint256 WantedByOrphan(const CBlock* pblockOrphan); const CBlockIndex* GetLastBlockIndex(const CBlockIndex* pindex, bool fProofOfStake); +#ifdef TESTING +void BitcoinMiner(CWallet *pwallet, bool fProofOfStake, bool fGenerateSingleBlock = false, CBlockIndex* parent = NULL); +#else void BitcoinMiner(CWallet *pwallet, bool fProofOfStake); +#endif /** Retrieve a transaction (from memory pool, or from disk, if possible) */ bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock, bool fAllowSlow = false); /** Connect/disconnect blocks until pindexNew is the new tip of the active block chain */ diff --git a/src/rpcwallet.cpp b/src/rpcwallet.cpp index 84b21b695e9..dab9d2509d3 100644 --- a/src/rpcwallet.cpp +++ b/src/rpcwallet.cpp @@ -93,6 +93,9 @@ Value getinfo(const Array& params, bool fHelp) if (pwalletMain->IsCrypted()) obj.push_back(Pair("unlocked_until", (boost::int64_t)nWalletUnlockTime / 1000)); obj.push_back(Pair("errors", GetWarnings("statusbar"))); +#ifdef TESTING + obj.push_back(Pair("time", DateTimeStrFormat(GetAdjustedTime()))); +#endif return obj; } diff --git a/src/test/Gemfile b/src/test/Gemfile new file mode 100644 index 00000000000..4054eff5070 --- /dev/null +++ b/src/test/Gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gem 'docker-api', require: 'docker' +gem 'httparty' + +gem 'cucumber' +gem 'rspec-expectations' diff --git a/src/test/Gemfile.lock b/src/test/Gemfile.lock new file mode 100644 index 00000000000..d5cf2e73e9c --- /dev/null +++ b/src/test/Gemfile.lock @@ -0,0 +1,39 @@ +GEM + remote: https://rubygems.org/ + specs: + archive-tar-minitar (0.5.2) + builder (3.2.2) + cucumber (1.3.16) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12) + multi_json (>= 1.7.5, < 2.0) + multi_test (>= 0.1.1) + diff-lcs (1.2.5) + docker-api (1.13.2) + archive-tar-minitar + excon (>= 0.38.0) + json + excon (0.39.5) + gherkin (2.12.2) + multi_json (~> 1.3) + httparty (0.13.1) + json (~> 1.8) + multi_xml (>= 0.5.2) + json (1.8.1) + multi_json (1.10.1) + multi_test (0.1.1) + multi_xml (0.5.5) + rspec-expectations (3.0.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.0.0) + rspec-support (3.0.4) + +PLATFORMS + ruby + +DEPENDENCIES + cucumber + docker-api + httparty + rspec-expectations diff --git a/src/test/containers/README.md b/src/test/containers/README.md new file mode 100644 index 00000000000..f222a7d0ff0 --- /dev/null +++ b/src/test/containers/README.md @@ -0,0 +1,43 @@ +To run the functional tests made with cucumber you need: + +* Ruby >= 1.9 +* The "bundler" gem (once you've installed Ruby you can run "gem install bundler") +* Docker >= 1.0 (installed so that you can use it without sudo) + +On Ubuntu 14.04 you can install all this with these commands: + + sudo apt-get install ruby ruby-dev + sudo gem install bundler --no-rdoc --no-ri + + sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9 + sudo sh -c "echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" + sudo apt-get update + sudo apt-get install lxc-docker + sudo gpasswd -a ${USER} docker + sudo chown ${USER} /var/run/docker.sock + +To prepare all the containers, just run: + + src/test/containers/init -j4 + +It will build the containers and install the Ruby gems with bundler. If anything fails, ask for support. + +Then to run the tests, run this: + + src/test/containers/make -j4 && src/test/cucumber + +You can adjust the -j flag appropriately. + + +If you changed blockchain properties and must restart it, run this to rebuild the seed image and the sample network node images: + + src/test/containers/build_seed + src/test/containers/build_net + +When you run the tests, the containers are not removed so that you can inspect them. + +You can use src/test/containers/show_log to display the log of the last container that used a specific image. + +When you're done working, you should remove all containers with: + + src/test/containers/remove_peercoin_containers diff --git a/src/test/containers/base/Dockerfile b/src/test/containers/base/Dockerfile new file mode 100644 index 00000000000..d2ab9ad8ee2 --- /dev/null +++ b/src/test/containers/base/Dockerfile @@ -0,0 +1,7 @@ +FROM ubuntu:14.04 +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 8842CE5E +RUN echo deb http://ppa.launchpad.net/bitcoin/bitcoin/ubuntu trusty main >/etc/apt/sources.list.d/bitcoin.list +RUN apt-get update +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections +RUN apt-get install -y libboost-filesystem1.54.0 libboost-program-options1.54.0 libboost-thread1.54.0 +RUN apt-get install -y libdb4.8++ diff --git a/src/test/containers/base_devel/Dockerfile b/src/test/containers/base_devel/Dockerfile new file mode 100644 index 00000000000..9fa30e52b96 --- /dev/null +++ b/src/test/containers/base_devel/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu:14.04 +RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 8842CE5E +RUN echo deb http://ppa.launchpad.net/bitcoin/bitcoin/ubuntu trusty main >/etc/apt/sources.list.d/bitcoin.list +RUN apt-get update +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections +RUN apt-get install -y git build-essential g++ +RUN apt-get install -y libboost-all-dev libdb4.8++-dev libqrencode-dev +RUN apt-get install -y libssl-dev diff --git a/src/test/containers/boot.rb b/src/test/containers/boot.rb new file mode 100644 index 00000000000..3854b77cd1f --- /dev/null +++ b/src/test/containers/boot.rb @@ -0,0 +1,6 @@ +Dir.chdir File.expand_path('..', __FILE__) + +require 'rubygems' +require 'bundler/setup' + +require './coin_container' diff --git a/src/test/containers/build_base b/src/test/containers/build_base new file mode 100755 index 00000000000..d5fa712c0c0 --- /dev/null +++ b/src/test/containers/build_base @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +cd $(dirname $0)/base +docker build -t peercoin/base . + +cd ../base_devel +docker build -t peercoin/devel . diff --git a/src/test/containers/build_net b/src/test/containers/build_net new file mode 100755 index 00000000000..be959bc6a29 --- /dev/null +++ b/src/test/containers/build_net @@ -0,0 +1,99 @@ +#!/usr/bin/env ruby + +require File.expand_path('../boot', __FILE__) + +puts "Starting seed" +seed = CoinContainer.new( + image: 'peercoin/seed', + args: { + "timetravel" => -60*24*3600, + } +) +seed.wait_for_boot + +node_names = %w( a b c d e ) +puts "Starting #{node_names.size} nodes: #{node_names.inspect}" +nodes = node_names.map do |name| + CoinContainer.new( + image: 'peercoin/node', + links: [seed.name], + delete_at_exit: true, + args: { + "timetravel" => -60*24*3600, + } + ) +end + +all = [seed] + nodes + +amount = 100_000 +amount_string = amount.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse +outputs = 100 + +nodes.each_with_index do |node, i| + node.wait_for_boot + + address = node.rpc("getaccountaddress", "") + + amount_per_output = (amount / outputs).to_i + puts "Sending #{amount_string} to node #{node_names[i]} in #{outputs} transactions of #{amount_per_output}" + outputs.times do + seed.rpc("sendtoaddress", address, amount_per_output) + end + seed.generate_stake +end + +puts "Waiting for confirmations" +loop do + all.each { |n| n.rpc("timetravel", 60) } + seed.generate_stake + balances = nodes.map { |n| n.rpc("getbalance") } + puts "Balances: #{balances.inspect}" + if balances.all? { |b| b == amount } + break + end +end + + +nModifierInterval=60*20 +count = 64 +puts "Building next #{count} blocks at nModifierInterval intervals (#{nModifierInterval} seconds)" +count.times do |i| + all.each { |n| n.rpc("timetravel", nModifierInterval) } + seed.generate_stake + print "\r%2d/%2d" % [i+1, count] + STDOUT.flush +end +puts + +puts "Building 10 more blocks at 1 minute intervals" +10.times do |i| + all.each { |n| n.rpc("timetravel", 60) } + seed.generate_stake + print "\r%2d/%2d" % [i+1, 10] + STDOUT.flush +end +puts + +seed_height = seed.block_count + +puts "Waiting for all nodes to sync at seed height (#{seed_height})" +loop do + heights = nodes.map(&:block_count) + p heights + break if heights.all? { |h| h == seed_height } + sleep 1 +end + +puts "Shutting down seed" +seed.shutdown + +nodes.each_with_index do |node, i| + name = node_names[i] + image_name = "peercoinnet/#{name}" + puts "Creating image #{image_name}" + node.shutdown + node.wait_for_shutdown + node.commit(image_name) +end + diff --git a/src/test/containers/build_seed b/src/test/containers/build_seed new file mode 100755 index 00000000000..d26961ca94a --- /dev/null +++ b/src/test/containers/build_seed @@ -0,0 +1,57 @@ +#!/usr/bin/env ruby + +require File.expand_path('../boot', __FILE__) + +# Move to genesis time +shift = (Time.at(1345090000) - Time.now).to_i + 24 * 3600 + +puts "Creating seed" +seed = CoinContainer.new( + image: 'peercoin/base', + delete_at_exit: false, + args: { + "timetravel" => shift, + }, +) + +puts "Creating node" +node = CoinContainer.new( + image: 'peercoin/base', + links: [seed], + remove_wallet_after_shutdown: true, + delete_at_exit: true, + args: { + "timetravel" => shift, + }, +) + +puts "Waiting for node to boot" +node.wait_for_boot + +nodes = [seed, node] + +seed.rpc "setgenerate", true + +max = 500 +puts "Waiting for #{max} blocks" +loop do + count = node.rpc("getblockcount") + if count.to_i > 0 + print "\r%3d/%3d (seed balance: %s)" % [count, max, seed.rpc("getbalance")] + STDOUT.flush + end + if count.to_i >= max + puts + break + end + nodes.each { |n| n.rpc("timetravel", 60 * 60) } + sleep 1 +end + +puts "Building images peercoin/seed and peercoin/node" +seed.shutdown +node.shutdown +seed.wait_for_shutdown +node.wait_for_shutdown +seed.commit('peercoin/seed') +node.commit('peercoin/node') diff --git a/src/test/containers/check_net_can_mint b/src/test/containers/check_net_can_mint new file mode 100755 index 00000000000..7fe2bdd8521 --- /dev/null +++ b/src/test/containers/check_net_can_mint @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby + +require File.expand_path('../boot', __FILE__) + +node_names = %w( a b c d e ) +nodes = [] +node_names.each do |name| + puts "Starting node #{name}" + nodes << CoinContainer.new(image: "peercoinnet/#{name}", links: nodes.map(&:name), args: {stakegen: true}) +end + +puts "Waiting for all nodes to boot" +nodes.each(&:wait_for_boot) + +puts "Moving 30 days forward" +nodes.each do |node| + node.rpc "timetravel", 30*24*3600 +end + +loop do + puts "Node block heights: #{nodes.map(&:block_count).inspect}" + sleep 1 +end + diff --git a/src/test/containers/coin_container.rb b/src/test/containers/coin_container.rb new file mode 100644 index 00000000000..69f843ebec6 --- /dev/null +++ b/src/test/containers/coin_container.rb @@ -0,0 +1,227 @@ +require 'docker' +require 'httparty' + +class CoinContainer + def create(options = {}) + default_options = { + image: "peercoin/base", + shutdown_at_exit: true, + delete_at_exit: false, + remove_addr_after_shutdown: true, + remove_wallet_after_shutdown: false, + } + + options = default_options.merge(options) + + links = options[:links] + case links + when Hash + links = links.map { |link_name, alias_name| [link_name, alias_name] } + when Array + links = links.map do |n| + name = case n + when String then n + when CoinContainer then n.name + else raise "Unknown link: #{n.inspect}" + end + [name, name.sub(/^\//, '')] + end + when nil + links = [] + else + raise "Invalid links" + end + name = options[:name] + + connects = links.map do |linked_name, alias_name| + upname = alias_name.upcase + "-addnode=$#{upname}_PORT_9903_TCP_ADDR:$#{upname}_PORT_9903_TCP_PORT" + end + + default_args = { + testnet: true, + printtoconsole: true, + rpcuser: 'bob', + rpcpassword: 'bar', + rpcallowip: '*.*.*.*', + logtimestamps: true, + keypool: 1, + stakegen: false, + dnsseed: false, + } + + args = default_args.merge(options[:args] || {}) + + cmd_args = args.map do |key, value| + case value + when true + "-#{key}" + when false + "-#{key}=0" + when Numeric + "-#{key}=#{value}" + else + "-#{key}=\"#{value}\"" + end + end + cmd_args += connects + + bash_cmd = "" + + if options[:show_environment] + bash_cmd += "echo Environment:; env; " + end + + bash_cmd += "./ppcoind " + cmd_args.join(" ") + + if options[:remove_addr_after_shutdown] + bash_cmd += "; rm /.ppcoin/testnet/addr.dat" + end + + if options[:remove_wallet_after_shutdown] + bash_cmd += "; rm /.ppcoin/testnet/wallet.dat" + end + + command = [ + "stdbuf", "-oL", "-eL", + '/bin/bash', '-c', + bash_cmd, + ] + + create_options = { + 'Image' => options[:image], + 'WorkingDir' => '/code', + 'Tty' => true, + 'Cmd' => command, + 'ExposedPorts' => { + "9903/tcp" => {}, + "9904/tcp" => {}, + }, + 'name' => name, + } + node_container = Docker::Container.create(create_options) + + if options[:shutdown_at_exit] + at_exit do + shutdown rescue nil + end + end + if options[:delete_at_exit] + at_exit do + container.delete(force: true) + end + end + + node_container.start( + 'Binds' => ["#{File.expand_path('../../..', __FILE__)}:/code"], + 'PortBindings' => { + "9904/tcp" => ['127.0.0.1'], + "9903/tcp" => ['127.0.0.1'], + }, + 'Links' => links.map { |link_name, alias_name| "#{link_name}:#{alias_name}" }, + ) + + @container = node_container + end + + def load_data + @json = @container.json + @name = @json["Name"] + + ports = @json["NetworkSettings"]["Ports"] + if ports.nil? + raise "Unable to get port. Usualy this means the daemon process failed to start." + end + port = ports["9904/tcp"].first["HostPort"].to_i + @rpc_port = port + @port= ports["9903/tcp"].first["HostPort"].to_i + end + + def initialize(options = {}) + if id = options[:id] + get(id) + else + create(options) + end + load_data + end + + def get(id) + @container = Docker::Container.get(id) + end + + attr_reader :rpc_port, :port, :container, :name + + def json + container.json + end + + def id + @json["Id"] + end + + def rpc(method, *params) + data = { + method: method, + params: params, + id: 'jsonrpc', + } + url = "http://localhost:#{rpc_port}/" + auth = { + username: "bob", + password: "bar", + } + response = HTTParty.post url, body: data.to_json, headers: { 'Content-Type' => 'application/json' }, basic_auth: auth + result = JSON.parse(response.body) + raise result.inspect if result["error"] + result["result"] + end + + def wait_for_boot + begin + rpc("getinfo") + rescue Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError, Errno::EPIPE + sleep 0.1 + retry + end + end + + def commit(repo) + image = container.commit + image.tag 'repo' => repo + end + + def shutdown + rpc("shutdown") + end + + def wait_for_shutdown + container.wait + end + + def block_count + rpc("getblockcount").to_i + end + + def generate_stake(parent = nil) + rpc("generatestake", *[parent].compact) + end + + def top_hash + rpc("getblockhash", rpc("getblockcount")) + end + + def connection_count + rpc("getconnectioncount").to_i + end + + def info + rpc "getinfo" + end + + def new_address(account = "") + rpc "getnewaddress", account + end +end + + diff --git a/src/test/containers/init b/src/test/containers/init new file mode 100755 index 00000000000..2787de6fdd7 --- /dev/null +++ b/src/test/containers/init @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e +set -x + +cd $(dirname $0) +./build_base +./make "$@" +cd .. +bundle install +containers/build_seed +containers/build_net +./cucumber diff --git a/src/test/containers/make b/src/test/containers/make new file mode 100755 index 00000000000..3e521556c8d --- /dev/null +++ b/src/test/containers/make @@ -0,0 +1,6 @@ +#!/bin/bash + +cd $(dirname $0)/../.. + +set -x +docker run -v $PWD/..:/code -w /code/src peercoin/devel make -f makefile.unix CXXFLAGS="-O2 -DTESTING" DEBUGFLAGS=-ggdb USE_UPNP=- "$@" diff --git a/src/test/containers/node b/src/test/containers/node new file mode 100755 index 00000000000..08f879c61b6 --- /dev/null +++ b/src/test/containers/node @@ -0,0 +1,7 @@ +#!/bin/bash + +cd $(dirname $0)/../.. + +set -x +docker run -v $PWD/..:/code -p '0.0.0.0::9903' -p '0.0.0.0::9904' -w /code/src --tty peercoin/base ./ppcoind "$@" + diff --git a/src/test/containers/remove_peercoin_containers b/src/test/containers/remove_peercoin_containers new file mode 100755 index 00000000000..dfe0e69bac2 --- /dev/null +++ b/src/test/containers/remove_peercoin_containers @@ -0,0 +1,3 @@ +#!/bin/bash + +docker ps -a | tail -n +1 | egrep ' (peercoin|peercoinnet)/' | awk '{print $1}' | xargs docker rm -f diff --git a/src/test/containers/rpc b/src/test/containers/rpc new file mode 100755 index 00000000000..acafe0a74af --- /dev/null +++ b/src/test/containers/rpc @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +require File.expand_path('../boot', __FILE__) +require 'json' + +if ARGV.size != 2 + puts "usage: #$0 " + exit 1 +end + +id = ARGV[0] +command = ARGV[1..-1] + +container = CoinContainer.new(id: id) +p container.rpc(*command) diff --git a/src/test/containers/show_log b/src/test/containers/show_log new file mode 100755 index 00000000000..683ea7ef697 --- /dev/null +++ b/src/test/containers/show_log @@ -0,0 +1,18 @@ +#!/bin/bash + +name="$1" + +if [ -z "$name" ]; then + echo "usage: $0 " + echo "Will display the log of the last container that used image " + echo "Example: $0 peercoinnet/a" + exit 1 +fi + +ID=$(docker ps -a | tail -n +1 | grep "$name" | head -n 1 | awk '{print $1}') +if [ -z "$ID" ]; then + echo "No container found" + exit 1 +fi + +docker logs $ID |less diff --git a/src/test/cucumber b/src/test/cucumber new file mode 100755 index 00000000000..97aa4bfc512 --- /dev/null +++ b/src/test/cucumber @@ -0,0 +1,4 @@ +#!/bin/bash + +cd $(dirname $0) +bundle exec cucumber "$@" diff --git a/src/test/features/revert_duplicate.feature b/src/test/features/revert_duplicate.feature new file mode 100644 index 00000000000..4bea4e05331 --- /dev/null +++ b/src/test/features/revert_duplicate.feature @@ -0,0 +1,69 @@ +Feature: The top block is removed if a duplicate stake is received + + Scenario: A node reuses the stake of the top block in another block + Given a network with nodes "A", "B" and "C" able to mint + When node "A" finds a block "X" + Then all nodes should be at block "X" + When node "B" finds a block "Y" + Then all nodes should be at block "Y" + When node "B" sends a duplicate "Y2" of block "Y" + Then all nodes should be at block "X" + When node "C" finds a block "T" + Then all nodes should be at block "T" + + + Scenario: A node builds a block on top of a block that was removed because of duplication + Given a network with nodes "A", "B" and "C" able to mint + When node "A" finds a block "X" + Then all nodes should be at block "X" + When node "B" finds a block "Y" + Then all nodes should be at block "Y" + When node "B" sends a duplicate "Y2" of block "Y" + Then all nodes should be at block "X" + When node "C" finds a block "Z" on top of block "Y2" + Then all nodes should be at block "Z" + + + Scenario: Transactions unconfirmed by duplicate removal gets confirmed again in the next block + Given a network with nodes "A", "B" and "C" able to mint + When node "A" finds a block "X" + Then all nodes should be at block "X" + + When node "B" generates a new address "addr" + And node "C" sends "1000" to "addr" through transaction "tx" + Then all nodes should have 1 transaction in memory pool + + When node "A" finds a block "Y" + Then all nodes should be at block "Y" + And transaction "tx" on node "C" should have 1 confirmation + And all nodes should have 0 transactions in memory pool + + When node "A" sends a duplicate "Y2" of block "Y" + Then all nodes should be at block "X" + And transaction "tx" on node "C" should have 0 confirmations + And all nodes should have 1 transaction in memory pool + + When node "A" finds a block "Z" + Then all nodes should be at block "Z" + And transaction "tx" on node "C" should have 1 confirmations + And all nodes should have 0 transactions in memory pool + + + Scenario: A node sends a block with a duplicate stake of an already confirmed block + Given a network with nodes "A", "B" and "C" able to mint + When node "A" finds a block "X" + Then all nodes should be at block "X" + When node "B" finds a block "Y" + Then all nodes should be at block "Y" + When node "A" sends a duplicate "X2" of block "X" + Then all nodes should be at block "Y" + + + Scenario: A node sends a block with a duplicate stake but without a valid signature because it doesn't have the associated private key + Given a network with nodes "A", "B" and "C" able to mint + When node "A" finds a block "X" + Then all nodes should be at block "X" + When node "B" finds a block "Y" + Then all nodes should be at block "Y" + When node "C" sends a duplicate "Y2" of block "Y" + Then all nodes should be at block "Y" diff --git a/src/test/features/step_definitions/common.rb b/src/test/features/step_definitions/common.rb new file mode 100644 index 00000000000..580af7e4ea9 --- /dev/null +++ b/src/test/features/step_definitions/common.rb @@ -0,0 +1,118 @@ +Before do + @blocks = {} + @addresses = {} + @nodes = {} + @tx = {} +end + +Given(/^a network with nodes? (.+) able to mint$/) do |node_names| + node_names = node_names.scan(/"(.*?)"/).map(&:first) + available_nodes = %w( a b c d e ) + raise "More than #{available_nodes.size} nodes not supported" if node_names.size > available_nodes.size + @nodes = {} + + node_names.each_with_index do |name, i| + options = { + image: "peercoinnet/#{available_nodes[i]}", + links: @nodes.values.map(&:name), + args: { + debug: true, + timetravel: 30*24*3600, + }, + } + node = CoinContainer.new(options) + @nodes[name] = node + node.wait_for_boot + end + + wait_for(10) do + @nodes.values.all? do |node| + count = node.connection_count + count == @nodes.size - 1 + end + end + wait_for do + @nodes.values.map do |node| + count = node.block_count + count + end.uniq.size == 1 + end +end + +After do + if @nodes + require 'thread' + @nodes.values.reverse.map do |node| + Thread.new do + node.shutdown + #node.wait_for_shutdown + #begin + # node.container.delete(force: true) + #rescue + #end + end + end.each(&:join) + end +end + +When(/^node "(.*?)" finds a block "([^"]*?)"$/) do |node, block| + @blocks[block] = @nodes[node].generate_stake +end + +When(/^node "(.*?)" finds a block$/) do |node| + @nodes[node].generate_stake +end + +Then(/^all nodes should be at block "(.*?)"$/) do |block| + begin + wait_for do + main = @nodes.values.map(&:top_hash) + main.all? { |hash| hash == @blocks[block] } + end + rescue + raise "Not at block #{block}: #{@nodes.values.map(&:top_hash).map { |hash| @blocks.key(hash) }.inspect}" + end +end + +Given(/^all nodes reach the same height$/) do + wait_for do + expect(@nodes.values.map(&:block_count).uniq.size).to eq(1) + end +end + +When(/^node "(.*?)" sends "(.*?)" to "([^"]*?)" in transaction "(.*?)"$/) do |arg1, arg2, arg3, arg4| + @tx[arg4] = @nodes[arg1].rpc "sendtoaddress", @addresses[arg3], parse_number(arg2) +end + +When(/^node "(.*?)" sends "(.*?)" to "([^"]*?)"$/) do |arg1, arg2, arg3| + @nodes[arg1].rpc "sendtoaddress", @addresses[arg3], parse_number(arg2) +end + +When(/^node "(.*?)" finds a block received by all other nodes$/) do |arg1| + node = @nodes[arg1] + block = node.generate_stake + wait_for do + main = @nodes.values.map(&:top_hash) + main.all? { |hash| hash == block } + end +end + +Given(/^node "(.*?)" generates a new address "(.*?)"$/) do |arg1, arg2| + @addresses[arg2] = @nodes[arg1].rpc("getnewaddress") +end + +When(/^node "(.*?)" sends "(.*?)" to "(.*?)" through transaction "(.*?)"$/) do |arg1, arg2, arg3, arg4| + @tx[arg4] = @nodes[arg1].rpc "sendtoaddress", @addresses[arg3], arg2.to_f +end + +Then(/^transaction "(.*?)" on node "(.*?)" should have (\d+) confirmations?$/) do |arg1, arg2, arg3| + wait_for do + expect(@nodes[arg2].rpc("gettransaction", @tx[arg1])["confirmations"]).to eq(arg3.to_i) + end +end + +Then(/^all nodes should (?:have|reach) (\d+) transactions? in memory pool$/) do |arg1| + wait_for do + expect(@nodes.values.map { |node| node.rpc("getmininginfo")["pooledtx"] }).to eq(@nodes.map { arg1.to_i }) + end +end diff --git a/src/test/features/step_definitions/revert_duplicate.rb b/src/test/features/step_definitions/revert_duplicate.rb new file mode 100644 index 00000000000..ac28f540eca --- /dev/null +++ b/src/test/features/step_definitions/revert_duplicate.rb @@ -0,0 +1,7 @@ +When(/^node "(.*?)" sends a duplicate "(.*?)" of block "(.*?)"$/) do |node, duplicate, original| + @blocks[duplicate] = @nodes[node].rpc("duplicateblock", @blocks[original]) +end + +When(/^node "(.*?)" finds a block "(.*?)" on top of block "(.*?)"$/) do |node, block, parent| + @blocks[block] = @nodes[node].generate_stake(@blocks[parent]) +end diff --git a/src/test/features/support/env.rb b/src/test/features/support/env.rb new file mode 100644 index 00000000000..c8240c6497e --- /dev/null +++ b/src/test/features/support/env.rb @@ -0,0 +1,6 @@ + +require 'rubygems' +require 'bundler/setup' + +require File.expand_path('../../../containers/coin_container', __FILE__) + diff --git a/src/test/features/support/helpers.rb b/src/test/features/support/helpers.rb new file mode 100644 index 00000000000..af8774ed53b --- /dev/null +++ b/src/test/features/support/helpers.rb @@ -0,0 +1,28 @@ +require 'timeout' +def wait_for(timeout = 5) + last_exception = nil + begin + Timeout.timeout(timeout) do + loop do + begin + break if yield + rescue RSpec::Expectations::ExpectationNotMetError, RuntimeError => e + last_exception = e + end + sleep 0.1 + end + end + rescue Timeout::Error + if last_exception + raise last_exception + else + raise + end + end +end + +def parse_number(n) + n.gsub(',', '').to_f +end + + diff --git a/src/util.cpp b/src/util.cpp index 77fcc53616a..ae4f4f6a06c 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -92,6 +92,11 @@ bool fCachedPath[2] = {false, false}; // Init OpenSSL library multithreading support static CCriticalSection** ppmutexOpenSSL; + +#ifdef TESTING +int64 nTimeShift = 0; +#endif + void locking_callback(int mode, int i, const char* file, int line) { if (mode & CRYPTO_LOCK) { @@ -1316,7 +1321,11 @@ int64 GetTime() { if (nMockTime) return nMockTime; +#ifdef TESTING + return time(NULL) + nTimeShift; +#else return time(NULL); +#endif } void SetMockTime(int64 nMockTimeIn) diff --git a/src/util.h b/src/util.h index 3907437eb25..d3f24c9462c 100644 --- a/src/util.h +++ b/src/util.h @@ -160,6 +160,10 @@ extern bool fNoListen; extern bool fLogTimestamps; extern volatile bool fReopenDebugLog; +#ifdef TESTING +extern int64 nTimeShift; +#endif + void RandAddSeed(); void RandAddSeedPerfmon(); int ATTR_WARN_PRINTF(1,2) OutputDebugStringF(const char* pszFormat, ...);