kryptika 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kryptika/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .core import Block, Blockchain, Transaction, Wallet
2
+ from .storage import SQLiteStorage
3
+ from .network import Node, run_server
4
+
5
+ __all__ = [
6
+ "Block", "Blockchain", "Transaction", "Wallet",
7
+ "SQLiteStorage",
8
+ "Node", "run_server",
9
+ ]
@@ -0,0 +1,5 @@
1
+ from .transaction import Transaction, Wallet
2
+ from .block import Block
3
+ from .blockchain import Blockchain
4
+
5
+ __all__ = ["Transaction", "Wallet", "Block", "Blockchain"]
kryptika/core/block.py ADDED
@@ -0,0 +1,92 @@
1
+ """
2
+ block.py — Block for Kryptika.
3
+
4
+ A block is a container that holds:
5
+ - A list of transactions
6
+ - A timestamp
7
+ - A nonce (the number found during Proof of Work)
8
+ - The hash of the previous block (the chain link)
9
+ - Its own SHA-256 hash
10
+
11
+ Changing any field changes the hash, which breaks the chain link to
12
+ the next block — this is how tampering is detected.
13
+ """
14
+
15
+ import hashlib
16
+ import json
17
+ import time
18
+ from typing import Optional
19
+ from .transaction import Transaction
20
+
21
+
22
+ class Block:
23
+ """A single block in the blockchain."""
24
+
25
+ def __init__(
26
+ self,
27
+ index: int,
28
+ transactions: list[Transaction],
29
+ prev_hash: str,
30
+ timestamp: Optional[float] = None,
31
+ nonce: int = 0,
32
+ ):
33
+ self.index = index
34
+ self.transactions = transactions
35
+ self.prev_hash = prev_hash
36
+ self.timestamp = timestamp if timestamp is not None else time.time()
37
+ self.nonce = nonce
38
+ self.hash = self.compute_hash()
39
+
40
+ def compute_hash(self) -> str:
41
+ """Return the SHA-256 hash of this block's contents.
42
+
43
+ sort_keys=True ensures the output is deterministic regardless
44
+ of dictionary insertion order across Python versions or platforms.
45
+ """
46
+ content = json.dumps(
47
+ {
48
+ "index": self.index,
49
+ "transactions": [tx.to_dict() for tx in self.transactions],
50
+ "prev_hash": self.prev_hash,
51
+ "timestamp": self.timestamp,
52
+ "nonce": self.nonce,
53
+ },
54
+ sort_keys=True,
55
+ )
56
+ return hashlib.sha256(content.encode()).hexdigest()
57
+
58
+ def to_dict(self) -> dict:
59
+ """Serialise this block to a plain dictionary."""
60
+ return {
61
+ "index": self.index,
62
+ "transactions": [tx.to_dict() for tx in self.transactions],
63
+ "prev_hash": self.prev_hash,
64
+ "timestamp": self.timestamp,
65
+ "nonce": self.nonce,
66
+ "hash": self.hash,
67
+ }
68
+
69
+ @classmethod
70
+ def from_dict(cls, data: dict) -> "Block":
71
+ """Reconstruct a Block from a dictionary.
72
+
73
+ The stored hash is restored as-is. Blockchain.is_valid() will
74
+ recompute and compare hashes to catch any tampering.
75
+ """
76
+ block = cls(
77
+ index = data["index"],
78
+ transactions = [Transaction.from_dict(tx) for tx in data["transactions"]],
79
+ prev_hash = data["prev_hash"],
80
+ timestamp = data["timestamp"],
81
+ nonce = data["nonce"],
82
+ )
83
+ block.hash = data["hash"]
84
+ return block
85
+
86
+ def __repr__(self) -> str:
87
+ return (
88
+ f"Block(index={self.index}, "
89
+ f"txs={len(self.transactions)}, "
90
+ f"nonce={self.nonce}, "
91
+ f"hash={self.hash[:12]}...)"
92
+ )
@@ -0,0 +1,300 @@
1
+ """
2
+ blockchain.py — Blockchain for Kryptika.
3
+
4
+ The Blockchain is an append-only list of Blocks secured by:
5
+ 1. SHA-256 hashing — every block hashes its own content.
6
+ 2. Chain linking — every block stores the previous block's hash.
7
+ 3. Proof of Work — every block's hash must start with N zeros.
8
+ 4. Transaction signing — every user transaction must be ECDSA-signed.
9
+
10
+ Tamper with any block and is_valid() immediately catches it.
11
+
12
+ Balance calculation
13
+ -------------------
14
+ Balances are computed by walking every block on the chain (UTXO-lite model).
15
+ credit: every tx where address == recipient
16
+ debit: every tx where address == sender (amount + fee)
17
+
18
+ Amounts are rounded to 8 decimal places to avoid floating-point drift.
19
+ """
20
+
21
+ from decimal import Decimal, ROUND_HALF_UP
22
+ from typing import Optional
23
+ from .block import Block
24
+ from .transaction import Transaction
25
+
26
+ GENESIS_PREV_HASH = "0" * 64
27
+
28
+
29
+ def _round(value: float) -> float:
30
+ """Round a coin value to 8 decimal places using banker's-grade rounding."""
31
+ return float(Decimal(str(value)).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP))
32
+
33
+
34
+ class Blockchain:
35
+ """An append-only chain of Blocks with Proof of Work.
36
+
37
+ Invariants enforced by is_valid():
38
+ - Genesis block must have prev_hash = 64 zeros.
39
+ - Every block's stored hash must match its recomputed hash.
40
+ - Every block's prev_hash must equal the previous block's hash.
41
+ - Every block's hash must start with `difficulty` leading zeros.
42
+ - Every non-coinbase transaction must carry a valid ECDSA signature.
43
+ """
44
+
45
+ def __init__(self, difficulty: int = 3):
46
+ self.difficulty = difficulty
47
+ self.chain: list[Block] = []
48
+ self._mine_and_add(Block(
49
+ index = 0,
50
+ transactions = [],
51
+ prev_hash = GENESIS_PREV_HASH,
52
+ ))
53
+
54
+ # ------------------------------------------------------------------
55
+ # Internal helpers
56
+ # ------------------------------------------------------------------
57
+
58
+ def _mine_and_add(self, block: Block) -> None:
59
+ """Find a nonce that satisfies Proof of Work, then append the block."""
60
+ target = "0" * self.difficulty
61
+ while not block.hash.startswith(target):
62
+ block.nonce += 1
63
+ block.hash = block.compute_hash()
64
+ self.chain.append(block)
65
+
66
+ # ------------------------------------------------------------------
67
+ # Public API
68
+ # ------------------------------------------------------------------
69
+
70
+ def mine_block(self, transactions: list[Transaction], miner_address: str) -> Block:
71
+ """Validate transactions, collect fees, prepend coinbase, mine, append.
72
+
73
+ Coinbase reward = MINING_REWARD + sum(all transaction fees).
74
+ Sender balance check accounts for all transactions in the same
75
+ batch — a sender cannot overspend across multiple txs in one block.
76
+
77
+ Raises ValueError if:
78
+ - Any transaction uses COINBASE_SENDER.
79
+ - Any transaction has an invalid signature.
80
+ - Any sender has insufficient funds (amount + fee) accounting for
81
+ all earlier transactions from the same sender in this batch.
82
+ """
83
+ fee_total = 0.0
84
+ batch_spend: dict[str, float] = {} # cumulative spend per sender this batch
85
+
86
+ for tx in transactions:
87
+ if tx.sender == Transaction.COINBASE_SENDER:
88
+ raise ValueError(
89
+ "Transactions with COINBASE sender are not permitted. "
90
+ "Coinbase rewards are added automatically by mine_block()."
91
+ )
92
+
93
+ valid, reason = tx.is_valid()
94
+ if not valid:
95
+ raise ValueError(f"Invalid transaction: {reason}")
96
+
97
+ total_cost = _round(tx.amount + tx.fee)
98
+ confirmed_bal = self.get_balance(tx.sender)
99
+ already_spent = batch_spend.get(tx.sender, 0.0)
100
+ effective_bal = _round(confirmed_bal - already_spent)
101
+
102
+ if effective_bal < total_cost:
103
+ raise ValueError(
104
+ f"Insufficient funds for {tx.sender[:16]}…: "
105
+ f"confirmed={confirmed_bal:.8f}, "
106
+ f"spent-in-batch={already_spent:.8f}, "
107
+ f"effective={effective_bal:.8f}, "
108
+ f"needed={total_cost:.8f}."
109
+ )
110
+
111
+ batch_spend[tx.sender] = _round(already_spent + total_cost)
112
+ fee_total = _round(fee_total + tx.fee)
113
+
114
+ block = Block(
115
+ index = self.height,
116
+ transactions = [Transaction.coinbase(miner_address, fee_total)] + transactions,
117
+ prev_hash = self.last_block.hash,
118
+ )
119
+ self._mine_and_add(block)
120
+ return block
121
+
122
+ @property
123
+ def last_block(self) -> Block:
124
+ return self.chain[-1]
125
+
126
+ @property
127
+ def height(self) -> int:
128
+ return len(self.chain)
129
+
130
+ def get_balance(self, address: str) -> float:
131
+ """Calculate the confirmed balance for an address.
132
+
133
+ Walks the entire chain once:
134
+ + amount for every tx where address is the recipient
135
+ - amount for every tx where address is the sender
136
+ - fee for every tx where address is the sender
137
+ """
138
+ balance = 0.0
139
+ for block in self.chain:
140
+ for tx in block.transactions:
141
+ if tx.recipient == address:
142
+ balance = _round(balance + tx.amount)
143
+ if tx.sender == address:
144
+ balance = _round(balance - tx.amount - tx.fee)
145
+ return balance
146
+
147
+ def get_history(self, address: str) -> list[dict]:
148
+ """Return all confirmed transactions involving *address*, oldest first.
149
+
150
+ Each entry contains:
151
+ block, timestamp, type, sender, recipient, amount, fee, note, tx_id
152
+ where type is one of: mining_reward | sent | received
153
+ """
154
+ history = []
155
+ for block in self.chain:
156
+ for tx in block.transactions:
157
+ if tx.sender != address and tx.recipient != address:
158
+ continue
159
+ if tx.sender == Transaction.COINBASE_SENDER:
160
+ tx_type = "mining_reward"
161
+ elif tx.sender == address:
162
+ tx_type = "sent"
163
+ else:
164
+ tx_type = "received"
165
+ history.append({
166
+ "block": block.index,
167
+ "timestamp": block.timestamp,
168
+ "type": tx_type,
169
+ "sender": tx.sender,
170
+ "recipient": tx.recipient,
171
+ "amount": tx.amount,
172
+ "fee": tx.fee,
173
+ "note": tx.note,
174
+ "tx_id": tx.tx_id,
175
+ })
176
+ return history
177
+
178
+ def is_valid(self) -> tuple[bool, Optional[str]]:
179
+ """Validate the entire chain. Returns (True, None) or (False, reason).
180
+
181
+ Checks performed on every block:
182
+ - Genesis block has the canonical prev_hash (64 zeros).
183
+ - Stored hash matches freshly recomputed hash (tamper detection).
184
+ - prev_hash matches the previous block's hash (link integrity).
185
+ - Hash satisfies Proof of Work (correct number of leading zeros).
186
+ - Every transaction has a valid signature.
187
+ """
188
+ if not self.chain:
189
+ return True, None
190
+
191
+ target = "0" * self.difficulty
192
+
193
+ # Genesis check
194
+ if self.chain[0].prev_hash != GENESIS_PREV_HASH:
195
+ return False, (
196
+ f"Block #0 (genesis) has wrong prev_hash "
197
+ f"'{self.chain[0].prev_hash[:16]}…' -- expected 64 zeros."
198
+ )
199
+
200
+ for i, curr in enumerate(self.chain):
201
+ if curr.hash != curr.compute_hash():
202
+ return False, (
203
+ f"Block #{i} has been tampered with — "
204
+ f"stored hash does not match computed hash."
205
+ )
206
+
207
+ if i > 0 and curr.prev_hash != self.chain[i - 1].hash:
208
+ return False, (
209
+ f"Block #{i} has a broken chain link — "
210
+ f"prev_hash does not match Block #{i - 1}'s hash."
211
+ )
212
+
213
+ if not curr.hash.startswith(target):
214
+ return False, (
215
+ f"Block #{i} fails Proof of Work — "
216
+ f"hash does not start with {self.difficulty} zero(s)."
217
+ )
218
+
219
+ for tx in curr.transactions:
220
+ valid, reason = tx.is_valid()
221
+ if not valid:
222
+ return False, f"Block #{i} contains an invalid transaction: {reason}"
223
+
224
+ return True, None
225
+
226
+ def get_block(self, index: int) -> Optional[Block]:
227
+ """Return the block at *index*, or None if out of range."""
228
+ if 0 <= index < self.height:
229
+ return self.chain[index]
230
+ return None
231
+
232
+ def to_list(self) -> list[dict]:
233
+ """Serialise the full chain to a list of dicts (for JSON transport)."""
234
+ return [block.to_dict() for block in self.chain]
235
+
236
+ @classmethod
237
+ def from_list(cls, data: list[dict], difficulty: int = 3) -> "Blockchain":
238
+ """Reconstruct a Blockchain from a list of block dicts.
239
+
240
+ Does NOT re-mine blocks — use is_valid() to verify the result.
241
+ """
242
+ bc = cls.__new__(cls)
243
+ bc.difficulty = difficulty
244
+ bc.chain = [Block.from_dict(item) for item in data]
245
+ return bc
246
+
247
+ def __repr__(self) -> str:
248
+ return f"Blockchain(height={self.height}, difficulty={self.difficulty})"
249
+
250
+ def _build_candidate_block(
251
+ self, transactions: list[Transaction], miner_address: str
252
+ ) -> Block:
253
+ """Validate transactions and mine a candidate block WITHOUT appending it.
254
+
255
+ Called by Node.mine() so PoW can run outside the node lock.
256
+ The caller is responsible for appending the returned block to self.chain.
257
+
258
+ Raises ValueError on invalid transactions or insufficient funds.
259
+ """
260
+ fee_total = 0.0
261
+ batch_spend: dict[str, float] = {}
262
+
263
+ for tx in transactions:
264
+ if tx.sender == Transaction.COINBASE_SENDER:
265
+ raise ValueError(
266
+ "Transactions with COINBASE sender are not permitted."
267
+ )
268
+ valid, reason = tx.is_valid()
269
+ if not valid:
270
+ raise ValueError(f"Invalid transaction: {reason}")
271
+
272
+ total_cost = _round(tx.amount + tx.fee)
273
+ confirmed_bal = self.get_balance(tx.sender)
274
+ already_spent = batch_spend.get(tx.sender, 0.0)
275
+ effective_bal = _round(confirmed_bal - already_spent)
276
+
277
+ if effective_bal < total_cost:
278
+ raise ValueError(
279
+ f"Insufficient funds for {tx.sender[:16]}…: "
280
+ f"confirmed={confirmed_bal:.8f}, "
281
+ f"spent-in-batch={already_spent:.8f}, "
282
+ f"effective={effective_bal:.8f}, "
283
+ f"needed={total_cost:.8f}."
284
+ )
285
+ batch_spend[tx.sender] = _round(already_spent + total_cost)
286
+ fee_total = _round(fee_total + tx.fee)
287
+
288
+ block = Block(
289
+ index = self.height,
290
+ transactions = [Transaction.coinbase(miner_address, fee_total)] + transactions,
291
+ prev_hash = self.last_block.hash,
292
+ )
293
+
294
+ # Run Proof of Work
295
+ target = "0" * self.difficulty
296
+ while not block.hash.startswith(target):
297
+ block.nonce += 1
298
+ block.hash = block.compute_hash()
299
+
300
+ return block