// 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 "Globals.h" #include "CryptoNoteCore/Account.h" #include "CryptoNoteCore/CryptoNoteFormatUtils.h" #include "CryptoNoteCore/CryptoNoteTools.h" #include "CryptoNoteCore/TransactionApi.h" #include "Transfers/TransfersSynchronizer.h" #include "Transfers/BlockchainSynchronizer.h" #include #include #include #include #include "../IntegrationTestLib/TestWalletLegacy.h" using namespace CryptoNote; using namespace Crypto; using namespace Tests::Common; class IInterruptable { public: virtual void interrupt() = 0; }; class WalletLegacyObserver : public IWalletLegacyObserver { public: virtual void actualBalanceUpdated(uint64_t actualBalance) override { std::cout << "Actual balance updated = " << currency.formatAmount(actualBalance) << std::endl; m_actualBalance = actualBalance; m_sem.notify(); } virtual void sendTransactionCompleted(TransactionId transactionId, std::error_code result) override { std::cout << "Transaction sent, result = " << result << std::endl; } std::atomic m_actualBalance; Tests::Common::Semaphore m_sem; }; class TransactionConsumer : public IBlockchainConsumer { public: TransactionConsumer() { syncStart.timestamp = time(nullptr); syncStart.height = 0; } virtual SynchronizationStart getSyncStart() override { return syncStart; } virtual void onBlockchainDetach(uint32_t height) override { std::lock_guard lk(m_mutex); auto it = m_transactions.lower_bound(height); m_transactions.erase(it, m_transactions.end()); } virtual bool onNewBlocks(const CompleteBlock* blocks, uint32_t startHeight, uint32_t count) override { std::lock_guard lk(m_mutex); for(size_t i = 0; i < count; ++i) { for (const auto& tx : blocks[i].transactions) { m_transactions[startHeight + i].insert(tx->getTransactionHash()); } } m_cv.notify_all(); return true; } bool waitForTransaction(const Hash& txHash) { std::unique_lock lk(m_mutex); while (!hasTransaction(txHash)) { m_cv.wait_for(lk, std::chrono::seconds(1)); } return true; } std::error_code onPoolUpdated(const std::vector>& addedTransactions, const std::vector& deletedTransactions) override { //stub return std::error_code(); } const std::unordered_set& getKnownPoolTxIds() const override { //stub static std::unordered_set empty; return empty; } std::error_code addUnconfirmedTransaction(const ITransactionReader& /*transaction*/) override { throw std::runtime_error("Not implemented"); } void removeUnconfirmedTransaction(const Crypto::Hash& /*transactionHash*/) override { throw std::runtime_error("Not implemented"); } virtual void addObserver(IBlockchainConsumerObserver* observer) override { //stub } virtual void removeObserver(IBlockchainConsumerObserver* observer) override { //stub } private: bool hasTransaction(const Hash& txHash) { for (const auto& kv : m_transactions) { if (kv.second.count(txHash) > 0) return true; } return false; } std::mutex m_mutex; std::condition_variable m_cv; std::map> m_transactions; SynchronizationStart syncStart; }; class TransfersObserver : public ITransfersObserver, public IInterruptable { public: virtual void onTransactionUpdated(ITransfersSubscription* object, const Hash& transactionHash) override { { std::lock_guard lk(m_mutex); m_transfers.push_back(transactionHash); auto key = object->getAddress().spendPublicKey; std::string address = Common::toHex(&key, sizeof(key)); LOG_DEBUG("Transfer to " + address); } m_cv.notify_all(); } bool waitTransfer() { std::unique_lock lk(m_mutex); size_t prevSize = m_transfers.size(); while (!m_interrupted && m_transfers.size() == prevSize) { m_cv.wait_for(lk, std::chrono::seconds(10)); } return true; } bool waitTransactionTransfer(const Hash& transactionHash) { std::unique_lock lk(m_mutex); while (!m_interrupted) { auto it = std::find(m_transfers.begin(), m_transfers.end(), transactionHash); if (it == m_transfers.end()) { m_cv.wait_for(lk, std::chrono::seconds(10)); } else { m_transfers.erase(it); break; } } return true; } private: bool hasTransaction(const Hash& transactionHash) { return std::find(m_transfers.begin(), m_transfers.end(), transactionHash) != m_transfers.end(); } void interrupt() override { std::lock_guard lock(m_mutex); m_interrupted = true; m_cv.notify_all(); } private: std::mutex m_mutex; std::condition_variable m_cv; std::vector m_transfers; bool m_interrupted = false; }; class AccountGroup { public: enum { TRANSACTION_SPENDABLE_AGE = 5 }; AccountGroup(ITransfersSynchronizer& sync) : m_sync(sync) {} void generateAccounts(size_t count) { CryptoNote::AccountBase acc; while (count--) { acc.generate(); AccountSubscription sub; sub.keys = reinterpret_cast(acc.getAccountKeys()); sub.syncStart.timestamp = 0; sub.syncStart.height = 0; sub.transactionSpendableAge = TRANSACTION_SPENDABLE_AGE; m_accounts.push_back(sub); m_addresses.push_back(currency.accountAddressAsString(acc)); } } void subscribeAll() { m_observers.reset(new TransfersObserver[m_accounts.size()]); for (size_t i = 0; i < m_accounts.size(); ++i) { m_sync.addSubscription(m_accounts[i]).addObserver(&m_observers[i]); } } std::vector getAddresses() { std::vector addr; for (const auto& acc : m_accounts) { addr.push_back(acc.keys.address); } return addr; } ITransfersContainer& getTransfers(size_t idx) { return m_sync.getSubscription(m_accounts[idx].keys.address)->getContainer(); } std::vector m_accounts; std::vector m_addresses; ITransfersSynchronizer& m_sync; std::unique_ptr m_observers; }; class MultisignatureTest : public TransfersTest { public: virtual void SetUp() override { launchTestnet(2); } }; template class FutureGuard { public: FutureGuard(std::future&& f) : m_future(std::move(f)) { } ~FutureGuard() { if (m_future.valid()) { try { m_future.get(); } catch (...) { } } } R get() { return m_future.get(); } private: std::future m_future; }; class Interrupter { public: Interrupter(IInterruptable& interrpuptable) : m_interrpuptable(interrpuptable) { } ~Interrupter() { if (!m_cancelled) { m_interrpuptable.interrupt(); } } void cancel() { m_cancelled = true; } private: IInterruptable& m_interrpuptable; bool m_cancelled = false; }; TEST_F(TransfersTest, base) { uint64_t TRANSFER_AMOUNT; currency.parseAmount("500000.5", TRANSFER_AMOUNT); launchTestnet(2); std::unique_ptr node1; std::unique_ptr node2; nodeDaemons[0]->makeINode(node1); nodeDaemons[1]->makeINode(node2); CryptoNote::AccountBase dstAcc; dstAcc.generate(); AccountKeys dstKeys = reinterpret_cast(dstAcc.getAccountKeys()); BlockchainSynchronizer blockSync(*node2.get(), currency.genesisBlockHash()); TransfersSyncronizer transferSync(currency, blockSync, *node2.get()); TransfersObserver transferObserver; WalletLegacyObserver walletObserver; AccountSubscription sub; sub.syncStart.timestamp = 0; sub.syncStart.height = 0; sub.keys = dstKeys; sub.transactionSpendableAge = 5; ITransfersSubscription& transferSub = transferSync.addSubscription(sub); ITransfersContainer& transferContainer = transferSub.getContainer(); transferSub.addObserver(&transferObserver); Tests::Common::TestWalletLegacy wallet1(m_dispatcher, m_currency, *node1); ASSERT_FALSE(static_cast(wallet1.init())); wallet1.wallet()->addObserver(&walletObserver); ASSERT_TRUE(mineBlocks(*nodeDaemons[0], wallet1.address(), 1)); ASSERT_TRUE(mineBlocks(*nodeDaemons[0], wallet1.address(), currency.minedMoneyUnlockWindow())); wallet1.waitForSynchronizationToHeight(static_cast(2 + currency.minedMoneyUnlockWindow())); // start syncing and wait for a transfer FutureGuard waitFuture(std::async(std::launch::async, [&transferObserver] { return transferObserver.waitTransfer(); })); Interrupter transferObserverInterrupter(transferObserver); blockSync.start(); Hash txId; ASSERT_FALSE(static_cast(wallet1.sendTransaction(currency.accountAddressAsString(dstAcc), TRANSFER_AMOUNT, txId))); ASSERT_TRUE(mineBlocks(*nodeDaemons[0], wallet1.address(), 1)); ASSERT_TRUE(waitFuture.get()); transferObserverInterrupter.cancel(); std::cout << "Received transfer: " << currency.formatAmount(transferContainer.balance(ITransfersContainer::IncludeAll)) << std::endl; ASSERT_EQ(TRANSFER_AMOUNT, transferContainer.balance(ITransfersContainer::IncludeAll)); ASSERT_GT(transferContainer.getTransactionOutputs(txId, ITransfersContainer::IncludeAll).size(), 0); blockSync.stop(); } std::unique_ptr createTransferToMultisignature( ITransfersContainer& tc, // money source uint64_t amount, uint64_t fee, const AccountKeys& senderKeys, const std::vector& recipients, uint32_t requiredSignatures) { std::vector transfers; tc.getOutputs(transfers, ITransfersContainer::IncludeAllUnlocked | ITransfersContainer::IncludeStateSoftLocked); 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 receiver tx->addOutput(amount, recipients, requiredSignatures); // 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; } std::error_code submitTransaction(INode& node, ITransactionReader& tx) { auto data = tx.getTransactionData(); CryptoNote::Transaction outTx; fromBinaryArray(outTx, data); LOG_DEBUG("Submitting transaction " + Common::toHex(tx.getTransactionHash().data, 32)); std::promise result; node.relayTransaction(outTx, [&result](std::error_code ec) { result.set_value(ec); }); auto err = result.get_future().get(); if (err) { LOG_DEBUG("Error: " + err.message()); } else { LOG_DEBUG("Submitted successfully"); } return err; } std::unique_ptr createTransferFromMultisignature( AccountGroup& consilium, const AccountPublicAddress& receiver, const Hash& txHash, uint64_t amount, uint64_t fee) { auto& tc = consilium.getTransfers(0); std::vector transfers = tc.getTransactionOutputs(txHash, ITransfersContainer::IncludeTypeMultisignature | ITransfersContainer::IncludeStateSoftLocked | ITransfersContainer::IncludeStateUnlocked); EXPECT_FALSE(transfers.empty()); const TransactionOutputInformation& out = transfers[0]; auto tx = createTransaction(); MultisignatureInput msigInput; msigInput.amount = out.amount; msigInput.outputIndex = out.globalOutputIndex; msigInput.signatureCount = out.requiredSignatures; tx->addInput(msigInput); tx->addOutput(amount, receiver); uint64_t change = out.amount - amount - fee; tx->addOutput(change, consilium.getAddresses(), out.requiredSignatures); for (size_t i = 0; i < out.requiredSignatures; ++i) { tx->signInputMultisignature(0, out.transactionPublicKey, out.outputInTransaction, consilium.m_accounts[i].keys); } return tx; } TEST_F(MultisignatureTest, createMulitisignatureTransaction) { std::unique_ptr node1; std::unique_ptr node2; nodeDaemons[0]->makeINode(node1); nodeDaemons[1]->makeINode(node2); BlockchainSynchronizer blockSync(*node2.get(), currency.genesisBlockHash()); TransfersSyncronizer transferSync(currency, blockSync, *node2.get()); // add transaction collector TransactionConsumer txConsumer; blockSync.addConsumer(&txConsumer); AccountGroup sender(transferSync); AccountGroup consilium(transferSync); sender.generateAccounts(1); sender.subscribeAll(); consilium.generateAccounts(3); consilium.subscribeAll(); auto senderSubscription = transferSync.getSubscription(sender.m_accounts[0].keys.address); auto& senderContainer = senderSubscription->getContainer(); blockSync.start(); AccountPublicAddress senderAddress; ASSERT_TRUE(currency.parseAccountAddressString(sender.m_addresses[0], senderAddress)); ASSERT_TRUE(mineBlocks(*nodeDaemons[0], senderAddress, 1 + currency.minedMoneyUnlockWindow())); // wait for incoming transfer while (senderContainer.balance() == 0) { sender.m_observers[0].waitTransfer(); auto unlockedBalance = senderContainer.balance(ITransfersContainer::IncludeAllUnlocked | ITransfersContainer::IncludeStateSoftLocked); auto totalBalance = senderContainer.balance(ITransfersContainer::IncludeAll); LOG_DEBUG("Balance: " + currency.formatAmount(unlockedBalance) + " (" + currency.formatAmount(totalBalance) + ")"); } uint64_t fundBalance = 0; for (int iteration = 1; iteration <= 3; ++iteration) { LOG_DEBUG("***** Iteration " + std::to_string(iteration) + " ******"); auto sendAmount = senderContainer.balance() / 2; LOG_DEBUG("Creating transaction with amount = " + currency.formatAmount(sendAmount)); auto tx2msig = createTransferToMultisignature( senderContainer, sendAmount, currency.minimumFee(), sender.m_accounts[0].keys, consilium.getAddresses(), 3); auto txHash = tx2msig->getTransactionHash(); // Use node1, in order to tx will be in its pool when next block is being created auto err = submitTransaction(*node1, *tx2msig); ASSERT_EQ(std::error_code(), err); ASSERT_TRUE(mineBlocks(*nodeDaemons[0], senderAddress, 1)); LOG_DEBUG("Waiting for transaction to be included in block..."); txConsumer.waitForTransaction(txHash); LOG_DEBUG("Transaction in blockchain, waiting for observers to receive transaction..."); uint64_t expectedFundBalance = fundBalance + sendAmount; // wait for consilium to receive the transfer for (size_t i = 0; i < consilium.m_accounts.size(); ++i) { auto& observer = consilium.m_observers[i]; auto sub = transferSync.getSubscription(consilium.m_accounts[i].keys.address); ASSERT_TRUE(sub != nullptr); while (true) { observer.waitTransactionTransfer(txHash); uint64_t unlockedBalance = sub->getContainer().balance(ITransfersContainer::IncludeTypeMultisignature | ITransfersContainer::IncludeStateSoftLocked | ITransfersContainer::IncludeStateUnlocked); if (unlockedBalance == expectedFundBalance) { break; } } } LOG_DEBUG("Creating transaction to spend multisignature output"); uint64_t returnAmount = sendAmount / 2; auto spendMsigTx = createTransferFromMultisignature( consilium, sender.m_accounts[0].keys.address, txHash, returnAmount, currency.minimumFee()); auto spendMsigTxHash = spendMsigTx->getTransactionHash(); err = submitTransaction(*node1, *spendMsigTx); ASSERT_EQ(std::error_code(), err); ASSERT_TRUE(mineBlocks(*nodeDaemons[0], senderAddress, 1)); LOG_DEBUG("Waiting for transaction to be included in block..."); txConsumer.waitForTransaction(spendMsigTxHash); LOG_DEBUG("Checking left balances"); uint64_t leftAmount = expectedFundBalance - returnAmount - currency.minimumFee(); for (size_t i = 0; i < consilium.m_accounts.size(); ++i) { auto& observer = consilium.m_observers[i]; for (uint64_t unlockedBalance = leftAmount + 1; unlockedBalance != leftAmount;) { observer.waitTransactionTransfer(spendMsigTxHash); unlockedBalance = consilium.getTransfers(i).balance(ITransfersContainer::IncludeTypeMultisignature | ITransfersContainer::IncludeStateSoftLocked | ITransfersContainer::IncludeStateUnlocked); } } fundBalance = leftAmount; } blockSync.stop(); LOG_DEBUG("Success!!!"); }