Skip to content

Commit 91112d5

Browse files
Merge pull request #89 from SIDDHANTCOOKIE/feature/genesis-config
Feat/ transaction receipts
2 parents 329e7cb + 0812e46 commit 91112d5

17 files changed

Lines changed: 248 additions & 262 deletions

main.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727

2828
from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block
2929
from minichain.validators import is_valid_receiver
30+
from minichain.block import calculate_receipt_root
3031

3132

3233
logger = logging.getLogger(__name__)
3334

34-
BURN_ADDRESS = "0" * 40
3535
TRUSTED_PEERS = set()
3636
LOCALHOST_PEERS = {"127.0.0.1", "::1", "localhost", "0:0:0:0:0:0:0:1"}
3737

@@ -61,13 +61,17 @@ def mine_and_process_block(chain, mempool, miner_pk):
6161
temp_state = chain.state.copy()
6262
mineable_txs = []
6363
stale_txs = []
64+
receipts = []
6465
for tx in pending_txs:
6566
expected_nonce = temp_state.get_account(tx.sender).get("nonce", 0)
6667
if tx.nonce < expected_nonce:
6768
stale_txs.append(tx)
6869
continue
69-
if temp_state.validate_and_apply(tx):
70+
71+
receipt = temp_state.validate_and_apply(tx)
72+
if receipt is not None:
7073
mineable_txs.append(tx)
74+
receipts.append(receipt)
7175

7276
if stale_txs:
7377
mempool.remove_transactions(stale_txs)
@@ -82,6 +86,9 @@ def mine_and_process_block(chain, mempool, miner_pk):
8286
index=chain.last_block.index + 1,
8387
previous_hash=chain.last_block.hash,
8488
transactions=mineable_txs,
89+
state_root=temp_state.state_root(),
90+
receipt_root=calculate_receipt_root(receipts),
91+
receipts=receipts,
8592
miner=miner_pk,
8693
)
8794

minichain/block.py

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,36 @@
44
from collections.abc import Sequence
55

66
from .transaction import Transaction
7+
from .receipt import Receipt
78
from .serialization import canonical_json_hash, canonical_json_bytes
89

9-
1010
def _sha256(data: str) -> str:
1111
return hashlib.sha256(data.encode()).hexdigest()
1212

13-
# <-- Updated to Sequence to accept the frozen tuple
14-
def _calculate_merkle_root(transactions: Sequence[Transaction]) -> Optional[str]:
15-
if not transactions:
13+
def _calculate_merkle_tree(hashes: Sequence[str]) -> Optional[str]:
14+
if not hashes:
1615
return None
17-
18-
# Hash each transaction deterministically
19-
tx_hashes = [
20-
tx.tx_id
21-
for tx in transactions
22-
]
23-
24-
# Build Merkle tree
25-
while len(tx_hashes) > 1:
26-
if len(tx_hashes) % 2 != 0:
27-
tx_hashes.append(tx_hashes[-1]) # duplicate last if odd
28-
16+
hashes_list = list(hashes)
17+
while len(hashes_list) > 1:
18+
if len(hashes_list) % 2 != 0:
19+
hashes_list.append(hashes_list[-1])
2920
new_level = []
30-
for i in range(0, len(tx_hashes), 2):
31-
combined = tx_hashes[i] + tx_hashes[i + 1]
21+
for i in range(0, len(hashes_list), 2):
22+
combined = hashes_list[i] + hashes_list[i + 1]
3223
new_level.append(_sha256(combined))
24+
hashes_list = new_level
25+
return hashes_list[0]
3326

34-
tx_hashes = new_level
27+
# <-- Updated to Sequence to accept the frozen tuple
28+
def _calculate_merkle_root(transactions: Sequence[Transaction]) -> Optional[str]:
29+
if not transactions:
30+
return None
31+
return _calculate_merkle_tree([tx.tx_id for tx in transactions])
3532

36-
return tx_hashes[0]
33+
def calculate_receipt_root(receipts: Sequence[Receipt]) -> Optional[str]:
34+
if not receipts:
35+
return None
36+
return _calculate_merkle_tree([canonical_json_hash(r.to_dict()) for r in receipts])
3737

3838
class Block:
3939
def __init__(
@@ -43,12 +43,16 @@ def __init__(
4343
transactions: Optional[Sequence[Transaction]] = None,
4444
timestamp: Optional[float] = None,
4545
difficulty: Optional[int] = None,
46-
miner: Optional[str] = None
46+
state_root: Optional[str] = None,
47+
receipt_root: Optional[str] = None,
48+
receipts: Optional[Sequence[Receipt]] = None,
49+
miner: Optional[str] = None,
4750
):
4851
self.index = index
4952
self.previous_hash = previous_hash
5053
# Freeze transactions into an immutable tuple to prevent header/body mismatch
5154
self.transactions = tuple(transactions) if transactions else ()
55+
self.receipts = tuple(receipts) if receipts else ()
5256
self.miner = miner
5357
# Deterministic timestamp (ms)
5458
self.timestamp: int = (
@@ -60,10 +64,15 @@ def __init__(
6064
self.nonce: int = 0
6165
self.hash: Optional[str] = None
6266
self.state_root: Optional[str] = state_root
67+
self.receipt_root: Optional[str] = receipt_root
6368
self.miner: Optional[str] = miner
6469

65-
# NEW: compute merkle root once
70+
# NEW: compute merkle roots once
6671
self.merkle_root: Optional[str] = _calculate_merkle_root(self.transactions)
72+
73+
# If receipt_root is missing but we have receipts, calculate it.
74+
if self.receipt_root is None and self.receipts:
75+
self.receipt_root = calculate_receipt_root(self.receipts)
6776

6877
# -------------------------
6978
# HEADER (used for mining)
@@ -74,10 +83,10 @@ def to_header_dict(self):
7483
"previous_hash": self.previous_hash,
7584
"merkle_root": self.merkle_root,
7685
"state_root": self.state_root,
86+
"receipt_root": self.receipt_root,
7787
"timestamp": self.timestamp,
7888
"difficulty": self.difficulty,
7989
"nonce": self.nonce,
80-
"miner": self.miner,
8190
}
8291
# Include miner in header only when present (optional field) <-- Reworded comment
8392
if self.miner is not None:
@@ -91,6 +100,9 @@ def to_body_dict(self):
91100
return {
92101
"transactions": [
93102
tx.to_dict() for tx in self.transactions
103+
],
104+
"receipts": [
105+
r.to_dict() for r in self.receipts
94106
]
95107
}
96108

@@ -115,6 +127,10 @@ def from_dict(cls, payload: dict):
115127
Transaction.from_dict(tx_payload)
116128
for tx_payload in payload.get("transactions", [])
117129
]
130+
receipts = [
131+
Receipt.from_dict(r_payload)
132+
for r_payload in payload.get("receipts", [])
133+
]
118134

119135
# Safely extract and cast difficulty if it exists
120136
raw_diff = payload.get("difficulty")
@@ -123,13 +139,15 @@ def from_dict(cls, payload: dict):
123139
# Safely extract and cast timestamp if it exists <-- Added explicit timestamp casting
124140
raw_ts = payload.get("timestamp")
125141
parsed_ts = int(raw_ts) if raw_ts is not None else None
126-
127142
block = cls(
128143
index=int(payload["index"]),
129144
previous_hash=payload["previous_hash"],
130145
transactions=transactions,
131146
timestamp=parsed_ts, # <-- Passed the casted timestamp
132147
difficulty=parsed_diff,
148+
state_root=payload.get("state_root"),
149+
receipt_root=payload.get("receipt_root"),
150+
receipts=receipts,
133151
miner=payload.get("miner"),
134152
)
135153
block.nonce = int(payload.get("nonce", 0))
@@ -143,6 +161,12 @@ def from_dict(cls, payload: dict):
143161
# Recalculate and verify the Merkle root!
144162
if "merkle_root" in payload and payload["merkle_root"] != block.merkle_root:
145163
raise ValueError("merkle_root does not match transactions")
164+
165+
if "receipt_root" in payload:
166+
expected_receipt_root = calculate_receipt_root(block.receipts)
167+
if payload["receipt_root"] != expected_receipt_root:
168+
raise ValueError("receipt_root does not match receipts")
169+
146170
return block
147171

148172
@property

minichain/chain.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .block import Block
1+
from .block import Block, calculate_receipt_root
22
from .state import State
33
from .pow import calculate_hash
44
import logging
@@ -47,18 +47,18 @@ def _create_genesis_block(self, genesis_path):
4747
with open(genesis_path, "r") as f:
4848
config = json.load(f)
4949
except Exception as e:
50-
logger.error(f"Failed to load genesis config: {e}")
50+
logger.error("Failed to load genesis config: %s", e)
5151
sys.exit(1)
5252
else:
53-
logger.error(f"Failed to load genesis config: file {genesis_path} does not exist.")
53+
logger.error("Failed to load genesis config: file %s does not exist.", genesis_path)
5454
sys.exit(1)
5555

5656
# Apply genesis allocations
5757
alloc = config.get("alloc", {})
5858
for address, data in alloc.items():
5959
balance = data.get("balance", 0)
6060
if not isinstance(balance, int) or balance < 0:
61-
logger.error(f"Invalid genesis balance for {address}: {balance}. Must be a non-negative integer.")
61+
logger.error("Invalid genesis balance for %s: %s. Must be a non-negative integer.", address, balance)
6262
sys.exit(1)
6363
account = self.state.get_account(address)
6464
account['balance'] = balance
@@ -72,15 +72,17 @@ def _create_genesis_block(self, genesis_path):
7272
transactions=[],
7373
timestamp=timestamp,
7474
difficulty=difficulty,
75-
state_root=self.state.state_root()
75+
state_root=self.state.state_root(),
76+
receipt_root=None,
77+
receipts=[]
7678
)
7779

7880
computed_hash = calculate_hash(genesis_block.to_header_dict())
7981
config_hash = config.get("hash")
8082

8183
if config_hash:
8284
if config_hash != computed_hash:
83-
logger.error(f"Genesis hash mismatch. Config hash: {config_hash}, Computed hash: {computed_hash}")
85+
logger.error("Genesis hash mismatch. Config hash: %s, Computed hash: %s", config_hash, computed_hash)
8486
sys.exit(1)
8587
genesis_block.hash = config_hash
8688
else:
@@ -111,17 +113,29 @@ def add_block(self, block):
111113

112114
# Validate transactions on a temporary state copy
113115
temp_state = self.state.copy()
116+
receipts = []
114117

115118
for tx in block.transactions:
116-
result = temp_state.validate_and_apply(tx)
119+
receipt = temp_state.validate_and_apply(tx)
117120

118-
# Reject block if any transaction fails
119-
if not result:
121+
# Reject block if any transaction fails mathematical validation (None)
122+
if receipt is None:
120123
logger.warning("Block %s rejected: Transaction failed validation", block.index)
121124
return False
125+
126+
receipts.append(receipt)
122127

123128
if block.miner:
124129
temp_state.credit_mining_reward(block.miner)
130+
131+
computed_receipt_root = calculate_receipt_root(receipts)
132+
if block.receipt_root != computed_receipt_root:
133+
logger.warning("Block %s rejected: Invalid receipt root. Expected %s, got %s", block.index, computed_receipt_root, block.receipt_root)
134+
return False
135+
136+
if [r.to_dict() for r in block.receipts] != [r.to_dict() for r in receipts]:
137+
logger.warning("Block %s rejected: Receipts payload mismatch", block.index)
138+
return False
125139

126140
# Verify state root
127141
if block.state_root != temp_state.state_root():

minichain/contract.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def execute(self, contract_address, sender_address, payload, amount):
114114
logger.error("Contract execution crashed without result")
115115
return False
116116
if result["status"] != "success":
117-
logger.error(f"Contract Execution Failed: {result.get('error')}")
117+
logger.error("Contract Execution Failed: %s", result.get('error'))
118118
return False
119119

120120
# Validate storage is JSON serializable
@@ -155,7 +155,7 @@ def _validate_code_ast(self, code):
155155
logger.warning("Rejected type() call.")
156156
return False
157157
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in {"getattr", "setattr", "delattr"}:
158-
logger.warning(f"Rejected direct call to {node.func.id}.")
158+
logger.warning("Rejected direct call to %s.", node.func.id)
159159
return False
160160
if isinstance(node, ast.Constant) and isinstance(node.value, str):
161161
if "__" in node.value:

0 commit comments

Comments
 (0)