mirror of
https://github.com/oxen-io/oxen-core.git
synced 2023-12-14 02:22:56 +01:00
initial commit for decoy selection
This commit is contained in:
parent
ae61454692
commit
d7fe7646fc
16 changed files with 253 additions and 16 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
36
src/wallet3/decoy_selection/decoy_selection.cpp
Normal file
36
src/wallet3/decoy_selection/decoy_selection.cpp
Normal 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
|
26
src/wallet3/decoy_selection/decoy_selection.hpp
Normal file
26
src/wallet3/decoy_selection/decoy_selection.hpp
Normal 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
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue