Browse Source

Merge pull request #6685 from SomberNight/202010_bitcoin_script

bitcoin/transaction: construct_script, and clean-ups
patch-4
ghost43 5 years ago
committed by GitHub
parent
commit
8e9d6a4c91
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      electrum/bitcoin.py
  2. 2
      electrum/coinchooser.py
  3. 9
      electrum/gui/qt/paytoedit.py
  4. 4
      electrum/lnsweep.py
  5. 122
      electrum/lnutil.py
  6. 2
      electrum/plugins/keepkey/keepkey.py
  7. 2
      electrum/plugins/ledger/ledger.py
  8. 2
      electrum/plugins/safe_t/safe_t.py
  9. 5
      electrum/submarine_swaps.py
  10. 80
      electrum/tests/test_transaction.py
  11. 98
      electrum/transaction.py
  12. 4
      electrum/wallet.py

65
electrum/bitcoin.py

@ -24,11 +24,11 @@
# SOFTWARE. # SOFTWARE.
import hashlib import hashlib
from typing import List, Tuple, TYPE_CHECKING, Optional, Union from typing import List, Tuple, TYPE_CHECKING, Optional, Union, Sequence
import enum import enum
from enum import IntEnum, 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 version
from . import segwit_addr from . import segwit_addr
from . import constants from . import constants
@ -299,6 +299,38 @@ def add_number_to_script(i: int) -> bytes:
return bfh(push_script(script_num_to_hex(i))) 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: def relayfee(network: 'Network' = None) -> int:
"""Returns feerate in sat/kbyte.""" """Returns feerate in sat/kbyte."""
from .simple_config import FEERATE_DEFAULT_RELAY, FEERATE_MAX_RELAY 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) return hash_to_segwit_addr(sha256(bfh(script)), witver=0, net=net)
def p2wpkh_nested_script(pubkey: str) -> str: def p2wpkh_nested_script(pubkey: str) -> str:
pkh = bh2u(hash_160(bfh(pubkey))) pkh = hash_160(bfh(pubkey))
return '00' + push_script(pkh) return construct_script([0, pkh])
def p2wsh_nested_script(witness_script: str) -> str: def p2wsh_nested_script(witness_script: str) -> str:
wsh = bh2u(sha256(bfh(witness_script))) wsh = sha256(bfh(witness_script))
return '00' + push_script(wsh) return construct_script([0, wsh])
def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
if net is None: net = constants.net 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 witprog is not None:
if not (0 <= witver <= 16): if not (0 <= witver <= 16):
raise BitcoinException(f'impossible witness version: {witver}') raise BitcoinException(f'impossible witness version: {witver}')
script = bh2u(add_number_to_script(witver)) return construct_script([witver, bytes(witprog)])
script += push_script(bh2u(bytes(witprog)))
return script
addrtype, hash_160_ = b58_address_to_hash160(addr) addrtype, hash_160_ = b58_address_to_hash160(addr)
if addrtype == net.ADDRTYPE_P2PKH: if addrtype == net.ADDRTYPE_P2PKH:
script = pubkeyhash_to_p2pkh_script(bh2u(hash_160_)) script = pubkeyhash_to_p2pkh_script(bh2u(hash_160_))
elif addrtype == net.ADDRTYPE_P2SH: elif addrtype == net.ADDRTYPE_P2SH:
script = opcodes.OP_HASH160.hex() script = construct_script([opcodes.OP_HASH160, hash_160_, opcodes.OP_EQUAL])
script += push_script(bh2u(hash_160_))
script += opcodes.OP_EQUAL.hex()
else: else:
raise BitcoinException(f'unknown address type: {addrtype}') raise BitcoinException(f'unknown address type: {addrtype}')
return script return script
@ -481,13 +509,16 @@ def script_to_scripthash(script: str) -> str:
return bh2u(bytes(reversed(h))) return bh2u(bytes(reversed(h)))
def public_key_to_p2pk_script(pubkey: str) -> str: 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: def pubkeyhash_to_p2pkh_script(pubkey_hash160: str) -> str:
script = bytes([opcodes.OP_DUP, opcodes.OP_HASH160]).hex() return construct_script([
script += push_script(pubkey_hash160) opcodes.OP_DUP,
script += bytes([opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]).hex() opcodes.OP_HASH160,
return script pubkey_hash160,
opcodes.OP_EQUALVERIFY,
opcodes.OP_CHECKSIG
])
__b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' __b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

2
electrum/coinchooser.py

@ -120,7 +120,7 @@ class CoinChooserBase(Logger):
constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200) constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200)
def make_Bucket(desc: str, coins: List[PartialTxInput]): 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 # note that we're guessing whether the tx uses segwit based
# on this single bucket # on this single bucket
weight = sum(Transaction.estimated_input_weight(coin, witness) weight = sum(Transaction.estimated_input_weight(coin, witness)

9
electrum/gui/qt/paytoedit.py

@ -31,8 +31,8 @@ from PyQt5.QtGui import QFontMetrics, QFont
from electrum import bitcoin from electrum import bitcoin
from electrum.util import bfh, maybe_extract_bolt11_invoice from electrum.util import bfh, maybe_extract_bolt11_invoice
from electrum.transaction import push_script, PartialTxOutput from electrum.transaction import PartialTxOutput
from electrum.bitcoin import opcodes from electrum.bitcoin import opcodes, construct_script
from electrum.logging import Logger from electrum.logging import Logger
from electrum.lnaddr import LnDecodeException from electrum.lnaddr import LnDecodeException
@ -111,11 +111,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
for word in x.split(): for word in x.split():
if word[0:3] == 'OP_': if word[0:3] == 'OP_':
opcode_int = opcodes[word] opcode_int = opcodes[word]
assert opcode_int < 256 # opcode is single-byte script += construct_script([opcode_int])
script += bitcoin.int_to_hex(opcode_int)
else: else:
bfh(word) # to test it is hex data bfh(word) # to test it is hex data
script += push_script(word) script += construct_script([word])
return script return script
def parse_amount(self, x): def parse_amount(self, x):

4
electrum/lnsweep.py

@ -6,7 +6,7 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Calla
from enum import Enum, auto from enum import Enum, auto
from .util import bfh, bh2u 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 . import ecc
from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script, 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, 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, get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
map_htlcs_to_ctx_output_idxs, Direction) 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) PartialTxOutput, TxOutpoint)
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
from .logging import get_logger, Logger from .logging import get_logger, Logger

122
electrum/lnutil.py

@ -19,7 +19,8 @@ from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOut
PartialTxOutput, opcodes, TxOutput) PartialTxOutput, opcodes, TxOutput)
from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
from . import ecc, bitcoin, crypto, transaction 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 . import segwit_addr
from .i18n import _ from .i18n import _
from .lnaddr import lndecode 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(local_feerate) is int
assert type(revocationpubkey) is bytes assert type(revocationpubkey) is bytes
assert type(local_delayedpubkey) is bytes assert type(local_delayedpubkey) is bytes
script = bytes([opcodes.OP_IF]) \ script = bfh(construct_script([
+ bfh(push_script(bh2u(revocationpubkey))) \ opcodes.OP_IF,
+ bytes([opcodes.OP_ELSE]) \ revocationpubkey,
+ bitcoin.add_number_to_script(to_self_delay) \ opcodes.OP_ELSE,
+ bytes([opcodes.OP_CHECKSEQUENCEVERIFY, opcodes.OP_DROP]) \ to_self_delay,
+ bfh(push_script(bh2u(local_delayedpubkey))) \ opcodes.OP_CHECKSEQUENCEVERIFY,
+ bytes([opcodes.OP_ENDIF, opcodes.OP_CHECKSIG]) opcodes.OP_DROP,
local_delayedpubkey,
opcodes.OP_ENDIF,
opcodes.OP_CHECKSIG,
]))
p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
weight = HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT 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(localhtlcsig) is bytes
assert type(payment_preimage) is bytes assert type(payment_preimage) is bytes
assert type(witness_script) 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, def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int,
amount_msat: int, witness_script: str) -> List[PartialTxInput]: 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(remote_htlcpubkey) is bytes
assert type(local_htlcpubkey) is bytes assert type(local_htlcpubkey) is bytes
assert type(payment_hash) 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))))\ script = bfh(construct_script([
+ bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_CHECKSIG, opcodes.OP_ELSE]) \ opcodes.OP_DUP,
+ bfh(push_script(bh2u(remote_htlcpubkey)))\ opcodes.OP_HASH160,
+ bytes([opcodes.OP_SWAP, opcodes.OP_SIZE]) + bitcoin.add_number_to_script(32) + bytes([opcodes.OP_EQUAL, opcodes.OP_NOTIF, opcodes.OP_DROP])\ bitcoin.hash_160(revocation_pubkey),
+ bitcoin.add_number_to_script(2) + bytes([opcodes.OP_SWAP]) + bfh(push_script(bh2u(local_htlcpubkey))) + bitcoin.add_number_to_script(2)\ opcodes.OP_EQUAL,
+ bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_HASH160])\ opcodes.OP_IF,
+ bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) + bytes([opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF]) 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, def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> 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(i) is bytes
assert type(cltv_expiry) is int assert type(cltv_expiry) is int
return bytes([opcodes.OP_DUP, opcodes.OP_HASH160]) \ script = bfh(construct_script([
+ bfh(push_script(bh2u(bitcoin.hash_160(revocation_pubkey)))) \ opcodes.OP_DUP,
+ bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_CHECKSIG, opcodes.OP_ELSE]) \ opcodes.OP_HASH160,
+ bfh(push_script(bh2u(remote_htlcpubkey))) \ bitcoin.hash_160(revocation_pubkey),
+ bytes([opcodes.OP_SWAP, opcodes.OP_SIZE]) \ opcodes.OP_EQUAL,
+ bitcoin.add_number_to_script(32) \ opcodes.OP_IF,
+ bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_HASH160]) \ opcodes.OP_CHECKSIG,
+ bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) \ opcodes.OP_ELSE,
+ bytes([opcodes.OP_EQUALVERIFY]) \ remote_htlcpubkey,
+ bitcoin.add_number_to_script(2) \ opcodes.OP_SWAP,
+ bytes([opcodes.OP_SWAP]) \ opcodes.OP_SIZE,
+ bfh(push_script(bh2u(local_htlcpubkey))) \ 32,
+ bitcoin.add_number_to_script(2) \ opcodes.OP_EQUAL,
+ bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_DROP]) \ opcodes.OP_IF,
+ bitcoin.add_number_to_script(cltv_expiry) \ opcodes.OP_HASH160,
+ bytes([opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF]) 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, 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: 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( def make_commitment_output_to_local_witness_script(
revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> bytes: 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) \ script = bfh(construct_script([
+ bytes([opcodes.OP_CHECKSEQUENCEVERIFY, opcodes.OP_DROP]) + bfh(push_script(bh2u(delayed_pubkey))) + bytes([opcodes.OP_ENDIF, opcodes.OP_CHECKSIG]) opcodes.OP_IF,
return local_script 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( def make_commitment_output_to_local_address(
revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> str: revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> str:

2
electrum/plugins/keepkey/keepkey.py

@ -53,7 +53,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
prev_tx = {} prev_tx = {}
for txin in tx.inputs(): for txin in tx.inputs():
tx_hash = txin.prevout.txid.hex() 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.')) raise UserFacingException(_('Missing previous tx for legacy input.'))
prev_tx[tx_hash] = txin.utxo prev_tx[tx_hash] = txin.utxo

2
electrum/plugins/ledger/ledger.py

@ -386,7 +386,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
redeemScript = Transaction.get_preimage_script(txin) redeemScript = Transaction.get_preimage_script(txin)
txin_prev_tx = txin.utxo 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.')) raise UserFacingException(_('Missing previous tx for legacy input.'))
txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None
inputs.append([txin_prev_tx_raw, inputs.append([txin_prev_tx_raw,

2
electrum/plugins/safe_t/safe_t.py

@ -51,7 +51,7 @@ class SafeTKeyStore(Hardware_KeyStore):
prev_tx = {} prev_tx = {}
for txin in tx.inputs(): for txin in tx.inputs():
tx_hash = txin.prevout.txid.hex() 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.')) raise UserFacingException(_('Missing previous tx for legacy input.'))
prev_tx[tx_hash] = txin.utxo prev_tx[tx_hash] = txin.utxo

5
electrum/submarine_swaps.py

@ -7,8 +7,9 @@ import attr
from .crypto import sha256, hash_160 from .crypto import sha256, hash_160
from .ecc import ECPrivkey 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 .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script,
from .transaction import TxOutpoint, PartialTxInput, PartialTxOutput, PartialTransaction, construct_witness is_segwit_address, construct_witness)
from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
from .util import log_exceptions from .util import log_exceptions
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address, LN_MAX_HTLC_VALUE_MSAT from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address, LN_MAX_HTLC_VALUE_MSAT

80
electrum/tests/test_transaction.py

@ -1,10 +1,15 @@
from typing import NamedTuple, Union from typing import NamedTuple, Union
from electrum import transaction, bitcoin 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.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' signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700" v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700"
@ -840,3 +845,74 @@ class TestTransaction(ElectrumTestCase):
self._run_naive_tests_on_tx(raw_tx, txid) self._run_naive_tests_on_tx(raw_tx, txid)
# txns from Bitcoin Core ends <--- # 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())

98
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, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
int_to_hex, push_script, b58_address_to_hash160, int_to_hex, push_script, b58_address_to_hash160,
opcodes, add_number_to_script, base_decode, is_segwit_script_type, opcodes, add_number_to_script, base_decode, is_segwit_script_type,
base_encode) base_encode, construct_witness, construct_script)
from .crypto import sha256d from .crypto import sha256d
from .logging import get_logger from .logging import get_logger
@ -243,6 +243,11 @@ class TxInput:
n = vds.read_compact_size() n = vds.read_compact_size()
return list(vds.read_bytes(vds.read_compact_size()) for i in range(n)) 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): class BCDataStream(object):
"""Workalike python implementation of Bitcoin's CDataStream class.""" """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) 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: def parse_witness(vds: BCDataStream, txin: TxInput) -> None:
n = vds.read_compact_size() n = vds.read_compact_size()
witness_elements = list(vds.read_bytes(vds.read_compact_size()) for i in range(n)) 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: def multisig_script(public_keys: Sequence[str], m: int) -> str:
n = len(public_keys) n = len(public_keys)
assert 1 <= m <= n <= 15, f'm {m}, n {n}' assert 1 <= m <= n <= 15, f'm {m}, n {n}'
op_m = bh2u(add_number_to_script(m)) return construct_script([m, *public_keys, n, opcodes.OP_CHECKMULTISIG])
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()
@ -650,8 +640,8 @@ class Transaction:
assert isinstance(txin, PartialTxInput) assert isinstance(txin, PartialTxInput)
_type = txin.script_type _type = txin.script_type
if not cls.is_segwit_input(txin): if not txin.is_segwit():
return '00' return construct_witness([])
if _type in ('address', 'unknown') and estimate_size: if _type in ('address', 'unknown') and estimate_size:
_type = cls.guess_txintype_from_address(txin.address) _type = cls.guess_txintype_from_address(txin.address)
@ -660,28 +650,11 @@ class Transaction:
return construct_witness([sig_list[0], pubkeys[0]]) return construct_witness([sig_list[0], pubkeys[0]])
elif _type in ['p2wsh', 'p2wsh-p2sh']: elif _type in ['p2wsh', 'p2wsh-p2sh']:
witness_script = multisig_script(pubkeys, txin.num_sig) 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']: elif _type in ['p2pk', 'p2pkh', 'p2sh']:
return '00' return construct_witness([])
raise UnknownTxinType(f'cannot construct witness for txin_type: {_type}') 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 @classmethod
def guess_txintype_from_address(cls, addr: Optional[str]) -> str: def guess_txintype_from_address(cls, addr: Optional[str]) -> str:
# It's not possible to tell the script type in general # It's not possible to tell the script type in general
@ -714,47 +687,46 @@ class Transaction:
assert isinstance(txin, PartialTxInput) assert isinstance(txin, PartialTxInput)
if txin.is_p2sh_segwit() and txin.redeem_script: 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(): if txin.is_native_segwit():
return '' return ''
_type = txin.script_type _type = txin.script_type
pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size) 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: if _type in ('address', 'unknown') and estimate_size:
_type = self.guess_txintype_from_address(txin.address) _type = self.guess_txintype_from_address(txin.address)
if _type == 'p2pk': if _type == 'p2pk':
return script return construct_script([sig_list[0]])
elif _type == 'p2sh': elif _type == 'p2sh':
# put op_0 before script # put op_0 before script
script = '00' + script
redeem_script = multisig_script(pubkeys, txin.num_sig) redeem_script = multisig_script(pubkeys, txin.num_sig)
script += push_script(redeem_script) return construct_script([0, *sig_list, redeem_script])
return script
elif _type == 'p2pkh': elif _type == 'p2pkh':
script += push_script(pubkeys[0]) return construct_script([sig_list[0], pubkeys[0]])
return script
elif _type in ['p2wpkh', 'p2wsh']: elif _type in ['p2wpkh', 'p2wsh']:
return '' return ''
elif _type == 'p2wpkh-p2sh': elif _type == 'p2wpkh-p2sh':
redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0]) redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0])
return push_script(redeem_script) return construct_script([redeem_script])
elif _type == 'p2wsh-p2sh': elif _type == 'p2wsh-p2sh':
if estimate_size: if estimate_size:
witness_script = '' witness_script = ''
else: else:
witness_script = self.get_preimage_script(txin) witness_script = self.get_preimage_script(txin)
redeem_script = bitcoin.p2wsh_nested_script(witness_script) 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}') raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}')
@classmethod @classmethod
def get_preimage_script(cls, txin: 'PartialTxInput') -> str: def get_preimage_script(cls, txin: 'PartialTxInput') -> str:
if txin.witness_script: if txin.witness_script:
opcodes_in_witness_script = [x[0] for x in script_GetOp(txin.witness_script)] if opcodes.OP_CODESEPARATOR in [x[0] for x in script_GetOp(txin.witness_script)]:
if opcodes.OP_CODESEPARATOR in opcodes_in_witness_script:
raise Exception('OP_CODESEPARATOR black magic is not supported') raise Exception('OP_CODESEPARATOR black magic is not supported')
return txin.witness_script.hex() 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] pubkeys = [pk.hex() for pk in txin.pubkeys]
if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
@ -790,7 +762,7 @@ class Transaction:
hashOutputs=hashOutputs) hashOutputs=hashOutputs)
def is_segwit(self, *, guess_for_address=False): 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()) for txin in self.inputs())
def invalidate_ser_cache(self): def invalidate_ser_cache(self):
@ -848,7 +820,7 @@ class Transaction:
def txid(self) -> Optional[str]: def txid(self) -> Optional[str]:
if self._cached_txid is None: if self._cached_txid is None:
self.deserialize() 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(): if not all_segwit and not self.is_complete():
return None return None
try: try:
@ -892,7 +864,7 @@ class Transaction:
script = cls.input_script(txin, estimate_size=True) script = cls.input_script(txin, estimate_size=True)
input_size = len(cls.serialize_input(txin, script)) // 2 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 witness_size = len(cls.serialize_witness(txin, estimate_size=True)) // 2
else: else:
witness_size = 1 if is_segwit_tx else 0 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, # 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. # for non-segwit inputs, we might end up burning coins as miner fees.
if for_signing and False: 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: " raise PSBTInputConsistencyFailure(f"PSBT input validation: "
f"If a witness UTXO is provided, no non-witness signature may be created") f"If a witness UTXO is provided, no non-witness signature may be created")
if self.redeem_script and self.address: if self.redeem_script and self.address:
@ -1359,7 +1331,7 @@ class PartialTxInput(TxInput, PSBTSection):
return True return True
if self.is_coinbase_input(): if self.is_coinbase_input():
return True 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 return True
signatures = list(self.part_sigs.values()) signatures = list(self.part_sigs.values())
s = len(signatures) s = len(signatures)
@ -1461,6 +1433,20 @@ class PartialTxInput(TxInput, PSBTSection):
self._is_p2sh_segwit = calc_if_p2sh_segwit_now() self._is_p2sh_segwit = calc_if_p2sh_segwit_now()
return self._is_p2sh_segwit 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: 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."""
return (self.part_sigs return (self.part_sigs
@ -1809,7 +1795,7 @@ class PartialTransaction(Transaction):
raise Exception("only SIGHASH_ALL signing is supported!") raise Exception("only SIGHASH_ALL signing is supported!")
nHashType = int_to_hex(sighash, 4) nHashType = int_to_hex(sighash, 4)
preimage_script = self.get_preimage_script(txin) preimage_script = self.get_preimage_script(txin)
if self.is_segwit_input(txin): if txin.is_segwit():
if bip143_shared_txdigest_fields is None: if bip143_shared_txdigest_fields is None:
bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields()
hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts

4
electrum/wallet.py

@ -2162,7 +2162,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if all([txin.utxo for txin in tx.inputs()]): if all([txin.utxo for txin in tx.inputs()]):
return None return None
# a single segwit input -> fine # 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 return None
# coinjoin or similar # coinjoin or similar
if any([not self.is_mine(txin.address) for txin in tx.inputs()]): 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 input amounts could not be verified as the previous transactions are missing.\n"
"The amount of money being spent CANNOT be verified.")) "The amount of money being spent CANNOT be verified."))
# some inputs are legacy # 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") + ": " return (_("Warning") + ": "
+ _("The fee could not be verified. Signing non-segwit inputs is risky:\n" + _("The fee could not be verified. Signing non-segwit inputs is risky:\n"
"if this transaction was maliciously modified before you sign,\n" "if this transaction was maliciously modified before you sign,\n"

Loading…
Cancel
Save