electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 8e9d6a4c913df9759fc54f0829d8569f223b35aa
parent 200f547a07f77e2edf04cd544a1bf954fb7c9b1c
Author: ghost43 <somber.night@protonmail.com>
Date:   Sat, 24 Oct 2020 23:06:55 +0000

Merge pull request #6685 from SomberNight/202010_bitcoin_script

bitcoin/transaction: construct_script, and clean-ups
Diffstat:
Melectrum/bitcoin.py | 65++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Melectrum/coinchooser.py | 2+-
Melectrum/gui/qt/paytoedit.py | 9++++-----
Melectrum/lnsweep.py | 4++--
Melectrum/lnutil.py | 122++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Melectrum/plugins/keepkey/keepkey.py | 2+-
Melectrum/plugins/ledger/ledger.py | 2+-
Melectrum/plugins/safe_t/safe_t.py | 2+-
Melectrum/submarine_swaps.py | 5+++--
Melectrum/tests/test_transaction.py | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Melectrum/transaction.py | 98++++++++++++++++++++++++++++++++++---------------------------------------------
Melectrum/wallet.py | 4++--
12 files changed, 270 insertions(+), 125 deletions(-)

diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py @@ -24,11 +24,11 @@ # SOFTWARE. import hashlib -from typing import List, Tuple, TYPE_CHECKING, Optional, Union +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 @@ -299,6 +299,38 @@ def add_number_to_script(i: int) -> bytes: return bfh(push_script(script_num_to_hex(i))) +def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: + """Constructs a witness from the given stack items.""" + witness = var_int(len(items)) + for item in items: + if type(item) is int: + 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 @@ -374,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 @@ -424,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 @@ -481,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/coinchooser.py b/electrum/coinchooser.py @@ -120,7 +120,7 @@ class CoinChooserBase(Logger): constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200) def make_Bucket(desc: str, coins: List[PartialTxInput]): - witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins) + witness = any(coin.is_segwit(guess_for_address=True) for coin in coins) # note that we're guessing whether the tx uses segwit based # on this single bucket weight = sum(Transaction.estimated_input_weight(coin, witness) diff --git 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/lnsweep.py b/electrum/lnsweep.py @@ -6,7 +6,7 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Calla from enum import Enum, auto from .util import bfh, bh2u -from .bitcoin import redeem_script_to_address, dust_threshold +from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness from . import ecc from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey, @@ -15,7 +15,7 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, map_htlcs_to_ctx_output_idxs, Direction) -from .transaction import (Transaction, TxOutput, construct_witness, PartialTransaction, PartialTxInput, +from .transaction import (Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) from .simple_config import SimpleConfig from .logging import get_logger, Logger diff --git 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 +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 @@ -475,7 +480,7 @@ def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes, assert type(localhtlcsig) is bytes assert type(payment_preimage) is bytes assert type(witness_script) is bytes - return bfh(transaction.construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script])) + return bfh(construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script])) def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int, amount_msat: int, witness_script: str) -> List[PartialTxInput]: @@ -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/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py @@ -53,7 +53,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): prev_tx = {} for txin in tx.inputs(): tx_hash = txin.prevout.txid.hex() - if txin.utxo is None and not Transaction.is_segwit_input(txin): + if txin.utxo is None and not txin.is_segwit(): raise UserFacingException(_('Missing previous tx for legacy input.')) prev_tx[tx_hash] = txin.utxo diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py @@ -386,7 +386,7 @@ class Ledger_KeyStore(Hardware_KeyStore): redeemScript = Transaction.get_preimage_script(txin) txin_prev_tx = txin.utxo - if txin_prev_tx is None and not Transaction.is_segwit_input(txin): + if txin_prev_tx is None and not txin.is_segwit(): raise UserFacingException(_('Missing previous tx for legacy input.')) txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None inputs.append([txin_prev_tx_raw, diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py @@ -51,7 +51,7 @@ class SafeTKeyStore(Hardware_KeyStore): prev_tx = {} for txin in tx.inputs(): tx_hash = txin.prevout.txid.hex() - if txin.utxo is None and not Transaction.is_segwit_input(txin): + if txin.utxo is None and not txin.is_segwit(): raise UserFacingException(_('Missing previous tx for legacy input.')) prev_tx[tx_hash] = txin.utxo diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py @@ -7,8 +7,9 @@ import attr from .crypto import sha256, hash_160 from .ecc import ECPrivkey -from .bitcoin import address_to_script, script_to_p2wsh, redeem_script_to_address, opcodes, p2wsh_nested_script, push_script, is_segwit_address -from .transaction import TxOutpoint, PartialTxInput, PartialTxOutput, PartialTransaction, construct_witness +from .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script, + is_segwit_address, construct_witness) +from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey from .util import log_exceptions from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address, LN_MAX_HTLC_VALUE_MSAT diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py @@ -1,10 +1,15 @@ from typing import NamedTuple, Union from electrum import transaction, bitcoin -from electrum.transaction import convert_raw_tx_to_hex, tx_from_any, Transaction, PartialTransaction +from electrum.transaction import (convert_raw_tx_to_hex, tx_from_any, Transaction, + PartialTransaction, TxOutpoint, PartialTxInput, + PartialTxOutput) from electrum.util import bh2u, bfh +from electrum.bitcoin import (deserialize_privkey, opcodes, + construct_script, construct_witness) +from electrum.ecc import ECPrivkey -from . import ElectrumTestCase +from . import ElectrumTestCase, TestCaseForTestnet signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700" @@ -840,3 +845,74 @@ class TestTransaction(ElectrumTestCase): self._run_naive_tests_on_tx(raw_tx, txid) # txns from Bitcoin Core ends <--- + + +class TestTransactionTestnet(TestCaseForTestnet): + + def test_spending_op_cltv_p2sh(self): + # from https://github.com/brianddk/reddit/blob/8ca383c9e00cb5a4c1201d1bab534d5886d3cb8f/python/elec-p2sh-hodl.py + wif = 'cQNjiPwYKMBr2oB3bWzf3rgBsu198xb8Nxxe51k6D3zVTA98L25N' + sats = 9999 + sats_less_fees = sats - 200 + locktime = 1602565200 + + # Build the Transaction Input + _, privkey, compressed = deserialize_privkey(wif) + pubkey = ECPrivkey(privkey).get_public_key_hex(compressed=compressed) + prevout = TxOutpoint(txid=bfh('6d500966f9e494b38a04545f0cea35fc7b3944e341a64b804fed71cdee11d434'), out_idx=1) + txin = PartialTxInput(prevout=prevout) + txin.nsequence = 2 ** 32 - 3 + txin.script_type = 'p2sh' + redeem_script = bfh(construct_script([ + locktime, opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, pubkey, opcodes.OP_CHECKSIG, + ])) + txin.redeem_script = redeem_script + + # Build the Transaction Output + txout = PartialTxOutput.from_address_and_value( + 'tb1qv9hg20f0g08d460l67ph6p4ukwt7m0ttqzj7mk', sats_less_fees) + + # Build and sign the transaction + tx = PartialTransaction.from_io([txin], [txout], locktime=locktime, version=1) + sig = tx.sign_txin(0, privkey) + txin.script_sig = bfh(construct_script([sig, redeem_script])) + + # note: in testnet3 chain, signature differs (no low-R grinding), + # so txid there is: a8110bbdd40d65351f615897d98c33cbe33e4ebedb4ba2fc9e8c644423dadc93 + self.assertEqual('3266138b0b79007f35ac9a1824e294763708bd4a6440b5c227f4e1251b66e92b', + tx.txid()) + + def test_spending_op_cltv_p2wsh(self): + wif = 'cSw3py1CQa2tmzzDm3ghQVrgqqNuFhUyBXjABge5j8KRxzd6kaFj' + sats = 99_878 + sats_less_fees = sats - 300 + locktime = 1602572140 + + # Build the Transaction Input + _, privkey, compressed = deserialize_privkey(wif) + pubkey = ECPrivkey(privkey).get_public_key_hex(compressed=compressed) + witness_script = bfh(construct_script([ + locktime, opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, pubkey, opcodes.OP_CHECKSIG, + ])) + from_addr = bitcoin.script_to_p2wsh(witness_script.hex()) + self.assertEqual("tb1q9dn6qke9924xe3zmptmhrdge0s043pjxpjndypgnu2t9fvsd4crs2qjuer", from_addr) + prevout = TxOutpoint(txid=bfh('8680971efd5203025cffe746f8598d0a704fae81f236ffe009c2609ec673d59a'), out_idx=0) + txin = PartialTxInput(prevout=prevout) + txin._trusted_value_sats = sats + txin.nsequence = 0 + txin.script_sig = b'' + txin.witness_script = witness_script + + # Build the Transaction Output + txout = PartialTxOutput.from_address_and_value( + 'tb1qtgsfkgptcxdn6dz6wh8c4dguk3cezwne5j5c47', sats_less_fees) + + # Build and sign the transaction + tx = PartialTransaction.from_io([txin], [txout], locktime=locktime, version=2) + sig = tx.sign_txin(0, privkey) + txin.witness = bfh(construct_witness([sig, witness_script])) + + self.assertEqual('1cdb274755b144090c7134b6459e8d4cb6b4552fe620102836d751e8389b2694', + tx.txid()) + self.assertEqual('020000000001019ad573c69e60c209e0ff36f281ae4f700a8d59f846e7ff5c020352fd1e97808600000000000000000001fa840100000000001600145a209b202bc19b3d345a75cf8ab51cb471913a790247304402207b191c1e3ff1a2d3541770b496c9f871406114746b3aa7347ec4ef0423d3a975022043d3a746fa7a794d97e95d74b6d17d618dfc4cd7644476813e08006f271e51bd012a046c4f855fb1752102aec53aa5f347219a7378b13006eb16ce48125f9cf14f04a5509a565ad5e51507ac6c4f855f', + tx.serialize()) diff --git 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) + base_encode, construct_witness, construct_script) from .crypto import sha256d from .logging import get_logger @@ -243,6 +243,11 @@ class TxInput: n = vds.read_compact_size() return list(vds.read_bytes(vds.read_compact_size()) for i in range(n)) + def is_segwit(self, *, guess_for_address=False) -> bool: + if self.witness not in (b'\x00', b'', None): + return True + return False + class BCDataStream(object): """Workalike python implementation of Bitcoin's CDataStream class.""" @@ -479,18 +484,6 @@ def parse_input(vds: BCDataStream) -> TxInput: return TxInput(prevout=prevout, script_sig=script_sig, nsequence=nsequence) -def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: - """Constructs a witness from the given stack items.""" - witness = var_int(len(items)) - for item in items: - if type(item) is int: - item = bitcoin.script_num_to_hex(item) - elif isinstance(item, (bytes, bytearray)): - item = bh2u(item) - witness += bitcoin.witness_push(item) - return witness - - def parse_witness(vds: BCDataStream, txin: TxInput) -> None: n = vds.read_compact_size() witness_elements = list(vds.read_bytes(vds.read_compact_size()) for i in range(n)) @@ -512,10 +505,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]) @@ -650,8 +640,8 @@ class Transaction: assert isinstance(txin, PartialTxInput) _type = txin.script_type - if not cls.is_segwit_input(txin): - return '00' + if not txin.is_segwit(): + return construct_witness([]) if _type in ('address', 'unknown') and estimate_size: _type = cls.guess_txintype_from_address(txin.address) @@ -660,29 +650,12 @@ 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 - def is_segwit_input(cls, txin: 'TxInput', *, guess_for_address=False) -> bool: - if txin.witness not in (b'\x00', b'', None): - return True - if not isinstance(txin, PartialTxInput): - return False - if txin.is_native_segwit() or txin.is_p2sh_segwit(): - return True - if txin.is_native_segwit() is False and txin.is_p2sh_segwit() is False: - return False - if txin.witness_script: - return True - _type = txin.script_type - if _type == 'address' and guess_for_address: - _type = cls.guess_txintype_from_address(txin.address) - return is_segwit_script_type(_type) - - @classmethod def guess_txintype_from_address(cls, addr: Optional[str]) -> str: # It's not possible to tell the script type in general # just from an address. @@ -714,47 +687,46 @@ 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 def get_preimage_script(cls, txin: 'PartialTxInput') -> str: if txin.witness_script: - opcodes_in_witness_script = [x[0] for x in script_GetOp(txin.witness_script)] - if opcodes.OP_CODESEPARATOR in opcodes_in_witness_script: + if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(txin.witness_script)]: raise Exception('OP_CODESEPARATOR black magic is not supported') return txin.witness_script.hex() + if not txin.is_segwit() and txin.redeem_script: + if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(txin.redeem_script)]: + raise Exception('OP_CODESEPARATOR black magic is not supported') + return txin.redeem_script.hex() pubkeys = [pk.hex() for pk in txin.pubkeys] if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: @@ -790,7 +762,7 @@ class Transaction: hashOutputs=hashOutputs) def is_segwit(self, *, guess_for_address=False): - return any(self.is_segwit_input(txin, guess_for_address=guess_for_address) + return any(txin.is_segwit(guess_for_address=guess_for_address) for txin in self.inputs()) def invalidate_ser_cache(self): @@ -848,7 +820,7 @@ class Transaction: def txid(self) -> Optional[str]: if self._cached_txid is None: self.deserialize() - all_segwit = all(self.is_segwit_input(x) for x in self.inputs()) + all_segwit = all(txin.is_segwit() for txin in self.inputs()) if not all_segwit and not self.is_complete(): return None try: @@ -892,7 +864,7 @@ class Transaction: script = cls.input_script(txin, estimate_size=True) input_size = len(cls.serialize_input(txin, script)) // 2 - if cls.is_segwit_input(txin, guess_for_address=True): + if txin.is_segwit(guess_for_address=True): witness_size = len(cls.serialize_witness(txin, estimate_size=True)) // 2 else: witness_size = 1 if is_segwit_tx else 0 @@ -1219,7 +1191,7 @@ class PartialTxInput(TxInput, PSBTSection): # without verifying the input amount. This means, given a maliciously modified PSBT, # for non-segwit inputs, we might end up burning coins as miner fees. if for_signing and False: - if not Transaction.is_segwit_input(self) and self.witness_utxo: + if not self.is_segwit() and self.witness_utxo: raise PSBTInputConsistencyFailure(f"PSBT input validation: " f"If a witness UTXO is provided, no non-witness signature may be created") if self.redeem_script and self.address: @@ -1359,7 +1331,7 @@ class PartialTxInput(TxInput, PSBTSection): return True if self.is_coinbase_input(): return True - if self.script_sig is not None and not Transaction.is_segwit_input(self): + if self.script_sig is not None and not self.is_segwit(): return True signatures = list(self.part_sigs.values()) s = len(signatures) @@ -1461,6 +1433,20 @@ class PartialTxInput(TxInput, PSBTSection): self._is_p2sh_segwit = calc_if_p2sh_segwit_now() return self._is_p2sh_segwit + def is_segwit(self, *, guess_for_address=False) -> bool: + if super().is_segwit(): + return True + if self.is_native_segwit() or self.is_p2sh_segwit(): + return True + if self.is_native_segwit() is False and self.is_p2sh_segwit() is False: + return False + if self.witness_script: + return True + _type = self.script_type + if _type == 'address' and guess_for_address: + _type = Transaction.guess_txintype_from_address(self.address) + return is_segwit_script_type(_type) + def already_has_some_signatures(self) -> bool: """Returns whether progress has been made towards completing this input.""" return (self.part_sigs @@ -1809,7 +1795,7 @@ class PartialTransaction(Transaction): raise Exception("only SIGHASH_ALL signing is supported!") nHashType = int_to_hex(sighash, 4) preimage_script = self.get_preimage_script(txin) - if self.is_segwit_input(txin): + if txin.is_segwit(): if bip143_shared_txdigest_fields is None: bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -2162,7 +2162,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if all([txin.utxo for txin in tx.inputs()]): return None # a single segwit input -> fine - if len(tx.inputs()) == 1 and Transaction.is_segwit_input(tx.inputs()[0]) and tx.inputs()[0].witness_utxo: + if len(tx.inputs()) == 1 and tx.inputs()[0].is_segwit() and tx.inputs()[0].witness_utxo: return None # coinjoin or similar if any([not self.is_mine(txin.address) for txin in tx.inputs()]): @@ -2170,7 +2170,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): + _("The input amounts could not be verified as the previous transactions are missing.\n" "The amount of money being spent CANNOT be verified.")) # some inputs are legacy - if any([not Transaction.is_segwit_input(txin) for txin in tx.inputs()]): + if any([not txin.is_segwit() for txin in tx.inputs()]): return (_("Warning") + ": " + _("The fee could not be verified. Signing non-segwit inputs is risky:\n" "if this transaction was maliciously modified before you sign,\n"