sign whole transaction body instead of partial transaction

This commit is contained in:
Scott Sadler 2017-02-28 11:27:55 +01:00
parent 4a5a5566e7
commit bae05e80a0
3 changed files with 44 additions and 63 deletions

View File

@ -686,22 +686,17 @@ class Transaction(object):
key_pairs = {gen_public_key(PrivateKey(private_key)): key_pairs = {gen_public_key(PrivateKey(private_key)):
PrivateKey(private_key) for private_key in private_keys} PrivateKey(private_key) for private_key in private_keys}
for index, input_ in enumerate(self.inputs): tx_dict = self.to_dict()
# NOTE: We clone the current transaction but only add the output tx_dict = Transaction._remove_signatures(tx_dict)
# and input we're currently working on plus all tx_serialized = Transaction._to_str(tx_dict)
# previously signed ones. for i, input_ in enumerate(self.inputs):
tx_partial = Transaction(self.operation, self.asset, [input_], message = '%s:%s' % (i, tx_serialized)
self.outputs, self.metadata, self.inputs[i] = self._sign_input(input_, message, key_pairs)
self.version)
tx_partial_dict = tx_partial.to_dict()
tx_partial_dict = Transaction._remove_signatures(tx_partial_dict)
tx_serialized = Transaction._to_str(tx_partial_dict)
self._sign_input(input_, index, tx_serialized, key_pairs)
return self return self
def _sign_input(self, input_, index, tx_serialized, key_pairs): @classmethod
"""Signs a single Input with a partial Transaction as message. def _sign_input(cls, input_, message, key_pairs):
"""Signs a single Input.
Note: Note:
This method works only for the following Cryptoconditions This method works only for the following Cryptoconditions
@ -712,31 +707,27 @@ class Transaction(object):
Args: Args:
input_ (:class:`~bigchaindb.common.transaction. input_ (:class:`~bigchaindb.common.transaction.
Input`) The Input to be signed. Input`) The Input to be signed.
index (int): The index of the input to be signed. message (str): The message to be signed
tx_serialized (str): The Transaction to be used as message.
key_pairs (dict): The keys to sign the Transaction with. key_pairs (dict): The keys to sign the Transaction with.
""" """
if isinstance(input_.fulfillment, Ed25519Fulfillment): if isinstance(input_.fulfillment, Ed25519Fulfillment):
self._sign_simple_signature_fulfillment(input_, index, return cls._sign_simple_signature_fulfillment(input_, message,
tx_serialized, key_pairs) key_pairs)
elif isinstance(input_.fulfillment, ThresholdSha256Fulfillment): elif isinstance(input_.fulfillment, ThresholdSha256Fulfillment):
self._sign_threshold_signature_fulfillment(input_, index, return cls._sign_threshold_signature_fulfillment(input_, message,
tx_serialized, key_pairs)
key_pairs)
else: else:
raise ValueError("Fulfillment couldn't be matched to " raise ValueError("Fulfillment couldn't be matched to "
'Cryptocondition fulfillment type.') 'Cryptocondition fulfillment type.')
def _sign_simple_signature_fulfillment(self, input_, index, @classmethod
tx_serialized, key_pairs): def _sign_simple_signature_fulfillment(cls, input_, message, key_pairs):
"""Signs a Ed25519Fulfillment. """Signs a Ed25519Fulfillment.
Args: Args:
input_ (:class:`~bigchaindb.common.transaction. input_ (:class:`~bigchaindb.common.transaction.
Input`) The input to be signed. Input`) The input to be signed.
index (int): The index of the input to be message (str): The message to be signed
signed.
tx_serialized (str): The Transaction to be used as message.
key_pairs (dict): The keys to sign the Transaction with. key_pairs (dict): The keys to sign the Transaction with.
""" """
# NOTE: To eliminate the dangers of accidentally signing a condition by # NOTE: To eliminate the dangers of accidentally signing a condition by
@ -748,23 +739,21 @@ class Transaction(object):
try: try:
# cryptoconditions makes no assumptions of the encoding of the # cryptoconditions makes no assumptions of the encoding of the
# message to sign or verify. It only accepts bytestrings # message to sign or verify. It only accepts bytestrings
input_.fulfillment.sign(tx_serialized.encode(), key_pairs[public_key]) input_.fulfillment.sign(message.encode(), key_pairs[public_key])
except KeyError: except KeyError:
raise KeypairMismatchException('Public key {} is not a pair to ' raise KeypairMismatchException('Public key {} is not a pair to '
'any of the private keys' 'any of the private keys'
.format(public_key)) .format(public_key))
self.inputs[index] = input_ return input_
def _sign_threshold_signature_fulfillment(self, input_, index, @classmethod
tx_serialized, key_pairs): def _sign_threshold_signature_fulfillment(cls, input_, message, key_pairs):
"""Signs a ThresholdSha256Fulfillment. """Signs a ThresholdSha256Fulfillment.
Args: Args:
input_ (:class:`~bigchaindb.common.transaction. input_ (:class:`~bigchaindb.common.transaction.
Input`) The Input to be signed. Input`) The Input to be signed.
index (int): The index of the Input to be message (str): The message to be signed
signed.
tx_serialized (str): The Transaction to be used as message.
key_pairs (dict): The keys to sign the Transaction with. key_pairs (dict): The keys to sign the Transaction with.
""" """
input_ = deepcopy(input_) input_ = deepcopy(input_)
@ -794,8 +783,8 @@ class Transaction(object):
# cryptoconditions makes no assumptions of the encoding of the # cryptoconditions makes no assumptions of the encoding of the
# message to sign or verify. It only accepts bytestrings # message to sign or verify. It only accepts bytestrings
subffill.sign(tx_serialized.encode(), private_key) subffill.sign(message.encode(), private_key)
self.inputs[index] = input_ return input_
def inputs_valid(self, outputs=None): def inputs_valid(self, outputs=None):
"""Validates the Inputs in the Transaction against given """Validates the Inputs in the Transaction against given
@ -848,24 +837,19 @@ class Transaction(object):
raise ValueError('Inputs and ' raise ValueError('Inputs and '
'output_condition_uris must have the same count') 'output_condition_uris must have the same count')
def gen_tx(input_, output, output_condition_uri=None): tx_dict = self.to_dict()
"""Splits multiple IO Transactions into partial single IO tx_dict = Transaction._remove_signatures(tx_dict)
Transactions. tx_serialized = Transaction._to_str(tx_dict)
"""
tx = Transaction(self.operation, self.asset, [input_],
self.outputs, self.metadata, self.version)
tx_dict = tx.to_dict()
tx_dict = Transaction._remove_signatures(tx_dict)
tx_serialized = Transaction._to_str(tx_dict)
return self.__class__._input_valid(input_, def validate(i, output_condition_uri=None):
self.operation, """ Validate input against output condition URI """
tx_serialized, message = '%s:%s' % (i, tx_serialized)
output_condition_uri)
partial_transactions = map(gen_tx, self.inputs, return self._input_valid(self.inputs[i], self.operation, message,
self.outputs, output_condition_uris) output_condition_uri)
return all(partial_transactions)
return all(validate(i, cond)
for i, cond in enumerate(output_condition_uris))
@staticmethod @staticmethod
def _input_valid(input_, operation, tx_serialized, output_condition_uri=None): def _input_valid(input_, operation, tx_serialized, output_condition_uri=None):

View File

@ -49,4 +49,4 @@ Here's some explanation of the contents of a :ref:`transaction <transaction>`:
Later, when we get to the models for the block and the vote, we'll see that both include a signature (from the node which created it). You may wonder why transactions don't have signatures... The answer is that they do! They're just hidden inside the ``fulfillment`` string of each input. A creation transaction is signed by whoever created it. A transfer transaction is signed by whoever currently controls or owns it. Later, when we get to the models for the block and the vote, we'll see that both include a signature (from the node which created it). You may wonder why transactions don't have signatures... The answer is that they do! They're just hidden inside the ``fulfillment`` string of each input. A creation transaction is signed by whoever created it. A transfer transaction is signed by whoever currently controls or owns it.
What gets signed? For each input in the transaction, the "fullfillment message" that gets signed includes the ``operation``, ``data``, ``version``, ``id``, corresponding ``condition``, and the fulfillment itself, except with its fulfillment string set to ``null``. The computed signature goes into creating the ``fulfillment`` string of the input. What gets signed? For each input in the transaction, the "fullfillment message" that gets signed includes the JSON serialized body of the transaction, minus any fulfillment strings, and with "n:" prepended where n is the index of the input being signed. The computed signature goes into creating the ``fulfillment`` string of the input.

View File

@ -496,7 +496,8 @@ def test_validate_tx_simple_create_signature(user_input, user_output, user_priv,
tx = Transaction(Transaction.CREATE, asset_definition, [user_input], [user_output]) tx = Transaction(Transaction.CREATE, asset_definition, [user_input], [user_output])
expected = deepcopy(user_output) expected = deepcopy(user_output)
expected.fulfillment.sign(str(tx).encode(), PrivateKey(user_priv)) message = ('0:' + str(tx)).encode()
expected.fulfillment.sign(message, PrivateKey(user_priv))
tx.sign([user_priv]) tx.sign([user_priv])
assert tx.inputs[0].to_dict()['fulfillment'] == \ assert tx.inputs[0].to_dict()['fulfillment'] == \
@ -513,7 +514,6 @@ def test_invoke_simple_signature_fulfillment_with_invalid_params(utx,
with raises(KeypairMismatchException): with raises(KeypairMismatchException):
invalid_key_pair = {'wrong_pub_key': 'wrong_priv_key'} invalid_key_pair = {'wrong_pub_key': 'wrong_priv_key'}
utx._sign_simple_signature_fulfillment(user_input, utx._sign_simple_signature_fulfillment(user_input,
0,
'somemessage', 'somemessage',
invalid_key_pair) invalid_key_pair)
@ -524,13 +524,11 @@ def test_sign_threshold_with_invalid_params(utx, user_user2_threshold_input,
with raises(KeypairMismatchException): with raises(KeypairMismatchException):
utx._sign_threshold_signature_fulfillment(user_user2_threshold_input, utx._sign_threshold_signature_fulfillment(user_user2_threshold_input,
0,
'somemessage', 'somemessage',
{user3_pub: user3_priv}) {user3_pub: user3_priv})
with raises(KeypairMismatchException): with raises(KeypairMismatchException):
user_user2_threshold_input.owners_before = ['somewrongvalue'] user_user2_threshold_input.owners_before = ['somewrongvalue']
utx._sign_threshold_signature_fulfillment(user_user2_threshold_input, utx._sign_threshold_signature_fulfillment(user_user2_threshold_input,
0,
'somemessage', 'somemessage',
None) None)
@ -560,13 +558,11 @@ def test_validate_multiple_inputs(user_input, user_output, user_priv,
expected_first = deepcopy(tx) expected_first = deepcopy(tx)
expected_second = deepcopy(tx) expected_second = deepcopy(tx)
expected_first.inputs = [expected_first.inputs[0]]
expected_second.inputs = [expected_second.inputs[1]]
expected_first_bytes = str(expected_first).encode() expected_first_bytes = ('0:' + str(tx)).encode()
expected_first.inputs[0].fulfillment.sign(expected_first_bytes, expected_first.inputs[0].fulfillment.sign(expected_first_bytes,
PrivateKey(user_priv)) PrivateKey(user_priv))
expected_second_bytes = str(expected_second).encode() expected_second_bytes = ('1:' + str(tx)).encode()
expected_second.inputs[0].fulfillment.sign(expected_second_bytes, expected_second.inputs[0].fulfillment.sign(expected_second_bytes,
PrivateKey(user_priv)) PrivateKey(user_priv))
tx.sign([user_priv]) tx.sign([user_priv])
@ -596,10 +592,11 @@ def test_validate_tx_threshold_create_signature(user_user2_threshold_input,
tx = Transaction(Transaction.CREATE, asset_definition, tx = Transaction(Transaction.CREATE, asset_definition,
[user_user2_threshold_input], [user_user2_threshold_input],
[user_user2_threshold_output]) [user_user2_threshold_output])
message = ('0:' + str(tx)).encode()
expected = deepcopy(user_user2_threshold_output) expected = deepcopy(user_user2_threshold_output)
expected.fulfillment.subconditions[0]['body'].sign(str(tx).encode(), expected.fulfillment.subconditions[0]['body'].sign(message,
PrivateKey(user_priv)) PrivateKey(user_priv))
expected.fulfillment.subconditions[1]['body'].sign(str(tx).encode(), expected.fulfillment.subconditions[1]['body'].sign(message,
PrivateKey(user2_priv)) PrivateKey(user2_priv))
tx.sign([user_priv, user2_priv]) tx.sign([user_priv, user2_priv])
@ -861,7 +858,7 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub,
expected_input = deepcopy(inputs[0]) expected_input = deepcopy(inputs[0])
expected['id'] = transfer_tx['id'] expected['id'] = transfer_tx['id']
expected_input.fulfillment.sign(serialize(expected).encode(), expected_input.fulfillment.sign(('0:' + serialize(expected)).encode(),
PrivateKey(user_priv)) PrivateKey(user_priv))
expected_ffill = expected_input.fulfillment.serialize_uri() expected_ffill = expected_input.fulfillment.serialize_uri()
transfer_ffill = transfer_tx['inputs'][0]['fulfillment'] transfer_ffill = transfer_tx['inputs'][0]['fulfillment']