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:
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"