566 lines
16 KiB
C++
566 lines
16 KiB
C++
// Copyright (c) 2011-2015 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 <mutex>
|
|
#include <condition_variable>
|
|
#include <future>
|
|
#include <atomic>
|
|
|
|
#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) {
|
|
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) {
|
|
std::cout << "Transaction sent, result = " << result << std::endl;
|
|
}
|
|
|
|
std::atomic<uint64_t> 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<std::mutex> 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<std::mutex> 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<std::mutex> lk(m_mutex);
|
|
while (!hasTransaction(txHash)) {
|
|
m_cv.wait_for(lk, std::chrono::seconds(1));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::error_code onPoolUpdated(const std::vector<std::unique_ptr<ITransactionReader>>& addedTransactions, const std::vector<Crypto::Hash>& deletedTransactions) override {
|
|
//stub
|
|
return std::error_code();
|
|
}
|
|
|
|
void getKnownPoolTxIds(std::vector<Crypto::Hash>& ids) 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<uint64_t, std::unordered_set<Hash>> m_transactions;
|
|
SynchronizationStart syncStart;
|
|
};
|
|
|
|
class TransfersObserver : public ITransfersObserver, public IInterruptable {
|
|
public:
|
|
virtual void onTransactionUpdated(ITransfersSubscription* object, const Hash& transactionHash) override {
|
|
{
|
|
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(m_mutex);
|
|
m_interrupted = true;
|
|
m_cv.notify_all();
|
|
}
|
|
|
|
private:
|
|
std::mutex m_mutex;
|
|
std::condition_variable m_cv;
|
|
std::vector<Hash> 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<const AccountKeys&>(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<AccountPublicAddress> getAddresses() {
|
|
std::vector<AccountPublicAddress> 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<AccountSubscription> m_accounts;
|
|
std::vector<std::string> m_addresses;
|
|
ITransfersSynchronizer& m_sync;
|
|
std::unique_ptr<TransfersObserver[]> m_observers;
|
|
};
|
|
|
|
class MultisignatureTest : public TransfersTest {
|
|
public:
|
|
|
|
virtual void SetUp() override {
|
|
launchTestnet(2);
|
|
}
|
|
};
|
|
|
|
template <typename R>
|
|
class FutureGuard {
|
|
public:
|
|
FutureGuard(std::future<R>&& 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<R> 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<CryptoNote::INode> node1;
|
|
std::unique_ptr<CryptoNote::INode> node2;
|
|
|
|
nodeDaemons[0]->makeINode(node1);
|
|
nodeDaemons[1]->makeINode(node2);
|
|
|
|
CryptoNote::AccountBase dstAcc;
|
|
dstAcc.generate();
|
|
|
|
AccountKeys dstKeys = reinterpret_cast<const AccountKeys&>(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<bool>(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<uint32_t>(2 + currency.minedMoneyUnlockWindow()));
|
|
|
|
// start syncing and wait for a transfer
|
|
FutureGuard<bool> waitFuture(std::async(std::launch::async, [&transferObserver] { return transferObserver.waitTransfer(); }));
|
|
Interrupter transferObserverInterrupter(transferObserver);
|
|
blockSync.start();
|
|
|
|
Hash txId;
|
|
ASSERT_FALSE(static_cast<bool>(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<ITransaction> createTransferToMultisignature(
|
|
ITransfersContainer& tc, // money source
|
|
uint64_t amount,
|
|
uint64_t fee,
|
|
const AccountKeys& senderKeys,
|
|
const std::vector<AccountPublicAddress>& recipients,
|
|
uint32_t requiredSignatures) {
|
|
|
|
std::vector<TransactionOutputInformation> transfers;
|
|
tc.getOutputs(transfers, ITransfersContainer::IncludeAllUnlocked | ITransfersContainer::IncludeStateSoftLocked);
|
|
|
|
auto tx = createTransaction();
|
|
|
|
std::vector<std::pair<TransactionTypes::InputKeyInfo, KeyPair>> 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<std::error_code> 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<ITransaction> createTransferFromMultisignature(
|
|
AccountGroup& consilium, const AccountPublicAddress& receiver, const Hash& txHash, uint64_t amount, uint64_t fee) {
|
|
|
|
auto& tc = consilium.getTransfers(0);
|
|
|
|
std::vector<TransactionOutputInformation> 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<CryptoNote::INode> node1;
|
|
std::unique_ptr<CryptoNote::INode> 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!!!");
|
|
}
|