├── pythonchain ├── __init__.py ├── runtime.py ├── wallet.py ├── base.py └── block.py └── .gitignore /pythonchain/__init__.py: -------------------------------------------------------------------------------- 1 | from .runtime import registry 2 | from . import block 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *~ 3 | *pyc 4 | env 5 | env3 6 | dist 7 | *egg-info 8 | MANIFEST 9 | .cache 10 | -------------------------------------------------------------------------------- /pythonchain/runtime.py: -------------------------------------------------------------------------------- 1 | 2 | # global data set during operation 3 | 4 | registry = {} 5 | 6 | 7 | 8 | registry["blockchain_file"] = "../blockchain.dat" 9 | 10 | registry["coin_reward"] = 50 11 | registry["miner_public_key"] = '3059301306072a8648ce3d020106082a8648ce3d03010703420004e41f8a01a053469bf5736ab8cfae1c92af496a0a63a260c6ad96afe6ef6f278213518c8f86871f36f87421e721ff31e1c39afb9a4e06d1b5bc2688839fb13ab4' 12 | registry["network_difficulty"] = 10000 13 | 14 | registry["server_wallet"] = b'\xb6\x003059301306072a8648ce3d020106082a8648ce3d0301070342000447a57999918883c1ca1dc6561e3224b050615f1771da6eb42ab775879b9f5951adfd658ec1587ea5ba1b0fab748deebcc62651fa8e75122d1238a68d9ba5ba02\x14\x01308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420f16f5d3ef4c464d7b4c8b5ca99c6f6b4164b3df50b558c181fb748bdc7a63d5da1440342000447a57999918883c1ca1dc6561e3224b050615f1771da6eb42ab775879b9f5951adfd658ec1587ea5ba1b0fab748deebcc62651fa8e75122d1238a68d9ba5ba02' 15 | -------------------------------------------------------------------------------- /pythonchain/wallet.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence, Iterable 2 | 3 | import Crypto 4 | import Crypto.Random 5 | from Crypto.Hash import SHA256 6 | from Crypto.PublicKey import ECC 7 | from Crypto.Signature import DSS 8 | 9 | 10 | from pythonchain import base 11 | from pythonchain import block 12 | from pythonchain.runtime import registry 13 | 14 | 15 | 16 | 17 | class Wallet(base.Base): 18 | public_key = base.ShortString() 19 | private_key = base.ShortString() 20 | 21 | def __init__(self, **kwargs): 22 | if not kwargs: 23 | kwargs.update(self.new_keys()) 24 | super().__init__(**kwargs) 25 | 26 | def new_keys(self): 27 | private_key = ECC.generate(curve="P-256") 28 | public_key = private_key.public_key() 29 | 30 | response = { 31 | 'private_key': private_key.export_key(format='DER').hex(), 32 | 'public_key': public_key.export_key(format='DER').hex() 33 | } 34 | 35 | return response 36 | 37 | def balance(self, format=True): 38 | value = sum(output.amount for tr_id, index, output in block.BlockChain().unspent_outputs(filter=self.public_key)) 39 | if format: 40 | return f"{value/block.TOKENMULTIPLIER:.02f}" 41 | return value 42 | 43 | def simple_transaction(self, target, amount, fee=0): 44 | bl = block.BlockChain() 45 | outputs = bl.unspent_outputs(filter=self.public_key) 46 | partial_balance = 0 47 | to_use = [] 48 | for output in outputs: 49 | partial_balance += output[2].amount 50 | to_use.append(output) 51 | if partial_balance >= (amount + fee): 52 | break 53 | else: 54 | raise ValueError("No single unspent output have this much money") 55 | inputs=[] 56 | for output in to_use: 57 | ti = block.TransactionInput() 58 | ti.transaction = output[0] 59 | ti.index = output[1] 60 | inputs.append(ti) 61 | to1 = block.TransactionOutput() 62 | to1.wallet = target.public_key if isinstance(target, Wallet) else target 63 | to1.amount = amount 64 | 65 | change = partial_balance - amount - fee 66 | to2 = block.TransactionOutput() 67 | to2.wallet = self.public_key 68 | to2.amount = change 69 | 70 | tr = block.Transaction() 71 | tr.inputs.extend(inputs) 72 | tr.outputs.extend([to1, to2]) 73 | tr.sign_transaction(self) 74 | return tr 75 | 76 | -------------------------------------------------------------------------------- /pythonchain/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides a base class to serialize well behaved Python objects, 3 | with data fields, as well-defined byte-sequences. 4 | """ 5 | 6 | from collections import OrderedDict 7 | from collections.abc import MutableSequence, Sequence, Iterable 8 | 9 | from decimal import Decimal as _Decimal 10 | 11 | def _init_field_dictionary(cls): 12 | cls.fields = OrderedDict() 13 | # fetch all fields in superclasses, in reverse order 14 | # skipíng itself and object (the root): 15 | for supercls in cls.__mro__[-1:1:-1]: 16 | for field_name, value in cls.__dict__.get("fields", {}).items(): 17 | cls.fields[field_name] = value 18 | 19 | class Field: 20 | type = type(None) 21 | 22 | def __init__(self, default=None): 23 | self.default = default 24 | 25 | def __get__(self, instance, owner=None): 26 | if not instance: 27 | return self 28 | return instance.__dict__.get(self.name, self.default) 29 | 30 | def __set__(self, instance, value): 31 | if not isinstance(value, self.type): 32 | raise TypeError(f"Attribute '{self.name}' must be a '{self.type.__name__}' instance") 33 | instance.__dict__[self.name] = value 34 | 35 | def __set_name__(self, owner, name): 36 | self.name = name 37 | # Ensure each field-containing class has its own 'fields' 38 | # instance (regardless of superclasses) 39 | if not "fields" in owner.__dict__: 40 | _init_field_dictionary(owner) 41 | owner.fields[self.name] = self 42 | 43 | def serialize(self, instance): 44 | """returns a representation of self as bytes""" 45 | return b"" 46 | 47 | @classmethod 48 | def import_(cls, data, offset=0)->(object, int): 49 | """Return data that can be set as this field content 50 | 51 | given a bytes-like object, and an offset to the data 52 | inside it 53 | """ 54 | # Default "nonetype" fields take no space 55 | # on the serialized stream 56 | return None, offset 57 | 58 | 59 | NoneField = Field 60 | 61 | class Int(Field): 62 | type = int 63 | data_size = 8 64 | neg = False 65 | 66 | def __init__(self, default=0): 67 | super().__init__(default) 68 | 69 | def __set__(self, instance, value): 70 | if self.__class__.neg: 71 | value = ~value 72 | if value >= 2 ** (8 * self.__class__.data_size): 73 | raise ValueError("Value too big") 74 | super().__set__(instance, value) 75 | 76 | def serialize(self, instance): 77 | return self.__get__(instance).to_bytes(self.__class__.data_size, "little") 78 | 79 | @classmethod 80 | def import_(cls, data ,offset=0): 81 | op = (lambda i: ~i) if cls.neg else (lambda i: i) 82 | return op(int.from_bytes(data[offset: offset + cls.data_size], "little")), offset + cls.data_size 83 | 84 | UInt64 = Int 85 | 86 | class UInt512(Int): 87 | data_size = 64 88 | 89 | class UInt256(Int): 90 | data_size = 32 91 | 92 | class UInt128(Int): 93 | data_size = 16 94 | 95 | class Int64(Int): 96 | neg = True 97 | 98 | class Int32(Int): 99 | data_size = 4 100 | neg = True 101 | 102 | class UInt32(Int): 103 | data_size = 4 104 | 105 | class Int16(Int): 106 | data_size = 2 107 | neg = True 108 | 109 | class UInt16(Int): 110 | data_size = 2 111 | 112 | class UInt8(Int): 113 | data_size = 1 114 | 115 | Byte = UInt8 116 | 117 | 118 | class String(Field): 119 | type = str 120 | encoding = "utf-8" 121 | len_bytes = 2 122 | 123 | def __init__(self, default=""): 124 | super().__init__(default) 125 | 126 | def __set__(self, instance, value): 127 | cls = self.__class__ 128 | if len(value.encode(cls.encoding)) >= 2 ** (8 * cls.len_bytes): 129 | raise ValueError("Text too big") 130 | super().__set__(instance, value) 131 | 132 | def serialize(self, instance): 133 | cls = self.__class__ 134 | value = self.__get__(instance).encode(cls.encoding) 135 | return len(value).to_bytes(self.len_bytes, "little") + value 136 | 137 | @classmethod 138 | def import_(cls, data ,offset=0): 139 | start_pos = offset + cls.len_bytes 140 | length = int.from_bytes(data[offset: start_pos], "little") 141 | end_pos = start_pos + length 142 | raw = data[start_pos: end_pos] 143 | return raw.decode(cls.encoding), end_pos 144 | 145 | 146 | class _shortstr(str): 147 | def __repr__(self): 148 | v = super().__repr__() 149 | return v[:5] + "..." + v[-5:] 150 | 151 | 152 | class ShortString(String): 153 | def __get__(self, instance, owner=None): 154 | return _shortstr(super().__get__(instance, owner=None)) 155 | 156 | 157 | class Decimal(String): 158 | 159 | def __init__(self, default="0"): 160 | super().__init__(default) 161 | 162 | def __set__(self, instance, value): 163 | if isinstance(value, float): 164 | value = str(value) 165 | x = _Decimal(value) # noQA - Just for checking if it is a well formed decimal. 166 | super().__set__(instance, str(value)) 167 | 168 | def __get__(self, instance, owner=None): 169 | value = super().__get__(instance, owner) 170 | if owner: 171 | return _Decimal(super().__get__(instance, owner)) 172 | # else: owner == None implies we are called from the serializer 173 | return value 174 | 175 | 176 | class Base: 177 | 178 | def __init__(self, **kwargs): 179 | super().__init__() 180 | for name, value in kwargs.items(): 181 | if name not in self.fields: 182 | raise TypeError(f"Attribute '{name}' not reconized for this object") 183 | setattr(self, name, value) 184 | 185 | def serialize(self): 186 | cls = self.__class__ 187 | state = bytearray() 188 | for fieldname, field in cls.fields.items(): 189 | state += field.serialize(self) 190 | return bytes(state) 191 | 192 | @classmethod 193 | def from_data(cls, data, offset=0): 194 | from pathlib import Path 195 | if isinstance(data, (str, Path)): 196 | with open(data, "rb") as file: 197 | data = file.read() 198 | self = cls() 199 | for field_name, field in cls.fields.items(): 200 | field_data, offset = field.import_(data, offset) 201 | setattr(self, field_name, field_data) 202 | # optional state, used when objects are restored in sequence 203 | # from an outer stream 204 | self._offset = offset 205 | return self 206 | 207 | def __repr__(self): 208 | return "<{0}: {{{1}}}>".format( 209 | self.__class__.__name__, 210 | ",\n".join("{}: {}".format(field, repr(getattr(self, field))) for field in self.fields) 211 | ) 212 | 213 | 214 | class ModelField(Field): 215 | """Field able to hold a single copy of any model derived form Base""" 216 | 217 | type = Base 218 | 219 | def __init__(self, type): 220 | self.type = type 221 | super().__init__(default=type()) 222 | 223 | def serialize(self, instance): 224 | """returns a representation of self as bytes""" 225 | return self.__get__(instance).serialize() 226 | 227 | def import_(self, data, offset=0)->(object, int): 228 | cls = self.__class__ 229 | result = self.type.from_data(data, offset) 230 | 231 | return result, result._offset 232 | 233 | 234 | 235 | def sequence_factory(sequence_type): 236 | class InnerSequence(MutableSequence): 237 | maxlen = 2 ** 16 238 | 239 | def __init__(self): 240 | self.type = sequence_type 241 | self.data = [] 242 | 243 | def __getitem__(self, index): 244 | return self.data[index] 245 | 246 | def __setitem__(self, index, value): 247 | if not isinstance(value, self.type): 248 | raise TypeError(f"Items must be instances of '{self.type.__name__}'") 249 | self.data[index] = value 250 | 251 | def __delitem__(self, index): 252 | del self.data[index] 253 | 254 | def __len__(self): 255 | return len(self.data) 256 | 257 | def insert(self, index, value): 258 | if not isinstance(value, self.type): 259 | raise TypeError(f"Items must be instances of '{self.type.__name__}'") 260 | if index >= self.maxlen: 261 | raise IndexError(f"Maximum number of itens is {self.maxlen}") 262 | self.data.insert(index, value) 263 | 264 | def fill(self, sequence): 265 | self.data[:] = [] 266 | for value in sequence: 267 | self.append(value) 268 | 269 | def __repr__(self): 270 | return repr(self.data) 271 | 272 | return InnerSequence 273 | 274 | 275 | type_ = type 276 | 277 | class SequenceField(Field): 278 | """Allows a sequence of Base models to be used as a field""" 279 | type = None 280 | len_bytes = 2 281 | 282 | def __init__(self, type, default=None): 283 | if not issubclass(type, Base): 284 | raise TypeError("Types for sequence should be a Base type. Add fields to it") 285 | self.type = type 286 | super().__init__(default=sequence_factory(type)) 287 | self._initialized = True 288 | 289 | def __get__(self, instance, owner=None): 290 | if not instance: 291 | return self 292 | return instance.__dict__.setdefault(self.name, self.default()) 293 | 294 | def __set__(self, instance, value): 295 | if not isinstance(value, (Sequence, Iterable)): 296 | raise TypeError("This field can only be set to a sequence of '{self.type.__name__}' objects") 297 | self.__get__(instance).fill(value) 298 | 299 | def serialize(self, instance): 300 | """returns a representation of self as bytes""" 301 | if issubclass(self.type, Base): 302 | data = self.__get__(instance) 303 | return len(data).to_bytes(self.len_bytes, "little") + b"".join(obj.serialize() for obj in data) 304 | raise NotImplementedError # Todo: allow items to be Fields, instead of just Base objects 305 | 306 | 307 | def import_(self, data, offset=0)->(object, int): 308 | """Return data that can be set as this field content 309 | """ 310 | cls = self.__class__ 311 | start_pos = offset + cls.len_bytes 312 | length = int.from_bytes(data[offset: start_pos], "little") 313 | offset += cls.len_bytes 314 | result = [] 315 | for i in range(length): 316 | result.append(self.type.from_data(data, offset)) 317 | offset = result[-1]._offset 318 | 319 | return result, offset 320 | 321 | 322 | 323 | class Test0(Base): 324 | a = Field() 325 | b = Field() 326 | c = UInt8() 327 | d = UInt64() 328 | e = String() 329 | f = Decimal() 330 | 331 | 332 | class Test1(Base): 333 | g = SequenceField(Test0) 334 | 335 | 336 | class Test2(Base): 337 | h = ModelField(Test1) 338 | i = ModelField(Test0) 339 | 340 | 341 | 342 | 343 | -------------------------------------------------------------------------------- /pythonchain/block.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence, Iterable, MutableSequence 2 | from collections import OrderedDict 3 | import itertools 4 | import uuid 5 | 6 | import Crypto 7 | import Crypto.Random 8 | from Crypto.Hash import SHA256 9 | from Crypto.PublicKey import ECC 10 | from Crypto.Signature import DSS 11 | 12 | 13 | from pythonchain import base 14 | from pythonchain.runtime import registry 15 | 16 | TOKENMULTIPLIER = 100 # 100000000 # 100 million 17 | 18 | class TransactionError(Exception): 19 | """Base Blockchain Exception""" 20 | 21 | class BlockError(TransactionError): 22 | """Error on block verification""" 23 | 24 | 25 | class InvalidOutputReferenceError(TransactionError): 26 | """Tried to use invalid output in a transaction""" 27 | 28 | 29 | class SecretError(TransactionError): 30 | """Error in signature checking""" 31 | 32 | 33 | class WalletError(TransactionError): 34 | """Error in wallet used""" 35 | 36 | 37 | class AlreadySpentError(TransactionError): 38 | """Trying to use spent transaction in new transaction""" 39 | 40 | 41 | class AmountError(TransactionError, ValueError): 42 | """Used when there is an error in the values""" 43 | 44 | 45 | class TransactionOutput(base.Base): 46 | ID = base.UInt128() 47 | wallet = base.String() 48 | amount = base.UInt64() 49 | extra_data = base.String() 50 | 51 | def __init__(self, **kwargs): 52 | kwargs.setdefault("ID", int(uuid.uuid4())) 53 | super().__init__(**kwargs) 54 | 55 | 56 | class TransactionInput(base.Base): 57 | transaction = base.UInt128() 58 | index = base.UInt16() 59 | signature = base.UInt512() 60 | 61 | def verify(self): 62 | 63 | try: 64 | output = BlockChain().get_output_from_input(self) 65 | pubkey = ECC.import_key(bytes.fromhex(output.wallet)) 66 | except Exception as error: 67 | raise InvalidOutputReferenceError from error 68 | 69 | verifier = DSS.new(pubkey, "fips-186-3") 70 | # privkey = ECC.import_key(key) 71 | # signer = DSS.new(privkey, "fips-186-3") 72 | 73 | hash_ = SHA256.new(output.serialize()) 74 | try: 75 | verifier.verify(hash_, self.signature.to_bytes(64, "little")) 76 | 77 | except ValueError as check_fail: 78 | raise SecretError from check_fail 79 | 80 | def sign(self, wallet): 81 | private_key = ECC.import_key(bytes.fromhex(wallet.private_key)) 82 | signer = DSS.new(private_key, 'fips-186-3') 83 | output = BlockChain().get_output_from_input(self) 84 | hash_ = SHA256.new(output.serialize()) 85 | self.signature = int.from_bytes(signer.sign(hash_), "little") 86 | 87 | 88 | class Transaction(base.Base): 89 | ID = base.UInt128() 90 | inputs = base.SequenceField(TransactionInput) 91 | outputs = base.SequenceField(TransactionOutput) 92 | signature = base.UInt512() 93 | transaction_type = base.UInt8() 94 | 95 | def __init__(self, **kwargs): 96 | kwargs.setdefault("ID", int(uuid.uuid4())) 97 | kwargs.setdefault("transaction_type", 0) # 0 indicates normal transaction. 1 is for reward transaction 98 | self.blockchain = BlockChain() 99 | super().__init__(**kwargs) 100 | 101 | 102 | def get_fee(self): 103 | self.verify() 104 | if self.transaction_type == 1: 105 | # Reward transaction - balance comes out of thin air 106 | return 0 107 | bl = BlockChain() 108 | input_amount = sum(bl.get_output_from_input(inp).amount for inp in self.inputs) 109 | output_amount = sum(out.amount for out in self.outputs) 110 | 111 | if output_amount > input_amount: 112 | raise AmountError(f"Total fee is negative '{output_amount - input_amount}'") 113 | return input_amount - output_amount 114 | 115 | def verify(self): 116 | for input in self.inputs: 117 | input.verify() 118 | self.verify_transaction_signature() 119 | 120 | def verify_transaction_signature(self): 121 | if self.transaction_type == 1: 122 | return 123 | wallet = BlockChain().get_output_from_input(self.inputs[0]).wallet 124 | pubkey = ECC.import_key(bytes.fromhex(wallet)) 125 | 126 | verifier = DSS.new(pubkey, "fips-186-3") 127 | 128 | signature = self.signature 129 | self.signature = 0 130 | hash_ = SHA256.new(self.serialize()) 131 | self.signature = signature 132 | try: 133 | verifier.verify(hash_, signature.to_bytes(64, "little")) 134 | 135 | except ValueError as check_fail: 136 | raise SecretError("Invalid transaction signature") 137 | 138 | 139 | 140 | def sign_transaction(self, wallets): 141 | """ 142 | Sign transaction with private key 143 | # based on https://github.com/adilmoujahid/blockchain-python-tutorial/blob/master/blockchain_client/blockchain_client.py 144 | """ 145 | from .wallet import Wallet 146 | 147 | if isinstance(wallets, (Sequence, Iterable)): 148 | wallets = {wallet.public_key: wallet for wallet in wallets} 149 | elif isinstance(wallets, Wallet): 150 | wallets = {wallets.public_key: wallets} 151 | 152 | for input in self.inputs: 153 | output = BlockChain().get_output_from_input(input) 154 | try: 155 | input.sign(wallets[output.wallet]) 156 | except KeyError as error: 157 | raise WalletError from error 158 | 159 | # Use the wallet used for the first input to sign the whole transaction 160 | if self.inputs: 161 | signer_wallet = BlockChain().get_output_from_input(self.inputs[0]).wallet 162 | wallet = wallets[signer_wallet] 163 | elif self.transaction_type == 1: # we have input if we are a reward transaction 164 | wallet = next(iter(wallets.values())) 165 | else: 166 | raise TransactionError("Trying to sign transaction with no input") 167 | 168 | private_key = ECC.import_key(bytes.fromhex(wallet.private_key)) 169 | signer = DSS.new(private_key, 'fips-186-3') 170 | self.signature = 0 171 | hash_ = SHA256.new(self.serialize()) 172 | self.signature = int.from_bytes(signer.sign(hash_), "little") 173 | 174 | 175 | 176 | class Block(base.Base): 177 | number = base.UInt32() 178 | previous_block = base.UInt256() 179 | transactions = base.SequenceField(Transaction) 180 | nonce = base.UInt128() 181 | difficulty = base.UInt256() 182 | 183 | def __init__(self, **kwargs): 184 | kwargs.setdefault("difficulty", registry["network_difficulty"]) 185 | super().__init__(**kwargs) 186 | self.state = "new" 187 | 188 | def prepare(self): 189 | """Prepares block acording to current chain so 190 | that it gets ready to be mined. 191 | 192 | If no transactions have been manually added prior to calling 193 | this, will pick all transactions in the blockchain pool. 194 | """ 195 | if not self.transactions: 196 | self.fetch_transactions() 197 | previous_block = BlockChain()[-1] 198 | self.previous_block = int.from_bytes(previous_block.hash(), "big") 199 | self.number = previous_block.number + 1 200 | self.make_reward() 201 | 202 | def fetch_transactions(self): 203 | bl = BlockChain() 204 | self.transactions.fill(bl.transaction_pool.values()) 205 | 206 | def set_previous(self, block): 207 | self.previous_block = sha256(block.serialize()).digest() 208 | 209 | def make_reward(self): 210 | from .wallet import Wallet 211 | reward = registry["coin_reward"] 212 | target_wallet = registry["miner_public_key"] 213 | server_wallet = Wallet.from_data(registry["server_wallet"]) 214 | 215 | fees = sum(transaction.get_fee() for transaction in self.transactions) 216 | out1 = TransactionOutput(wallet=target_wallet, amount=reward * TOKENMULTIPLIER, extra_data="Mining Reward") 217 | out2 = TransactionOutput(wallet=target_wallet, amount=fees, extra_data="Mining Fees") 218 | tr = Transaction() 219 | tr.transaction_type = 1 # Reward transaction 220 | tr.outputs.append(out1) 221 | tr.outputs.append(out2) 222 | tr.sign_transaction(server_wallet) 223 | self.transactions.append(tr) 224 | 225 | def check_difficulty(self): 226 | difficulty = (2 ** 256 - 1) // self.difficulty 227 | return int.from_bytes(self.hash(), "big") <= difficulty 228 | 229 | def hash(self): 230 | # This is what makes one "mining attempt" 231 | hash_ = SHA256.new(self.serialize()) 232 | return hash_.digest() 233 | 234 | def __hash__(self): 235 | # Python coerces the return value of this to Int64 236 | return self.hash() 237 | 238 | def verify(self): 239 | first = True 240 | for transaction in reversed(self.transactions): 241 | if transaction.transaction_type == 1 and not first: 242 | raise BlockError("Reward transaction not as last of transactions list") 243 | transaction.verify() 244 | first = False 245 | if not self.check_difficulty(): 246 | raise BlockError("Block hash do not match its set difficulty") 247 | 248 | 249 | def mine_block(block): 250 | counter = 0 251 | while not block.check_difficulty(): 252 | block.nonce = int(uuid.uuid4()) 253 | 254 | def create_block_zero(): 255 | blockchain = BlockChain.__new__(BlockChain) 256 | blockchain.file = registry["blockchain_file"] 257 | blockchain.blocks = [] 258 | 259 | # registry["blockchain"] = blockchain 260 | 261 | bl = Block() 262 | bl.difficulty = 1 263 | bl.number = 0 264 | tr = Transaction() 265 | tr.transaction_type = 1 266 | bl.previous_block = 0 267 | bl.make_reward() 268 | mine_block(bl) 269 | # Add block directly to internal data structure: skip verification 270 | # on blockchain.append code 271 | blockchain.blocks.append(bl) 272 | blockchain.last_read_block = -1 273 | blockchain.commit_unwriten_blocks() 274 | 275 | 276 | class BlockChain(MutableSequence): 277 | """the blockchain 278 | """ 279 | 280 | def __new__(cls, *args, **kwargs): 281 | # Singleton pattern - one blockchain per proccess. 282 | if "blockchain" in registry: 283 | return registry["blockchain"] 284 | self = super().__new__(cls, *args, **kwargs) 285 | registry["blockchain"] = self 286 | return self 287 | 288 | def __init__(self): 289 | # Avoid recursion on trying to get same instance. 290 | if hasattr(self, "bootstrapped"): 291 | return 292 | 293 | self.bootstrapped = True 294 | self.file = registry["blockchain_file"] 295 | self.blocks = [] 296 | 297 | self.transaction_pool = {} 298 | 299 | # TODO: do not load all blockchain into memory 300 | # just scan to get the unspent transaction outputs and metadata 301 | self.load_blocks() 302 | self.verify() 303 | 304 | 305 | def add_transaction(self, transaction): 306 | """Adds a transaction to the transaction pool.""" 307 | 308 | self.validate_inputs(transaction) 309 | transaction.verify() 310 | self.transaction_pool[transaction.ID] = transaction 311 | 312 | def validate_inputs(self, transaction): 313 | # check that all inputs are unsent in current blockchain state. 314 | inputs = {tr for tr in transaction.inputs} 315 | for past_transaction in self.all_transactions(): 316 | for inp in past_transaction.inputs: 317 | if inp.transaction in inputs: 318 | raise AlreadySpentError 319 | # TODO: check inputs against inputs in transactions in self.transaction_pool 320 | 321 | def all_transactions(self): 322 | # TODO: return transactions from all blocks in the chain 323 | return self.transactions.values() 324 | 325 | def get_output_from_input(self, input): 326 | try: 327 | inp_transaction = self.transactions[input.transaction] 328 | output = inp_transaction.outputs[input.index] 329 | except Exception as error: 330 | raise WalletError from error 331 | return output 332 | 333 | def load_blocks(self): 334 | with open(self.file, "rb") as file_: 335 | data = file_.read() 336 | self.blocks[:] = [] 337 | offset = 0 338 | while offset < len(data): 339 | block = Block.from_data(data, offset) 340 | offset = block._offset 341 | self.blocks.append(block) 342 | self.last_read_block = self.blocks[-1].number 343 | self.load_transactions() 344 | 345 | def load_transactions(self): 346 | self.transactions = {} 347 | for block in self: 348 | for transaction in block.transactions: 349 | self.transactions[transaction.ID] = transaction 350 | 351 | def commit_unwriten_blocks(self): 352 | with open(self.file, "ab") as file_: 353 | for i,block in enumerate(self.blocks): 354 | block.verify() 355 | if block.number <= self.last_read_block: 356 | continue 357 | if i: # skip when writing block zero. 358 | self.verify_block_in_chain(block, i - 1) 359 | file_.write(block.serialize()) 360 | self.last_read_block = self.blocks[-1].number 361 | self.load_transactions() 362 | 363 | def verify_block_in_chain(self, block, previous_block_index=-1): 364 | previous_hash = int.from_bytes(self.blocks[previous_block_index].hash(), "big") 365 | if previous_hash != block.previous_block: 366 | raise BlockError("Previous block address don't match the one in chain") 367 | 368 | def verify(self): 369 | for i, block in enumerate(self.blocks): 370 | block.verify() 371 | if i: 372 | self.verify_block_in_chain(block, i - 1) 373 | 374 | def unspent_outputs(self, filter=None): 375 | outputs = {} 376 | for block in self: 377 | for transaction in block.transactions: 378 | outputs[transaction.ID] = local = OrderedDict() 379 | for i, output in enumerate(transaction.outputs): 380 | local[i] = output 381 | for input in transaction.inputs: 382 | try: 383 | del outputs[input.transaction][input.index] 384 | except (KeyError, IndexError) as error: 385 | raise TransactionError(f"Invalid input for transaction {transaction.ID} found in chain") 386 | result = [] 387 | for tr_id, transactions_local in outputs.items(): 388 | for index, output in transactions_local.items(): 389 | if not filter or output.wallet in filter: 390 | result.append((tr_id, index, output)) 391 | return result 392 | 393 | def __getitem__(self, index): 394 | return self.blocks[index] 395 | 396 | def __setitem__(self, index, value): 397 | raise RuntimeError("Can't change blocks on the chain") 398 | 399 | def __delitem__(self, index): 400 | raise RuntimeError("Can't change blocks on the chain") 401 | 402 | def __len__(self): 403 | return len(self.blocks) 404 | 405 | def insert(self, index, value): 406 | raise RuntimeError("Can't change blocks on the chain") 407 | 408 | def append(self, block): 409 | # TODO: verify 410 | self.verify_block_in_chain(block) 411 | self.blocks.append(block) 412 | for transaction in block.transactions: 413 | self.transaction_pool.pop(transaction.ID, None) 414 | 415 | def __repr__(self): 416 | return f"Blockchain with {len(self.blocks)} valid blocks" 417 | --------------------------------------------------------------------------------