Skip to content

Commit

Permalink
[electrum] move get_preimage_script from Transaction to TxInput
Browse files Browse the repository at this point in the history
Summary:
And improve support of p2pk inputs: a pubkey can be passed but is not available from a deserialized tx, an address is not applicable. This is required to test get_preimage_script.

So far get_preimage_script had only partial test coverage for p2pkh transaction. Add coverage for p2pk and multisig p2sh.

Drop the special case for transaction type "unknown", because nothing in this codebase uses or defines  `txin["scriptCode"]`

Add a `OutPoint.from_str` helper which is just used in tests for now, but can be used later for parsing JSON wallet files.

Depends on D14496

Test Plan: `python test_runner.py`

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Subscribers: Fabien

Differential Revision: https://reviews.bitcoinabc.org/D14497
  • Loading branch information
PiRK committed Sep 18, 2023
1 parent 0ab06e2 commit a205331
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 53 deletions.
6 changes: 0 additions & 6 deletions electrum/electrumabc/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,12 +567,6 @@ def script_to_address(script: bytes) -> Address:
return addr


def public_key_to_p2pk_script(pubkey):
script = push_script(pubkey)
script += "ac" # op_checksig
return script


__b58chars = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
assert len(__b58chars) == 58

Expand Down
81 changes: 71 additions & 10 deletions electrum/electrumabc/tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@

from .. import transaction
from ..address import Address, PublicKey, Script, ScriptOutput, UnknownAddress
from ..bitcoin import TYPE_ADDRESS, TYPE_PUBKEY, TYPE_SCRIPT, OpCodes, ScriptType
from ..bitcoin import (
TYPE_ADDRESS,
TYPE_PUBKEY,
TYPE_SCRIPT,
OpCodes,
ScriptType,
push_script,
)
from ..keystore import xpubkey_to_address
from ..uint256 import UInt256
from ..util import bh2u
Expand Down Expand Up @@ -852,6 +859,7 @@ def _deser_test(
expected_pubkeys: Optional[List[str]] = None,
expected_address: Optional[Address] = None,
expected_value: Optional[int] = None,
expected_preimage_script: Optional[str] = None,
):
tx = transaction.Transaction(tx_hex)
input_dict = tx.inputs()[0]
Expand Down Expand Up @@ -916,6 +924,18 @@ def _deser_test(
self.assertEqual(txinput.num_required_sigs, len(expected_sigs))
self.assertEqual(txinput.num_valid_sigs, len(expected_sigs))

# preimage script
if txinput.type in (ScriptType.coinbase, ScriptType.unknown, ScriptType.p2pk):
with self.assertRaises(RuntimeError):
txinput.get_preimage_script()
elif txinput.get_value() is None:
with self.assertRaises(transaction.InputValueMissing):
tx.serialize_preimage(0)
else:
self.assertEqual(
txinput.get_preimage_script().hex(), expected_preimage_script
)

def test_multisig_p2sh_deserialization(self):
self._deser_test(
tx_hex="0100000001b98d550fa331da21038952d6931ffd3607c440ab2985b75477181b577de118b10b000000fdfd0000483045022100a26ea637a6d39aa27ea7a0065e9691d477e23ad5970b5937a9b06754140cf27102201b00ed050b5c468ee66f9ef1ff41dfb3bd64451469efaab1d4b56fbf92f9df48014730440220080421482a37cc9a98a8dc3bf9d6b828092ad1a1357e3be34d9c5bbdca59bb5f02206fa88a389c4bf31fa062977606801f3ea87e86636da2625776c8c228bcd59f8a014c69522102420e820f71d17989ed73c0ff2ec1c1926cf989ad6909610614ee90cf7db3ef8721036eae8acbae031fdcaf74a824f3894bf54881b42911bd3ad056ea59a33ffb3d312103752669b75eb4dc0cca209af77a59d2c761cbb47acc4cf4b316ded35080d92e8253aeffffffff0101ac3a00000000001976a914a6b6bcc85975bf6a01a0eabb2ac97d5a418223ad88ac00000000",
Expand Down Expand Up @@ -970,6 +990,17 @@ def test_coinbase_deserialization(self):
)

def test_2of2_multisig_incomplete(self):
expected_pubkeys = [
"02ce914e4644565afe48d5bc3b5ef304c7fcf41c0defd668f45196edbb1411f07f",
"03dec2a5937425f5657083ef25022f66b6924e21519ddac5cbbc5c9212a04428da",
]
expected_preimage_script = (
f"{OpCodes.OP_2:x}"
+ push_script(expected_pubkeys[0])
+ push_script(expected_pubkeys[1])
+ f"{OpCodes.OP_2:x}{OpCodes.OP_CHECKMULTISIG:x}"
)

self._deser_test(
tx_hex="0200000001d9b830c4f60b839c512d00dfa08db658eae54ca849b81bca8798f250cb2e93c903000000fb0001ff483045022100fbc08cdbd62d7328496735d9cc66f1a7e978da1ea3de54f8a8344d0928606c8002205ec8c3adbe502e40e72a593de14248ba19b8098ed591803accd47338a73f8427414cad524c53ff0488b21e03f918d62980000000d14a70b732b0badca35b38671d321ddce735bfc9b1ab823140b288e308cf9007020fb77d2e3dab47533c29e7280725a20427c27a37545a1daa330e5d7146f66a39000006004c53ff0488b21e036ae03b288000000074514157090ae16af9dff0cb13666d75b59e4ac734099309de6c8dc0108c2c250303b19c01f4ab103e723ac060c3ab5a5de5b1773b77b63f286ebd2b88eee791d40000060052aefeffffff248a01000000000001f68801000000000017a9149ee12d650f43157a0a5c3b615d061bb5290b28248700000000",
expected_type=ScriptType.p2sh,
Expand All @@ -978,17 +1009,27 @@ def test_2of2_multisig_incomplete(self):
"3045022100fbc08cdbd62d7328496735d9cc66f1a7e978da1ea3de54f8a8344d0928606c8002205ec8c3adbe502e40e72a593de14248ba19b8098ed591803accd47338a73f842741",
],
num_required_sigs=2,
expected_pubkeys=[
"02ce914e4644565afe48d5bc3b5ef304c7fcf41c0defd668f45196edbb1411f07f",
"03dec2a5937425f5657083ef25022f66b6924e21519ddac5cbbc5c9212a04428da",
],
expected_pubkeys=expected_pubkeys,
expected_address=Address.from_string(
"ecash:ppfhzqryfq5u9y3ccqw3j9qaa9rsyz746sar20zk99"
),
expected_value=100_900,
expected_preimage_script=expected_preimage_script,
)

def test_2of3_multisig_incomplete(self):
expected_pubkeys = [
"0206a2c2c875b34b2d52b4055ab62f6fa048a4f5317269937fe2133fbe7916237a",
"0256c49b291e84eb49e6a0de7cd116c44d61ddfd531d2d4cc9194ef8ba2a01564c",
"029fe778a1477a830016f44a661e98f09a9bcc43133fb716597a3bdfbfb98708e3",
]
expected_preimage_script = (
f"{OpCodes.OP_2:x}"
+ push_script(expected_pubkeys[0])
+ push_script(expected_pubkeys[1])
+ push_script(expected_pubkeys[2])
+ f"{OpCodes.OP_3:x}{OpCodes.OP_CHECKMULTISIG:x}"
)
self._deser_test(
tx_hex="0200000001f5315ddddb23ec54b2ba6a67155c81cdbd8a98bca0fa6ebfebaeb4af415fb0a600000000fd53010001ff01ff483045022100dffbcb3902d92650b75fb5528d1d6816f4e775f92e44d9eda606a0e197d7ffb402204e787b0bd06aa7640e79279fde55f2c98b4f82348491ddf7c5a6be8d21cfca86414d0201524c53ff0488b21e035a37440380000000a412cfa165936158fd7f75d8b615805aa92cefb4aa4e295832f9811f7c1ed0c10290cddf66380e9e6eece36340f41a87a07fbbf329ddb51c1f14a3130c3485998e00000e004c53ff0488b21e0250161d8f0000000035ebe2e8adb327d4cb6c79b57245b010a7c235b23d0d10569226aac40076fcdb02857cce864e8560924496ce4f74df94d483ddd02031e6dc2db55a75e88c2a2b5c00000e004c53ff0488b21e0351b32e0c80000000ca0cff29d8c48ae7993841a188998639ff1e7b9bcefb3f800e9ca4924b5b857d03b0a87930c29eb53f05a2b0e9df56674b5ec0e9109790bbf20c420e4ea1a8371500000e0053aefeffffffa1dc010000000000014edb01000000000017a91451d3c1ef675df7f432b2cf68270e5c4b30187db78700000000",
expected_type=ScriptType.p2sh,
Expand All @@ -998,15 +1039,12 @@ def test_2of3_multisig_incomplete(self):
"3045022100dffbcb3902d92650b75fb5528d1d6816f4e775f92e44d9eda606a0e197d7ffb402204e787b0bd06aa7640e79279fde55f2c98b4f82348491ddf7c5a6be8d21cfca8641",
],
num_required_sigs=2,
expected_pubkeys=[
"0206a2c2c875b34b2d52b4055ab62f6fa048a4f5317269937fe2133fbe7916237a",
"0256c49b291e84eb49e6a0de7cd116c44d61ddfd531d2d4cc9194ef8ba2a01564c",
"029fe778a1477a830016f44a661e98f09a9bcc43133fb716597a3bdfbfb98708e3",
],
expected_pubkeys=expected_pubkeys,
expected_address=Address.from_string(
"ecash:pql2m9sh88h86lk8cvsf3ngvhcduyvmd0qx05dqrrw"
),
expected_value=122_017,
expected_preimage_script=expected_preimage_script,
)

def test_from_coin_dict(self):
Expand Down Expand Up @@ -1035,6 +1073,29 @@ def test_from_coin_dict(self):
txinput.pubkeys[0].hex(),
"036fcbca5dcae003f020769f56260c7b36b0ea645ca8d80056d7a6bd2066a5b07d",
)
self.assertEqual(
txinput.get_preimage_script().hex(),
"76a914a2891b070ca8d2164a1015c6276eb3ba78b7593388ac",
)

def test_p2pk_preimage_script(self):
# we need only a valid pubkey, everything else can be mocked
pubkey = bytes.fromhex(
"036fcbca5dcae003f020769f56260c7b36b0ea645ca8d80056d7a6bd2066a5b07d"
)
txinput = transaction.TxInput.from_keys(
transaction.OutPoint.from_str("00" * 32 + ":0"),
sequence=0,
script_type=ScriptType.p2pk,
num_required_sigs=1,
x_pubkeys=[b""],
signatures=[b""],
pubkeys=[pubkey],
)
self.assertEqual(
txinput.get_preimage_script().hex(),
push_script(pubkey.hex()) + f"{OpCodes.OP_CHECKSIG:x}",
)


class TestScriptMatching(unittest.TestCase):
Expand Down
70 changes: 33 additions & 37 deletions electrum/electrumabc/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ def __hash__(self):
def __str__(self):
return f"{self.txid.to_string()}:{self.n})"

@staticmethod
def from_str(outpoint: str) -> OutPoint:
txid_hex, n_str = outpoint.split(":")
return OutPoint(UInt256.from_hex(txid_hex), int(n_str))


class TxInput:
def __init__(
Expand Down Expand Up @@ -527,9 +532,9 @@ def from_keys(
num_required_sigs,
x_pubkeys,
signatures,
address,
)
)
assert address is not None or script_type == ScriptType.p2pk
return TxInput(
outpoint,
sequence,
Expand Down Expand Up @@ -572,6 +577,24 @@ def from_coin_dict(coin: Dict) -> TxInput:
value=value,
)

def get_preimage_script(self) -> bytes:
if self.type == ScriptType.p2pkh:
return self.address.to_script()
if self.type == ScriptType.p2sh:
pubkeys, x_pubkeys = self.get_sorted_pubkeys()
return multisig_script(pubkeys, self.num_required_sigs)
if self.type == ScriptType.p2pk:
if self.pubkeys is None:
raise RuntimeError(
"Cannot get preimage for p2pk input without knowing the pubkey"
)
return bitcoin.push_script_bytes(self.pubkeys[0]) + bytes(
[OpCodes.OP_CHECKSIG]
)
raise RuntimeError(
f"Cannot get preimage script for input with type {self.type.name}"
)


class BCDataStream(object):
def __init__(self):
Expand Down Expand Up @@ -1180,23 +1203,6 @@ def input_script(self, txin, estimate_size=False, sign_schnorr=False):
def is_txin_complete(cls, txin):
return TxInput.from_coin_dict(txin).is_complete()

@classmethod
def get_preimage_script(self, txin):
_type = txin["type"]
if _type == "p2pkh":
return txin["address"].to_script().hex()
elif _type == "p2sh":
pubkeys, x_pubkeys = TxInput.from_coin_dict(txin).get_sorted_pubkeys()
return multisig_script(pubkeys, txin["num_sig"]).hex()
elif _type == "p2pk":
pubkey = txin["pubkeys"][0]
return bitcoin.public_key_to_p2pk_script(pubkey)
elif _type == "unknown":
# this approach enables most P2SH smart contracts (but take care if using OP_CODESEPARATOR)
return txin["scriptCode"]
else:
raise RuntimeError("Unknown txin type", _type)

@classmethod
def serialize_outpoint(self, txin):
return bh2u(bfh(txin["prevout_hash"])[::-1]) + bitcoin.int_to_le_hex(
Expand Down Expand Up @@ -1278,7 +1284,7 @@ def calc_common_sighash(self, use_cache=False):
Warning: If you modify non-signature parts of the transaction
afterwards, this cache will be wrong!"""
inputs = self.inputs()
inputs = self.txinputs()
outputs = self.outputs()
meta = (len(inputs), len(outputs))

Expand All @@ -1296,17 +1302,10 @@ def calc_common_sighash(self, use_cache=False):
del cmeta, res, self._cached_sighash_tup

hashPrevouts = bitcoin.Hash(
bfh("".join(self.serialize_outpoint(txin) for txin in inputs))
b"".join(txin.outpoint.serialize() for txin in inputs)
)
hashSequence = bitcoin.Hash(
bfh(
"".join(
bitcoin.int_to_le_hex(
txin.get("sequence", DEFAULT_TXIN_SEQUENCE), 4
)
for txin in inputs
)
)
b"".join(txin.sequence.to_bytes(4, "little") for txin in inputs)
)
hashOutputs = bitcoin.Hash(
bfh("".join(self.serialize_output(o) for o in outputs))
Expand All @@ -1327,17 +1326,14 @@ def serialize_preimage(self, i, nHashType=0x00000041, use_cache=False):
nHashType = bitcoin.int_to_le_hex(nHashType, 4)
nLocktime = bitcoin.int_to_le_hex(self.locktime, 4)

txin = self.inputs()[i]
outpoint = self.serialize_outpoint(txin)
preimage_script = self.get_preimage_script(txin)
txin: TxInput = self.txinputs()[i]
outpoint = txin.outpoint.to_hex()
preimage_script = txin.get_preimage_script().hex()
scriptCode = bitcoin.var_int(len(preimage_script) // 2) + preimage_script
try:
amount = bitcoin.int_to_le_hex(txin["value"], 8)
except KeyError:
if txin.get_value() is None:
raise InputValueMissing
nSequence = bitcoin.int_to_le_hex(
txin.get("sequence", DEFAULT_TXIN_SEQUENCE), 4
)
amount = bitcoin.int_to_le_hex(txin.get_value(), 8)
nSequence = bitcoin.int_to_le_hex(txin.sequence, 4)

hashPrevouts, hashSequence, hashOutputs = self.calc_common_sighash(
use_cache=use_cache
Expand Down

0 comments on commit a205331

Please sign in to comment.