Calculate the fee based on the current pending transaction.

When building the pending transaction we can call GetFee() to calculate
how much the transaction will cost. It takes a single parameter for the
number of inputs because we will want to specify how many when
estimating.

We then build a list of the potential fees for up to 300 inputs and pass
that to our output selection function which will use it to determine if
the outputs selected will be sufficient to cover the fees. This allows
us to know in advance how much the fees will be rather than trial and
error.
This commit is contained in:
Sean Darcy 2021-12-02 08:51:26 +11:00
parent b85c01ba60
commit 0ac65075eb
12 changed files with 281 additions and 83 deletions

View File

@ -35,6 +35,9 @@ namespace wallet
virtual void
deregister_wallet(Wallet& wallet, std::promise<void>& p) = 0;
virtual std::pair<int64_t, int64_t>
get_fee_parameters() = 0;
};
} // namespace wallet

View File

@ -175,6 +175,30 @@ namespace wallet
// RPC response is chain length, not top height
top_block_height = new_height - 1;
}, "de");
omq->request(conn, "rpc.get_fee_estimate",
[this](bool ok, std::vector<std::string> response)
{
if (not ok or response.size() != 2 or response[0] != "200")
return;
oxenmq::bt_dict_consumer dc{response[1]};
int64_t new_fee_per_byte = 0;
int64_t new_fee_per_output = 0;
if (not dc.skip_until("fee_per_byte"))
throw std::runtime_error("bad response from rpc.get_fee_estimate, key 'fee_per_byte' missing");
new_fee_per_byte = dc.consume_integer<int64_t>();
if (not dc.skip_until("fee_per_output"))
throw std::runtime_error("bad response from rpc.get_fee_estimate, key 'fee_per_output' missing");
new_fee_per_output = dc.consume_integer<int64_t>();
fee_per_byte = new_fee_per_byte;
fee_per_output = new_fee_per_output;
}, "de");
}
DefaultDaemonComms::DefaultDaemonComms(std::shared_ptr<oxenmq::OxenMQ> omq)
@ -247,6 +271,12 @@ namespace wallet
}, sync_thread);
}
std::pair<int64_t, int64_t>
DefaultDaemonComms::get_fee_parameters()
{
return std::make_pair(fee_per_byte,fee_per_output);
}
void
DefaultDaemonComms::deregister_wallet(wallet::Wallet& wallet, std::promise<void>& p)
{

View File

@ -1,6 +1,7 @@
#pragma once
#include "daemon_comms.hpp"
#include "cryptonote_config.h"
#include <crypto/crypto.h>
@ -43,6 +44,10 @@ namespace wallet
void
deregister_wallet(Wallet& wallet, std::promise<void>& p);
std::pair<int64_t, int64_t>
get_fee_parameters();
private:
void
@ -73,6 +78,9 @@ namespace wallet
int64_t sync_from_height = 0;
bool syncing = false;
int64_t max_sync_blocks = DEFAULT_MAX_SYNC_BLOCKS;
int64_t fee_per_byte = FEE_PER_BYTE_V13;
int64_t fee_per_output = FEE_PER_OUTPUT_V18;
};
} // namespace wallet

View File

@ -11,7 +11,14 @@ namespace wallet
available_outputs.end(),
0,
[](const auto& accumulator, const auto& x) { return accumulator + x.amount; });
if (wallet_balance < amount)
int64_t fee = 0;
auto pos = fee_map.find(1);
if (pos == fee_map.end()) {
throw std::runtime_error("Missing fee amount");
} else {
fee = pos->second;
}
if (wallet_balance < amount + fee)
throw std::runtime_error("Insufficient Wallet Balance");
// Prefer a single output if suitable
@ -20,7 +27,7 @@ namespace wallet
available_outputs.begin(),
available_outputs.end(),
std::back_inserter(outputs_bigger_than_amount),
[amount](const auto& x) { return static_cast<int64_t>(x.amount) > amount; });
[amount, fee](const auto& x) { return static_cast<int64_t>(x.amount) > amount + fee; });
if (outputs_bigger_than_amount.size() > 0)
{
@ -73,8 +80,14 @@ namespace wallet
// Iterate through the list until we have sufficient return value
std::vector<Output> multiple_outputs{};
int i = 0;
while (amount > 0)
while (amount + fee > 0)
{
auto pos = fee_map.find(i+1);
if (pos == fee_map.end()) {
throw std::runtime_error("Missing fee amount");
} else {
fee = pos->second;
}
multiple_outputs.push_back(available_outputs[indices[i]]);
amount = amount - available_outputs[indices[i]].amount;
i++;

View File

@ -14,5 +14,17 @@ namespace wallet
public:
std::vector<Output>
operator()(const std::vector<Output>& available_outputs, int64_t amount) const;
void
push_fee(int64_t input_count, int64_t fee) {fee_map[input_count]=fee;};
void
clear_fees(){ fee_map.clear(); };
private:
// Keeps track of the fees that need to be paid on top of the amount passed in
// key represents the number of outputs and value represents the fee that needs
// to be included if that many outputs are chosen
std::map<int64_t, int64_t> fee_map;
};
} // namespace wallet

View File

@ -1,5 +1,6 @@
#include "transaction_constructor.hpp"
#include "pending_transaction.hpp"
#include "oxen_economy.h"
namespace wallet
{
@ -20,23 +21,23 @@ namespace wallet
}
void
PendingTransaction::UpdateChange()
PendingTransaction::update_change()
{
change.amount = SumInputs() - SumOutputs();
change.amount = sum_inputs() - sum_outputs() - get_fee();
}
int64_t
PendingTransaction::SumInputs()
PendingTransaction::sum_inputs() const
{
return std::accumulate(
chosenOutputs.begin(),
chosenOutputs.end(),
chosen_outputs.begin(),
chosen_outputs.end(),
0,
[](int64_t accumulator, const Output& output) { return accumulator + output.amount; });
}
int64_t
PendingTransaction::SumOutputs()
PendingTransaction::sum_outputs() const
{
return std::accumulate(
recipients.begin(),
@ -47,10 +48,75 @@ namespace wallet
});
}
bool
PendingTransaction::Finalise()
int64_t
PendingTransaction::get_fee() const
{
if (SumInputs() - SumOutputs() - change.amount == 0)
return get_fee(chosen_outputs.size());
}
int64_t
PendingTransaction::get_fee(int64_t n_inputs) const
{
// TODO sean add this
int64_t fixed_fee = 0;
// TODO sean add this
int64_t burn_pct = 0;
int64_t fee_percent = BLINK_BURN_TX_FEE_PERCENT_V18; // 100%
if (blink)
fee_percent = BLINK_MINER_TX_FEE_PERCENT + burn_pct; // Blink ends up being 300%
int64_t fee = (get_tx_weight(n_inputs) * fee_per_byte + (recipients.size() + 1) * fee_per_output) * fee_percent / 100;
// Add fixed amount to the fee for items such as burning. This is defined in the pending transactions
fee += fixed_fee;
return fee;
}
size_t
PendingTransaction::get_tx_weight(int64_t n_inputs) const
{
size_t size = 0;
// If there is no inputs then we estimate using one input
if (n_inputs == 0)
n_inputs = 1;
size_t n_outputs = recipients.size() + 1; // Recipients plus change
if (n_outputs == 0)
throw std::runtime_error{"Get Transaction Weight called on a transaction with no recipients"};
size += 1 + 6; // tx prefix, first few bytes
size += n_inputs * (1+6+(mixin_count+1)*2+32); // vin
size += n_outputs * (6+32); // vout
size += extra_size(); // extra
// rct signatures
size += 1; // type
size_t log_padded_outputs = 0;
while ((uint64_t(1)<<log_padded_outputs) < n_outputs)
++log_padded_outputs;
size += (2 * (6 + static_cast<int64_t>(log_padded_outputs)) + 4 + 5) * 32 + 3; // rangeSigs
size += n_inputs * (32 * (mixin_count+1) + 64); // CLSAGs
size += 32 * n_inputs; // pseudoOuts
size += 8 * n_outputs; // ecdhInfo
size += 32 * n_outputs; // outPk - only commitment is saved
size += 4; // txnFee
if (n_outputs > 2)
{
const uint64_t bp_base = 368;
size_t log_padded_outputs = 2;
while ((uint64_t(1)<<log_padded_outputs) < n_outputs)
++log_padded_outputs;
uint64_t nlr = 2 * (6 + log_padded_outputs);
const uint64_t bp_size = 32 * (9 + nlr);
const uint64_t bp_clawback = (bp_base * (1<<log_padded_outputs) - bp_size) * 4 / 5;
size += bp_clawback;
}
return size;
}
bool
PendingTransaction::finalise()
{
if (sum_inputs() - sum_outputs() - fee - change.amount == 0)
return true;
else
return false;

View File

@ -23,7 +23,7 @@ namespace wallet
struct PendingTransaction
{
version txVersion;
version tx_version;
std::vector<TransactionRecipient> recipients; // does not include change
@ -33,23 +33,39 @@ namespace wallet
cryptonote::transaction tx;
std::vector<Output> chosenOutputs;
std::vector<Output> chosen_outputs;
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 extra_size() const {return 0;};
PendingTransaction() = default;
PendingTransaction(const std::vector<TransactionRecipient>& new_recipients);
int64_t
get_fee() const;
int64_t
get_fee(int64_t n_inputs) const;
size_t
get_tx_weight(int64_t n_inputs) const;
void
UpdateChange();
update_change();
int64_t
SumInputs();
sum_inputs() const;
int64_t
SumOutputs();
sum_outputs() const;
bool
Finalise();
finalise();
};
} // namespace wallet

View File

@ -6,76 +6,77 @@
namespace wallet
{
PendingTransaction
TransactionConstructor::CreateTransaction(
const std::vector<TransactionRecipient>& recipients, int64_t feePerKB) const
TransactionConstructor::create_transaction(
const std::vector<TransactionRecipient>& recipients) const
{
PendingTransaction txNew(recipients);
SelectInputsAndFinalise(txNew, feePerKB);
return txNew;
PendingTransaction new_tx(recipients);
new_tx.fee_per_byte = fee_per_byte;
new_tx.fee_per_output = fee_per_output;
select_inputs_and_finalise(new_tx);
return new_tx;
}
// SelectInputs will choose some available unspent outputs from the database and allocate to the
// transaction can be called multiple times and will add until enough is sufficient
void
TransactionConstructor::SelectInputs(PendingTransaction& ptx, int64_t feePerKB) const
TransactionConstructor::select_inputs(PendingTransaction& ptx) const
{
const int64_t single_input_size = 1500;
const int64_t additional_input = 500;
// const int64_t feePerKB = 0.000366 * 1e9;
const int64_t dust_amount = single_input_size * feePerKB / 1000;
int64_t estimated_fee = EstimateFee();
// int64_t estimated_fee = estimate_fee(2, fake_outs_count, min_outputs, extra.size(), clsag,
// base_fee, fee_percent, fixed_fee, fee_quantization_mask);
int64_t transaction_total = ptx.SumOutputs() + estimated_fee;
const int64_t single_input_size = ptx.get_fee(1);
const int64_t double_input_size = ptx.get_fee(2);
const int64_t additional_input = double_input_size - single_input_size;
const int64_t dust_amount = single_input_size * ptx.fee_per_byte;
OutputSelector select_outputs{};
const int noutputs_estimate = 300; // number of outputs to precompute fee for
for (int64_t output_count = 1; output_count < noutputs_estimate; ++output_count)
{
select_outputs.push_fee(output_count, ptx.get_fee(output_count));
}
int64_t transaction_total = ptx.sum_outputs();
// Check that we actually have enough in the outputs to build this transaction. Fail early. We
// then increase the transaction_total to include an amount sufficient to cover a reasonable
// change amount. Transaction fee is high for the first input and prefer that the change amount
// is enough to cover that, but if we dont have enough in the wallet then try for enough to
// cover the fee as an additional (2nd+) input. Finally if the wallet balance is not sufficient
// change amount. Transaction fee is high for the first input because there is overhead to cover
// and prefer that the change amount is enough to cover that overhead, but if we dont have enough
// in the wallet then try to ensure there is enough to cover the fee
// as an additional (2nd+) input. Finally if the wallet balance is not sufficient
// allow the change to be dust but this will only occur if the wallet has enough to cover the
// transaction but not enough to also cover the dust which should be extremely unlikely.
int64_t wallet_balance = db->prepared_get<int>(
"SELECT sum(amount) FROM outputs WHERE amount > ?", additional_input * feePerKB / 1000);
"SELECT sum(amount) FROM outputs WHERE amount > ?", additional_input * static_cast<int64_t>(ptx.fee_per_byte));
if (wallet_balance < transaction_total)
throw std::runtime_error("Insufficient Wallet Balance");
else if (wallet_balance > transaction_total + single_input_size * feePerKB / 1000)
transaction_total += single_input_size * feePerKB / 1000;
else if (wallet_balance > transaction_total + additional_input * feePerKB / 1000)
transaction_total += additional_input * feePerKB / 1000;
else if (wallet_balance > transaction_total + single_input_size * static_cast<int64_t>(ptx.fee_per_byte))
transaction_total += single_input_size * ptx.fee_per_byte;
else if (wallet_balance > transaction_total + additional_input * static_cast<int64_t>(ptx.fee_per_byte))
transaction_total += additional_input * ptx.fee_per_byte;
std::vector<Output> available_outputs{};
SQLite::Statement st{
db->db,
"SELECT amount, output_index, global_index, unlock_time, block_height, spending, "
"spent_height FROM outputs WHERE amount > ? ORDER BY amount"};
st.bind(1, additional_input * feePerKB / 1000);
st.bind(1, additional_input * static_cast<int64_t>(ptx.fee_per_byte));
while (st.executeStep())
{
wallet::Output o(db::get<int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int64_t>(st));
available_outputs.push_back(o);
}
OutputSelector selectOutputs{};
ptx.chosenOutputs = selectOutputs(available_outputs, transaction_total);
ptx.UpdateChange();
ptx.chosen_outputs = select_outputs(available_outputs, transaction_total);
ptx.fee = ptx.get_fee();
ptx.update_change();
}
void
TransactionConstructor::SelectInputsAndFinalise(PendingTransaction& ptx, int64_t feePerKB) const
TransactionConstructor::select_inputs_and_finalise(PendingTransaction& ptx) const
{
while (true)
{
if (ptx.Finalise())
if (ptx.finalise())
break;
else
SelectInputs(ptx, feePerKB);
select_inputs(ptx);
}
}
int64_t
TransactionConstructor::EstimateFee() const
{
return 0;
}
} // namespace wallet

View File

@ -18,21 +18,28 @@ namespace wallet
{
public:
TransactionConstructor(std::shared_ptr<db::Database> database, std::shared_ptr<DaemonComms> dmn)
: db(std::move(database)), daemon(std::move(dmn)){};
: db(std::move(database)), daemon(std::move(dmn))
{
std::tie(fee_per_byte, fee_per_output) = daemon->get_fee_parameters();
};
PendingTransaction
CreateTransaction(const std::vector<TransactionRecipient>& recipients, int64_t feePerKB) const;
create_transaction(const std::vector<TransactionRecipient>& recipients) const;
uint64_t fee_per_byte = FEE_PER_BYTE_V13;
uint64_t fee_per_output = FEE_PER_OUTPUT_V18;
private:
void
SelectInputs(PendingTransaction& ptx, int64_t feePerKB) const;
select_inputs(PendingTransaction& ptx) const;
void
SelectInputsAndFinalise(PendingTransaction& ptx, int64_t feePerKB) const;
select_inputs_and_finalise(PendingTransaction& ptx) const;
int64_t
EstimateFee() const;
estimate_fee() const;
std::shared_ptr<db::Database> db;
std::shared_ptr<DaemonComms> daemon;
};
} // namespace wallet

View File

@ -0,0 +1,25 @@
#pragma once
#include <wallet3/default_daemon_comms.hpp>
namespace wallet
{
class MockDaemonComms: public DefaultDaemonComms
{
public:
MockDaemonComms() : DefaultDaemonComms(get_omq()){};
std::shared_ptr<oxenmq::OxenMQ> get_omq() {
return std::make_shared<oxenmq::OxenMQ>();
}
std::pair<int64_t, int64_t>
get_fee_parameters() override {
return std::make_pair(0,0);
}
};
} // namespace wallet

View File

@ -27,10 +27,10 @@ class MockWallet : public Wallet
int64_t height = 0;
std::shared_ptr<db::Database> GetDB() { return db; };
std::shared_ptr<db::Database> get_db() { return db; };
void
StoreTestTransaction(const int64_t amount)
store_test_transaction(const int64_t amount)
{
height++;
@ -38,7 +38,7 @@ class MockWallet : public Wallet
b.height = height;
auto hash = debug_random_filled<crypto::hash>(height);
b.hash = hash;
AddBlock(b);
add_block(b);
std::vector<wallet::Output> dummy_outputs;
wallet::Output o{};
@ -49,7 +49,7 @@ class MockWallet : public Wallet
dummy_outputs.push_back(o);
SQLite::Transaction db_tx(db->db);
StoreTransaction(hash, height, dummy_outputs);
store_transaction(hash, height, dummy_outputs);
db_tx.commit();
};
};

View File

@ -3,33 +3,36 @@
#include <wallet3/wallet.hpp>
#include <wallet3/db_schema.hpp>
#include <wallet3/default_daemon_comms.hpp>
#include <sqlitedb/database.hpp>
#include "mock_wallet.hpp"
#include "mock_daemon_comms.hpp"
TEST_CASE("Transaction Creation", "[wallet,tx]")
{
auto wallet = wallet::MockWallet();
auto ctor = wallet::TransactionConstructor(wallet.GetDB(), nullptr);
auto comms = std::make_shared<wallet::MockDaemonComms>();
auto ctor = wallet::TransactionConstructor(wallet.get_db(), comms);
ctor.fee_per_byte = 0;
ctor.fee_per_output = 0;
SECTION("Expect Fail if database is empty")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 4);
REQUIRE_THROWS(ctor.CreateTransaction(recipients, {}));
REQUIRE_THROWS(ctor.create_transaction(recipients));
}
wallet.StoreTestTransaction(5);
wallet.store_test_transaction(5);
SECTION("Creates a successful single transaction")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 4);
wallet::PendingTransaction ptx = ctor.CreateTransaction(recipients, {});
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosenOutputs.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 1);
REQUIRE(ptx.change.amount == 1);
}
@ -37,18 +40,18 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 6);
REQUIRE_THROWS(ctor.CreateTransaction(recipients, {}));
REQUIRE_THROWS(ctor.create_transaction(recipients));
}
wallet.StoreTestTransaction(5);
wallet.StoreTestTransaction(7);
wallet.store_test_transaction(5);
wallet.store_test_transaction(7);
SECTION("Creates a successful single transaction prefering to use a single input if possible")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 6);
wallet::PendingTransaction ptx = ctor.CreateTransaction(recipients, {});
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosenOutputs.size() == 1);
REQUIRE(ptx.chosen_outputs.size() == 1);
REQUIRE(ptx.change.amount == 1);
}
@ -56,21 +59,35 @@ TEST_CASE("Transaction Creation", "[wallet,tx]")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 8);
wallet::PendingTransaction ptx = ctor.CreateTransaction(recipients, {});
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosenOutputs.size() == 2);
REQUIRE(ptx.chosen_outputs.size() == 2);
}
wallet.StoreTestTransaction(1000);
wallet.StoreTestTransaction(1000);
wallet.store_test_transaction(4000);
wallet.store_test_transaction(4000);
ctor.fee_per_byte = 1;
SECTION("Creates a successful transaction using 2 inputs and avoids creating dust")
SECTION("Creates a successful transaction using 2 inputs, avoids creating dust and uses correct fee using 1 oxen per byte")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 1001);
wallet::PendingTransaction ptx = ctor.CreateTransaction(recipients, 1000);
recipients.emplace_back(wallet::address{}, 4001);
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
REQUIRE(ptx.chosenOutputs.size() == 2);
REQUIRE(ptx.change.amount == 999);
REQUIRE(ptx.chosen_outputs.size() == 2);
// 8000 (Inputs) - 4001 (Recipient) - 1857 bytes x 1 oxen (Fee)
REQUIRE(ptx.change.amount == 2142);
}
ctor.fee_per_output = 50;
SECTION("Creates a successful transaction using 2 inputs, avoids creating dust and uses correct fee using 1 oxen per byte and 50 oxen per output")
{
std::vector<wallet::TransactionRecipient> recipients;
recipients.emplace_back(wallet::address{}, 4001);
wallet::PendingTransaction ptx = ctor.create_transaction(recipients);
REQUIRE(ptx.recipients.size() == 1);
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);
}
}