Skip to content

Commit f4fed6e

Browse files
author
Codebuff Contributor
committed
feat(blockchain): add proof-of-work blockchain implementation
1 parent 791deb4 commit f4fed6e

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

blockchain/proof_of_work.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
import json
5+
from time import time
6+
7+
8+
class Block:
9+
"""
10+
A single block in a proof-of-work blockchain.
11+
12+
Each block contains data along with a cryptographic hash linking it to the
13+
previous block. The block's hash must satisfy a difficulty target (start
14+
with a certain number of zero bits) which is found through mining.
15+
16+
Attributes:
17+
index: Position of the block in the chain
18+
timestamp: Unix time when the block was created
19+
data: Payload stored in the block
20+
previous_hash: Hash of the previous block (hex digest)
21+
nonce: Number used to satisfy the proof-of-work requirement
22+
hash: SHA-256 hash of this block (hex digest)
23+
24+
>>> Block(0, 1000.0, "genesis", "0", 0, "0000abc")
25+
Block(0, 1000.0, 'genesis', '0', 0, hash=0000abc)
26+
27+
Blocks with the same content are equal:
28+
>>> Block(0, 0.0, "a", "0", 1, "ab") == Block(0, 0.0, "a", "0", 1, "ab")
29+
True
30+
>>> Block(0, 0.0, "a", "0", 1, "ab") == Block(0, 0.0, "b", "0", 1, "ab")
31+
False
32+
"""
33+
34+
def __init__(
35+
self,
36+
index: int,
37+
timestamp: float,
38+
data: str,
39+
previous_hash: str,
40+
nonce: int,
41+
hash_value: str,
42+
) -> None:
43+
self.index = index
44+
self.timestamp = timestamp
45+
self.data = data
46+
self.previous_hash = previous_hash
47+
self.nonce = nonce
48+
self.hash = hash_value
49+
50+
def __repr__(self) -> str:
51+
return (
52+
f"Block({self.index}, {self.timestamp}, "
53+
f"{self.data!r}, {self.previous_hash!r}, {self.nonce}, "
54+
f"hash={self.hash})"
55+
)
56+
57+
def __eq__(self, other: object) -> bool:
58+
if not isinstance(other, Block):
59+
return NotImplemented
60+
return (
61+
self.index == other.index
62+
and self.timestamp == other.timestamp
63+
and self.data == other.data
64+
and self.previous_hash == other.previous_hash
65+
and self.nonce == other.nonce
66+
and self.hash == other.hash
67+
)
68+
69+
def to_dict(self) -> dict:
70+
"""Serialise the block to a JSON-compatible dictionary.
71+
72+
>>> Block(0, 0.0, "a", "0", 0, "ab").to_dict()["data"]
73+
'a'
74+
"""
75+
return {
76+
"index": self.index,
77+
"timestamp": self.timestamp,
78+
"data": self.data,
79+
"previous_hash": self.previous_hash,
80+
"nonce": self.nonce,
81+
"hash": self.hash,
82+
}
83+
84+
85+
def calculate_hash(
86+
index: int,
87+
timestamp: float,
88+
data: str,
89+
previous_hash: str,
90+
nonce: int,
91+
) -> str:
92+
"""Compute the SHA-256 hash of a block's contents.
93+
94+
The hash is taken over the concatenation of index, timestamp, data,
95+
previous_hash, and nonce, serialised as a JSON string.
96+
97+
>>> h = calculate_hash(1, 0.0, "tx", "0000", 42)
98+
>>> isinstance(h, str) and len(h) == 64
99+
True
100+
101+
Same inputs always produce the same hash:
102+
>>> a = calculate_hash(1, 0.0, "tx", "0000", 42)
103+
>>> a == calculate_hash(1, 0.0, "tx", "0000", 42)
104+
True
105+
"""
106+
payload = json.dumps(
107+
[index, timestamp, data, previous_hash, nonce],
108+
sort_keys=True,
109+
separators=(",", ":"),
110+
)
111+
return hashlib.sha256(payload.encode()).hexdigest()
112+
113+
114+
def mine_block(
115+
index: int,
116+
timestamp: float,
117+
data: str,
118+
previous_hash: str,
119+
difficulty: int,
120+
) -> Block:
121+
"""Mine a new block by finding a nonce that satisfies the difficulty target.
122+
123+
The block's hash must start with ``difficulty`` consecutive zero characters.
124+
This is a brute-force search that increments the nonce until a valid hash
125+
is found. Higher difficulty values require exponentially more work.
126+
127+
Difficulty must be a non-negative integer:
128+
Mined blocks always satisfy the proof-of-work requirement:
129+
>>> b = mine_block(1, 0.0, "hello", "abcd", 3)
130+
>>> b.hash[:3]
131+
'000'
132+
133+
>>> b = mine_block(2, 0.0, "world", b.hash, 4)
134+
>>> b.hash[:4]
135+
'0000'
136+
137+
Different nonces produce different hashes for the same block data:
138+
>>> a = calculate_hash(5, 0.0, "same", "prev", 0)
139+
>>> b = calculate_hash(5, 0.0, "same", "prev", 1)
140+
>>> a != b
141+
True
142+
"""
143+
prefix = "0" * difficulty
144+
nonce = 0
145+
hash_value = calculate_hash(index, timestamp, data, previous_hash, nonce)
146+
while not hash_value.startswith(prefix):
147+
nonce += 1
148+
hash_value = calculate_hash(
149+
index, timestamp, data, previous_hash, nonce
150+
)
151+
return Block(index, timestamp, data, previous_hash, nonce, hash_value)
152+
153+
154+
class Blockchain:
155+
"""
156+
A simple proof-of-work blockchain.
157+
158+
Maintains a chain of blocks where each block references the hash of the
159+
previous block. New blocks are mined with an adjustable difficulty level,
160+
and the integrity of the chain can be verified at any time.
161+
162+
>>> chain = Blockchain(difficulty=2)
163+
>>> _ = chain.add_block("Payment: Alice -> Bob: 10 BTC")
164+
>>> _ = chain.add_block("Payment: Bob -> Charlie: 5 BTC")
165+
>>> len(chain)
166+
3
167+
>>> chain.is_valid()
168+
True
169+
170+
Tampering with block data invalidates the chain:
171+
>>> chain.chain[1].data = "Tampered!"
172+
>>> chain.is_valid()
173+
False
174+
"""
175+
176+
def __init__(self, difficulty: int = 2) -> None:
177+
self.difficulty = difficulty
178+
self.chain: list[Block] = []
179+
self._create_genesis_block()
180+
181+
def _create_genesis_block(self) -> None:
182+
"""Create the first block of the chain (the genesis block).
183+
184+
The genesis block has index 0, arbitrary data, and previous_hash "0".
185+
"""
186+
genesis = mine_block(0, time(), "Genesis Block", "0", self.difficulty)
187+
self.chain.append(genesis)
188+
189+
def add_block(self, data: str) -> Block:
190+
"""Add a new block with the given data to the chain.
191+
192+
The block is mined with the chain's configured difficulty level.
193+
194+
>>> chain = Blockchain(difficulty=1)
195+
>>> block = chain.add_block("test")
196+
>>> block.index
197+
1
198+
>>> block.data
199+
'test'
200+
>>> chain.chain[-1].hash == block.hash
201+
True
202+
"""
203+
previous_block = self.chain[-1]
204+
new_block = mine_block(
205+
len(self.chain),
206+
time(),
207+
data,
208+
previous_block.hash,
209+
self.difficulty,
210+
)
211+
self.chain.append(new_block)
212+
return new_block
213+
214+
def is_valid(self) -> bool:
215+
"""Verify the integrity of the entire blockchain.
216+
217+
Checks that:
218+
1. Every block's hash matches its recomputed hash
219+
2. Every block's previous_hash matches the hash of the preceding block
220+
3. The genesis block has index 0 and previous_hash "0"
221+
222+
>>> chain = Blockchain(difficulty=1)
223+
>>> chain.is_valid()
224+
True
225+
"""
226+
for i in range(1, len(self.chain)):
227+
current = self.chain[i]
228+
previous = self.chain[i - 1]
229+
230+
if current.hash != calculate_hash(
231+
current.index,
232+
current.timestamp,
233+
current.data,
234+
current.previous_hash,
235+
current.nonce,
236+
):
237+
return False
238+
if current.previous_hash != previous.hash:
239+
return False
240+
return True
241+
242+
def __len__(self) -> int:
243+
return len(self.chain)
244+
245+
def to_json(self) -> str:
246+
"""Serialise the entire blockchain to a JSON string.
247+
248+
>>> chain = Blockchain(difficulty=1)
249+
>>> _ = chain.add_block("data")
250+
>>> isinstance(chain.to_json(), str)
251+
True
252+
"""
253+
return json.dumps(
254+
[block.to_dict() for block in self.chain], indent=2
255+
)
256+
257+
258+
if __name__ == "__main__":
259+
from doctest import testmod
260+
261+
testmod()
262+
263+
print("Mining a simple blockchain (difficulty=3)...")
264+
chain = Blockchain(difficulty=3)
265+
chain.add_block("Transaction 1: Alice sends 10 BTC to Bob")
266+
chain.add_block("Transaction 2: Bob sends 5 BTC to Charlie")
267+
chain.add_block("Transaction 3: Charlie sends 2 BTC to Dave")
268+
269+
print(f"\nBlockchain valid: {chain.is_valid()}")
270+
print(f"Total blocks: {len(chain)}")
271+
print(f"\nFull chain:\n{chain.to_json()}")

0 commit comments

Comments
 (0)