initial commit for decoy selection

This commit is contained in:
Sean Darcy 2021-12-07 09:49:08 +11:00
parent ae61454692
commit d7fe7646fc
16 changed files with 253 additions and 16 deletions

View file

@ -6,6 +6,7 @@ add_library(wallet3
transaction_scanner.cpp
pending_transaction.cpp
output_selection/output_selection.cpp
decoy_selection/decoy_selection.cpp
wallet.cpp
wallet2½.cpp)

View file

@ -38,6 +38,9 @@ namespace wallet
virtual std::pair<int64_t, int64_t>
get_fee_parameters() = 0;
virtual std::future<std::vector<Decoy>>
fetch_decoys(std::vector<int64_t>& indexes) = 0;
};
} // namespace wallet

View file

@ -17,6 +17,7 @@ namespace wallet
R"(
CREATE TABLE blocks (
height INTEGER NOT NULL PRIMARY KEY,
transaction_count INTEGER NOT NULL,
hash TEXT NOT NULL,
timestamp INTEGER NOT NULL
);

View file

@ -1,11 +1,28 @@
#pragma once
#include <crypto/crypto.h>
#include <cryptonote_basic/cryptonote_basic.h>
#include "output.hpp"
namespace wallet
{
struct Decoy
{
// TODO: this
//outs - array of structure outkey as follows:
//height - unsigned int; block height of the output
//key - String; the public key of the output
//mask - String
//txid - String; transaction id
//unlocked - boolean; States if output is locked (false) or not (true)
int64_t height;
std::string key; // Hex public key of the output
std::string mask;
std::string txid;
bool unlocked;
};
} // namespace wallet

View file

@ -0,0 +1,36 @@
#include "decoy_selection.hpp"
namespace wallet
{
std::vector<Decoy>
DecoySelector::operator()(const Output& selected_output)
{
std::vector<Decoy> return_decoys;
const size_t n_decoys = 13;
// Select some random outputs according to gamma distribution
std::random_device rd;
std::default_random_engine rng(rd());
// TODO(sean): these should be built using the distribution
int64_t min_output_index = 100;
int64_t max_output_index = 100000;
constexpr int ALPHA = 1;
constexpr int BETA = 2;
std::gamma_distribution<double> distribution(ALPHA, BETA);
// Build a distribution and apply a score to each element of available outputs depending
// on distance from the number chosen. Lower score is better.
std::vector<int64_t> decoy_indexes;
for (size_t i = 0; i < n_decoys; ++i)
{
int64_t output_height_from_distribution = max_output_index - std::round(distribution(rng) * (max_output_index - min_output_index)/10);
decoy_indexes.push_back(output_height_from_distribution);
}
// TODO(sean): we need to also request the chosen output
auto decoy_promise = daemon->fetch_decoys(decoy_indexes);
decoy_promise.wait();
return decoy_promise.get();
}
} // namespace wallet

View file

@ -0,0 +1,26 @@
#pragma once
#include <vector>
#include "../daemon_comms.hpp"
#include "../output.hpp"
#include "../decoy.hpp"
namespace wallet
{
// DecoySelector will choose some a subset of outputs from the provided list of outputs according
// to the decoy selection algorithm. The decoys selected should hide the selected output within a
// ring signature and requires careful selection to avoid privacy decreasing analysis
class DecoySelector
{
public:
std::vector<Decoy>
operator()(const Output& selected_output);
DecoySelector(std::shared_ptr<DaemonComms> dmn) : daemon(std::move(dmn)) {};
private:
std::shared_ptr<DaemonComms> daemon;
};
} // namespace wallet

View file

@ -7,6 +7,7 @@
#include <common/string_util.h>
#include <epee/misc_log_ex.h>
#include "oxenmq/oxenmq.h"
#include <iostream>
@ -260,6 +261,110 @@ namespace wallet
omq->request(conn, "rpc.get_blocks", req_cb, oxenmq::bt_serialize(req_params_dict));
}
std::future<std::vector<Decoy>>
DefaultDaemonComms::fetch_decoys(std::vector<int64_t>& indexes)
{
auto p = std::make_shared<std::promise<std::vector<Decoy> > >();
auto fut = p->get_future();
auto req_cb = [p=std::move(p)](bool ok, std::vector<std::string> response)
{
if (not ok or response.size() == 0)
{
//TODO: error logging/handling
return;
}
if (not response.size())
{
std::cout << "on_get_outputs_response(): empty get_outputs response\n";
//TODO: error handling
return;
}
std::cout << "on_get_outputs_response() got " << response.size() - 1 << " outputs.\n";
const auto& status = response[0];
if (status != "OK" and status != "END")
{
std::cout << "get_outputs response: " << response[0] << "\n";
//TODO: error handling
return;
}
// "OK" response with no outputs
// TODO: decide/confirm this behavior on the daemon side of things
if (response.size() == 1)
{
std::cout << "get_blocks response.size() == 1\n";
return;
}
std::vector<Decoy> outputs;
try
{
auto itr = response.cbegin();
itr++;
while( itr != response.cend())
{
const auto& output_str = *itr;
auto output_dict = oxenmq::bt_dict_consumer{output_str};
Decoy& o = outputs.emplace_back();
if (output_dict.key() != "height")
return;
o.height = output_dict.consume_integer<int64_t>();
if (output_dict.key() != "key")
return;
o.key = output_dict.consume_string_view();
if (output_dict.key() != "mask")
return;
o.mask = output_dict.consume_string_view();
if (output_dict.key() != "txid")
return;
o.txid = output_dict.consume_string_view();
if (output_dict.key() != "unlocked")
return;
o.unlocked = output_dict.consume_integer<bool>();
if (not output_dict.is_finished())
return;
itr++;
}
}
catch (const std::exception& e)
{
std::cout << e.what() << "\n";
return;
}
if (outputs.size() == 0)
{
std::cout << "received no outputs, but server said response OK\n";
return;
}
}; // req_cb
oxenmq::bt_dict req_params_dict;
oxenmq::bt_list decoy_list_bt;
for (auto index : indexes)
{
oxenmq::bt_dict decoy_bt;
decoy_bt["amounts"] = 0;
decoy_bt["index"] = index;
decoy_list_bt.push_back(std::move(decoy_bt));
}
req_params_dict["outputs"] = std::move(decoy_list_bt);
omq->request(conn, "rpc.get_outs", req_cb, oxenmq::bt_serialize(req_params_dict));
return fut;
}
void
DefaultDaemonComms::register_wallet(wallet::Wallet& wallet, int64_t height, bool check_sync_height)
{

View file

@ -44,10 +44,12 @@ namespace wallet
void
deregister_wallet(Wallet& wallet, std::promise<void>& p);
std::pair<int64_t, int64_t>
get_fee_parameters();
std::future<std::vector<Decoy>>
fetch_decoys(std::vector<int64_t>& indexes);
private:
void

View file

@ -3,6 +3,7 @@
#include <cryptonote_basic/cryptonote_basic.h>
#include "address.hpp"
#include "output.hpp"
#include "decoy.hpp"
#include <vector>
#include <string>
@ -35,12 +36,14 @@ namespace wallet
std::vector<Output> chosen_outputs;
std::vector<std::vector<Decoy>> decoys;
bool blink = true;
int64_t fee = 0;
uint64_t fee_per_byte = FEE_PER_BYTE_V13;
uint64_t fee_per_output = FEE_PER_OUTPUT_V18;
int8_t mixin_count = CRYPTONOTE_DEFAULT_TX_MIXIN;
size_t mixin_count = CRYPTONOTE_DEFAULT_TX_MIXIN;
size_t extra_size() const {return 0;};
PendingTransaction() = default;

View file

@ -1,6 +1,8 @@
#include "transaction_constructor.hpp"
#include "pending_transaction.hpp"
#include "decoy.hpp"
#include "output_selection/output_selection.hpp"
#include "decoy_selection/decoy_selection.hpp"
#include <sqlitedb/database.hpp>
namespace wallet
@ -68,6 +70,17 @@ namespace wallet
ptx.update_change();
}
// select_decoys will choose some available outputs from the database and allocate to the
// transaction to sign as part of the ring signature
void
TransactionConstructor::select_decoys(PendingTransaction& ptx) const
{
ptx.decoys = {};
DecoySelector decoy_selection(daemon);
for (const auto& output : ptx.chosen_outputs)
ptx.decoys.emplace_back(decoy_selection(output));
}
void
TransactionConstructor::select_inputs_and_finalise(PendingTransaction& ptx) const
{
@ -78,5 +91,6 @@ namespace wallet
else
select_inputs(ptx);
}
select_decoys(ptx);
}
} // namespace wallet

View file

@ -32,8 +32,13 @@ namespace wallet
private:
void
select_inputs(PendingTransaction& ptx) const;
void
select_decoys(PendingTransaction& ptx) const;
void
select_inputs_and_finalise(PendingTransaction& ptx) const;
int64_t
estimate_fee() const;

View file

@ -60,8 +60,9 @@ namespace wallet
SQLite::Transaction db_tx(db->db);
db->prepared_exec(
"INSERT INTO blocks(height,hash,timestamp) VALUES(?,?,?)",
"INSERT INTO blocks(height,transaction_count,hash,timestamp) VALUES(?,?,?,?)",
block.height,
static_cast<int64_t>(block.transactions.size()),
tools::type_to_hex(block.hash),
block.timestamp);

View file

@ -24,7 +24,7 @@ TEST_CASE("DB Schema", "[wallet,db]")
SECTION("Insert and fetch block")
{
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?);", 42, "Adams", 0));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?,?);", 42, 0, "Adams", 0));
std::string hash;
REQUIRE_NOTHROW(hash = db.prepared_get<std::string>("SELECT hash FROM blocks WHERE height = 42"));
@ -34,7 +34,7 @@ TEST_CASE("DB Schema", "[wallet,db]")
SECTION("Insert and fetch transaction")
{
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?);", 0, "foo", 0));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?,?);", 0, 0, "foo", 0));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO transactions VALUES(?,?,?);", 42, 0, "footx"));
std::tuple<std::string, int> res;
@ -66,7 +66,7 @@ TEST_CASE("DB Triggers", "[wallet,db]")
REQUIRE_NOTHROW(wallet::create_schema(db.db));
REQUIRE(db.db.tableExists("blocks"));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?);", 0, "foo", 0));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?,?);", 0, 0, "foo", 0));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO transactions VALUES(?,?,?);", 0, 0, "footx"));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO key_images VALUES(?,?);", 0, "key_image"));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO outputs VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
@ -78,7 +78,7 @@ TEST_CASE("DB Triggers", "[wallet,db]")
REQUIRE(db.prepared_get<int64_t>("SELECT balance FROM metadata WHERE id = 0") == 42);
}
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?);", 1, "bar", 0));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO blocks VALUES(?,?,?,?);", 1, 0, "bar", 0));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO transactions VALUES(?,?,?);", 1, 1, "bartx"));
REQUIRE_NOTHROW(db.prepared_exec("INSERT INTO spends VALUES(?,?,?,?);", 0, 0, 1, 1));

View file

@ -14,10 +14,6 @@
int main(void)
{
auto oxenmq = std::make_shared<oxenmq::OxenMQ>();
auto ctor = std::make_shared<wallet::TransactionConstructor>(nullptr, nullptr);
crypto::secret_key spend_priv;
tools::hex_to_type<crypto::secret_key>("d6a2eac72d1432fb816793aa7e8e86947116ac1423cbad5804ca49893e03b00c", spend_priv);
crypto::public_key spend_pub;
@ -30,14 +26,13 @@ int main(void)
auto keyring = std::make_shared<wallet::Keyring>(spend_priv, spend_pub, view_priv, view_pub);
auto oxenmq = std::make_shared<oxenmq::OxenMQ>();
auto comms = std::make_shared<wallet::DefaultDaemonComms>(oxenmq);
oxenmq->start();
comms->set_remote("ipc://./oxend.sock");
oxenmq->start();
auto ctor = std::make_shared<wallet::TransactionConstructor>(nullptr, comms);
auto wallet = wallet::Wallet::create(oxenmq, keyring, ctor, comms, ":memory:", "");
std::this_thread::sleep_for(2s);
auto chain_height = comms->get_height();

View file

@ -19,6 +19,19 @@ class MockDaemonComms: public DefaultDaemonComms
get_fee_parameters() override {
return std::make_pair(0,0);
}
std::future<std::vector<Decoy>>
fetch_decoys(std::vector<int64_t>& indexes) override {
auto p = std::promise<std::vector<Decoy>>();
auto fut = p.get_future();
std::vector<Decoy> returned_decoys;
for (auto index : indexes)
returned_decoys.push_back(Decoy{index, "","","",true});
p.set_value(returned_decoys);
return fut;
}
};

View file

@ -34,6 +34,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 1);
REQUIRE(ptx.change.amount == 1);
REQUIRE(ptx.decoys.size() == ptx.chosen_outputs.size());
for (const auto& decoys : ptx.decoys)
REQUIRE(decoys.size() == 13);
}
SECTION("Fails to create a transaction if amount is not enough")
@ -53,6 +56,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 1);
REQUIRE(ptx.change.amount == 1);
REQUIRE(ptx.decoys.size() == ptx.chosen_outputs.size());
for (const auto& decoys : ptx.decoys)
REQUIRE(decoys.size() == 13);
}
SECTION("Creates a successful transaction using 2 inputs")
@ -62,6 +68,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 2);
REQUIRE(ptx.decoys.size() == ptx.chosen_outputs.size());
for (const auto& decoys : ptx.decoys)
REQUIRE(decoys.size() == 13);
}
wallet.store_test_transaction(4000);
@ -77,6 +86,9 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
REQUIRE(ptx.chosen_outputs.size() == 2);
// 8000 (Inputs) - 4001 (Recipient) - 1857 bytes x 1 oxen (Fee)
REQUIRE(ptx.change.amount == 2142);
REQUIRE(ptx.decoys.size() == ptx.chosen_outputs.size());
for (const auto& decoys : ptx.decoys)
REQUIRE(decoys.size() == 13);
}
ctor.fee_per_output = 50;
@ -89,5 +101,8 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
REQUIRE(ptx.chosen_outputs.size() == 2);
// 8000 (Inputs) - 4001 (Recipient) - 1857 bytes x 1 oxen (Fee) - 100 (Fee for 2x outputs @ 50 oxen)
REQUIRE(ptx.change.amount == 2042);
REQUIRE(ptx.decoys.size() == ptx.chosen_outputs.size());
for (const auto& decoys : ptx.decoys)
REQUIRE(decoys.size() == 13);
}
}