Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RELEASE] Several mitigations for attacking privacy via key reusing forks #3322

Merged
merged 15 commits into from Mar 16, 2018

Conversation

@moneromooo-monero
Copy link
Collaborator

@moneromooo-monero moneromooo-monero commented Feb 28, 2018

No description provided.

@moneromooo-monero
Copy link
Collaborator Author

@moneromooo-monero moneromooo-monero commented Feb 28, 2018

I've just thought there should be a "set_ring" command, in case the forkers don't merge this. I'll do this momentarily.

@SamsungGalaxyPlayer
Copy link

@SamsungGalaxyPlayer SamsungGalaxyPlayer commented Feb 28, 2018

I can't speak for the code itself, but I support the general approach. It boils down to three things:

  1. When spending on a fork, use the same exact ring member set.

  2. If spending on the fork and/or main chain, consider using new outputs created after the split. These outputs will not be compromised by the key image attack, since they cannot be spent on both chains. Wallet software can select exclusively from this set at a cost of standing out, or it can select one to make sure there will be at least some plausible deniability while likely not standing out significantly.

  3. Blacklist known bad outputs compromised through key image reuse and 0-mixin (effects for the latter have largely faded). These are known to be decoys.

Of course, a disclaimer that if a large proportion of transactions (for ringsize 5 >33%; ringsize 7 >50%) on the forked chain don't take advantage of 1, then there is a relatively likely chance that the real input can be revealed.

For ringsize 5, suppose 50% of initial transactions on a fork use this tool. Then, approx. 6.25% of other transactions will be compromised (total 53%, since 50%*6.25%) on the fork chain. Suppose half of the transactions on Monero are related to the fork, then the proportion of compromised inputs on the Monero chain falls to approx. 53/2 = 27%.

These are terribly rough estimates, but even small proportions of people using this tool help the forked chain to a small extent and Monero to a larger extent.

Copy link
Contributor

@stoffu stoffu left a comment

Looks good, although my confidence is particularly low regarding the get_outs function which is quite long.

/**
* @brief gets per block distribution of rct outputs
*
* @return amount the amount to get a ditribution for

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

@param


distribution.clear();
uint64_t db_height = m_db->height();
distribution.resize(db_height - start_height);

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

Initialize with zero: distribution.resize(db_height - start_height, 0);

uint32_t rpc_version;
boost::optional<std::string> result = m_node_rpc_proxy.get_rpc_version(rpc_version);
// no error
if (!!result)

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

I find the design of NodeRPCProxy a bit strange, because all the query methods always return boost::optional<std::string>() which equals to boost::none. So this section is always no-op.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 1, 2018
Author Collaborator

It returns an optional error string.

This comment has been minimized.

@stoffu

stoffu Mar 1, 2018
Contributor

I'd also expect so, but all the return statement I see in node_rpc_proxy.cpp is always return boost::optional<std::string>();. Or am I missing places where such an optional error string gets returned?

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 1, 2018
Author Collaborator

Various calls to CHECK_AND_ASSERT_MES, which typically return resp_t.result.status.

This comment has been minimized.

@stoffu

stoffu Mar 1, 2018
Contributor

Oops, sorry I was completely overlooking them.

req.params.amounts.push_back(0);
m_daemon_rpc_mutex.lock();
MDEBUG("calling get_output_distribution");
bool r = net_utils::invoke_http_json("/json_rpc", req, res, m_http_client, rpc_timeout);

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

invoke_http_json_rpc can be used instead to eliminate boilerplate lines like req.jsonrpc = "2.0";.

@@ -36,6 +36,7 @@
#include <vector>

#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_core/cryptonote_tx_utils.h"

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

This should be in ringdb.cpp instead.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 1, 2018
Author Collaborator

wallet_errors.h uses cryptonote::tx_source_entry in tx_not_constructed

This comment has been minimized.

@stoffu

stoffu Mar 1, 2018
Contributor

Ah, right. The code has been compiling fine merely because of the specific order wallet_errors.h was included, I suppose.

m_cmd_binder.set_handler("blackball",
boost::bind(&simple_wallet::blackball, this, _1),
tr("blackball <output public key> | <filename> [<add>]"),
tr("Blackball an output so it never gets selected as a fake output in a ring"));

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

an output (or a set of outputs in a file)


po::options_description desc_cmd_only("Command line options");
po::options_description desc_cmd_sett("Command line options and settings options");
const command_line::arg_descriptor<std::string> arg_blackball_db_dir = {"blackball-db-dir", "Specify blackball database directory",

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

Perhaps this could use the dependent argument as in #3170?

continue;

const std::vector<uint64_t> absolute = cryptonote::relative_output_offsets_to_absolute(txin.key_offsets);
if (n == 0)

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

Why only for the n==0 case?

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 1, 2018
Author Collaborator

After a fork, there'll be (amount,index) duplicates. You want to use the main db only.

This comment has been minimized.

@stoffu

stoffu Mar 1, 2018
Contributor

OK. It wasn't clear in the code that n==0 is supposed be the main db. I think some more words should be added to the arg description.

{
m_ring_database = filename;
MGINFO("ringdb path set to " << filename);
delete m_ringdb;

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

Check if not null?

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 1, 2018
Author Collaborator

delete does so.

This comment has been minimized.

@stoffu

stoffu Mar 1, 2018
Contributor

Ouch, embarrassing :)

}
if (relative)
{
if (!ring.back())

This comment has been minimized.

@stoffu

stoffu Feb 28, 2018
Contributor

Doesn’t this exclude the case with the first index being 0?

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 1, 2018
Author Collaborator

Yes, I removed some "useless code", was not useless after all :/

@stoffu
Copy link
Contributor

@stoffu stoffu commented Feb 28, 2018

Some of my review comments got hidden due to push -f, but they're still unaddressed. Please take a look.

@moneromooo-monero moneromooo-monero force-pushed the moneromooo-monero:detfake-master branch 4 times, most recently from 2eaeb4f to 7985728 Mar 1, 2018
bool remove_rings(const cryptonote::transaction_prefix &tx);
bool get_ring(const crypto::chacha_key &key, const crypto::key_image &key_image, std::vector<uint64_t> &outs);

bool get_output_distribution(uint64_t &start_height, std::vector<uint64_t> &offsets);

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

  • I don't see this function being used anywhere. What's the intent?
  • Why is the second argument named offsets instead of distribution?

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

It's used by my patch to use a gamma distribution for fake out selection, which hasn't been PR'd. Since I picked this code from this patch, I might as well bring in the function that uses it too.

}
else
{
if (ring.size() > 1 && ring[ring.size() - 2] >= ring[ring.size() - 1])

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

Shouldn't this test be performed for every index?

if (ring.size() > 1)
{
  for (size_t i = 1; i < ring.size(); ++i)
  {
    if (ring[i - 1] >= ring[i])
    {
      ...failure...
      return true;
    }
  }
}

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

It's done for every index, there's a loop above.

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

Sorry, I misread.

}
fclose(f);
bool add = false;
if (args.size() > 1 && args[1] != "add")

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

This logic seems incorrect, where add never becomes true. My suggestion:

if (args.size() > 1)
{
  if (args[1] != "add)
  {
    ...failure...
    return true;
  }
  add = true;
}

MDEBUG("Removing ring data for key image " << txin.k_image);
dbr = mdb_del(txn, dbi_rings, &key, NULL);
THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to add ring to database: " + std::string(mdb_strerror(dbr)));

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

"Failed to remove ring from database: "

bool clear_blackballs();

private:
bool blackball_worker(const crypto::public_key &output, int op);

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

Looks like further refactoring is possible using exactly the same strategy; i.e. by introducing these internal worker functions:

bool rings_worker(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx, int op);
bool ring_worker(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, std::vector<uint64_t> &outs, bool relative, int op);

For the second one, the parameter outs would act as input/output when op is SET/GET. Also, I think it makes sense to have the function get_ring accept the parameter relative.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

Yes, but I did those before the blackball one, and when I started doing this it created lots of conflicts.

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

What kind of conflicts? Can't you just add some switches to the worker function and direct the execution flow based on the op code?

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

Here's a patch that I came up with:

diff --git a/src/wallet/ringdb.cpp b/src/wallet/ringdb.cpp
index 522a7ca7..6b22292e 100644
--- a/src/wallet/ringdb.cpp
+++ b/src/wallet/ringdb.cpp
@@ -170,6 +170,8 @@ static size_t get_ring_data_size(size_t n_entries)
   return n_entries * (32 + 1024); // highball 1kB for the ring data to make sure
 }
 
+enum { RINGS_ADD, RINGS_REMOVE};
+enum { RING_GET, RING_SET};
 enum { BLACKBALL_BLACKBALL, BLACKBALL_UNBLACKBALL, BLACKBALL_QUERY, BLACKBALL_CLEAR};
 
 namespace tools
@@ -218,13 +220,24 @@ ringdb::~ringdb()
   mdb_env_close(env);
 }
 
-bool ringdb::add_rings(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx)
+bool ringdb::rings_worker(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx, int op)
 {
   MDB_txn *txn;
   int dbr;
   bool tx_active = false;
 
-  dbr = resize_env(env, filename.c_str(), get_ring_data_size(tx.vin.size()));
+  if (op == RINGS_ADD)
+  {
+    dbr = resize_env(env, filename.c_str(), get_ring_data_size(tx.vin.size()));
+  }
+  else if (op == RINGS_REMOVE)
+  {
+    dbr = resize_env(env, filename.c_str(), 0);
+  }
+  else
+  {
+    THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, "Invalid rings op");
+  }
   THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to set env map size");
   dbr = mdb_txn_begin(env, NULL, 0, &txn);
   THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr)));
@@ -244,130 +257,119 @@ bool ringdb::add_rings(const crypto::chacha_key &chacha_key, const cryptonote::t
     std::string key_ciphertext = encrypt(txin.k_image, chacha_key);
     key.mv_data = (void*)key_ciphertext.data();
     key.mv_size = key_ciphertext.size();
-    MDEBUG("Saving relative ring for key image " << txin.k_image << ": " <<
-        boost::join(txin.key_offsets | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " "));
-    std::string compressed_ring = compress_ring(txin.key_offsets);
-    std::string data_ciphertext = encrypt(compressed_ring, txin.k_image, chacha_key);
-    data.mv_size = data_ciphertext.size();
-    data.mv_data = (void*)data_ciphertext.c_str();
-    dbr = mdb_put(txn, dbi_rings, &key, &data, 0);
-    THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to add ring to database: " + std::string(mdb_strerror(dbr)));
+
+    if (op == RINGS_ADD)
+    {
+      MDEBUG("Saving relative ring for key image " << txin.k_image << ": " <<
+          boost::join(txin.key_offsets | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " "));
+      std::string compressed_ring = compress_ring(txin.key_offsets);
+      std::string data_ciphertext = encrypt(compressed_ring, txin.k_image, chacha_key);
+      data.mv_size = data_ciphertext.size();
+      data.mv_data = (void*)data_ciphertext.c_str();
+      dbr = mdb_put(txn, dbi_rings, &key, &data, 0);
+    }
+    else
+    {
+      dbr = mdb_get(txn, dbi_rings, &key, &data);
+      THROW_WALLET_EXCEPTION_IF(dbr && dbr != MDB_NOTFOUND, tools::error::wallet_internal_error, "Failed to look for key image in LMDB table: " + std::string(mdb_strerror(dbr)));
+      if (dbr == MDB_NOTFOUND)
+        continue;
+      THROW_WALLET_EXCEPTION_IF(data.mv_size <= 0, tools::error::wallet_internal_error, "Invalid ring data size");
+
+      MDEBUG("Removing ring data for key image " << txin.k_image);
+      dbr = mdb_del(txn, dbi_rings, &key, NULL);
+    }
+    THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, std::string("Failed to ") + (op == RINGS_ADD ? "add ring to" : "remove ring from") + " database: " + std::string(mdb_strerror(dbr)));
   }
 
   dbr = mdb_txn_commit(txn);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to commit txn adding ring to database: " + std::string(mdb_strerror(dbr)));
+  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, std::string("Failed to commit txn ") + (op == RING_GET ? "adding rings to" : "removing rings from") + " database: " + std::string(mdb_strerror(dbr)));
   tx_active = false;
   return true;
 }
 
+bool ringdb::add_rings(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx)
+{
+  return rings_worker(chacha_key, tx, RINGS_ADD);
+}
+
 bool ringdb::remove_rings(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx)
+{
+  return rings_worker(chacha_key, tx, RINGS_REMOVE);
+}
+
+bool ringdb::ring_worker(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, std::vector<uint64_t> &outs, bool relative, int op)
 {
   MDB_txn *txn;
   int dbr;
   bool tx_active = false;
 
-  dbr = resize_env(env, filename.c_str(), 0);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to set env map size");
+  if (op == RING_GET)
+  {
+    dbr = resize_env(env, filename.c_str(), 0);
+  }
+  else if (op == RING_SET)
+  {
+    dbr = resize_env(env, filename.c_str(), outs.size() * 64);
+  }
+  else
+  {
+    THROW_WALLET_EXCEPTION(tools::error::wallet_internal_error, "Invalid ring op");
+  }
+  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to set env map size: " + std::string(mdb_strerror(dbr)));
   dbr = mdb_txn_begin(env, NULL, 0, &txn);
   THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr)));
   epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);});
   tx_active = true;
 
-  for (const auto &in: tx.vin)
+  MDB_val key, data;
+  std::string key_ciphertext = encrypt(key_image, chacha_key);
+  key.mv_data = (void*)key_ciphertext.data();
+  key.mv_size = key_ciphertext.size();
+  if (op == RING_GET)
   {
-    if (in.type() != typeid(cryptonote::txin_to_key))
-      continue;
-    const auto &txin = boost::get<cryptonote::txin_to_key>(in);
-    const uint32_t ring_size = txin.key_offsets.size();
-    if (ring_size == 1)
-      continue;
-
-    MDB_val key, data;
-    std::string key_ciphertext = encrypt(txin.k_image, chacha_key);
-    key.mv_data = (void*)key_ciphertext.data();
-    key.mv_size = key_ciphertext.size();
-
     dbr = mdb_get(txn, dbi_rings, &key, &data);
     THROW_WALLET_EXCEPTION_IF(dbr && dbr != MDB_NOTFOUND, tools::error::wallet_internal_error, "Failed to look for key image in LMDB table: " + std::string(mdb_strerror(dbr)));
     if (dbr == MDB_NOTFOUND)
-      continue;
+      return false;
     THROW_WALLET_EXCEPTION_IF(data.mv_size <= 0, tools::error::wallet_internal_error, "Invalid ring data size");
 
-    MDEBUG("Removing ring data for key image " << txin.k_image);
-    dbr = mdb_del(txn, dbi_rings, &key, NULL);
-    THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to add ring to database: " + std::string(mdb_strerror(dbr)));
+    std::string data_plaintext = decrypt(std::string((const char*)data.mv_data, data.mv_size), key_image, chacha_key);
+    outs = decompress_ring(data_plaintext);
+    MDEBUG("Found ring for key image " << key_image << ":");
+    MDEBUG("Relative: " << boost::join(outs | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " "));
+    if (!relative)
+    {
+      outs = cryptonote::relative_output_offsets_to_absolute(outs);
+      MDEBUG("Absolute: " << boost::join(outs | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " "));
+    }
+
+  }
+  else
+  {
+    std::string compressed_ring = compress_ring(relative ? outs : cryptonote::absolute_output_offsets_to_relative(outs));
+    std::string data_ciphertext = encrypt(compressed_ring, key_image, chacha_key);
+    data.mv_size = data_ciphertext.size();
+    data.mv_data = (void*)data_ciphertext.c_str();
+    dbr = mdb_put(txn, dbi_rings, &key, &data, 0);
+    THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to set ring for key image in LMDB table: " + std::string(mdb_strerror(dbr)));
   }
 
   dbr = mdb_txn_commit(txn);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to commit txn adding ring to database: " + std::string(mdb_strerror(dbr)));
+  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, std::string("Failed to commit txn ") + (op == RING_GET ? "getting ring from" : "setting ring to") + " database: " + std::string(mdb_strerror(dbr)));
   tx_active = false;
   return true;
 }
 
-bool ringdb::get_ring(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, std::vector<uint64_t> &outs)
+bool ringdb::get_ring(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, std::vector<uint64_t> &outs, bool relative)
 {
-  MDB_txn *txn;
-  int dbr;
-  bool tx_active = false;
-
-  dbr = resize_env(env, filename.c_str(), 0);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to set env map size: " + std::string(mdb_strerror(dbr)));
-  dbr = mdb_txn_begin(env, NULL, 0, &txn);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr)));
-  epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);});
-  tx_active = true;
-
-  MDB_val key, data;
-  std::string key_ciphertext = encrypt(key_image, chacha_key);
-  key.mv_data = (void*)key_ciphertext.data();
-  key.mv_size = key_ciphertext.size();
-  dbr = mdb_get(txn, dbi_rings, &key, &data);
-  THROW_WALLET_EXCEPTION_IF(dbr && dbr != MDB_NOTFOUND, tools::error::wallet_internal_error, "Failed to look for key image in LMDB table: " + std::string(mdb_strerror(dbr)));
-  if (dbr == MDB_NOTFOUND)
-    return false;
-  THROW_WALLET_EXCEPTION_IF(data.mv_size <= 0, tools::error::wallet_internal_error, "Invalid ring data size");
-
-  std::string data_plaintext = decrypt(std::string((const char*)data.mv_data, data.mv_size), key_image, chacha_key);
-  outs = decompress_ring(data_plaintext);
-  MDEBUG("Found ring for key image " << key_image << ":");
-  MDEBUG("Relative: " << boost::join(outs | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " "));
-  outs = cryptonote::relative_output_offsets_to_absolute(outs);
-  MDEBUG("Absolute: " << boost::join(outs | boost::adaptors::transformed([](uint64_t out){return std::to_string(out);}), " "));
-
-  dbr = mdb_txn_commit(txn);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to commit txn getting ring from database: " + std::string(mdb_strerror(dbr)));
-  tx_active = false;
-  return true;
+  return ring_worker(chacha_key, key_image, outs, relative, RING_GET);
 }
 
 bool ringdb::set_ring(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, const std::vector<uint64_t> &outs, bool relative)
 {
-  MDB_txn *txn;
-  int dbr;
-  bool tx_active = false;
-
-  dbr = resize_env(env, filename.c_str(), outs.size() * 64);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to set env map size: " + std::string(mdb_strerror(dbr)));
-  dbr = mdb_txn_begin(env, NULL, 0, &txn);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to create LMDB transaction: " + std::string(mdb_strerror(dbr)));
-  epee::misc_utils::auto_scope_leave_caller txn_dtor = epee::misc_utils::create_scope_leave_handler([&](){if (tx_active) mdb_txn_abort(txn);});
-  tx_active = true;
-
-  MDB_val key, data;
-  std::string key_ciphertext = encrypt(key_image, chacha_key);
-  key.mv_data = (void*)key_ciphertext.data();
-  key.mv_size = key_ciphertext.size();
-  std::string compressed_ring = compress_ring(relative ? outs : cryptonote::absolute_output_offsets_to_relative(outs));
-  std::string data_ciphertext = encrypt(compressed_ring, key_image, chacha_key);
-  data.mv_size = data_ciphertext.size();
-  data.mv_data = (void*)data_ciphertext.c_str();
-  dbr = mdb_put(txn, dbi_rings, &key, &data, 0);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to set ring for key image in LMDB table: " + std::string(mdb_strerror(dbr)));
-
-  dbr = mdb_txn_commit(txn);
-  THROW_WALLET_EXCEPTION_IF(dbr, tools::error::wallet_internal_error, "Failed to commit txn setting ring to database: " + std::string(mdb_strerror(dbr)));
-  tx_active = false;
-  return true;
+  std::vector<uint64_t> outs_copy = outs;
+  return ring_worker(chacha_key, key_image, outs_copy, relative, RING_SET);
 }
 
 bool ringdb::blackball_worker(const crypto::public_key &output, int op)
diff --git a/src/wallet/ringdb.h b/src/wallet/ringdb.h
index 5cc41958..7d17421f 100644
--- a/src/wallet/ringdb.h
+++ b/src/wallet/ringdb.h
@@ -45,7 +45,7 @@ namespace tools
 
     bool add_rings(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx);
     bool remove_rings(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx);
-    bool get_ring(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, std::vector<uint64_t> &outs);
+    bool get_ring(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, std::vector<uint64_t> &outs, bool relative = false);
     bool set_ring(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, const std::vector<uint64_t> &outs, bool relative);
 
     bool blackball(const crypto::public_key &output);
@@ -54,6 +54,8 @@ namespace tools
     bool clear_blackballs();
 
   private:
+    bool rings_worker(const crypto::chacha_key &chacha_key, const cryptonote::transaction_prefix &tx, int op);
+    bool ring_worker(const crypto::chacha_key &chacha_key, const crypto::key_image &key_image, std::vector<uint64_t> &outs, bool relative, int op);
     bool blackball_worker(const crypto::public_key &output, int op);
 
   private:

A few duplicate lines of code in {add,remove}_rings and {get,set}_ring are eliminated.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

I'm not sure what you are asking by "kind of conflict". Git conflicts. Did you try squashing this in the relevant commits ? You'll see the conflicts you get.

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

I see. Here's my attempt in reordering and squashing commits: https://github.com/stoffu/monero/commits/pr-3322-rebase

Notable changes I had to make are:

  • squash the commit turning ringdb from namespace-based to object-based
  • squash the last commit introducing the set_ring command into the first commit introducing ringdb
  • separate the parameter name change in wallet2::tx_add_fake_output

Do you think this is reasonable?

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

Looks reasonable by the commit messages, but (1) I'm not PRing something I did not write unless I can ensure there's nothing fishy in it and (2) I didn't want to spend time on this before, so I'm not spending time on it now either. Maybe once I have time if this is not merged already.

*
* @param amount the amount to get a ditribution for
* @param start_height the height of the first rct output
* @param distribution the start offset of the first rct output in this block (same as previous if none)

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

return-by-reference to the above two params, just like other member functions

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

Well, it's blindingly obvious, no ? But OK.

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

It's mostly for keeping the consistency in this header file. If existing declarations are given verbose comments, so should be the new ones, IMO.

auto it = outputs.find(od);
if (it == outputs.end())
{
outputs[output_data(out.amount, indices[out.amount], coinbase, height)];

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

Perhaps outputs[od] = {}; can make the intent a bit clearer?

}
else
{
it->first.info(coinbase, height);

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

I'm not sure how/why this code path can get executed, because indices for any amount gets incremented every time od is added to outputs. In other words, there will never be two output_data with the same amount and index during the loop.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

Transactions are not processed in blockchain order so you can get an output used before it's created.

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

Ah, didn't know that.

{
if (std::find(r1.begin(), r1.end(), out) != r1.end())
common.push_back(out);
}

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

How about using std::set_intersection?

std::set_intersection(r1.begin(), r1.end(), r2.begin(), r2.end(), std::back_inserter(common));

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

Mostly because I did not know that existed. At least I know what the current code does. The "equivalent code" in the docs isn't immediately obviously the same as mine, so I'm keeping mine.

}
else
{
MINFO("The intersection has more than one element, it's still ok");

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

If r0 and r1 are of different lengths, ring members that aren't common in both get identified as fakes. To make use of this information, relative_rings[txin.k_image] should be set to the smallest ring among all that share the same key image.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

Very good observation, thanks.

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

should be set to the smallest ring among all that share the same key image.

Actually, not just the smallest among all the relevant rings, but the set intersection of all of them which can be smaller than any one of them.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

That's what I've done, yes.

THROW_WALLET_EXCEPTION_IF(*result == CORE_RPC_STATUS_BUSY, tools::error::daemon_busy, "getversion");
if (*result != CORE_RPC_STATUS_OK)
{
MDEBUG("Cannot determined daemon RPC version, not requesting rct distribution");

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

"determined" -> "determine"

To prevent "fix typo" commits :)

m_cmd_binder.set_handler("blackballed",
boost::bind(&simple_wallet::blackballed, this, _1),
tr("blackballed <output public key>"),
tr("Checks whether an output is blackballed"));

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

"Checks" -> "Check"

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 2, 2018
Author Collaborator

That's correct too.

This comment has been minimized.

@stoffu

stoffu Mar 2, 2018
Contributor

The same reason as with the unblackball, just for the linguistic consistency.

@stoffu
stoffu approved these changes Mar 3, 2018
Copy link
Contributor

@stoffu stoffu left a comment

Confirmed the bug was fixed. Thanks again for developing this great tool!

num_outs = segregation_limit[amount].first;
else for (const auto &he: resp_t.result.histogram)
if (he.amount == amount)
num_outs = he.unlocked_instances;

This comment has been minimized.

@stoffu

stoffu Mar 3, 2018
Contributor

Maybe break once found?

@moneromooo-monero moneromooo-monero force-pushed the moneromooo-monero:detfake-master branch from 6fe5be8 to 2b02c65 Mar 3, 2018
@moneromooo-monero moneromooo-monero changed the title Several mitigations for attacking privacy via key reusing forks [RELEASE] Several mitigations for attacking privacy via key reusing forks Mar 4, 2018
Copy link
Contributor

@vtnerd vtnerd left a comment

Will finish later. The comments on line 358-364 in blockchain_blackball.cpp are worth pushing this review sonner than later ...

if (amount == 0)
start_height = m_testnet ? testnet_hard_forks[2].height : mainnet_hard_forks[2].height;
else
start_height = 0;

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

The comment indicates this is for ringct, so the comment needs to be updated or this need to be a return false. Also, this would require a ~11-12MiB vector currently. Should the height be user provided/selected? The callee already needs to know which heights are unique and important for the algorithm that is using this API to work.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 6, 2018
Author Collaborator

It used to be rct only when first written, I'll change the comment. Good idea about the user being able to clamp.

if (!r)
return false;
for (size_t n = 1; n < distribution.size(); ++n)
distribution[n] += distribution[n-1];

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

Why have this logic here? Can't the callee do this if desired? Also, since these values are being serialized via varint code, so this increasing the transmission size.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 6, 2018
Author Collaborator

Because that is what I needed. Admittedly not a very complicated job to leave on the caller though.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 6, 2018
Author Collaborator

I made it optional (cumulative bool) in the request.

LOG_PRINT_L0("No inputs given");
return 1;
}
std::vector<Blockchain*> core_storage(inputs.size(), NULL);

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

std::vector<std::unique_ptr<Blockchain>>


int main(int argc, char* argv[])
{
TRY_ENTRY();

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

Same here.

// tx_memory_pool, Blockchain's constructor takes tx_memory_pool object.
LOG_PRINT_L0("Initializing source blockchain (BlockchainDB)");
const std::string input = command_line::get_arg(vm, arg_input);
Blockchain *core_storage = NULL;

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

std::unique_ptr

MINFO("The intersection has more than one element, it's still ok");
std::vector<uint64_t> new_ring;
for (const auto &out: relative_rings[txin.k_image])
if (std::find(common.begin(), common.end(), out) != common.end())

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

common contains absolute offsets, whereas relative_rings[txin.k_image] contains relative offsets. This find is unlikely to return any useful information.

for (const auto &out: relative_rings[txin.k_image])
if (std::find(common.begin(), common.end(), out) != common.end())
new_ring.push_back(out);
relative_rings[txin.k_image] = new_ring;

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

This write to relative_rings[txin.k_image] is always overwritten on line 364 without any reads in-between. Presumably this is supposed to update relative_rings[txin.k_image] so that it only contains the common elements in both rings. If that is the goal, then relative_offsets must store absolute offsets instead of relative offsets, because the expansion algorithm will not work correctly (unless you re-packed from absolute to relative right here). There is no advantage in doing relative offsets though - that optimization is for reducing the wire/stored db size.


size_t done = 0;
std::unordered_map<crypto::key_image, std::vector<uint64_t>> relative_rings;
std::map<output_data, std::unordered_set<crypto::key_image>> outputs;

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

These should most likely be hash maps. They will have many elements, and are called frequently enough to notice. boost::hash<> has built-in functionality for std::pair<>, and so does boost::unorderded_set<>. so the key could be std::pair<uint64_t, uint64_t> to save on typing (if you didn't want to use boost::hash<> with std::unordered_set due to extra long typename). The interfaces are identical.

mutable uint64_t height;
output_data(uint64_t a, uint64_t i, bool cb, uint64_t h): amount(a), index(i), coinbase(cb), height(h) {}
bool operator<(const output_data &other) const
{ if (amount < other.amount) return true; if (amount == other.amount && index < other.index) return true; return false; }

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

This could also be return std::make_pair(amount, index) < std::make_pair(other.amount, other.index);.

while (!newly_spent.empty())
{
LOG_PRINT_L0("Secondary pass due to " << newly_spent.size() << " newly found spent outputs");
std::set<output_data> work_spent = newly_spent;

This comment has been minimized.

@vtnerd

vtnerd Mar 6, 2018
Contributor

work_spend = std::move(newly_spent) for a quick/easy perfornance gain.

@moneromooo-monero moneromooo-monero force-pushed the moneromooo-monero:detfake-master branch 4 times, most recently from f45e326 to ef10a92 Mar 6, 2018
for (size_t n = 1; n < distribution.size(); ++n)
distribution[n] += distribution[n-1];
}
res.distributions.push_back({amount, start_height, distribution, base});

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

std::move(distribution)

{
std::stringstream str;
for (const auto &x: ring)
str << std::to_string(x) << " ";

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

Just do str << x << " ";. Currently a temporary std::string is allocated which is a complete waste.

{
if (ring.size() > 1 && ring[ring.size() - 2] >= ring[ring.size() - 1])
{
fail_msg_writer() << tr("invalid index: indices should be in strictly ascending order");

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

Why is this restriction necessary? Couldn't a sort be done after the loop? It's like not the user would complain, "oh but input[0][3] isn't output 100 !". At least I wouldn't be upset, because the important thing is that the outputs I wanted were used as dummy inputs.

One complication is the reuse of outputs, which should be disallowed because it was unlikely to be intentional. The current check is >= which allows re-use. This can still easily be detected after the loop and after the std::sort with std::unique. The relative branch catches this by checking for zero.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 8, 2018
Author Collaborator

It'll catch people using relative rings and claiming they're absolute. All places I know of show absolute rings sorted, so it's not prone to rejecting a lot of stuff.

return true;
}
uint64_t sum = 0;
for (uint64_t out: ring)

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

If its any easier this is:

const uint64_t sum = std::accumulate(ring.begin(), ring.end() - 1, 0);
if (ring.back() > std::numeric_limits<uint64_t>::max() - sum)
{
  ...
}

The accumulation cannot overflow, because the previous iteration would've trapped it.

std::list<crypto::public_key> outputs;
char str[65];

FILE *f = fopen(args[0].c_str(), "r");

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

This file could be left open if something in here throws (and resource leaked if caught and process continued). Not a huge risk of that, but it can easily be handled by an existing functor:

std::unique_ptr<FILE, tools::close_file> f{fopen(args[0].c_str(), "r")};

The calls below would have to use f.get(), and the fclose would have to be removed, but nothing else.

distribution.clear();
uint64_t db_height = m_db->height();
distribution.resize(db_height - start_height, 0);
bool r = for_all_outputs(amount, [&](uint64_t height) {

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

I didn't comment on this before, but isn't this call ... costly? Yikes, the DB call could benefit from only have to iterate through a subset of values too.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 8, 2018
Author Collaborator

It is fairly costly.

bool found = false;
for (const auto &d: resp_t.distributions)
{
if (d.amount == amount)

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

Might've been easier to do a sort + binary_search for this. It would've made the error case easier to detect anyway.

@@ -5585,6 +5986,20 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>>
outs.back().reserve(fake_outputs_count + 1);
const rct::key mask = td.is_rct() ? rct::commit(td.amount(), td.m_mask) : rct::zeroCommit(td.amount());

uint64_t num_outs = 0;

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

This is somewhat of a nitpck, but this variable name is a little confusing. Its not really the number of outs, its a limit on the index number that can be used ... this makes the code below seem "wrong" at first glance.

{
// if there are just enough outputs to mix with, use all of them.
// Eventually this should become impossible.
for (const auto &he: resp_t.result.histogram)

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

Looks like a std::find on the amount field.

num_outs = segregation_limit[amount].first;
else for (const auto &he: resp_t.result.histogram)
{
if (he.amount == amount)

This comment has been minimized.

@vtnerd

vtnerd Mar 8, 2018
Contributor

std::find ?

@stoffu
Copy link
Contributor

@stoffu stoffu commented Mar 8, 2018

I think the following is needed in order for the GUI version to work:

diff --git a/external/db_drivers/liblmdb/CMakeLists.txt b/external/db_drivers/liblmdb/CMakeLists.txt
index cb2501ee..2e8822f5 100644
--- a/external/db_drivers/liblmdb/CMakeLists.txt
+++ b/external/db_drivers/liblmdb/CMakeLists.txt
@@ -50,4 +50,16 @@ if(${ARCH_WIDTH} EQUAL 32)
   target_compile_definitions(lmdb
     PUBLIC -DMDB_VL32)
 endif()
+
+# GUI/libwallet install target
+if (BUILD_GUI_DEPS)
+    if(IOS)
+        set(lib_folder lib-${ARCH})
+    else()
+        set(lib_folder lib)
+    endif()
+    install(TARGETS lmdb
+        ARCHIVE DESTINATION ${lib_folder}
+        LIBRARY DESTINATION ${lib_folder})
+endif()
 set_property(TARGET lmdb APPEND PROPERTY COMPILE_FLAGS "-fPIC")
@moneromooo-monero
Copy link
Collaborator Author

@moneromooo-monero moneromooo-monero commented Mar 8, 2018

stoffu, do you want to PR that ?

@moneromooo-monero moneromooo-monero force-pushed the moneromooo-monero:detfake-master branch from ef10a92 to 04494f8 Mar 8, 2018
@moneromooo-monero
Copy link
Collaborator Author

@moneromooo-monero moneromooo-monero commented Mar 8, 2018

I left out all the std::find stuff, that'd just churn. There might be comments I've not seen, github has a "show 20 more things" link, but when clicked it doesn't show more...

@stoffu
Copy link
Contributor

@stoffu stoffu commented Mar 8, 2018

@moneromooo-monero

stoffu, do you want to PR that ?

I don't bother, no. Please just include it here.

@moneromooo-monero moneromooo-monero force-pushed the moneromooo-monero:detfake-master branch from 04494f8 to ff89bce Mar 8, 2018
@moneromooo-monero
Copy link
Collaborator Author

@moneromooo-monero moneromooo-monero commented Mar 8, 2018

done

@moneromooo-monero moneromooo-monero force-pushed the moneromooo-monero:detfake-master branch 3 times, most recently from 5810974 to bfc4e57 Mar 14, 2018
uint64_t height;
boost::optional<std::string> result = m_node_rpc_proxy.get_height(height);
throw_on_rpc_response_error(result, "get_info");
bool is_shortly_after_segregation_fork = height >= segregation_fork_height && height + SEGREGATION_FORK_VICINITY < segregation_fork_height;

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

This seems to always evaluate to false. Didn't you intend the following instead?

height >= segregation_fork_height && height < segregation_fork_height + SEGREGATION_FORK_VICINITY

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 15, 2018
Author Collaborator

Yes, thanks.

@@ -5688,6 +5720,51 @@ void wallet2::get_outs(std::vector<std::vector<tools::wallet2::get_outs_entry>>
THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram");
THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status);

// if we want to segregate fake outs pre or post fork, get distribution
std::unordered_map<uint64_t, std::pair<uint64_t, uint64_t>> segregation_limit;
float min_pre_fork_ratio = 0.0f, min_post_fork_ratio = 0.0f;

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

Looks like these aren't used anywhere?

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 15, 2018
Author Collaborator

They're used to calculate pre/post _fork_outputs_count below.

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

Really? Which line? I’m asking this because I couldn’t find after word search.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 15, 2018
Author Collaborator

line 5949, in the "how many fake outs to draw on a pre-fork triangular distribution" part.

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

Those are pre_fork_num_out_ratio and post_fork_num_out_ratio; here these are min_pre_fork_ratio and min_post_fork_ratio.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 15, 2018
Author Collaborator

Ah, I'd missed them, removed.

size_t pre_fork_outputs_count = requested_outputs_count * pre_fork_num_out_ratio;
size_t post_fork_outputs_count = requested_outputs_count * post_fork_num_out_ratio;
// how many fake outs to draw otherwise
size_t normal_output_count = requested_outputs_count - pre_fork_outputs_count - post_fork_num_out_ratio;

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

post_fork_outputs_count instead of post_fork_num_out_ratio

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 15, 2018
Author Collaborator

Sorry, careless with auto complete :/

LOG_PRINT_L1("Using " << recent_outputs_count << " recent outputs");
LOG_PRINT_L1("Fake output makeup: " << requested_outputs_count << " requested: " << recent_outputs_count << " recent, " <<
pre_fork_outputs_count << " pre-fork, " << post_fork_outputs_count << " post-fork, " <<
(normal_output_count - recent_outputs_count - pre_fork_outputs_count - post_fork_outputs_count) << " full-chain");

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

Shouldn't this be (normal_output_count - recent_outputs_count)? The current line evaluates to some weird value:

normal_output_count - recent_outputs_count - pre_fork_outputs_count - post_fork_outputs_count
=
(requested_outputs_count - pre_fork_outputs_count - post_fork_outputs_count)
- (requested_outputs_count - pre_fork_outputs_count - post_fork_outputs_count) * RECENT_OUTPUT_RATIO
- pre_fork_outputs_count - post_fork_outputs_count
=
(1-RECENT_OUTPUT_RATIO)*requested_outputs_count - (2+RECENT_OUTPUT_RATIO)*(pre_fork_outputs_count+post_fork_outputs_count)

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 15, 2018
Author Collaborator

Right, I wrongly subtract the post/pre twice, thanks.

{
switch (m_nettype)
{
case STAGENET: /* start_height = stagenet_hard_forks[2].height; break; */ return false;

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

Why is this commented out? The fork height is defined for stagenet as well.

This comment has been minimized.

@moneromooo-monero

moneromooo-monero Mar 15, 2018
Author Collaborator

There were no forks for it at the time. I'll change.

@moneromooo-monero moneromooo-monero force-pushed the moneromooo-monero:detfake-master branch 2 times, most recently from 7715e1e to fd3578f Mar 15, 2018
Copy link
Collaborator

@fluffypony fluffypony left a comment

Reviewed

@@ -1484,9 +1526,21 @@ void wallet2::process_outgoing(const crypto::hash &txid, const cryptonote::trans
entry.first->second.m_subaddr_account = subaddr_account;
entry.first->second.m_subaddr_indices = subaddr_indices;
}
else

This comment has been minimized.

@stoffu

stoffu Mar 15, 2018
Contributor

This else causes the block below to be not executed when the insertion took place. I believe this is unwanted.

This comment has been minimized.