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

Blink #915

Merged
merged 71 commits into from
Nov 29, 2019
Merged

Blink #915

merged 71 commits into from
Nov 29, 2019

Conversation

jagerman
Copy link
Member

@jagerman jagerman commented Oct 27, 2019

This is the bulk of the work for blink. There are two pieces yet to come
which will follow in the next day or two which are: the p2p communication of blink
transactions (which needs to be fully synchronized, not just shared,
unlike regular mempool txes); and an implementation of fee burning.

Blink approval, multi-quorum signing, cli wallet and node support for
submission denial are all implemented here.

This overhauls and fixes various parts of the SNNetwork interface to fix
some issues (particularly around non-SN communication with SNs, which
wasn't working).

There are also a few sundry FIXME's and TODO's of other minor details
that will follow shortly under cleanup/testing/etc.

Note that this PR builds on top of PR #844 (and includes feedback from it), which is roughly the first half of the commits; I'll close #844. It also includes the requested shared_ptr -> regular pointer change requested in #872 (that didn't get included because of time constraints).

src/cryptonote_core/service_node_list.cpp Show resolved Hide resolved
src/cryptonote_protocol/quorumnet.cpp Outdated Show resolved Hide resolved
src/cryptonote_protocol/quorumnet.cpp Show resolved Hide resolved
src/cryptonote_protocol/quorumnet.cpp Show resolved Hide resolved
src/quorumnet/sn_network.cpp Outdated Show resolved Hide resolved
src/cryptonote_protocol/quorumnet.cpp Outdated Show resolved Hide resolved
src/wallet/wallet2.cpp Outdated Show resolved Hide resolved
}
else if (m_transfers[kit->second].m_spent || m_transfers[kit->second].amount() >= tx_scan_info[o].amount)
auto &transfer = m_transfers[kit->second];
if (transfer.m_unmined_blink && !pool && transfer.amount() == tx_scan_info[o].amount)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's sort of a loop hole here using the burning bug where you can immediately replace an older unspent output with a blink and make the output unusable because it sets the global output index to 0 and doesn't set m_unmined_blink to the appropriate value.

  1. Construct a non-blink transaction to the recipient, preserve the tx private key you generate for the recipient
  2. Construct a blink transaction with an amount greater than the previous sent amount, reusing the tx private key to send to the same stealth address.

Because the new output's amount > prior amount the else case triggers in this if/else chain. In that case we are replacing our pre-existing transfer in the wallet with the new incoming output because the new output's amount is greater than the one currently in the wallet. The new output is from blink- we replace the information in the transfer entry and also update transfer.m_global_output_index = pool ? 0 : o_indices[o];

But here we are replacing a pre-existing normal transaction, i.e. the transfer is not marked as a blink. When we go try and spend it, the wallet will try and reference the 0th global output and fail because presumably you don't hold the keys for that output.

A suggested fix might look like adding a bunch of sanity checks. My diff also includes folding down the first two if cases that repeat the same steps.

  • disallow blinks replacing blinks (blinks should be immutable)
  • disallow mined txs replacing unmined-blinks with different amounts
  • allow blinks to replace older mined transactions iff it transfers an amount greater than the old amount
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index 7d82b2a1f..090198c49 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -1927,10 +1927,12 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
             + "got " + (kit == m_pub_keys.end() ? "<none>" : boost::lexical_cast<std::string>(kit->second))
             + ", m_transfers.size() is " + boost::lexical_cast<std::string>(m_transfers.size()));
 
+        bool process_transaction = !pool || blink;
+        bool unmined_blink       = pool && blink;
         if (kit == m_pub_keys.end())
         {
           uint64_t amount = tx.vout[o].amount ? tx.vout[o].amount : tx_scan_info[o].amount;
-          if (!pool || blink)
+          if (process_transaction)
           {
             pk_to_unlock_times[tx_scan_info[o].in_ephemeral.pub] = tx_scan_info[o].unlock_time;
 
@@ -1938,8 +1940,8 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
             transfer_details& td = m_transfers.back();
             td.m_block_height = height; // NB: will be zero for a blink; we update when the blink tx gets mined
             td.m_internal_output_index = o;
-            td.m_global_output_index = pool ? 0 : o_indices[o]; // blink tx doesn't have this; will get updated when it gets into a block
-            td.m_unmined_blink = pool && blink;
+            td.m_global_output_index = unmined_blink ? 0 : o_indices[o]; // blink tx doesn't have this; will get updated when it gets into a block
+            td.m_unmined_blink = unmined_blink;
             td.m_tx = (const cryptonote::transaction_prefix&)tx;
             td.m_txid = txid;
             td.m_key_image = tx_scan_info[o].ki;
@@ -2005,39 +2007,42 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
           notify = true;
           continue;
         }
+
+        // NOTE: Pre-existing transfer already exists for the output
         auto &transfer = m_transfers[kit->second];
-        if (transfer.m_unmined_blink && !pool && transfer.amount() == tx_scan_info[o].amount)
+        THROW_WALLET_EXCEPTION_IF(blink && transfer.m_unmined_blink,
+                                  error::wallet_internal_error,
+                                  "A blink replacing an unmined blink should not happen, when a transaction is mined, "
+                                  "blink metadata is dropped and the TX should just be a normal TX");
+        THROW_WALLET_EXCEPTION_IF(pool && transfer.m_unmined_blink,
+                                  error::wallet_internal_error,
+                                  "An output replacing a unmined blink output must not be from the pool.");
+
+        if (transfer.m_spent || transfer.amount() >= tx_scan_info[o].amount)
         {
-          MINFO("Public key " << epee::string_tools::pod_to_hex(kit->first)
-              << " of blink tx " << transfer.m_txid << " (for " << print_money(tx_scan_info[o].amount) << ")"
-              << " status updated: now mined in block " << height);
+          if (transfer.amount() > tx_scan_info[o].amount)
+          {
+            LOG_ERROR("Public key " << epee::string_tools::pod_to_hex(kit->first)
+                << " from received " << print_money(tx_scan_info[o].amount) << " output already exists with "
+                << (transfer.m_spent ? "spent" : "unspent") << " "
+                << print_money(transfer.amount()) << " in tx " << transfer.m_txid << ", received output ignored");
+          }
 
-          blink_got_mined = true;
+          if (transfer.m_unmined_blink)
+          {
+            blink_got_mined = true;
+            THROW_WALLET_EXCEPTION_IF(transfer.amount() != tx_scan_info[o].amount, error::wallet_internal_error, "A blink should credit the amount exactly as we recorded it when it arrived in the mempool");
+            THROW_WALLET_EXCEPTION_IF(transfer.m_spent, error::wallet_internal_error, "Blink can not be spent before it is mined, this should never happen");
 
-          // We previous had this as a blink, but now it's been mined so update the tx status with the height and output index
-          transfer.m_block_height = height;
-          transfer.m_global_output_index = o_indices[o];
-          transfer.m_unmined_blink = false;
+            MINFO("Public key " << epee::string_tools::pod_to_hex(kit->first)
+                << " of blink tx " << transfer.m_txid << " (for " << print_money(transfer.amount()) << ")"
+                << " status updated: now mined in block " << height);
 
-          auto iter = std::find_if(
-              tx_money_got_in_outs.begin(),
-              tx_money_got_in_outs.end(),
-              [&tx_scan_info,&o](const tx_money_got_in_out& value)
-              {
-                return value.index == tx_scan_info[o].received->index &&
-                  value.amount == tx_scan_info[o].amount &&
-                  value.unlock_time == tx_scan_info[o].unlock_time;
-              }
-            );
-          THROW_WALLET_EXCEPTION_IF(iter == tx_money_got_in_outs.end(), error::wallet_internal_error, "Could not find the output we just added, this should never happen");
-          tx_money_got_in_outs.erase(iter);
-        }
-        else if (transfer.m_spent || m_transfers[kit->second].amount() >= tx_scan_info[o].amount)
-        {
-          LOG_ERROR("Public key " << epee::string_tools::pod_to_hex(kit->first)
-              << " from received " << print_money(tx_scan_info[o].amount) << " output already exists with "
-              << (m_transfers[kit->second].m_spent ? "spent" : "unspent") << " "
-              << print_money(m_transfers[kit->second].amount()) << " in tx " << m_transfers[kit->second].m_txid << ", received output ignored");
+            // We previous had this as a blink, but now it's been mined so update the tx status with the height and output index
+            transfer.m_block_height = height;
+            transfer.m_global_output_index = o_indices[o];
+            transfer.m_unmined_blink = false;
+          }
 
           auto iter = std::find_if(
               tx_money_got_in_outs.begin(),
@@ -2110,13 +2115,14 @@ void wallet2::process_new_transaction(const crypto::hash &txid, const cryptonote
           if (iter->amount == 0)
             tx_money_got_in_outs.erase(iter);
 
-          uint64_t amount = tx.vout[o].amount ? tx.vout[o].amount : tx_scan_info[o].amount;
+          uint64_t amount       = tx.vout[o].amount ? tx.vout[o].amount : tx_scan_info[o].amount;
           uint64_t extra_amount = amount - transfer.amount();
-          if (!pool || blink)
+          if (process_transaction)
           {
             transfer.m_block_height = height;
             transfer.m_internal_output_index = o;
-            transfer.m_global_output_index = pool ? 0 : o_indices[o]; // blink tx doesn't have this; will get updated when it gets into a block
+            transfer.m_global_output_index = unmined_blink ? 0 : o_indices[o]; // blink tx doesn't have this; will get updated when it gets into a block
+            transfer.m_unmined_blink = unmined_blink;
             transfer.m_tx = (const cryptonote::transaction_prefix&)tx;
             transfer.m_txid = txid;
             transfer.m_amount = amount;

src/cryptonote_core/cryptonote_core.cpp Outdated Show resolved Hide resolved
src/cryptonote_core/cryptonote_core.cpp Outdated Show resolved Hide resolved
src/cryptonote_core/cryptonote_core.cpp Show resolved Hide resolved
src/cryptonote_core/tx_pool.h Show resolved Hide resolved
Comment on lines +974 to +975
// FIXME - am I 100% sure I want to do the above? Verifying the TX would cut off being able to
// induce a node to broadcast a junk TX to other quorum members.
Copy link
Collaborator

@Doy-lee Doy-lee Nov 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some thought, I feel like we want this. This seems too abusable. It sort of work outs if you just do handle_incoming_tx and set do_not_relay to true.

Then your added set_relayable() to make it relayable once the signatures have arrived.

Currently calling tx_memory_pool::add_tx directly doesn't check if the tx exists in the blockchain yet (but going through core::handle_incoming_tx does, though that appears to be the only check missing when going straight to tx_memory_pool::add_tx)

Adds the zmq-based quorumnet communication layer.

Note that this is only essentially isolated library code in this commit
(which doesn't depend on anything else in loki).  The glue code that
ties them together follows in later commits.
This adds vote relaying via quorumnet.

- The main glue between existing code and quorumnet code is in
  cryptonote_protocol/quorumnet.{h,cpp}
- Uses function pointers assigned at initialization to call into the
  glue without making cn_core depend on p2p
- Adds ed25519 and quorumnet port to uptime proofs and serialization.
- Added a second uptime proof signature using the ed25519 key to that
  SNs are proving they actually have it (otherwise they could spoof
  someone else's pubkey).
- Adds quorumnet port, defaulting to zmq rpc port + 1.  quorumnet
  listens on the p2p IPv4 address (but broadcasts the `public_ip` to the
  network).
- Derives x25519 when received or deserialized.
- Converted service_info_info::version_t into a scoped enum (with the
  same underlying type).
- Added contribution_t::version_t scoped enum.  It was using
  service_info_info::version for a 0 value which seemed rather wrong.

Random small details:
- Renamed internal `arg_sn_bind_port` for consistency
- Moved default stagenet ports from 38153-38155 to 38056-38058, and add the default stagenet
  quorumnet port at 38059 (this keeps the range contiguous; otherwise the next stagenet default port
  would collide with the testnet p2p port).
For some reason they use GLOB for sources but this is strongly
discouraged.  This commit explicitly lists the files, just like every
other CMakeLists.txt in the project.
Something was apparently pulling this in already on my local system, but
the travis builds failed.
testing_quorum is a bit of a misnomer: it only does testing when used
for an obligations quorum, but it is also used for checkpointing and
soon Blink.
The map lookup is only really needed here for supporting multiple
SNNetwork instances, which is not going to be a common case.  This adds
an optimization for the common singleton case (which probably incurs a
small penalty for the multiple-SNNetwork case, but we aren't even
currently using that in loki).
The mismatch due to the offset between internal parts we have and the
parts we were actually sent from the remote looked wrong at first
glance; this commit makes it more obvious.
Quorumnet channels are bidirectional, which means we want to be able to
broadcast info to nodes that connect to us (if they have done so).  This
paves the way by providing an iterator through incoming connection
quorum members.
Also moves it into `quorumnet` namespace (instead of
`quorumnet::detail`).
But only non-temporary std::string lvalues, *not* temporaries since they
completely defeat the point.

Also beefs up the class documentation.
This adds a tx extra field that specifies an amount of the tx fee that
must be burned; the miner can claim only (txnFee - burnFee) when
including the block.

This will be used for the extra, burned part of blink fees and LNS fees
and any other transaction that requires fee burning in the future.
Neither of these have a place in modern C++11; boost::value_initialized
is entirely superseded by `Type var{};` which does value initialization
(or default construction if a default constructor is defined).  More
problematically, each `boost::value_initialized<T>` requires
instantiation of another wrapping templated type which is a pointless
price to pay the compiler in C++11 or newer.

Also removed is the AUTO_VAL_INIT macro (which is just a simple macro
around constructing a boost::value_initialized<T>).

BOOST_FOREACH is a similarly massive pile of code to implement
C++11-style for-each loops. (And bizarrely it *doesn't* appear to fall
back to C++ for-each loops even when under a C++11 compiler!)

This removes both entirely from the codebase.
This allows the caller to also take the response by rvalue reference so
that they can move outsubvalues.  The rvalue is totally fine here (once
the callback is invoked it is never used again) and still binds
perfectly well to const-lvalue accepting callbacks.
Removes one unnecessary layer of templated indirection in kv
serialization, and removes use of boost::mpl::vector code generation.

Also removes a double-specification of the same type in the epee
array_entry specification.
We don't impose any alignment on hashable types, but this means the
hashing function is doing invalid misaligned access when converting to a
size_t.  This aligns all of the primitive data types (crypto::hash,
public keys, etc.) to the same alignment as size_t.

That cascades into a few places in epee which only allow byte spanning
types that have byte alignment when what it really requires is just that
the type has no padding.  In C++17 this is exactly the purpose of
std::has_unique_object_representations, but that isn't available (or
even implementable) in C++14 so add specializations for the type that
need it to tell epee that we know those types are properly packed and
that it can safely use them as bytes.

Related to this, monero/epee also misuses `is_standard_layout` when the
purpose is actually `is_trivially_copyable`, so fixed that too.  (You
need the latter but don't need the former for a type to be safely
memcpy'able; the only purpose of `is_standard_layout` is when you need
to be sure your structs are compatible with C structs which is
irrelevant here).
These templates aren't instantiable with anything except for these exact
argument types, so don't use a template.
This adds a thread-local, pre-seeded rng at `tools::rng` (to avoid the
multiple places we are creating + seeding such an RNG currently).

This also moves the portable uniform value and generic shuffle code
there as well as neither function is specific to service nodes and this
seems a logical place for them.
There is nothing gained at all by trying to pad the data to an even
multiple of 1024 but it adds quite a lot of complexity (which would get
worse with blink).  Just adding some 0-1023 random bytes prevents
traffic analysis just as well and considerably simplifies the code.
Pool printing is separately implemented in rpc_command_executor; this
method in tx_pool is both obsolete (missing some fields) and not called
anywhere.
The only difference between long and short is one string (the json
info).

Also remove std::endl because using it like this indicates that the
author doesn't understand what std::endl is for.

Also removed a completely ridiculous inline lambda.
The default copy and move constructors would be broken and dangerous;
delete the copy ctor and provide a usable move ctor.  Also explicitly
delete copy and move assignment (they are implicitly deleted already but
making this explicit signals intent).

We can also save a bit of indirection by storing the BlockchainDB
reference directly rather than the Blockchain (which isn't needed except
to get the db).

Also remove the `m_active` variable: the two bools here are doing
nothing that a single m_batch bool can't do.
This code was too convoluted not to fix.

- There are 5 linear searches done over 2 vectors (without any changes
  done to the vectors in between).
- One of the linear searches calls out to epee to convert the same value
  to a hex string for every element.
- Both a set and a map are used here with identical key contents.  The
  old code looks like it only conditionally adds to the map if it finds
  a match in the loop, but the for loop is doing exactly the same test
  as the `find_if` that guards the entire `else if` containing it, so it
  will *always* match.
- Removed some error code that can never actually run (because of the
  above set/map equivalence).
These structures will always end up packed anyway (everything in them is
8-byte aligned and a multiple of 8 bytes), but the pack pragma also
changes their alignment which isn't good (and causes warnings).  This
just static_asserts that they are padding-free instead, which is the
intention.
There are a bunch trivial forwarding wrappers in cryptonote_core that
simply call the same method in the pool, and blink would require adding
several more.  Instead of all of these existing (and new) wrappers, just
directly expose the tx_pool reference so that anything with a `core`
reference can access and call to the mempool directly.
- Adds blink signature synchronization and storage through the regular
  p2p network
- Adds wallet support (though this is still currently buggy and needs
  additional fixes - it sees the tx when it arrives in the mempool but
  isn't properly updating when the blink tx gets mined.)
- confirmed was erroneously being set to true for mempool transactions
  causing it to sort at the beginning instead of the end of the list.
  (Since it gets zero initialized just delete it)

- show_transfers `min_height` was for some inexpicable reason being
  treated as a strict inequality so that `show_transfers 123 456` would
  not show you transactions from height 123.  (The max_height was *not*
  strict, so you *do* get transactions from height 456).
Rename inner `brd` variable to avoid shadowing the outer scope `brd`
variable.
This catches any exception thrown in the inner quorumnet blink code and
sets it in the promise if it occurs, which propagates it out to
core_rpc_server to catch and deal with.
- set the proper promise
- reuse reference rather than repeating lookups
- fix unneeded mempool lock
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants