unsplit MS support, many TODOs still

This commit is contained in:
Peter D. Gray 2025-04-17 09:55:28 -04:00
parent d22631028a
commit 3d0ec47333
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
4 changed files with 65 additions and 37 deletions

View File

@ -24,6 +24,7 @@ rehash
## Usage
```sh
!!
$ psbt_faker --help
Usage: psbt_faker [OPTIONS] OUTPUT.PSBT [XPUB]

14
ms-example.txt Normal file
View File

@ -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

View File

@ -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__':

View File

@ -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