Browse Source

Merge pull request #6685 from SomberNight/202010_bitcoin_script

bitcoin/transaction: construct_script, and clean-ups
patch-4
ghost43 4 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.
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'

2
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)

9
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):

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 .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

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

2
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

2
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,

2
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

5
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

80
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())

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,
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,28 +650,11 @@ 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
@ -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

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

Loading…
Cancel
Save