diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 7732c92dc..cca69b5ae 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -28,7 +28,7 @@ from typing import List, Tuple, TYPE_CHECKING, Optional, Union, Sequence import enum from enum import IntEnum, Enum -from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict +from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict, is_hex_str from . import version from . import segwit_addr from . import constants @@ -307,10 +307,30 @@ def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: item = script_num_to_hex(item) elif isinstance(item, (bytes, bytearray)): item = bh2u(item) + else: + assert is_hex_str(item) witness += witness_push(item) return witness +def construct_script(items: Sequence[Union[str, int, bytes, opcodes]]) -> str: + """Constructs bitcoin script from given items.""" + script = '' + for item in items: + if isinstance(item, opcodes): + script += item.hex() + elif type(item) is int: + script += add_number_to_script(item).hex() + elif isinstance(item, (bytes, bytearray)): + script += push_script(item.hex()) + elif isinstance(item, str): + assert is_hex_str(item) + script += push_script(item) + else: + raise Exception(f'unexpected item for script: {item!r}') + return script + + def relayfee(network: 'Network' = None) -> int: """Returns feerate in sat/kbyte.""" from .simple_config import FEERATE_DEFAULT_RELAY, FEERATE_MAX_RELAY @@ -386,12 +406,12 @@ def script_to_p2wsh(script: str, *, net=None) -> str: return hash_to_segwit_addr(sha256(bfh(script)), witver=0, net=net) def p2wpkh_nested_script(pubkey: str) -> str: - pkh = bh2u(hash_160(bfh(pubkey))) - return '00' + push_script(pkh) + pkh = hash_160(bfh(pubkey)) + return construct_script([0, pkh]) def p2wsh_nested_script(witness_script: str) -> str: - wsh = bh2u(sha256(bfh(witness_script))) - return '00' + push_script(wsh) + wsh = sha256(bfh(witness_script)) + return construct_script([0, wsh]) def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: if net is None: net = constants.net @@ -436,16 +456,12 @@ def address_to_script(addr: str, *, net=None) -> str: if witprog is not None: if not (0 <= witver <= 16): raise BitcoinException(f'impossible witness version: {witver}') - script = bh2u(add_number_to_script(witver)) - script += push_script(bh2u(bytes(witprog))) - return script + return construct_script([witver, bytes(witprog)]) addrtype, hash_160_ = b58_address_to_hash160(addr) if addrtype == net.ADDRTYPE_P2PKH: script = pubkeyhash_to_p2pkh_script(bh2u(hash_160_)) elif addrtype == net.ADDRTYPE_P2SH: - script = opcodes.OP_HASH160.hex() - script += push_script(bh2u(hash_160_)) - script += opcodes.OP_EQUAL.hex() + script = construct_script([opcodes.OP_HASH160, hash_160_, opcodes.OP_EQUAL]) else: raise BitcoinException(f'unknown address type: {addrtype}') return script @@ -493,13 +509,16 @@ def script_to_scripthash(script: str) -> str: return bh2u(bytes(reversed(h))) def public_key_to_p2pk_script(pubkey: str) -> str: - return push_script(pubkey) + opcodes.OP_CHECKSIG.hex() + return construct_script([pubkey, opcodes.OP_CHECKSIG]) def pubkeyhash_to_p2pkh_script(pubkey_hash160: str) -> str: - script = bytes([opcodes.OP_DUP, opcodes.OP_HASH160]).hex() - script += push_script(pubkey_hash160) - script += bytes([opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]).hex() - return script + return construct_script([ + opcodes.OP_DUP, + opcodes.OP_HASH160, + pubkey_hash160, + opcodes.OP_EQUALVERIFY, + opcodes.OP_CHECKSIG + ]) __b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 890156ad6..4dac3e804 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -31,8 +31,8 @@ from PyQt5.QtGui import QFontMetrics, QFont from electrum import bitcoin from electrum.util import bfh, maybe_extract_bolt11_invoice -from electrum.transaction import push_script, PartialTxOutput -from electrum.bitcoin import opcodes +from electrum.transaction import PartialTxOutput +from electrum.bitcoin import opcodes, construct_script from electrum.logging import Logger from electrum.lnaddr import LnDecodeException @@ -111,11 +111,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): for word in x.split(): if word[0:3] == 'OP_': opcode_int = opcodes[word] - assert opcode_int < 256 # opcode is single-byte - script += bitcoin.int_to_hex(opcode_int) + script += construct_script([opcode_int]) else: bfh(word) # to test it is hex data - script += push_script(word) + script += construct_script([word]) return script def parse_amount(self, x): diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 4d1469416..e51a91e73 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -19,7 +19,8 @@ from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOut PartialTxOutput, opcodes, TxOutput) from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number from . import ecc, bitcoin, crypto, transaction -from .bitcoin import push_script, redeem_script_to_address, address_to_script, construct_witness +from .bitcoin import (push_script, redeem_script_to_address, address_to_script, + construct_witness, construct_script) from . import segwit_addr from .i18n import _ from .lnaddr import lndecode @@ -452,13 +453,17 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela assert type(local_feerate) is int assert type(revocationpubkey) is bytes assert type(local_delayedpubkey) is bytes - script = bytes([opcodes.OP_IF]) \ - + bfh(push_script(bh2u(revocationpubkey))) \ - + bytes([opcodes.OP_ELSE]) \ - + bitcoin.add_number_to_script(to_self_delay) \ - + bytes([opcodes.OP_CHECKSEQUENCEVERIFY, opcodes.OP_DROP]) \ - + bfh(push_script(bh2u(local_delayedpubkey))) \ - + bytes([opcodes.OP_ENDIF, opcodes.OP_CHECKSIG]) + script = bfh(construct_script([ + opcodes.OP_IF, + revocationpubkey, + opcodes.OP_ELSE, + to_self_delay, + opcodes.OP_CHECKSEQUENCEVERIFY, + opcodes.OP_DROP, + local_delayedpubkey, + opcodes.OP_ENDIF, + opcodes.OP_CHECKSIG, + ])) p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) weight = HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT @@ -503,13 +508,35 @@ def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, assert type(remote_htlcpubkey) is bytes assert type(local_htlcpubkey) is bytes assert type(payment_hash) is bytes - return bytes([opcodes.OP_DUP, opcodes.OP_HASH160]) + bfh(push_script(bh2u(bitcoin.hash_160(revocation_pubkey))))\ - + bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_CHECKSIG, opcodes.OP_ELSE]) \ - + bfh(push_script(bh2u(remote_htlcpubkey)))\ - + bytes([opcodes.OP_SWAP, opcodes.OP_SIZE]) + bitcoin.add_number_to_script(32) + bytes([opcodes.OP_EQUAL, opcodes.OP_NOTIF, opcodes.OP_DROP])\ - + bitcoin.add_number_to_script(2) + bytes([opcodes.OP_SWAP]) + bfh(push_script(bh2u(local_htlcpubkey))) + bitcoin.add_number_to_script(2)\ - + bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_HASH160])\ - + bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) + bytes([opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF]) + script = bfh(construct_script([ + opcodes.OP_DUP, + opcodes.OP_HASH160, + bitcoin.hash_160(revocation_pubkey), + opcodes.OP_EQUAL, + opcodes.OP_IF, + opcodes.OP_CHECKSIG, + opcodes.OP_ELSE, + remote_htlcpubkey, + opcodes.OP_SWAP, + opcodes.OP_SIZE, + 32, + opcodes.OP_EQUAL, + opcodes.OP_NOTIF, + opcodes.OP_DROP, + 2, + opcodes.OP_SWAP, + local_htlcpubkey, + 2, + opcodes.OP_CHECKMULTISIG, + opcodes.OP_ELSE, + opcodes.OP_HASH160, + crypto.ripemd(payment_hash), + opcodes.OP_EQUALVERIFY, + opcodes.OP_CHECKSIG, + opcodes.OP_ENDIF, + opcodes.OP_ENDIF, + ])) + return script def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> bytes: @@ -517,22 +544,38 @@ def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, assert type(i) is bytes assert type(cltv_expiry) is int - return bytes([opcodes.OP_DUP, opcodes.OP_HASH160]) \ - + bfh(push_script(bh2u(bitcoin.hash_160(revocation_pubkey)))) \ - + bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_CHECKSIG, opcodes.OP_ELSE]) \ - + bfh(push_script(bh2u(remote_htlcpubkey))) \ - + bytes([opcodes.OP_SWAP, opcodes.OP_SIZE]) \ - + bitcoin.add_number_to_script(32) \ - + bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_HASH160]) \ - + bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) \ - + bytes([opcodes.OP_EQUALVERIFY]) \ - + bitcoin.add_number_to_script(2) \ - + bytes([opcodes.OP_SWAP]) \ - + bfh(push_script(bh2u(local_htlcpubkey))) \ - + bitcoin.add_number_to_script(2) \ - + bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_DROP]) \ - + bitcoin.add_number_to_script(cltv_expiry) \ - + bytes([opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF]) + script = bfh(construct_script([ + opcodes.OP_DUP, + opcodes.OP_HASH160, + bitcoin.hash_160(revocation_pubkey), + opcodes.OP_EQUAL, + opcodes.OP_IF, + opcodes.OP_CHECKSIG, + opcodes.OP_ELSE, + remote_htlcpubkey, + opcodes.OP_SWAP, + opcodes.OP_SIZE, + 32, + opcodes.OP_EQUAL, + opcodes.OP_IF, + opcodes.OP_HASH160, + crypto.ripemd(payment_hash), + opcodes.OP_EQUALVERIFY, + 2, + opcodes.OP_SWAP, + local_htlcpubkey, + 2, + opcodes.OP_CHECKMULTISIG, + opcodes.OP_ELSE, + opcodes.OP_DROP, + cltv_expiry, + opcodes.OP_CHECKLOCKTIMEVERIFY, + opcodes.OP_DROP, + opcodes.OP_CHECKSIG, + opcodes.OP_ENDIF, + opcodes.OP_ENDIF, + ])) + return script def make_htlc_output_witness_script(is_received_htlc: bool, remote_revocation_pubkey: bytes, remote_htlc_pubkey: bytes, local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes: @@ -796,9 +839,18 @@ def make_commitment( def make_commitment_output_to_local_witness_script( revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> bytes: - local_script = bytes([opcodes.OP_IF]) + bfh(push_script(bh2u(revocation_pubkey))) + bytes([opcodes.OP_ELSE]) + bitcoin.add_number_to_script(to_self_delay) \ - + bytes([opcodes.OP_CHECKSEQUENCEVERIFY, opcodes.OP_DROP]) + bfh(push_script(bh2u(delayed_pubkey))) + bytes([opcodes.OP_ENDIF, opcodes.OP_CHECKSIG]) - return local_script + script = bfh(construct_script([ + opcodes.OP_IF, + revocation_pubkey, + opcodes.OP_ELSE, + to_self_delay, + opcodes.OP_CHECKSEQUENCEVERIFY, + opcodes.OP_DROP, + delayed_pubkey, + opcodes.OP_ENDIF, + opcodes.OP_CHECKSIG, + ])) + return script def make_commitment_output_to_local_address( revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> str: diff --git a/electrum/transaction.py b/electrum/transaction.py index bc48dbfcb..aca0a61ad 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -48,7 +48,7 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, int_to_hex, push_script, b58_address_to_hash160, opcodes, add_number_to_script, base_decode, is_segwit_script_type, - base_encode, construct_witness) + base_encode, construct_witness, construct_script) from .crypto import sha256d from .logging import get_logger @@ -500,10 +500,7 @@ def parse_output(vds: BCDataStream) -> TxOutput: def multisig_script(public_keys: Sequence[str], m: int) -> str: n = len(public_keys) assert 1 <= m <= n <= 15, f'm {m}, n {n}' - op_m = bh2u(add_number_to_script(m)) - op_n = bh2u(add_number_to_script(n)) - keylist = [push_script(k) for k in public_keys] - return op_m + ''.join(keylist) + op_n + opcodes.OP_CHECKMULTISIG.hex() + return construct_script([m, *public_keys, n, opcodes.OP_CHECKMULTISIG]) @@ -639,7 +636,7 @@ class Transaction: _type = txin.script_type if not cls.is_segwit_input(txin): - return '00' + return construct_witness([]) if _type in ('address', 'unknown') and estimate_size: _type = cls.guess_txintype_from_address(txin.address) @@ -648,9 +645,9 @@ class Transaction: return construct_witness([sig_list[0], pubkeys[0]]) elif _type in ['p2wsh', 'p2wsh-p2sh']: witness_script = multisig_script(pubkeys, txin.num_sig) - return construct_witness([0] + sig_list + [witness_script]) + return construct_witness([0, *sig_list, witness_script]) elif _type in ['p2pk', 'p2pkh', 'p2sh']: - return '00' + return construct_witness([]) raise UnknownTxinType(f'cannot construct witness for txin_type: {_type}') @classmethod @@ -702,38 +699,34 @@ class Transaction: assert isinstance(txin, PartialTxInput) if txin.is_p2sh_segwit() and txin.redeem_script: - return push_script(txin.redeem_script.hex()) + return construct_script([txin.redeem_script]) if txin.is_native_segwit(): return '' _type = txin.script_type pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size) - script = ''.join(push_script(x) for x in sig_list) if _type in ('address', 'unknown') and estimate_size: _type = self.guess_txintype_from_address(txin.address) if _type == 'p2pk': - return script + return construct_script([sig_list[0]]) elif _type == 'p2sh': # put op_0 before script - script = '00' + script redeem_script = multisig_script(pubkeys, txin.num_sig) - script += push_script(redeem_script) - return script + return construct_script([0, *sig_list, redeem_script]) elif _type == 'p2pkh': - script += push_script(pubkeys[0]) - return script + return construct_script([sig_list[0], pubkeys[0]]) elif _type in ['p2wpkh', 'p2wsh']: return '' elif _type == 'p2wpkh-p2sh': redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0]) - return push_script(redeem_script) + return construct_script([redeem_script]) elif _type == 'p2wsh-p2sh': if estimate_size: witness_script = '' else: witness_script = self.get_preimage_script(txin) redeem_script = bitcoin.p2wsh_nested_script(witness_script) - return push_script(redeem_script) + return construct_script([redeem_script]) raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}') @classmethod