-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtransaction.py
More file actions
326 lines (287 loc) · 12.9 KB
/
transaction.py
File metadata and controls
326 lines (287 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# pyncoin/transaction.py
''' Implements a cryptocurrency transaction. '''
import functools
import hashlib
from decimal import Decimal
import ecdsa
from utils import RawSerializable, bytes_to_int, int_to_bytes, bytes_to_hex, hex_to_bytes
from utils import BadRequestError, UnauthorizedError
def get_public_key(private_key):
''' Gets the public key from the private key.
Params:
- private_key (bytes): The private key
Returns (bytes): The public key corrisponding the private key.
'''
secexp = bytes_to_int(private_key)
sk = ecdsa.SigningKey.from_secret_exponent(secexp)
vk = sk.get_verifying_key()
return vk.to_string()
class TxOut(RawSerializable):
''' Transaction output. '''
def __init__(self, address, amount):
''' Initializes the TxOut instance.
Params:
- address (bytes): The address of the receiver.
- amount (Decimal): The amount to be transfered.
'''
self.address = address
self.amount = amount
def __eq__(self, other):
return (isinstance(other, self.__class__)
and self.address == other.address
and self.amount == other.amount)
def to_raw(self):
return {
'address': bytes_to_hex(self.address),
'amount': self.amount
}
@classmethod
def from_raw(cls, raw_obj):
address = hex_to_bytes(raw_obj['address'])
amount = raw_obj['amount'] if isinstance(raw_obj['amount'], Decimal) else Decimal(raw_obj['amount'])
return cls(address, amount)
@staticmethod
def is_valid_address(address):
if len(address) != 48:
print('invalid public key length')
return False
return True
def has_valid_structure(self):
return (isinstance(self.address, bytes)
and TxOut.is_valid_address(self.address)
and isinstance(self.amount, Decimal)
)
class TxIn(RawSerializable):
''' Transaction input. '''
def __init__(self, tx_out_id, tx_out_index, signature=None):
''' Initializes the TxIn instance.
Params:
- tx_out_id (bytes): The id of the output transaction providing the coins for this transaction.
- tx_out_index (int): The index of the block containing the output transaction
- signature (bytes): The signature of the TxIn, signed by the private key of the output transaction.
'''
self.tx_out_id = tx_out_id
self.tx_out_index = tx_out_index
self.signature = signature
def __eq__(self, other):
return (isinstance(self, other.__class__)
and self.tx_out_id == other.tx_out_id
and self.tx_out_index == other.tx_out_index)
def to_raw(self):
return {
'txOutId': bytes_to_hex(self.tx_out_id),
'txOutIndex': self.tx_out_index,
'signature': bytes_to_hex(self.signature) if self.signature is not None else None
}
@classmethod
def from_raw(cls, raw_obj):
tx_out_id = hex_to_bytes(raw_obj['txOutId'])
tx_out_index = raw_obj['txOutIndex']
signature = hex_to_bytes(raw_obj['signature']) if raw_obj['signature'] is not None else None
return cls(tx_out_id, tx_out_index, signature)
def has_valid_structure(self):
return (isinstance(self.signature, bytes)
and isinstance(self.tx_out_id, bytes)
and isinstance(self.tx_out_index, int)
)
def validate(self, transaction, unspent_tx_outs):
condition = lambda uTxO: uTxO.tx_out_id == self.tx_out_id and uTxO.tx_out_index == self.tx_out_index
referenced_uTxO = next((uTxO for uTxO in unspent_tx_outs if condition(uTxO)), None)
if not referenced_uTxO:
print('referenced tx_out not found: {}'.format(self.__dict__))
return False
address = referenced_uTxO.address
vk = ecdsa.VerifyingKey.from_string(address)
result = False
print('validating tx_in signature: {}\naddress: {}\ndata: {}'
.format(bytes_to_hex(self.signature), bytes_to_hex(address), bytes_to_hex(transaction.id)))
try:
if self.signature:
result = vk.verify(self.signature, transaction.id)
except ecdsa.BadSignatureError:
print('bad signature for tx_in: {}'.format(self))
pass
return result
def get_amount(self, unspent_tx_outs):
return UnspentTxOut.find(self.tx_out_id, self.tx_out_index, unspent_tx_outs).amount
@staticmethod
def has_duplicates(tx_ins):
key = lambda tx_in: tx_in.tx_out_id + int_to_bytes(tx_in.tx_out_index)
groups = set()
for tx_in in tx_ins:
tx_key = key(tx_in)
if tx_key in groups:
print('duplicate tx_in: {}'.format(key))
return True
else:
groups.add(tx_key)
return False
class UnspentTxOut(RawSerializable):
''' Unspent transaction outputs. '''
def __init__(self, tx_out_id, tx_out_index, address, amount):
self.tx_out_id = tx_out_id
self.tx_out_index = tx_out_index
self.address = address
self.amount = amount
def __eq__(self, other):
return (isinstance(other, self.__class__)
and self.tx_out_id == other.tx_out_id
and self.tx_out_index == other.tx_out_index
and self.address == other.address
and self.amount == other.amount)
def matches_tx_in(self, tx_in):
return self.tx_out_id == tx_in.tx_out_id and self.tx_out_index == tx_in.tx_out_index
@staticmethod
def find(transaction_id, index, unspent_tx_outs):
condition = lambda uTxO: uTxO.tx_out_id == transaction_id and uTxO.tx_out_index == index
return next((uTxO for uTxO in unspent_tx_outs if condition(uTxO)), None)
@staticmethod
def update_unspent_tx_outs(new_transactions, current_unspent_tx_outs):
new_unspent_tx_outs = [
UnspentTxOut(tx.id, index, tx_out.address, tx_out.amount)
for tx in new_transactions for index, tx_out in enumerate(tx.tx_outs)
]
consumed_tx_outs = [
UnspentTxOut(tx_in.tx_out_id, tx_in.tx_out_index, '', 0)
for tx in new_transactions for tx_in in tx.tx_ins
]
consumed = lambda uTxO: UnspentTxOut.find(uTxO.tx_out_id, uTxO.tx_out_index, consumed_tx_outs)
resulting_unspent_tx_outs = [uTxO for uTxO in current_unspent_tx_outs if not consumed(uTxO)]
resulting_unspent_tx_outs.extend(new_unspent_tx_outs)
return resulting_unspent_tx_outs
def to_raw(self):
return {
'txOutId': bytes_to_hex(self.tx_out_id),
'txOutIndex': self.tx_out_index,
'address': bytes_to_hex(self.address),
'amount': self.amount
}
@classmethod
def from_raw(cls, raw_obj):
tx_out_id = hex_to_bytes(raw_obj['txOutId'])
tx_out_index = raw_obj['txOutIndex']
address = hex_to_bytes(raw_obj['address'])
amount = raw_obj['amount'] if isinstance(raw_obj['amount'], Decimal) else Decimal(raw_obj['amount'])
return cls(tx_out_id, tx_out_index, address, amount)
class Transaction(RawSerializable):
''' A transaction.'''
COINBASE_AMOUNT = Decimal(50)
def __init__(self, tx_ins, tx_outs, identifier=None):
''' Initializes the Transaction instance.
Params:
- tx_ins (list<TxIn>): The list of transaction inputs.
- tx_outs (list<TxOut>): The list of transaction outputs.
'''
self.tx_ins = tx_ins
self.tx_outs = tx_outs
self.id = identifier if identifier is not None else self.get_id()
def __eq__(self, other):
return (isinstance(other, self.__class__)
and self.tx_ins == other.tx_ins
and self.tx_outs == other.tx_outs)
def to_raw(self):
return {
'txIns': TxIn.to_raw_list(self.tx_ins),
'txOuts': TxOut.to_raw_list(self.tx_outs),
'id': bytes_to_hex(self.id)
}
@classmethod
def from_raw(cls, raw_obj):
tx_ins = TxIn.from_raw_list(raw_obj['txIns'])
tx_outs = TxOut.from_raw_list(raw_obj['txOuts'])
identifier = hex_to_bytes(raw_obj['id'])
return cls(tx_ins, tx_outs, identifier)
def get_id(self):
hasher = hashlib.sha256()
for tx_in in self.tx_ins:
hasher.update(tx_in.tx_out_id)
hasher.update(int_to_bytes(tx_in.tx_out_index))
for tx_out in self.tx_outs:
hasher.update(tx_out.address)
(amount_num, amount_denom) = tx_out.amount.as_integer_ratio()
hasher.update(int_to_bytes(amount_num))
hasher.update(int_to_bytes(amount_denom))
return hasher.digest()
def sign_input(self, tx_in_index, private_key, unspent_tx_outs):
tx_in = self.tx_ins[tx_in_index]
data_to_sign = self.id
referenced_unspent_tx_out = UnspentTxOut.find(tx_in.tx_out_id, tx_in.tx_out_index, unspent_tx_outs)
if not referenced_unspent_tx_out:
print('could not find referenced txOut')
raise BadRequestError('could not find referenced txOut')
referenced_address = referenced_unspent_tx_out.address
if get_public_key(private_key) != referenced_address:
print('trying to sign an input with private ' +
' key that does not match the address that is referenced in txIn')
raise UnauthorizedError('invalid private key')
print('signing data: {}\nfor address: {}'
.format(bytes_to_hex(data_to_sign), bytes_to_hex(get_public_key(private_key))))
sk = ecdsa.SigningKey.from_string(private_key)
signature = sk.sign(data_to_sign)
print('signature: {}'.format(bytes_to_hex(signature)))
return signature
def has_valid_structure(self):
return (isinstance(self.id, bytes)
and isinstance(self.tx_outs, list)
and isinstance(self.tx_ins, list)
and all([tx_in.has_valid_structure() for tx_in in self.tx_ins])
and all([tx_out.has_valid_structure() for tx_out in self.tx_outs])
)
def validate(self, unspent_tx_outs):
if self.id != self.get_id():
print('invalid tx id: {}'.format(self))
return False
has_valid_tx_ins = all([tx_in.validate(self, unspent_tx_outs) for tx_in in self.tx_ins])
if not has_valid_tx_ins:
print('some of tx_ins are invalid in tx: {}'.format(self))
return False
total_tx_in_values = sum([tx_in.get_amount(unspent_tx_outs) for tx_in in self.tx_ins])
total_tx_out_values = sum([tx_out.amount for tx_out in self.tx_outs])
if total_tx_in_values != total_tx_out_values:
print('total_tx_in_values != total_tx_out_values in tx: {}'.format(self))
return False
return True
def validate_coinbase(self, block_index):
if self.id != self.get_id():
print('invalid tx id: {}'.format(self.id))
return False
if len(self.tx_ins) != 1:
print('one tx_in must be specified in the coinbase transaction')
return False
if self.tx_ins[0].tx_out_index != block_index:
print('the tx_in index in coinbase tx must be the block height')
return False
if len(self.tx_outs) != 1:
print('invalid number of tx_outs in coinbase transaction')
return False
if self.tx_outs[0].amount != Transaction.COINBASE_AMOUNT:
print('invalid coinbase amount in coinbase transaction')
return False
return True
@staticmethod
def validate_block_transactions(transactions, unspent_tx_outs, block_index):
if len(transactions) == 0:
return True
coinbase_tx = transactions[0]
if not coinbase_tx.validate_coinbase(block_index):
print('invalid coinbase tx: {}'.format(coinbase_tx.__dict__))
return False
tx_ins = [tx_in for tx in transactions for tx_in in tx.tx_ins]
if TxIn.has_duplicates(tx_ins):
return False
normal_transactions = transactions[1:]
return all([tx.validate(unspent_tx_outs) for tx in normal_transactions])
@staticmethod
def process_transactions(transactions, unspent_tx_outs, block_index):
if not all([tx.has_valid_structure() for tx in transactions]):
print('some of the transactions has invalid structure')
return None
if not Transaction.validate_block_transactions(transactions, unspent_tx_outs, block_index):
print('invalid block transactions')
return None
return UnspentTxOut.update_unspent_tx_outs(transactions, unspent_tx_outs)
@staticmethod
def coinbase(address, block_index):
tx_in = TxIn(bytes(), block_index, bytes())
tx_out = TxOut(address, Transaction.COINBASE_AMOUNT)
return Transaction([tx_in], [tx_out])