transaction: add method verify_sig_for_txin

This new `Transaction.verify_sig_for_txin` function is an instance method of `Transaction` instead of `PartialTransaction`.
It takes a complete txin, a pubkey and a signature, and verifies the signature.

- `get_preimage_script` is renamed to `get_scriptcode_for_sighash` and now effectively has two implementations:
  - the old impl became `PartialTxInput.get_scriptcode_for_sighash`
    - this assumes we are the ones constructing a spending txin and can have knowledge beyond what will be revealed onchain
  - the new impl is in the base class, `TxInput.get_scriptcode_for_sighash`
    - this assumes the txin is already "complete", and mimics a consensus-verifier by extracting the required fields
      from the already complete witness/scriptSig and the scriptpubkey of the funding utxo
- `serialize_preimage` now does not require a PartialTransaction, it also works on the base class Transaction

-----

I intend to use this for debugging only atm: I noticed TxBatcher sometimes creates invalid signatures by seeing
that bitcoind rejects txs with `mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)`.
However the txs in question have multiple txins, with some txins containing multiple signatures, and bitcoind does not tell us
which txin/signature is invalid. Knowing which signature is invalid would be a start, but I can now add some temp debug logging
to `serialize_preimage` to compare the message being signed with the message being verified.

As can be seen from the tests, the signature and the pubkey needs to be manually extracted from the txin to be verified:
we still don't have a script interpreter so we don't have logic to "verify a txin". However this new code adds logic
to verify a signature for a txin/pubkey combo (which is a small part of an interpreter/verifier).
This commit is contained in:
SomberNight
2025-05-18 14:39:05 +00:00
parent 120d8ac62e
commit 0508625afc
4 changed files with 262 additions and 43 deletions

View File

@@ -565,10 +565,10 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
if p2pkhTransaction: if p2pkhTransaction:
tx_copy = copy.deepcopy(tx) tx_copy = copy.deepcopy(tx)
# monkey-patch method of tx_copy instance to change serialization # monkey-patch method of tx_copy instance to change serialization
def input_script(self, txin: PartialTxInput, *, estimate_size=False): def input_script(self, txin: PartialTxInput, *, estimate_size=False) -> bytes:
desc = txin.script_descriptor desc = txin.script_descriptor
if isinstance(desc, descriptor.PKHDescriptor): if isinstance(desc, descriptor.PKHDescriptor):
return Transaction.get_preimage_script(txin) return txin.get_scriptcode_for_sighash()
raise Exception(f"unsupported txin type. only p2pkh is supported. got: {desc.to_string()[:10]}") raise Exception(f"unsupported txin type. only p2pkh is supported. got: {desc.to_string()[:10]}")
tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction) tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction)
tx_dbb_serialized = tx_copy.serialize_to_network() tx_dbb_serialized = tx_copy.serialize_to_network()

View File

@@ -592,7 +592,7 @@ class Ledger_Client_Legacy(Ledger_Client):
self.give_error("No matching pubkey for sign_transaction") # should never happen self.give_error("No matching pubkey for sign_transaction") # should never happen
full_path = convert_bip32_intpath_to_strpath(full_path)[2:] full_path = convert_bip32_intpath_to_strpath(full_path)[2:]
redeemScript = Transaction.get_preimage_script(txin).hex() redeemScript = txin.get_scriptcode_for_sighash().hex()
txin_prev_tx = txin.utxo txin_prev_tx = txin.utxo
if txin_prev_tx is None and not txin.is_segwit(): if txin_prev_tx is None and not txin.is_segwit():
raise UserFacingException(_('Missing previous tx for legacy input.')) raise UserFacingException(_('Missing previous tx for legacy input.'))

View File

@@ -218,7 +218,7 @@ class BIP143SharedTxDigestFields(NamedTuple): # witness v0
hashOutputs: bytes hashOutputs: bytes
@classmethod @classmethod
def from_tx(cls, tx: 'PartialTransaction') -> 'BIP143SharedTxDigestFields': def from_tx(cls, tx: 'Transaction') -> 'BIP143SharedTxDigestFields':
inputs = tx.inputs() inputs = tx.inputs()
outputs = tx.outputs() outputs = tx.outputs()
hashPrevouts = sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs)) hashPrevouts = sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs))
@@ -241,7 +241,7 @@ class BIP341SharedTxDigestFields(NamedTuple): # witness v1
sha_outputs: bytes sha_outputs: bytes
@classmethod @classmethod
def from_tx(cls, tx: 'PartialTransaction') -> 'BIP341SharedTxDigestFields': def from_tx(cls, tx: 'Transaction') -> 'BIP341SharedTxDigestFields':
inputs = tx.inputs() inputs = tx.inputs()
outputs = tx.outputs() outputs = tx.outputs()
sha_prevouts = sha256(b''.join(txin.prevout.serialize_to_network() for txin in inputs)) sha_prevouts = sha256(b''.join(txin.prevout.serialize_to_network() for txin in inputs))
@@ -270,12 +270,12 @@ class SighashCache:
self._witver0 = None # type: Optional[BIP143SharedTxDigestFields] self._witver0 = None # type: Optional[BIP143SharedTxDigestFields]
self._witver1 = None # type: Optional[BIP341SharedTxDigestFields] self._witver1 = None # type: Optional[BIP341SharedTxDigestFields]
def get_witver0_data_for_tx(self, tx: 'PartialTransaction') -> BIP143SharedTxDigestFields: def get_witver0_data_for_tx(self, tx: 'Transaction') -> BIP143SharedTxDigestFields:
if self._witver0 is None: if self._witver0 is None:
self._witver0 = BIP143SharedTxDigestFields.from_tx(tx) self._witver0 = BIP143SharedTxDigestFields.from_tx(tx)
return self._witver0 return self._witver0
def get_witver1_data_for_tx(self, tx: 'PartialTransaction') -> BIP341SharedTxDigestFields: def get_witver1_data_for_tx(self, tx: 'Transaction') -> BIP341SharedTxDigestFields:
if self._witver1 is None: if self._witver1 is None:
self._witver1 = BIP341SharedTxDigestFields.from_tx(tx) self._witver1 = BIP341SharedTxDigestFields.from_tx(tx)
return self._witver1 return self._witver1
@@ -342,6 +342,8 @@ class TxInput:
self.__address = None # type: Optional[str] self.__address = None # type: Optional[str]
self.__value_sats = None # type: Optional[int] self.__value_sats = None # type: Optional[int]
self._is_taproot = None # type: Optional[bool] # None means unknown
def get_time_based_relative_locktime(self) -> Optional[int]: def get_time_based_relative_locktime(self) -> Optional[int]:
# see bip 68 # see bip 68
if self.nsequence & (1<<31): if self.nsequence & (1<<31):
@@ -451,6 +453,12 @@ class TxInput:
return True return True
return False return False
def is_taproot(self) -> Optional[bool]:
if self._is_taproot is None:
if self.address:
self._is_taproot = bitcoin.is_taproot_address(self.address)
return self._is_taproot
async def add_info_from_network( async def add_info_from_network(
self, self,
network: Optional['Network'], network: Optional['Network'],
@@ -480,6 +488,67 @@ class TxInput:
self.utxo = await fetch_from_network(txid=self.prevout.txid.hex()) self.utxo = await fetch_from_network(txid=self.prevout.txid.hex())
return self.utxo is not None return self.utxo is not None
def get_scriptcode_for_sighash(self) -> bytes:
"""Reconstructs the scriptcode part of the preimage for OP_CHECKSIG,
for an already complete txin, in order to *verify the signature*.
"""
scriptpubkey = self.scriptpubkey
if scriptpubkey is None:
raise Exception("missing scriptpubkey. 'utxo' not set?")
script_type = get_script_type_from_output_script(scriptpubkey)
if script_type == "p2wsh":
wit_elems = self.witness_elements()
if not wit_elems:
raise Exception(f"missing witness for {script_type=}")
witness_script = wit_elems[-1]
if self.address != bitcoin.script_to_p2wsh(witness_script):
raise Exception("witness_script from witness does not match address")
if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(witness_script)]:
raise Exception('OP_CODESEPARATOR black magic is not supported')
return witness_script
elif script_type == "p2wpkh":
pkh = scriptpubkey[-20:]
assert len(pkh) == 20
p2pkh_script = bitcoin.pubkeyhash_to_p2pkh_script(pkh)
return p2pkh_script
elif script_type == "p2sh":
if not self.script_sig:
raise Exception(f"missing script_sig for {script_type=}")
parsed_ss = list(script_GetOp(self.script_sig))
redeem_script = parsed_ss[-1][1]
if self.address != bitcoin.hash160_to_p2sh(hash_160(redeem_script)):
raise Exception("redeem_script from script_sig does not match address")
if self.is_segwit(): # p2sh-wrapped-segwit
inner_script_type = get_script_type_from_output_script(redeem_script)
if inner_script_type == "p2wsh":
wit_elems = self.witness_elements()
witness_script = wit_elems[-1]
if redeem_script != bitcoin.p2wsh_nested_script(witness_script):
raise Exception("witness_script from witness does not match redeem_script")
if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(witness_script)]:
raise Exception('OP_CODESEPARATOR black magic is not supported')
return witness_script
elif inner_script_type == "p2wpkh":
pkh = self.script_sig[-20:]
assert len(pkh) == 20
p2pkh_script = bitcoin.pubkeyhash_to_p2pkh_script(pkh)
return p2pkh_script
else:
raise Exception(f"unexpected {inner_script_type=} wrapped in p2sh. script_sig={self.script_sig.hex()}")
else:
if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(redeem_script)]:
raise Exception('OP_CODESEPARATOR black magic is not supported')
return redeem_script
elif script_type in ("p2pkh", "p2pk"):
# For "raw scriptpubkey" case, it is usually the scriptPubKey that is signed.
# Complications for the general case:
# - signatures should be removed ("FindAndDelete")
# - OP_CODESEPARATOR black magic
return scriptpubkey
else:
raise Exception(f"cannot handle {script_type=} ({scriptpubkey.hex()=})")
raise Exception("should not get here")
class BCDataStream(object): class BCDataStream(object):
"""Workalike python implementation of Bitcoin's CDataStream class.""" """Workalike python implementation of Bitcoin's CDataStream class."""
@@ -670,6 +739,7 @@ class OPGeneric:
OPPushDataPubkey = OPPushDataGeneric(lambda x: x in (33, 65)) OPPushDataPubkey = OPPushDataGeneric(lambda x: x in (33, 65))
OP_ANYSEGWIT_VERSION = OPGeneric(lambda x: x in list(range(opcodes.OP_1, opcodes.OP_16 + 1))) OP_ANYSEGWIT_VERSION = OPGeneric(lambda x: x in list(range(opcodes.OP_1, opcodes.OP_16 + 1)))
SCRIPTPUBKEY_TEMPLATE_P2PK = [OPPushDataGeneric(lambda x: x in (33, 65)), opcodes.OP_CHECKSIG]
SCRIPTPUBKEY_TEMPLATE_P2PKH = [opcodes.OP_DUP, opcodes.OP_HASH160, SCRIPTPUBKEY_TEMPLATE_P2PKH = [opcodes.OP_DUP, opcodes.OP_HASH160,
OPPushDataGeneric(lambda x: x == 20), OPPushDataGeneric(lambda x: x == 20),
opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG] opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]
@@ -677,6 +747,7 @@ SCRIPTPUBKEY_TEMPLATE_P2SH = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x
SCRIPTPUBKEY_TEMPLATE_WITNESS_V0 = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))] SCRIPTPUBKEY_TEMPLATE_WITNESS_V0 = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]
SCRIPTPUBKEY_TEMPLATE_P2WPKH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 20)] SCRIPTPUBKEY_TEMPLATE_P2WPKH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 20)]
SCRIPTPUBKEY_TEMPLATE_P2WSH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 32)] SCRIPTPUBKEY_TEMPLATE_P2WSH = [opcodes.OP_0, OPPushDataGeneric(lambda x: x == 32)]
SCRIPTPUBKEY_TEMPLATE_P2TR = [opcodes.OP_1, OPPushDataGeneric(lambda x: x == 32)]
SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT = [OP_ANYSEGWIT_VERSION, OPPushDataGeneric(lambda x: x in list(range(2, 40 + 1)))] SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT = [OP_ANYSEGWIT_VERSION, OPPushDataGeneric(lambda x: x in list(range(2, 40 + 1)))]
@@ -741,13 +812,15 @@ def match_script_against_template(script, template, debug=False) -> bool:
return True return True
def get_script_type_from_output_script(_bytes: bytes) -> Optional[str]: def get_script_type_from_output_script(scriptpubkey: bytes) -> Optional[str]:
if _bytes is None: if scriptpubkey is None:
return None return None
try: try:
decoded = [x for x in script_GetOp(_bytes)] decoded = [x for x in script_GetOp(scriptpubkey)]
except MalformedBitcoinScript: except MalformedBitcoinScript:
return None return None
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PK):
return 'p2pk'
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PKH): if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PKH):
return 'p2pkh' return 'p2pkh'
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2SH): if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2SH):
@@ -756,6 +829,8 @@ def get_script_type_from_output_script(_bytes: bytes) -> Optional[str]:
return 'p2wpkh' return 'p2wpkh'
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2WSH): if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2WSH):
return 'p2wsh' return 'p2wsh'
if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2TR):
return 'p2tr'
return None return None
@@ -974,6 +1049,7 @@ class Transaction:
self, self,
txin_index: int, txin_index: int,
*, *,
sighash: Optional[int] = None,
sighash_cache: SighashCache = None, sighash_cache: SighashCache = None,
) -> bytes: ) -> bytes:
nVersion = int.to_bytes(self.version, length=4, byteorder="little", signed=True) nVersion = int.to_bytes(self.version, length=4, byteorder="little", signed=True)
@@ -981,9 +1057,12 @@ class Transaction:
inputs = self.inputs() inputs = self.inputs()
outputs = self.outputs() outputs = self.outputs()
txin = inputs[txin_index] txin = inputs[txin_index]
sighash = txin.sighash if isinstance(txin, PartialTxInput):
if sighash is None: if sighash is None:
sighash = Sighash.DEFAULT if txin.is_taproot() else Sighash.ALL sighash = txin.sighash
if sighash is None:
sighash = Sighash.DEFAULT if txin.is_taproot() else Sighash.ALL
assert sighash is not None
if not Sighash.is_valid(sighash, is_taproot=txin.is_taproot()): if not Sighash.is_valid(sighash, is_taproot=txin.is_taproot()):
raise Exception(f"SIGHASH_FLAG ({sighash}) not supported!") raise Exception(f"SIGHASH_FLAG ({sighash}) not supported!")
if sighash_cache is None: if sighash_cache is None:
@@ -1044,7 +1123,7 @@ class Transaction:
else: else:
hashOutputs = bytes(32) hashOutputs = bytes(32)
outpoint = txin.prevout.serialize_to_network() outpoint = txin.prevout.serialize_to_network()
preimage_script = self.get_preimage_script(txin) preimage_script = txin.get_scriptcode_for_sighash()
scriptCode = var_int(len(preimage_script)) + preimage_script scriptCode = var_int(len(preimage_script)) + preimage_script
amount = int.to_bytes(txin.value_sats(), length=8, byteorder="little", signed=False) amount = int.to_bytes(txin.value_sats(), length=8, byteorder="little", signed=False)
nSequence = int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False) nSequence = int.to_bytes(txin.nsequence, length=4, byteorder="little", signed=False)
@@ -1054,7 +1133,7 @@ class Transaction:
else: # legacy sighash (pre-segwit) else: # legacy sighash (pre-segwit)
if sighash != Sighash.ALL: if sighash != Sighash.ALL:
raise Exception(f"SIGHASH_FLAG ({sighash}) not supported! (for legacy sighash)") raise Exception(f"SIGHASH_FLAG ({sighash}) not supported! (for legacy sighash)")
preimage_script = self.get_preimage_script(txin) preimage_script = txin.get_scriptcode_for_sighash()
txins = var_int(len(inputs)) + b"".join( txins = var_int(len(inputs)) + b"".join(
txin.serialize_to_network(script_sig=preimage_script if txin_index==k else b"") txin.serialize_to_network(script_sig=preimage_script if txin_index==k else b"")
for k, txin in enumerate(inputs)) for k, txin in enumerate(inputs))
@@ -1064,23 +1143,24 @@ class Transaction:
return preimage return preimage
raise Exception("should not reach this") raise Exception("should not reach this")
@classmethod def verify_sig_for_txin(
def get_preimage_script(cls, txin: 'PartialTxInput') -> bytes: self,
if txin.witness_script: *,
if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(txin.witness_script)]: txin_index: int,
raise Exception('OP_CODESEPARATOR black magic is not supported') pubkey_bytes: bytes,
return txin.witness_script sig: bytes,
if not txin.is_segwit() and txin.redeem_script: sighash_cache: SighashCache = None,
if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(txin.redeem_script)]: ) -> bool:
raise Exception('OP_CODESEPARATOR black magic is not supported') txin = self.inputs()[txin_index]
return txin.redeem_script if txin.is_taproot():
raise Exception("not implemented") # TODO
if desc := txin.script_descriptor: else:
sc = desc.expand() der_sig, sighash = sig[:-1], sig[-1]
if script := sc.scriptcode_for_sighash: pre_hash = self.serialize_preimage(txin_index, sighash=sighash, sighash_cache=sighash_cache)
return script pubkey = ecc.ECPubkey(pubkey_bytes)
raise Exception(f"don't know scriptcode for descriptor: {desc.to_string()}") msg_hash = sha256d(pre_hash)
raise UnknownTxinType(f'cannot construct preimage_script') sig64 = ecc.ecdsa_sig64_from_der_sig(der_sig)
return pubkey.ecdsa_verify(sig64, msg_hash)
def is_segwit(self, *, guess_for_address=False): def is_segwit(self, *, guess_for_address=False):
return any(txin.is_segwit(guess_for_address=guess_for_address) return any(txin.is_segwit(guess_for_address=guess_for_address)
@@ -1580,7 +1660,7 @@ class PartialTxInput(TxInput, PSBTSection):
self._witness_utxo = None # type: Optional[TxOutput] self._witness_utxo = None # type: Optional[TxOutput]
self.sigs_ecdsa = {} # type: Dict[bytes, bytes] # pubkey -> sig self.sigs_ecdsa = {} # type: Dict[bytes, bytes] # pubkey -> sig
self.tap_key_sig = None # type: Optional[bytes] # sig for taproot key-path-spending self.tap_key_sig = None # type: Optional[bytes] # sig for taproot key-path-spending
self.sighash = None # type: Optional[int] self.sighash = None # type: Optional[int] # note: wrong abstraction level. should be per-signature
self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path)
self.redeem_script = None # type: Optional[bytes] self.redeem_script = None # type: Optional[bytes]
self.witness_script = None # type: Optional[bytes] self.witness_script = None # type: Optional[bytes]
@@ -1594,7 +1674,6 @@ class PartialTxInput(TxInput, PSBTSection):
self._trusted_address = None # type: Optional[str] self._trusted_address = None # type: Optional[str]
self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown
self._is_native_segwit = None # type: Optional[bool] # None means unknown self._is_native_segwit = None # type: Optional[bool] # None means unknown
self._is_taproot = None # type: Optional[bool] # None means unknown
self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est
@property @property
@@ -1967,13 +2046,12 @@ class PartialTxInput(TxInput, PSBTSection):
return dummy_desc.is_segwit() return dummy_desc.is_segwit()
return False # can be false-negative return False # can be false-negative
def is_taproot(self) -> bool: def is_taproot(self) -> Optional[bool]:
if self._is_taproot is None: if (is_taproot := super().is_taproot()) is not None:
if self.address: return is_taproot
self._is_taproot = bitcoin.is_taproot_address(self.address)
if desc := self.script_descriptor: if desc := self.script_descriptor:
return desc.is_taproot() return desc.is_taproot()
return self._is_taproot return None
def already_has_some_signatures(self) -> bool: def already_has_some_signatures(self) -> bool:
"""Returns whether progress has been made towards completing this input.""" """Returns whether progress has been made towards completing this input."""
@@ -1982,6 +2060,33 @@ class PartialTxInput(TxInput, PSBTSection):
or self.script_sig is not None or self.script_sig is not None
or self.witness is not None) or self.witness is not None)
def get_scriptcode_for_sighash(self) -> bytes:
"""Constructs the scriptcode part of the preimage for OP_CHECKSIG,
for a partial txin, to create a new signature.
Note: the base impl works by mimicking a consensus-verifier and extracting
the required fields from the already complete witness/scriptSig
and the scriptpubkey of the funding utxo.
In contrast, here we are the one constructing a spending txin,
and can have knowledge beyond what will be revealed onchain.
"""
if self.witness_script:
if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(self.witness_script)]:
raise Exception('OP_CODESEPARATOR black magic is not supported')
return self.witness_script
if not self.is_segwit() and self.redeem_script:
if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(self.redeem_script)]:
raise Exception('OP_CODESEPARATOR black magic is not supported')
return self.redeem_script
if desc := self.script_descriptor:
sc = desc.expand()
if script := sc.scriptcode_for_sighash:
return script
raise Exception(f"don't know scriptcode for descriptor: {desc.to_string()}")
raise UnknownTxinType(f'cannot construct preimage_script')
class PartialTxOutput(TxOutput, PSBTSection): class PartialTxOutput(TxOutput, PSBTSection):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -2105,10 +2210,10 @@ class PartialTransaction(Transaction):
return d return d
@classmethod @classmethod
def from_tx(cls, tx: Transaction) -> 'PartialTransaction': def from_tx(cls, tx: Transaction, *, strip_witness: bool = True) -> 'PartialTransaction':
assert tx assert tx
res = cls() res = cls()
res._inputs = [PartialTxInput.from_txin(txin, strip_witness=True) res._inputs = [PartialTxInput.from_txin(txin, strip_witness=strip_witness)
for txin in tx.inputs()] for txin in tx.inputs()]
res._outputs = [PartialTxOutput.from_txout(txout) for txout in tx.outputs()] res._outputs = [PartialTxOutput.from_txout(txout) for txout in tx.outputs()]
res.version = tx.version res.version = tx.version
@@ -2395,14 +2500,14 @@ class PartialTransaction(Transaction):
def serialize(self) -> str: def serialize(self) -> str:
"""Returns PSBT as base64 text, or raw hex of network tx (if complete).""" """Returns PSBT as base64 text, or raw hex of network tx (if complete)."""
self.finalize_psbt() self.finalize_psbt() # FIXME this side-effects self
if self.is_complete(): if self.is_complete():
return Transaction.serialize(self) return Transaction.serialize(self)
return self._serialize_as_base64() return self._serialize_as_base64()
def serialize_as_bytes(self, *, force_psbt: bool = False) -> bytes: def serialize_as_bytes(self, *, force_psbt: bool = False) -> bytes:
"""Returns PSBT as raw bytes, or raw bytes of network tx (if complete).""" """Returns PSBT as raw bytes, or raw bytes of network tx (if complete)."""
self.finalize_psbt() self.finalize_psbt() # FIXME this side-effects self
if force_psbt or not self.is_complete(): if force_psbt or not self.is_complete():
with io.BytesIO() as fd: with io.BytesIO() as fd:
self._serialize_psbt(fd) self._serialize_psbt(fd)

View File

@@ -943,6 +943,120 @@ class TestTransactionTestnet(ElectrumTestCase):
self.assertEqual('020000000001019ad573c69e60c209e0ff36f281ae4f700a8d59f846e7ff5c020352fd1e97808600000000000000000001fa840100000000001600145a209b202bc19b3d345a75cf8ab51cb471913a790247304402207b191c1e3ff1a2d3541770b496c9f871406114746b3aa7347ec4ef0423d3a975022043d3a746fa7a794d97e95d74b6d17d618dfc4cd7644476813e08006f271e51bd012a046c4f855fb1752102aec53aa5f347219a7378b13006eb16ce48125f9cf14f04a5509a565ad5e51507ac6c4f855f', self.assertEqual('020000000001019ad573c69e60c209e0ff36f281ae4f700a8d59f846e7ff5c020352fd1e97808600000000000000000001fa840100000000001600145a209b202bc19b3d345a75cf8ab51cb471913a790247304402207b191c1e3ff1a2d3541770b496c9f871406114746b3aa7347ec4ef0423d3a975022043d3a746fa7a794d97e95d74b6d17d618dfc4cd7644476813e08006f271e51bd012a046c4f855fb1752102aec53aa5f347219a7378b13006eb16ce48125f9cf14f04a5509a565ad5e51507ac6c4f855f',
tx.serialize()) tx.serialize())
class TestTransactionVerifySig(ElectrumTestCase):
def test_verifysig_spending_uncompressed_p2pk(self):
funding_tx_raw = "010000000001021b41471d6af3aa80ebe536dbf4f505a6d46af456131a8e12e1950171959b690e0f00000000fdffffff2ef29833a69863b31e884fc5e6f7b99a23b5601e14f0eb65905faa42fec0776d0000000000fdffffff02f96a070000000000160014e61b989a740056254b5f8061281ac96ca15d35e140420f00000000004341049afa8fb50f52104b381a673c6e4fb7fb54987271d0e948dd9a568bb2af6f9310a7a809ce06e09d1510e5836f20414596232e2c0be63715459fa3cf8e7092af05ac0247304402201fe20012c1c732a6a8f942c4e0feed5ed0bddfb94db736ec3d0c0d38f0f7f46a022021d690e6d2688b90b76002f4c3134981502d666211e85e8a6ca91e78405dfa3801210346fb31136ab48e6c648865264d32004b43643d01f0ba485cffac4bb0b3f739470247304402204a2473ab4b3bfc8e6b1a6b8675dc2c3d115d8c04f5df37f29779dca6d300d9db02205e72ebbccd018c67b86ae4da6b0e6222902a8de85915ed6115330b9328764b370121027a93ffc9444a12d99307318e2e538949072cb35b2aca344b8163795a022414c7d73a1400"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]
pubkey = list(script_GetOp(funding_tx.outputs()[1].scriptpubkey))[0][1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))
def test_verifysig_spending_compressed_p2pk(self):
funding_tx_raw = "02000000000102b7bfcd442c91134743c6e4100bb9f79456a6015de3c3920166bb0c3b7a8f7c070100000000fdffffff5ab39480d4b35ffa843691d944a8479dfe825d38b03fcb1804197482bfad80fb0100000000fdffffff02d4ec000000000000160014769114e56e0913de3719a3b00a446b78e61751f007b201000000000023210332e147520e4743299d95196afaf9db7c86fe02507d9ca89acd7a4e96a63653d5ac0247304402200387fe79ffe10cec73d9b131058d7128665f729d14597828b483842889c4f5ea02201197b2f1295e4011e2d174d53c240fd13c6351451ab961ccb3678fc21fa5323b0121023c221dfbf7c3f61b9e5f66343c1a302d6beca2a8883504b0f484faec9919636b024730440220687d387af37df458efc104ee0065262cb5ea195e526ed7a480fd16e6cf708c3a022019bd3fd9c3ca3f1a1fbeabe20547876eb4572a7339de37b706fbd55031e60428012102c9c459e58b01a864d7bb80f6d577326465a04219c48541b5f3ea556a06ca61a425ed2400"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "02000000015eb359ccfcd67c3e6b10bb937a796807007708c1f413840d0e627a3f94a1a48401000000484730440220043fc85a43e918ac41e494e309fdf204ca245d260cb5ea09108b196ca65d8a09022056f852f0f521e79ab2124d7e9f779c7290329ce5628ef8e92601980b065d3eb501fdffffff017f9e010000000000160014a709434b13a510e6db68bdd672062c70a2f39d3a26ed2400"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]
pubkey = list(script_GetOp(funding_tx.outputs()[1].scriptpubkey))[0][1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))
def test_verifysig_spending_uncompressed_p2pkh(self):
funding_tx_raw = "02000000000101c6a49fbd701f1526c8e43025a6dda8dd235b3593cfd38af040cba3e37b474fdb0e00000000fdffffff020e640300000000001976a914f1b02b7028fb81aefbb25809a2baf8d94d0c2ba288acb9e3080000000000160014c2eee75efe6621be177f7edd8198f671d1640c2602473044022072b8a6154590704063c377af451b4d69f76cc9064085d4a0c80f08625c57628802207844164839d93ce54ce7db092bbd809d5270142b5dedc823e95400e8bdae88c6012102b6ad13f48fd679a209b7d822376550e5e694a3a2862546ceb72c4012977eac4829ed2400"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "02000000010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca715000000008a47304402206d2dae571ca2f51e0d4a8ce6a6335fa25ac09f4bbed26439124d93f035bdbb130220249dc2039f1da338a40679f0e79c25a2dc2983688e6c04753348f2aa8435e375014104b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b2987b4c862d5b687bb5328adccc69e67a17b109b6328228695a1c384573acd6199fdffffff0186500300000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071f2aed2400"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]
pubkey = list(script_GetOp(spending_tx.inputs()[0].script_sig))[1][1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))
def test_verifysig_spending_compressed_p2pkh(self):
funding_tx_raw = "020000000001010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca7150100000000fdffffff0240e20100000000001976a914f1d49f51f9b58c4805431c303d12d3dcf51ae54188ace9000700000000001600145bdb04f2d096ee48b8b350c85481392ab47c01e70247304402200a72a4599cb27f16011cd67e2951733d6775cbd008506eacb2c20d69db3f531702204c944ec09224a347481c9eea78cac79b77b194b19dfef01b1e3b428010a82570012102fc38612ca7cc42d05a7089f1a6ec3900535604bd779f83c7817aae7bfd907dbd2aed2400"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "02000000016717835a2e1e152a69e7528a0f1346c1d37ee6e76c5e23b5d1c5a5b40241768a000000006a473044022038ad38003943bfd3ed39ba4340d545753fcad632a8fe882d01e4f0140ddb3cfb022019498260e29f5fbbcde9176bfb3553b7acec5fe284a9a3a33547a2d082b60355012103b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b29fdffffff0158de010000000000160014f1d49f51f9b58c4805431c303d12d3dcf51ae5412aed2400"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig = list(script_GetOp(spending_tx.inputs()[0].script_sig))[0][1]
pubkey = list(script_GetOp(spending_tx.inputs()[0].script_sig))[1][1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))
def test_verifysig_spending_p2wpkh_p2sh(self):
funding_tx_raw = "020000000001038fc862be3bc8022866cc83b4f2feeaa914b015a3c6644251960baaccc4a5740b0000000000fdffffff7bfd61e391034e28848fae269183f1c5929e26befd5b2d798cf12c91d4d00dbf0100000000fdffffff014764d324e70e7e3e4fa27077bda2d880b3d1545588b75f79deb2855d9f31cb0000000000fdffffff01f04902000000000017a9147d0530db22c8124ff1558269f543dfeedd37131b87024730440220568ae75314f6414ccf2b0bbed522e1b4b1086ed6eb185ba4bc044ba2723c1f3402206c82253797d0f180db38986b46d8ad952829cf25bc31e3ca6ee54665f5a44b3c0121038a466bdcb979b96d70fde84b9ded4aba0c3cd9c0d2d59121fc3555428fd1a4890247304402203ba1b482b0b6ce5c3d29ef21ee8afad641af8381d3b131103c384757922f0c04022072320e260b60fc862669b2ea3dfb663f7f3a0b6babe8d265ac9ebf268e7225c2012103ff0877f34157a3444afbfdd7432032a93187bc1932e1c155d56dd66ef527906c02473044022058b1c1a2a8c1a256d4870b550ba93777a2cce36b89abe3515f024fd4eec48ce4022023e0002193a26064275433e8ade98642d74d58ee4f8e9717a8acca737856a6c401210364e8f5d9c30986931bca1197138d7250a17a0711a223f113b3ccc11ef09efccb2aed2400"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "020000000001011d1725072a6e60687a59b878ecaf940ea0385880613d9d5502102bd78ef97b9a0000000017160014e7a6a58b657f629516cc37ee2863cbabdadb3fd4fdffffff01fc47020000000000160014e7a6a58b657f629516cc37ee2863cbabdadb3fd402473044022048ea4c558fd374f5d5066440a7f4933393cb377802cb949e3039fedf0378a29402204b4a58c591117cc1e37f07b03cc03cc6198dbf547e2bff813e2e2102bd2057e00121029f46ba81b3c6ad84e52841364dc54ca1097d0c30a68fb529766504c4b1c599352aed2400"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig = spending_tx.inputs()[0].witness_elements()[0]
pubkey = spending_tx.inputs()[0].witness_elements()[1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))
def test_verifysig_spending_p2wpkh(self):
funding_tx_raw = "02000000000101208840a3310ae4b88181374b5812f56f5dd56f12574f3bcd8041b48bfadc92cf0000000000fdffffff02fc7f010000000000160014d339efed7cd5d28d31995caf10b8973a9a13c656a08601000000000043410403886197eb13c59721b94a29f9a68a841caedb7782b35121cd81d50d0cc70db3f8955c7a07b08dd6470141b66eedd324406e29d6b6799033314512334461e3f9ac0247304402203328153753e934d7a13215bf58f093f84281d57f8c7d42f3b7704cd714c7b32c02205a502f3f3e4302561ccc93df413be3c78a439ff35b60cea03d19f8804a9a1239012103f41052be701441d1bc8f7cc6a6053d7e7f5e63be212fe5e3687344ddd52e3af525ed2400"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "02000000000101e328aeb4f9dc1b85a2709ce59b0478a15ed9fb5e7f84fb62422f99b8cd6ad7010000000000fdffffff01087e010000000000160014bf08acd69f1b7012c2b91642b352ce3627db89010247304402204993099c4663d92ef4c9a28b3f45a40a6585754fe22ecfdc0a76c43fda7c9d04022006a75e0fd3ad1862d8e81015a71d2a1489ec7a9264e6e63b8fe6bb90c27e799b0121038ca94e7c715152fd89803c2a40a934c7c4035fb87b3cba981cd1e407369cfe312aed2400"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig = spending_tx.inputs()[0].witness_elements()[0]
pubkey = spending_tx.inputs()[0].witness_elements()[1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey, sig=sig))
def test_verifysig_spending_p2sh_multisig_2of3(self):
funding_tx_raw = "010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "01000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb201000000fc004730440220751ee3599e59debb8b2aeef61bb5f574f26379cd961caf382d711a507bc632390220598d53e62557c4a5ab8cfb2f8948f37cca06a861714b55c781baf2c3d7a580b501473044022023b55c679397bdf3a04d545adc6193eabc11b3a28850d3d46049a51a30c6732402205dbfdade5620e9072ae4aa7577c5f0fd294f59a6b0064cc7105093c0fe7a6d24014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefeffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig1 = list(script_GetOp(spending_tx.inputs()[0].script_sig))[1][1]
sig2 = list(script_GetOp(spending_tx.inputs()[0].script_sig))[2][1]
redeem_script = list(script_GetOp(spending_tx.inputs()[0].script_sig))[-1][1]
pubkey1 = list(script_GetOp(redeem_script))[1][1]
pubkey2 = list(script_GetOp(redeem_script))[2][1]
pubkey3 = list(script_GetOp(redeem_script))[3][1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig1))
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey2, sig=sig2))
self.assertFalse(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey3, sig=sig2))
def test_verifysig_spending_p2wsh_p2sh_multisig_2of2(self):
funding_tx_raw = "01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "0100000000010149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e01000000232200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163cfeffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba0123904004730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660147304402203ecf75b0316a449dd31bc549251b687dc904194aa551941bd5e8c67603661bdb02204ed58b3a6b070ec138d2127093bebcc6581495818fa611583e1c81cd9b2cf5ee0147522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae00000000"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig1 = spending_tx.inputs()[0].witness_elements()[1]
sig2 = spending_tx.inputs()[0].witness_elements()[2]
witness_script = spending_tx.inputs()[0].witness_elements()[-1]
pubkey1 = list(script_GetOp(witness_script))[1][1]
pubkey2 = list(script_GetOp(witness_script))[2][1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig1))
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey2, sig=sig2))
self.assertFalse(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig2))
def test_verifysig_spending_p2wsh_multisig_2of3(self):
funding_tx_raw = "01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c11300"
funding_tx = Transaction(funding_tx_raw)
spending_tx_raw = "01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000"
spending_tx = Transaction(spending_tx_raw)
spending_tx.inputs()[0].utxo = funding_tx
sig1 = spending_tx.inputs()[0].witness_elements()[1]
sig2 = spending_tx.inputs()[0].witness_elements()[2]
witness_script = spending_tx.inputs()[0].witness_elements()[-1]
pubkey1 = list(script_GetOp(witness_script))[1][1]
pubkey2 = list(script_GetOp(witness_script))[2][1]
pubkey3 = list(script_GetOp(witness_script))[3][1]
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey1, sig=sig1))
self.assertTrue(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey3, sig=sig2))
self.assertFalse(spending_tx.verify_sig_for_txin(txin_index=0, pubkey_bytes=pubkey2, sig=sig2))
class TestSighashBIP143(ElectrumTestCase): class TestSighashBIP143(ElectrumTestCase):
#These tests are taken from bip143, https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki #These tests are taken from bip143, https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
#Input of transaction #Input of transaction