Merge pull request #239 from wowario/upstream

Upstream
master
jw 4 years ago committed by GitHub
commit 67eaf231ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
PACKAGE=qt
$(package)_version=5.7.1
$(package)_download_path=https://download.qt.io/archive/qt/5.7/5.7.1/submodules
$(package)_download_path=http://linorg.usp.br/Qt/archive/qt/5.7/5.7.1/submodules
$(package)_suffix=opensource-src-$($(package)_version).tar.gz
$(package)_file_name=qtbase-$($(package)_suffix)
$(package)_sha256_hash=95f83e532d23b3ddbde7973f380ecae1bac13230340557276f75f2e37984e410

@ -53,9 +53,7 @@ bool matches_category(relay_method method, relay_category category) noexcept
case relay_category::all:
return true;
case relay_category::relayable:
if (method == relay_method::none)
return false;
return true;
return method != relay_method::none;
case relay_category::broadcasted:
case relay_category::legacy:
break;
@ -65,6 +63,7 @@ bool matches_category(relay_method method, relay_category category) noexcept
{
default:
case relay_method::local:
case relay_method::stem:
return false;
case relay_method::block:
case relay_method::fluff:
@ -80,6 +79,7 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept
kept_by_block = 0;
do_not_relay = 0;
is_local = 0;
dandelionpp_stem = 0;
switch (method)
{
@ -92,6 +92,9 @@ void txpool_tx_meta_t::set_relay_method(relay_method method) noexcept
default:
case relay_method::fluff:
break;
case relay_method::stem:
dandelionpp_stem = 1;
break;
case relay_method::block:
kept_by_block = 1;
break;
@ -106,9 +109,26 @@ relay_method txpool_tx_meta_t::get_relay_method() const noexcept
return relay_method::none;
if (is_local)
return relay_method::local;
if (dandelionpp_stem)
return relay_method::stem;
return relay_method::fluff;
}
bool txpool_tx_meta_t::upgrade_relay_method(relay_method method) noexcept
{
static_assert(relay_method::none < relay_method::local, "bad relay_method value");
static_assert(relay_method::local < relay_method::stem, "bad relay_method value");
static_assert(relay_method::stem < relay_method::fluff, "bad relay_method value");
static_assert(relay_method::fluff < relay_method::block, "bad relay_method value");
if (get_relay_method() < method)
{
set_relay_method(method);
return true;
}
return false;
}
const command_line::arg_descriptor<std::string> arg_db_sync_mode = {
"db-sync-mode"
, "Specify sync option, using format [safe|fast|fastest]:[sync|async]:[<nblocks_per_sync>[blocks]|<nbytes_per_sync>[bytes]]."

@ -160,7 +160,7 @@ struct txpool_tx_meta_t
uint64_t max_used_block_height;
uint64_t last_failed_height;
uint64_t receive_time;
uint64_t last_relayed_time;
uint64_t last_relayed_time; //!< If Dandelion++ stem, randomized embargo timestamp. Otherwise, last relayed timestmap.
// 112 bytes
uint8_t kept_by_block;
uint8_t relayed;
@ -168,13 +168,17 @@ struct txpool_tx_meta_t
uint8_t double_spend_seen: 1;
uint8_t pruned: 1;
uint8_t is_local: 1;
uint8_t bf_padding: 5;
uint8_t dandelionpp_stem : 1;
uint8_t bf_padding: 4;
uint8_t padding[76]; // till 192 bytes
void set_relay_method(relay_method method) noexcept;
relay_method get_relay_method() const noexcept;
//! \return True if `get_relay_method()` now returns `method`.
bool upgrade_relay_method(relay_method method) noexcept;
//! See `relay_category` description
bool matches(const relay_category category) const noexcept
{
@ -1290,6 +1294,25 @@ public:
*/
virtual bool get_pruned_tx_blobs_from(const crypto::hash& h, size_t count, std::vector<cryptonote::blobdata> &bd) const = 0;
/**
* @brief fetches a variable number of blocks and transactions from the given height, in canonical blockchain order
*
* The subclass should return the blocks and transactions stored from the one with the given
* height. The number of blocks returned is variable, based on the max_size passed.
*
* @param start_height the height of the first block
* @param min_count the minimum number of blocks to return, if they exist
* @param max_count the maximum number of blocks to return
* @param max_size the maximum size of block/transaction data to return (will be exceeded by one blocks's worth at most, if min_count is met)
* @param blocks the returned block/transaction data
* @param pruned whether to return full or pruned tx data
* @param skip_coinbase whether to return or skip coinbase transactions (they're in blocks regardless)
* @param get_miner_tx_hash whether to calculate and return the miner (coinbase) tx hash
*
* @return true iff the blocks and transactions were found
*/
virtual bool get_blocks_from(uint64_t start_height, size_t min_count, size_t max_count, size_t max_size, std::vector<std::pair<std::pair<cryptonote::blobdata, crypto::hash>, std::vector<std::pair<crypto::hash, cryptonote::blobdata>>>>& blocks, bool pruned, bool skip_coinbase, bool get_miner_tx_hash) const = 0;
/**
* @brief fetches the prunable transaction blob with the given hash
*

@ -3108,6 +3108,104 @@ bool BlockchainLMDB::get_pruned_tx_blobs_from(const crypto::hash& h, size_t coun
return true;
}
bool BlockchainLMDB::get_blocks_from(uint64_t start_height, size_t min_count, size_t max_count, size_t max_size, std::vector<std::pair<std::pair<cryptonote::blobdata, crypto::hash>, std::vector<std::pair<crypto::hash, cryptonote::blobdata>>>>& blocks, bool pruned, bool skip_coinbase, bool get_miner_tx_hash) const
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
check_open();
TXN_PREFIX_RDONLY();
RCURSOR(blocks);
RCURSOR(tx_indices);
RCURSOR(txs_pruned);
if (!pruned)
{
RCURSOR(txs_prunable);
}
blocks.reserve(std::min<size_t>(max_count, 10000)); // guard against very large max count if only checking bytes
const uint64_t blockchain_height = height();
uint64_t size = 0;
MDB_val_copy<uint64_t> key(start_height);
MDB_val k, v, val_tx_id;
uint64_t tx_id = ~0;
MDB_cursor_op op = MDB_SET;
for (uint64_t h = start_height; h < blockchain_height && blocks.size() < max_count && (size < max_size || blocks.size() < min_count); ++h)
{
MDB_cursor_op op = h == start_height ? MDB_SET : MDB_NEXT;
int result = mdb_cursor_get(m_cur_blocks, &key, &v, op);
if (result == MDB_NOTFOUND)
throw0(BLOCK_DNE(std::string("Attempt to get block from height ").append(boost::lexical_cast<std::string>(h)).append(" failed -- block not in db").c_str()));
else if (result)
throw0(DB_ERROR(lmdb_error("Error attempting to retrieve a block from the db", result).c_str()));
blocks.resize(blocks.size() + 1);
auto &current_block = blocks.back();
current_block.first.first.assign(reinterpret_cast<char*>(v.mv_data), v.mv_size);
size += v.mv_size;
cryptonote::block b;
if (!parse_and_validate_block_from_blob(current_block.first.first, b))
throw0(DB_ERROR("Invalid block"));
current_block.first.second = get_miner_tx_hash ? cryptonote::get_transaction_hash(b.miner_tx) : crypto::null_hash;
// get the tx_id for the first tx (the first block's coinbase tx)
if (h == start_height)
{
crypto::hash hash = cryptonote::get_transaction_hash(b.miner_tx);
MDB_val_set(v, hash);
result = mdb_cursor_get(m_cur_tx_indices, (MDB_val *)&zerokval, &v, MDB_GET_BOTH);
if (result)
throw0(DB_ERROR(lmdb_error("Error attempting to retrieve block coinbase transaction from the db: ", result).c_str()));
const txindex *tip = (const txindex *)v.mv_data;
tx_id = tip->data.tx_id;
val_tx_id.mv_data = &tx_id;
val_tx_id.mv_size = sizeof(tx_id);
}
if (skip_coinbase)
{
result = mdb_cursor_get(m_cur_txs_pruned, &val_tx_id, &v, op);
if (result)
throw0(DB_ERROR(lmdb_error("Error attempting to retrieve transaction data from the db: ", result).c_str()));
if (!pruned)
{
result = mdb_cursor_get(m_cur_txs_prunable, &val_tx_id, &v, op);
if (result)
throw0(DB_ERROR(lmdb_error("Error attempting to retrieve transaction data from the db: ", result).c_str()));
}
}
op = MDB_NEXT;
current_block.second.reserve(b.tx_hashes.size());
for (const auto &tx_hash: b.tx_hashes)
{
// get pruned data
cryptonote::blobdata tx_blob;
result = mdb_cursor_get(m_cur_txs_pruned, &val_tx_id, &v, op);
if (result)
throw0(DB_ERROR(lmdb_error("Error attempting to retrieve transaction data from the db: ", result).c_str()));
tx_blob.assign((const char*)v.mv_data, v.mv_size);
if (!pruned)
{
result = mdb_cursor_get(m_cur_txs_prunable, &val_tx_id, &v, op);
if (result)
throw0(DB_ERROR(lmdb_error("Error attempting to retrieve transaction data from the db: ", result).c_str()));
tx_blob.append(reinterpret_cast<const char*>(v.mv_data), v.mv_size);
}
current_block.second.push_back(std::make_pair(tx_hash, std::move(tx_blob)));
size += current_block.second.back().second.size();
}
}
TXN_POSTFIX_RDONLY();
return true;
}
bool BlockchainLMDB::get_prunable_tx_blob(const crypto::hash& h, cryptonote::blobdata &bd) const
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);

@ -255,6 +255,7 @@ public:
virtual bool get_tx_blob(const crypto::hash& h, cryptonote::blobdata &tx) const;
virtual bool get_pruned_tx_blob(const crypto::hash& h, cryptonote::blobdata &tx) const;
virtual bool get_pruned_tx_blobs_from(const crypto::hash& h, size_t count, std::vector<cryptonote::blobdata> &bd) const;
virtual bool get_blocks_from(uint64_t start_height, size_t min_count, size_t max_count, size_t max_size, std::vector<std::pair<std::pair<cryptonote::blobdata, crypto::hash>, std::vector<std::pair<crypto::hash, cryptonote::blobdata>>>>& blocks, bool pruned, bool skip_coinbase, bool get_miner_tx_hash) const;
virtual bool get_prunable_tx_blob(const crypto::hash& h, cryptonote::blobdata &tx) const;
virtual bool get_prunable_tx_hash(const crypto::hash& tx_hash, crypto::hash &prunable_hash) const;

@ -70,6 +70,7 @@ public:
virtual bool get_tx_blob(const crypto::hash& h, cryptonote::blobdata &tx) const override { return false; }
virtual bool get_pruned_tx_blob(const crypto::hash& h, cryptonote::blobdata &tx) const override { return false; }
virtual bool get_pruned_tx_blobs_from(const crypto::hash& h, size_t count, std::vector<cryptonote::blobdata> &bd) const { return false; }
virtual bool get_blocks_from(uint64_t start_height, size_t min_count, size_t max_count, size_t max_size, std::vector<std::pair<std::pair<cryptonote::blobdata, crypto::hash>, std::vector<std::pair<crypto::hash, cryptonote::blobdata>>>>& blocks, bool pruned, bool skip_coinbase, bool get_miner_tx_hash) const { return false; }
virtual bool get_prunable_tx_blob(const crypto::hash& h, cryptonote::blobdata &tx) const override { return false; }
virtual bool get_prunable_tx_hash(const crypto::hash& tx_hash, crypto::hash &prunable_hash) const override { return false; }
virtual uint64_t get_block_height(const crypto::hash& h) const override { return 0; }

@ -0,0 +1,70 @@
// Copyright (c) 2020, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <chrono>
#include "crypto/crypto.h"
namespace crypto
{
//! Generate poisson distributed values in discrete `D` time units.
template<typename D>
struct random_poisson_duration
{
using result_type = D; //!< std::chrono::duration time unit precision
using rep = typename result_type::rep; //!< Type used to represent duration value
//! \param average for generated durations
explicit random_poisson_duration(result_type average)
: dist(average.count() < 0 ? 0 : average.count())
{}
//! Generate a crypto-secure random duration
result_type operator()()
{
crypto::random_device rand{};
return result_type{dist(rand)};
}
private:
std::poisson_distribution<rep> dist;
};
/* A custom duration is used for subsecond precision because of the
variance. If 5000 milliseconds is given, 95% of the values fall between
4859ms-5141ms in 1ms increments (not enough time variance). Providing 1/4
seconds would yield 95% of the values between 3s-7.25s in 1/4s
increments. */
//! Generate random durations with 1 second precision
using random_poisson_seconds = random_poisson_duration<std::chrono::seconds>;
//! Generate random duration with 1/4 second precision
using random_poisson_subseconds =
random_poisson_duration<std::chrono::duration<std::chrono::milliseconds::rep, std::ratio<1, 4>>>;
}

@ -29,6 +29,9 @@
// Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
#pragma once
#include "cryptonote_protocol/enums.h"
namespace cryptonote
{
/************************************************************************/
@ -36,7 +39,9 @@ namespace cryptonote
/************************************************************************/
struct tx_verification_context
{
bool m_should_be_relayed;
static_assert(unsigned(relay_method::none) == 0, "default m_relay initialization is not to relay_method::none");
relay_method m_relay; // gives indication on how tx should be relayed (if at all)
bool m_verifivation_failed; //bad tx, should drop connection
bool m_verifivation_impossible; //the transaction is related with an alternative blockchain
bool m_added_to_pool;

@ -109,7 +109,12 @@
#define CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME 604800 //seconds, one week
#define CRYPTONOTE_DANDELIONPP_FLUSH_AVERAGE 5 // seconds
#define CRYPTONOTE_DANDELIONPP_STEMS 2 // number of outgoing stem connections per epoch
#define CRYPTONOTE_DANDELIONPP_FLUFF_PROBABILITY 10 // out of 100
#define CRYPTONOTE_DANDELIONPP_MIN_EPOCH 10 // minutes
#define CRYPTONOTE_DANDELIONPP_EPOCH_RANGE 30 // seconds
#define CRYPTONOTE_DANDELIONPP_FLUSH_AVERAGE 5 // seconds average for poisson distributed fluff flush
#define CRYPTONOTE_DANDELIONPP_EMBARGO_AVERAGE 173 // seconds (see tx_pool.cpp for more info)
// see src/cryptonote_protocol/levin_notify.cpp
#define CRYPTONOTE_NOISE_MIN_EPOCH 5 // minutes

@ -2535,38 +2535,10 @@ bool Blockchain::find_blockchain_supplement(const uint64_t req_start_block, cons
}
db_rtxn_guard rtxn_guard(m_db);
total_height = get_current_blockchain_height();
size_t count = 0, size = 0;
blocks.reserve(std::min(std::min(max_count, (size_t)10000), (size_t)(total_height - start_height)));
for(uint64_t i = start_height; i < total_height && count < max_count && (size < FIND_BLOCKCHAIN_SUPPLEMENT_MAX_SIZE || count < 3); i++, count++)
{
blocks.resize(blocks.size()+1);
blocks.back().first.first = m_db->get_block_blob_from_height(i);
block b;
CHECK_AND_ASSERT_MES(parse_and_validate_block_from_blob(blocks.back().first.first, b), false, "internal error, invalid block");
blocks.back().first.second = get_miner_tx_hash ? cryptonote::get_transaction_hash(b.miner_tx) : crypto::null_hash;
std::vector<cryptonote::blobdata> txs;
if (pruned)
{
CHECK_AND_ASSERT_MES(m_db->get_pruned_tx_blobs_from(b.tx_hashes.front(), b.tx_hashes.size(), txs), false, "Failed to retrieve all transactions needed");
}
else
{
std::vector<crypto::hash> mis;
get_transactions_blobs(b.tx_hashes, txs, mis, pruned);
CHECK_AND_ASSERT_MES(!mis.size(), false, "internal error, transaction from block not found");
}
size += blocks.back().first.first.size();
for (const auto &t: txs)
size += t.size();
CHECK_AND_ASSERT_MES(m_db->get_blocks_from(start_height, 3, max_count, FIND_BLOCKCHAIN_SUPPLEMENT_MAX_SIZE, blocks, pruned, true, get_miner_tx_hash),
false, "Error getting blocks");
CHECK_AND_ASSERT_MES(txs.size() == b.tx_hashes.size(), false, "mismatched sizes of b.tx_hashes and txs");
blocks.back().second.reserve(txs.size());
for (size_t i = 0; i < txs.size(); ++i)
{
blocks.back().second.push_back(std::make_pair(b.tx_hashes[i], std::move(txs[i])));
}
}
return true;
}
//------------------------------------------------------------------

@ -1324,6 +1324,7 @@ namespace cryptonote
break;
case relay_method::block:
case relay_method::fluff:
case relay_method::stem:
public_req.txs.push_back(std::move(std::get<1>(tx)));
break;
}
@ -1335,9 +1336,9 @@ namespace cryptonote
re-relaying public and private _should_ be acceptable here. */
const boost::uuids::uuid source = boost::uuids::nil_uuid();
if (!public_req.txs.empty())
get_protocol()->relay_transactions(public_req, source, epee::net_utils::zone::public_);
get_protocol()->relay_transactions(public_req, source, epee::net_utils::zone::public_, relay_method::fluff);
if (!private_req.txs.empty())
get_protocol()->relay_transactions(private_req, source, epee::net_utils::zone::invalid);
get_protocol()->relay_transactions(private_req, source, epee::net_utils::zone::invalid, relay_method::local);
}
return true;
}

@ -46,6 +46,7 @@
#include "warnings.h"
#include "common/perf_timer.h"
#include "crypto/hash.h"
#include "crypto/duration.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "txpool"
@ -58,6 +59,29 @@ namespace cryptonote
{
namespace
{
/*! The Dandelion++ has formula for calculating the average embargo timeout:
(-k*(k-1)*hop)/(2*log(1-ep))
where k is the number of hops before this node and ep is the probability
that one of the k hops hits their embargo timer, and hop is the average
time taken between hops. So decreasing ep will make it more probable
that "this" node is the first to expire the embargo timer. Increasing k
will increase the number of nodes that will be "hidden" as a prior
recipient of the tx.
As example, k=5 and ep=0.1 means "this" embargo timer has a 90%
probability of being the first to expire amongst 5 nodes that saw the
tx before "this" one. These values are independent to the fluff
probability, but setting a low k with a low p (fluff probability) is
not ideal since a blackhole is more likely to reveal earlier nodes in
the chain.
This value was calculated with k=10, ep=0.10, and hop = 175 ms. A
testrun from a recent Intel laptop took ~80ms to
receive+parse+proces+send transaction. At least 50ms will be added to
the latency if crossing an ocean. So 175ms is the fudge factor for
a single hop with 173s being the embargo timer. */
constexpr const std::chrono::seconds dandelionpp_embargo_average{CRYPTONOTE_DANDELIONPP_EMBARGO_AVERAGE};
//TODO: constants such as these should at least be in the header,
// but probably somewhere more accessible to the rest of the
// codebase. As it stands, it is at best nontrivial to test
@ -262,34 +286,51 @@ namespace cryptonote
}
}else
{
//update transactions container
meta.weight = tx_weight;
meta.fee = fee;
meta.max_used_block_id = max_used_block_id;
meta.max_used_block_height = max_used_block_height;
meta.last_failed_height = 0;
meta.last_failed_id = null_hash;
meta.receive_time = receive_time;
meta.last_relayed_time = time(NULL);
meta.relayed = relayed;
meta.set_relay_method(tx_relay);
meta.double_spend_seen = false;
meta.pruned = tx.pruned;
meta.bf_padding = 0;
memset(meta.padding, 0, sizeof(meta.padding));
try
{
if (kept_by_block)
m_parsed_tx_cache.insert(std::make_pair(id, tx));
CRITICAL_REGION_LOCAL1(m_blockchain);
LockedTXN lock(m_blockchain.get_db());
m_blockchain.remove_txpool_tx(id);
if (!insert_key_images(tx, id, tx_relay))
return false;
m_blockchain.add_txpool_tx(id, blob, meta);
m_txs_by_fee_and_receive_time.emplace(std::pair<double, std::time_t>(fee / (double)(tx_weight ? tx_weight : 1), receive_time), id);
const bool existing_tx = m_blockchain.get_txpool_tx_meta(id, meta);
if (existing_tx)
{
/* If Dandelion++ loop. Do not use txes in the `local` state in the
loop detection - txes in that state should be outgoing over i2p/tor
then routed back via public dandelion++ stem. Pretend to be
another stem node in that situation, a loop over the public
network hasn't been hit yet. */
if (tx_relay == relay_method::stem && meta.dandelionpp_stem)
tx_relay = relay_method::fluff;
}
else
meta.set_relay_method(relay_method::none);
if (meta.upgrade_relay_method(tx_relay) || !existing_tx) // synchronize with embargo timer or stem/fluff out-of-order messages
{
//update transactions container
meta.last_relayed_time = std::numeric_limits<decltype(meta.last_relayed_time)>::max();
meta.receive_time = receive_time;
meta.weight = tx_weight;
meta.fee = fee;
meta.max_used_block_id = max_used_block_id;
meta.max_used_block_height = max_used_block_height;
meta.last_failed_height = 0;
meta.last_failed_id = null_hash;
meta.relayed = relayed;
meta.double_spend_seen = false;
meta.pruned = tx.pruned;
meta.bf_padding = 0;
memset(meta.padding, 0, sizeof(meta.padding));
if (!insert_key_images(tx, id, tx_relay))
return false;
m_blockchain.remove_txpool_tx(id);
m_blockchain.add_txpool_tx(id, blob, meta);
m_txs_by_fee_and_receive_time.emplace(std::pair<double, std::time_t>(fee / (double)(tx_weight ? tx_weight : 1), receive_time), id);
}
lock.commit();
}
catch (const std::exception &e)
@ -299,8 +340,9 @@ namespace cryptonote
}
tvc.m_added_to_pool = true;
if(meta.fee > 0 && tx_relay != relay_method::none)
tvc.m_should_be_relayed = true;
static_assert(unsigned(relay_method::none) == 0, "expected relay_method::none value to be zero");
if(meta.fee > 0)
tvc.m_relay = tx_relay;
}
tvc.m_verifivation_failed = false;
@ -404,27 +446,18 @@ namespace cryptonote
CHECKED_GET_SPECIFIC_VARIANT(in, const txin_to_key, txin, false);
std::unordered_set<crypto::hash>& kei_image_set = m_spent_key_images[txin.k_image];
/* If any existing key-image in the set is publicly visible AND this is
not forcibly "kept_by_block", then fail (duplicate key image). If all
existing key images are supposed to be hidden, we silently allow so
that the node doesn't leak knowledge of a local/stem tx. */
bool visible = false;
// Only allow multiple txes per key-image if kept-by-block. Only allow
// the same txid if going from local/stem->fluff.
if (tx_relay != relay_method::block)
{
for (const crypto::hash& other_id : kei_image_set)
visible |= m_blockchain.txpool_tx_matches_category(other_id, relay_category::legacy);
}
CHECK_AND_ASSERT_MES(!visible, false, "internal error: tx_relay=" << unsigned(tx_relay)
const bool one_txid =
(kei_image_set.empty() || (kei_image_set.size() == 1 && *(kei_image_set.cbegin()) == id));
CHECK_AND_ASSERT_MES(one_txid, false, "internal error: tx_relay=" << unsigned(tx_relay)
<< ", kei_image_set.size()=" << kei_image_set.size() << ENDL << "txin.k_image=" << txin.k_image << ENDL
<< "tx_id=" << id);
}
/* If adding a tx (hash) that already exists, fail only if the tx has
been publicly "broadcast" previously. This way, when a private tx is
received for the first time from a remote node, "this" node will
respond as-if it were seen for the first time. LMDB does the
"hard-check" on key-images, so the effect is overwriting the existing
tx_pool metadata and "first seen" time. */
const bool new_or_previously_private =
kei_image_set.insert(id).second ||
!m_blockchain.txpool_tx_matches_category(id, relay_category::legacy);
@ -562,7 +595,7 @@ namespace cryptonote
td.last_failed_height = meta.last_failed_height;
td.last_failed_id = meta.last_failed_id;
td.receive_time = meta.receive_time;
td.last_relayed_time = meta.last_relayed_time;
td.last_relayed_time = meta.dandelionpp_stem ? 0 : meta.last_relayed_time;
td.relayed = meta.relayed;
td.do_not_relay = meta.do_not_relay;
td.double_spend_seen = meta.double_spend_seen;
@ -695,8 +728,13 @@ namespace cryptonote
txs.reserve(m_blockchain.get_txpool_tx_count());
m_blockchain.for_all_txpool_txes([this, now, &txs](const crypto::hash &txid, const txpool_tx_meta_t &meta, const cryptonote::blobdata *){
// 0 fee transactions are never relayed
if(!meta.pruned && meta.fee > 0 && !meta.do_not_relay && now - meta.last_relayed_time > get_relay_delay(now, meta.receive_time))
if(!meta.pruned && meta.fee > 0 && !meta.do_not_relay)
{
if (!meta.dandelionpp_stem && now - meta.last_relayed_time <= get_relay_delay(now, meta.receive_time))
return true;
if (meta.dandelionpp_stem && meta.last_relayed_time < now) // for dandelion++ stem, this value is the embargo timeout
return true;
// if the tx is older than half the max lifetime, we don't re-relay it, to avoid a problem
// mentioned by smooth where nodes would flush txes at slightly different times, causing
// flushed txes to be re-added when received from a node which was just about to flush it
@ -721,9 +759,11 @@ namespace cryptonote
//---------------------------------------------------------------------------------
void tx_memory_pool::set_relayed(const epee::span<const crypto::hash> hashes, const relay_method method)
{
crypto::random_poisson_seconds embargo_duration{dandelionpp_embargo_average};
const auto now = std::chrono::system_clock::now();
CRITICAL_REGION_LOCAL(m_transactions_lock);
CRITICAL_REGION_LOCAL1(m_blockchain);
const time_t now = time(NULL);
LockedTXN lock(m_blockchain.get_db());
for (const auto& hash : hashes)
{
@ -732,9 +772,15 @@ namespace cryptonote
txpool_tx_meta_t meta;
if (m_blockchain.get_txpool_tx_meta(hash, meta))
{
// txes can be received as "stem" or "fluff" in either order
meta.upgrade_relay_method(method);
meta.relayed = true;
meta.last_relayed_time = now;
meta.set_relay_method(method);
if (meta.dandelionpp_stem)
meta.last_relayed_time = std::chrono::system_clock::to_time_t(now + embargo_duration());
else
meta.last_relayed_time = std::chrono::system_clock::to_time_t(now);
m_blockchain.update_txpool_tx(hash, meta);
}
}
@ -919,7 +965,7 @@ namespace cryptonote
txi.receive_time = include_sensitive_data ? meta.receive_time : 0;
txi.relayed = meta.relayed;
// In restricted mode we do not include this data:
txi.last_relayed_time = include_sensitive_data ? meta.last_relayed_time : 0;
txi.last_relayed_time = (include_sensitive_data && !meta.dandelionpp_stem) ? meta.last_relayed_time : 0;
txi.do_not_relay = meta.do_not_relay;
txi.double_spend_seen = meta.double_spend_seen;
tx_infos.push_back(std::move(txi));
@ -971,7 +1017,7 @@ namespace cryptonote
txi.last_failed_block_hash = meta.last_failed_id;
txi.receive_time = meta.receive_time;
txi.relayed = meta.relayed;
txi.last_relayed_time = meta.last_relayed_time;
txi.last_relayed_time = meta.dandelionpp_stem ? 0 : meta.last_relayed_time;
txi.do_not_relay = meta.do_not_relay;
txi.double_spend_seen = meta.double_spend_seen;
tx_infos.push_back(txi);

@ -197,10 +197,12 @@ namespace cryptonote
{
std::vector<blobdata> txs;
std::string _; // padding
bool dandelionpp_fluff; //zero initialization defaults to stem mode
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(txs)
KV_SERIALIZE(_)
KV_SERIALIZE_OPT(dandelionpp_fluff, true) // backwards compatible mode is fluff
END_KV_SERIALIZE_MAP()
};
typedef epee::misc_utils::struct_init<request_t> request;

@ -129,7 +129,7 @@ namespace cryptonote
//----------------- i_bc_protocol_layout ---------------------------------------
virtual bool relay_block(NOTIFY_NEW_BLOCK::request& arg, cryptonote_connection_context& exclude_context);
virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone);
virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay);
//----------------------------------------------------------------------------------
//bool get_payload_sync_data(HANDSHAKE_DATA::request& hshd, cryptonote_connection_context& context);
bool should_drop_connection(cryptonote_connection_context& context, uint32_t next_stripe);

@ -926,29 +926,60 @@ namespace cryptonote
return 1;
}
std::vector<cryptonote::blobdata> newtxs;
newtxs.reserve(arg.txs.size());
for (size_t i = 0; i < arg.txs.size(); ++i)
relay_method tx_relay;
std::vector<blobdata> stem_txs{};
std::vector<blobdata> fluff_txs{};
if (arg.dandelionpp_fluff)
{
cryptonote::tx_verification_context tvc{};
m_core.handle_incoming_tx({arg.txs[i], crypto::null_hash}, tvc, relay_method::fluff, true);
if(tvc.m_verifivation_failed)
tx_relay = relay_method::fluff;
fluff_txs.reserve(arg.txs.size());
}
else
{
tx_relay = relay_method::stem;
stem_txs.reserve(arg.txs.size());
}
for (auto& tx : arg.txs)
{
tx_verification_context tvc{};
if (!m_core.handle_incoming_tx({tx, crypto::null_hash}, tvc, tx_relay, true))
{
LOG_PRINT_CCONTEXT_L1("Tx verification failed, dropping connection");
drop_connection(context, false, false);
return 1;
}
if(tvc.m_should_be_relayed)
newtxs.push_back(std::move(arg.txs[i]));
switch (tvc.m_relay)
{
case relay_method::local:
case relay_method::stem:
stem_txs.push_back(std::move(tx));
break;
case relay_method::block:
case relay_method::fluff:
fluff_txs.push_back(std::move(tx));
break;
default:
case relay_method::none:
break;
}
}
arg.txs = std::move(newtxs);
if(arg.txs.size())
if (!stem_txs.empty())
{
//TODO: add announce usage here
relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone());
arg.dandelionpp_fluff = false;
arg.txs = std::move(stem_txs);
relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone(), relay_method::stem);
}
if (!fluff_txs.empty())
{
//TODO: add announce usage here
arg.dandelionpp_fluff = true;
arg.txs = std::move(fluff_txs);
relay_transactions(arg, context.m_connection_id, context.m_remote_address.get_zone(), relay_method::fluff);
}
return 1;
}
//------------------------------------------------------------------------------------------------------------------------
@ -2387,14 +2418,14 @@ skip:
}
//------------------------------------------------------------------------------------------------------------------------
template<class t_core>
bool t_cryptonote_protocol_handler<t_core>::relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone)
bool t_cryptonote_protocol_handler<t_core>::relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay)
{
/* Push all outgoing transactions to this function. The behavior needs to
identify how the transaction is going to be relayed, and then update the
local mempool before doing the relay. The code was already updating the
DB twice on received transactions - it is difficult to workaround this
due to the internal design. */
return m_p2p->send_txs(std::move(arg.txs), zone, source, m_core) != epee::net_utils::zone::invalid;
return m_p2p->send_txs(std::move(arg.txs), zone, source, m_core, tx_relay) != epee::net_utils::zone::invalid;
}
//------------------------------------------------------------------------------------------------------------------------
template<class t_core>

@ -41,7 +41,7 @@ namespace cryptonote
struct i_cryptonote_protocol
{
virtual bool relay_block(NOTIFY_NEW_BLOCK::request& arg, cryptonote_connection_context& exclude_context)=0;
virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone)=0;
virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay)=0;
//virtual bool request_objects(NOTIFY_REQUEST_GET_OBJECTS::request& arg, cryptonote_connection_context& context)=0;
};
@ -54,7 +54,7 @@ namespace cryptonote
{
return false;
}
virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone)
virtual bool relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, const boost::uuids::uuid& source, epee::net_utils::zone zone, relay_method tx_relay)
{
return false;
}

@ -37,7 +37,8 @@ namespace cryptonote
{
none = 0, //!< Received via RPC with `do_not_relay` set
local, //!< Received via RPC; trying to send over i2p/tor, etc.
block, //!< Received in block, takes precedence over others
fluff //!< Received/sent over public networks
stem, //!< Received/send over network using Dandelion++ stem
fluff, //!< Received/sent over network using Dandelion++ fluff
block //!< Received in block, takes precedence over others
};
}

@ -30,6 +30,7 @@
#include <boost/asio/steady_timer.hpp>
#include <boost/system/system_error.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <chrono>
#include <deque>
#include <stdexcept>
@ -38,8 +39,10 @@
#include "common/expect.h"
#include "common/varint.h"
#include "cryptonote_config.h"
#include "crypto/random.h"
#include "crypto/crypto.h"
#include "crypto/duration.h"
#include "cryptonote_basic/connection_context.h"
#include "cryptonote_core/i_core_events.h"
#include "cryptonote_protocol/cryptonote_protocol_defs.h"
#include "net/dandelionpp.h"
#include "p2p/net_node.h"
@ -61,11 +64,14 @@ namespace levin
{
namespace
{
constexpr std::size_t connection_id_reserve_size = 100;
constexpr const std::size_t connection_id_reserve_size = 100;
constexpr const std::chrono::minutes noise_min_epoch{CRYPTONOTE_NOISE_MIN_EPOCH};
constexpr const std::chrono::seconds noise_epoch_range{CRYPTONOTE_NOISE_EPOCH_RANGE};
constexpr const std::chrono::minutes dandelionpp_min_epoch{CRYPTONOTE_DANDELIONPP_MIN_EPOCH};
constexpr const std::chrono::seconds dandelionpp_epoch_range{CRYPTONOTE_DANDELIONPP_EPOCH_RANGE};
constexpr const std::chrono::seconds noise_min_delay{CRYPTONOTE_NOISE_MIN_DELAY};
constexpr const std::chrono::seconds noise_delay_range{CRYPTONOTE_NOISE_DELAY_RANGE};
@ -83,22 +89,8 @@ namespace levin
connections (Dandelion++ makes similar assumptions in its stem
algorithm). The randomization yields 95% values between 1s-4s in
1/4s increments. */
constexpr const fluff_stepsize fluff_average_out{fluff_stepsize{fluff_average_in} / 2};
class random_poisson
{
std::poisson_distribution<fluff_stepsize::rep> dist;
public:
explicit random_poisson(fluff_stepsize average)
: dist(average.count() < 0 ? 0 : average.count())
{}
fluff_stepsize operator()()
{
crypto::random_device rand{};
return fluff_stepsize{dist(rand)};
}
};
using fluff_duration = crypto::random_poisson_subseconds::result_type;
constexpr const fluff_duration fluff_average_out{fluff_duration{fluff_average_in} / 2};
/*! Select a randomized duration from 0 to `range`. The precision will be to
the systems `steady_clock`. As an example, supplying 3 seconds to this
@ -132,10 +124,11 @@ namespace levin
return outs;
}
std::string make_tx_payload(std::vector<blobdata>&& txs, const bool pad)
std::string make_tx_payload(std::vector<blobdata>&& txs, const bool pad, const bool fluff)
{
NOTIFY_NEW_TRANSACTIONS::request request{};
request.txs = std::move(txs);
request.dandelionpp_fluff = fluff;
if (pad)
{
@ -172,9 +165,9 @@ namespace levin
return fullBlob;
}
bool make_payload_send_txs(connections& p2p, std::vector<blobdata>&& txs, const boost::uuids::uuid& destination, const bool pad)
bool make_payload_send_txs(connections& p2p, std::vector<blobdata>&& txs, const boost::uuids::uuid& destination, const bool pad, const bool fluff)
{
const cryptonote::blobdata blob = make_tx_payload(std::move(txs), pad);
const cryptonote::blobdata blob = make_tx_payload(std::move(txs), pad, fluff);
p2p.for_connection(destination, [&blob](detail::p2p_context& context) {
on_levin_traffic(context, true, true, false, blob.size(), get_command_from_message(blob));
return true;
@ -251,7 +244,8 @@ namespace levin
flush_time(std::chrono::steady_clock::time_point::max()),
connection_count(0),
is_public(is_public),
pad_txs(pad_txs)
pad_txs(pad_txs),
fluffing(false)
{
for (std::size_t count = 0; !noise.empty() && count < CRYPTONOTE_NOISE_CHANNELS; ++count)
channels.emplace_back(io_service);
@ -268,6 +262,7 @@ namespace levin
std::atomic<std::size_t> connection_count; //!< Only update in strand, can be read at any time
const bool is_public; //!< Zone is public ipv4/ipv6 connections
const bool pad_txs; //!< Pad txs to the next boundary for privacy
bool fluffing; //!< Zone is in Dandelion++ fluff epoch
};
} // detail
@ -362,10 +357,11 @@ namespace levin
return true;
});
// Always send txs in stem mode over i2p/tor, see comments in `send_txs` below.
for (auto& connection : connections)
{
std::sort(connection.first.begin(), connection.first.end()); // don't leak receive order
make_payload_send_txs(*zone_->p2p, std::move(connection.first), connection.second, zone_->pad_txs);
make_payload_send_txs(*zone_->p2p, std::move(connection.first), connection.second, zone_->pad_txs, zone_->is_public);
}
if (next_flush != std::chrono::steady_clock::time_point::max())
@ -387,29 +383,38 @@ namespace levin
void operator()()
{
if (!zone_ || !zone_->p2p || txs_.empty())
run(std::move(zone_), epee::to_span(txs_), source_);
}
static void run(std::shared_ptr<detail::zone> zone, epee::span<const blobdata> txs, const boost::uuids::uuid& source)
{
if (!zone || !zone->p2p || txs.empty())
return;
assert(zone_->strand.running_in_this_thread());
assert(zone->strand.running_in_this_thread());
const auto now = std::chrono::steady_clock::now();
auto next_flush = std::chrono::steady_clock::time_point::max();
random_poisson in_duration(fluff_average_in);
random_poisson out_duration(fluff_average_out);
crypto::random_poisson_subseconds in_duration(fluff_average_in);
crypto::random_poisson_subseconds out_duration(fluff_average_out);
MDEBUG("Queueing " << txs.size() << " transaction(s) for Dandelion++ fluffing");
bool available = false;
zone_->p2p->foreach_connection([this, now, &in_duration, &out_duration, &next_flush, &available] (detail::p2p_context& context)
zone->p2p->foreach_connection([txs, now, &zone, &source, &in_duration, &out_duration, &next_flush, &available] (detail::p2p_context& context)
{
if (this->source_ != context.m_connection_id && (this->zone_->is_public || !context.m_is_income))
// When i2p/tor, only fluff to outbound connections
if (source != context.m_connection_id && (zone->is_public || !context.m_is_income))
{
available = true;
if (context.fluff_txs.empty())
context.flush_time = now + (context.m_is_income ? in_duration() : out_duration());
next_flush = std::min(next_flush, context.flush_time);
context.fluff_txs.reserve(context.fluff_txs.size() + this->txs_.size());
for (const blobdata& tx : this->txs_)
context.fluff_txs.reserve(context.fluff_txs.size() + txs.size());
for (const blobdata& tx : txs)
context.fluff_txs.push_back(tx); // must copy instead of move (multiple conns)
}
return true;
@ -418,8 +423,8 @@ namespace levin
if (!available)
MWARNING("Unable to send transaction(s), no available connections");
if (next_flush < zone_->flush_time)
fluff_flush::queue(std::move(zone_), next_flush);
if (next_flush < zone->flush_time)
fluff_flush::queue(std::move(zone), next_flush);
}
};
@ -471,6 +476,11 @@ namespace levin
assert(zone->strand.running_in_this_thread());
zone->connection_count = zone->map.size();
// only noise uses the "noise channels", only update when enabled
if (zone->noise.empty())
return;
for (auto id = zone->map.begin(); id != zone->map.end(); ++id)
{
const std::size_t i = id - zone->map.begin();
@ -478,27 +488,76 @@ namespace levin
}
}
//! \pre Called within `zone_->strand`.
static void run(std::shared_ptr<detail::zone> zone, std::vector<boost::uuids::uuid> out_connections)
{
if (!zone)
return;
assert(zone->strand.running_in_this_thread());
if (zone->map.update(std::move(out_connections)))
post(std::move(zone));
}
//! \pre Called within `zone_->strand`.
void operator()()
{
if (!zone_)
run(std::move(zone_), std::move(out_connections_));
}
};
//! Checks fluff status for this node, and then does stem or fluff for txes
struct dandelionpp_notify
{
std::shared_ptr<detail::zone> zone_;
i_core_events* core_;
std::vector<blobdata> txs_;
boost::uuids::uuid source_;
//! \pre Called in `zone_->strand`
void operator()()
{
if (!zone_ || !core_ || txs_.empty())
return;
assert(zone_->strand.running_in_this_thread());
if (zone_->map.update(std::move(out_connections_)))
post(std::move(zone_));
if (zone_->fluffing)
{
core_->on_transactions_relayed(epee::to_span(txs_), relay_method::fluff);
fluff_notify::run(std::move(zone_), epee::to_span(txs_), source_);
}
else // forward tx in stem
{
core_->on_transactions_relayed(epee::to_span(txs_), relay_method::stem);
for (int tries = 2; 0 < tries; tries--)
{
const boost::uuids::uuid destination = zone_->map.get_stem(source_);
if (!destination.is_nil() && make_payload_send_txs(*zone_->p2p, std::vector<blobdata>{txs_}, destination, zone_->pad_txs, false))
{
/* Source is intentionally omitted in debug log for privacy - a
nil uuid indicates source is that node. */
MDEBUG("Sent " << txs_.size() << " transaction(s) to " << destination << " using Dandelion++ stem");
return;
}
// connection list may be outdated, try again
update_channels::run(zone_, get_out_connections(*zone_->p2p));
}
MERROR("Unable to send transaction(s) via Dandelion++ stem");
}
}
};
//! Swaps out noise channels entirely; new epoch start.
//! Swaps out noise/dandelionpp channels entirely; new epoch start.
class change_channels
{
std::shared_ptr<detail::zone> zone_;
net::dandelionpp::connection_map map_; // Requires manual copy constructor
bool fluffing_;
public:
explicit change_channels(std::shared_ptr<detail::zone> zone, net::dandelionpp::connection_map map)
: zone_(std::move(zone)), map_(std::move(map))
explicit change_channels(std::shared_ptr<detail::zone> zone, net::dandelionpp::connection_map map, const bool fluffing)
: zone_(std::move(zone)), map_(std::move(map)), fluffing_(fluffing)
{}
change_channels(change_channels&&) = default;
@ -510,11 +569,15 @@ namespace levin
void operator()()
{
if (!zone_)
return
return;
assert(zone_->strand.running_in_this_thread());
if (zone_->is_public)
MDEBUG("Starting new Dandelion++ epoch: " << (fluffing_ ? "fluff" : "stem"));
zone_->map = std::move(map_);
zone_->fluffing = fluffing_;
update_channels::post(std::move(zone_));
}
};
@ -608,9 +671,10 @@ namespace levin
if (error && error != boost::system::errc::operation_canceled)
throw boost::system::system_error{error, "start_epoch timer failed"};
const bool fluffing = crypto::rand_idx(unsigned(100)) < CRYPTONOTE_DANDELIONPP_FLUFF_PROBABILITY;
const auto start = std::chrono::steady_clock::now();
zone_->strand.dispatch(
change_channels{zone_, net::dandelionpp::connection_map{get_out_connections(*(zone_->p2p)), count_}}
change_channels{zone_, net::dandelionpp::connection_map{get_out_connections(*(zone_->p2p)), count_}, fluffing}
);
detail::zone& alias = *zone_;
@ -626,10 +690,16 @@ namespace levin
if (!zone_->p2p)
throw std::logic_error{"cryptonote::levin::notify cannot have nullptr p2p argument"};
if (!zone_->noise.empty())
const bool noise_enabled = !zone_->noise.empty();
if (noise_enabled || is_public)
{
const auto now = std::chrono::steady_clock::now();
start_epoch{zone_, noise_min_epoch, noise_epoch_range, CRYPTONOTE_NOISE_CHANNELS}();
const auto min_epoch = noise_enabled ? noise_min_epoch : dandelionpp_min_epoch;
const auto epoch_range = noise_enabled ? noise_epoch_range : dandelionpp_epoch_range;
const std::size_t out_count = noise_enabled ? CRYPTONOTE_NOISE_CHANNELS : CRYPTONOTE_DANDELIONPP_STEMS;
start_epoch{zone_, min_epoch, epoch_range, out_count}();
for (std::size_t channel = 0; channel < zone_->channels.size(); ++channel)
send_noise::wait(now, zone_, channel);
}
@ -679,7 +749,7 @@ namespace levin
zone_->flush_txs.cancel();
}
bool notify::send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source)
bool notify::send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source, i_core_events& core, relay_method tx_relay)
{
if (txs.empty())
return true;
@ -687,6 +757,17 @@ namespace levin
if (!zone_)
return false;
/* If noise is enabled in a zone, it always takes precedence. The technique
provides good protection against ISP adversaries, but not sybil
adversaries. Noise is currently only enabled over I2P/Tor - those
networks provide protection against sybil attacks (we only send to
outgoing connections).
If noise is disabled, Dandelion++ is used for public networks only.
Dandelion++ over I2P/Tor should be an interesting case to investigate,
but the mempool/stempool needs to know the zone a tx originated from to
work properly. */
if (!zone_->noise.empty() && !zone_->channels.empty())
{
// covert send in "noise" channel
@ -694,8 +775,17 @@ namespace levin
CRYPTONOTE_MAX_FRAGMENTS * CRYPTONOTE_NOISE_BYTES <= LEVIN_DEFAULT_MAX_PACKET_SIZE, "most nodes will reject this fragment setting"
);
// padding is not useful when using noise mode
const std::string payload = make_tx_payload(std::move(txs), false);
if (tx_relay == relay_method::stem)
{
MWARNING("Dandelion++ stem not supported over noise networks");
tx_relay = relay_method::local; // do not put into stempool embargo (hopefully not there already!).
}
core.on_transactions_relayed(epee::to_span(txs), tx_relay);
// Padding is not useful when using noise mode. Send as stem so receiver
// forwards in Dandelion++ mode.
const std::string payload = make_tx_payload(std::move(txs), false, false);
epee::byte_slice message = epee::levin::make_fragmented_notify(
zone_->noise, NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<std::uint8_t>(payload)
);
@ -714,9 +804,31 @@ namespace levin
}
else
{
zone_->strand.dispatch(fluff_notify{zone_, std::move(txs), source});
switch (tx_relay)
{
default:
case relay_method::none:
case relay_method::block:
return false;
case relay_method::stem:
tx_relay = relay_method::fluff; // don't set stempool embargo when skipping to fluff
/* fallthrough */
case relay_method::local:
if (zone_->is_public)
{
// this will change a local tx to stem or fluff ...
zone_->strand.dispatch(
dandelionpp_notify{zone_, std::addressof(core), std::move(txs), source}
);
break;
}
/* fallthrough */
case relay_method::fluff:
core.on_transactions_relayed(epee::to_span(txs), tx_relay);
zone_->strand.dispatch(fluff_notify{zone_, std::move(txs), source});
break;
}
}
return true;
}
} // levin

@ -35,6 +35,7 @@
#include "byte_slice.h"
#include "cryptonote_basic/blobdatatype.h"
#include "cryptonote_protocol/enums.h"
#include "cryptonote_protocol/fwd.h"
#include "net/enums.h"
#include "span.h"
@ -122,7 +123,7 @@ namespace levin
particular stem.
\return True iff the notification is queued for sending. */
bool send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source);
bool send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source, i_core_events& core, relay_method tx_relay);
};
} // levin
} // net

@ -331,7 +331,7 @@ namespace nodetool
virtual void callback(p2p_connection_context& context);
//----------------- i_p2p_endpoint -------------------------------------------------------------
virtual bool relay_notify_to_list(int command, const epee::span<const uint8_t> data_buff, std::vector<std::pair<epee::net_utils::zone, boost::uuids::uuid>> connections);
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core);
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, cryptonote::relay_method tx_relay);
virtual bool invoke_command_to_peer(int command, const epee::span<const uint8_t> req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context);
virtual bool invoke_notify_to_peer(int command, const epee::span<const uint8_t> req_buff, const epee::net_utils::connection_context_base& context);
virtual bool drop_connection(const epee::net_utils::connection_context_base& context);

@ -1966,18 +1966,13 @@ namespace nodetool
}
//-----------------------------------------------------------------------------------
template<class t_payload_net_handler>
epee::net_utils::zone node_server<t_payload_net_handler>::send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core)
epee::net_utils::zone node_server<t_payload_net_handler>::send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, const cryptonote::relay_method tx_relay)
{
namespace enet = epee::net_utils;
const auto send = [&txs, &source, &core] (std::pair<const enet::zone, network_zone>& network)
const auto send = [&txs, &source, &core, tx_relay] (std::pair<const enet::zone, network_zone>& network)
{
const bool is_public = (network.first == enet::zone::public_);
const cryptonote::relay_method tx_relay = is_public ?
cryptonote::relay_method::fluff : cryptonote::relay_method::local;
core.on_transactions_relayed(epee::to_span(txs), tx_relay);
if (network.second.m_notifier.send_txs(std::move(txs), source))
if (network.second.m_notifier.send_txs(std::move(txs), source, core, tx_relay))
return network.first;
return enet::zone::invalid;
};

@ -50,7 +50,7 @@ namespace nodetool
struct i_p2p_endpoint
{
virtual bool relay_notify_to_list(int command, const epee::span<const uint8_t> data_buff, std::vector<std::pair<epee::net_utils::zone, boost::uuids::uuid>> connections)=0;
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core)=0;
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, cryptonote::relay_method tx_relay)=0;
virtual bool invoke_command_to_peer(int command, const epee::span<const uint8_t> req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context)=0;
virtual bool invoke_notify_to_peer(int command, const epee::span<const uint8_t> req_buff, const epee::net_utils::connection_context_base& context)=0;
virtual bool drop_connection(const epee::net_utils::connection_context_base& context)=0;
@ -75,7 +75,7 @@ namespace nodetool
{
return false;
}
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core)
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, cryptonote::i_core_events& core, cryptonote::relay_method tx_relay)
{
return epee::net_utils::zone::invalid;
}

@ -1152,7 +1152,7 @@ namespace cryptonote
return true;
}
if(!tvc.m_should_be_relayed)
if(tvc.m_relay == relay_method::none)
{
LOG_PRINT_L0("[on_send_raw_tx]: tx accepted, but not relayed");
res.reason = "Not relayed";
@ -1162,8 +1162,8 @@ namespace cryptonote
}
NOTIFY_NEW_TRANSACTIONS::request r;
r.txs.push_back(tx_blob);
m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid);
r.txs.push_back(std::move(tx_blob));
m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local);
//TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes
res.status = CORE_RPC_STATUS_OK;
return true;
@ -2776,8 +2776,8 @@ namespace cryptonote
if (!m_core.get_pool_transaction(txid, txblob, relay_category::legacy))
{
NOTIFY_NEW_TRANSACTIONS::request r;
r.txs.push_back(txblob);
m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid);
r.txs.push_back(std::move(txblob));
m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local);
//TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes
}
else

@ -349,10 +349,10 @@ namespace rpc
res.error_details = "Invalid hex";
return;
}
handleTxBlob(tx_blob, req.relay, res);
handleTxBlob(std::move(tx_blob), req.relay, res);
}
void DaemonHandler::handleTxBlob(const std::string& tx_blob, bool relay, SendRawTx::Response& res)
void DaemonHandler::handleTxBlob(std::string&& tx_blob, bool relay, SendRawTx::Response& res)
{
if (!m_p2p.get_payload_object().is_synchronized())
{
@ -423,7 +423,7 @@ namespace rpc
return;
}
if(!tvc.m_should_be_relayed || !relay)
if(tvc.m_relay == relay_method::none || !relay)
{
MERROR("[SendRawTx]: tx accepted, but not relayed");
res.error_details = "Not relayed";
@ -434,8 +434,8 @@ namespace rpc
}
NOTIFY_NEW_TRANSACTIONS::request r;
r.txs.push_back(tx_blob);
m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid);
r.txs.push_back(std::move(tx_blob));
m_core.get_protocol()->relay_transactions(r, boost::uuids::nil_uuid(), epee::net_utils::zone::invalid, relay_method::local);
//TODO: make sure that tx has reached other nodes here, probably wait to receive reflections from other nodes
res.status = Message::STATUS_OK;

@ -138,7 +138,7 @@ class DaemonHandler : public RpcHandler
bool getBlockHeaderByHash(const crypto::hash& hash_in, cryptonote::rpc::BlockHeaderResponse& response);
void handleTxBlob(const std::string& tx_blob, bool relay, SendRawTx::Response& res);
void handleTxBlob(std::string&& tx_blob, bool relay, SendRawTx::Response& res);
cryptonote::core& m_core;
t_p2p& m_p2p;

@ -193,7 +193,7 @@ namespace
" account tag <tag_name> <account_index_1> [<account_index_2> ...]\n"
" account untag <account_index_1> [<account_index_2> ...]\n"
" account tag_description <tag_name> <description>");
const char* USAGE_ADDRESS("address [ new <label text with white spaces allowed> | all | <index_min> [<index_max>] | label <index> <label text with white spaces allowed> | device [<index>]]");
const char* USAGE_ADDRESS("address [ new <label text with white spaces allowed> | all | <index_min> [<index_max>] | label <index> <label text with white spaces allowed> | device [<index>] | one-off <account> <subaddress>]");
const char* USAGE_INTEGRATED_ADDRESS("integrated_address [device] [<payment_id> | <address>]");
const char* USAGE_ADDRESS_BOOK("address_book [(add (<address>|<integrated address>) [<description possibly with whitespaces>])|(delete <index>)]");
const char* USAGE_SET_VARIABLE("set <option> [<value>]");
@ -3180,7 +3180,7 @@ simple_wallet::simple_wallet()
m_cmd_binder.set_handler("address",
boost::bind(&simple_wallet::on_command, this, &simple_wallet::print_address, _1),
tr(USAGE_ADDRESS),
tr("If no arguments are specified or <index> is specified, the wallet shows the default or specified address. If \"all\" is specified, the wallet shows all the existing addresses in the currently selected account. If \"new \" is specified, the wallet creates a new address with the provided label text (which can be empty). If \"label\" is specified, the wallet sets the label of the address specified by <index> to the provided label text."));
tr("If no arguments are specified or <index> is specified, the wallet shows the default or specified address. If \"all\" is specified, the wallet shows all the existing addresses in the currently selected account. If \"new \" is specified, the wallet creates a new address with the provided label text (which can be empty). If \"label\" is specified, the wallet sets the label of the address specified by <index> to the provided label text. If \"one-off\" is specified, the address for the specified index is generated and displayed, and remembered by the wallet"));
m_cmd_binder.set_handler("integrated_address",
boost::bind(&simple_wallet::on_command, this, &simple_wallet::print_integrated_address, _1),
tr(USAGE_INTEGRATED_ADDRESS),
@ -5198,8 +5198,11 @@ void simple_wallet::check_background_mining(const epee::wipeable_string &passwor
if (is_background_mining_enabled)
{
// already active, nice
m_wallet->setup_background_mining(tools::wallet2::BackgroundMiningYes);
m_wallet->rewrite(m_wallet_file, password);
if (setup == tools::wallet2::BackgroundMiningMaybe)
{
m_wallet->setup_background_mining(tools::wallet2::BackgroundMiningYes);
m_wallet->rewrite(m_wallet_file, password);
}
start_background_mining();
return;
}
@ -5222,6 +5225,11 @@ void simple_wallet::check_background_mining(const epee::wipeable_string &passwor
m_wallet->rewrite(m_wallet_file, password);
start_background_mining();
}
else
{
// the setting is already enabled, and the daemon is not mining yet, so start it
start_background_mining();
}
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::start_mining(const std::vector<std::string>& args)
@ -9298,6 +9306,24 @@ bool simple_wallet::print_address(const std::vector<std::string> &args/* = std::
print_address_sub(m_wallet->get_num_subaddresses(m_current_subaddress_account) - 1);
m_wallet->device_show_address(m_current_subaddress_account, m_wallet->get_num_subaddresses(m_current_subaddress_account) - 1, boost::none);
}
else if (local_args[0] == "one-off")
{
local_args.erase(local_args.begin());
std::string label;
if (local_args.size() != 2)
{
fail_msg_writer() << tr("Expected exactly two arguments for index");
return true;
}
uint32_t major, minor;
if (!epee::string_tools::get_xtype_from_string(major, local_args[0]) || !epee::string_tools::get_xtype_from_string(minor, local_args[1]))
{
fail_msg_writer() << tr("failed to parse index: ") << local_args[0] << " " << local_args[1];
return true;
}
m_wallet->create_one_off_subaddress({major, minor});
success_msg_writer() << boost::format(tr("Address at %u %u: %s")) % major % minor % m_wallet->get_subaddress_as_str({major, minor});
}
else if (local_args.size() >= 2 && local_args[0] == "label")
{
if (!epee::string_tools::get_xtype_from_string(index, local_args[1]))

@ -1552,6 +1552,12 @@ void wallet2::expand_subaddresses(const cryptonote::subaddress_index& index)
}
}
//----------------------------------------------------------------------------------------------------
void wallet2::create_one_off_subaddress(const cryptonote::subaddress_index& index)
{
const crypto::public_key pkey = get_subaddress_spend_public_key(index);
m_subaddresses[pkey] = index;
}
//----------------------------------------------------------------------------------------------------
std::string wallet2::get_subaddress_label(const cryptonote::subaddress_index& index) const
{
if (index.major >= m_subaddress_labels.size() || index.minor >= m_subaddress_labels[index.major].size())
@ -2095,7 +2101,8 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
td.m_amount = amount;
td.m_pk_index = pk_index - 1;
td.m_subaddr_index = tx_scan_info[o].received->index;
expand_subaddresses(tx_scan_info[o].received->index);
if (tx_scan_info[o].received->index.major < m_subaddress_labels.size() && tx_scan_info[o].received->index.minor < m_subaddress_labels[tx_scan_info[o].received->index.major].size())
expand_subaddresses(tx_scan_info[o].received->index);
if (tx.vout[o].amount == 0)
{
td.m_mask = tx_scan_info[o].mask;
@ -2173,7 +2180,8 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
td.m_amount = amount;
td.m_pk_index = pk_index - 1;
td.m_subaddr_index = tx_scan_info[o].received->index;
expand_subaddresses(tx_scan_info[o].received->index);
if (tx_scan_info[o].received->index.major < m_subaddress_labels.size() && tx_scan_info[o].received->index.minor < m_subaddress_labels[tx_scan_info[o].received->index.major].size())
expand_subaddresses(tx_scan_info[o].received->index);
if (tx.vout[o].amount == 0)
{
td.m_mask = tx_scan_info[o].mask;
@ -12664,7 +12672,8 @@ process:
const crypto::public_key& out_key = boost::get<cryptonote::txout_to_key>(td.m_tx.vout[td.m_internal_output_index].target).key;
bool r = cryptonote::generate_key_image_helper(m_account.get_keys(), m_subaddresses, out_key, tx_pub_key, additional_tx_pub_keys, td.m_internal_output_index, in_ephemeral, td.m_key_image, m_account.get_device());
THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to generate key image");
expand_subaddresses(td.m_subaddr_index);
if (td.m_subaddr_index.major < m_subaddress_labels.size() && td.m_subaddr_index.minor < m_subaddress_labels[td.m_subaddr_index.major].size())
expand_subaddresses(td.m_subaddr_index);
td.m_key_image_known = true;
td.m_key_image_request = true;
td.m_key_image_partial = false;

@ -793,6 +793,7 @@ private:
size_t get_num_subaddresses(uint32_t index_major) const { return index_major < m_subaddress_labels.size() ? m_subaddress_labels[index_major].size() : 0; }
void add_subaddress(uint32_t index_major, const std::string& label); // throws when index is out of bound
void expand_subaddresses(const cryptonote::subaddress_index& index);
void create_one_off_subaddress(const cryptonote::subaddress_index& index);
std::string get_subaddress_label(const cryptonote::subaddress_index& index) const;
void set_subaddress_label(const cryptonote::subaddress_index &index, const std::string &label);
void set_subaddress_lookahead(size_t major, size_t minor);

@ -116,7 +116,8 @@ struct event_visitor_settings
{
set_txs_keeped_by_block = 1 << 0,
set_txs_do_not_relay = 1 << 1,
set_local_relay = 1 << 2
set_local_relay = 1 << 2,
set_txs_stem = 1 << 3
};
event_visitor_settings(int a_mask = 0)
@ -548,6 +549,10 @@ public:
{
m_tx_relay = cryptonote::relay_method::none;
}
else if (settings.mask & event_visitor_settings::set_txs_stem)
{
m_tx_relay = cryptonote::relay_method::stem;
}
else
{
m_tx_relay = cryptonote::relay_method::fluff;

@ -161,6 +161,8 @@ int main(int argc, char* argv[])
GENERATE_AND_PLAY(txpool_spend_key_all);
GENERATE_AND_PLAY(txpool_double_spend_norelay);
GENERATE_AND_PLAY(txpool_double_spend_local);
GENERATE_AND_PLAY(txpool_double_spend_keyimage);
GENERATE_AND_PLAY(txpool_stem_loop);
// Double spend
GENERATE_AND_PLAY(gen_double_spend_in_tx<false>);

@ -125,10 +125,12 @@ txpool_double_spend_base::txpool_double_spend_base()
, m_no_relay_hashes()
, m_all_hashes()
, m_no_new_index(0)
, m_failed_index(0)
, m_new_timestamp_index(0)
, m_last_tx(crypto::hash{})
{
REGISTER_CALLBACK_METHOD(txpool_double_spend_base, mark_no_new);
REGISTER_CALLBACK_METHOD(txpool_double_spend_base, mark_failed);
REGISTER_CALLBACK_METHOD(txpool_double_spend_base, mark_timestamp_change);
REGISTER_CALLBACK_METHOD(txpool_double_spend_base, timestamp_change_pause);
REGISTER_CALLBACK_METHOD(txpool_double_spend_base, check_unchanged);
@ -143,6 +145,12 @@ bool txpool_double_spend_base::mark_no_new(cryptonote::core& /*c*/, size_t ev_in
return true;
}
bool txpool_double_spend_base::mark_failed(cryptonote::core& /*c*/, size_t ev_index, const std::vector<test_event_entry>& /*events*/)
{
m_failed_index = ev_index + 1;
return true;
}
bool txpool_double_spend_base::mark_timestamp_change(cryptonote::core& /*c*/, size_t ev_index, const std::vector<test_event_entry>& /*events*/)
{
m_new_timestamp_index = ev_index + 1;
@ -483,6 +491,8 @@ bool txpool_double_spend_base::check_tx_verification_context(const cryptonote::t
m_last_tx = cryptonote::get_transaction_hash(tx);
if (m_no_new_index == event_idx)
return !tvc.m_verifivation_failed && !tx_added;
else if (m_failed_index == event_idx)
return tvc.m_verifivation_failed;// && !tx_added;
else
return !tvc.m_verifivation_failed && tx_added;
}
@ -542,7 +552,6 @@ bool txpool_double_spend_local::generate(std::vector<test_event_entry>& events)
DO_CALLBACK(events, "mark_no_new");
events.push_back(tx_0);
DO_CALLBACK(events, "check_txpool_spent_keys");
DO_CALLBACK(events, "mark_timestamp_change");
DO_CALLBACK(events, "check_unchanged");
SET_EVENT_VISITOR_SETT(events, 0);
DO_CALLBACK(events, "timestamp_change_pause");
@ -559,3 +568,73 @@ bool txpool_double_spend_local::generate(std::vector<test_event_entry>& events)
return true;
}
bool txpool_double_spend_keyimage::generate(std::vector<test_event_entry>& events) const
{
INIT_MEMPOOL_TEST();
DO_CALLBACK(events, "check_txpool_spent_keys");
SET_EVENT_VISITOR_SETT(events, event_visitor_settings::set_local_relay);
DO_CALLBACK(events, "mark_no_new");
const std::size_t tx_index1 = events.size();
MAKE_TX(events, tx_0, miner_account, bob_account, send_amount, blk_0);
SET_EVENT_VISITOR_SETT(events, event_visitor_settings::set_txs_stem);
DO_CALLBACK(events, "increase_all_tx_count");
DO_CALLBACK(events, "check_txpool_spent_keys");
DO_CALLBACK(events, "mark_timestamp_change");
DO_CALLBACK(events, "check_new_hidden");
DO_CALLBACK(events, "timestamp_change_pause");
DO_CALLBACK(events, "mark_no_new");
const std::size_t tx_index2 = events.size();
events.push_back(tx_0);
DO_CALLBACK(events, "check_txpool_spent_keys");
DO_CALLBACK(events, "mark_timestamp_change");
DO_CALLBACK(events, "check_unchanged");
// use same key image with different id
cryptonote::transaction tx_1;
{
auto events_copy = events;
events_copy.erase(events_copy.begin() + tx_index1);
events_copy.erase(events_copy.begin() + tx_index2 - 1);
MAKE_TX(events_copy, tx_temp, miner_account, bob_account, send_amount, blk_0);
tx_1 = tx_temp;
}
// same key image
DO_CALLBACK(events, "timestamp_change_pause");
DO_CALLBACK(events, "mark_failed");
events.push_back(tx_1);
DO_CALLBACK(events, "check_unchanged");
return true;
}
bool txpool_stem_loop::generate(std::vector<test_event_entry>& events) const
{
INIT_MEMPOOL_TEST();
DO_CALLBACK(events, "check_txpool_spent_keys");
SET_EVENT_VISITOR_SETT(events, event_visitor_settings::set_txs_stem);
DO_CALLBACK(events, "mark_no_new");
MAKE_TX(events, tx_0, miner_account, bob_account, send_amount, blk_0);
DO_CALLBACK(events, "increase_all_tx_count");
DO_CALLBACK(events, "check_txpool_spent_keys");
DO_CALLBACK(events, "mark_timestamp_change");
DO_CALLBACK(events, "check_new_hidden");
DO_CALLBACK(events, "timestamp_change_pause");
events.push_back(tx_0);
DO_CALLBACK(events, "increase_broadcasted_tx_count");
DO_CALLBACK(events, "check_txpool_spent_keys");
DO_CALLBACK(events, "mark_timestamp_change");
DO_CALLBACK(events, "check_new_broadcasted");
DO_CALLBACK(events, "timestamp_change_pause");
DO_CALLBACK(events, "mark_no_new");
events.push_back(tx_0);
DO_CALLBACK(events, "check_unchanged");
return true;
}

@ -77,6 +77,7 @@ class txpool_double_spend_base : public txpool_base
std::unordered_set<crypto::hash> m_no_relay_hashes;
std::unordered_map<crypto::hash, uint64_t> m_all_hashes;
size_t m_no_new_index;
size_t m_failed_index;
size_t m_new_timestamp_index;
crypto::hash m_last_tx;
@ -86,6 +87,7 @@ public:
txpool_double_spend_base();
bool mark_no_new(cryptonote::core& c, size_t ev_index, const std::vector<test_event_entry>& events);
bool mark_failed(cryptonote::core& c, size_t ev_index, const std::vector<test_event_entry>& events);
bool mark_timestamp_change(cryptonote::core& c, size_t ev_index, const std::vector<test_event_entry>& events);
//! Pause for 1 second, so that `receive_time` for tx meta changes (tx hidden from public rpc being updated)
@ -116,3 +118,21 @@ struct txpool_double_spend_local : txpool_double_spend_base
bool generate(std::vector<test_event_entry>& events) const;
};
struct txpool_double_spend_keyimage : txpool_double_spend_base
{
txpool_double_spend_keyimage()
: txpool_double_spend_base()
{}
bool generate(std::vector<test_event_entry>& events) const;
};
struct txpool_stem_loop : txpool_double_spend_base
{
txpool_stem_loop()
: txpool_double_spend_base()
{}
bool generate(std::vector<test_event_entry>& events) const;
};

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save