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 18, 2014
1 parent b945034 commit 8b64b5c
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 41 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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 @@ -160,11 +160,14 @@ def market(give_asset, get_asset):
def cli(method, params, unsigned):
# Get unsigned transaction serialisation.

is_multisig = script.is_multisig(params['source'])
params['source'] = script.make_canonical(params['source'])
pubkey = None

if not is_multisig:
if script.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 @@ -195,7 +198,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 @@ -205,7 +208,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 script.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 Expand Up @@ -496,7 +499,7 @@ def set_options(data_dir=None, backend_rpc_connect=None,
def balances(address):
address = script.canonical(address)
script.validate(address)
balances = get_address(db, address=address)['balances']
balances = get_address(db, address=addr)['balances']
table = PrettyTable(['Asset', 'Amount'])
btc_balance = backend.get_btc_balance(address)
table.add_row([config.BTC, btc_balance]) # BTC
Expand Down
2 changes: 1 addition & 1 deletion lib/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,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
30 changes: 19 additions & 11 deletions lib/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,17 +221,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 AddressError('Public key for address ‘{}’ not published in blockchain.'.format(pubkeyhash))
def multisig_pubkeyhashes_to_pubkeys(address):
signatures_required, pubkeyhashes, signatures_possible = 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 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 construct_array(signatures_required, pubkeys, signatures_possible)

Expand Down
46 changes: 23 additions & 23 deletions lib/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,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 @@ -205,8 +205,8 @@ def serialise (block_index, encoding, inputs, destination_outputs, data_output=N
tx_script += data_pubkey_1 # (Fake) public key (1/2)
tx_script += op_push(33) # Push bytes of data chunk (fake) public key (2/2)
tx_script += data_pubkey_2 # (Fake) public key (2/2)
tx_script += op_push(len(dust_return_public_key)) # Push bytes of source public key
tx_script += dust_return_public_key # Source public key
tx_script += op_push(len(dust_return_pubkey)) # Push bytes of source public key
tx_script += dust_return_pubkey # Source public key
tx_script += OP_3 # OP_3
tx_script += OP_CHECKMULTISIG # OP_CHECKMULTISIG
else:
Expand All @@ -215,8 +215,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.
tx_script = OP_1 # OP_1
tx_script += op_push(len(dust_return_public_key)) # Push bytes of source public key
tx_script += dust_return_public_key # Source public key
tx_script += op_push(len(dust_return_pubkey)) # Push bytes of source public key
tx_script += dust_return_pubkey # Source public key
tx_script += op_push(len(data_chunk)) # Push bytes of data chunk (fake) public key
tx_script += data_chunk # (Fake) public key
tx_script += OP_2 # OP_2
Expand Down Expand Up @@ -266,7 +266,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 @@ -295,7 +295,7 @@ def construct (db, tx_info, encoding='auto', fee_per_kb=config.DEFAULT_FEE_PER_K
# Address.
script.validate(address)
if script.is_multisig(address):
destination_outputs_new.append((script.multisig_pubkeyhashes_to_pubkeys(address), value))
destination_outputs_new.append((script.multisig_pubkeyhashes_to_pubkeys(address, provided_pubkeys), value))
else:
destination_outputs_new.append((address, value))

Expand Down Expand Up @@ -363,27 +363,27 @@ def chunks(l, n):
if source:
script.validate(source)

self_public_key = None
# Get `dust_return_pubkey`, if necessary.
if encoding in ('multisig', 'pubkeyhash'):
if script.is_multisig(source):
a, self_pubkeys, b = script.extract_array(script.multisig_pubkeyhashes_to_pubkeys(source))
self_public_key = binascii.unhexlify(self_pubkeys[0])
a, self_pubkeys, b = script.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 Expand Up @@ -431,15 +431,15 @@ def chunks(l, n):

# Change output.
if script.is_multisig(source):
change_address = script.multisig_pubkeyhashes_to_pubkeys(source)
change_address = script.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
3 changes: 2 additions & 1 deletion test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,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 8b64b5c

Please sign in to comment.