Skip to content

Commit 256e170

Browse files
committed
Merge bitcoin/bitcoin#29777: test: refactor: introduce and use calculate_input_weight helper
6d91cb7 test: add unit tests for `calculate_input_weight` (Sebastian Falbesoner) f81fad5 test: introduce and use `calculate_input_weight` helper (Sebastian Falbesoner) Pull request description: Rather than manually estimating an input's weight by adding up all the involved components (fixed-size skeleton, compact-serialized lengths, and the actual scriptSig / witness stack items) we can simply take use of the serialization classes `CTxIn` / `CTxInWitness` instead, to achieve the same with significantly less code. The new helper is used in the functional tests rpc_psbt.py and wallet_send.py, where the previous manual estimation code was duplicated. Unit tests are added in the second commit. ACKs for top commit: kevkevinpal: tACK [6d91cb7](bitcoin/bitcoin@6d91cb7) QureshiFaisal: tACK [6d91cb7](bitcoin/bitcoin@6d91cb7) achow101: ACK 6d91cb7 AngusP: tACK 6d91cb7 rkrux: tACK [6d91cb7](bitcoin/bitcoin@6d91cb7) Tree-SHA512: 04424e4d94d0e13745a9c11df2dd3697c98552bbb0e792c4af67ecbb66060adc3cc0cefc202cdee2d9db0baf85b8bedf2eb339ac4b316d986b5f10f6b70c5a33
2 parents 10bd32a + 6d91cb7 commit 256e170

File tree

4 files changed

+70
-29
lines changed

4 files changed

+70
-29
lines changed

test/functional/rpc_psbt.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
CTxIn,
1717
CTxOut,
1818
MAX_BIP125_RBF_SEQUENCE,
19-
WITNESS_SCALE_FACTOR,
20-
ser_compact_size,
2119
)
2220
from test_framework.psbt import (
2321
PSBT,
@@ -42,6 +40,7 @@
4240
find_vout_for_address,
4341
)
4442
from test_framework.wallet_util import (
43+
calculate_input_weight,
4544
generate_keypair,
4645
get_generate_key,
4746
)
@@ -752,17 +751,9 @@ def test_psbt_input_keys(psbt_input, keys):
752751
input_idx = i
753752
break
754753
psbt_in = dec["inputs"][input_idx]
755-
# Calculate the input weight
756-
# (prevout + sequence + length of scriptSig + scriptsig) * WITNESS_SCALE_FACTOR + len of num scriptWitness stack items + (length of stack item + stack item) * N stack items
757-
# Note that occasionally this weight estimate may be slightly larger or smaller than the real weight
758-
# as sometimes ECDSA signatures are one byte shorter than expected with a probability of 1/128
759-
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
760-
len_scriptsig += len(ser_compact_size(len_scriptsig))
761-
len_scriptwitness = (sum([(len(x) // 2) + len(ser_compact_size(len(x) // 2)) for x in psbt_in["final_scriptwitness"]]) + len(ser_compact_size(len(psbt_in["final_scriptwitness"])))) if "final_scriptwitness" in psbt_in else 0
762-
len_prevout_txid = 32
763-
len_prevout_index = 4
764-
len_sequence = 4
765-
input_weight = ((len_prevout_txid + len_prevout_index + len_sequence + len_scriptsig) * WITNESS_SCALE_FACTOR) + len_scriptwitness
754+
scriptsig_hex = psbt_in["final_scriptSig"]["hex"] if "final_scriptSig" in psbt_in else ""
755+
witness_stack_hex = psbt_in["final_scriptwitness"] if "final_scriptwitness" in psbt_in else None
756+
input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex)
766757
low_input_weight = input_weight // 2
767758
high_input_weight = input_weight * 2
768759

test/functional/test_framework/wallet_util.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Useful util functions for testing the wallet"""
66
from collections import namedtuple
7+
import unittest
78

89
from test_framework.address import (
910
byte_to_base58,
@@ -15,6 +16,11 @@
1516
script_to_p2wsh,
1617
)
1718
from test_framework.key import ECKey
19+
from test_framework.messages import (
20+
CTxIn,
21+
CTxInWitness,
22+
WITNESS_SCALE_FACTOR,
23+
)
1824
from test_framework.script_util import (
1925
key_to_p2pkh_script,
2026
key_to_p2wpkh_script,
@@ -123,6 +129,19 @@ def generate_keypair(compressed=True, wif=False):
123129
privkey = bytes_to_wif(privkey.get_bytes(), compressed)
124130
return privkey, pubkey
125131

132+
def calculate_input_weight(scriptsig_hex, witness_stack_hex=None):
133+
"""Given a scriptSig and a list of witness stack items for an input in hex format,
134+
calculate the total input weight. If the input has no witness data,
135+
`witness_stack_hex` can be set to None."""
136+
tx_in = CTxIn(scriptSig=bytes.fromhex(scriptsig_hex))
137+
witness_size = 0
138+
if witness_stack_hex is not None:
139+
tx_inwit = CTxInWitness()
140+
for witness_item_hex in witness_stack_hex:
141+
tx_inwit.scriptWitness.stack.append(bytes.fromhex(witness_item_hex))
142+
witness_size = len(tx_inwit.serialize())
143+
return len(tx_in.serialize()) * WITNESS_SCALE_FACTOR + witness_size
144+
126145
class WalletUnlock():
127146
"""
128147
A context manager for unlocking a wallet with a passphrase and automatically locking it afterward.
@@ -141,3 +160,42 @@ def __enter__(self):
141160
def __exit__(self, *args):
142161
_ = args
143162
self.wallet.walletlock()
163+
164+
165+
class TestFrameworkWalletUtil(unittest.TestCase):
166+
def test_calculate_input_weight(self):
167+
SKELETON_BYTES = 32 + 4 + 4 # prevout-txid, prevout-index, sequence
168+
SMALL_LEN_BYTES = 1 # bytes needed for encoding scriptSig / witness item lenghts < 253
169+
LARGE_LEN_BYTES = 3 # bytes needed for encoding scriptSig / witness item lengths >= 253
170+
171+
# empty scriptSig, no witness
172+
self.assertEqual(calculate_input_weight(""),
173+
(SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR)
174+
self.assertEqual(calculate_input_weight("", None),
175+
(SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR)
176+
# small scriptSig, no witness
177+
scriptSig_small = "00"*252
178+
self.assertEqual(calculate_input_weight(scriptSig_small, None),
179+
(SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR)
180+
# small scriptSig, empty witness stack
181+
self.assertEqual(calculate_input_weight(scriptSig_small, []),
182+
(SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR + SMALL_LEN_BYTES)
183+
# large scriptSig, no witness
184+
scriptSig_large = "00"*253
185+
self.assertEqual(calculate_input_weight(scriptSig_large, None),
186+
(SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR)
187+
# large scriptSig, empty witness stack
188+
self.assertEqual(calculate_input_weight(scriptSig_large, []),
189+
(SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR + SMALL_LEN_BYTES)
190+
# empty scriptSig, 5 small witness stack items
191+
self.assertEqual(calculate_input_weight("", ["00", "11", "22", "33", "44"]),
192+
((SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 5 * SMALL_LEN_BYTES + 5)
193+
# empty scriptSig, 253 small witness stack items
194+
self.assertEqual(calculate_input_weight("", ["00"]*253),
195+
((SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + LARGE_LEN_BYTES + 253 * SMALL_LEN_BYTES + 253)
196+
# small scriptSig, 3 large witness stack items
197+
self.assertEqual(calculate_input_weight(scriptSig_small, ["00"*253]*3),
198+
((SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 3 * LARGE_LEN_BYTES + 3*253)
199+
# large scriptSig, 3 large witness stack items
200+
self.assertEqual(calculate_input_weight(scriptSig_large, ["00"*253]*3),
201+
((SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 3 * LARGE_LEN_BYTES + 3*253)

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"crypto.ripemd160",
8686
"script",
8787
"segwit_addr",
88+
"wallet_util",
8889
]
8990

9091
EXTENDED_SCRIPTS = [

test/functional/wallet_send.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99

1010
from test_framework.authproxy import JSONRPCException
1111
from test_framework.descriptors import descsum_create
12-
from test_framework.messages import (
13-
ser_compact_size,
14-
WITNESS_SCALE_FACTOR,
15-
)
1612
from test_framework.test_framework import BitcoinTestFramework
1713
from test_framework.util import (
1814
assert_equal,
@@ -21,7 +17,10 @@
2117
assert_raises_rpc_error,
2218
count_bytes,
2319
)
24-
from test_framework.wallet_util import generate_keypair
20+
from test_framework.wallet_util import (
21+
calculate_input_weight,
22+
generate_keypair,
23+
)
2524

2625

2726
class WalletSendTest(BitcoinTestFramework):
@@ -543,17 +542,9 @@ def run_test(self):
543542
input_idx = i
544543
break
545544
psbt_in = dec["inputs"][input_idx]
546-
# Calculate the input weight
547-
# (prevout + sequence + length of scriptSig + scriptsig) * WITNESS_SCALE_FACTOR + len of num scriptWitness stack items + (length of stack item + stack item) * N stack items
548-
# Note that occasionally this weight estimate may be slightly larger or smaller than the real weight
549-
# as sometimes ECDSA signatures are one byte shorter than expected with a probability of 1/128
550-
len_scriptsig = len(psbt_in["final_scriptSig"]["hex"]) // 2 if "final_scriptSig" in psbt_in else 0
551-
len_scriptsig += len(ser_compact_size(len_scriptsig))
552-
len_scriptwitness = (sum([(len(x) // 2) + len(ser_compact_size(len(x) // 2)) for x in psbt_in["final_scriptwitness"]]) + len(ser_compact_size(len(psbt_in["final_scriptwitness"])))) if "final_scriptwitness" in psbt_in else 0
553-
len_prevout_txid = 32
554-
len_prevout_index = 4
555-
len_sequence = 4
556-
input_weight = ((len_prevout_txid + len_prevout_index + len_sequence + len_scriptsig) * WITNESS_SCALE_FACTOR) + len_scriptwitness
545+
scriptsig_hex = psbt_in["final_scriptSig"]["hex"] if "final_scriptSig" in psbt_in else ""
546+
witness_stack_hex = psbt_in["final_scriptwitness"] if "final_scriptwitness" in psbt_in else None
547+
input_weight = calculate_input_weight(scriptsig_hex, witness_stack_hex)
557548

558549
# Input weight error conditions
559550
assert_raises_rpc_error(

0 commit comments

Comments
 (0)