This commit is contained in:
Peter D. Gray 2020-02-10 12:46:15 -05:00
parent 7f5852ec17
commit 2b9cc32f8c
No known key found for this signature in database
GPG Key ID: F0E6CC6AFC16CF7B
4 changed files with 326 additions and 111 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@ public.txt
output.psbt
foo*.psbt
foo*.txt
pp
# Byte-compiled / optimized / DLL files

View File

@ -1,8 +1,8 @@
# PSBT Faker
A simple program to create test PSBT files, that are plausible and self-consistent so
the PSBT-signing tools will sign them. Does not involve any blockchains... completely
made up inputs.
that PSBT-signing tools will sign them. Does not involve any blockchains... completely
made up inputs, output addresses choosen at random.
## Usage
@ -10,7 +10,25 @@ made up inputs.
```
# python3 -m pip install --editable .
# rehash
# pbst_faker test.psbt 1destaddr
# pbst_faker --help
Usage: psbt_faker [OPTIONS] OUTPUT.PSBT XPUB
Construct a valid PSBT which spends non-existant BTC to random addresses!
Options:
-t, --testnet Assume testnet3 addresses (default mainet)
-s, --segwit Make ins/outs be segwit style
-n, --num-outs INTEGER Number of outputs (default 1)
-c, --num-change INTEGER Number of change outputs (default 1)
-v, --value INTEGER Total BTC value of inputs (integer, default
3)
-f, --fee INTEGER Miner's fee in Satoshis
-a, --styles [p2wpkh|p2wsh|p2sh|p2pkh|p2wsh-p2sh|p2wpkh-p2sh]
Output address style (multiple ok)
-6, --base64 Output base64 (default binary)
--help Show this message and exit.
```
## Requirements
@ -21,3 +39,67 @@ made up inputs.
(See `requirements.txt`)
## Examples
```
$ psbt_faker foo.psbt tpubD6NzVbkrYhZ4Xp6tGusznF6KMdYHy1JSCdDk3XVLDuAA7EgJKghA5J1FP4pDXb4sCypJjAYPB4uTTXkVo2iWzK8BsMaccXTNyShDx3gxagi -s -a p2wsh --fee 15000000 -c 0
Fake PSBT would send 3 BTC to:
2.85000000 => bc1qqalzjffzy9nwcd35t0phdyugdmmqpskldgcw3xd40qxh32z908msf5alem
0.15000000 => miners fee
$ psbt_faker foo.psbt tpubD6NzVbkrYhZ4Xp6tGusznF6KMdYHy1JSCdDk3XVLDuAA7EgJKghA5J1FP4pDXb4sCypJjAYPB4uTTXkVo2iWzK8BsMaccXTNyShDx3gxagi -n 10
Fake PSBT would send 3 BTC to:
0.27272636 => 17VardgvHiYjDEtpBRWpqQLgrvKDUiGGaW
0.27272636 => 1A1FDLRD1caNjbwpr4odqpcB2sGgZSgGqZ
0.27272636 => 1P3Zr4zQko2CDbDDiqrkMduSppNB3Pb1Aq
0.27272636 => 1LcDusCVB6KjjAcrk5NvscV4AQ3cRJTR8j
0.27272636 => 15oy1fAxnbYr6Vgz7eNwjBQfujdvssdRaG
0.27272636 => 1EkYuiLo9Tt3cYCJwMfDvX38MddTBMqPc1
0.27272636 => 185VxgHqCEYudH6XXwdDiQtqfEUXGMxSXJ
0.27272636 => 19dR12aRSj8nyUaJLM11ruExa7N6jdAmUJ
0.27272636 => 1Ppj73d7z6cQvKhzezmaBywbJRSUnrymPE
0.27272636 => 1CPCdAWTrVqgS8cHVTbDQwkCvASjTfcaTe
0.27272636 => 1F2WTuA3BRpYmM82gsLuAdyAiLPYoUYijP (change back)
0.00001000 => miners fee
$ psbt_faker foo.psbt tpubD6NzVbkrYhZ4Xp6tGusznF6KMdYHy1JSCdDk3XVLDuAA7EgJKghA5J1FP4pDXb4sCypJjAYPB4uTTXkVo2iWzK8BsMaccXTNyShDx3gxagi -n 3 -v 100 -c 10
Fake PSBT would send 100 BTC to:
7.69230692 => 13mRoGiQHzmPhaCgQZbjw42njWhV3ymqDw
7.69230692 => 1MMbuGuuaJ9GnRXh4ixa6xiKER3xzg52TJ
7.69230692 => 1NjnUBrWSSx8iK5TC3XJXqQ7grC23kpZX2
7.69230692 => 1Aq96VVsd2nocTqAYQ4PnD6XhotKqmrBNn (change back)
7.69230692 => 1Bj7KprFDJ1d1F1se3DKedASFYvjWNaZMT (change back)
7.69230692 => 1HVTgLgZF95tF4B1CJk4BEvkLmT3hYDrmA (change back)
7.69230692 => 17Uz3tHeG1Zf8W4hmst2kQtbH17tHe3UTN (change back)
7.69230692 => 1LyLjaPcXbo5TxJYMYyUT9HzCgkJnKef1j (change back)
7.69230692 => 19DasuH8grQGc4MrPPR5abYUZAKF9UbbwZ (change back)
7.69230692 => 1JpymwTGWfXpcurLnsFbLcPRnkkzvRiKsy (change back)
7.69230692 => 1PoFUSStjmogrv2eEtRjbpz8N5reETVVZn (change back)
7.69230692 => 1Q8yrQsHotMNkrsAyEynDAuqC8rDs7nG41 (change back)
7.69230692 => 1AbiaE64hjUygVoqkedaLvneHht8bbvPgo (change back)
0.00001000 => miners fee
psbt_faker foo.psbt tpubD6NzVbkrYhZ4Xp6tGusznF6KMdYHy1JSCdDk3XVLDuAA7EgJKghA5J1FP4pDXb4sCypJjAYPB4uTTXkVo2iWzK8BsMaccXTNyShDx3gxagi -n 10 -a p2wpkh -a p2wsh -a p2sh -a p2pkh -a p2wsh-p2sh -a p2wpkh-p2sh
Fake PSBT would send 3 BTC to:
0.27272636 => bc1q2l0zgfksxacs8hdxwmq56ftpzagcyvq8z237qf
0.27272636 => bc1q4ru6vpngexl348we0fkydheat3azcvr96uc975tmvcy0z8kjaz6qz30498
0.27272636 => 37Axq8rmQGjEHVoCb877RiNfWnnMtFCZ6H
0.27272636 => 16JDSqRVvYdWV4KntQ5wjUK5es6CaiTyBc
0.27272636 => 3HXq92K1xvx6QMNmQTHPWPLNiEReez595d
0.27272636 => 3LyBpZ2aaTs1Qj1NFmGGttL8PyhEzB9iDW
0.27272636 => bc1qsplnzq8n500q4zg6a8m2nj4c8ygvlp8p8zuppc
0.27272636 => bc1qw9hery5rjcujuf3f09djlxepepx6luen7jq9t0hfsu44dv3t6x3s4k4aw5
0.27272636 => 3Ld1TUaWQouRRGGAc8PSzvqtgjfyxdM3Vr
0.27272636 => 1ABmPHMdqK4MqF9BkACv8PHHYL7McmbYAq
0.27272636 => 15mkVohf2A1g9nVo9tn2KtN2f4eBHQCche (change back)
0.00001000 => miners fee
```

137
main.py
View File

@ -26,11 +26,11 @@ from pycoin.key.BIP32Node import BIP32Node
from pycoin.convention import tx_fee
import urllib.request
from txn import *
b2a_hex = lambda a: str(_b2a_hex(a), 'ascii')
#xfp2hex = lambda a: b2a_hex(a[::-1]).upper()
TESTNET = False
def str2ipath(s):
# convert text to numeric path for BIP174
for i in s.split('/'):
@ -56,122 +56,43 @@ def str2path(xfp, s):
p = list(str2ipath(s))
return struct.pack('<%dI' % (1 + len(p)), xfp, *p)
def calc_pubkey(xpubs, path):
# given a map of paths to xpubs, and a single path, calculate the pubkey
assert path[0:2] == 'm/'
hard_prefix = '/'.join(s for s in path.split('/') if s[-1] == "'")
hard_depth = hard_prefix.count('/') + 1
want = ('m/'+hard_prefix) if hard_prefix else 'm'
assert want in xpubs, f"Need: {want} to build pubkey of {path}"
node = BIP32Node.from_hwif(xpubs[want])
parts = [s for s in path.split('/') if s != 'm'][hard_depth:]
# node = node.subkey_for_path(path[2:])
if not parts:
assert want == path
else:
for sk in parts:
node = node.subkey_for_path(sk)
return node.sec()
@click.command()
@click.argument('out_psbt', type=click.File('wb'))
@click.argument('payout_addresses', type=str, nargs='*')
@click.option('--testnet', '-t', help="Assume testnet3 addresses", is_flag=True, default=False)
@click.option('--xpub', help="Provide XPUB value", default=None)
@click.option('--num-change', '-c', help="Number of change outputs", default=1)
@click.option('--xfp', '--fingerprint', help="Provide XFP value, otherwise discovered from xpub", default=None)
def faker(num_change, payout_addresses, out_psbt, testnet, xfp=None, xpub=None):
@click.argument('out_psbt', type=click.File('wb'), metavar="OUTPUT.PSBT")
@click.argument('xpub', type=str)
@click.option('--testnet', '-t', help="Assume testnet3 addresses (default mainet)", is_flag=True, default=False)
@click.option('--segwit', '-s', help="Make ins/outs be segwit style", is_flag=True, default=False)
@click.option('--num-outs', '-n', help="Number of outputs (default 1)", default=1)
@click.option('--num-change', '-c', help="Number of change outputs (default 1)", default=1)
@click.option('--value', '-v', help="Total BTC value of inputs (integer, default 3)", default=3)
@click.option('--fee', '-f', help="Miner's fee in Satoshis", default=1000)
@click.option('--styles', '-a', help="Output address style (multiple ok)", multiple=True, default=None, type=click.Choice(ADDR_STYLES))
@click.option('--base64', '-6', help="Output base64 (default binary)", is_flag=True, default=False)
def faker(num_change, num_outs, out_psbt, value, testnet, xpub, segwit, fee, styles, base64):
'''Construct a valid PSBT which spends non-existant BTC to random addresses!'''
global TESTNET
TESTNET = testnet
num_ins = int(value)
total_outs = num_outs + num_change
''' Match lines like:
m/0'/0'/0' => n3ieqYKgVR8oB2zsHVX1Pr7Zc31pP3C7ZJ
m/0/2 => mh7finD8ctq159hbRzAeevSuFBJ1NQjoH2
and also
m => tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh
'''
chg_style = 'p2pkh' if not segwit else 'p2wpkh'
psbt = BasicPSBT()
if not styles:
styles = [chg_style]
for path, addr in addrs:
print(f"addr: {addr} ... ", end='')
rr = explora('address', addr, 'utxo')
if not rr:
print('nada')
continue
here = 0
for u in rr:
here += u['value']
tt = TxIn(h2b_rev(u['txid']), u['vout'])
spending.append(tt)
#print(rr)
pin = BasicPSBTInput(idx=len(psbt.inputs))
psbt.inputs.append(pin)
pubkey = calc_pubkey(xpubs, path)
pin.bip32_paths[pubkey] = str2path(xfp, path)
# fetch the UTXO for witness signging
td = explora('tx', u['txid'], 'hex', is_json=False)
outpt = Tx.from_hex(td.decode('ascii')).txs_out[u['vout']]
with BytesIO() as b:
outpt.stream(b)
pin.witness_utxo = b.getvalue()
psbt, outs = fake_txn(num_ins, total_outs, master_xpub=xpub, fee=fee,
segwit_in=segwit, outstyles=styles, change_style=chg_style,
is_testnet=testnet, change_outputs=list(range(num_outs, num_outs+num_change)))
print('%.8f BTC' % (here / 1E8))
total += here
out_psbt.write(psbt if not base64 else b64encode(psbt))
if len(spending) > 15:
print("Reached practical limit on # of inputs. "
"You'll need to repeat this process again later.")
break
print(f"\nFake PSBT would send {num_ins} BTC to: ")
print('\n'.join(" %.8f => %s %s" % (amt,dest, ' (change back)' if chg else '') for amt,dest,chg in outs))
if fee:
print(" %.8f => miners fee" % (Decimal(fee)/Decimal(1E8)))
assert total
print("Found total: %.8f BTC" % (total / 1E8))
print("Planning to send to: %s" % payout_address)
dest_scr = standard_tx_out_script(payout_address)
txn = Tx(2,spending,[TxOut(total, dest_scr)])
fee = tx_fee.recommended_fee_for_tx(txn)
# placeholder, single output that isn't change
pout = BasicPSBTOutput(idx=0)
psbt.outputs.append(pout)
print("Guestimate fee: %.8f BTC" % (fee / 1E8))
txn.txs_out[0].coin_value -= fee
# write txn into PSBT
with BytesIO() as b:
txn.stream(b)
psbt.txn = b.getvalue()
out_psbt.write(psbt.as_bytes())
print("PSBT to be signed:\n\n\t" + out_psbt.name, end='\n\n')
#print("\nPSBT to be signed: " + out_psbt.name, end='\n\n')
if __name__ == '__main__':
recovery()
faker()
# EOF

211
txn.py Normal file
View File

@ -0,0 +1,211 @@
#
# Creating fake transactions. Not simple... but only for testing purposes, so ....
#
import time, os, random
from binascii import b2a_hex, a2b_hex
from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput, PSBT_IN_REDEEM_SCRIPT
from io import BytesIO
from pprint import pprint, pformat
from decimal import Decimal
from pycoin.key.BIP32Node import BIP32Node
# all possible addr types, including multisig/scripts
ADDR_STYLES = ['p2wpkh', 'p2wsh', 'p2sh', 'p2pkh', 'p2wsh-p2sh', 'p2wpkh-p2sh']
# single-signer
ADDR_STYLES_SINGLE = ['p2wpkh', 'p2pkh', 'p2wpkh-p2sh']
def prandom(count):
# make some bytes, randomly, but not: deterministic
return bytes(random.randint(0, 255) for i in range(count))
def fake_dest_addr(style='p2pkh'):
# Make a plausible output address, but it's random garbage. Cant use for change outs
# See CTxOut.get_address() in ../shared/serializations
if style == 'p2wpkh':
return bytes([0, 20]) + prandom(20)
if style == 'p2wsh':
return bytes([0, 32]) + prandom(32)
if style in ['p2sh', 'p2wsh-p2sh', 'p2wpkh-p2sh']:
# all equally bogus P2SH outputs
return bytes([0xa9, 0x14]) + prandom(20) + bytes([0x87])
if style == 'p2pkh':
return bytes([0x76, 0xa9, 0x14]) + prandom(20) + bytes([0x88, 0xac])
# missing: if style == 'p2pk' => pay to pubkey, considered obsolete
raise ValueError('not supported: ' + style)
def make_change_addr(wallet, style):
# provide script, pubkey and xpath for a legit-looking change output
import struct, random
from pycoin.encoding import hash160
redeem_scr, actual_scr = None, None
deriv = [12, 34, random.randint(0, 1000)]
xfp, = struct.unpack('I', wallet.fingerprint())
dest = wallet.subkey_for_path('/'.join(str(i) for i in deriv))
target = dest.hash160()
assert len(target) == 20
is_segwit = False
if style == 'p2pkh':
redeem_scr = bytes([0x76, 0xa9, 0x14]) + target + bytes([0x88, 0xac])
elif style == 'p2wpkh':
redeem_scr = bytes([0, 20]) + target
is_segwit = True
elif style == 'p2wpkh-p2sh':
redeem_scr = bytes([0, 20]) + target
actual_scr = bytes([0xa9, 0x14]) + hash160(redeem_scr) + bytes([0x87])
else:
raise ValueError('cant make fake change output of type: ' + style)
return redeem_scr, actual_scr, is_segwit, dest.sec(), struct.pack('4I', xfp, *deriv)
def fake_txn(num_ins, num_outs, master_xpub=None, subpath="0/%d", fee=10000,
outvals=None, segwit_in=False, outstyles=['p2pkh'], is_testnet=False,
change_style='p2pkh',
change_outputs=[]):
# make various size txn's ... completely fake and pointless values
# - but has UTXO's to match needs
# - input total = num_inputs * 1BTC
from pycoin.tx.Tx import Tx
from pycoin.tx.TxIn import TxIn
from pycoin.tx.TxOut import TxOut
from pycoin.serialize import h2b_rev
from struct import pack
psbt = BasicPSBT()
txn = Tx(2,[],[])
# we have a key; use it to provide "plausible" value inputs
mk = BIP32Node.from_wallet_key(master_xpub)
xfp = mk.fingerprint()
psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)]
psbt.outputs = [BasicPSBTOutput(idx=i) for i in range(num_outs)]
outputs = []
for i in range(num_ins):
# make a fake txn to supply each of the inputs
# - each input is 1BTC
# addr where the fake money will be stored.
subkey = mk.subkey_for_path(subpath % i)
sec = subkey.sec()
assert len(sec) == 33, "expect compressed"
assert subpath[0:2] == '0/'
psbt.inputs[i].bip32_paths[sec] = xfp + pack('<II', 0, i)
# UTXO that provides the funding for to-be-signed txn
supply = Tx(2,[TxIn(pack('4Q', 0xdead, 0xbeef, 0, 0), 73)],[])
scr = bytes([0x76, 0xa9, 0x14]) + subkey.hash160() + bytes([0x88, 0xac])
supply.txs_out.append(TxOut(1E8, scr))
with BytesIO() as fd:
if not segwit_in:
supply.stream(fd)
psbt.inputs[i].utxo = fd.getvalue()
else:
supply.txs_out[-1].stream(fd)
psbt.inputs[i].witness_utxo = fd.getvalue()
spendable = TxIn(supply.hash(), 0)
txn.txs_in.append(spendable)
for i in range(num_outs):
is_change = False
# random P2PKH
if not outstyles:
style = ADDR_STYLES[i % len(ADDR_STYLES)]
else:
style = outstyles[i % len(outstyles)]
if i in change_outputs:
scr, act_scr, isw, pubkey, sp = make_change_addr(mk, change_style)
psbt.outputs[i].bip32_paths[pubkey] = sp
is_change = True
else:
scr = act_scr = fake_dest_addr(style)
isw = ('w' in style)
assert scr
act_scr = act_scr or scr
if isw:
psbt.outputs[i].witness_script = scr
elif style.endswith('sh'):
psbt.outputs[i].redeem_script = scr
if not outvals:
h = TxOut(round(((1E8*num_ins)-fee) / num_outs, 4), act_scr)
else:
h = TxOut(outvals[i], act_scr)
outputs.append( (Decimal(h.coin_value)/Decimal(1E8), act_scr, is_change) )
txn.txs_out.append(h)
with BytesIO() as b:
txn.stream(b)
psbt.txn = b.getvalue()
rv = BytesIO()
psbt.serialize(rv)
return rv.getvalue(), [(n, render_address(s, is_testnet), ic) for n,s,ic in outputs]
def render_address(script, testnet=True):
# take a scriptPubKey (part of the TxOut) and convert into conventional human-readable
# string... aka: the "payment address"
from pycoin.encoding import b2a_hashed_base58
from pycoin.contrib.segwit_addr import encode as bech32_encode
ll = len(script)
if not testnet:
bech32_hrp = 'bc'
b58_addr = bytes([0])
b58_script = bytes([5])
b58_privkey = bytes([128])
else:
bech32_hrp = 'tb'
b58_addr = bytes([111])
b58_script = bytes([196])
b58_privkey = bytes([239])
# P2PKH
if ll == 25 and script[0:3] == b'\x76\xA9\x14' and script[23:26] == b'\x88\xAC':
return b2a_hashed_base58(b58_addr + script[3:3+20])
# P2SH
if ll == 23 and script[0:2] == b'\xA9\x14' and script[22] == 0x87:
return b2a_hashed_base58(b58_script + script[2:2+20])
# P2WPKH
if ll == 22 and script[0:2] == b'\x00\x14':
return bech32_encode(bech32_hrp, 0, script[2:])
# P2WSH
if ll == 34 and script[0:2] == b'\x00\x20':
return bech32_encode(bech32_hrp, 0, script[2:])
raise ValueError('Unknown payment script', repr(script))
# EOF