Skip to content

Commit 5345b80

Browse files
committed
Use largest stack size when estimating size
When estimating the size of a taproot witness (i.e. DummySignatureCreator is being used), use the largest stack size rather than the smallest. This avoids underpaying fees. Also updates the tests to make sure that Taproot spends do not underestimate the fee
1 parent 58a988d commit 5345b80

File tree

3 files changed

+42
-11
lines changed

3 files changed

+42
-11
lines changed

src/script/sign.cpp

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCrea
226226
sigdata.tr_builder = builder;
227227
}
228228

229+
// Detect if this is the dummy signer. If so, we want to choose the largest stack size for worst case size estimation
230+
bool use_largest = creator.UseLargest();
231+
std::vector<std::vector<unsigned char>> largest_result_stack;
232+
uint64_t largest_result_stack_size = 0;
233+
229234
// Try key path spending.
230235
{
231236
KeyOriginInfo info;
@@ -248,26 +253,43 @@ static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCrea
248253
}
249254
}
250255
if (sigdata.taproot_key_path_sig.size()) {
251-
result = Vector(sigdata.taproot_key_path_sig);
252-
return true;
256+
if (use_largest) {
257+
largest_result_stack = Vector(sigdata.taproot_key_path_sig);
258+
largest_result_stack_size = GetSerializeSize(largest_result_stack, PROTOCOL_VERSION);
259+
} else {
260+
result = Vector(sigdata.taproot_key_path_sig);
261+
return true;
262+
}
253263
}
254264
}
255265

256266
// Try script path spending.
257267
std::vector<std::vector<unsigned char>> smallest_result_stack;
268+
uint64_t smallest_result_stack_size = 0;
258269
for (const auto& [key, control_blocks] : sigdata.tr_spenddata.scripts) {
259270
const auto& [script, leaf_ver] = key;
260271
std::vector<std::vector<unsigned char>> result_stack;
261272
if (SignTaprootScript(provider, creator, sigdata, leaf_ver, script, result_stack)) {
262273
result_stack.emplace_back(std::begin(script), std::end(script)); // Push the script
263274
result_stack.push_back(*control_blocks.begin()); // Push the smallest control block
264-
if (smallest_result_stack.size() == 0 ||
265-
GetSerializeSize(result_stack, PROTOCOL_VERSION) < GetSerializeSize(smallest_result_stack, PROTOCOL_VERSION)) {
266-
smallest_result_stack = std::move(result_stack);
275+
uint64_t stack_size = GetSerializeSize(result_stack, PROTOCOL_VERSION);
276+
if (smallest_result_stack.empty() ||
277+
stack_size < smallest_result_stack_size) {
278+
smallest_result_stack = result_stack;
279+
smallest_result_stack_size = stack_size;
280+
}
281+
if (use_largest && (largest_result_stack.empty() ||
282+
stack_size > largest_result_stack_size)) {
283+
largest_result_stack = result_stack;
284+
largest_result_stack_size = stack_size;
267285
}
268286
}
269287
}
270-
if (smallest_result_stack.size() != 0) {
288+
if (use_largest && !largest_result_stack.empty()) {
289+
result = std::move(largest_result_stack);
290+
return true;
291+
}
292+
if (!smallest_result_stack.empty()) {
271293
result = std::move(smallest_result_stack);
272294
return true;
273295
}

test/functional/test_framework/util.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
5151
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
5252

5353

54+
def assert_fee_enough(fee, tx_size, feerate_BTC_kvB):
55+
"""Assert the fee meets the feerate"""
56+
target_fee = get_fee(tx_size, feerate_BTC_kvB)
57+
if fee < target_fee:
58+
raise AssertionError("Fee of %s BTC too low! (Should be at least %s BTC)" % (str(fee), str(target_fee)))
59+
60+
5461
def assert_equal(thing1, thing2, *args):
5562
if thing1 != thing2 or any(thing1 != arg for arg in args):
5663
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))

test/functional/wallet_taproot.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from test_framework.address import output_key_to_p2tr
1212
from test_framework.key import H_POINT
1313
from test_framework.test_framework import BitcoinTestFramework
14-
from test_framework.util import assert_equal
14+
from test_framework.util import assert_equal, assert_fee_enough
1515
from test_framework.descriptors import descsum_create
1616
from test_framework.script import (
1717
CScript,
@@ -296,8 +296,9 @@ def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, key
296296
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
297297
test_balance = int(rpc_online.getbalance() * 100000000)
298298
ret_amnt = random.randrange(100000, test_balance)
299-
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
300-
res = rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True, fee_rate=200)
299+
res = rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True, fee_rate=10)
300+
txinfo = rpc_online.gettransaction(txid=res, verbose=True)
301+
assert_fee_enough(-txinfo["fee"], txinfo["decoded"]["vsize"], Decimal(0.00010000))
301302
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
302303
assert(rpc_online.gettransaction(res)["confirmations"] > 0)
303304

@@ -348,8 +349,7 @@ def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change)
348349
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
349350
test_balance = int(psbt_online.getbalance() * 100000000)
350351
ret_amnt = random.randrange(100000, test_balance)
351-
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
352-
psbt = psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 200, "change_type": address_type})['psbt']
352+
psbt = psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 10, "change_type": address_type})['psbt']
353353
res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
354354
for wallet in [psbt_offline, key_only_wallet]:
355355
res = wallet.walletprocesspsbt(psbt=psbt, finalize=False)
@@ -371,6 +371,8 @@ def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change)
371371
assert res[0]["allowed"]
372372

373373
txid = self.nodes[0].sendrawtransaction(rawtx)
374+
txinfo = psbt_online.gettransaction(txid=txid, verbose=True)
375+
assert_fee_enough(-txinfo["fee"], txinfo["decoded"]["vsize"], Decimal(0.00010000))
374376
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
375377
assert(psbt_online.gettransaction(txid)['confirmations'] > 0)
376378

0 commit comments

Comments
 (0)