2

How do I sign a P2SH transaction in Python? I am using the Python API that ships with the bitcoin source code, in directory bitcoin/test/functional. I'm using regtest.

Here is a fragment of my unit test, it works as I expect. It creates a transaction, signs it, and broadcasts it:

    tx2 = CTransaction()
    # txid0 is the ID of a previous TX, v is the index of the relevant output:
    tx2.vin.append(CTxIn(COutPoint(int(txid0, 16), v), b""))
    # For now, send to a dummy address:
    tx2.vout.append(CTxOut(int(9 * COIN), CScript([OP_TRUE])))
    tx2 = w0_rpc.signrawtransactionwithwallet(ToHex(tx2))["hex"]
    #tx2.vin[0].scriptSig = CScript(signature, public_key)
    txid = n1.sendrawtransaction(tx2, True)

Instead of making the call to signrawtransactionwithwallet(), I would like to explicity assign a value to tx2.vin[0].scriptSig. I gather that this value should look something like (signature, public_key), but I can't figure out how to do it.

How do I go about this? Is there any documentation of this Python library? So far I am looking at the existing Python test cases, and following the docs for the Bitcoin RPC interface.

Edit. I managed to accomplish this by reverse engineering the existing unit tests. I was able to use CScript objects and did not need to go as low level as the approach in the accepted answer, although it's good to know that that option exists if I need it. Here is the code for a unit test which 1) pays to a P2SH that is locked with two signatures and 2) spends from that address without calling signrawtransactionwithwallet(), instead manually assigning a signature to tx.vin[0].scriptSig.

#!/usr/bin/env python3

import pprint

from test_framework.test_framework import BitcoinTestFramework
from test_framework.messages import CTransaction, CTxIn, CTxOut, COutPoint, ToHex, COIN, sha256
from test_framework.script import CScript, CScriptOp, OP_1, OP_DROP, OP_2, OP_3, OP_HASH160, OP_EQUAL, hash160, OP_TRUE, OP_DUP, OP_EQUALVERIFY, OP_CHECKSIG, OP_CHECKMULTISIG, OP_CHECKSIGVERIFY, SignatureHash, SIGHASH_ALL
from test_framework.util import hex_str_to_bytes, bytes_to_hex_str
from test_framework.address import byte_to_base58
from test_framework.key import ECKey

class RawTxTest(BitcoinTestFramework):

    def set_test_params(self):
        self.num_nodes = 2
        self.extra_args = [[],["-txindex"]]

    def run_test(self):
        """Implement a P2SH transaction with OP_CHECKSIG without using signrawtransactionwithkey()"""

        self.log.info(f"\n    DEBUG START")

        # initialize node variables
        n0 = self.nodes[0] # This node used for initial balance
        n1 = self.nodes[1] # All test wallets created on this node

        # TRANSACTION #0 - pay from node 0 to addr0
        w0 = n1.createwallet(wallet_name="wallet0")
        w0_rpc = n1.get_wallet_rpc('wallet0')
        addr0 = w0_rpc.getnewaddress()
        self.log.info(f"\n    DEBUG addr0={addr0}")

        txid0 = n0.sendtoaddress(addr0, 10.0)
        n0.generate(6)
        self.sync_all()

        # dump the tx to the log
        raw_tx0 = w0_rpc.getrawtransaction(txid0, True)
        s = pprint.pformat(raw_tx0)
        self.log.info(f"\n    DEBUG tx0={s}")

        # write the balance to the log
        bal0 = w0_rpc.getbalance()
        self.log.info(f"\n    DEBUG bal0={bal0:f}")

        # TRANSACTION #1 - Pay from addr0 to P2SH   
        coinbase_key0 = ECKey()
        coinbase_key0.generate()
        coinbase_pubkey0 = coinbase_key0.get_pubkey().get_bytes()
        coinbase_key1 = ECKey()
        coinbase_key1.generate()
        coinbase_pubkey1 = coinbase_key1.get_pubkey().get_bytes()
        #redeemScript = CScript([coinbase_pubkey0, OP_CHECKSIG])
        redeemScript = CScript([coinbase_pubkey0, OP_CHECKSIGVERIFY, coinbase_pubkey1, OP_CHECKSIG])
        redeemScriptHex = redeemScript.hex()
        self.log.info(f"\n    DEBUG redeemScriptHex={redeemScriptHex}")
        redeemScript160 = hash160(redeemScript)
        madd1 = byte_to_base58(redeemScript160, 196)
        self.log.info(f"\n    DEBUG madd1={madd1}")
        p2sh_script = CScript([OP_HASH160, redeemScript160, OP_EQUAL])

        tx1 = CTransaction()
        vout = [v["n"] for v in raw_tx0["vout"] if addr0 in v["scriptPubKey"].get("addresses", [])]
        assert len(vout) == 1
        v = vout[0]
        tx1.vin.append(CTxIn(COutPoint(int(txid0, 16), v)))
        a1 = w0_rpc.getaddressinfo(madd1)
        self.log.info(f"\n    DEBUG a1={a1}")
        scriptPubKey = a1["scriptPubKey"]
        pubkey1 = hex_str_to_bytes(scriptPubKey)
        tx1.vout.append(CTxOut(int(8 * COIN), p2sh_script))

        priv0 = w0_rpc.dumpprivkey(addr0)
        tx1 = w0_rpc.signrawtransactionwithkey(ToHex(tx1), [priv0])["hex"]
        txid1 = n0.sendrawtransaction(tx1, True)
        n0.generate(6)
        self.sync_all()

        # dump the tx to the log
        raw_tx1 = w0_rpc.getrawtransaction(txid1, True)
        s = pprint.pformat(raw_tx1)
        self.log.info(f"\n    DEBUG tx1={s}")

        # write the unspent tx to the log
        w0_rpc.importaddress(madd1)
        us1 = w0_rpc.listunspent()
        self.log.info(f"\n    DEBUG us1={us1}")

        # TRANSACTION #2 - Pay from P2SH to addr2
        addr2 = w0_rpc.getnewaddress()
        self.log.info(f"\n    DEBUG addr2={addr2}")
        a2 = w0_rpc.getaddressinfo(addr2)
        pubkey2 = hex_str_to_bytes(a2['pubkey'])
        p2pkh2 = CScript([OP_DUP, OP_HASH160, hash160(pubkey2), OP_EQUALVERIFY, OP_CHECKSIG])
        tx2 = CTransaction()
        tx2.vin.append(CTxIn(COutPoint(int(txid1, 16), 0)))
        tx2.vout.append(CTxOut(int(7 * COIN), p2pkh2))

        (sighash, err) = SignatureHash(redeemScript, tx2, 0, SIGHASH_ALL)
        sig0 = coinbase_key0.sign_ecdsa(sighash) + bytes(bytearray([SIGHASH_ALL]))
        sig1 = coinbase_key1.sign_ecdsa(sighash) + bytes(bytearray([SIGHASH_ALL]))
        #tx2.vin[0].scriptSig = CScript([sig0, redeemScript])
        tx2.vin[0].scriptSig = CScript([sig1, sig0, redeemScript])
        #tx2.rehash()
        txid2 = n0.sendrawtransaction(ToHex(tx2), True)
        n0.generate(6)
        self.sync_all()

        # dump the tx to the log
        raw_tx2 = w0_rpc.getrawtransaction(txid2, True)
        s = pprint.pformat(raw_tx2)
        self.log.info(f"\n    DEBUG tx2={s}")

        # write the balance to the log
        bal2 = w0_rpc.getbalance()
        self.log.info(f"\n    DEBUG bal2={bal2:f}")

        self.log.info(f"\n    DEBUG END")

if __name__ == '__main__':
    RawTxTest().main()
2

To manually build scriptSig you need to concatenate the length of the signature, the signature, the length of the pubkey and then the pubkey in binary form.

For example, let's take a look at a random TX I happen to have open on my console:

./bitcoin-cli getrawtransaction 055f9c6dc094cf21fa224e1eb4a54ee3cc44ae9daa8aa47f98df5c73c48997f9 1

We want the vin:

    {
      "txid": "b187426f2fdd5a7ac2f49d822f68e07f48486ee53a8a45de2494f12acb37a0d8",
      "vout": 3,
      "scriptSig": {
        "asm": "3046022100d78c31a20fa11533475be893b229eb4d252e600dcc2a0735d360c541b6aec813022100e3eaa72c915ef47d94ccbd18c2ba6d9ae5b98be6e9fbf968d4bbbb003e06d687[ALL] 030e001332b43924be343986cca3df669f57b0dedd120990e727787f8dea50fdbc",
        "hex": "493046022100d78c31a20fa11533475be893b229eb4d252e600dcc2a0735d360c541b6aec813022100e3eaa72c915ef47d94ccbd18c2ba6d9ae5b98be6e9fbf968d4bbbb003e06d6870121030e001332b43924be343986cca3df669f57b0dedd120990e727787f8dea50fdbc"
      },
      "sequence": 4294967295
    }

As you can see there are two formats for scriptSig: hex and assembly language. The assembly language is simply the hex with interpreted opcodes. Notice how hex has a leading "49" before the assembly format? 0x49 is 73 in decimal, which tells it to push 73 bytes onto the stack. After the 49 and 73 bytes (146 characters), we see 0x21 which is 33 in decimal. So we push 33 bytes onto the stack (the remaining 66 characters). This completes the scriptSig part of the unlocking script: pushing the sig and the pubkey onto the stack.

(Note that in hex encoding each character encodes 4 bits, so a byte takes 2 chars. That's why when pushing 33 bytes you see 66 characters on the hex encoding, for example.)

To build your own scriptSig from the above data, you need to decode the hex into binary byte array:

myscript = bytearray.fromhex(script_hex)
tx2.vin[0].scriptSig = myscript

In your case you don't have the hex yet because you're building the TX yourself, so what you need to do is build a byte array yourself and attribute it to tx2.vin[0].scriptSig

In Python pseudo-code:

# assuming signature,pubkey are byte arrays
# otherwise you need to encode them first
mybytes = [len(signature)] # initial byte = signature length
mybytes.append(signature)
mybytes.append(len(pubkey))
mybytes.append(pubkey)
tx2.vin[0].scriptSig = mybytes

On the Python API source code you can see that the definition of scriptSig is binary so if you get the above steps right your TX should work.

  • 1
    I can't thank you enough for that thorough response. I managed to accomplish what I needed with a slightly higher level approach. I just updated my OP to include the code. I'm glad to know about the lower level approach in case I ever need it. – eric Oct 18 at 16:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.