From a6588cfc583550e7b2a049076827061e9b051f7e Mon Sep 17 00:00:00 2001 From: Antonio Juarez Date: Thu, 27 Aug 2015 19:55:14 +0100 Subject: [PATCH] Bytecoin v.1.0.8 release --- ReleaseNotes.txt | 7 + include/ITransfersContainer.h | 19 +- include/IWallet.h | 2 + src/BlockchainExplorer/BlockchainExplorer.cpp | 4 - src/CryptoNoteConfig.h | 7 +- src/CryptoNoteCore/Core.cpp | 13 +- src/CryptoNoteCore/CryptoNoteTools.cpp | 15 + src/CryptoNoteCore/CryptoNoteTools.h | 1 + src/CryptoNoteCore/Currency.cpp | 119 +++- src/CryptoNoteCore/Currency.h | 9 +- src/CryptoNoteCore/TransactionPool.cpp | 8 +- src/CryptoNoteCore/TransactionPool.h | 1 + .../CryptoNoteProtocolHandler.cpp | 2 +- src/HTTP/HttpParser.cpp | 9 +- src/InProcessNode/InProcessNode.cpp | 7 +- src/NodeRpcProxy/NodeRpcProxy.cpp | 3 - .../PaymentServiceJsonRpcServer.cpp | 11 +- src/Platform/OSX/System/Dispatcher.cpp | 27 +- src/Platform/OSX/System/TcpConnection.cpp | 2 +- src/Platform/OSX/System/TcpConnector.cpp | 2 +- src/Rpc/JsonRpc.h | 5 +- src/Rpc/RpcServer.cpp | 17 +- src/Transfers/BlockchainSynchronizer.cpp | 4 +- src/Transfers/IBlockchainSynchronizer.h | 2 +- src/Transfers/TransfersConsumer.cpp | 3 +- src/Transfers/TransfersContainer.cpp | 47 +- src/Transfers/TransfersContainer.h | 20 +- src/Wallet/IFusionManager.h | 2 +- src/Wallet/WalletErrors.h | 42 +- src/Wallet/WalletGreen.cpp | 438 ++++++++++-- src/Wallet/WalletGreen.h | 32 +- .../WalletRpcServerCommandsDefinitions.h | 19 +- src/Wallet/WalletSerialization.cpp | 55 +- src/Wallet/WalletSerialization.h | 5 +- src/version.h.in | 4 +- tests/System/RemoteContextTests.cpp | 8 +- tests/System/TimerTests.cpp | 4 +- tests/UnitTests/ICoreStub.cpp | 34 +- tests/UnitTests/ICoreStub.h | 6 +- tests/UnitTests/INodeStubs.cpp | 4 + tests/UnitTests/INodeStubs.h | 2 + tests/UnitTests/TestBlockchainGenerator.cpp | 12 +- tests/UnitTests/TestBlockchainGenerator.h | 1 + tests/UnitTests/TestCurrency.cpp | 22 +- tests/UnitTests/TestInprocessNode.cpp | 96 ++- tests/UnitTests/TestTransactionPoolDetach.cpp | 8 +- tests/UnitTests/TestWallet.cpp | 645 +++++++++++++++++- tests/UnitTests/TransactionApiHelpers.cpp | 23 +- tests/UnitTests/TransactionApiHelpers.h | 4 + 49 files changed, 1580 insertions(+), 252 deletions(-) diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 359fdaf1..eaa97d2d 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -1,3 +1,10 @@ +Release notes 1.0.8 + +- Fusion transactions for Bytecoin Wallet +- IWallet high-level API update +- JSON RPC improvements +- Synchronization improvements for OS X + Release notes 1.0.7 - Fusion transactions support diff --git a/include/ITransfersContainer.h b/include/ITransfersContainer.h index 96cbd12c..8aca7ee8 100644 --- a/include/ITransfersContainer.h +++ b/include/ITransfersContainer.h @@ -75,6 +75,7 @@ public: IncludeStateUnlocked = 0x01, IncludeStateLocked = 0x02, IncludeStateSoftLocked = 0x04, + IncludeStateSpent = 0x08, // output type IncludeTypeKey = 0x100, IncludeTypeMultisignature = 0x200, @@ -92,14 +93,16 @@ public: IncludeDefault = IncludeKeyUnlocked }; - virtual size_t transfersCount() = 0; - virtual size_t transactionsCount() = 0; - virtual uint64_t balance(uint32_t flags = IncludeDefault) = 0; - virtual void getOutputs(std::vector& transfers, uint32_t flags = IncludeDefault) = 0; - virtual bool getTransactionInformation(const Crypto::Hash& transactionHash, TransactionInformation& info, int64_t& txBalance) = 0; - virtual std::vector getTransactionOutputs(const Crypto::Hash& transactionHash, uint32_t flags = IncludeDefault) = 0; - virtual void getUnconfirmedTransactions(std::vector& transactions) = 0; - virtual std::vector getSpentOutputs() = 0; + virtual size_t transfersCount() const = 0; + virtual size_t transactionsCount() const = 0; + virtual uint64_t balance(uint32_t flags = IncludeDefault) const = 0; + virtual void getOutputs(std::vector& transfers, uint32_t flags = IncludeDefault) const = 0; + virtual bool getTransactionInformation(const Crypto::Hash& transactionHash, TransactionInformation& info, int64_t& txBalance) const = 0; + virtual std::vector getTransactionOutputs(const Crypto::Hash& transactionHash, uint32_t flags = IncludeDefault) const = 0; + //only type flags are feasible for this function + virtual std::vector getTransactionInputs(const Crypto::Hash& transactionHash, uint32_t flags) const = 0; + virtual void getUnconfirmedTransactions(std::vector& transactions) const = 0; + virtual std::vector getSpentOutputs() const = 0; }; } diff --git a/include/IWallet.h b/include/IWallet.h index d10db02c..69b46441 100755 --- a/include/IWallet.h +++ b/include/IWallet.h @@ -74,6 +74,7 @@ struct WalletTransaction { uint64_t creationTime; uint64_t unlockTime; std::string extra; + bool isBase; }; struct WalletTransfer { @@ -99,6 +100,7 @@ public: virtual KeyPair getViewKey() const = 0; virtual std::string createAddress() = 0; virtual std::string createAddress(const Crypto::SecretKey& spendSecretKey) = 0; + virtual std::string createAddress(const Crypto::PublicKey& spendPublicKey) = 0; virtual void deleteAddress(const std::string& address) = 0; virtual uint64_t getActualBalance() const = 0; diff --git a/src/BlockchainExplorer/BlockchainExplorer.cpp b/src/BlockchainExplorer/BlockchainExplorer.cpp index bdfe4ee1..159bf75b 100755 --- a/src/BlockchainExplorer/BlockchainExplorer.cpp +++ b/src/BlockchainExplorer/BlockchainExplorer.cpp @@ -461,10 +461,6 @@ void BlockchainExplorer::poolChanged() { return; } - if (!*isBlockchainActualPtr) { - logger(WARNING) << "Blockchain not actual."; - } - std::unique_lock lock(mutex); std::shared_ptr> newTransactionsHashesPtr = std::make_shared>(); diff --git a/src/CryptoNoteConfig.h b/src/CryptoNoteConfig.h index 3f988e15..0d1b3d7a 100644 --- a/src/CryptoNoteConfig.h +++ b/src/CryptoNoteConfig.h @@ -63,8 +63,8 @@ const uint64_t CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME = 60 * 60 * 24 * 7; const uint64_t CRYPTONOTE_NUMBER_OF_PERIODS_TO_FORGET_TX_DELETED_FROM_POOL = 7; // CRYPTONOTE_NUMBER_OF_PERIODS_TO_FORGET_TX_DELETED_FROM_POOL * CRYPTONOTE_MEMPOOL_TX_LIVETIME = time to forget tx const size_t FUSION_TX_MAX_SIZE = CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE * 30 / 100; -const size_t FUSION_TX_MIN_INPUT_COUNT = 6; -const size_t FUSION_TX_MIN_IN_OUT_COUNT_RATIO = 3; +const size_t FUSION_TX_MIN_INPUT_COUNT = 12; +const size_t FUSION_TX_MIN_IN_OUT_COUNT_RATIO = 4; const uint64_t UPGRADE_HEIGHT = 546602; const unsigned UPGRADE_VOTING_THRESHOLD = 90; // percent @@ -162,7 +162,8 @@ const CheckpointData CHECKPOINTS[] = { {796000, "04e387a00d35db21d4d93d04040b31f22573972a7e61d72cc07d0ab69bcb9c44"}, {800000, "d7fa4eea02e5ce60b949136569c0ea7ac71ea46e0065311054072ac415560b86"}, {804000, "bcc8b3782499aae508c40d5587d1cc5d68281435ea9bfc6804a262047f7b934d"}, - {810500, "302b2349f221232820adc3dadafd8a61b035491e33af669c78a687949eb0a381"} + {810500, "302b2349f221232820adc3dadafd8a61b035491e33af669c78a687949eb0a381"}, + {816000, "32b7fdd4e4d715db81f8f09f4ba5e5c78e8113f2804d61a57378baee479ce745"} }; } // CryptoNote diff --git a/src/CryptoNoteCore/Core.cpp b/src/CryptoNoteCore/Core.cpp index a2f1553a..a8238c5e 100755 --- a/src/CryptoNoteCore/Core.cpp +++ b/src/CryptoNoteCore/Core.cpp @@ -473,20 +473,14 @@ void core::on_synchronized() { bool core::getPoolChanges(const Crypto::Hash& tailBlockId, const std::vector& knownTxsIds, std::vector& addedTxs, std::vector& deletedTxsIds) { - if (tailBlockId != m_blockchain.getTailId()) { - return false; - } - getPoolChanges(knownTxsIds, addedTxs, deletedTxsIds); - return true; + return tailBlockId == m_blockchain.getTailId(); } bool core::getPoolChangesLite(const Crypto::Hash& tailBlockId, const std::vector& knownTxsIds, std::vector& addedTxs, std::vector& deletedTxsIds) { std::vector added; - if (!getPoolChanges(tailBlockId, knownTxsIds, added, deletedTxsIds)) { - return false; - } + bool returnStatus = getPoolChanges(tailBlockId, knownTxsIds, added, deletedTxsIds); for (const auto& tx: added) { TransactionPrefixInfo tpi; @@ -496,12 +490,13 @@ bool core::getPoolChangesLite(const Crypto::Hash& tailBlockId, const std::vector addedTxs.push_back(std::move(tpi)); } - return true; + return returnStatus; } void core::getPoolChanges(const std::vector& knownTxsIds, std::vector& addedTxs, std::vector& deletedTxsIds) { std::vector addedTxsIds; + auto guard = m_mempool.obtainGuard(); m_mempool.get_difference(knownTxsIds, addedTxsIds, deletedTxsIds); std::vector misses; m_mempool.getTransactions(addedTxsIds, addedTxs, misses); diff --git a/src/CryptoNoteCore/CryptoNoteTools.cpp b/src/CryptoNoteCore/CryptoNoteTools.cpp index e1c406ee..24065ab7 100755 --- a/src/CryptoNoteCore/CryptoNoteTools.cpp +++ b/src/CryptoNoteCore/CryptoNoteTools.cpp @@ -56,6 +56,21 @@ uint64_t getInputAmount(const Transaction& transaction) { return amount; } +std::vector getInputsAmounts(const Transaction& transaction) { + std::vector inputsAmounts; + inputsAmounts.reserve(transaction.inputs.size()); + + for (auto& input: transaction.inputs) { + if (input.type() == typeid(KeyInput)) { + inputsAmounts.push_back(boost::get(input).amount); + } else if (input.type() == typeid(MultisignatureInput)) { + inputsAmounts.push_back(boost::get(input).amount); + } + } + + return inputsAmounts; +} + uint64_t getOutputAmount(const Transaction& transaction) { uint64_t amount = 0; for (auto& output : transaction.outputs) { diff --git a/src/CryptoNoteCore/CryptoNoteTools.h b/src/CryptoNoteCore/CryptoNoteTools.h index 79850634..97660f28 100755 --- a/src/CryptoNoteCore/CryptoNoteTools.h +++ b/src/CryptoNoteCore/CryptoNoteTools.h @@ -120,6 +120,7 @@ Crypto::Hash getObjectHash(const T& object) { } uint64_t getInputAmount(const Transaction& transaction); +std::vector getInputsAmounts(const Transaction& transaction); uint64_t getOutputAmount(const Transaction& transaction); void decomposeAmount(uint64_t amount, uint64_t dustThreshold, std::vector& decomposedAmounts); } diff --git a/src/CryptoNoteCore/Currency.cpp b/src/CryptoNoteCore/Currency.cpp index fac0ae59..6993f6cb 100755 --- a/src/CryptoNoteCore/Currency.cpp +++ b/src/CryptoNoteCore/Currency.cpp @@ -37,6 +37,29 @@ using namespace Common; namespace CryptoNote { +const std::vector Currency::PRETTY_AMOUNTS = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 20, 30, 40, 50, 60, 70, 80, 90, + 100, 200, 300, 400, 500, 600, 700, 800, 900, + 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, + 10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000, + 100000, 200000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, + 1000000, 2000000, 3000000, 4000000, 5000000, 6000000, 7000000, 8000000, 9000000, + 10000000, 20000000, 30000000, 40000000, 50000000, 60000000, 70000000, 80000000, 90000000, + 100000000, 200000000, 300000000, 400000000, 500000000, 600000000, 700000000, 800000000, 900000000, + 1000000000, 2000000000, 3000000000, 4000000000, 5000000000, 6000000000, 7000000000, 8000000000, 9000000000, + 10000000000, 20000000000, 30000000000, 40000000000, 50000000000, 60000000000, 70000000000, 80000000000, 90000000000, + 100000000000, 200000000000, 300000000000, 400000000000, 500000000000, 600000000000, 700000000000, 800000000000, 900000000000, + 1000000000000, 2000000000000, 3000000000000, 4000000000000, 5000000000000, 6000000000000, 7000000000000, 8000000000000, 9000000000000, + 10000000000000, 20000000000000, 30000000000000, 40000000000000, 50000000000000, 60000000000000, 70000000000000, 80000000000000, 90000000000000, + 100000000000000, 200000000000000, 300000000000000, 400000000000000, 500000000000000, 600000000000000, 700000000000000, 800000000000000, 900000000000000, + 1000000000000000, 2000000000000000, 3000000000000000, 4000000000000000, 5000000000000000, 6000000000000000, 7000000000000000, 8000000000000000, 9000000000000000, + 10000000000000000, 20000000000000000, 30000000000000000, 40000000000000000, 50000000000000000, 60000000000000000, 70000000000000000, 80000000000000000, 90000000000000000, + 100000000000000000, 200000000000000000, 300000000000000000, 400000000000000000, 500000000000000000, 600000000000000000, 700000000000000000, 800000000000000000, 900000000000000000, + 1000000000000000000, 2000000000000000000, 3000000000000000000, 4000000000000000000, 5000000000000000000, 6000000000000000000, 7000000000000000000, 8000000000000000000, 9000000000000000000, + 10000000000000000000ull +}; + bool Currency::init() { if (!generateGenesisBlock()) { logger(ERROR, BRIGHT_RED) << "Failed to generate genesis block"; @@ -209,50 +232,73 @@ bool Currency::constructMinerTx(uint32_t height, size_t medianSize, uint64_t alr return true; } -bool Currency::isFusionTransaction(const Transaction& transaction, uint64_t inputAmount, size_t size) const { - assert(getInputAmount(transaction) == inputAmount); - assert(getObjectBinarySize(transaction) == size); - +bool Currency::isFusionTransaction(const std::vector& inputsAmounts, const std::vector& outputsAmounts, size_t size) const { if (size > fusionTxMaxSize()) { return false; } - if (transaction.inputs.size() < fusionTxMinInputCount()) { + if (inputsAmounts.size() < fusionTxMinInputCount()) { return false; } - if (transaction.inputs.size() < transaction.outputs.size() * fusionTxMinInOutCountRatio()) { + if (inputsAmounts.size() < outputsAmounts.size() * fusionTxMinInOutCountRatio()) { return false; } + uint64_t inputAmount = 0; + for (auto amount: inputsAmounts) { + if (amount < defaultDustThreshold()) { + return false; + } + + inputAmount += amount; + } + std::vector expectedOutputsAmounts; - expectedOutputsAmounts.reserve(transaction.outputs.size()); + expectedOutputsAmounts.reserve(outputsAmounts.size()); decomposeAmount(inputAmount, defaultDustThreshold(), expectedOutputsAmounts); - assert(!expectedOutputsAmounts.empty()); - - if (expectedOutputsAmounts.size() != transaction.outputs.size()) { - return false; - } - std::sort(expectedOutputsAmounts.begin(), expectedOutputsAmounts.end()); - if (expectedOutputsAmounts.front() <= defaultDustThreshold()) { - return false; + return expectedOutputsAmounts == outputsAmounts; +} + +bool Currency::isFusionTransaction(const Transaction& transaction, size_t size) const { + assert(getObjectBinarySize(transaction) == size); + + std::vector outputsAmounts; + outputsAmounts.reserve(transaction.outputs.size()); + for (const TransactionOutput& output : transaction.outputs) { + outputsAmounts.push_back(output.amount); } - auto it1 = expectedOutputsAmounts.begin(); - auto it2 = transaction.outputs.begin(); - for (; it1 != expectedOutputsAmounts.end(); ++it1, ++it2) { - if (*it1 != it2->amount) { - return false; - } - } - - return true; + return isFusionTransaction(getInputsAmounts(transaction), outputsAmounts, size); } bool Currency::isFusionTransaction(const Transaction& transaction) const { - return isFusionTransaction(transaction, getInputAmount(transaction), getObjectBinarySize(transaction)); + return isFusionTransaction(transaction, getObjectBinarySize(transaction)); +} + +bool Currency::isAmountApplicableInFusionTransactionInput(uint64_t amount, uint64_t threshold) const { + uint8_t ignore; + return isAmountApplicableInFusionTransactionInput(amount, threshold, ignore); +} + +bool Currency::isAmountApplicableInFusionTransactionInput(uint64_t amount, uint64_t threshold, uint8_t& amountPowerOfTen) const { + if (amount >= threshold) { + return false; + } + + if (amount < defaultDustThreshold()) { + return false; + } + + auto it = std::lower_bound(PRETTY_AMOUNTS.begin(), PRETTY_AMOUNTS.end(), amount); + if (it == PRETTY_AMOUNTS.end() || amount != *it) { + return false; + } + + amountPowerOfTen = static_cast(std::distance(PRETTY_AMOUNTS.begin(), it) / 9); + return true; } std::string Currency::accountAddressAsString(const AccountBase& account) const { @@ -440,6 +486,29 @@ bool Currency::checkProofOfWork(Crypto::cn_context& context, const Block& block, return false; } +size_t Currency::getApproximateMaximumInputCount(size_t transactionSize, size_t outputCount, size_t mixinCount) const { + const size_t KEY_IMAGE_SIZE = sizeof(Crypto::KeyImage); + const size_t OUTPUT_KEY_SIZE = sizeof(decltype(KeyOutput::key)); + const size_t AMOUNT_SIZE = sizeof(uint64_t) + 2; //varint + const size_t GLOBAL_INDEXES_VECTOR_SIZE_SIZE = sizeof(uint8_t);//varint + const size_t GLOBAL_INDEXES_INITIAL_VALUE_SIZE = sizeof(uint32_t);//varint + const size_t GLOBAL_INDEXES_DIFFERENCE_SIZE = sizeof(uint32_t);//varint + const size_t SIGNATURE_SIZE = sizeof(Crypto::Signature); + const size_t EXTRA_TAG_SIZE = sizeof(uint8_t); + const size_t INPUT_TAG_SIZE = sizeof(uint8_t); + const size_t OUTPUT_TAG_SIZE = sizeof(uint8_t); + const size_t PUBLIC_KEY_SIZE = sizeof(Crypto::PublicKey); + const size_t TRANSACTION_VERSION_SIZE = sizeof(uint8_t); + const size_t TRANSACTION_UNLOCK_TIME_SIZE = sizeof(uint64_t); + + const size_t outputsSize = outputCount * (OUTPUT_TAG_SIZE + OUTPUT_KEY_SIZE + AMOUNT_SIZE); + const size_t headerSize = TRANSACTION_VERSION_SIZE + TRANSACTION_UNLOCK_TIME_SIZE + EXTRA_TAG_SIZE + PUBLIC_KEY_SIZE; + const size_t inputSize = INPUT_TAG_SIZE + AMOUNT_SIZE + KEY_IMAGE_SIZE + SIGNATURE_SIZE + GLOBAL_INDEXES_VECTOR_SIZE_SIZE + GLOBAL_INDEXES_INITIAL_VALUE_SIZE + + mixinCount * (GLOBAL_INDEXES_DIFFERENCE_SIZE + SIGNATURE_SIZE); + + return (transactionSize - headerSize - outputsSize) / inputSize; +} + CurrencyBuilder::CurrencyBuilder(Logging::ILogger& log) : m_currency(log) { maxBlockNumber(parameters::CRYPTONOTE_MAX_BLOCK_NUMBER); maxBlockBlobSize(parameters::CRYPTONOTE_MAX_BLOCK_BLOB_SIZE); diff --git a/src/CryptoNoteCore/Currency.h b/src/CryptoNoteCore/Currency.h index c687c6ef..72db9721 100755 --- a/src/CryptoNoteCore/Currency.h +++ b/src/CryptoNoteCore/Currency.h @@ -104,7 +104,10 @@ public: const BinaryArray& extraNonce = BinaryArray(), size_t maxOuts = 1, bool penalizeFee = false) const; bool isFusionTransaction(const Transaction& transaction) const; - bool isFusionTransaction(const Transaction& transaction, uint64_t inputAmount, size_t size) const; + bool isFusionTransaction(const Transaction& transaction, size_t size) const; + bool isFusionTransaction(const std::vector& inputsAmounts, const std::vector& outputsAmounts, size_t size) const; + bool isAmountApplicableInFusionTransactionInput(uint64_t amount, uint64_t threshold) const; + bool isAmountApplicableInFusionTransactionInput(uint64_t amount, uint64_t threshold, uint8_t& amountPowerOfTen) const; std::string accountAddressAsString(const AccountBase& account) const; std::string accountAddressAsString(const AccountPublicAddress& accountPublicAddress) const; @@ -120,6 +123,8 @@ public: bool checkProofOfWorkV2(Crypto::cn_context& context, const Block& block, difficulty_type currentDiffic, Crypto::Hash& proofOfWork) const; bool checkProofOfWork(Crypto::cn_context& context, const Block& block, difficulty_type currentDiffic, Crypto::Hash& proofOfWork) const; + size_t getApproximateMaximumInputCount(size_t transactionSize, size_t outputCount, size_t mixinCount) const; + private: Currency(Logging::ILogger& log) : logger(log, "currency") { } @@ -182,6 +187,8 @@ private: std::string m_txPoolFileName; std::string m_blockchinIndicesFileName; + static const std::vector PRETTY_AMOUNTS; + bool m_testnet; Block m_genesisBlock; diff --git a/src/CryptoNoteCore/TransactionPool.cpp b/src/CryptoNoteCore/TransactionPool.cpp index 564599eb..be89eb55 100644 --- a/src/CryptoNoteCore/TransactionPool.cpp +++ b/src/CryptoNoteCore/TransactionPool.cpp @@ -133,7 +133,7 @@ namespace CryptoNote { } const uint64_t fee = inputs_amount - outputs_amount; - bool isFusionTransaction = fee == 0 && m_currency.isFusionTransaction(tx, inputs_amount, blobSize); + bool isFusionTransaction = fee == 0 && m_currency.isFusionTransaction(tx, blobSize); if (!keptByBlock && !isFusionTransaction && fee < m_currency.minimumFee()) { logger(INFO) << "transaction fee is not enough: " << m_currency.formatAmount(fee) << ", minimum fee: " << m_currency.formatAmount(m_currency.minimumFee()); @@ -222,6 +222,7 @@ namespace CryptoNote { //succeed return true; } + //--------------------------------------------------------------------------------- bool tx_memory_pool::add_tx(const Transaction &tx, tx_verification_context& tvc, bool keeped_by_block) { Crypto::Hash h = NULL_HASH; @@ -308,6 +309,11 @@ namespace CryptoNote { void tx_memory_pool::unlock() const { m_transactions_lock.unlock(); } + + std::unique_lock tx_memory_pool::obtainGuard() const { + return std::unique_lock(m_transactions_lock); + } + //--------------------------------------------------------------------------------- bool tx_memory_pool::is_transaction_ready_to_go(const Transaction& tx, TransactionCheckInfo& txd) const { diff --git a/src/CryptoNoteCore/TransactionPool.h b/src/CryptoNoteCore/TransactionPool.h index c0301da2..01350736 100755 --- a/src/CryptoNoteCore/TransactionPool.h +++ b/src/CryptoNoteCore/TransactionPool.h @@ -107,6 +107,7 @@ namespace CryptoNote { void lock() const; void unlock() const; + std::unique_lock obtainGuard() const; bool fill_block_template(Block &bl, size_t median_size, size_t maxCumulativeSize, uint64_t already_generated_coins, size_t &total_size, uint64_t &fee); diff --git a/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp b/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp index e1d65be3..554e0420 100644 --- a/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp +++ b/src/CryptoNoteProtocol/CryptoNoteProtocolHandler.cpp @@ -646,7 +646,7 @@ void CryptoNoteProtocolHandler::updateObservedHeight(uint32_t peerHeight, const if (m_observedHeight != height) { updated = true; } - } else if (context.m_remote_blockchain_height == m_observedHeight) { + } else if (peerHeight != context.m_remote_blockchain_height && context.m_remote_blockchain_height == m_observedHeight) { //the client switched to alternative chain and had maximum observed height. need to recalculate max height recalculateMaxObservedHeight(context); if (m_observedHeight != height) { diff --git a/src/HTTP/HttpParser.cpp b/src/HTTP/HttpParser.cpp index 455a0cb0..5dc94085 100755 --- a/src/HTTP/HttpParser.cpp +++ b/src/HTTP/HttpParser.cpp @@ -16,6 +16,9 @@ // along with Bytecoin. If not, see . #include "HttpParser.h" + +#include + #include "HttpParserErrorCodes.h" namespace { @@ -98,7 +101,7 @@ void HttpParser::receiveResponse(std::istream& stream, HttpResponse& response) { response.addHeader(name, value); auto headers = response.getHeaders(); size_t length = 0; - auto it = headers.find("Content-Length"); + auto it = headers.find("content-length"); if (it != headers.end()) { length = std::stoul(it->second); } @@ -182,6 +185,8 @@ bool HttpParser::readHeader(std::istream& stream, std::string& name, std::string throw std::system_error(make_error_code(CryptoNote::error::HttpParserErrorCodes::UNEXPECTED_SYMBOL)); } + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + c = stream.peek(); if (c == '\r') { stream.get(c).get(c); @@ -196,7 +201,7 @@ bool HttpParser::readHeader(std::istream& stream, std::string& name, std::string } size_t HttpParser::getBodyLen(const HttpRequest::Headers& headers) { - auto it = headers.find("Content-Length"); + auto it = headers.find("content-length"); if (it != headers.end()) { size_t bytes = std::stoul(it->second); return bytes; diff --git a/src/InProcessNode/InProcessNode.cpp b/src/InProcessNode/InProcessNode.cpp index d1c20ac2..89f67faf 100644 --- a/src/InProcessNode/InProcessNode.cpp +++ b/src/InProcessNode/InProcessNode.cpp @@ -540,15 +540,10 @@ void InProcessNode::getPoolSymmetricDifferenceAsync(std::vector&& std::vector added; isBcActual = core.getPoolChangesLite(knownBlockId, knownPoolTxIds, added, deletedTxIds); - if (!isBcActual) { - ec = make_error_code(CryptoNote::error::INTERNAL_NODE_ERROR); - callback(ec); - return; - } try { for (const auto& tx: added) { - newTxs.push_back(createTransactionPrefix(tx.txPrefix, reinterpret_cast(tx.txHash))); + newTxs.push_back(createTransactionPrefix(tx.txPrefix, tx.txHash)); } } catch (std::system_error& ex) { ec = ex.code(); diff --git a/src/NodeRpcProxy/NodeRpcProxy.cpp b/src/NodeRpcProxy/NodeRpcProxy.cpp index f6d0e917..f5adb0df 100644 --- a/src/NodeRpcProxy/NodeRpcProxy.cpp +++ b/src/NodeRpcProxy/NodeRpcProxy.cpp @@ -536,9 +536,6 @@ std::error_code NodeRpcProxy::doGetPoolSymmetricDifference(std::vector(event.udata)->context; break; } @@ -274,21 +283,24 @@ void Dispatcher::spawn(std::function&& procedure) { void Dispatcher::yield() { struct timespec zeroTimeout = { 0, 0 }; + int updatesCounter = 0; for (;;) { struct kevent events[16]; - int count = kevent(kqueue, NULL, 0, events, 16, &zeroTimeout); + struct kevent updates[16]; + int count = kevent(kqueue, updates, updatesCounter, events, 16, &zeroTimeout); if (count == 0) { break; } + updatesCounter = 0; if (count > 0) { for (int i = 0; i < count; ++i) { + if (events[i].flags & EV_ERROR) { + continue; + } + if (events[i].filter == EVFILT_USER && events[i].ident == 0) { - struct kevent event; - EV_SET(&event, 0, EVFILT_USER, EV_ADD | EV_DISABLE, NOTE_FFNOP, 0, NULL); - if (kevent(kqueue, &event, 1, NULL, 0, NULL) == -1) { - throw std::runtime_error("Dispatcher::yield, kevent failed, " + lastErrorMessage()); - } + EV_SET(&updates[updatesCounter++], 0, EVFILT_USER, EV_ADD | EV_DISABLE, NOTE_FFNOP, 0, NULL); MutextGuard guard(*reinterpret_cast(this->mutex)); while (!remoteSpawningProcedures.empty()) { @@ -302,6 +314,9 @@ void Dispatcher::yield() { static_cast(events[i].udata)->context->interruptProcedure = nullptr; pushContext(static_cast(events[i].udata)->context); + if (events[i].filter == EVFILT_WRITE) { + EV_SET(&updates[updatesCounter++], events[i].ident, EVFILT_WRITE, EV_DELETE | EV_DISABLE, 0, 0, NULL); + } } } else { if (errno != EINTR) { diff --git a/src/Platform/OSX/System/TcpConnection.cpp b/src/Platform/OSX/System/TcpConnection.cpp index 8d85d991..5a91f641 100755 --- a/src/Platform/OSX/System/TcpConnection.cpp +++ b/src/Platform/OSX/System/TcpConnection.cpp @@ -168,7 +168,7 @@ size_t TcpConnection::write(const uint8_t* data, size_t size) { context.context = dispatcher->getCurrentContext(); context.interrupted = false; struct kevent event; - EV_SET(&event, connection, EVFILT_WRITE, EV_ADD | EV_ENABLE | EV_ONESHOT, 0, 0, &context); + EV_SET(&event, connection, EVFILT_WRITE, EV_ADD | EV_ENABLE, 0, 0, &context); if (kevent(dispatcher->getKqueue(), &event, 1, NULL, 0, NULL) == -1) { message = "kevent failed, " + lastErrorMessage(); } else { diff --git a/src/Platform/OSX/System/TcpConnector.cpp b/src/Platform/OSX/System/TcpConnector.cpp index 6a5d07f1..91d10d19 100755 --- a/src/Platform/OSX/System/TcpConnector.cpp +++ b/src/Platform/OSX/System/TcpConnector.cpp @@ -107,7 +107,7 @@ TcpConnection TcpConnector::connect(const Ipv4Address& address, uint16_t port) { connectorContext.connection = connection; struct kevent event; - EV_SET(&event, connection, EVFILT_WRITE, EV_ADD | EV_ENABLE | EV_ONESHOT | EV_CLEAR, 0, 0, &connectorContext); + EV_SET(&event, connection, EVFILT_WRITE, EV_ADD | EV_ENABLE, 0, 0, &connectorContext); if (kevent(dispatcher->getKqueue(), &event, 1, NULL, 0, NULL) == -1) { message = "kevent failed, " + lastErrorMessage(); } else { diff --git a/src/Rpc/JsonRpc.h b/src/Rpc/JsonRpc.h index dc484041..00cbbe75 100755 --- a/src/Rpc/JsonRpc.h +++ b/src/Rpc/JsonRpc.h @@ -21,9 +21,10 @@ #include #include +#include "CoreRpcServerCommandsDefinitions.h" +#include #include "Serialization/ISerializer.h" #include "Serialization/SerializationTools.h" -#include namespace CryptoNote { @@ -204,7 +205,7 @@ bool invokeMethod(const JsonRpcRequest& jsReq, JsonRpcResponse& jsRes, Handler h Request req; Response res; - if (!jsReq.loadParams(req)) { + if (!std::is_same::value && !jsReq.loadParams(req)) { throw JsonRpcError(JsonRpc::errInvalidParams); } diff --git a/src/Rpc/RpcServer.cpp b/src/Rpc/RpcServer.cpp index e372279b..a6e5a2fb 100755 --- a/src/Rpc/RpcServer.cpp +++ b/src/Rpc/RpcServer.cpp @@ -306,18 +306,15 @@ bool RpcServer::onGetPoolChanges(const COMMAND_RPC_GET_POOL_CHANGES::request& re rsp.status = CORE_RPC_STATUS_OK; std::vector addedTransactions; rsp.isTailBlockActual = m_core.getPoolChanges(req.tailBlockId, req.knownTxsIds, addedTransactions, rsp.deletedTxsIds); - if (rsp.isTailBlockActual) { - for (auto& tx : addedTransactions) { - BinaryArray txBlob; - if (!toBinaryArray(tx, txBlob)) { - rsp.status = "Internal error"; - break;; - } - - rsp.addedTxs.emplace_back(std::move(txBlob)); + for (auto& tx : addedTransactions) { + BinaryArray txBlob; + if (!toBinaryArray(tx, txBlob)) { + rsp.status = "Internal error"; + break;; } - } + rsp.addedTxs.emplace_back(std::move(txBlob)); + } return true; } diff --git a/src/Transfers/BlockchainSynchronizer.cpp b/src/Transfers/BlockchainSynchronizer.cpp index 9876fc2a..08f36149 100755 --- a/src/Transfers/BlockchainSynchronizer.cpp +++ b/src/Transfers/BlockchainSynchronizer.cpp @@ -327,7 +327,6 @@ void BlockchainSynchronizer::onGetBlocksCompleted(std::error_code ec) { } void BlockchainSynchronizer::processBlocks(GetBlocksResponse& response) { - uint32_t newHeight = response.startHeight + static_cast(response.newBlocks.size()); BlockchainInterval interval; interval.startHeight = response.startHeight; std::vector blocks; @@ -365,6 +364,7 @@ void BlockchainSynchronizer::processBlocks(GetBlocksResponse& response) { blocks.push_back(std::move(completeBlock)); } + uint32_t processedBlockCount = response.startHeight + static_cast(response.newBlocks.size()); if (!checkIfShouldStop()) { response.newBlocks.clear(); std::unique_lock lk(m_consumersMutex); @@ -393,7 +393,7 @@ void BlockchainSynchronizer::processBlocks(GetBlocksResponse& response) { setFutureState(State::blockchainSync); m_observerManager.notify( &IBlockchainSynchronizerObserver::synchronizationProgressUpdated, - newHeight, + processedBlockCount, std::max(m_node.getKnownBlockCount(), m_node.getLocalBlockCount())); break; } diff --git a/src/Transfers/IBlockchainSynchronizer.h b/src/Transfers/IBlockchainSynchronizer.h index 9deb7122..dcb5fbe5 100755 --- a/src/Transfers/IBlockchainSynchronizer.h +++ b/src/Transfers/IBlockchainSynchronizer.h @@ -33,7 +33,7 @@ struct CompleteBlock; class IBlockchainSynchronizerObserver { public: - virtual void synchronizationProgressUpdated(uint32_t current, uint32_t total) {} + virtual void synchronizationProgressUpdated(uint32_t processedBlockCount, uint32_t totalBlockCount) {} virtual void synchronizationCompleted(std::error_code result) {} }; diff --git a/src/Transfers/TransfersConsumer.cpp b/src/Transfers/TransfersConsumer.cpp index 059570f1..050494dd 100755 --- a/src/Transfers/TransfersConsumer.cpp +++ b/src/Transfers/TransfersConsumer.cpp @@ -160,6 +160,7 @@ void TransfersConsumer::onBlockchainDetach(uint32_t height) { bool TransfersConsumer::onNewBlocks(const CompleteBlock* blocks, uint32_t startHeight, uint32_t count) { assert(blocks); + assert(count > 0); struct Tx { TransactionBlockInfo blockInfo; @@ -273,7 +274,7 @@ bool TransfersConsumer::onNewBlocks(const CompleteBlock* blocks, uint32_t startH return false; } - auto newHeight = startHeight + count; + auto newHeight = startHeight + count - 1; forEachSubscription([newHeight](TransfersSubscription& sub) { sub.advanceHeight(newHeight); }); diff --git a/src/Transfers/TransfersContainer.cpp b/src/Transfers/TransfersContainer.cpp index cdb6f96e..d4cdcbd9 100755 --- a/src/Transfers/TransfersContainer.cpp +++ b/src/Transfers/TransfersContainer.cpp @@ -588,24 +588,24 @@ bool TransfersContainer::advanceHeight(uint32_t height) { std::lock_guard lk(m_mutex); if (m_currentHeight <= height) { - m_currentHeight = height; + m_currentHeight = height; return true; } return false; } -size_t TransfersContainer::transfersCount() { +size_t TransfersContainer::transfersCount() const { std::lock_guard lk(m_mutex); return m_unconfirmedTransfers.size() + m_availableTransfers.size() + m_spentTransfers.size(); } -size_t TransfersContainer::transactionsCount() { +size_t TransfersContainer::transactionsCount() const { std::lock_guard lk(m_mutex); return m_transactions.size(); } -uint64_t TransfersContainer::balance(uint32_t flags) { +uint64_t TransfersContainer::balance(uint32_t flags) const { std::lock_guard lk(m_mutex); uint64_t amount = 0; @@ -626,7 +626,7 @@ uint64_t TransfersContainer::balance(uint32_t flags) { return amount; } -void TransfersContainer::getOutputs(std::vector& transfers, uint32_t flags) { +void TransfersContainer::getOutputs(std::vector& transfers, uint32_t flags) const { std::lock_guard lk(m_mutex); for (const auto& t : m_availableTransfers) { if (t.visible && isIncluded(t, flags)) { @@ -643,7 +643,7 @@ void TransfersContainer::getOutputs(std::vector& t } } -bool TransfersContainer::getTransactionInformation(const Hash& transactionHash, TransactionInformation& info, int64_t& txBalance) { +bool TransfersContainer::getTransactionInformation(const Hash& transactionHash, TransactionInformation& info, int64_t& txBalance) const { std::lock_guard lk(m_mutex); auto it = m_transactions.find(transactionHash); if (it == m_transactions.end()) { @@ -682,7 +682,7 @@ bool TransfersContainer::getTransactionInformation(const Hash& transactionHash, } std::vector TransfersContainer::getTransactionOutputs(const Hash& transactionHash, - uint32_t flags) { + uint32_t flags) const { std::lock_guard lk(m_mutex); std::vector result; @@ -704,10 +704,37 @@ std::vector TransfersContainer::getTransactionOutp } } + if ((flags & IncludeStateSpent) != 0) { + auto spentRange = m_spentTransfers.get().equal_range(transactionHash); + for (auto i = spentRange.first; i != spentRange.second; ++i) { + if (isIncluded(i->type, IncludeStateAll, flags)) { + result.push_back(*i); + } + } + } + return result; } -void TransfersContainer::getUnconfirmedTransactions(std::vector& transactions) { +std::vector TransfersContainer::getTransactionInputs(const Hash& transactionHash, uint32_t flags) const { + //only type flags are feasible + assert((flags & IncludeStateAll) == 0); + flags |= IncludeStateUnlocked; + + std::lock_guard lk(m_mutex); + + std::vector result; + auto transactionInputsRange = m_spentTransfers.get().equal_range(transactionHash); + for (auto it = transactionInputsRange.first; it != transactionInputsRange.second; ++it) { + if (isIncluded(it->type, IncludeStateUnlocked, flags)) { + result.push_back(*it); + } + } + + return result; +} + +void TransfersContainer::getUnconfirmedTransactions(std::vector& transactions) const { std::lock_guard lk(m_mutex); transactions.clear(); for (auto& element : m_transactions) { @@ -717,7 +744,7 @@ void TransfersContainer::getUnconfirmedTransactions(std::vector& t } } -std::vector TransfersContainer::getSpentOutputs() { +std::vector TransfersContainer::getSpentOutputs() const { std::lock_guard lk(m_mutex); std::vector spentOutputs; @@ -788,7 +815,7 @@ void TransfersContainer::load(std::istream& in) { bool TransfersContainer::isSpendTimeUnlocked(uint64_t unlockTime) const { if (unlockTime < m_currency.maxBlockHeight()) { // interpret as block index - return m_currentHeight - 1 + m_currency.lockedTxAllowedDeltaBlocks() >= unlockTime; + return m_currentHeight + m_currency.lockedTxAllowedDeltaBlocks() >= unlockTime; } else { //interpret as time uint64_t current_time = static_cast(time(NULL)); diff --git a/src/Transfers/TransfersContainer.h b/src/Transfers/TransfersContainer.h index 81e1474d..6cdb9ff0 100755 --- a/src/Transfers/TransfersContainer.h +++ b/src/Transfers/TransfersContainer.h @@ -161,14 +161,16 @@ public: bool advanceHeight(uint32_t height); // ITransfersContainer - virtual size_t transfersCount() override; - virtual size_t transactionsCount() override; - virtual uint64_t balance(uint32_t flags) override; - virtual void getOutputs(std::vector& transfers, uint32_t flags) override; - virtual bool getTransactionInformation(const Crypto::Hash& transactionHash, TransactionInformation& info, int64_t& txBalance) override; - virtual std::vector getTransactionOutputs(const Crypto::Hash& transactionHash, uint32_t flags) override; - virtual void getUnconfirmedTransactions(std::vector& transactions) override; - virtual std::vector getSpentOutputs() override; + virtual size_t transfersCount() const override; + virtual size_t transactionsCount() const override; + virtual uint64_t balance(uint32_t flags) const override; + virtual void getOutputs(std::vector& transfers, uint32_t flags) const override; + virtual bool getTransactionInformation(const Crypto::Hash& transactionHash, TransactionInformation& info, int64_t& txBalance) const override; + virtual std::vector getTransactionOutputs(const Crypto::Hash& transactionHash, uint32_t flags) const override; + //only type flags are feasible for this function + virtual std::vector getTransactionInputs(const Crypto::Hash& transactionHash, uint32_t flags) const override; + virtual void getUnconfirmedTransactions(std::vector& transactions) const override; + virtual std::vector getSpentOutputs() const override; // IStreamSerializable virtual void save(std::ostream& os) override; @@ -280,7 +282,7 @@ private: uint32_t m_currentHeight; // current height is needed to check if a transfer is unlocked size_t m_transactionSpendableAge; const CryptoNote::Currency& m_currency; - std::mutex m_mutex; + mutable std::mutex m_mutex; }; } diff --git a/src/Wallet/IFusionManager.h b/src/Wallet/IFusionManager.h index dd132900..276b8bfc 100644 --- a/src/Wallet/IFusionManager.h +++ b/src/Wallet/IFusionManager.h @@ -25,7 +25,7 @@ namespace CryptoNote { class IFusionManager { public: struct EstimateResult { - size_t belowThresholdCount; + size_t fusionReadyCount; size_t totalOutputCount; }; diff --git a/src/Wallet/WalletErrors.h b/src/Wallet/WalletErrors.h index 2943463e..a0e55413 100644 --- a/src/Wallet/WalletErrors.h +++ b/src/Wallet/WalletErrors.h @@ -42,7 +42,10 @@ enum WalletErrorCodes { TX_TRANSFER_IMPOSSIBLE, WRONG_VERSION, FEE_TOO_SMALL, - KEY_GENERATION_ERROR + KEY_GENERATION_ERROR, + INDEX_OUT_OF_RANGE, + ADDRESS_ALREADY_EXISTS, + TRACKING_MODE }; // custom category: @@ -60,24 +63,27 @@ public: virtual std::string message(int ev) const { switch (ev) { - case NOT_INITIALIZED: return "Object was not initialized"; - case WRONG_PASSWORD: return "The password is wrong"; - case ALREADY_INITIALIZED: return "The object is already initialized"; - case INTERNAL_WALLET_ERROR: return "Internal error occured"; - case MIXIN_COUNT_TOO_BIG: return "MixIn count is too big"; - case BAD_ADDRESS: return "Bad address"; + case NOT_INITIALIZED: return "Object was not initialized"; + case WRONG_PASSWORD: return "The password is wrong"; + case ALREADY_INITIALIZED: return "The object is already initialized"; + case INTERNAL_WALLET_ERROR: return "Internal error occured"; + case MIXIN_COUNT_TOO_BIG: return "MixIn count is too big"; + case BAD_ADDRESS: return "Bad address"; case TRANSACTION_SIZE_TOO_BIG: return "Transaction size is too big"; - case WRONG_AMOUNT: return "Wrong amount"; - case SUM_OVERFLOW: return "Sum overflow"; - case ZERO_DESTINATION: return "The destination is empty"; - case TX_CANCEL_IMPOSSIBLE: return "Impossible to cancel transaction"; - case WRONG_STATE: return "The wallet is in wrong state (maybe loading or saving), try again later"; - case OPERATION_CANCELLED: return "The operation you've requested has been cancelled"; - case TX_TRANSFER_IMPOSSIBLE: return "Transaction transfer impossible"; - case WRONG_VERSION: return "Wrong version"; - case FEE_TOO_SMALL: return "Transaction fee is too small"; - case KEY_GENERATION_ERROR: return "Cannot generate new key"; - default: return "Unknown error"; + case WRONG_AMOUNT: return "Wrong amount"; + case SUM_OVERFLOW: return "Sum overflow"; + case ZERO_DESTINATION: return "The destination is empty"; + case TX_CANCEL_IMPOSSIBLE: return "Impossible to cancel transaction"; + case WRONG_STATE: return "The wallet is in wrong state (maybe loading or saving), try again later"; + case OPERATION_CANCELLED: return "The operation you've requested has been cancelled"; + case TX_TRANSFER_IMPOSSIBLE: return "Transaction transfer impossible"; + case WRONG_VERSION: return "Wrong version"; + case FEE_TOO_SMALL: return "Transaction fee is too small"; + case KEY_GENERATION_ERROR: return "Cannot generate new key"; + case INDEX_OUT_OF_RANGE: return "Index is out of range"; + case ADDRESS_ALREADY_EXISTS: return "Address already exists"; + case TRACKING_MODE: return "The wallet is in tracking mode"; + default: return "Unknown error"; } } diff --git a/src/Wallet/WalletGreen.cpp b/src/Wallet/WalletGreen.cpp index f83c5a40..24c46698 100755 --- a/src/Wallet/WalletGreen.cpp +++ b/src/Wallet/WalletGreen.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -48,10 +49,6 @@ using namespace CryptoNote; namespace { -const uint32_t WALLET_SOFTLOCK_BLOCKS_COUNT = 1; - -const uint64_t DUST_THRESHOLD = 10000; - void asyncRequestCompletion(System::Event& requestFinished) { requestFinished.set(); } @@ -152,18 +149,23 @@ CryptoNote::WalletEvent makeSyncCompletedEvent() { return event; } +size_t getTransactionSize(const ITransactionReader& transaction) { + return transaction.getTransactionData().size(); +} + } namespace CryptoNote { -WalletGreen::WalletGreen(System::Dispatcher& dispatcher, const Currency& currency, INode& node) : +WalletGreen::WalletGreen(System::Dispatcher& dispatcher, const Currency& currency, INode& node, uint32_t transactionSoftLockTime) : m_dispatcher(dispatcher), m_currency(currency), m_node(node), m_blockchainSynchronizer(node, currency.genesisBlockHash()), m_synchronizer(currency, m_blockchainSynchronizer, node), m_eventOccured(m_dispatcher), - m_readyEvent(m_dispatcher) + m_readyEvent(m_dispatcher), + m_transactionSoftLockTime(transactionSoftLockTime) { m_upperTransactionSizeLimit = m_currency.blockGrantedFullRewardZone() * 125 / 100 - m_currency.minerTxBlobReservedSize(); m_readyEvent.set(); @@ -226,6 +228,7 @@ void WalletGreen::clearCaches() { m_change.clear(); m_actualBalance = 0; m_pendingBalance = 0; + m_fusionTxsCache.clear(); } void WalletGreen::initWithKeys(const Crypto::PublicKey& viewPublicKey, const Crypto::SecretKey& viewSecretKey, const std::string& password) { @@ -272,7 +275,8 @@ void WalletGreen::unsafeSave(std::ostream& destination, bool saveDetails, bool s m_unlockTransactionsJob, m_change, m_transactions, - m_transfers + m_transfers, + m_transactionSoftLockTime ); StdOutputStream output(destination); @@ -312,7 +316,8 @@ void WalletGreen::unsafeLoad(std::istream& source, const std::string& password) m_unlockTransactionsJob, m_change, m_transactions, - m_transfers + m_transfers, + m_transactionSoftLockTime ); StdInputStream inputStream(source); @@ -387,6 +392,10 @@ std::string WalletGreen::createAddress(const Crypto::SecretKey& spendSecretKey) return doCreateAddress(spendPublicKey, spendSecretKey); } +std::string WalletGreen::createAddress(const Crypto::PublicKey& spendPublicKey) { + return doCreateAddress(spendPublicKey, NULL_SECRET_KEY); +} + std::string WalletGreen::doCreateAddress(const Crypto::PublicKey& spendPublicKey, const Crypto::SecretKey& spendSecretKey) { throwIfNotInitialized(); throwIfStopped(); @@ -395,15 +404,36 @@ std::string WalletGreen::doCreateAddress(const Crypto::PublicKey& spendPublicKey m_blockchainSynchronizer.stop(); } - addWallet(spendPublicKey, spendSecretKey); - std::string address = m_currency.accountAddressAsString({ spendPublicKey, m_viewPublicKey }); + try { + addWallet(spendPublicKey, spendSecretKey); + } catch (std::exception&) { + if (m_walletsContainer.get().size() != 0) { + m_blockchainSynchronizer.start(); + } + + throw; + } m_blockchainSynchronizer.start(); - return address; + return m_currency.accountAddressAsString({ spendPublicKey, m_viewPublicKey }); } void WalletGreen::addWallet(const Crypto::PublicKey& spendPublicKey, const Crypto::SecretKey& spendSecretKey) { + auto& index = m_walletsContainer.get(); + + auto trackingMode = getTrackingMode(); + + if ((trackingMode == WalletTrackingMode::TRACKING && spendSecretKey != NULL_SECRET_KEY) || + (trackingMode == WalletTrackingMode::NOT_TRACKING && spendSecretKey == NULL_SECRET_KEY)) { + throw std::system_error(make_error_code(error::BAD_ADDRESS)); + } + + auto insertIt = index.find(spendPublicKey); + if (insertIt != index.end()) { + throw std::system_error(make_error_code(error::ADDRESS_ALREADY_EXISTS)); + } + time_t creationTimestamp = time(nullptr); AccountSubscription sub; @@ -411,7 +441,7 @@ void WalletGreen::addWallet(const Crypto::PublicKey& spendPublicKey, const Crypt sub.keys.address.spendPublicKey = spendPublicKey; sub.keys.viewSecretKey = m_viewSecretKey; sub.keys.spendSecretKey = spendSecretKey; - sub.transactionSpendableAge = 10; + sub.transactionSpendableAge = m_transactionSoftLockTime; sub.syncStart.height = 0; sub.syncStart.timestamp = static_cast(creationTimestamp) - (60 * 60 * 24); @@ -425,7 +455,7 @@ void WalletGreen::addWallet(const Crypto::PublicKey& spendPublicKey, const Crypt wallet.creationTimestamp = creationTimestamp; trSubscription.addObserver(this); - m_walletsContainer.get().push_back(std::move(wallet)); + index.insert(insertIt, std::move(wallet)); } void WalletGreen::deleteAddress(const std::string& address) { @@ -495,7 +525,11 @@ WalletTransaction WalletGreen::getTransaction(size_t transactionIndex) const { throwIfNotInitialized(); throwIfStopped(); - return m_transactions.get().at(transactionIndex); + if (m_transactions.size() <= transactionIndex) { + throw std::system_error(make_error_code(CryptoNote::error::INDEX_OUT_OF_RANGE)); + } + + return m_transactions.get()[transactionIndex]; } size_t WalletGreen::getTransactionTransferCount(size_t transactionIndex) const { @@ -554,6 +588,7 @@ size_t WalletGreen::transfer( throwIfNotInitialized(); throwIfStopped(); + throwIfTrackingMode(); return doTransfer(pickWalletsWithMoney(), destinations, fee, mixIn, extra, unlockTimestamp); } @@ -580,6 +615,7 @@ size_t WalletGreen::transfer( throwIfNotInitialized(); throwIfStopped(); + throwIfTrackingMode(); WalletOuts wallet = pickWallet(sourceAddress); std::vector wallets; @@ -610,7 +646,7 @@ size_t WalletGreen::doTransfer(std::vector&& wallets, uint64_t neededMoney = countNeededMoney(destinations, fee); std::vector selectedTransfers; - uint64_t foundMoney = selectTransfers(neededMoney, mixIn == 0, DUST_THRESHOLD, std::move(wallets), selectedTransfers); + uint64_t foundMoney = selectTransfers(neededMoney, mixIn == 0, m_currency.defaultDustThreshold(), std::move(wallets), selectedTransfers); if (foundMoney < neededMoney) { throw std::system_error(make_error_code(error::WRONG_AMOUNT), "Not enough money"); @@ -631,16 +667,20 @@ size_t WalletGreen::doTransfer(std::vector&& wallets, changeDestination.amount = foundMoney - neededMoney; std::vector decomposedOutputs; - splitDestinations(destinations, changeDestination, DUST_THRESHOLD, m_currency, decomposedOutputs); + splitDestinations(destinations, changeDestination, m_currency.defaultDustThreshold(), m_currency, decomposedOutputs); std::unique_ptr tx = makeTransaction(decomposedOutputs, keysInfo, extra, unlockTimestamp); size_t txId = insertOutgoingTransaction(tx->getTransactionHash(), -static_cast(neededMoney), fee, tx->getExtra(), unlockTimestamp); pushBackOutgoingTransfers(txId, destinations); + m_fusionTxsCache.emplace(txId, false); + + markOutputsSpent(tx->getTransactionHash(), selectedTransfers); try { sendTransaction(tx.get()); } catch (std::exception&) { + deleteSpentOutputs(tx->getTransactionHash()); pushEvent(makeTransactionCreatedEvent(txId)); throw; } @@ -650,7 +690,6 @@ size_t WalletGreen::doTransfer(std::vector&& wallets, m_transactions.get().modify(txIt, [] (WalletTransaction& tx) { tx.state = WalletTransactionState::SUCCEEDED; }); - markOutputsSpent(tx->getTransactionHash(), selectedTransfers); m_change[tx->getTransactionHash()] = changeDestination.amount; updateUsedWalletsBalances(selectedTransfers); @@ -677,6 +716,7 @@ size_t WalletGreen::insertOutgoingTransaction(const Hash& transactionHash, int64 insertTx.hash = transactionHash; insertTx.totalAmount = totalAmount; insertTx.timestamp = 0; //0 until included in a block + insertTx.isBase = false; size_t txId = m_transactions.get().size(); m_transactions.get().push_back(std::move(insertTx)); @@ -690,18 +730,38 @@ bool WalletGreen::transactionExists(const Hash& hash) { return it != hashIndex.end(); } -void WalletGreen::updateTransactionHeight(const Hash& hash, uint32_t blockHeight) { +bool WalletGreen::updateWalletTransactionInfo(const Hash& hash, const CryptoNote::TransactionInformation& info) { auto& hashIndex = m_transactions.get(); + bool updated = false; auto it = hashIndex.find(hash); if (it != hashIndex.end()) { - bool r = hashIndex.modify(it, [&blockHeight] (WalletTransaction& transaction) { - transaction.blockHeight = blockHeight; - //transaction may be deleted first than added again - transaction.state = WalletTransactionState::SUCCEEDED; + bool r = hashIndex.modify(it, [&info, &updated] (WalletTransaction& transaction) { + if (transaction.blockHeight != info.blockHeight) { + transaction.blockHeight = info.blockHeight; + updated = true; + } + + if (transaction.timestamp != info.timestamp) { + transaction.timestamp = info.timestamp; + updated = true; + } + + if (transaction.state != WalletTransactionState::SUCCEEDED) { + //transaction may be deleted first then added again + transaction.state = WalletTransactionState::SUCCEEDED; + updated = true; + } + + bool isBase = info.totalAmountIn == 0; + if (transaction.isBase != isBase) { + transaction.isBase = isBase; + updated = true; + } }); + assert(r); - return; + return updated; } throw std::system_error(make_error_code(std::errc::invalid_argument)); @@ -720,6 +780,7 @@ size_t WalletGreen::insertIncomingTransaction(const TransactionInformation& info tx.extra.assign(reinterpret_cast(info.extra.data()), info.extra.size()); tx.totalAmount = txBalance; tx.creationTime = info.timestamp; + tx.isBase = info.totalAmountIn == 0; index.push_back(std::move(tx)); return index.size() - 1; @@ -886,7 +947,7 @@ uint64_t WalletGreen::selectTransfers( return foundMoney; }; -std::vector WalletGreen::pickWalletsWithMoney() { +std::vector WalletGreen::pickWalletsWithMoney() const { auto& walletsIndex = m_walletsContainer.get(); std::vector walletOuts; @@ -986,6 +1047,7 @@ void WalletGreen::prepareInputs( keyInfo.realOutput.transactionIndex = static_cast(insertedIn - keyInfo.outputs.begin()); keyInfo.realOutput.outputInTransaction = input.out.outputInTransaction; + //Important! outputs in selectedTransfers and in keysInfo must have the same order! InputInfo inputInfo; inputInfo.keyInfo = std::move(keyInfo); inputInfo.walletRecord = input.wallet; @@ -1028,23 +1090,27 @@ void WalletGreen::throwIfNotInitialized() const { void WalletGreen::onError(ITransfersSubscription* object, uint32_t height, std::error_code ec) { } -void WalletGreen::synchronizationProgressUpdated(uint32_t current, uint32_t total) { - m_dispatcher.remoteSpawn( [current, total, this] () { onSynchronizationProgressUpdated(current, total); } ); +void WalletGreen::synchronizationProgressUpdated(uint32_t processedBlockCount, uint32_t totalBlockCount) { + m_dispatcher.remoteSpawn( [processedBlockCount, totalBlockCount, this] () { onSynchronizationProgressUpdated(processedBlockCount, totalBlockCount); } ); } void WalletGreen::synchronizationCompleted(std::error_code result) { m_dispatcher.remoteSpawn([this] () { onSynchronizationCompleted(); } ); } -void WalletGreen::onSynchronizationProgressUpdated(uint32_t current, uint32_t total) { +void WalletGreen::onSynchronizationProgressUpdated(uint32_t processedBlockCount, uint32_t totalBlockCount) { + assert(processedBlockCount > 0); + System::EventLock lk(m_readyEvent); if (m_state == WalletState::NOT_INITIALIZED) { return; } - pushEvent(makeSyncProgressUpdatedEvent(current, total)); - unlockBalances(current); + pushEvent(makeSyncProgressUpdatedEvent(processedBlockCount, totalBlockCount)); + + uint32_t currentHeight = processedBlockCount - 1; + unlockBalances(currentHeight); } void WalletGreen::onSynchronizationCompleted() { @@ -1061,12 +1127,14 @@ void WalletGreen::unlockBalances(uint32_t height) { auto& index = m_unlockTransactionsJob.get(); auto upper = index.upper_bound(height); - for (auto it = index.begin(); it != upper; ++it) { - updateBalance(it->container); - } + if (index.begin() != upper) { + for (auto it = index.begin(); it != upper; ++it) { + updateBalance(it->container); + } - index.erase(index.begin(), upper); - pushEvent(makeMoneyUnlockedEvent()); + index.erase(index.begin(), upper); + pushEvent(makeMoneyUnlockedEvent()); + } } void WalletGreen::onTransactionUpdated(ITransfersSubscription* object, const Hash& transactionHash) { @@ -1089,29 +1157,30 @@ void WalletGreen::transactionUpdated(ITransfersSubscription* object, const Hash& bool found = container->getTransactionInformation(transactionHash, info, txBalance); assert(found); - WalletEvent event; - if (transactionExists(info.transactionHash)) { - updateTransactionHeight(info.transactionHash, info.blockHeight); + bool updated = updateWalletTransactionInfo(info.transactionHash, info); auto id = getTransactionId(info.transactionHash); - event = makeTransactionUpdatedEvent(id); + + if (updated) { + pushEvent(makeTransactionUpdatedEvent(id)); + } } else { auto id = insertIncomingTransaction(info, txBalance); insertIncomingTransfer(id, m_currency.accountAddressAsString({ getWalletRecord(container).spendPublicKey, m_viewPublicKey }), txBalance); + m_fusionTxsCache.emplace(id, isFusionTransaction(m_transactions.get()[id])); - event = makeTransactionCreatedEvent(id); + pushEvent(makeTransactionCreatedEvent(id)); } + m_change.erase(transactionHash); + if (info.blockHeight != CryptoNote::WALLET_UNCONFIRMED_TRANSACTION_HEIGHT) { - //TODO: make proper calculation of unlock height - uint32_t height = info.blockHeight + static_cast(info.unlockTime) + WALLET_SOFTLOCK_BLOCKS_COUNT + 1; - m_change.erase(transactionHash); - insertUnlockTransactionJob(transactionHash, height, container); + uint32_t unlockHeight = std::max(info.blockHeight + m_transactionSoftLockTime, static_cast(info.unlockTime)); + insertUnlockTransactionJob(transactionHash, unlockHeight, container); } updateBalance(container); - pushEvent(event); } void WalletGreen::pushEvent(const WalletEvent& event) { @@ -1296,19 +1365,288 @@ void WalletGreen::throwIfStopped() const { } } +void WalletGreen::throwIfTrackingMode() const { + if (getTrackingMode() == WalletTrackingMode::TRACKING) { + throw std::system_error(make_error_code(error::TRACKING_MODE)); + } +} + +WalletGreen::WalletTrackingMode WalletGreen::getTrackingMode() const { + if (m_walletsContainer.get().empty()) { + return WalletTrackingMode::NO_ADDRESSES; + } + + return m_walletsContainer.get().begin()->spendSecretKey == NULL_SECRET_KEY ? + WalletTrackingMode::TRACKING : WalletTrackingMode::NOT_TRACKING; +} + size_t WalletGreen::createFusionTransaction(uint64_t threshold, uint64_t mixin) { - // TODO NOT IMPLEMENTED - throw std::runtime_error("WalletGreen::createFusionTransaction not implemented."); + System::EventLock lk(m_readyEvent); + + throwIfNotInitialized(); + throwIfStopped(); + + //TODO: check if wallet is not in tracking mode + const size_t MAX_FUSION_OUTPUT_COUNT = 4; + + if (threshold <= m_currency.defaultDustThreshold()) { + throw std::runtime_error("Threshold must be greater than " + std::to_string(m_currency.defaultDustThreshold())); + } + + if (m_walletsContainer.get().size() == 0) { + throw std::runtime_error("You must have at least one address"); + } + + size_t estimatedFusionInputsCount = m_currency.getApproximateMaximumInputCount(m_currency.fusionTxMaxSize(), MAX_FUSION_OUTPUT_COUNT, mixin); + if (estimatedFusionInputsCount < m_currency.fusionTxMinInputCount()) { + throw std::system_error(make_error_code(error::MIXIN_COUNT_TOO_BIG)); + } + + std::vector fusionInputs = pickRandomFusionInputs(threshold, m_currency.fusionTxMinInputCount(), estimatedFusionInputsCount); + if (fusionInputs.size() < m_currency.fusionTxMinInputCount()) { + //nothing to optimize + return WALLET_INVALID_TRANSACTION_ID; + } + + typedef CryptoNote::COMMAND_RPC_GET_RANDOM_OUTPUTS_FOR_AMOUNTS::outs_for_amount outs_for_amount; + std::vector mixinResult; + if (mixin != 0) { + requestMixinOuts(fusionInputs, mixin, mixinResult); + } + + std::vector keysInfo; + prepareInputs(fusionInputs, mixinResult, mixin, keysInfo); + + std::unique_ptr fusionTransaction; + size_t transactionSize; + int round = 0; + uint64_t transactionAmount; + do { + if (round != 0) { + fusionInputs.pop_back(); + keysInfo.pop_back(); + } + + uint64_t inputsAmount = std::accumulate(fusionInputs.begin(), fusionInputs.end(), static_cast(0), [] (uint64_t amount, const OutputToTransfer& input) { + return amount + input.out.amount; + }); + + transactionAmount = inputsAmount; + + ReceiverAmounts decomposedOutputs = decomposeFusionOutputs(inputsAmount); + assert(decomposedOutputs.amounts.size() <= MAX_FUSION_OUTPUT_COUNT); + + fusionTransaction = makeTransaction(std::vector{decomposedOutputs}, keysInfo, "", 0); + + transactionSize = getTransactionSize(*fusionTransaction); + ++round; + } while (transactionSize > m_currency.fusionTxMaxSize() && fusionInputs.size() >= m_currency.fusionTxMinInputCount()); + + if (fusionInputs.size() < m_currency.fusionTxMinInputCount()) { + throw std::runtime_error("Unable to create fusion transaction"); + } + + size_t transactionId = insertOutgoingTransaction(fusionTransaction->getTransactionHash(), 0, 0, fusionTransaction->getExtra(), 0); + m_fusionTxsCache.emplace(transactionId, true); + + WalletTransfer destination = {m_currency.accountAddressAsString({m_walletsContainer.get().begin()->spendPublicKey, + m_viewPublicKey }), 0}; + pushBackOutgoingTransfers(transactionId, std::vector {destination}); + + markOutputsSpent(fusionTransaction->getTransactionHash(), fusionInputs); + + try { + sendTransaction(fusionTransaction.get()); + } catch (std::exception&) { + deleteSpentOutputs(fusionTransaction->getTransactionHash()); + pushEvent(makeTransactionCreatedEvent(transactionId)); + throw; + } + + auto txIt = m_transactions.get().begin(); + std::advance(txIt, transactionId); + m_transactions.get().modify(txIt, + [] (WalletTransaction& tx) { tx.state = WalletTransactionState::SUCCEEDED; }); + + m_change[fusionTransaction->getTransactionHash()] = transactionAmount; + + updateUsedWalletsBalances(fusionInputs); + + pushEvent(makeTransactionCreatedEvent(transactionId)); + + return transactionId; +} + +WalletGreen::ReceiverAmounts WalletGreen::decomposeFusionOutputs(uint64_t inputsAmount) { + assert(m_walletsContainer.get().size() > 0); + + WalletGreen::ReceiverAmounts outputs; + outputs.receiver = {m_walletsContainer.get().begin()->spendPublicKey, m_viewPublicKey}; + + decomposeAmount(inputsAmount, 0, outputs.amounts); + std::sort(outputs.amounts.begin(), outputs.amounts.end()); + + return outputs; } bool WalletGreen::isFusionTransaction(size_t transactionId) const { - // TODO NOT IMPLEMENTED - throw std::runtime_error("WalletGreen::isFusionTransaction not implemented."); + throwIfNotInitialized(); + throwIfStopped(); + + if (m_transactions.size() <= transactionId) { + throw std::system_error(make_error_code(CryptoNote::error::INDEX_OUT_OF_RANGE)); + } + + auto isFusionIter = m_fusionTxsCache.find(transactionId); + if (isFusionIter != m_fusionTxsCache.end()) { + return isFusionIter->second; + } + + bool result = isFusionTransaction(m_transactions.get()[transactionId]); + m_fusionTxsCache.emplace(transactionId, result); + return result; +} + +bool WalletGreen::isFusionTransaction(const WalletTransaction& walletTx) const { + if (walletTx.fee != 0) { + return false; + } + + uint64_t inputsSum = 0; + uint64_t outputsSum = 0; + std::vector outputsAmounts; + std::vector inputsAmounts; + TransactionInformation txInfo; + bool gotTx = false; + const auto& walletsIndex = m_walletsContainer.get(); + for (const WalletRecord& wallet : walletsIndex) { + for (const TransactionOutputInformation& output : wallet.container->getTransactionOutputs(walletTx.hash, ITransfersContainer::IncludeTypeKey | ITransfersContainer::IncludeStateAll)) { + if (outputsAmounts.size() <= output.outputInTransaction) { + outputsAmounts.resize(output.outputInTransaction + 1, 0); + } + + assert(output.amount != 0); + assert(outputsAmounts[output.outputInTransaction] == 0); + outputsAmounts[output.outputInTransaction] = output.amount; + outputsSum += output.amount; + } + + for (const TransactionOutputInformation& input : wallet.container->getTransactionInputs(walletTx.hash, ITransfersContainer::IncludeTypeKey)) { + inputsSum += input.amount; + inputsAmounts.push_back(input.amount); + } + + if (!gotTx) { + int64_t ignore; + gotTx = wallet.container->getTransactionInformation(walletTx.hash, txInfo, ignore); + } + } + + if (!gotTx) { + return false; + } + + if (outputsSum != inputsSum || outputsSum != txInfo.totalAmountOut || inputsSum != txInfo.totalAmountIn) { + return false; + } else { + return m_currency.isFusionTransaction(inputsAmounts, outputsAmounts, 0); //size = 0 here because can't get real size of tx in wallet. + } } IFusionManager::EstimateResult WalletGreen::estimate(uint64_t threshold) const { - // TODO NOT IMPLEMENTED - throw std::runtime_error("WalletGreen::estimate not implemented."); + throwIfNotInitialized(); + throwIfStopped(); + + IFusionManager::EstimateResult result{0, 0}; + auto walletOuts = pickWalletsWithMoney(); + std::array::digits10 + 1> bucketSizes; + bucketSizes.fill(0); + for (size_t walletIndex = 0; walletIndex < walletOuts.size(); ++walletIndex) { + for (auto& out : walletOuts[walletIndex].outs) { + uint8_t powerOfTen = 0; + if (m_currency.isAmountApplicableInFusionTransactionInput(out.amount, threshold, powerOfTen) && !isOutputUsed(out)) { + assert(powerOfTen < std::numeric_limits::digits10 + 1); + bucketSizes[powerOfTen]++; + } + } + + result.totalOutputCount += walletOuts[walletIndex].outs.size(); + } + + for (auto bucketSize : bucketSizes) { + if (bucketSize >= m_currency.fusionTxMinInputCount()) { + result.fusionReadyCount += bucketSize; + } + } + + return result; +} + +std::vector WalletGreen::pickRandomFusionInputs(uint64_t threshold, size_t minInputCount, size_t maxInputCount) { + std::vector allFusionReadyOuts; + auto walletOuts = pickWalletsWithMoney(); + std::array::digits10 + 1> bucketSizes; + bucketSizes.fill(0); + for (size_t walletIndex = 0; walletIndex < walletOuts.size(); ++walletIndex) { + for (auto& out : walletOuts[walletIndex].outs) { + uint8_t powerOfTen = 0; + if (m_currency.isAmountApplicableInFusionTransactionInput(out.amount, threshold, powerOfTen) && !isOutputUsed(out)) { + allFusionReadyOuts.push_back({std::move(out), walletOuts[walletIndex].wallet}); + assert(powerOfTen < std::numeric_limits::digits10 + 1); + bucketSizes[powerOfTen]++; + } + } + } + + //now, pick the bucket + std::vector bucketNumbers(bucketSizes.size()); + std::iota(bucketNumbers.begin(), bucketNumbers.end(), 0); + std::shuffle(bucketNumbers.begin(), bucketNumbers.end(), std::default_random_engine{Crypto::rand()}); + size_t bucketNumberIndex = 0; + for (; bucketNumberIndex < bucketNumbers.size(); ++bucketNumberIndex) { + if (bucketSizes[bucketNumbers[bucketNumberIndex]] >= minInputCount) { + break; + } + } + + if (bucketNumberIndex == bucketNumbers.size()) { + return {}; + } + + size_t selectedBucket = bucketNumbers[bucketNumberIndex]; + assert(selectedBucket < std::numeric_limits::digits10 + 1); + assert(bucketSizes[selectedBucket] >= minInputCount); + uint64_t lowerBound = 1; + for (size_t i = 0; i < selectedBucket; ++i) { + lowerBound *= 10; + } + + uint64_t upperBound = selectedBucket == std::numeric_limits::digits10 ? UINT64_MAX : lowerBound * 10; + std::vector selectedOuts; + selectedOuts.reserve(bucketSizes[selectedBucket]); + for (size_t outIndex = 0; outIndex < allFusionReadyOuts.size(); ++outIndex) { + if (allFusionReadyOuts[outIndex].out.amount >= lowerBound && allFusionReadyOuts[outIndex].out.amount < upperBound) { + selectedOuts.push_back(std::move(allFusionReadyOuts[outIndex])); + } + } + + assert(selectedOuts.size() >= minInputCount); + + auto outputsSortingFunction = [](const OutputToTransfer& l, const OutputToTransfer& r) { return l.out.amount < r.out.amount; }; + if (selectedOuts.size() <= maxInputCount) { + std::sort(selectedOuts.begin(), selectedOuts.end(), outputsSortingFunction); + return selectedOuts; + } + + ShuffleGenerator> generator(selectedOuts.size()); + std::vector trimmedSelectedOuts; + trimmedSelectedOuts.reserve(maxInputCount); + for (size_t i = 0; i < maxInputCount; ++i) { + trimmedSelectedOuts.push_back(std::move(selectedOuts[generator()])); + } + + std::sort(trimmedSelectedOuts.begin(), trimmedSelectedOuts.end(), outputsSortingFunction); + return trimmedSelectedOuts; } } //namespace CryptoNote diff --git a/src/Wallet/WalletGreen.h b/src/Wallet/WalletGreen.h index 5cb18778..65e1ae6f 100755 --- a/src/Wallet/WalletGreen.h +++ b/src/Wallet/WalletGreen.h @@ -36,7 +36,7 @@ class WalletGreen : public IWallet, IBlockchainSynchronizerObserver, IFusionManager { public: - WalletGreen(System::Dispatcher& dispatcher, const Currency& currency, INode& node); + WalletGreen(System::Dispatcher& dispatcher, const Currency& currency, INode& node, uint32_t transactionSoftLockTime = 1); virtual ~WalletGreen(); virtual void initialize(const std::string& password) override; @@ -53,6 +53,7 @@ public: virtual KeyPair getViewKey() const override; virtual std::string createAddress() override; virtual std::string createAddress(const Crypto::SecretKey& spendSecretKey) override; + virtual std::string createAddress(const Crypto::PublicKey& spendPublicKey) override; virtual void deleteAddress(const std::string& address) override; virtual uint64_t getActualBalance() const override; @@ -81,6 +82,7 @@ public: protected: void throwIfNotInitialized() const; void throwIfStopped() const; + void throwIfTrackingMode() const; void doShutdown(); void clearCaches(); void initWithKeys(const Crypto::PublicKey& viewPublicKey, const Crypto::SecretKey& viewSecretKey, const std::string& password); @@ -109,16 +111,18 @@ protected: virtual void onError(ITransfersSubscription* object, uint32_t height, std::error_code ec) override; virtual void onTransactionUpdated(ITransfersSubscription* object, const Crypto::Hash& transactionHash) override; + void transactionUpdated(ITransfersSubscription* object, const Crypto::Hash& transactionHash); + virtual void onTransactionDeleted(ITransfersSubscription* object, const Crypto::Hash& transactionHash) override; - virtual void synchronizationProgressUpdated(uint32_t current, uint32_t total) override; + void transactionDeleted(ITransfersSubscription* object, const Crypto::Hash& transactionHash); + + virtual void synchronizationProgressUpdated(uint32_t processedBlockCount, uint32_t totalBlockCount) override; virtual void synchronizationCompleted(std::error_code result) override; - void transactionUpdated(ITransfersSubscription* object, const Crypto::Hash& transactionHash); - void transactionDeleted(ITransfersSubscription* object, const Crypto::Hash& transactionHash); - void onSynchronizationProgressUpdated(uint32_t current, uint32_t total); + void onSynchronizationProgressUpdated(uint32_t processedBlockCount, uint32_t totalBlockCount); void onSynchronizationCompleted(); - std::vector pickWalletsWithMoney(); + std::vector pickWalletsWithMoney() const; WalletOuts pickWallet(const std::string& address); void updateBalance(CryptoNote::ITransfersContainer* container); @@ -138,6 +142,7 @@ protected: AccountKeys makeAccountKeys(const WalletRecord& wallet) const; size_t getTransactionId(const Crypto::Hash& transactionHash) const; void pushEvent(const WalletEvent& event); + bool isFusionTransaction(const WalletTransaction& walletTx) const; size_t doTransfer(std::vector&& wallets, const std::vector& destinations, @@ -171,7 +176,7 @@ protected: size_t insertOutgoingTransaction(const Crypto::Hash& transactionHash, int64_t totalAmount, uint64_t fee, const BinaryArray& extra, uint64_t unlockTimestamp); bool transactionExists(const Crypto::Hash& hash); - void updateTransactionHeight(const Crypto::Hash& hash, uint32_t blockHeight); + bool updateWalletTransactionInfo(const Crypto::Hash& hash, const CryptoNote::TransactionInformation& info); size_t insertIncomingTransaction(const TransactionInformation& info, int64_t txBalance); void insertIncomingTransfer(size_t txId, const std::string& address, int64_t amount); void pushBackOutgoingTransfers(size_t txId, const std::vector &destinations); @@ -181,12 +186,21 @@ protected: void unsafeLoad(std::istream& source, const std::string& password); void unsafeSave(std::ostream& destination, bool saveDetails, bool saveCache); - + std::vector pickRandomFusionInputs(uint64_t threshold, size_t minInputCount, size_t maxInputCount); + ReceiverAmounts decomposeFusionOutputs(uint64_t inputsAmount); enum class WalletState { INITIALIZED, NOT_INITIALIZED }; + enum class WalletTrackingMode { + TRACKING, + NOT_TRACKING, + NO_ADDRESSES + }; + + WalletTrackingMode getTrackingMode() const; + std::pair getTransactionTransfers(size_t transactionIndex) const; System::Dispatcher& m_dispatcher; @@ -200,6 +214,7 @@ protected: WalletTransactions m_transactions; WalletTransfers m_transfers; //sorted TransactionChanges m_change; + mutable std::unordered_map m_fusionTxsCache; // txIndex -> isFusion BlockchainSynchronizer m_blockchainSynchronizer; TransfersSyncronizer m_synchronizer; @@ -219,6 +234,7 @@ protected: uint64_t m_pendingBalance = 0; uint64_t m_upperTransactionSizeLimit; + uint32_t m_transactionSoftLockTime; }; } //namespace CryptoNote diff --git a/src/Wallet/WalletRpcServerCommandsDefinitions.h b/src/Wallet/WalletRpcServerCommandsDefinitions.h index b61f127b..e89554ac 100755 --- a/src/Wallet/WalletRpcServerCommandsDefinitions.h +++ b/src/Wallet/WalletRpcServerCommandsDefinitions.h @@ -19,6 +19,7 @@ #include "CryptoNoteProtocol/CryptoNoteProtocolDefinitions.h" #include "CryptoNoteCore/CryptoNoteBasic.h" #include "crypto/hash.h" +#include "Rpc/CoreRpcServerCommandsDefinitions.h" #include "WalletRpcServerErrorCodes.h" namespace Tools @@ -31,13 +32,9 @@ using CryptoNote::ISerializer; #define WALLET_RPC_STATUS_OK "OK" #define WALLET_RPC_STATUS_BUSY "BUSY" - struct EMPTY_STRUCT { - void serialize(ISerializer& s) {} - }; - struct COMMAND_RPC_GET_BALANCE { - typedef EMPTY_STRUCT request; + typedef CryptoNote::EMPTY_STRUCT request; struct response { @@ -93,8 +90,8 @@ using CryptoNote::ISerializer; struct COMMAND_RPC_STORE { - typedef EMPTY_STRUCT request; - typedef EMPTY_STRUCT response; + typedef CryptoNote::EMPTY_STRUCT request; + typedef CryptoNote::EMPTY_STRUCT response; }; struct payment_details @@ -158,7 +155,7 @@ using CryptoNote::ISerializer; }; struct COMMAND_RPC_GET_TRANSFERS { - typedef EMPTY_STRUCT request; + typedef CryptoNote::EMPTY_STRUCT request; struct response { std::list transfers; @@ -170,7 +167,7 @@ using CryptoNote::ISerializer; }; struct COMMAND_RPC_GET_HEIGHT { - typedef EMPTY_STRUCT request; + typedef CryptoNote::EMPTY_STRUCT request; struct response { uint64_t height; @@ -182,8 +179,8 @@ using CryptoNote::ISerializer; }; struct COMMAND_RPC_RESET { - typedef EMPTY_STRUCT request; - typedef EMPTY_STRUCT response; + typedef CryptoNote::EMPTY_STRUCT request; + typedef CryptoNote::EMPTY_STRUCT response; }; } } diff --git a/src/Wallet/WalletSerialization.cpp b/src/Wallet/WalletSerialization.cpp index f50bfe55..b6a498a3 100755 --- a/src/Wallet/WalletSerialization.cpp +++ b/src/Wallet/WalletSerialization.cpp @@ -248,6 +248,7 @@ CryptoNote::WalletTransaction convert(const CryptoNote::WalletLegacyTransaction& mtx.creationTime = tx.sentTime; mtx.unlockTime = tx.unlockTime; mtx.extra = tx.extra; + mtx.isBase = tx.isCoinbase; return mtx; } @@ -284,7 +285,8 @@ WalletSerializer::WalletSerializer( UnlockTransactionJobs& unlockTransactions, TransactionChanges& change, WalletTransactions& transactions, - WalletTransfers& transfers + WalletTransfers& transfers, + uint32_t transactionSoftLockTime ) : m_transfersObserver(transfersObserver), m_viewPublicKey(viewPublicKey), @@ -297,7 +299,8 @@ WalletSerializer::WalletSerializer( m_unlockTransactions(unlockTransactions), m_change(change), m_transactions(transactions), - m_transfers(transfers) + m_transfers(transfers), + m_transactionSoftLockTime(transactionSoftLockTime) { } void WalletSerializer::save(const std::string& password, Common::IOutputStream& destination, bool saveDetails, bool saveCache) { @@ -556,6 +559,10 @@ void WalletSerializer::loadCurrentVersion(Common::IInputStream& source, const st loadUnlockTransactionsJobs(source, cryptoContext); loadChange(source, cryptoContext); } + + if (details && cache) { + updateTransactionsBaseStatus(); + } } void WalletSerializer::loadWalletV1(Common::IInputStream& source, const std::string& password) { @@ -668,11 +675,32 @@ void WalletSerializer::loadWallets(Common::IInputStream& source, CryptoContext& deserializeEncrypted(count, "wallets_count", cryptoContext, source); cryptoContext.incIv(); + bool isTrackingMode; + for (uint64_t i = 0; i < count; ++i) { WalletRecordDto dto; deserializeEncrypted(dto, "", cryptoContext, source); cryptoContext.incIv(); + if (i == 0) { + isTrackingMode = dto.spendSecretKey == NULL_SECRET_KEY; + } else if ((isTrackingMode && dto.spendSecretKey != NULL_SECRET_KEY) || (!isTrackingMode && dto.spendSecretKey == NULL_SECRET_KEY)) { + throw std::system_error(make_error_code(error::BAD_ADDRESS), "All addresses must be whether tracking or not"); + } + + if (dto.spendSecretKey != NULL_SECRET_KEY) { + Crypto::PublicKey restoredPublicKey; + bool r = Crypto::secret_key_to_public_key(dto.spendSecretKey, restoredPublicKey); + + if (!r || dto.spendPublicKey != restoredPublicKey) { + throw std::system_error(make_error_code(error::WRONG_PASSWORD), "Restored spend public key doesn't correspond to secret key"); + } + } else { + if (!Crypto::check_key(dto.spendPublicKey)) { + throw std::system_error(make_error_code(error::WRONG_PASSWORD), "Public spend key is incorrect"); + } + } + WalletRecord wallet; wallet.spendPublicKey = dto.spendPublicKey; wallet.spendSecretKey = dto.spendSecretKey; @@ -696,7 +724,7 @@ void WalletSerializer::subscribeWallets() { sub.keys.address.spendPublicKey = wallet.spendPublicKey; sub.keys.viewSecretKey = m_viewSecretKey; sub.keys.spendSecretKey = wallet.spendSecretKey; - sub.transactionSpendableAge = 10; + sub.transactionSpendableAge = m_transactionSoftLockTime; sub.syncStart.height = 0; sub.syncStart.timestamp = static_cast(wallet.creationTimestamp) - (60 * 60 * 24); @@ -793,6 +821,26 @@ void WalletSerializer::loadChange(Common::IInputStream& source, CryptoContext& c } } +// can't do it in loadTransactions, TransfersContainer is not yet loaded +void WalletSerializer::updateTransactionsBaseStatus() { + auto& transactions = m_transactions.get(); + auto begin = std::begin(transactions); + auto end = std::end(transactions); + for (; begin != end; ++begin) { + transactions.modify(begin, [this](WalletTransaction& tx) { + auto& wallets = m_walletsContainer.get(); + TransactionInformation txInfo; + auto it = std::find_if(std::begin(wallets), std::end(wallets), [&](const WalletRecord& rec) { + int64_t id = 0; + assert(rec.container != nullptr); + return rec.container->getTransactionInformation(tx.hash, txInfo, id); + }); + + tx.isBase = it != std::end(wallets) && txInfo.totalAmountIn == 0; + }); + } +} + void WalletSerializer::loadTransactions(Common::IInputStream& source, CryptoContext& cryptoContext) { uint64_t count = 0; deserializeEncrypted(count, "transactions_count", cryptoContext, source); @@ -815,6 +863,7 @@ void WalletSerializer::loadTransactions(Common::IInputStream& source, CryptoCont tx.creationTime = dto.creationTime; tx.unlockTime = dto.unlockTime; tx.extra = dto.extra; + tx.isBase = false; m_transactions.get().push_back(std::move(tx)); } diff --git a/src/Wallet/WalletSerialization.h b/src/Wallet/WalletSerialization.h index 4a518a9c..999b7bab 100755 --- a/src/Wallet/WalletSerialization.h +++ b/src/Wallet/WalletSerialization.h @@ -49,7 +49,8 @@ public: UnlockTransactionJobs& unlockTransactions, TransactionChanges& change, WalletTransactions& transactions, - WalletTransfers& transfers + WalletTransfers& transfers, + uint32_t transactionSoftLockTime ); void save(const std::string& password, Common::IOutputStream& destination, bool saveDetails, bool saveCache); @@ -99,6 +100,7 @@ private: void loadWalletV1Keys(CryptoNote::BinaryInputStreamSerializer& serializer); void loadWalletV1Details(CryptoNote::BinaryInputStreamSerializer& serializer); void addWalletV1Details(const std::vector& txs, const std::vector& trs); + void updateTransactionsBaseStatus(); ITransfersObserver& m_transfersObserver; Crypto::PublicKey& m_viewPublicKey; @@ -112,6 +114,7 @@ private: TransactionChanges& m_change; WalletTransactions& m_transactions; WalletTransfers& m_transfers; + uint32_t m_transactionSoftLockTime; }; } //namespace CryptoNote diff --git a/src/version.h.in b/src/version.h.in index 6af740ac..b36e418b 100644 --- a/src/version.h.in +++ b/src/version.h.in @@ -1,4 +1,4 @@ #define BUILD_COMMIT_ID "@VERSION@" -#define PROJECT_VERSION "1.0.7.1" -#define PROJECT_VERSION_BUILD_NO "571" +#define PROJECT_VERSION "1.0.8" +#define PROJECT_VERSION_BUILD_NO "608" #define PROJECT_VERSION_LONG PROJECT_VERSION "." PROJECT_VERSION_BUILD_NO "(" BUILD_COMMIT_ID ")" diff --git a/tests/System/RemoteContextTests.cpp b/tests/System/RemoteContextTests.cpp index cd091342..b82b755c 100644 --- a/tests/System/RemoteContextTests.cpp +++ b/tests/System/RemoteContextTests.cpp @@ -102,14 +102,14 @@ TEST_F(RemoteContextTests, canExecuteOtherContextsWhileWaiting) { ContextGroup cg(dispatcher); cg.spawn([&] { RemoteContext<> context(dispatcher, [&] { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); }); }); cg.spawn([&] { - System::Timer(dispatcher).sleep(std::chrono::milliseconds(5)); + System::Timer(dispatcher).sleep(std::chrono::milliseconds(50)); auto end = std::chrono::high_resolution_clock::now(); - ASSERT_GE(std::chrono::duration_cast(end - start).count(), 5); - ASSERT_LE(std::chrono::duration_cast(end - start).count(), 9); + ASSERT_GE(std::chrono::duration_cast(end - start).count(), 50); + ASSERT_LT(std::chrono::duration_cast(end - start).count(), 100); }); cg.wait(); diff --git a/tests/System/TimerTests.cpp b/tests/System/TimerTests.cpp index aa26d176..061b64c6 100755 --- a/tests/System/TimerTests.cpp +++ b/tests/System/TimerTests.cpp @@ -85,7 +85,7 @@ TEST_F(TimerTests, doubleTimerTest) { first.wait(); second.wait(); ASSERT_GE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - begin).count(), 150); - ASSERT_TRUE((std::chrono::high_resolution_clock::now() - begin) < std::chrono::milliseconds(250)); + ASSERT_TRUE((std::chrono::high_resolution_clock::now() - begin) < std::chrono::milliseconds(275)); } TEST_F(TimerTests, doubleTimerTestGroup) { @@ -136,7 +136,7 @@ TEST_F(TimerTests, doubleTimerTestTwoGroupsWait) { contextGroup.wait(); ASSERT_GE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - begin).count(), 150); - ASSERT_TRUE((std::chrono::high_resolution_clock::now() - begin) < std::chrono::milliseconds(250)); + ASSERT_TRUE((std::chrono::high_resolution_clock::now() - begin) < std::chrono::milliseconds(275)); } TEST_F(TimerTests, movedTimerIsWorking2) { diff --git a/tests/UnitTests/ICoreStub.cpp b/tests/UnitTests/ICoreStub.cpp index 0445b625..2fab23c1 100755 --- a/tests/UnitTests/ICoreStub.cpp +++ b/tests/UnitTests/ICoreStub.cpp @@ -91,12 +91,38 @@ std::vector ICoreStub::getPoolTransactions() { bool ICoreStub::getPoolChanges(const Crypto::Hash& tailBlockId, const std::vector& knownTxsIds, std::vector& addedTxs, std::vector& deletedTxsIds) { - return true; + std::unordered_set knownSet; + for (const Crypto::Hash& txId : knownTxsIds) { + if (transactionPool.find(txId) == transactionPool.end()) { + deletedTxsIds.push_back(txId); + } + + knownSet.insert(txId); + } + + for (const std::pair& poolEntry : transactionPool) { + if (knownSet.find(poolEntry.first) == knownSet.end()) { + addedTxs.push_back(poolEntry.second); + } + } + + return poolChangesResult; } bool ICoreStub::getPoolChangesLite(const Crypto::Hash& tailBlockId, const std::vector& knownTxsIds, std::vector& addedTxs, std::vector& deletedTxsIds) { - return true; + std::vector added; + bool returnStatus = getPoolChanges(tailBlockId, knownTxsIds, added, deletedTxsIds); + + for (const auto& tx : added) { + CryptoNote::TransactionPrefixInfo tpi; + tpi.txPrefix = tx; + tpi.txHash = getObjectHash(tx); + + addedTxs.push_back(std::move(tpi)); + } + + return returnStatus; } void ICoreStub::getPoolChanges(const std::vector& knownTxsIds, std::vector& addedTxs, @@ -316,3 +342,7 @@ bool ICoreStub::addMessageQueue(CryptoNote::MessageQueue& messageQueuePtr) { return true; } + +void ICoreStub::setPoolChangesResult(bool result) { + poolChangesResult = result; +} diff --git a/tests/UnitTests/ICoreStub.h b/tests/UnitTests/ICoreStub.h index 74f8042f..8e240003 100644 --- a/tests/UnitTests/ICoreStub.h +++ b/tests/UnitTests/ICoreStub.h @@ -29,7 +29,8 @@ class ICoreStub: public CryptoNote::ICore { public: ICoreStub() : topHeight(0), globalIndicesResult(false), randomOutsResult(false), poolTxVerificationResult(true) {}; - ICoreStub(const CryptoNote::Block& genesisBlock) : topHeight(0), globalIndicesResult(false), randomOutsResult(false), poolTxVerificationResult(true) { + ICoreStub(const CryptoNote::Block& genesisBlock) : topHeight(0), globalIndicesResult(false), randomOutsResult(false), poolTxVerificationResult(true), + poolChangesResult(true) { addBlock(genesisBlock); }; @@ -104,6 +105,7 @@ public: void addTransaction(const CryptoNote::Transaction& tx); void setPoolTxVerificationResult(bool result); + void setPoolChangesResult(bool result); private: uint32_t topHeight; @@ -122,4 +124,6 @@ private: std::unordered_map transactions; std::unordered_map transactionPool; bool poolTxVerificationResult; + + bool poolChangesResult; }; diff --git a/tests/UnitTests/INodeStubs.cpp b/tests/UnitTests/INodeStubs.cpp index be0da74a..42b5023c 100644 --- a/tests/UnitTests/INodeStubs.cpp +++ b/tests/UnitTests/INodeStubs.cpp @@ -75,6 +75,10 @@ void INodeTrivialRefreshStub::getNewBlocks(std::vector&& knownBloc task.detach(); } +void INodeTrivialRefreshStub::waitForAsyncContexts() { + m_asyncCounter.waitAsyncContextsFinish(); +} + void INodeTrivialRefreshStub::doGetNewBlocks(std::vector knownBlockIds, std::vector& newBlocks, uint32_t& startHeight, std::vector blockchain, const Callback& callback) { diff --git a/tests/UnitTests/INodeStubs.h b/tests/UnitTests/INodeStubs.h index bf360697..6f2c194e 100644 --- a/tests/UnitTests/INodeStubs.h +++ b/tests/UnitTests/INodeStubs.h @@ -122,6 +122,8 @@ public: std::function&)> getGlobalOutsFunctor = [](const Crypto::Hash&, std::vector&) {}; + void waitForAsyncContexts(); + protected: void doGetNewBlocks(std::vector knownBlockIds, std::vector& newBlocks, uint32_t& startHeight, std::vector blockchain, const Callback& callback); diff --git a/tests/UnitTests/TestBlockchainGenerator.cpp b/tests/UnitTests/TestBlockchainGenerator.cpp index e7a35584..becccc70 100644 --- a/tests/UnitTests/TestBlockchainGenerator.cpp +++ b/tests/UnitTests/TestBlockchainGenerator.cpp @@ -39,7 +39,7 @@ public: return base_class::init(); } - void generate(const AccountPublicAddress& address, Transaction& tx) + void generate(const AccountPublicAddress& address, Transaction& tx, uint64_t unlockTime = 0) { std::vector destinations; @@ -47,7 +47,7 @@ public: [&](uint64_t chunk) { destinations.push_back(CryptoNote::TransactionDestinationEntry(chunk, address)); }, [&](uint64_t a_dust) { destinations.push_back(CryptoNote::TransactionDestinationEntry(a_dust, address)); }); - CryptoNote::constructTransaction(this->m_miners[this->real_source_idx].getAccountKeys(), this->m_sources, destinations, std::vector(), tx, 0, m_logger); + CryptoNote::constructTransaction(this->m_miners[this->real_source_idx].getAccountKeys(), this->m_sources, destinations, std::vector(), tx, unlockTime, m_logger); } void generateSingleOutputTx(const AccountPublicAddress& address, uint64_t amount, Transaction& tx) { @@ -57,7 +57,6 @@ public: } }; - TestBlockchainGenerator::TestBlockchainGenerator(const CryptoNote::Currency& currency) : m_currency(currency), generator(currency) @@ -182,8 +181,7 @@ bool TestBlockchainGenerator::doGenerateTransactionsInOneBlock(const AccountPubl std::vector txs; for (size_t i = 0; i < n; ++i) { Transaction tx; - creator.generate(address, tx); - tx.unlockTime = 10; //default unlock time for coinbase transactions + creator.generate(address, tx, m_blockchain.size() + 10); txs.push_back(tx); } @@ -310,6 +308,10 @@ bool TestBlockchainGenerator::addOrphan(const Crypto::Hash& hash, uint32_t heigh return m_orthanBlocksIndex.add(block); } +void TestBlockchainGenerator::setMinerAccount(const CryptoNote::AccountBase& account) { + miner_acc = account; +} + bool TestBlockchainGenerator::getGeneratedTransactionsNumber(uint32_t height, uint64_t& generatedTransactions) { return m_generatedTransactionsIndex.find(height, generatedTransactions); } diff --git a/tests/UnitTests/TestBlockchainGenerator.h b/tests/UnitTests/TestBlockchainGenerator.h index fbe7262e..4b829251 100644 --- a/tests/UnitTests/TestBlockchainGenerator.h +++ b/tests/UnitTests/TestBlockchainGenerator.h @@ -61,6 +61,7 @@ public: bool getTransactionGlobalIndexesByHash(const Crypto::Hash& transactionHash, std::vector& globalIndexes); bool getMultisignatureOutputByGlobalIndex(uint64_t amount, uint32_t globalIndex, CryptoNote::MultisignatureOutput& out); + void setMinerAccount(const CryptoNote::AccountBase& account); private: struct MultisignatureOutEntry { diff --git a/tests/UnitTests/TestCurrency.cpp b/tests/UnitTests/TestCurrency.cpp index 8fd37f15..8d222007 100644 --- a/tests/UnitTests/TestCurrency.cpp +++ b/tests/UnitTests/TestCurrency.cpp @@ -101,10 +101,12 @@ TEST_F(Currency_isFusionTransactionTest, failsIfTransactionHasOutputsWithTheSame ASSERT_FALSE(m_currency.isFusionTransaction(tx)); } -TEST_F(Currency_isFusionTransactionTest, failsIfTransactionHasDustOutput) { - FusionTransactionBuilder builder(m_currency, 37 * m_currency.defaultDustThreshold() / 10); +TEST_F(Currency_isFusionTransactionTest, succeedsIfTransactionHasDustOutput) { + FusionTransactionBuilder builder(m_currency, 11 * m_currency.defaultDustThreshold()); auto tx = builder.buildTx(); - ASSERT_FALSE(m_currency.isFusionTransaction(tx)); + ASSERT_EQ(2, tx.outputs.size()); + ASSERT_EQ(m_currency.defaultDustThreshold(), tx.outputs[0].amount); + ASSERT_TRUE(m_currency.isFusionTransaction(tx)); } TEST_F(Currency_isFusionTransactionTest, failsIfTransactionFeeIsNotZero) { @@ -113,3 +115,17 @@ TEST_F(Currency_isFusionTransactionTest, failsIfTransactionFeeIsNotZero) { auto tx = builder.buildTx(); ASSERT_FALSE(m_currency.isFusionTransaction(tx)); } + +TEST_F(Currency_isFusionTransactionTest, succedsIfTransactionHasInputEqualsDustThreshold) { + FusionTransactionBuilder builder(m_currency, TEST_AMOUNT); + builder.setFirstInput(m_currency.defaultDustThreshold()); + auto tx = builder.buildTx(); + ASSERT_TRUE(m_currency.isFusionTransaction(tx)); +} + +TEST_F(Currency_isFusionTransactionTest, failsIfTransactionHasInputLessThanDustThreshold) { + FusionTransactionBuilder builder(m_currency, TEST_AMOUNT); + builder.setFirstInput(m_currency.defaultDustThreshold() - 1); + auto tx = builder.buildTx(); + ASSERT_FALSE(m_currency.isFusionTransaction(tx)); +} diff --git a/tests/UnitTests/TestInprocessNode.cpp b/tests/UnitTests/TestInprocessNode.cpp index 721a0e04..a163fa1e 100644 --- a/tests/UnitTests/TestInprocessNode.cpp +++ b/tests/UnitTests/TestInprocessNode.cpp @@ -29,6 +29,7 @@ #include "Logging/FileLogger.h" #include "CryptoNoteCore/TransactionApi.h" #include "CryptoNoteCore/CryptoNoteTools.h" +#include "CryptoNoteCore/VerificationContext.h" #include "Common/StringTools.h" using namespace Crypto; @@ -527,7 +528,7 @@ TEST_F(InProcessNodeTests, getTxMany) { } TEST_F(InProcessNodeTests, getTxFail) { -size_t POOL_TX_NUMBER = 10; + size_t POOL_TX_NUMBER = 10; size_t BLOCKCHAIN_TX_NUMBER = 10; std::vector transactionHashes; @@ -669,6 +670,99 @@ TEST_F(InProcessNodeTests, getLastLocalBlockTimestampError) { ASSERT_THROW(newNode.getLastLocalBlockTimestamp(), std::exception); } +TEST_F(InProcessNodeTests, getPoolDiffereceNotInited) { + CryptoNote::InProcessNode newNode(coreStub, protocolQueryStub); + + std::vector knownPoolTxIds; + Crypto::Hash knownBlockId = boost::value_initialized(); + bool isBcActual = false; + std::vector> newTxs; + std::vector deletedTxIds; + + CallbackStatus status; + newNode.getPoolSymmetricDifference(std::move(knownPoolTxIds), knownBlockId, isBcActual, newTxs, deletedTxIds, [&status](std::error_code ec) { status.setStatus(ec); }); + ASSERT_TRUE(status.wait()); + ASSERT_NE(std::error_code(), status.getStatus()); +} + +TEST_F(InProcessNodeTests, getPoolDiffereceActualBC) { + size_t POOL_TX_NUMBER = 10; + + std::unordered_set transactionHashes; + + coreStub.setPoolChangesResult(true); + + for (size_t i = 0; i < POOL_TX_NUMBER; ++i) { + auto txptr = CryptoNote::createTransaction(); + auto tx = ::createTx(*txptr.get()); + transactionHashes.insert(CryptoNote::getObjectHash(tx)); + CryptoNote::tx_verification_context tvc = boost::value_initialized(); + bool keptByBlock = false; + coreStub.handleIncomingTransaction(tx, CryptoNote::getObjectHash(tx), CryptoNote::getObjectBinarySize(tx), tvc, keptByBlock); + ASSERT_TRUE(tvc.m_added_to_pool); + ASSERT_FALSE(tvc.m_verifivation_failed); + } + + ASSERT_EQ(transactionHashes.size(), POOL_TX_NUMBER); + + std::vector knownPoolTxIds; + Crypto::Hash knownBlockId = CryptoNote::getObjectHash(generator.getBlockchain().back()); + bool isBcActual = false; + std::vector> newTxs; + std::vector deletedTxIds; + + CallbackStatus status; + node.getPoolSymmetricDifference(std::move(knownPoolTxIds), knownBlockId, isBcActual, newTxs, deletedTxIds, [&status](std::error_code ec) { status.setStatus(ec); }); + ASSERT_TRUE(status.wait()); + ASSERT_EQ(std::error_code(), status.getStatus()); + ASSERT_TRUE(isBcActual); + ASSERT_EQ(newTxs.size(), transactionHashes.size()); + ASSERT_TRUE(deletedTxIds.empty()); + + for (const auto& tx : newTxs) { + ASSERT_NE(transactionHashes.find(tx->getTransactionHash()), transactionHashes.end()); + } +} + +TEST_F(InProcessNodeTests, getPoolDiffereceNotActualBC) { + size_t POOL_TX_NUMBER = 10; + + std::unordered_set transactionHashes; + + coreStub.setPoolChangesResult(false); + + for (size_t i = 0; i < POOL_TX_NUMBER; ++i) { + auto txptr = CryptoNote::createTransaction(); + auto tx = ::createTx(*txptr.get()); + transactionHashes.insert(CryptoNote::getObjectHash(tx)); + CryptoNote::tx_verification_context tvc = boost::value_initialized(); + bool keptByBlock = false; + coreStub.handleIncomingTransaction(tx, CryptoNote::getObjectHash(tx), CryptoNote::getObjectBinarySize(tx), tvc, keptByBlock); + ASSERT_TRUE(tvc.m_added_to_pool); + ASSERT_FALSE(tvc.m_verifivation_failed); + } + + ASSERT_EQ(transactionHashes.size(), POOL_TX_NUMBER); + + std::vector knownPoolTxIds; + Crypto::Hash knownBlockId = CryptoNote::getObjectHash(generator.getBlockchain().back()); + bool isBcActual = false; + std::vector> newTxs; + std::vector deletedTxIds; + + CallbackStatus status; + node.getPoolSymmetricDifference(std::move(knownPoolTxIds), knownBlockId, isBcActual, newTxs, deletedTxIds, [&status](std::error_code ec) { status.setStatus(ec); }); + ASSERT_TRUE(status.wait()); + ASSERT_EQ(std::error_code(), status.getStatus()); + ASSERT_FALSE(isBcActual); + ASSERT_EQ(newTxs.size(), transactionHashes.size()); + ASSERT_TRUE(deletedTxIds.empty()); + + for (const auto& tx : newTxs) { + ASSERT_NE(transactionHashes.find(tx->getTransactionHash()), transactionHashes.end()); + } +} + //TODO: make relayTransaction unit test //TODO: make getNewBlocks unit test //TODO: make queryBlocks unit test diff --git a/tests/UnitTests/TestTransactionPoolDetach.cpp b/tests/UnitTests/TestTransactionPoolDetach.cpp index 256fcb0b..454ea6d9 100755 --- a/tests/UnitTests/TestTransactionPoolDetach.cpp +++ b/tests/UnitTests/TestTransactionPoolDetach.cpp @@ -433,6 +433,9 @@ TEST_F(DetachTest, testDetachWithWallet) { 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(); @@ -447,14 +450,13 @@ TEST_F(DetachTest, testDetachWithWallet) { Bob.getTransaction(txId, txInfo); - - ASSERT_EQ(txInfo.blockHeight, m_node.getLastLocalBlockHeight()); + 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(m_node.getLastLocalBlockHeight() - 1); + m_node.startAlternativeChain(txInfo.blockHeight - 1); generator.generateEmptyBlocks(2); //sync Bob diff --git a/tests/UnitTests/TestWallet.cpp b/tests/UnitTests/TestWallet.cpp index 88e914d3..31c86513 100755 --- a/tests/UnitTests/TestWallet.cpp +++ b/tests/UnitTests/TestWallet.cpp @@ -36,11 +36,36 @@ #include #include +#include "TransactionApiHelpers.h" + using namespace Crypto; using namespace Common; using namespace CryptoNote; namespace CryptoNote { + std::ostream& operator<<(std::ostream& o, const WalletTransactionState& st) { + switch (st) { + case WalletTransactionState::FAILED: + o << "FAILED"; + break; + case WalletTransactionState::CANCELLED: + o << "CANCELLED"; + break; + case WalletTransactionState::SUCCEEDED: + o << "SUCCEEDED"; + break; + } + return o; + } + std::ostream& operator<<(std::ostream& o, const WalletTransaction& tx) { + o << "WalletTransaction{state=" << tx.state << ", timestamp=" << tx.timestamp + << ", blockHeight=" << tx.blockHeight << ", hash=" << tx.hash + << ", totalAmount=" << tx.totalAmount << ", fee=" << tx.fee + << ", creationTime=" << tx.creationTime << ", unlockTime=" << tx.unlockTime + << ", extra=" << tx.extra << ", isBase=" << tx.isBase << "}"; + return o; + } + bool operator==(const WalletTransaction& lhs, const WalletTransaction& rhs) { if (lhs.state != rhs.state) { return false; @@ -78,6 +103,10 @@ namespace CryptoNote { return false; } + if (lhs.isBase != rhs.isBase) { + return false; + } + return true; } @@ -100,16 +129,22 @@ namespace CryptoNote { bool operator!=(const WalletTransfer& lhs, const WalletTransfer& rhs) { return !(lhs == rhs); } + + bool operator==(const IFusionManager::EstimateResult& lhs, const IFusionManager::EstimateResult& rhs) { + return lhs.fusionReadyCount == rhs.fusionReadyCount && lhs.totalOutputCount == rhs.totalOutputCount; + } } class WalletApi: public ::testing::Test { public: WalletApi() : + TRANSACTION_SOFTLOCK_TIME(10), currency(CryptoNote::CurrencyBuilder(logger).currency()), generator(currency), node(generator), alice(dispatcher, currency, node), - FEE(currency.minimumFee()) + FEE(currency.minimumFee()), + FUSION_THRESHOLD(currency.defaultDustThreshold() * 10) { } virtual void SetUp() override; @@ -121,8 +156,10 @@ protected: void generateBlockReward(const std::string& address); void generateAndUnlockMoney(); void generateAddressesWithPendingMoney(size_t count); + void generateFusionOutputsAndUnlock(WalletGreen& wallet, INodeTrivialRefreshStub& node, const CryptoNote::Currency& walletCurrency, uint64_t threshold); void unlockMoney(); void unlockMoney(CryptoNote::WalletGreen& wallet, INodeTrivialRefreshStub& inode); + void setMinerTo(CryptoNote::WalletGreen& wallet); template void waitValueChanged(CryptoNote::WalletGreen& wallet, T prev, std::function&& f); @@ -141,11 +178,14 @@ protected: void waitPendingBalanceUpdated(CryptoNote::WalletGreen& wallet, uint64_t prev); void waitForTransactionCount(CryptoNote::WalletGreen& wallet, uint64_t expected); + void waitForTransactionUpdated(CryptoNote::WalletGreen& wallet, size_t expectedTransactionId); void waitForActualBalance(uint64_t expected); + void waitForActualBalance(CryptoNote::WalletGreen& wallet, uint64_t expected); size_t sendMoneyToRandomAddressFrom(const std::string& address, uint64_t amount, uint64_t fee); size_t sendMoneyToRandomAddressFrom(const std::string& address); + size_t sendMoney(CryptoNote::WalletGreen& wallet, const std::string& to, int64_t amount, uint64_t fee, uint64_t mixIn = 0, const std::string& extra = "", uint64_t unlockTimestamp = 0); size_t sendMoney(const std::string& to, int64_t amount, uint64_t fee, uint64_t mixIn = 0, const std::string& extra = "", uint64_t unlockTimestamp = 0); void fillWalletWithDetailsCache(); @@ -156,6 +196,8 @@ protected: const std::vector& trs = std::vector(), const std::vector>& externalTxs = std::vector>()); + uint32_t TRANSACTION_SOFTLOCK_TIME; + System::Dispatcher dispatcher; Logging::ConsoleLogger logger; CryptoNote::Currency currency; @@ -167,6 +209,7 @@ protected: const uint64_t SENT = 1122334455; const uint64_t FEE; const std::string RANDOM_ADDRESS = "2634US2FAz86jZT73YmM8u5GPCknT2Wxj8bUCKivYKpThFhF2xsjygMGxbxZzM42zXhKUhym6Yy6qHHgkuWtruqiGkDpX6m"; + const uint64_t FUSION_THRESHOLD; }; void WalletApi::SetUp() { @@ -174,6 +217,20 @@ void WalletApi::SetUp() { aliceAddress = alice.createAddress(); } +void WalletApi::setMinerTo(CryptoNote::WalletGreen& wallet) { + AccountBase base; + AccountKeys keys; + auto viewKey = wallet.getViewKey(); + auto spendKey = wallet.getAddressSpendKey(0); + keys.address.spendPublicKey = spendKey.publicKey; + keys.address.viewPublicKey = viewKey.publicKey; + keys.viewSecretKey = viewKey.secretKey; + keys.spendSecretKey = spendKey.secretKey; + base.setAccountKeys(keys); + // mine to alice's address to make it recieve block base transaction + generator.setMinerAccount(base); +} + void WalletApi::TearDown() { alice.shutdown(); wait(100); //ObserverManager bug workaround @@ -196,13 +253,56 @@ void WalletApi::generateBlockReward(const std::string& address) { generator.getBlockRewardForAddress(parseAddress(address)); } +void WalletApi::generateFusionOutputsAndUnlock(WalletGreen& wallet, INodeTrivialRefreshStub& node, const CryptoNote::Currency& walletCurrency, uint64_t threshold) { + uint64_t digit = walletCurrency.defaultDustThreshold(); + uint64_t mul = 1; + + while (digit > 9) { + digit /= 10; + mul *= 10; + } + + auto initialAmount = wallet.getActualBalance(); + + CryptoNote::AccountPublicAddress publicAddress = parseAddress(wallet.getAddress(0)); + const size_t POWERS_COUNT = 3; + + uint64_t addedAmount = 0; + for (size_t power = 0; power < POWERS_COUNT; ++power) { + int start = power == 0 ? digit: 1; + if (start * mul > threshold) { + break; + } + + for (int count = 0, d = start; count < walletCurrency.fusionTxMinInputCount() && start * mul < threshold; ++count) { + //TODO: make it possible to put several outputs to one transaction + auto amount = d * mul; + generator.getSingleOutputTransaction(publicAddress, amount); + addedAmount += amount; + + if (++d > 9 || amount >= threshold) { + d = start; + } + } + + mul *= 10; + } + + assert(addedAmount > 0); + + generator.generateEmptyBlocks(11); + node.updateObservers(); + + waitForActualBalance(wallet, initialAmount + addedAmount); +} + void WalletApi::unlockMoney() { unlockMoney(alice, node); } void WalletApi::unlockMoney(CryptoNote::WalletGreen& wallet, INodeTrivialRefreshStub& inode) { auto prev = wallet.getActualBalance(); - generator.generateEmptyBlocks(11); //coinbase money should become available after 10 blocks + generator.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); //coinbase money should become available after 10 blocks inode.updateObservers(); waitActualBalanceUpdated(wallet, prev); } @@ -260,6 +360,10 @@ void WalletApi::waitForActualBalance(uint64_t expected) { waitForValue(alice, expected, [this] () { return this->alice.getActualBalance(); }); } +void WalletApi::waitForActualBalance(CryptoNote::WalletGreen& wallet, uint64_t expected) { + waitForValue(wallet, expected, [&wallet] () { return wallet.getActualBalance(); }); +} + void WalletApi::waitActualBalanceUpdated(CryptoNote::WalletGreen& wallet, uint64_t prev) { waitValueChanged(wallet, prev, [&wallet] () { return wallet.getActualBalance(); }); } @@ -280,6 +384,16 @@ void WalletApi::waitForTransactionCount(CryptoNote::WalletGreen& wallet, uint64_ waitForValue(wallet, expected, [&wallet] () { return wallet.getTransactionCount(); }); } +void WalletApi::waitForTransactionUpdated(CryptoNote::WalletGreen& wallet, size_t expectedTransactionId) { + WalletEvent event; + for (;;) { + event = wallet.getEvent(); + if (event.type == WalletEventType::TRANSACTION_UPDATED && event.transactionUpdated.transactionIndex == expectedTransactionId) { + break; + } + } +} + void WalletApi::generateAddressesWithPendingMoney(size_t count) { for (size_t i = 0; i < count; ++i) { generateBlockReward(alice.createAddress()); @@ -315,12 +429,16 @@ void WalletApi::fillWalletWithDetailsCache() { } } -size_t WalletApi::sendMoney(const std::string& to, int64_t amount, uint64_t fee, uint64_t mixIn, const std::string& extra, uint64_t unlockTimestamp) { +size_t WalletApi::sendMoney(CryptoNote::WalletGreen& wallet, const std::string& to, int64_t amount, uint64_t fee, uint64_t mixIn, const std::string& extra, uint64_t unlockTimestamp) { CryptoNote::WalletTransfer transfer; transfer.address = to; transfer.amount = amount; - return alice.transfer(transfer, fee, mixIn, extra, unlockTimestamp); + return wallet.transfer(transfer, fee, mixIn, extra, unlockTimestamp); +} + +size_t WalletApi::sendMoney(const std::string& to, int64_t amount, uint64_t fee, uint64_t mixIn, const std::string& extra, uint64_t unlockTimestamp) { + return sendMoney(alice, to, amount, fee, mixIn, extra, unlockTimestamp); } void WalletApi::wait(uint64_t milliseconds) { @@ -357,7 +475,7 @@ TEST_F(WalletApi, unlockMoney) { } TEST_F(WalletApi, transferFromOneAddress) { - CryptoNote::WalletGreen bob(dispatcher, currency, node); + CryptoNote::WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.initialize("pass2"); std::string bobAddress = bob.createAddress(); @@ -366,6 +484,7 @@ TEST_F(WalletApi, transferFromOneAddress) { auto alicePrev = alice.getActualBalance(); sendMoney(bobAddress, SENT, FEE); node.updateObservers(); + waitActualBalanceUpdated(alicePrev); waitPendingBalanceUpdated(bob, 0); @@ -379,6 +498,44 @@ TEST_F(WalletApi, transferFromOneAddress) { wait(100); } +TEST_F(WalletApi, pendingBalanceUpdatedAfterTransactionGotInBlock) { + generateAndUnlockMoney(); + + auto initialActual = alice.getActualBalance(); + + sendMoney(RANDOM_ADDRESS, SENT, FEE); + node.updateObservers(); + waitActualBalanceUpdated(initialActual); + waitPendingBalanceUpdated(0); + + auto prevPending = alice.getPendingBalance(); + + generator.generateEmptyBlocks(TRANSACTION_SOFTLOCK_TIME); + node.updateObservers(); + + waitPendingBalanceUpdated(prevPending); + ASSERT_EQ(0, alice.getPendingBalance()); +} + +TEST_F(WalletApi, moneyLockedIfTransactionIsSoftLocked) { + generateAndUnlockMoney(); + + CryptoNote::WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); + bob.initialize("pass2"); + + sendMoney(bob.createAddress(), SENT, FEE); + generator.generateEmptyBlocks(TRANSACTION_SOFTLOCK_TIME - 1); + node.updateObservers(); + + waitPendingBalanceUpdated(bob, 0); + + ASSERT_EQ(SENT, bob.getPendingBalance()); + ASSERT_EQ(0, bob.getActualBalance()); + + bob.shutdown(); + wait(100); +} + TEST_F(WalletApi, transferMixin) { generateAndUnlockMoney(); @@ -408,12 +565,12 @@ TEST_F(WalletApi, transferNegativeAmount) { TEST_F(WalletApi, transferFromTwoAddresses) { generateBlockReward(); generateBlockReward(alice.createAddress()); - generator.generateEmptyBlocks(11); + generator.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); node.updateObservers(); waitForActualBalance(2 * TEST_BLOCK_REWARD); - CryptoNote::WalletGreen bob(dispatcher, currency, node); + CryptoNote::WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.initialize("pass2"); std::string bobAddress = bob.createAddress(); @@ -445,14 +602,14 @@ TEST_F(WalletApi, transferTooBigTransaction) { TestBlockchainGenerator gen(cur); INodeTrivialRefreshStub n(gen); - CryptoNote::WalletGreen wallet(dispatcher, cur, n); + CryptoNote::WalletGreen wallet(dispatcher, cur, n, TRANSACTION_SOFTLOCK_TIME); wallet.initialize("pass"); wallet.createAddress(); gen.getBlockRewardForAddress(parseAddress(wallet.getAddress(0))); auto prev = wallet.getActualBalance(); - gen.generateEmptyBlocks(11); + gen.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); n.updateObservers(); waitActualBalanceUpdated(wallet, prev); @@ -494,7 +651,7 @@ TEST_F(WalletApi, transferFromSpecificAddress) { auto secondAddress = alice.createAddress(); generateBlockReward(secondAddress); - generator.generateEmptyBlocks(11); + generator.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); node.updateObservers(); waitActualBalanceUpdated(); @@ -520,7 +677,7 @@ TEST_F(WalletApi, loadEmptyWallet) { std::stringstream data; alice.save(data, true, true); - WalletGreen bob(dispatcher, currency, node); + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.load(data, "pass"); ASSERT_EQ(alice.getAddressCount(), bob.getAddressCount()); @@ -532,15 +689,77 @@ TEST_F(WalletApi, loadEmptyWallet) { wait(100); } -TEST_F(WalletApi, loadWalletWithoutAddresses) { +TEST_F(WalletApi, walletGetsBaseTransaction) { + // mine to alice's address to make it recieve block base transaction + setMinerTo(alice); + generateAndUnlockMoney(); + ASSERT_TRUE(alice.getTransaction(0).isBase); +} + +TEST_F(WalletApi, walletGetsNonBaseTransaction) { + generateAndUnlockMoney(); + ASSERT_FALSE(alice.getTransaction(0).isBase); +} + +TEST_F(WalletApi, loadWalletWithBaseTransaction) { + // mine to alice's address to make it recieve block base transaction + setMinerTo(alice); + generateAndUnlockMoney(); + + std::stringstream data; + alice.save(data, true, true); + WalletGreen bob(dispatcher, currency, node); + bob.load(data, "pass"); + + ASSERT_TRUE(bob.getTransaction(0).isBase); + bob.shutdown(); + wait(100); +} + +TEST_F(WalletApi, updateBaseTransactionAfterLoad) { + // mine to alice's address to make it recieve block base transaction + setMinerTo(alice); + generateAndUnlockMoney(); + + std::stringstream data; + alice.save(data, true, false); + + WalletGreen bob(dispatcher, currency, node); + bob.load(data, "pass"); + waitForWalletEvent(bob, CryptoNote::SYNC_COMPLETED, std::chrono::seconds(5)); + + ASSERT_TRUE(bob.getTransaction(0).isBase); + bob.shutdown(); + wait(100); +} + +TEST_F(WalletApi, setBaseTransactionAfterInSynchronization) { + // mine to alice's address to make it recieve block base transaction + setMinerTo(alice); + generateAndUnlockMoney(); + + std::stringstream data; + alice.save(data, false, false); + + WalletGreen bob(dispatcher, currency, node); + bob.load(data, "pass"); + waitForWalletEvent(bob, CryptoNote::SYNC_COMPLETED, std::chrono::seconds(5)); + + ASSERT_TRUE(bob.getTransaction(0).isBase); + bob.shutdown(); + wait(100); +} + +TEST_F(WalletApi, loadWalletWithoutAddresses) { + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.initialize("pass"); std::stringstream data; bob.save(data, false, false); bob.shutdown(); - WalletGreen carol(dispatcher, currency, node); + WalletGreen carol(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); carol.load(data, "pass"); ASSERT_EQ(0, carol.getAddressCount()); @@ -587,11 +806,12 @@ void compareWalletsTransactionTransfers(const CryptoNote::WalletGreen& alice, co TEST_F(WalletApi, loadCacheDetails) { fillWalletWithDetailsCache(); - + node.waitForAsyncContexts(); + waitForWalletEvent(alice, CryptoNote::SYNC_COMPLETED, std::chrono::seconds(5)); std::stringstream data; alice.save(data, true, true); - WalletGreen bob(dispatcher, currency, node); + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.load(data, "pass"); compareWalletsAddresses(alice, bob); @@ -609,7 +829,7 @@ TEST_F(WalletApi, loadNoCacheNoDetails) { std::stringstream data; alice.save(data, false, false); - WalletGreen bob(dispatcher, currency, node); + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.load(data, "pass"); compareWalletsAddresses(alice, bob); @@ -628,7 +848,7 @@ TEST_F(WalletApi, loadNoCacheDetails) { std::stringstream data; alice.save(data, true, false); - WalletGreen bob(dispatcher, currency, node); + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.load(data, "pass"); compareWalletsAddresses(alice, bob); @@ -648,7 +868,7 @@ TEST_F(WalletApi, loadCacheNoDetails) { std::stringstream data; alice.save(data, false, true); - WalletGreen bob(dispatcher, currency, node); + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.load(data, "pass"); compareWalletsAddresses(alice, bob); @@ -665,7 +885,7 @@ TEST_F(WalletApi, loadWithWrongPassword) { std::stringstream data; alice.save(data, false, false); - WalletGreen bob(dispatcher, currency, node); + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); ASSERT_ANY_THROW(bob.load(data, "pass2")); } @@ -695,7 +915,7 @@ void WalletApi::testIWalletDataCompatibility(bool details, const std::string& ca std::stringstream stream; walletSerializer.serialize(stream, "pass", details, std::string()); - WalletGreen wallet(dispatcher, currency, node); + WalletGreen wallet(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); wallet.load(stream, "pass"); EXPECT_EQ(1, wallet.getAddressCount()); @@ -842,7 +1062,7 @@ TEST_F(WalletApi, stopStart) { } TEST_F(WalletApi, uninitializedObject) { - WalletGreen bob(dispatcher, currency, node); + WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); ASSERT_ANY_THROW(bob.changePassword("s", "p")); std::stringstream stream; @@ -877,6 +1097,7 @@ TEST_F(WalletApi, checkSentTransaction) { ASSERT_EQ(-static_cast(SENT + FEE), tx.totalAmount); ASSERT_EQ(FEE, tx.fee); ASSERT_EQ(0, tx.unlockTime); + ASSERT_FALSE(tx.isBase); ASSERT_EQ(TX_PUB_KEY_EXTRA_SIZE, tx.extra.size()); //Transaction public key only } @@ -910,6 +1131,7 @@ TEST_F(WalletApi, checkSentTransactionWithExtra) { ASSERT_EQ(-static_cast(SENT + FEE), tx.totalAmount); ASSERT_EQ(FEE, tx.fee); ASSERT_EQ(0, tx.unlockTime); + ASSERT_FALSE(tx.isBase); ASSERT_EQ(extra, removeTxPublicKey(tx.extra)); } @@ -923,12 +1145,21 @@ TEST_F(WalletApi, checkFailedTransaction) { ASSERT_EQ(CryptoNote::WalletTransactionState::FAILED, tx.state); } +TEST_F(WalletApi, transactionSendsAfterFailedTransaction) { + generator.getSingleOutputTransaction(parseAddress(aliceAddress), SENT + FEE); + unlockMoney(); + + node.setNextTransactionError(); + ASSERT_ANY_THROW(sendMoney(RANDOM_ADDRESS, SENT, FEE)); + ASSERT_NO_THROW(sendMoney(RANDOM_ADDRESS, SENT, FEE)); +} + TEST_F(WalletApi, checkIncomingTransaction) { const std::string extra = createExtraNonce("\x01\x23\x45\x67\x89\xab\xcd\xef"); generateAndUnlockMoney(); - CryptoNote::WalletGreen bob(dispatcher, currency, node); + CryptoNote::WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.initialize("pass2"); std::string bobAddress = bob.createAddress(); @@ -963,7 +1194,7 @@ TEST_F(WalletApi, changePassword) { std::stringstream data; alice.save(data, false, false); - CryptoNote::WalletGreen bob(dispatcher, currency, node); + CryptoNote::WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); ASSERT_NO_THROW(bob.load(data, "pass2")); bob.shutdown(); @@ -993,7 +1224,7 @@ TEST_F(WalletApi, detachBlockchain) { auto alicePrev = alice.getActualBalance(); node.startAlternativeChain(1); - generator.generateEmptyBlocks(11); + generator.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); node.updateObservers(); waitActualBalanceUpdated(alicePrev); @@ -1016,7 +1247,7 @@ TEST_F(WalletApi, deleteAddresses) { TEST_F(WalletApi, incomingTxTransfer) { generateAndUnlockMoney(); - CryptoNote::WalletGreen bob(dispatcher, currency, node); + CryptoNote::WalletGreen bob(dispatcher, currency, node, TRANSACTION_SOFTLOCK_TIME); bob.initialize("pass2"); bob.createAddress(); bob.createAddress(); @@ -1077,7 +1308,7 @@ TEST_F(WalletApi, syncAfterLoad) { alice.shutdown(); generateBlockReward(); - generator.generateEmptyBlocks(11); + generator.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); alice.load(data, "pass"); @@ -1107,7 +1338,7 @@ TEST_F(WalletApi, DISABLED_loadTest) { using namespace std::chrono; INodeNoRelay noRelayNode(generator); - CryptoNote::WalletGreen wallet(dispatcher, currency, noRelayNode); + CryptoNote::WalletGreen wallet(dispatcher, currency, noRelayNode, TRANSACTION_SOFTLOCK_TIME); wallet.initialize("pass"); const size_t ADDRESSES_COUNT = 1000; @@ -1247,6 +1478,134 @@ TEST_F(WalletApi, getAddressSpendKeyThrowsIfStopped) { ASSERT_ANY_THROW(alice.getAddressSpendKey(0)); } +Crypto::PublicKey generatePublicKey() { + CryptoNote::KeyPair spendKeys; + Crypto::generate_keys(spendKeys.publicKey, spendKeys.secretKey); + + return spendKeys.publicKey; +} + +TEST_F(WalletApi, createTrackingKeyAddressSucceeded) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + wallet.initialize("pass"); + + Crypto::PublicKey publicKey = generatePublicKey(); + + ASSERT_NO_THROW(wallet.createAddress(publicKey)); + ASSERT_EQ(1, wallet.getAddressCount()); + wallet.shutdown(); +} + +TEST_F(WalletApi, createTrackingKeyThrowsIfNotInitialized) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + + Crypto::PublicKey publicKey = generatePublicKey(); + ASSERT_ANY_THROW(wallet.createAddress(publicKey)); +} + +TEST_F(WalletApi, createTrackingKeyThrowsIfStopped) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + wallet.initialize("pass"); + wallet.stop(); + + Crypto::PublicKey publicKey = generatePublicKey(); + ASSERT_ANY_THROW(wallet.createAddress(publicKey)); + wallet.shutdown(); +} + +TEST_F(WalletApi, createTrackingKeyThrowsIfKeyExists) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + wallet.initialize("pass"); + + Crypto::PublicKey publicKey = generatePublicKey(); + wallet.createAddress(publicKey); + ASSERT_ANY_THROW(wallet.createAddress(publicKey)); + wallet.shutdown(); +} + +TEST_F(WalletApi, createTrackingKeyThrowsIfWalletHasNotTrackingKeys) { + Crypto::PublicKey publicKey = generatePublicKey(); + ASSERT_ANY_THROW(alice.createAddress(publicKey)); +} + +TEST_F(WalletApi, getAddressSpendKeyForTrackingKeyReturnsNullSecretKey) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + wallet.initialize("pass"); + + Crypto::PublicKey publicKey = generatePublicKey(); + wallet.createAddress(publicKey); + + KeyPair spendKeys = wallet.getAddressSpendKey(0); + ASSERT_EQ(NULL_SECRET_KEY, spendKeys.secretKey); + + wallet.shutdown(); +} + +TEST_F(WalletApi, trackingAddressReceivesMoney) { + generateAndUnlockMoney(); + + CryptoNote::WalletGreen bob(dispatcher, currency, node); + bob.initialize("pass2"); + + Crypto::PublicKey publicKey = generatePublicKey(); + bob.createAddress(publicKey); + + sendMoney(bob.getAddress(0), SENT, FEE); + node.updateObservers(); + + auto expectedTransactionHeight = node.getLastKnownBlockHeight(); + waitPendingBalanceUpdated(bob, 0); + + ASSERT_EQ(SENT, bob.getPendingBalance()); + ASSERT_EQ(0, bob.getActualBalance()); + ASSERT_EQ(1, bob.getTransactionCount()); + + CryptoNote::WalletTransaction transaction = bob.getTransaction(0); + ASSERT_EQ(CryptoNote::WalletTransactionState::SUCCEEDED, transaction.state); + ASSERT_EQ(expectedTransactionHeight, transaction.blockHeight); + ASSERT_EQ(SENT, transaction.totalAmount); + ASSERT_EQ(FEE, transaction.fee); + ASSERT_EQ(0, transaction.unlockTime); + + bob.shutdown(); +} + +TEST_F(WalletApi, trackingAddressUnlocksMoney) { + generateAndUnlockMoney(); + + CryptoNote::WalletGreen bob(dispatcher, currency, node); + bob.initialize("pass2"); + + Crypto::PublicKey publicKey = generatePublicKey(); + bob.createAddress(publicKey); + + sendMoney(bob.getAddress(0), SENT, FEE); + generator.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); + node.updateObservers(); + waitActualBalanceUpdated(bob, 0); + + ASSERT_EQ(0, bob.getPendingBalance()); + ASSERT_EQ(SENT, bob.getActualBalance()); +} + +TEST_F(WalletApi, transferFromTrackingKeyThrows) { + generateAndUnlockMoney(); + + CryptoNote::WalletGreen bob(dispatcher, currency, node); + bob.initialize("pass2"); + + Crypto::PublicKey publicKey = generatePublicKey(); + bob.createAddress(publicKey); + + sendMoney(bob.getAddress(0), SENT, FEE); + generator.generateEmptyBlocks(currency.minedMoneyUnlockWindow()); + node.updateObservers(); + waitActualBalanceUpdated(bob, 0); + + ASSERT_ANY_THROW(sendMoney(bob, RANDOM_ADDRESS, SENT, FEE)); + bob.shutdown(); +} + TEST_F(WalletApi, walletGetsSyncCompletedEvent) { generator.generateEmptyBlocks(1); node.updateObservers(); @@ -1260,3 +1619,235 @@ TEST_F(WalletApi, walletGetsSyncProgressUpdatedEvent) { ASSERT_TRUE(waitForWalletEvent(alice, CryptoNote::SYNC_PROGRESS_UPDATED, std::chrono::seconds(5))); } + +struct CatchTransactionNodeStub : public INodeTrivialRefreshStub { + CatchTransactionNodeStub(TestBlockchainGenerator& generator): INodeTrivialRefreshStub(generator), caught(false) {} + + virtual void relayTransaction(const CryptoNote::Transaction& incomingTransaction, const Callback& callback) override { + transaction = incomingTransaction; + caught = true; + INodeTrivialRefreshStub::relayTransaction(incomingTransaction, callback); + } + + bool caught; + CryptoNote::Transaction transaction; +}; + +TEST_F(WalletApi, createFusionTransactionCreatesValidFusionTransactionWithoutMixin) { + CatchTransactionNodeStub catchNode(generator); + CryptoNote::WalletGreen wallet(dispatcher, currency, catchNode); + wallet.initialize("pass"); + wallet.createAddress(); + + generateFusionOutputsAndUnlock(wallet, node, currency, FUSION_THRESHOLD); + + ASSERT_NE(WALLET_INVALID_TRANSACTION_ID, wallet.createFusionTransaction(FUSION_THRESHOLD, 0)); + ASSERT_TRUE(catchNode.caught); + ASSERT_TRUE(currency.isFusionTransaction(catchNode.transaction)); + + wallet.shutdown(); +} + +TEST_F(WalletApi, createFusionTransactionCreatesValidFusionTransactionWithMixin) { + CatchTransactionNodeStub catchNode(generator); + CryptoNote::WalletGreen wallet(dispatcher, currency, catchNode); + wallet.initialize("pass"); + wallet.createAddress(); + + generateFusionOutputsAndUnlock(wallet, node, currency, FUSION_THRESHOLD); + + ASSERT_NE(WALLET_INVALID_TRANSACTION_ID, wallet.createFusionTransaction(FUSION_THRESHOLD, 2)); + ASSERT_TRUE(catchNode.caught); + ASSERT_TRUE(currency.isFusionTransaction(catchNode.transaction)); + + wallet.shutdown(); +} + +TEST_F(WalletApi, createFusionTransactionDoesnotAffectTotalBalance) { + generateFusionOutputsAndUnlock(alice, node, currency, FUSION_THRESHOLD); + + auto totalBalance = alice.getActualBalance() + alice.getPendingBalance(); + ASSERT_NE(WALLET_INVALID_TRANSACTION_ID, alice.createFusionTransaction(FUSION_THRESHOLD, 2)); + ASSERT_EQ(totalBalance, alice.getActualBalance() + alice.getPendingBalance()); +} + +TEST_F(WalletApi, createFusionTransactionFailsIfMixinToobig) { + generateFusionOutputsAndUnlock(alice, node, currency, FUSION_THRESHOLD); + ASSERT_ANY_THROW(alice.createFusionTransaction(FUSION_THRESHOLD, 10000000)); +} + +TEST_F(WalletApi, createFusionTransactionFailsIfNoTransfers) { + ASSERT_EQ(WALLET_INVALID_TRANSACTION_ID, alice.createFusionTransaction(FUSION_THRESHOLD, 0)); +} + +TEST_F(WalletApi, createFusionTransactionThrowsIfNotInitialized) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + ASSERT_ANY_THROW(wallet.createFusionTransaction(FUSION_THRESHOLD, 0)); +} + +TEST_F(WalletApi, createFusionTransactionThrowsIfStopped) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + wallet.initialize("pass"); + wallet.stop(); + ASSERT_ANY_THROW(wallet.createFusionTransaction(FUSION_THRESHOLD, 0)); + wallet.shutdown(); +} + +TEST_F(WalletApi, createFusionTransactionThrowsIfThresholdTooSmall) { + ASSERT_ANY_THROW(alice.createFusionTransaction(currency.defaultDustThreshold() - 1, 0)); +} + +TEST_F(WalletApi, createFusionTransactionThrowsIfNoAddresses) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + wallet.initialize("pass"); + ASSERT_ANY_THROW(wallet.createFusionTransaction(FUSION_THRESHOLD, 0)); + wallet.shutdown(); +} + +TEST_F(WalletApi, createFusionTransactionThrowsIfTransactionSendError) { + CatchTransactionNodeStub catchNode(generator); + CryptoNote::WalletGreen wallet(dispatcher, currency, catchNode); + wallet.initialize("pass"); + wallet.createAddress(); + + generateFusionOutputsAndUnlock(wallet, node, currency, FUSION_THRESHOLD); + + catchNode.setNextTransactionError(); + ASSERT_ANY_THROW(wallet.createFusionTransaction(FUSION_THRESHOLD, 0)); + wallet.shutdown(); +} + +TEST_F(WalletApi, fusionManagerEstimateThrowsIfNotInitialized) { + const uint64_t THRESHOLD = 100; + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + ASSERT_ANY_THROW(wallet.estimate(THRESHOLD)); +} + +TEST_F(WalletApi, fusionManagerEstimateThrowsIfStopped) { + const uint64_t THRESHOLD = 100; + alice.stop(); + ASSERT_ANY_THROW(alice.estimate(THRESHOLD)); +} + +TEST_F(WalletApi, fusionManagerEstimateEmpty) { + const uint64_t THRESHOLD = 100; + IFusionManager::EstimateResult emptyResult = {0, 0}; + ASSERT_EQ(emptyResult, alice.estimate(THRESHOLD)); +} + +TEST_F(WalletApi, fusionManagerEstimateLocked) { + auto pending = alice.getPendingBalance(); + generateBlockReward(); + node.updateObservers(); + waitPendingBalanceUpdated(alice, pending); + + IFusionManager::EstimateResult expectedResult = {0, 0}; + ASSERT_EQ(expectedResult, alice.estimate(0)); +} + +TEST_F(WalletApi, fusionManagerEstimateNullThreshold) { + generateAndUnlockMoney(); + + ASSERT_EQ(1, alice.getTransactionCount()); + CryptoNote::Transaction tx = boost::value_initialized(); + ASSERT_TRUE(generator.getTransactionByHash(alice.getTransaction(0).hash, tx, false)); + ASSERT_FALSE(tx.outputs.empty()); + + IFusionManager::EstimateResult expectedResult = {0, tx.outputs.size()}; + ASSERT_EQ(expectedResult, alice.estimate(0)); +} + +TEST_F(WalletApi, DISABLED_fusionManagerEstimate) { + generateAndUnlockMoney(); + + ASSERT_EQ(1, alice.getTransactionCount()); + CryptoNote::Transaction tx = boost::value_initialized(); + ASSERT_TRUE(generator.getTransactionByHash(alice.getTransaction(0).hash, tx, false)); + ASSERT_FALSE(tx.outputs.empty()); + + IFusionManager::EstimateResult expectedResult = {0, tx.outputs.size()}; + size_t maxOutputIndex = 0; + uint64_t maxOutputAmount = 0; + for (size_t i = 0; i < tx.outputs.size(); ++i) { + if (tx.outputs[i].amount > maxOutputAmount) { + maxOutputAmount = tx.outputs[i].amount; + maxOutputIndex = i; + } + + if (currency.isAmountApplicableInFusionTransactionInput(tx.outputs[i].amount, tx.outputs[i].amount + 1)) { + ++expectedResult.fusionReadyCount; + } + } + + ASSERT_EQ(expectedResult, alice.estimate(tx.outputs[maxOutputIndex].amount + 1)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransactionThrowsIfNotInitialized) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + ASSERT_ANY_THROW(wallet.isFusionTransaction(0)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransactionThrowsIfStopped) { + alice.stop(); + ASSERT_ANY_THROW(alice.isFusionTransaction(0)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransactionEmpty) { + ASSERT_ANY_THROW(alice.isFusionTransaction(0)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransactionNotFusion) { + generateAndUnlockMoney(); + + ASSERT_EQ(1, alice.getTransactionCount()); + ASSERT_FALSE(alice.isFusionTransaction(0)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransaction) { + generateFusionOutputsAndUnlock(alice, node, currency, FUSION_THRESHOLD); + + auto id = alice.createFusionTransaction(FUSION_THRESHOLD, 0); + ASSERT_NE(WALLET_INVALID_TRANSACTION_ID, id); + + node.updateObservers(); + waitForTransactionUpdated(alice, id); + + ASSERT_TRUE(alice.isFusionTransaction(id)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransactionNotInTransfersContainer) { + generateFusionOutputsAndUnlock(alice, node, currency, FUSION_THRESHOLD); + + auto id = alice.createFusionTransaction(FUSION_THRESHOLD, 0); + ASSERT_NE(WALLET_INVALID_TRANSACTION_ID, id); + + ASSERT_TRUE(alice.isFusionTransaction(id)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransactionThrowsIfOutOfRange) { + ASSERT_ANY_THROW(alice.isFusionTransaction(1)); +} + +TEST_F(WalletApi, fusionManagerIsFusionTransactionSpent) { + CryptoNote::WalletGreen wallet(dispatcher, currency, node); + wallet.initialize("pass"); + wallet.createAddress(); + + generateFusionOutputsAndUnlock(alice, node, currency, FUSION_THRESHOLD); + + auto id = alice.createFusionTransaction(FUSION_THRESHOLD, 0); + ASSERT_NE(WALLET_INVALID_TRANSACTION_ID, id); + + unlockMoney(); + CryptoNote::WalletTransfer transfer; + transfer.address = wallet.getAddress(0); + transfer.amount = alice.getActualBalance() - currency.minimumFee(); + alice.transfer(aliceAddress, transfer, currency.minimumFee(), 0); + + auto pending = wallet.getPendingBalance(); + node.updateObservers(); + waitPendingBalanceUpdated(wallet, pending); + + ASSERT_TRUE(alice.isFusionTransaction(id)); +} + diff --git a/tests/UnitTests/TransactionApiHelpers.cpp b/tests/UnitTests/TransactionApiHelpers.cpp index e58a80a7..e129a7e8 100755 --- a/tests/UnitTests/TransactionApiHelpers.cpp +++ b/tests/UnitTests/TransactionApiHelpers.cpp @@ -232,6 +232,7 @@ Crypto::Hash TestTransactionBuilder::getTransactionHash() const { FusionTransactionBuilder::FusionTransactionBuilder(const Currency& currency, uint64_t amount) : m_currency(currency), m_amount(amount), + m_firstInput(0), m_firstOutput(0), m_fee(0), m_extraSize(0), @@ -246,6 +247,14 @@ void FusionTransactionBuilder::setAmount(uint64_t val) { m_amount = val; } +uint64_t FusionTransactionBuilder::getFirstInput() const { + return m_firstInput; +} + +void FusionTransactionBuilder::setFirstInput(uint64_t val) { + m_firstInput = val; +} + uint64_t FusionTransactionBuilder::getFirstOutput() const { return m_firstOutput; } @@ -280,6 +289,8 @@ void FusionTransactionBuilder::setInputCount(size_t val) { std::unique_ptr FusionTransactionBuilder::buildReader() const { assert(m_inputCount > 0); + assert(m_firstInput <= m_amount); + assert(m_amount > m_currency.defaultDustThreshold()); TestTransactionBuilder builder; @@ -287,9 +298,15 @@ std::unique_ptr FusionTransactionBuilder::buildReader() cons builder.appendExtra(BinaryArray(m_extraSize, 0)); } - builder.addTestInput(m_amount - (m_inputCount - 1) * m_currency.defaultDustThreshold()); - for (size_t i = 0; i < m_inputCount - 1; ++i) { - builder.addTestInput(m_currency.defaultDustThreshold()); + if (m_firstInput != 0) { + builder.addTestInput(m_firstInput); + } + + if (m_amount > m_firstInput) { + builder.addTestInput(m_amount - m_firstInput - (m_inputCount - 1) * m_currency.defaultDustThreshold()); + for (size_t i = 0; i < m_inputCount - 1; ++i) { + builder.addTestInput(m_currency.defaultDustThreshold()); + } } AccountPublicAddress address = generateAddress(); diff --git a/tests/UnitTests/TransactionApiHelpers.h b/tests/UnitTests/TransactionApiHelpers.h index d12bf8f4..eaa43507 100644 --- a/tests/UnitTests/TransactionApiHelpers.h +++ b/tests/UnitTests/TransactionApiHelpers.h @@ -177,6 +177,9 @@ public: uint64_t getAmount() const; void setAmount(uint64_t val); + uint64_t getFirstInput() const; + void setFirstInput(uint64_t val); + uint64_t getFirstOutput() const; void setFirstOutput(uint64_t val); @@ -197,6 +200,7 @@ public: private: const Currency& m_currency; uint64_t m_amount; + uint64_t m_firstInput; uint64_t m_firstOutput; uint64_t m_fee; size_t m_extraSize;