transaction.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. """ Defines transactions and their inputs and outputs. """
  2. import logging
  3. from collections import namedtuple
  4. from binascii import hexlify, unhexlify
  5. from typing import List, Set
  6. from .crypto import get_hasher, Signing
  7. __all__ = ['TransactionTarget', 'TransactionInput', 'Transaction']
  8. TransactionTarget = namedtuple("TransactionTarget", ["recipient_pk", "amount"])
  9. """
  10. The recipient of a transaction ('coin').
  11. :ivar recipient_pk: The public key of the recipient.
  12. :vartype recipient_pk: Signing
  13. :ivar amount: The amount sent to `recipient_pk`.
  14. :vartype amount: int
  15. """
  16. class TransactionInput(namedtuple("TransactionInput", ["transaction_hash", "output_idx"])):
  17. """
  18. One transaction input (pointer to 'coin').
  19. :ivar transaction_hash: The hash of the transaction that sent money to the sender.
  20. :vartype transaction_hash: bytes
  21. :ivar output_idx: The index into `Transaction.targets` of the `transaction_hash`.
  22. :vartype output_idx: int
  23. """
  24. @classmethod
  25. def from_json_compatible(cls, obj):
  26. """ Creates a new object of this class, from a JSON-serializable representation. """
  27. return cls(unhexlify(obj['transaction_hash']), int(obj['output_idx']))
  28. def to_json_compatible(self):
  29. """ Returns a JSON-serializable representation of this object. """
  30. return {
  31. 'transaction_hash': hexlify(self.transaction_hash).decode(),
  32. 'output_idx': self.output_idx,
  33. }
  34. class Transaction:
  35. """
  36. A transaction.
  37. :ivar inputs: The inputs of this transaction. Empty in the case of block reward transactions.
  38. :vartype inputs: List[TransactionInput]
  39. :ivar targets: The targets of this transaction.
  40. :vartype targets: List[TransactionTarget]
  41. :ivar signatures: Signatures for each input. Must be in the same order as `inputs`. Filled
  42. by :func:`sign`.
  43. :vartype signatures: List[bytes]
  44. :ivar iv: The IV is used to differentiate block reward transactions. These have no inputs and
  45. therefore would otherwise hash to the same value, when the target is identical.
  46. Reuse of IVs leads to inaccessible coins.
  47. :vartype iv: bytes
  48. """
  49. def __init__(self, inputs: 'List[TransactionInput]', targets: 'List[TransactionTarget]',
  50. signatures: 'List[bytes]'=None, iv: bytes=None):
  51. self.inputs = inputs
  52. self.targets = targets
  53. self.signatures = signatures or []
  54. self.iv = iv
  55. self._hash = None
  56. def to_json_compatible(self):
  57. """ Returns a JSON-serializable representation of this object. """
  58. val = {}
  59. val['inputs'] = []
  60. for inp in self.inputs:
  61. val['inputs'].append(inp.to_json_compatible())
  62. val['targets'] = []
  63. for targ in self.targets:
  64. val['targets'].append({
  65. 'recipient_pk': targ.recipient_pk.to_json_compatible(),
  66. 'amount': targ.amount,
  67. })
  68. val['signatures'] = []
  69. for sig in self.signatures:
  70. val['signatures'].append(hexlify(sig).decode())
  71. if self.iv is not None:
  72. val['iv'] = hexlify(self.iv).decode()
  73. return val
  74. @classmethod
  75. def from_json_compatible(cls, obj: dict):
  76. """ Creates a new object of this class, from a JSON-serializable representation. """
  77. inputs = []
  78. for inp in obj['inputs']:
  79. inputs.append(TransactionInput.from_json_compatible(inp))
  80. targets = []
  81. for targ in obj['targets']:
  82. if targ['amount'] <= 0:
  83. raise ValueError("invalid amount")
  84. targets.append(TransactionTarget(Signing.from_json_compatible(targ['recipient_pk']),
  85. int(targ['amount'])))
  86. signatures = []
  87. for sig in obj['signatures']:
  88. signatures.append(unhexlify(sig))
  89. iv = unhexlify(obj['iv']) if 'iv' in obj else None
  90. return cls(inputs, targets, signatures, iv)
  91. def get_hash(self) -> bytes:
  92. """ Hash this transaction. Returns raw bytes. """
  93. if self._hash is None:
  94. h = get_hasher()
  95. if self.iv is not None:
  96. h.update(self.iv)
  97. h.update(Block._int_to_bytes(len(self.targets)))
  98. for target in self.targets:
  99. h.update(Block._int_to_bytes(target.amount))
  100. h.update(target.recipient_pk.as_bytes())
  101. h.update(Block._int_to_bytes(len(self.inputs)))
  102. for inp in self.inputs:
  103. h.update(inp.transaction_hash)
  104. h.update(Block._int_to_bytes(inp.output_idx))
  105. self._hash = h.digest()
  106. return self._hash
  107. def sign(self, private_keys: 'List[Signing]'):
  108. """
  109. Sign this transaction with the given private keys. The private keys need
  110. to be in the same order as the inputs.
  111. """
  112. for private_key in private_keys:
  113. self.signatures.append(private_key.sign(self.get_hash()))
  114. def _verify_signatures(self, chain: 'Blockchain'):
  115. """ Verifies that all inputs are signed and the signatures are valid. """
  116. if len(self.signatures) != len(self.inputs):
  117. logging.warning("wrong number of signatures")
  118. return False
  119. for (s, i) in zip(self.signatures, self.inputs):
  120. if not self._verify_single_sig(s, i, chain):
  121. return False
  122. return True
  123. def _verify_single_sig(self, sig: bytes, inp: TransactionInput, chain: 'Blockchain') -> bool:
  124. """ Verifies the signature on a single input. """
  125. outp = chain.unspent_coins.get(inp)
  126. if outp is None:
  127. logging.warning("Referenced transaction input could not be found.")
  128. return False
  129. if not outp.recipient_pk.verify_sign(self.get_hash(), sig):
  130. logging.warning("Transaction signature does not verify.")
  131. return False
  132. return True
  133. def _verify_single_spend(self, chain: 'Blockchain', other_trans: set) -> bool:
  134. """ Verifies that all inputs have not been spent yet. """
  135. inp_set = set(self.inputs)
  136. if len(self.inputs) != len(inp_set):
  137. logging.warning("Transaction may not spend the same coin twice.")
  138. return False
  139. other_inputs = {i for t in other_trans for i in t.inputs}
  140. if other_inputs.intersection(inp_set):
  141. logging.warning("Transaction may not spend the same coin as another transaction in the"
  142. " same block.")
  143. return False
  144. if any(i not in chain.unspent_coins for i in self.inputs):
  145. logging.debug("Transaction refers to a coin that was already spent.")
  146. return False
  147. return True
  148. def get_transaction_fee(self, chain: 'Blockchain'):
  149. """ Computes the transaction fees this transaction provides. """
  150. if not self.inputs:
  151. return 0 # block reward transaction pays no fees
  152. input_amount = sum(chain.unspent_coins[inp].amount for inp in self.inputs)
  153. output_amount = sum(outp.amount for outp in self.targets)
  154. return input_amount - output_amount
  155. def _verify_amounts(self, chain: 'Blockchain') -> bool:
  156. """
  157. Verifies that transaction fees are non-negative and output amounts are positive.
  158. """
  159. if self.get_transaction_fee(chain) < 0:
  160. logging.warning("Transferred amounts are larger than the inputs.")
  161. return False
  162. if any(outp.amount <= 0 for outp in self.targets):
  163. logging.warning("Transferred amounts must be positive.")
  164. return False
  165. return True
  166. def verify(self, chain: 'Blockchain', other_trans: 'Set[Transaction]') -> bool:
  167. """ Verifies that this transaction is completely valid. """
  168. return self._verify_single_spend(chain, other_trans) and \
  169. self._verify_signatures(chain) and self._verify_amounts(chain)
  170. from .blockchain import Blockchain
  171. from .block import Block