Generating a P2WPKH-P2SH (SegWit) Bitcoin address (Python)

October 17, 2017    Tagged: Bitcoin, Python, Crypto, Software

BIP141 (SegWit) briefly describes the generation of SegWit addresses that are backwards compatible by nesting the pay to witness public key hash (P2WPKH) transaction in a pay to script hash (P2SH) transaction.

The following example is the same P2WPKH, but nested in a BIP16 P2SH output.

witness:      <signature> <pubkey>
scriptSig:    <0 <20-byte-key-hash>>
              (0x160014{20-byte-key-hash})
scriptPubKey: HASH160 <20-byte-script-hash> EQUAL
              (0xA914{20-byte-script-hash}87)

The address which can spend such a transaction is then indistinguishable from a normal pay to script hash address (such as a standard multisig address).

Practically, this means that to generate an address, we need just need to prefix the HASH160 of the described scriptSig with the standard p2sh version byte (0x05/0xc4 for main/testnet respectively) and Base58 encode it.

First, translating the standard HASH160(x) = RIPEMD160(SHA256(x)) to Python:

import hashlib
def hash160(x): # Both accepts & returns bytes
    return hashlib.new('ripemd160', hashlib.sha256(x).digest()).digest()

Then we can define a function that accepts a compressed public key and generates the corresponding address. For simplicity I’ll use the Base58 encoding provided in bip32utils.

Edit: prusnak’s bip32utils library has since disappeared. Luckily I was using it so much that I forked it just in case: https://github.com/matthewdowney/bip32utils

$ pip install git+https://github.com/prusnak/bip32utils
from bip32utils import Base58

def p2wpkh_in_p2sh_addr(pk, testnet=False):
    """
    Compressed public key (hex string) -> p2wpkh nested in p2sh address. 'SegWit address.'
    """
    # Script sig is just PUSH(20){hash160(cpk)}
    push_20 = bytes.fromhex("0014")
    script_sig = push_20 + hash160_bytes(bytes.fromhex(pk))

    # Address is then prefix + hash160(script_sig)
    prefix = b"\xc4" if testnet else b"\x05"
    address = Base58.check_encode(prefix + hash160(script_sig))
    return address

Then you can go ahead and test it

>>> pub = "03a1af804ac108a8a51782198c2d034b28bf90c8803f5a53f76276fa69a4eae77f"
>>> p2wpkh_in_p2sh_addr(pub)
"36NvZTcMsMowbt78wPzJaHHWaNiyR73Y4g"
>>> p2wpkh_in_p2sh_addr(pub, testnet=True)
"2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2"