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 +9 -0
- kryptika/core/__init__.py +5 -0
- kryptika/core/block.py +92 -0
- kryptika/core/blockchain.py +300 -0
- kryptika/core/transaction.py +306 -0
- kryptika/main.py +86 -0
- kryptika/network/__init__.py +4 -0
- kryptika/network/node.py +348 -0
- kryptika/network/server.py +321 -0
- kryptika/storage/__init__.py +3 -0
- kryptika/storage/storage.py +231 -0
- kryptika-1.1.0.dist-info/METADATA +643 -0
- kryptika-1.1.0.dist-info/RECORD +17 -0
- kryptika-1.1.0.dist-info/WHEEL +5 -0
- kryptika-1.1.0.dist-info/entry_points.txt +2 -0
- kryptika-1.1.0.dist-info/licenses/LICENSE +21 -0
- kryptika-1.1.0.dist-info/top_level.txt +1 -0
kryptika/__init__.py
ADDED
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
|