33from __future__ import annotations
44
55import secrets
6- from dataclasses import dataclass
6+ import threading
7+ from dataclasses import dataclass , field
78from hashlib import sha256
89from typing import Any , ClassVar
910
@@ -15,9 +16,36 @@ def _generate_receipt_id() -> str:
1516 return f"rcpt_{ secrets .token_hex (4 )} "
1617
1718
18- # Global chain position counter (per-engine, monotonically increasing)
19- _chain_position_counter : int = 0
20- _last_receipt_hash : str = ""
19+ class ReceiptChain :
20+ """Thread-safe receipt chain state.
21+
22+ Each instance tracks its own chain position counter and last receipt hash,
23+ eliminating the previous global mutable state that was unsafe in
24+ multi-worker environments.
25+ """
26+
27+ def __init__ (self ) -> None :
28+ self ._lock = threading .Lock ()
29+ self ._chain_position_counter : int = 0
30+ self ._last_receipt_hash : str = ""
31+
32+ def advance (self ) -> tuple [int , str ]:
33+ """Atomically increment chain position and return (position, previous_hash)."""
34+ with self ._lock :
35+ self ._chain_position_counter += 1
36+ position = self ._chain_position_counter
37+ previous_hash = self ._last_receipt_hash
38+ return position , previous_hash
39+
40+ def set_last_hash (self , receipt_hash : str ) -> None :
41+ """Update the last receipt hash after computing it."""
42+ with self ._lock :
43+ self ._last_receipt_hash = receipt_hash
44+
45+
46+ # Default chain instance (per-process). For multi-worker deployments,
47+ # each worker gets its own ReceiptChain instance automatically.
48+ _default_chain = ReceiptChain ()
2149
2250
2351@dataclass
@@ -32,8 +60,14 @@ def receipt_id(self) -> str:
3260 return self ._data ["receipt_id" ]
3361
3462 @classmethod
35- def issue (cls , settlement : Any , platform_private_key : str ) -> "ExecutionReceipt" :
36- global _chain_position_counter , _last_receipt_hash
63+ def issue (
64+ cls ,
65+ settlement : Any ,
66+ platform_private_key : str ,
67+ chain : ReceiptChain | None = None ,
68+ ) -> "ExecutionReceipt" :
69+ if chain is None :
70+ chain = _default_chain
3771
3872 settlement_data = settlement .to_dict () if hasattr (settlement , "to_dict" ) else deep_copy (settlement )
3973
@@ -139,7 +173,7 @@ def issue(cls, settlement: Any, platform_private_key: str) -> "ExecutionReceipt"
139173 "dispute_filed" : state == "DISPUTED" ,
140174 }
141175 if quality_score :
142- impact ["quality_score_recorded_bps" ] = int (quality_score * 10000 )
176+ impact ["quality_score_recorded_bps" ] = round (quality_score * 10000 )
143177 reputation_impacts .append (impact )
144178
145179 reputation_impacts .append ({
@@ -153,9 +187,8 @@ def issue(cls, settlement: Any, platform_private_key: str) -> "ExecutionReceipt"
153187 # Verity hash (placeholder — real implementation links to Verity engine)
154188 verity_hash = f"sha256:{ sha256 (canonical_json_bytes (settlement_data )).hexdigest ()} "
155189
156- # Chain
157- _chain_position_counter += 1
158- chain_position = _chain_position_counter
190+ # Chain (thread-safe via ReceiptChain instance)
191+ chain_position , previous_hash = chain .advance ()
159192
160193 receipt_data : dict [str , Any ] = {
161194 "receipt_id" : _generate_receipt_id (),
@@ -183,12 +216,12 @@ def issue(cls, settlement: Any, platform_private_key: str) -> "ExecutionReceipt"
183216 },
184217 }
185218
186- if _last_receipt_hash :
187- receipt_data ["chain_previous_hash" ] = f"sha256:{ _last_receipt_hash } "
219+ if previous_hash :
220+ receipt_data ["chain_previous_hash" ] = f"sha256:{ previous_hash } "
188221
189222 # Compute receipt hash for chain continuity
190223 receipt_hash = sha256 (canonical_json_bytes (receipt_data )).hexdigest ()
191- _last_receipt_hash = receipt_hash
224+ chain . set_last_hash ( receipt_hash )
192225
193226 validate_against_schema (cls .SCHEMA , receipt_data )
194227 obj = cls (receipt_data )
0 commit comments