Last active
April 3, 2023 20:14
-
-
Save AdamISZ/439dd00994025c60162a87267c610eb4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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