Skip to content

Instantly share code, notes, and snippets.

@AdamISZ
Last active April 3, 2023 20:14
Show Gist options
  • Save AdamISZ/439dd00994025c60162a87267c610eb4 to your computer and use it in GitHub Desktop.
Save AdamISZ/439dd00994025c60162a87267c610eb4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python2
from __future__ import print_function
"""Simple illustration of 2 stage process:
1. Prepare a single (input, output) pair from some wallet
you have, and sign it with SIGHASH_SINGLE|SIGHASH_ANYONECANPAY
(the idea is that a utxo splitter service can do this for a client)
2. Take that partial transaction (serialized), and by feeding in
data from an input file gathered from Electrum GUI, add another
input and output (only one input here, should be OK to increase to more inputs),
specified by the user, and emit a transaction serialized according to
Electrum's custom partially-signed-transaction format.
3. That output can be passed into the Electrum GUI (Tools->Load Transaction),
then reviewed, then signed, then broadcast.
Tested to work for Standard wallet type (and service's utxo cannot be segwit).
To actually run this would require Joinmarket (the non-segwit version at
https://github.com/Joinmarket-Org/joinmarket), but that isn't really the idea.
"""
import binascii
import os
import sys
import random
from optparse import OptionParser
from pprint import pformat
import bitcoin as btc
from joinmarket import load_program_config
from joinmarket import validate_address, jm_single
from joinmarket import Wallet, sync_wallet
from joinmarket.wallet import estimate_tx_fee
from getpass import getpass
def get_parser():
parser = OptionParser(
usage=
'usage: %prog [options] walletfile electruminputfile\n',
description='Test of single|acp signing then prepare to sign in Electrum ')
parser.add_option(
'-r',
'--amount-range',
type='int',
nargs=2,
dest='amountrange',
default=(1000000, 2000000),
help=
'Spend a utxo between these two values in satoshis')
parser.add_option(
'-m',
'--mixdepth',
type='int',
dest='mixdepth',
help=
'Mixing depth to source utxo from. default=0.',
default=0)
parser.add_option('-a',
'--amtmixdepths',
action='store',
type='int',
dest='amtmixdepths',
help='number of mixdepths in wallet, default 5',
default=5)
parser.add_option('-g',
'--gap-limit',
type="int",
action='store',
dest='gaplimit',
help='gap limit for wallet, default=6',
default=6)
parser.add_option('--fast',
action='store_true',
dest='fastsync',
default=False,
help=('choose to do fast wallet sync, only for Core and '
'only for previously synced wallet'))
return parser
#grab a joinmarket wallet, not relevant for most readers:
def cli_get_wallet(wallet_name, sync=True):
walletclass = Wallet
if not os.path.exists(os.path.join('wallets', wallet_name)):
wallet = walletclass(wallet_name, None, max_mix_depth=options.amtmixdepths)
else:
while True:
try:
wallet = walletclass(wallet_name, max_mix_depth=options.amtmixdepths)
except Exception as e:
print("Failed to load wallet, error message: " + repr(e))
sys.exit(0)
break
if jm_single().config.get("BLOCKCHAIN",
"blockchain_source") == "electrum-server":
jm_single().bc_interface.synctype = "with-script"
if sync:
sync_wallet(wallet, fast=options.fastsync)
return wallet
#Next 3 functions are simple hack taken from Electrum, serialize
#the derivation path in two sets of 2 bytes.
def rev_hex(s):
return s.decode('hex')[::-1].encode('hex')
def int_to_hex(i, length=1):
s = hex(i)[2:].rstrip('L')
s = "0"*(2*length - len(s)) + s
return rev_hex(s)
def serialize_derivation(roc, i):
x = ''.join(map(lambda x: int_to_hex(x, 2), (roc, i)))
print("returning: ", x)
raw_input()
return x
if __name__ == "__main__":
parser = get_parser()
(options, args) = parser.parse_args()
load_program_config()
#This half is what's done server-side so obviously here is just for testing:
#===========================================================================
#We're going to create a 1-in, 1-out tx but signed with single|acp; these happen
#to be JM wallets, p2pkh (note, Electrum's latest release doesn't support segwit
#and seems unable to sign if other inputs are segwit (serialization seems to screw up).
wallet = cli_get_wallet(args[0])
candidates = wallet.get_utxos_by_mixdepth()[options.mixdepth]
myutxoin = None
for k, v in candidates.iteritems():
if v['value'] > options.amountrange[0] and v['value'] < options.amountrange[1]:
myutxoin = k
myinvalue = v['value']
myinaddress = v['address']
break
if not myutxoin:
print("Failed to find a suitable utxo candidate, alter the mixdepth or the amount range.")
exit(0)
mydestination = wallet.get_new_addr((options.mixdepth+1)%options.amtmixdepths, 1)
print('using destination: ', mydestination)
out = {"address": mydestination, "value": myinvalue}
tx = btc.mktx([myutxoin], [out])
part_signed_tx = btc.sign(tx, 0, wallet.get_key_from_addr(myinaddress),
hashcode=btc.SIGHASH_ANYONECANPAY|btc.SIGHASH_SINGLE)
deser_part_signed_tx = btc.deserialize(part_signed_tx)
sig = deser_part_signed_tx['ins'][0]['script']
print('got this script as signature: ')
print(sig)
print('got partially signed: ')
print(pformat(btc.deserialize(part_signed_tx)))
raw_input("Continue to Stage 2 ..")
#==============================================================================
#Stage 2 is to take the (a) master public key of the destination wallet and
#(b) the HD path for the source address (just one for now),
#along with (c) the utxo to spend, and (d) the amount it contains.
#Given these 4 pieces of information we can create a new partially signed transaction
#in Electrum's custom serialization such that Electrum can recognize and sign it
#when it's loaded with "Load Transaction".
#Populate the file that you specified as second argument with fields according to
#the comment section below here.
with open(args[1], "rb") as f:
elecdata = f.read().strip()
electrum_mpubk, electrum_receive_or_change, electrum_index, electrum_utxo, electrum_amount, electrum_out_addr = elecdata.split('\n')
electrum_receive_or_change = int(electrum_receive_or_change)
electrum_index = int(electrum_index)
electrum_amount = int(electrum_amount)
"""
#Fields required to set up the transaction ready for signing, one per line in the input file:
#
electrum_mpubk: Electrum wallet master public key, extract from GUI
#
electrum_receive_or_change: Use addresses tab, the branch for your input;
0 for 'receive' or 1 for 'change'
#
electrum_index: This one is very annoying; if you do "electrum listaddresses" you'll get
the simple linear list of addresses, note it's 0-indexed so the 4th should be value 3.
In the GUI, you can't find it by counting unless you followed completely standard usage pattern.
#
electrum_utxo: Can be retrieved by right clicking on an address that has non-zero
'Tx' entry on right, then right click on the desired transaction inside that window.
#
electrum_amount: also available from the above Tx details dialog.
#
electrum_out_addr: user-specified destination address.
"""
#Now we start building the partially signed, electrum serialized transaction:
ins = [myutxoin, electrum_utxo]
#replace this with your favourite fee estimation:
fee = estimate_tx_fee(2, 2) #default here is p2pkh
print('got fee of: ', fee, ' satoshis')
print('subtracting that from output: ', electrum_amount)
#This currently has a zero btc increase for the service, easy to modify:
outs = [out, {"address": electrum_out_addr, "value": electrum_amount - fee}]
unsigned_tx = btc.mktx(ins, outs)
deser_unsigned_tx = btc.deserialize(unsigned_tx)
#Firstly, we must reapply the signature we already created in Stage 1
deser_unsigned_tx['ins'][0]['script'] = sig
#prepare the non-b58 encoded master pub key:
rawmpub = btc.changebase(electrum_mpubk, 58, 256)
#Ignore (doing checksum manually due to obscure bug)
assert btc.bin_dbl_sha256(rawmpub[:-4])[:4] == rawmpub[-4:]
rawmpub = rawmpub[:-4]
#For the user's input, set the scriptSig to the custom Elecrum format;
#there are some high level points at http://docs.electrum.org/en/latest/transactions.html#
#but further details below. The below applies to the Standard Wallet ('ff' prepending mpk).
#And the first 'ff' entry is a flag saying "the sig is null".
custom_scriptsig = btc.serialize_script(["\xff", "\xff" + rawmpub + binascii.unhexlify(
serialize_derivation(electrum_receive_or_change,
electrum_index))])
print('Got custom scriptSig: ', binascii.hexlify(custom_scriptsig))
deser_unsigned_tx['ins'][1]['script'] = binascii.hexlify(custom_scriptsig)
print("Here is the customised Electrum transaction, copy and paste into 'Load Transaction' dialog:")
print(btc.serialize(deser_unsigned_tx))
print('done')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment