Skip to content
This repository was archived by the owner on Feb 12, 2025. It is now read-only.

Commit 0e847e5

Browse files
Fix fee calculation for OptimizeSize UTXO picking strategy (#924)
* FeeIsEnoughFor{P2WPKH, P2WSH, P2TR} tests * Fix for fee calculation in `OptimizeSize` strategy * `highest_first_limit_utxo` and `limit_utxo` strategies are removed because aren't used by WD
1 parent dcadaa4 commit 0e847e5

File tree

9 files changed

+126
-265
lines changed

9 files changed

+126
-265
lines changed

core/idl/wallet/bitcoin/bitcoin_like_wallet.djinni

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,6 @@ BitcoinLikePickingStrategy = enum {
230230
deep_outputs_first;
231231
optimize_size;
232232
merge_outputs;
233-
highest_first_limit_utxo;
234-
limit_utxo;
235233
}
236234

237235
BitcoinLikeTransactionBuilder = interface +c {

core/src/api/BitcoinLikePickingStrategy.cpp

Lines changed: 1 addition & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/api/BitcoinLikePickingStrategy.hpp

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/wallet/bitcoin/BitcoinLikeAccount.cpp

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -569,15 +569,6 @@ namespace ledger {
569569
sum = sum + utxo.value;
570570
}
571571
} break;
572-
case api::BitcoinLikePickingStrategy::HIGHEST_FIRST_LIMIT_UTXO:
573-
case api::BitcoinLikePickingStrategy::LIMIT_UTXO: {
574-
std::sort(utxos.begin(), utxos.end(), [](auto &lhs, auto &rhs) {
575-
return lhs.value.toInt64() > rhs.value.toInt64();
576-
});
577-
for (int i = 0; i < utxos.size() && i < maxUtxos.value_or(0); ++i) {
578-
sum = sum + utxos[i].value;
579-
}
580-
} break;
581572
default: {
582573
throw make_exception(api::ErrorCode::INVALID_ARGUMENT, "unknown strategy");
583574
}

core/src/wallet/bitcoin/scripts/BitcoinLikeScript.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include <math/bech32/Bech32Factory.h>
4040
#include <utils/hex.h>
4141
#include <wallet/bitcoin/networks.hpp>
42+
#include <wallet/currencies.hpp>
4243

4344
namespace ledger {
4445
namespace core {
@@ -127,6 +128,13 @@ namespace ledger {
127128
BitcoinLikeScript script;
128129
if (a->isP2WPKH() || a->isP2WSH()) {
129130
script << btccore::OP_0 << a->getHash160();
131+
} else if (a->isP2TR()) {
132+
if (currency.name != currencies::BITCOIN.name &&
133+
currency.name != currencies::BITCOIN_TESTNET.name &&
134+
currency.name != currencies::BITCOIN_REGTEST.name) {
135+
throw make_exception(api::ErrorCode::UNSUPPORTED_OPERATION, "Can't generate script from Taproot address ({}) in currency {}", address, currency.name);
136+
}
137+
script << btccore::OP_1 << a->getHash160();
130138
} else if (a->isP2PKH()) {
131139
script << btccore::OP_DUP << btccore::OP_HASH160 << a->getHash160() << btccore::OP_EQUALVERIFY
132140
<< btccore::OP_CHECKSIG;

core/src/wallet/bitcoin/transaction_builders/BitcoinLikeStrategyUtxoPicker.cpp

Lines changed: 47 additions & 151 deletions
Large diffs are not rendered by default.

core/src/wallet/bitcoin/transaction_builders/BitcoinLikeStrategyUtxoPicker.h

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,6 @@ namespace ledger {
6767
const std::vector<BitcoinLikeUtxo> &utxo,
6868
const BigInt &aggregatedAmount,
6969
const api::Currency &currrency);
70-
static std::vector<BitcoinLikeUtxo> filterWithHighestFirstLimitUtxo(
71-
const std::shared_ptr<BitcoinLikeUtxoPicker::Buddy> &buddy,
72-
std::vector<BitcoinLikeUtxo> utxos,
73-
const BigInt &aggregatedAmount,
74-
const api::Currency &currency,
75-
const optional<int32_t> &maxUtxo);
76-
static std::vector<BitcoinLikeUtxo> filterWithLimitUtxo(
77-
const std::shared_ptr<BitcoinLikeUtxoPicker::Buddy> &buddy,
78-
std::vector<BitcoinLikeUtxo> utxos,
79-
const BigInt &aggregatedAmount,
80-
const api::Currency &currency,
81-
const optional<int32_t> &maxUtxo);
8270
static bool hasEnough(const std::shared_ptr<Buddy> &buddy,
8371
const BigInt &aggregatedAmount,
8472
int inputCount,

core/test/bitcoin/bitcoin_utxo_picket_tests.cpp

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#include <gtest/gtest.h>
55
#include <ledger/core/api/Networks.hpp>
66
#include <spdlog/sinks/null_sink.h>
7+
#include <wallet/bitcoin/api_impl/BitcoinLikeScriptApi.h>
8+
#include <wallet/bitcoin/api_impl/BitcoinLikeTransactionApi.h>
9+
#include <wallet/bitcoin/scripts/BitcoinLikeScript.h>
710
#include <wallet/bitcoin/transaction_builders/BitcoinLikeStrategyUtxoPicker.h>
811
#include <wallet/common/Amount.h>
912
#include <wallet/currencies.hpp>
@@ -40,12 +43,6 @@ class MockKeychain : public BitcoinLikeKeychain {
4043
MOCK_CONST_METHOD0(getOutputSizeAsSignedTxInput, int32_t());
4144
};
4245

43-
class MockBitcoinLikeScript : public api::BitcoinLikeScript {
44-
public:
45-
MOCK_METHOD0(head, std::shared_ptr<api::BitcoinLikeScriptChunk>());
46-
MOCK_METHOD0(toString, std::string());
47-
};
48-
4946
class MockBitcoinLikeOutput : public api::BitcoinLikeOutput {
5047
public:
5148
MockBitcoinLikeOutput(int64_t amount) : _amount(std::make_shared<Amount>(currencies::BITCOIN, 0, BigInt(amount))){};
@@ -95,18 +92,20 @@ std::vector<BitcoinLikeUtxo> createUtxos(const std::vector<int64_t> &values) {
9592
return utxos;
9693
}
9794

98-
std::shared_ptr<BitcoinLikeUtxoPicker::Buddy> createBuddy(int64_t feesPerByte, int64_t outputAmount, const api::Currency &currency) {
95+
std::shared_ptr<BitcoinLikeUtxoPicker::Buddy> createBuddy(int64_t feesPerByte, int64_t outputAmount, const api::Currency &currency, const std::string keychainEngine = api::KeychainEngines::BIP32_P2PKH, const std::string address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") {
9996
BitcoinLikeTransactionBuildRequest r(std::make_shared<BigInt>(0));
100-
r.wipe = false;
101-
r.feePerByte = std::make_shared<BigInt>(feesPerByte);
102-
r.outputs.push_back(std::make_tuple(std::make_shared<BigInt>(outputAmount), std::make_shared<MockBitcoinLikeScript>()));
97+
r.wipe = false;
98+
r.feePerByte = std::make_shared<BigInt>(feesPerByte);
99+
ledger::core::BitcoinLikeScript outputScript = ledger::core::BitcoinLikeScript::fromAddress(address, currency);
100+
r.outputs.push_back(std::make_tuple(std::make_shared<BigInt>(outputAmount), std::make_shared<BitcoinLikeScriptApi>(outputScript)));
103101
r.utxoPicker = BitcoinUtxoPickerParams{api::BitcoinLikePickingStrategy::OPTIMIZE_SIZE, 0, optional<int32_t>()};
104102

105103
BitcoinLikeGetUtxoFunction g;
106104
BitcoinLikeGetTxFunction tx;
107105
std::shared_ptr<BitcoinLikeBlockchainExplorer> e;
108106
auto config = std::make_shared<ledger::core::DynamicObject>();
109-
config->putString(api::Configuration::KEYCHAIN_ENGINE, api::KeychainEngines::BIP32_P2PKH);
107+
config->putString(api::Configuration::KEYCHAIN_ENGINE, keychainEngine);
108+
config->putBoolean(api::Configuration::ALLOW_P2TR, true);
110109
std::shared_ptr<Preferences> preferences;
111110
std::shared_ptr<MockKeychain> k = std::make_shared<MockKeychain>(config, currency, 0, preferences);
112111
static std::shared_ptr<spdlog::logger> l = spdlog::null_logger_mt("null_sink");
@@ -185,3 +184,63 @@ TEST(OptimizeSize, ApproximationShouldTookEnough) {
185184
if (buddy->changeAmount.toInt64() != 0)
186185
EXPECT_GE(buddy->changeAmount.toInt64(), inputSizeInBytes * feesPerByte);
187186
}
187+
188+
void feeIsEnoughFor(const std::string address, const int64_t targetOutputSizeInBytes, const int64_t feesPerByte) {
189+
const api::Currency currency = currencies::BITCOIN_TESTNET;
190+
const int64_t inputSizeInBytes = 68; // we are spending P2WPKH input
191+
192+
const int64_t emtyTransactionSizeInBytes = 10;
193+
int64_t outputAmount = 50000000;
194+
std::vector<int64_t> inputAmounts{100000000};
195+
196+
auto buddy = createBuddy(feesPerByte, outputAmount, currency, api::KeychainEngines::BIP173_P2WPKH, address);
197+
198+
const int64_t changeOutputSizeInBytes = 8 + 1 + 1 + 1 + 20;
199+
const int64_t allOutputsSizeInBytes = targetOutputSizeInBytes + changeOutputSizeInBytes;
200+
201+
auto utxos = createUtxos(inputAmounts);
202+
auto pickedUtxos = BitcoinLikeStrategyUtxoPicker::filterWithOptimizeSize(buddy, utxos, BigInt(-1), currency);
203+
int64_t totalInputsValue = 0;
204+
for (auto utxo : pickedUtxos) {
205+
totalInputsValue += utxo.value.toLong();
206+
}
207+
int64_t transactionFees = totalInputsValue - buddy->changeAmount.toInt64() - outputAmount;
208+
int64_t minimumRequiredFees = (emtyTransactionSizeInBytes + allOutputsSizeInBytes + inputSizeInBytes * pickedUtxos.size()) * feesPerByte;
209+
210+
std::cerr << "transactionFees == " << transactionFees << std::endl;
211+
std::cerr << "minimumRequiredFees == " << minimumRequiredFees << std::endl;
212+
213+
EXPECT_GE(transactionFees, minimumRequiredFees);
214+
if (buddy->changeAmount.toInt64() != 0)
215+
EXPECT_GE(buddy->changeAmount.toInt64(), inputSizeInBytes * feesPerByte);
216+
}
217+
218+
TEST(OptimizeSize, FeeIsEnoughForP2WPKH) {
219+
const std::string address = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"; // P2WPKH
220+
221+
const int64_t targetOutputSizeInBytes = 8 + 1 + 1 + 1 + 20;
222+
// amount + script length + witness version + hash size + hash
223+
224+
for (int64_t feesPerByte = 1; feesPerByte < 1000000; feesPerByte *= 10)
225+
feeIsEnoughFor(address, targetOutputSizeInBytes, feesPerByte);
226+
}
227+
228+
TEST(OptimizeSize, FeeIsEnoughForP2WSH) {
229+
const std::string address = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"; // P2WSH
230+
231+
const int64_t targetOutputSizeInBytes = 8 + 1 + 1 + 1 + 32;
232+
// amount + script length + witness version + hash size + hash
233+
234+
for (int64_t feesPerByte = 1; feesPerByte < 1000000; feesPerByte *= 10)
235+
feeIsEnoughFor(address, targetOutputSizeInBytes, feesPerByte);
236+
}
237+
238+
TEST(OptimizeSize, FeeIsEnoughForP2TR) {
239+
const std::string address = "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c"; // P2TR
240+
241+
const int64_t targetOutputSizeInBytes = 8 + 1 + 1 + 1 + 32;
242+
// amount + script length + witness version + hash size + hash
243+
244+
for (int64_t feesPerByte = 1; feesPerByte < 1000000; feesPerByte *= 10)
245+
feeIsEnoughFor(address, targetOutputSizeInBytes, feesPerByte);
246+
}

core/test/integration/transactions/coin_selection_P2PKH.cpp

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -104,68 +104,6 @@ TEST_F(CoinSelectionP2PKH, PickMultipleUTXO) {
104104
EXPECT_EQ(tx->getOutputs().at(0)->getValue()->toLong(), 70000000);
105105
}
106106

107-
TEST_F(CoinSelectionP2PKH, HighestFirstEnough) {
108-
auto builder = tx_builder();
109-
builder->sendToAddress(api::Amount::fromLong(currency, 70000000), "2MvuUMAG1NFQmmM69Writ6zTsYCnQHFG9BF");
110-
builder->pickInputs(api::BitcoinLikePickingStrategy::HIGHEST_FIRST_LIMIT_UTXO, 0xFFFFFFFF, optional<int32_t>(2));
111-
builder->setFeesPerByte(api::Amount::fromLong(currency, 0));
112-
auto f = builder->build();
113-
auto tx = uv::wait(f);
114-
115-
EXPECT_LE(tx->getInputs().size(), 2);
116-
EXPECT_EQ(tx->getInputs().at(0)->getValue()->toLong(), 50000000);
117-
EXPECT_EQ(tx->getInputs().at(1)->getValue()->toLong(), 40000000);
118-
119-
EXPECT_EQ(tx->getOutputs().size(), 2);
120-
EXPECT_EQ(tx->getOutputs().at(0)->getValue()->toLong(), 70000000);
121-
}
122-
123-
TEST_F(CoinSelectionP2PKH, HighestFirstNotEnough) {
124-
auto builder = tx_builder();
125-
builder->sendToAddress(api::Amount::fromLong(currency, 100000000), "2MvuUMAG1NFQmmM69Writ6zTsYCnQHFG9BF");
126-
builder->pickInputs(api::BitcoinLikePickingStrategy::HIGHEST_FIRST_LIMIT_UTXO, 0xFFFFFFFF, optional<int32_t>(2));
127-
builder->setFeesPerByte(api::Amount::fromLong(currency, 0));
128-
auto f = builder->build();
129-
EXPECT_THROW(uv::wait(f), Exception);
130-
}
131-
132-
TEST_F(CoinSelectionP2PKH, LimitUtxoPickStrategy) {
133-
auto build = [=](int64_t amount) {
134-
auto builder = tx_builder();
135-
builder->sendToAddress(api::Amount::fromLong(currency, amount), "2MvuUMAG1NFQmmM69Writ6zTsYCnQHFG9BF");
136-
builder->pickInputs(api::BitcoinLikePickingStrategy::LIMIT_UTXO, 0xFFFFFFFF, optional<int32_t>(2));
137-
builder->setFeesPerByte(api::Amount::fromLong(currency, 0));
138-
auto f = builder->build();
139-
return uv::wait(f);
140-
};
141-
142-
{ // test 1
143-
auto tx = build(70000000);
144-
EXPECT_LE(tx->getInputs().size(), 2);
145-
EXPECT_EQ(tx->getInputs().at(0)->getValue()->toLong(), 50000000);
146-
EXPECT_EQ(tx->getInputs().at(1)->getValue()->toLong(), 40000000);
147-
EXPECT_EQ(tx->getOutputs().size(), 2);
148-
EXPECT_EQ(tx->getOutputs().at(0)->getValue()->toLong(), 70000000);
149-
}
150-
{ // test 2
151-
auto tx = build(50000000);
152-
EXPECT_LE(tx->getInputs().size(), 1);
153-
EXPECT_EQ(tx->getInputs().at(0)->getValue()->toLong(), 50000000);
154-
EXPECT_EQ(tx->getOutputs().size(), 1);
155-
EXPECT_EQ(tx->getOutputs().at(0)->getValue()->toLong(), 50000000);
156-
}
157-
{ // test 3
158-
EXPECT_THROW(build(100000000), Exception);
159-
}
160-
{ // test 4
161-
auto tx = build(10000000);
162-
EXPECT_LE(tx->getInputs().size(), 1);
163-
EXPECT_EQ(tx->getInputs().at(0)->getValue()->toLong(), 10000000);
164-
EXPECT_EQ(tx->getOutputs().size(), 1);
165-
EXPECT_EQ(tx->getOutputs().at(0)->getValue()->toLong(), 10000000);
166-
}
167-
}
168-
169107
TEST_F(CoinSelectionP2PKH, maxSpendable) {
170108
auto balance = uv::wait(account->getBalance());
171109
EXPECT_EQ(balance->toLong(), uv::wait(
@@ -177,15 +115,6 @@ TEST_F(CoinSelectionP2PKH, maxSpendable) {
177115
EXPECT_EQ(balance->toLong(), uv::wait(
178116
account->getMaxSpendable(api::BitcoinLikePickingStrategy::MERGE_OUTPUTS, optional<int32_t>()))
179117
->toLong());
180-
EXPECT_EQ(120000000, uv::wait(
181-
account->getMaxSpendable(api::BitcoinLikePickingStrategy::HIGHEST_FIRST_LIMIT_UTXO, optional<int32_t>(3)))
182-
->toLong());
183-
EXPECT_EQ(120000000, uv::wait(
184-
account->getMaxSpendable(api::BitcoinLikePickingStrategy::LIMIT_UTXO, optional<int32_t>(3)))
185-
->toLong());
186-
EXPECT_EQ(150000000, uv::wait(
187-
account->getMaxSpendable(api::BitcoinLikePickingStrategy::LIMIT_UTXO, optional<int32_t>(7)))
188-
->toLong());
189118
}
190119

191120
TEST_F(CoinSelectionP2PKH, PickAllUTXO) {

0 commit comments

Comments
 (0)