// 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 "EventWaiter.h" #include "Logging/ConsoleLogger.h" #include "Transfers/BlockchainSynchronizer.h" #include "Transfers/TransfersSynchronizer.h" #include "INodeStubs.h" #include "TestBlockchainGenerator.h" #include "TransactionApiHelpers.h" #include "CryptoNoteCore/TransactionApi.h" #include "WalletLegacy/WalletLegacy.h" #include #include #include using namespace CryptoNote; /* class TransfersObserver : public ITransfersObserver { public: virtual void onTransactionUpdated(ITransfersSubscription* object, const Hash& transactionHash, uint64_t amountIn, uint64_t amountOut) override { std::lock_guard lk(m_mutex); m_transfers.push_back(std::make_pair(transactionHash, amountIn - amountOut)); } std::vector> m_transfers; std::mutex m_mutex; }; */ class INodeStubWithPoolTx : public INodeTrivialRefreshStub { public: INodeStubWithPoolTx(TestBlockchainGenerator& generator) : INodeTrivialRefreshStub(generator), detached(false) {} void relayTransaction(const CryptoNote::Transaction& transaction, const Callback& callback) override { std::unique_lock lk(mutex); relayedTxs.push_back(std::make_pair(this->getLastLocalBlockHeight(), transaction)); lk.unlock(); INodeTrivialRefreshStub::relayTransaction(transaction, callback); } void startAlternativeChain(uint32_t height) override { std::unique_lock lk(mutex); INodeTrivialRefreshStub::startAlternativeChain(height); detachHeight = height; detached = true; } void getPoolSymmetricDifference(std::vector&& known_pool_tx_ids, Crypto::Hash known_block_id, bool& is_bc_actual, std::vector>& new_txs, std::vector& deleted_tx_ids, const Callback& callback) override { std::unique_lock lk(mutex); std::sort(relayedTxs.begin(), relayedTxs.end(), [](const std::pair& val1, const std::pair& val2)->bool {return val1.first < val2.first; }); is_bc_actual = true; if (detached) { size_t i = 0; for (; i < relayedTxs.size(); ++i) { if (relayedTxs[i].first >= detachHeight) { break; } } for (; i < relayedTxs.size(); ++i) { new_txs.push_back(CryptoNote::createTransactionPrefix(relayedTxs[i].second)); } } lk.unlock(); callback(std::error_code()); }; std::vector> relayedTxs; uint32_t detachHeight; bool detached; std::mutex mutex; }; class WalletSendObserver : public CryptoNote::IWalletLegacyObserver { public: WalletSendObserver() {} bool waitForSendEnd(std::error_code& ec) { if (!sent.wait_for(std::chrono::milliseconds(5000))) return false; ec = sendResult; return true; } virtual void sendTransactionCompleted(CryptoNote::TransactionId transactionId, std::error_code result) override { sendResult = result; sent.notify(); } std::error_code sendResult; EventWaiter sent; }; class DetachTest : public ::testing::Test, public IBlockchainSynchronizerObserver { public: DetachTest() : 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 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 generateMoneyForAccount(size_t idx) { generator.getBlockRewardForAddress( reinterpret_cast(m_accounts[idx].address)); } std::error_code submitTransaction(ITransactionReader& tx) { auto data = tx.getTransactionData(); CryptoNote::BinaryArray txblob(data.data(), data.data() + data.size()); CryptoNote::Transaction outTx; fromBinaryArray(outTx, data); std::promise result; std::future future = result.get_future(); m_node.relayTransaction(outTx, [&result](std::error_code ec) { std::promise promise = std::move(result); promise.set_value(ec); }); return future.get(); } void synchronizationCompleted(std::error_code result) override { decltype(syncCompleted) detachedPromise = std::move(syncCompleted); detachedPromise.set_value(result); } protected: std::vector m_accounts; std::vector m_subscriptions; Logging::ConsoleLogger m_logger; CryptoNote::Currency m_currency; TestBlockchainGenerator generator; INodeStubWithPoolTx 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; } } 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(DetachTest, testBlockchainDetach) { uint64_t sendAmount = 70000000000000; auto fee = m_currency.minimumFee(); addMinerAccount(); addAccounts(2); subscribeAccounts(); generator.generateEmptyBlocks(20); syncCompleted = std::promise(); syncCompletedFuture = syncCompleted.get_future(); m_sync.addObserver(this); m_sync.start(); syncCompletedFuture.get(); m_sync.removeObserver(this); auto& tc = m_subscriptions[0]->getContainer(); tc.balance(); ASSERT_LE(sendAmount, tc.balance(ITransfersContainer::IncludeAllUnlocked)); auto tx = createMoneyTransfer(sendAmount, fee, m_accounts[0], m_accounts[1].address, tc); submitTransaction(*tx); syncCompleted = std::promise(); syncCompletedFuture = syncCompleted.get_future(); m_sync.addObserver(this); m_node.updateObservers(); syncCompletedFuture.get(); m_sync.removeObserver(this); auto& tc2 = m_subscriptions[1]->getContainer(); ASSERT_EQ(sendAmount, tc2.balance(ITransfersContainer::IncludeAll)); ASSERT_EQ(0, tc2.balance(ITransfersContainer::IncludeAllUnlocked)); ASSERT_EQ(1, tc2.transactionsCount()); std::vector unconfirmed; tc2.getUnconfirmedTransactions(unconfirmed); ASSERT_EQ(0, unconfirmed.size()); m_node.startAlternativeChain(m_node.getLastLocalBlockHeight() - 1); generator.generateEmptyBlocks(2); syncCompleted = std::promise(); syncCompletedFuture = syncCompleted.get_future(); m_sync.addObserver(this); m_node.updateObservers(); syncCompletedFuture.get(); m_sync.removeObserver(this); auto& tc3 = m_subscriptions[1]->getContainer(); ASSERT_EQ(sendAmount, tc3.balance(ITransfersContainer::IncludeAll)); ASSERT_EQ(0, tc3.balance(ITransfersContainer::IncludeAllUnlocked)); ASSERT_EQ(1, tc3.transactionsCount()); tc3.getUnconfirmedTransactions(unconfirmed); ASSERT_EQ(1, unconfirmed.size()); ASSERT_EQ(reinterpret_cast(unconfirmed[0]), tx->getTransactionHash()); m_sync.stop(); } struct CompletionWalletObserver : public IWalletLegacyObserver { virtual void synchronizationCompleted(std::error_code result) override { decltype(syncCompleted) detachedPromise = std::move(syncCompleted); detachedPromise.set_value(result); } std::promise syncCompleted; std::future syncCompletedFuture; }; struct WaitForExternalTransactionObserver : public CryptoNote::IWalletLegacyObserver { public: WaitForExternalTransactionObserver() {} std::promise promise; virtual void externalTransactionCreated(CryptoNote::TransactionId transactionId) override { decltype(promise) detachedPromise = std::move(promise); detachedPromise.set_value(transactionId); } }; TEST_F(DetachTest, testDetachWithWallet) { auto fee = m_currency.minimumFee(); generator.generateEmptyBlocks(5); WalletLegacy Alice(m_currency, m_node); WalletLegacy Bob(m_currency, m_node); CompletionWalletObserver AliceCompleted, BobCompleted; AliceCompleted.syncCompleted = std::promise(); AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future(); BobCompleted.syncCompleted = std::promise(); BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future(); Alice.addObserver(&AliceCompleted); Bob.addObserver(&BobCompleted); Alice.initAndGenerate("pass"); Bob.initAndGenerate("pass"); AliceCompleted.syncCompletedFuture.get(); BobCompleted.syncCompletedFuture.get(); Alice.removeObserver(&AliceCompleted); Bob.removeObserver(&BobCompleted); AccountKeys AliceKeys; Alice.getAccountKeys(AliceKeys); generator.getBlockRewardForAddress(AliceKeys.address); generator.generateEmptyBlocks(10); AliceCompleted.syncCompleted = std::promise(); AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future(); BobCompleted.syncCompleted = std::promise(); BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future(); Alice.addObserver(&AliceCompleted); Bob.addObserver(&BobCompleted); m_node.updateObservers(); AliceCompleted.syncCompletedFuture.get(); BobCompleted.syncCompletedFuture.get(); Alice.removeObserver(&AliceCompleted); Bob.removeObserver(&BobCompleted); ASSERT_EQ(0, Alice.pendingBalance()); ASSERT_NE(0, Alice.actualBalance()); CryptoNote::WalletLegacyTransfer tr; tr.amount = Alice.actualBalance() / 2; tr.address = Bob.getAddress(); WalletSendObserver wso; Alice.addObserver(&wso); Alice.sendTransaction(tr, fee); std::error_code sendError; wso.waitForSendEnd(sendError); Alice.removeObserver(&wso); ASSERT_FALSE(sendError); WaitForExternalTransactionObserver etxo; auto externalTxFuture = etxo.promise.get_future(); Bob.addObserver(&etxo); AliceCompleted.syncCompleted = std::promise(); AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future(); BobCompleted.syncCompleted = std::promise(); BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future(); Alice.addObserver(&AliceCompleted); Bob.addObserver(&BobCompleted); auto expectedTransactionBlockHeight = m_node.getLastLocalBlockHeight(); generator.generateEmptyBlocks(1); //unlock bob's pending money m_node.updateObservers(); AliceCompleted.syncCompletedFuture.get(); BobCompleted.syncCompletedFuture.get(); Alice.removeObserver(&AliceCompleted); Bob.removeObserver(&BobCompleted); auto txId = externalTxFuture.get(); Bob.removeObserver(&etxo); WalletLegacyTransaction txInfo; Bob.getTransaction(txId, txInfo); ASSERT_EQ(txInfo.blockHeight, expectedTransactionBlockHeight); ASSERT_EQ(txInfo.totalAmount, tr.amount); ASSERT_EQ(Bob.pendingBalance(), 0); ASSERT_EQ(Bob.actualBalance(), tr.amount); m_node.startAlternativeChain(txInfo.blockHeight - 1); generator.generateEmptyBlocks(2); //sync Bob AliceCompleted.syncCompleted = std::promise(); AliceCompleted.syncCompletedFuture = AliceCompleted.syncCompleted.get_future(); BobCompleted.syncCompleted = std::promise(); BobCompleted.syncCompletedFuture = BobCompleted.syncCompleted.get_future(); Alice.addObserver(&AliceCompleted); Bob.addObserver(&BobCompleted); m_node.updateObservers(); AliceCompleted.syncCompletedFuture.get(); BobCompleted.syncCompletedFuture.get(); Alice.removeObserver(&AliceCompleted); Bob.removeObserver(&BobCompleted); Bob.getTransaction(txId, txInfo); ASSERT_EQ(txInfo.blockHeight, WALLET_LEGACY_UNCONFIRMED_TRANSACTION_HEIGHT); ASSERT_EQ(txInfo.totalAmount, tr.amount); ASSERT_EQ(Bob.pendingBalance(), tr.amount); ASSERT_EQ(Bob.actualBalance(), 0); }