Skip to content

Commit

Permalink
allow manually providing pubkeys for multi‐sig (fixes #413)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamkrellenstein committed Dec 16, 2014
1 parent 26ed67d commit 6a50840
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 43 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* code clean‐up
* mainnet burns are hard‐coded
* sanity checks for manually provided public and private keys
* allow manually providing pubkeys for multi‐sig addresses
* handle protocol changes more elegantly
* more sophisticated version checking
* removed obsolete `carefulness` CLI option
Expand Down
13 changes: 8 additions & 5 deletions counterparty-cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,14 @@ def market(give_asset, get_asset):
def cli(method, params, unsigned):
# Get unsigned transaction serialisation.

is_multisig = address.is_multisig(params['source'])
params['source'] = address.make_canonical(params['source'])
params['source'] = util.canonical_address(params['source'])
pubkey = None

if not is_multisig:
if address.is_multisig(params['source']):
answer = input('Public keys (hexadecimal, comma‐separated): ')
answer = anwser.replace(' ', '')
params['pubkey'] = answer.split(',')
else:
# Get public key for source.
source = params['source']
if not backend.is_valid(source):
Expand Down Expand Up @@ -200,7 +203,7 @@ def cli(method, params, unsigned):
regular_dust_size=params['regular_dust_size'],
multisig_dust_size=params['multisig_dust_size'],
op_return_value=params['op_return_value'],
self_public_key_hex=pubkey,
provided_pubkeys=pubkey,
allow_unconfirmed_inputs=params['allow_unconfirmed_inputs']))
exit(0)
"""
Expand All @@ -210,7 +213,7 @@ def cli(method, params, unsigned):
print('Transaction (unsigned):', unsigned_tx_hex)

# Ask to sign and broadcast (if not multi‐sig).
if is_multisig:
if address.is_multisig(params['source']):
print('Multi‐signature transactions are signed and broadcasted manually.')
elif not unsigned and input('Sign and broadcast? (y/N) ') == 'y':
if backend.is_mine(source):
Expand Down
2 changes: 1 addition & 1 deletion lib/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def compose_transaction(db, name, params,
regular_dust_size=regular_dust_size,
multisig_dust_size=multisig_dust_size,
op_return_value=op_return_value,
self_public_key_hex=pubkey,
provided_pubkeys=pubkey,
allow_unconfirmed_inputs=allow_unconfirmed_inputs,
exact_fee=fee,
fee_provided=fee_provided)
Expand Down
32 changes: 20 additions & 12 deletions lib/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import bitcoin as bitcoinlib
import binascii

from lib import (util, config, address)
from lib import util, config, address
from .exceptions import DecodeError

def hash160(x):
Expand Down Expand Up @@ -83,17 +83,25 @@ def private_key_to_public_key (private_key_wif):
return public_key_hex

def pubkeyhash_to_pubkey(pubkeyhash):
raw_transactions = blockchain.searchrawtransactions(pubkeyhash)
for tx in raw_transactions:
for vin in tx['vin']:
scriptsig = vin['scriptSig']
asm = scriptsig['asm'].split(' ')
pubkey = asm[1]
if pubkeyhash == script.pubkey_to_pubkeyhash(binascii.unhexlify(bytes(pubkey, 'utf-8'))):
return pubkey
raise exceptions.AddressError('Public key for address ‘{}’ not published in blockchain.'.format(pubkeyhash))
def multisig_pubkeyhashes_to_pubkeys(address):
signatures_required, pubkeyhashes, signatures_possible = address.extract_array(address)
if backend.is_mine(pubkeyhash):
# Derive from private key.
private_key_wif = backend.dumpprivkey(pubkeyhash)
pubkey = script.private_key_to_public_key(private_key_wif)
return pubkey
else:
# Search blockchain.
# TODO: Convert to python-bitcoinlib.
raw_transactions = blockchain.searchrawtransactions(pubkeyhash)
for tx in raw_transactions:
for vin in tx['vin']:
scriptsig = vin['scriptSig']
asm = scriptsig['asm'].split(' ')
pubkey = asm[1]
if pubkeyhash == script.pubkey_to_pubkeyhash(binascii.unhexlify(bytes(pubkey, 'utf-8'))):
return pubkey
raise exceptions.AddressError('Public key for address ‘{}’ not published in blockchain.'.format(pubkeyhash))
def multisig_pubkeyhashes_to_pubkeys (address, provided_pubkeys):
signatures_required, pubkeyhashes, signatures_possible = util.extract_array(address)
pubkeys = [pubkeyhash_to_pubkey(pubkeyhash) for pubkeyhash in pubkeyhashes]
return address.construct_array(signatures_required, pubkeys, signatures_possible)

Expand Down
48 changes: 24 additions & 24 deletions lib/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def make_fully_valid(pubkey):
return fully_valid_pubkey


def serialise (block_index, encoding, inputs, destination_outputs, data_output=None, change_output=None, dust_return_public_key=None):
def serialise (block_index, encoding, inputs, destination_outputs, data_output=None, change_output=None, dust_return_pubkey=None):
s = (1).to_bytes(4, byteorder='little') # Version

# Number of inputs.
Expand Down Expand Up @@ -200,8 +200,8 @@ def serialise (block_index, encoding, inputs, destination_outputs, data_output=N
script += data_pubkey_1 # (Fake) public key (1/2)
script += op_push(33) # Push bytes of data chunk (fake) public key (2/2)
script += data_pubkey_2 # (Fake) public key (2/2)
script += op_push(len(dust_return_public_key)) # Push bytes of source public key
script += dust_return_public_key # Source public key
script += op_push(len(dust_return_pubkey)) # Push bytes of source public key
script += dust_return_pubkey # Source public key
script += OP_3 # OP_3
script += OP_CHECKMULTISIG # OP_CHECKMULTISIG
else:
Expand All @@ -210,8 +210,8 @@ def serialise (block_index, encoding, inputs, destination_outputs, data_output=N
data_chunk = bytes([len(data_chunk)]) + data_chunk + (pad_length * b'\x00')
# Construct script.
script = OP_1 # OP_1
script += op_push(len(dust_return_public_key)) # Push bytes of source public key
script += dust_return_public_key # Source public key
script += op_push(len(dust_return_pubkey)) # Push bytes of source public key
script += dust_return_pubkey # Source public key
script += op_push(len(data_chunk)) # Push bytes of data chunk (fake) public key
script += data_chunk # (Fake) public key
script += OP_2 # OP_2
Expand Down Expand Up @@ -262,7 +262,7 @@ def construct (db, tx_info, encoding='auto', fee_per_kb=config.DEFAULT_FEE_PER_K
regular_dust_size=config.DEFAULT_REGULAR_DUST_SIZE,
multisig_dust_size=config.DEFAULT_MULTISIG_DUST_SIZE,
op_return_value=config.DEFAULT_OP_RETURN_VALUE,
exact_fee=None, fee_provided=0, self_public_key_hex=None,
exact_fee=None, fee_provided=0, provided_pubkeys=None,
allow_unconfirmed_inputs=False):

block_index = util.last_block(db)['block_index']
Expand Down Expand Up @@ -291,12 +291,12 @@ def construct (db, tx_info, encoding='auto', fee_per_kb=config.DEFAULT_FEE_PER_K
# Address.
address.validate(addr)
if address.is_multisig(addr):
destination_outputs_new.append((script.multisig_pubkeyhashes_to_pubkeys(addr), value))
destination_outputs_new.append((script.multisig_pubkeyhashes_to_pubkeys(addr, provided_pubkeys), value))
else:
destination_outputs_new.append((addr, value))
destination_outputs_new.append((address, value))

destination_outputs = destination_outputs_new
destination_btc_out = sum([value for addr, value in destination_outputs])
destination_btc_out = sum([value for address, value in destination_outputs])


'''Data'''
Expand Down Expand Up @@ -359,27 +359,27 @@ def chunks(l, n):
if source:
address.validate(source)

self_public_key = None
# Get `dust_return_pubkey`, if necessary.
if encoding in ('multisig', 'pubkeyhash'):
if address.is_multisig(source):
a, self_pubkeys, b = address.extract_array(script.multisig_pubkeyhashes_to_pubkeys(source))
self_public_key = binascii.unhexlify(self_pubkeys[0])
if util.is_multisig(source):
a, self_pubkeys, b = address.extract_array(script.multisig_pubkeyhashes_to_pubkeys(source, provided_pubkeys))
self_public_key_hex = self_pubkeys[0]
else:
if not self_public_key_hex:
if not provided_pubkeys:
# If public key was not provided, derive it from the private key.
private_key_wif = backend.dumpprivkey(source)
self_public_key_hex = script.private_key_to_public_key(private_key_wif)
else:
# If public key was provided, check that it matches the source address.
if source != script.pubkey_to_pubkeyhash(binascii.unhexlify(self_public_key_hex)):
raise InputError('provided public key does not match the source address')

# Convert hex public key into binary public key.
try:
self_public_key = binascii.unhexlify(self_public_key_hex)
is_compressed = is_sec_compressed(self_public_key)
except (EncodingError, binascii.Error):
raise InputError('Invalid private key.')
self_public_key_hex = script.pubkeyhash_to_pubkey(source)

# Convert hex public key into binary public key.
try:
dust_return_pubkey = binascii.unhexlify(self_public_key_hex)
is_compressed = is_sec_compressed(dust_return_pubkey)
except (EncodingError, binascii.Error):
raise InputError('Invalid private key.')
else:
dust_return_pubkey = None

# Calculate collective size of outputs.
if encoding == 'multisig': data_output_size = 81 # 71 for the data
Expand Down
3 changes: 2 additions & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def date_passed(date):
def init_api_access_log():
pass

def multisig_pubkeyhashes_to_pubkeys(address):
def multisig_pubkeyhashes_to_pubkeys(address, provided_pubkeys):
# TODO: Should be updated?!
array = address.split('_')
signatures_required = int(array[0])
pubkeyhashes = array[1:-1]
Expand Down

0 comments on commit 6a50840

Please sign in to comment.