// Copyright (c) 2011-2016 The Cryptonote developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "gtest/gtest.h" #include "Transfers/BlockchainSynchronizer.h" #include "Transfers/TransfersSynchronizer.h" #include "INodeStubs.h" #include "TestBlockchainGenerator.h" #include "TransactionApiHelpers.h" #include "CryptoNoteCore/TransactionApi.h" #include #include #include #include using namespace CryptoNote; class TransfersObserver : public ITransfersObserver { public: virtual void onTransactionUpdated(ITransfersSubscription* object, const Hash& transactionHash) override { std::lock_guard lk(m_mutex); m_transfers.emplace_back(transactionHash); } std::vector m_transfers; std::mutex m_mutex; }; class TransfersApi : public ::testing::Test, public IBlockchainSynchronizerObserver { public: TransfersApi() : m_currency(CryptoNote::CurrencyBuilder(m_logger).currency()), generator(m_currency), m_node(generator), m_sync(m_node, m_currency.genesisBlockHash()), m_transfersSync(m_currency, m_sync, m_node) { } void addAccounts(size_t count) { while (count--) { m_accounts.push_back(generateAccountKeys()); } } void addPaymentAccounts(size_t count) { KeyPair p1; Crypto::generate_keys(p1.publicKey, p1.secretKey); auto viewKeys = p1; while (count--) { Crypto::generate_keys(p1.publicKey, p1.secretKey); m_accounts.push_back(accountKeysFromKeypairs(viewKeys, p1)); } } void addMinerAccount() { m_accounts.push_back(reinterpret_cast(generator.getMinerAccount())); } AccountSubscription createSubscription(size_t acc, uint64_t timestamp = 0) { const auto& keys = m_accounts[acc]; AccountSubscription sub; sub.keys = keys; sub.syncStart.timestamp = timestamp; sub.syncStart.height = 0; sub.transactionSpendableAge = 5; return sub; } void subscribeAccounts() { m_transferObservers.reset(new TransfersObserver[m_accounts.size()]); for (size_t i = 0; i < m_accounts.size(); ++i) { m_subscriptions.push_back(&m_transfersSync.addSubscription(createSubscription(i))); m_subscriptions.back()->addObserver(&m_transferObservers[i]); } } void startSync() { syncCompleted = std::promise(); syncCompletedFuture = syncCompleted.get_future(); m_sync.addObserver(this); m_sync.start(); syncCompletedFuture.get(); m_sync.removeObserver(this); } void refreshSync() { syncCompleted = std::promise(); syncCompletedFuture = syncCompleted.get_future(); m_sync.addObserver(this); m_sync.lastKnownBlockHeightUpdated(0); syncCompletedFuture.get(); m_sync.removeObserver(this); } void synchronizationCompleted(std::error_code result) override { decltype(syncCompleted) detachedPromise = std::move(syncCompleted); detachedPromise.set_value(result); } void generateMoneyForAccount(size_t idx) { generator.getBlockRewardForAddress( reinterpret_cast(m_accounts[idx].address)); } std::error_code submitTransaction(ITransactionReader& tx) { auto data = tx.getTransactionData(); Transaction outTx; CryptoNote::fromBinaryArray(outTx, data); std::promise result; m_node.relayTransaction(outTx, [&result](std::error_code ec) { std::promise detachedPromise = std::move(result); detachedPromise.set_value(ec); }); return result.get_future().get(); } protected: boost::scoped_array m_transferObservers; std::vector m_accounts; std::vector m_subscriptions; Logging::ConsoleLogger m_logger; CryptoNote::Currency m_currency; TestBlockchainGenerator generator; INodeTrivialRefreshStub m_node; BlockchainSynchronizer m_sync; TransfersSyncronizer m_transfersSync; std::promise syncCompleted; std::future syncCompletedFuture; }; namespace CryptoNote { inline bool operator == (const TransactionOutputInformation& t1, const TransactionOutputInformation& t2) { return t1.type == t2.type && t1.amount == t2.amount && t1.outputInTransaction == t2.outputInTransaction && t1.transactionPublicKey == t2.transactionPublicKey; } } TEST_F(TransfersApi, testSubscriptions) { addAccounts(1); m_transfersSync.addSubscription(createSubscription(0)); std::vector subscriptions; m_transfersSync.getSubscriptions(subscriptions); const auto& addr = m_accounts[0].address; ASSERT_EQ(1, subscriptions.size()); ASSERT_EQ(addr, subscriptions[0]); ASSERT_TRUE(m_transfersSync.getSubscription(addr) != 0); ASSERT_TRUE(m_transfersSync.removeSubscription(addr)); subscriptions.clear(); m_transfersSync.getSubscriptions(subscriptions); ASSERT_EQ(0, subscriptions.size()); } TEST_F(TransfersApi, syncOneBlock) { addAccounts(2); subscribeAccounts(); generator.getBlockRewardForAddress(m_accounts[0].address); generator.generateEmptyBlocks(15); startSync(); auto& tc1 = m_transfersSync.getSubscription(m_accounts[0].address)->getContainer(); auto& tc2 = m_transfersSync.getSubscription(m_accounts[1].address)->getContainer(); ASSERT_NE(&tc1, &tc2); ASSERT_GT(tc1.balance(ITransfersContainer::IncludeAll), 0); ASSERT_GT(tc1.transfersCount(), 0); ASSERT_EQ(0, tc2.transfersCount()); } TEST_F(TransfersApi, syncMinerAcc) { addMinerAccount(); subscribeAccounts(); generator.generateEmptyBlocks(10); startSync(); ASSERT_NE(0, m_subscriptions[0]->getContainer().transfersCount()); } namespace { std::unique_ptr createMoneyTransfer( uint64_t amount, uint64_t fee, const AccountKeys& senderKeys, const AccountPublicAddress& reciever, ITransfersContainer& tc) { std::vector transfers; tc.getOutputs(transfers, ITransfersContainer::IncludeAllUnlocked); auto tx = createTransaction(); std::vector> inputs; uint64_t foundMoney = 0; for (const auto& t : transfers) { TransactionTypes::InputKeyInfo info; info.amount = t.amount; TransactionTypes::GlobalOutput globalOut; globalOut.outputIndex = t.globalOutputIndex; globalOut.targetKey = t.outputKey; info.outputs.push_back(globalOut); info.realOutput.outputInTransaction = t.outputInTransaction; info.realOutput.transactionIndex = 0; info.realOutput.transactionPublicKey = t.transactionPublicKey; KeyPair kp; tx->addInput(senderKeys, info, kp); inputs.push_back(std::make_pair(info, kp)); foundMoney += info.amount; if (foundMoney >= amount + fee) { break; } } // output to reciever tx->addOutput(amount, reciever); // change uint64_t change = foundMoney - amount - fee; if (change) { tx->addOutput(change, senderKeys.address); } for (size_t inputIdx = 0; inputIdx < inputs.size(); ++inputIdx) { tx->signInputKey(inputIdx, inputs[inputIdx].first, inputs[inputIdx].second); } return tx; } } TEST_F(TransfersApi, moveMoney) { addMinerAccount(); addAccounts(2); subscribeAccounts(); generator.generateEmptyBlocks(2 * m_currency.minedMoneyUnlockWindow()); // sendAmount is an even number uint64_t sendAmount = (get_outs_money_amount(generator.getBlockchain()[1].baseTransaction) / 4) * 2; auto fee = m_currency.minimumFee(); startSync(); auto& tc0 = m_subscriptions[0]->getContainer(); ASSERT_LE(sendAmount, tc0.balance(ITransfersContainer::IncludeAllUnlocked)); auto tx = createMoneyTransfer(sendAmount, fee, m_accounts[0], m_accounts[1].address, tc0); submitTransaction(*tx); refreshSync(); ASSERT_EQ(1, m_transferObservers[1].m_transfers.size()); ASSERT_EQ(tx->getTransactionHash(), m_transferObservers[1].m_transfers[0]); auto& tc1 = m_subscriptions[1]->getContainer(); ASSERT_EQ(sendAmount, tc1.balance(ITransfersContainer::IncludeAll)); ASSERT_EQ(0, tc1.balance(ITransfersContainer::IncludeAllUnlocked)); generator.generateEmptyBlocks(m_currency.minedMoneyUnlockWindow()); // unlock money refreshSync(); ASSERT_EQ(sendAmount, tc1.balance(ITransfersContainer::IncludeAllUnlocked)); auto tx2 = createMoneyTransfer(sendAmount / 2, fee, m_accounts[1], m_accounts[2].address, tc1); submitTransaction(*tx2); refreshSync(); ASSERT_EQ(2, m_transferObservers[1].m_transfers.size()); ASSERT_EQ(tx2->getTransactionHash(), m_transferObservers[1].m_transfers.back()); ASSERT_EQ(sendAmount / 2 - fee, m_subscriptions[1]->getContainer().balance(ITransfersContainer::IncludeAll)); ASSERT_EQ(sendAmount / 2, m_subscriptions[2]->getContainer().balance(ITransfersContainer::IncludeAll)); } struct lessOutKey { bool operator()(const TransactionOutputInformation& t1, const TransactionOutputInformation& t2) { return std::hash()(t1.outputKey) < std::hash()(t2.outputKey); } }; bool compareStates(TransfersSyncronizer& sync1, TransfersSyncronizer& sync2) { std::vector subs; sync1.getSubscriptions(subs); for (const auto& s : subs) { auto& tc1 = sync1.getSubscription(s)->getContainer(); if (sync2.getSubscription(s) == nullptr) return false; auto& tc2 = sync2.getSubscription(s)->getContainer(); std::vector out1; std::vector out2; tc1.getOutputs(out1); tc2.getOutputs(out2); std::sort(out1.begin(), out1.end(), lessOutKey()); std::sort(out2.begin(), out2.end(), lessOutKey()); if (out1 != out2) return false; } return true; } TEST_F(TransfersApi, state) { addMinerAccount(); subscribeAccounts(); generator.generateEmptyBlocks(20); startSync(); m_sync.stop(); std::stringstream memstm; m_transfersSync.save(memstm); m_sync.start(); BlockchainSynchronizer bsync2(m_node, m_currency.genesisBlockHash()); TransfersSyncronizer sync2(m_currency, bsync2, m_node); for (size_t i = 0; i < m_accounts.size(); ++i) { sync2.addSubscription(createSubscription(i)); } sync2.load(memstm); // compare transfers ASSERT_TRUE(compareStates(m_transfersSync, sync2)); // generate more blocks generator.generateEmptyBlocks(10); refreshSync(); syncCompleted = std::promise(); syncCompletedFuture = syncCompleted.get_future(); bsync2.addObserver(this); bsync2.start(); syncCompletedFuture.get(); bsync2.removeObserver(this); // check again ASSERT_TRUE(compareStates(m_transfersSync, sync2)); } TEST_F(TransfersApi, sameTrackingKey) { size_t offset = 2; // miner account + ordinary account size_t paymentAddresses = 1000; size_t payments = 10; addMinerAccount(); addAccounts(1); addPaymentAccounts(paymentAddresses); subscribeAccounts(); for (size_t i = 0; i < payments; ++i) { generateMoneyForAccount(i + offset); } startSync(); for (size_t i = 0; i < payments; ++i) { auto sub = m_subscriptions[offset + i]; EXPECT_NE(0, sub->getContainer().balance(ITransfersContainer::IncludeAll)); } }