memory optimize PSBT inputs

This commit is contained in:
scgbckbone 2025-08-01 15:57:40 +02:00
parent 962dd2ef3f
commit 7d3c9828ba
4 changed files with 46 additions and 103 deletions

View File

@ -538,6 +538,7 @@ class ApproveTransaction(UserAuthorizedAction):
msg = "Transaction is too complex"
return await self.failure(msg)
except BaseException as exc:
# sys.print_exception(exc)
return await self.failure("Signing failed late", exc)
try:

View File

@ -630,11 +630,10 @@ class psbtInputProxy(psbtProxy):
blank_flds = (
'unknown', 'witness_utxo', 'sighash', 'redeem_script', 'witness_script',
'fully_signed', 'af', 'num_our_keys', 'is_miniscript', "subpaths",
'required_key', 'scriptSig', 'amount', 'scriptCode', 'previous_txid',
'required_key', 'amount', 'previous_txid', 'is_segwit', 'spk',
'prevout_idx', 'sequence', 'req_time_locktime', 'req_height_locktime',
'taproot_merkle_root', 'taproot_script_sigs', 'taproot_scripts',
'taproot_subpaths', 'taproot_internal_key', 'taproot_key_sig', 'utxo',
'is_segwit', 'taproot_spk',
)
def __init__(self, fd, idx):
@ -658,10 +657,8 @@ class psbtInputProxy(psbtProxy):
#self.af = None # string representation of address format aka. script type
#self.required_key = None # which of our keys will be used to sign input
#self.scriptSig = None
#self.amount = None
#self.scriptCode = None # only expected for segwit inputs
#self.taproot_spk = None # only on taproot inputs - utxo scriptPubKey
#self.spk = None # scriptPubKey for input utxo
# self.taproot_subpaths = {} # will be empty if non-taproot
# self.taproot_internal_key = None # will be empty if non-taproot
@ -845,13 +842,10 @@ class psbtInputProxy(psbtProxy):
# See what it takes to sign this particular input
# - type of script
# - which pubkey needed
# - scriptSig value
# - also validates redeem_script when present
merkle_root = redeem_script = None
self.amount = utxo.nValue
ss_script_code = lambda x: b'\x19\x76\xa9\x14' + x + b'\x88\xac'
if (not self.subpaths and not self.taproot_subpaths) or self.fully_signed:
# without xfp+path we will not be able to sign this input
# - okay if fully signed
@ -859,6 +853,8 @@ class psbtInputProxy(psbtProxy):
return
self.af, addr_or_pubkey = utxo.get_address()
# save scriptPubKey of utxo for later use
self.spk = utxo.scriptPubKey
if self.af == "op_return":
return
@ -888,8 +884,6 @@ class psbtInputProxy(psbtProxy):
# pubkey provided is just wrong vs. UTXO
raise FatalPSBTIssue('Input #%d: pubkey wrong' % my_idx)
self.scriptSig = utxo.scriptPubKey
elif "pkh" in self.af:
# P2PKH & P2WPKH
# input is hash160 of a single public key
@ -902,13 +896,6 @@ class psbtInputProxy(psbtProxy):
# none of the pubkeys provided hashes to that address
raise FatalPSBTIssue('Input #%d: pubkey vs. address wrong' % my_idx)
if self.af == "p2wpkh":
# P2WPKH only
self.scriptCode = ss_script_code(addr_or_pubkey)
else:
# P2PKH only
self.scriptSig = utxo.scriptPubKey
elif "sh" in self.af:
# we must have the redeem script already (else fail)
ks = self.witness_script or self.redeem_script
@ -917,15 +904,12 @@ class psbtInputProxy(psbtProxy):
redeem_script = self.get(ks)
native_v0 = (self.af == "p2wsh")
if not native_v0:
self.scriptSig = redeem_script
if not native_v0 and (len(redeem_script) == 22) and \
redeem_script[0] == 0 and redeem_script[1] == 20 and \
len(self.subpaths) == 1:
# it's actually segwit p2wpkh inside p2sh
self.af = 'p2sh-p2wpkh'
self.scriptCode = ss_script_code(redeem_script[2:22])
which_key, = self.subpaths.keys()
else:
@ -945,20 +929,14 @@ class psbtInputProxy(psbtProxy):
if self.witness_script and (not native_v0) and (self.redeem_script[1] == 34):
# bugfix
self.af = 'p2sh-p2wsh'
self.scriptSig = self.get(self.redeem_script)
assert (self.scriptSig[0] == 0) and (self.scriptSig[1] == 32), "malformed nested segwit redeem"
assert self.redeem_script[1] == 34
if "wsh" in self.af:
# for both P2WSH & P2SH-P2WSH
if not self.witness_script:
raise FatalPSBTIssue('Need witness script for input #%d' % my_idx)
# "scriptCode is witnessScript preceeded by a
# compactSize integer for the size of witnessScript"
self.scriptCode = ser_string(self.get(self.witness_script))
elif self.af == 'p2tr':
self.taproot_spk = utxo.scriptPubKey
merkle_root = None if self.taproot_merkle_root is None else self.get(self.taproot_merkle_root)
if len(self.taproot_subpaths) == 1:
# keyspend without a script path
@ -966,10 +944,9 @@ class psbtInputProxy(psbtProxy):
xonly_pubkey, lhs_path = list(self.taproot_subpaths.items())[0]
lhs, path = lhs_path[0], lhs_path[1:] # meh - should be a tuple
assert not lhs, "LeafHashes have to be empty for internal key"
assert path[0] == my_xfp
output_key = taptweak(xonly_pubkey)
assert output_key == addr_or_pubkey
which_key = xonly_pubkey
if path[0] == my_xfp:
assert taptweak(xonly_pubkey) == addr_or_pubkey
which_key = xonly_pubkey
else:
# tapscript (is always miniscript wallet)
self.is_miniscript = True
@ -1048,6 +1025,28 @@ class psbtInputProxy(psbtProxy):
# Could probably free self.subpaths and self.redeem_script now, but only if we didn't
# need to re-serialize as a PSBT.
def segwit_v0_scriptCode(self):
# only v0 segwit
# only needed for sighash
assert self.is_segwit and self.af != "p2tr"
if self.af == "p2wpkh":
return b'\x19\x76\xa9\x14' + self.spk[2:2+20] + b'\x88\xac'
elif self.af == "p2sh-p2wpkh":
return b'\x19\x76\xa9\x14' + self.get(self.redeem_script)[2:22] + b'\x88\xac'
else:
assert self.af in ["p2wsh", "p2sh-p2wsh"]
# "scriptCode is witnessScript preceeded by a
# compactSize integer for the size of witnessScript"
return ser_string(self.get(self.witness_script))
def get_scriptSig(self):
if self.af in ["p2pk", "p2pkh"]:
return self.spk
elif "sh" in self.af and self.af != "p2wsh":
return self.get(self.redeem_script)
else:
return b""
def store(self, kt, key, val):
# Capture what we are interested in.
@ -2235,7 +2234,6 @@ class psbtObject(psbtProxy):
# but in other cases, no more signatures are possible
continue
txi.scriptSig = inp.scriptSig
schnorrsig = False
tr_sh = []
inp.handle_none_sighash()
@ -2330,11 +2328,14 @@ class psbtObject(psbtProxy):
else:
if not inp.is_segwit:
# Hash by serializing/blanking various subparts of the transaction
txi.scriptSig = inp.get_scriptSig()
digest = self.make_txn_sighash(in_idx, txi, inp.sighash)
else:
# Hash the inputs and such in totally new ways, based on BIP-143
if not inp.taproot_subpaths:
digest = self.make_txn_segwit_sighash(in_idx, txi, inp.amount, inp.scriptCode, inp.sighash)
digest = self.make_txn_segwit_sighash(in_idx, txi, inp.amount,
inp.segwit_v0_scriptCode(),
inp.sighash)
elif tr_sh:
pass # later()
else:
@ -2513,7 +2514,7 @@ class psbtObject(psbtProxy):
hashSequence.update(pack("<I", txi.nSequence))
inp = self.inputs[in_idx]
hashValues.update(pack("<q", inp.amount))
hashScriptPubKeys.update(ser_string(inp.taproot_spk))
hashScriptPubKeys.update(ser_string(inp.spk))
self.hashPrevouts = hashPrevouts.digest()
self.hashSequence = hashSequence.digest()
@ -2566,7 +2567,7 @@ class psbtObject(psbtProxy):
inp = self.inputs[in_idx]
msg += txi.prevout.serialize()
msg += pack("<q", inp.amount)
msg += ser_string(inp.taproot_spk)
msg += ser_string(inp.spk)
msg += pack("<I", txi.nSequence)
break
else:
@ -2809,9 +2810,8 @@ class psbtObject(psbtProxy):
if inp.is_segwit:
# p2sh-p2wsh & p2sh-p2wpkh still need redeem here (redeem is witness scriptPubKey)
# inp.scriptSig was correctly populated in determine_my_signing_key
# for p2wpkh & p2wsh inp.scriptSig is None (no redeem script bloat anymore)
txi.scriptSig = ser_string(inp.scriptSig) if inp.scriptSig else b""
txi.scriptSig = ser_string(inp.get_scriptSig())
# Actual signature will be in witness data area
else:
# insert the new signature(s), assuming fully signed txn.
@ -2822,7 +2822,7 @@ class psbtObject(psbtProxy):
for sig in sigs:
ss += ser_push_data(sig)
ss += ser_push_data(inp.scriptSig) # scriptSig contains actual redeem script
ss += ser_push_data(self.get(inp.redeem_script))
txi.scriptSig = ss
else:
pubkey, der_sig = ssig

View File

@ -9,39 +9,38 @@ from uio import BytesIO
cases = [
# TxOut, type, is_segwit, hash160/pubkey,
( 'c4f33d0000000000160014ad46a001d55bd55d157e716bf17c02f8964b5a19',
'p2pkh', True,
'p2wpkh',
'ad46a001d55bd55d157e716bf17c02f8964b5a19' ),
( '202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac',
'p2pkh', False,
'p2pkh',
'8280b37df378db99f66f85c95a783a76ac7a6d59' ),
# from legendary txid: 40eee3ae1760e3a8532263678cdf64569e6ad06abc133af64f735e52562bccc8
( '301b0f000000000017a914e9c3dd0c07aac76179ebc76a6c78d4d67c6c160a87',
'p2sh', False,
'p2sh',
'e9c3dd0c07aac76179ebc76a6c78d4d67c6c160a'),
# from testnet: a4c89e0ffb84d06a1e62f0f9f0f5974db250878caa1f71f9992a1f865b8ff2fa
# via <https://github.com/bitcoinjs/bitcoinjs-lib/issues/856>
( 'b88201000000000017a914f0ca58dc8e539421a3cb4a9c22c059973075287c87',
'p2sh', False,
'p2sh',
'f0ca58dc8e539421a3cb4a9c22c059973075287c'),
# XXX missing: P2SH segwit, 1of1 and N of M
( 'd0f13d0000000000160014f2369bac6d24ed11313fa65adda1971d10e17bff',
'p2pkh', True,
'p2wpkh',
'f2369bac6d24ed11313fa65adda1971d10e17bff')
]
for raw_txo, expect_type, expect_sw, expect_hash in cases:
for raw_txo, expect_type, expect_hash in cases:
expect_hash = a2b_hex(expect_hash)
out = CTxOut()
out.deserialize(BytesIO(a2b_hex(raw_txo)))
print("Case: %s... " % raw_txo[0:30])
addr_type, addr_or_pubkey, is_segwit = out.get_address()
addr_type, addr_or_pubkey = out.get_address()
assert is_segwit == expect_sw, 'wrong segwit'
assert addr_or_pubkey == expect_hash, 'wrong pubkey/addr'
assert addr_type == expect_type, addr_type

View File

@ -1,57 +0,0 @@
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# unit test for address decoding for multisig
from h import a2b_hex, b2a_hex
from utils import xfp2str, str2xfp
from chains import BitcoinMain, BitcoinTestnet, BitcoinRegtest
from multisig import disassemble_multisig_mn
from public_constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH
from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT, AFC_WRAPPED
pk = a2b_hex('0202c68b0228cb577123c2f41275dadf8f4958890d3daf3728e38492f4913077dc')
script = a2b_hex('52210202c68b0228cb577123c2f41275dadf8f4958890d3daf3728e38492f4913077dc2102316dfd8d084a2b645061423013b52f513846e80c10816f66330f5609c8f6e7e221025328ece688cdc37d679b3af650f5d51487c1fe2fbd733b38cbfb58a9588a2155210288eb170b0661a6e86d1f1ab53a1099970d1b4d4cdd44d503d926effeec1e20842102fc3285261cccf4e7a44219758ee0383d25133b19a9fa14441ecb6ce9f3a4a52821038d00b6b4752dbba6afe6dcc00ef4b1fb0c212695f28a7908256808c2c201c43521038d5bcc32c89e363d181a08eb1c7613c0ba9aa02643d04cf00ae2cfea4192c9722103a11fd11e66e3d50818e3826a9b157245e6b361e32db9036768b54b4bc09adf092103d357b96bf98bcd5705d0f4745c2557d452d46a7cb9a6b193521de4516790f1182103f5bf5e00104c8956127ff926c0c5dd74690f8e67a21898cecb256dda34428a795aae')
M, N = disassemble_multisig_mn(script)
assert M == 2
assert N == 10
# assert pubkeys[0] == pk
# assert keys == ['mpsMLTNqBNrsQuYNmZPj7ifqqMTSnZMMWH', 'mjoj9a1cFNPhvFkbrwzNPTBCWxhteAJHE5',
# 'mkjqteuKMDApEzsZbdphtufvVPmCFafLhM', 'mvRSS7xmYBjDUEQsxvNefXLbwQHpwm76wb',
# 'mhGBcrA9xDuBWttQLZFGRBJHcGEZyQpT3b', 'mkYFhxXQY6mMZbKxcuk6j6FD2Ff1gX6zgC',
# 'mmgkFCdHKxCHuTMcJ9CPncRMA2UPainW6j', 'mg5fNCy7TJiZ8L4uxU3XerW2twNYAY3hmU',
# 'myY1Xmhx6CdvFn6uzdUDo5EM2HxmsPXPJB', 'mozpwp3z32g9vBZxbpN6ySxx7A5EWw4Zfi']
addr = BitcoinMain.p2sh_address(AF_P2SH, script)
assert addr[0] == '3'
assert addr == '3Kt6KxjirrFS7GexJiXLLhmuaMzSbjp275'
addr = BitcoinTestnet.p2sh_address(AF_P2SH, script)
assert addr[0] == '2'
assert addr == '2NBSJPhfkUJknK4HVyr9CxemAniCcRfhqp4'
addr = BitcoinRegtest.p2sh_address(AF_P2SH, script)
assert addr[0] == '2'
assert addr == '2NBSJPhfkUJknK4HVyr9CxemAniCcRfhqp4'
addr = BitcoinMain.p2sh_address(AF_P2WSH, script)
assert addr[0:4] == 'bc1q', addr
assert len(addr) >= 62
assert addr == 'bc1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkqftu4jr'
addr = BitcoinTestnet.p2sh_address(AF_P2WSH, script)
assert addr[0:4] == 'tb1q', addr
assert len(addr) >= 62
assert addr == 'tb1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkq7r26gv'
addr = BitcoinRegtest.p2sh_address(AF_P2WSH, script)
assert addr[0:6] == 'bcrt1q', addr
assert len(addr) >= 64
assert addr == 'bcrt1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkqn6quak'
assert xfp2str(0x10203040) == '40302010'
for i in 0, 1, 0x12345678:
assert str2xfp(xfp2str(i)) == i