diff --git a/README.md b/README.md index 736bda5..8a9cf4f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ rehash ## Usage ```sh +!! $ psbt_faker --help Usage: psbt_faker [OPTIONS] OUTPUT.PSBT [XPUB] diff --git a/ms-example.txt b/ms-example.txt new file mode 100644 index 0000000..06a6a5e --- /dev/null +++ b/ms-example.txt @@ -0,0 +1,14 @@ +# Example Coldcard Multisig setup file +# +# This includes the simulator's default key, and BIP-39 derived keys using passwords +# "Me", "Myself", "And I". It is a 2 of 4 (but that could be edited here) +# +Name: MeMyselfAndI +Policy: 2 of 4 + +Derivation: m/45h + +6BA6CFD0: tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9 +747B698E: tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc +7BB026BE: tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa +0F056943: tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n diff --git a/psbt_faker/__init__.py b/psbt_faker/__init__.py index 7453e06..600e602 100755 --- a/psbt_faker/__init__.py +++ b/psbt_faker/__init__.py @@ -20,11 +20,7 @@ b2a_hex = lambda a: str(_b2a_hex(a), 'ascii') SIM_XPUB = 'tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh' -@click.group() -def main(): - pass - -@main.command() +@click.command() @click.argument('out_psbt', type=click.File('wb'), metavar="OUTPUT.PSBT") @click.argument('xpub', type=str, default=SIM_XPUB) @click.option('--num-outs', '-n', help="Number of outputs (default 1)", default=1) @@ -37,67 +33,80 @@ def main(): @click.option('--testnet', '-t', help="Assume testnet3 addresses (default mainnet)", is_flag=True, default=False) @click.option('--partial', '-p', help="Change first input so its different XPUB and result cannot be finalized", is_flag=True, default=False) @click.option('--zero-xfp', '-z', help="Provide zero XFP and junk XPUB (cannot be signed, but should be decodable)", is_flag=True, default=False) -def faker(num_change, num_outs, out_psbt, value, testnet, xpub, segwit, fee, styles, base64, partial, zero_xfp): +@click.option('--multisig', '-m', type=click.File('rt'), metavar="config.txt", help="[MS] CC Multisig config file (text)", default=None) +@click.option('--locktime', '-l', help="[MS] nLocktime value (default current block height)", default=None) +@click.option('--input-amount', '-n', help="[MS] Size of each input in sats (default 100k sats each input)", default=100000) +@click.option('--legacy', help="[MS] Make inputs be legacy p2sh style", is_flag=True, default=False) +def main(num_change, num_outs, out_psbt, value, testnet, xpub, segwit, fee, styles, base64, partial, zero_xfp, multisig, locktime, input_amount, legacy): '''Construct a valid PSBT which spends non-existant BTC to random addresses!''' num_ins = int(value) total_outs = num_outs + num_change + # TODO: PSBTv2 if flag set + if zero_xfp: xpub = None - chg_style = 'p2pkh' if not segwit else 'p2wpkh' - if not styles: - styles = [chg_style] + if multisig: + # TODO: flag to include xpubs in header - psbt, outs = fake_txn(num_ins, total_outs, master_xpub=xpub, fee=fee, - segwit_in=segwit, outstyles=styles, change_style=chg_style, - partial=partial, - is_testnet=testnet, change_outputs=list(range(num_outs, num_outs+num_change))) + chg_style = 'p2sh' if not segwit else 'p2wsh' + if not styles: + styles = [chg_style] + + # TODO: slow getting this, better to estimate unless they want real value + # TODO: for single-sig too + # TODO: modulate value (today + 2 days, etc) for CCC testing/validation + if locktime is None: + try: + import urllib.request + u = urllib.request.urlopen("https://mempool.space/api/blocks/tip/height") + locktime = int(u.read().decode()) + except: + locktime = 0 + + ms_config = multisig.read() + name, af, keys, M, N = from_simple_text(ms_config.split("\n")) + psbt, outs = fake_ms_txn(num_ins, num_outs, M, keys, fee=fee, locktime=locktime, + change_outputs=list(range(num_change)), outstyles=styles, + segwit_in=not legacy, input_amount=input_amount) + + else: + chg_style = 'p2pkh' if not segwit else 'p2wpkh' + if not styles: + styles = [chg_style] + + psbt, outs = fake_txn(num_ins, total_outs, master_xpub=xpub, fee=fee, + segwit_in=segwit, outstyles=styles, change_style=chg_style, + partial=partial, + is_testnet=testnet, change_outputs=list(range(num_outs, num_outs+num_change))) out_psbt.write(psbt if not base64 else b64encode(psbt)) 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)) + 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))) #print("\nPSBT to be signed: " + out_psbt.name, end='\n\n') -@main.command() -@click.argument('ms_conf', type=click.File('r'), metavar="CC ms export") +''' +@main.command('ms') @click.argument('out_psbt', type=click.File('wb'), metavar="OUTPUT.PSBT") -@click.option('--input-amount', '-n', help="Size of each input in sats (default 100k sats each input)", default=100000) @click.option('--num-ins', help="Number of inputs (default 1)", default=1) @click.option('--num-outs', help="Number of outputs (default 1)", default=1) @click.option('--num-change', '-c', help="Number of change outputs (default 1)", default=1) @click.option('--fee', '-f', help="Miner's fee in Satoshis", default=1000) -@click.option('--locktime', '-l', help="nLocktime value (default current block height)", default=None) -@click.option('--legacy', help="Make inputs be legacy p2sh style", is_flag=True, default=False) @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 ms_faker(ms_conf, out_psbt, num_ins, num_change, num_outs, legacy, fee, styles, base64, locktime, input_amount): - '''Construct a valid multisig PSBT which spends non-existant BTC to random addresses!''' - - if locktime is None: - try: - import urllib.request - u = urllib.request.urlopen("https://mempool.space/api/blocks/tip/height") - locktime = int(u.read().decode()) - except: - locktime = 0 - - ms_config = ms_conf.read() - name, af, keys, M, N = from_simple_text(ms_config.split("\n")) - psbt = fake_ms_txn(num_ins, num_outs, M, keys, fee=fee, locktime=locktime, - change_outputs=list(range(num_change)), outstyles=styles, - segwit_in=not legacy, input_amount=input_amount) - - out_psbt.write(psbt if not base64 else b64encode(psbt)) + ''Construct a valid multisig PSBT which spends non-existant BTC to random addresses!''' if __name__ == '__main__': diff --git a/psbt_faker/txn.py b/psbt_faker/txn.py index 43c6d43..17cc5c1 100644 --- a/psbt_faker/txn.py +++ b/psbt_faker/txn.py @@ -326,6 +326,7 @@ def fake_ms_txn(num_ins, num_outs, M, keys, fee=10000, outvals=None, segwit_in=T spendable = CTxIn(COutPoint(supply.sha256, 0), nSequence=seq) txn.vin.append(spendable) + outputs = [] for i in range(num_outs): if not outstyles: style = ADDR_STYLES_MULTI[i % len(ADDR_STYLES_MULTI)] @@ -367,12 +368,15 @@ def fake_ms_txn(num_ins, num_outs, M, keys, fee=10000, outvals=None, segwit_in=T txn.vout.append(h) + # TODO FIXME # + #XXX#outputs.append( (Decimal(h.nValue)/Decimal(1E8), scr, (i not in change_outputs)) ) + psbt.txn = txn.serialize_with_witness() rv = BytesIO() psbt.serialize(rv) - return rv.getvalue() + return rv.getvalue(), [(n, render_address(s, 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