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 11, 2014
1 parent 88af49c commit 3d81864
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 45 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,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
11 changes: 7 additions & 4 deletions counterparty-cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,14 @@ def market (give_asset, get_asset):
def cli(method, params, unsigned):
# Get unsigned transaction serialisation.

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

if not is_multisig:
if util.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 bitcoin.is_valid(source):
Expand Down Expand Up @@ -234,7 +237,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 @@ -244,7 +247,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 util.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 bitcoin.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
107 changes: 68 additions & 39 deletions lib/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,40 @@ class InputError (Exception):
D = decimal.Decimal

def pubkeyhash_to_pubkey(pubkeyhash):
# 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):
if is_mine(pubkeyhash):
# Derive from private key.
private_key_wif = get_private_key(pubkeyhash)
pubkey = 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]

pubkeys = []
for pubkeyhash in pubkeyhashes:
if provided_pubkeys != None and pubkeyhash in [script.pubkey_to_pubkeyhash(pubkey) for pubkey in provided_pubkeys]:
# Public key(s) were provided.
for pubkey in provided_pubkeys:
if pubkeyhash == script.pubkey_to_pubkeyhash(pubkey):
pubkeys.append(pubkey)
break
pubkeys.append(pubkey)
else:
# Use private key or blockchain. (If neither of those works, fail.)
pubkey.append(pubkeyhash_to_pubkey(pubkeyhash))

assert len(pubkeys) == len(pubkeyhashes)
return util.construct_array(signatures_required, pubkeys, signatures_possible)

def print_coin(coin):
Expand Down Expand Up @@ -190,7 +211,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 @@ -257,8 +278,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 @@ -267,8 +288,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 @@ -364,12 +385,20 @@ def transaction (db, tx_info, encoding='auto', fee_per_kb=config.DEFAULT_FEE_PER
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']
(source, destination_outputs, data) = tx_info

if type(provided_pubkeys) == list:
assert util.is_multisig(source)
elif type(provided_pubkeys) == str:
assert not util.is_multisig(source)
elif provided_pubkeys == None:
pass
else:
assert False

'''Destinations'''

Expand All @@ -393,7 +422,7 @@ def transaction (db, tx_info, encoding='auto', fee_per_kb=config.DEFAULT_FEE_PER
# Address.
util.validate_address(address)
if util.is_multisig(address):
destination_outputs_new.append((multisig_pubkeyhashes_to_pubkeys(address), value))
destination_outputs_new.append((multisig_pubkeyhashes_to_pubkeys(address, provided_pubkeys), value))
else:
destination_outputs_new.append((address, value))

Expand Down Expand Up @@ -461,27 +490,26 @@ def chunks(l, n):
if source:
util.validate_address(source)

self_public_key = None
# Get `dust_return_pubkey`, if necessary.
if encoding in ('multisig', 'pubkeyhash'):
if util.is_multisig(source):
a, self_pubkeys, b = util.extract_array(multisig_pubkeyhashes_to_pubkeys(source))
self_public_key = binascii.unhexlify(self_pubkeys[0])
a, self_pubkeys, b = util.extract_array(multisig_pubkeyhashes_to_pubkeys(source, provided_pubkeys))
self_public_key_hex = self_pubkeys[0]
else:
if not self_public_key_hex:
# If public key was not provided, derive it from the private key.
private_key_wif = get_private_key(source)
self_public_key_hex = 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)):
if provided_pubkeys:
if source != script.pubkey_to_pubkeyhash(provided_pubkeys):
raise InputError('provided public key does not match the source address')
else:
self_public_key_hex = pubkeyhash_to_pubkey(source)

# 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.')
# 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 Expand Up @@ -529,15 +557,15 @@ def chunks(l, n):

# Change output.
if util.is_multisig(source):
change_address = multisig_pubkeyhashes_to_pubkeys(source)
change_address = multisig_pubkeyhashes_to_pubkeys(source, provided_pubkeys)
else:
change_address = source
if change_quantity: change_output = (change_address, change_quantity)
else: change_output = None


# Serialise inputs and outputs.
unsigned_tx = serialise(block_index, encoding, inputs, destination_outputs, data_output, change_output, dust_return_public_key=self_public_key)
unsigned_tx = serialise(block_index, encoding, inputs, destination_outputs, data_output, change_output, dust_return_pubkey=dust_return_pubkey)
unsigned_tx_hex = binascii.hexlify(unsigned_tx).decode('utf-8')

# Check that the constructed transaction isn’t doing anything funny.
Expand Down Expand Up @@ -611,12 +639,13 @@ def get_unspent_txouts(source, return_confirmed=False):
outputs = {}
if util.is_multisig(source):
pubkeyhashes = util.pubkeyhash_array(source)
raw_transactions = blockchain.searchrawtransactions(pubkeyhashes[1])
input_address = pubkeyhashes[1]
else:
pubkeyhashes = [source]
raw_transactions = blockchain.searchrawtransactions(source)
input_address = source

canonical_address = util.canonical_address(source)
raw_transactions = blockchain.searchrawtransactions(source)

for tx in raw_transactions:
for vout in tx['vout']:
Expand Down
3 changes: 2 additions & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,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 3d81864

Please sign in to comment.