diff --git a/ZKP_Demo_Tool/# b/ZKP_Demo_Tool/# new file mode 100644 index 00000000..e69de29b diff --git a/ZKP_Demo_Tool/Readme.md b/ZKP_Demo_Tool/Readme.md new file mode 100644 index 00000000..5d345997 --- /dev/null +++ b/ZKP_Demo_Tool/Readme.md @@ -0,0 +1,68 @@ +# ๐Ÿ” Zero-Knowledge Proof Interactive Tutorial (Graph-Based Simulation) + +This project is a **visual and interactive educational tool** that demonstrates the fundamental principles of **Zero-Knowledge Proofs (ZKPs)** using graph-based simulations and commitment schemes. It is built entirely using **Python + PyQt5**, and is designed to engage both cryptography students and mentors with real-world analogies and intuitive illustrations. + +Developed by **Susmita Chakrabarty** as part of a GSoC proposal under the PyDataStructs organization. + +--- + +## What It Teaches + +This tutorial explains: + +- **Commitment Schemes**: Using SHA-256 to create secure, hidden commitments. +- ๐Ÿ” **Binding**: Commitments cannot be altered after submission. +- **Hiding**: Commitments reveal no information until the reveal phase. +- **ZKP Challenge Rounds**: Verifier issues a challenge, prover reveals commitment. +- **Graph-Based Real-World Use Cases**: Transaction roles in banking systems, fraud detection, and more. + +Each scene is part of a **narrative and interactive journey**, gradually introducing cryptographic guarantees in a way that is fun, memorable, and technically sound. + +--- + +## ๐Ÿ“ Project Structure +```plaintext +ZKP_Demo_Tool/ +โ”œโ”€โ”€ assets/ +โ”‚ โ””โ”€โ”€ scene1.png #Optional illustration or background image +โ”‚ +โ”œโ”€โ”€ tutorial/ +โ”‚ โ”œโ”€โ”€ tutorial_scene.py #Scene 1 - Intro to commitment schemes +โ”‚ โ”œโ”€โ”€ scene2_commitment.py # Scene 2 - ZKP using transaction graph +โ”‚ โ”œโ”€โ”€ scene3_bipartate.py # Scene 3 - Bipartite graph simulation +โ”‚ โ””โ”€โ”€ run_all_scenes.py # Launcher GUI for all scenes +โ”‚ +โ””โ”€โ”€ README.md # Project documentation + +``` +## How to Run + +### 1. Recommended: Start with the Launcher + +```bash +cd /ZKP_DEMO_TOOL/tutorial +python run_all_scenes.py +``` +## Designed For + +- Cryptography students and researchers +- Mentors reviewing GSoC/academic projects +- Visual learners who prefer intuitive simulations +- Anyone curious about ZKPs in real-world graphs + +--- + +## Future Directions + +- Add auto-mode for repeated ZKP rounds with statistics +- Export logs to PDF for classroom use +- Sound or animation effects for fraud detection +- Homomorphic commitments or multi-party extensions + +--- + +## Acknowledgments + +This demo was designed to be engaging and intellectually rich while remaining accessible. +Thanks to [Wikipedia - Commitment Scheme](https://en.wikipedia.org/wiki/Commitment_scheme), educational ZKP materials, and feedback from mentors and peers. + diff --git a/ZKP_Demo_Tool/assets/scene1.png b/ZKP_Demo_Tool/assets/scene1.png new file mode 100644 index 00000000..2d7eaf60 Binary files /dev/null and b/ZKP_Demo_Tool/assets/scene1.png differ diff --git a/ZKP_Demo_Tool/tutorial/run_all_scenes.py b/ZKP_Demo_Tool/tutorial/run_all_scenes.py new file mode 100644 index 00000000..757f578c --- /dev/null +++ b/ZKP_Demo_Tool/tutorial/run_all_scenes.py @@ -0,0 +1,52 @@ +import sys +import subprocess +from PyQt5.QtWidgets import ( + QApplication, QWidget, QPushButton, QLabel, QVBoxLayout +) +from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt + +class ZKPSceneLauncher(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("๐Ÿ” Zero-Knowledge Proof Demo Launcher") + self.setGeometry(300, 200, 500, 320) + self.setStyleSheet("background-color: #1e1e1e; color: white;") + + layout = QVBoxLayout() + layout.setSpacing(15) + + title = QLabel("๐ŸŽ“ Launch a Scene") + title.setFont(QFont("Arial", 18, QFont.Bold)) + title.setAlignment(Qt.AlignCenter) + layout.addWidget(title) + + # Scene 1 + scene1_btn = QPushButton(" Scene 1: Introduction to Commitment") + scene1_btn.clicked.connect(lambda: self.launch("tutorial_scene.py")) + layout.addWidget(scene1_btn) + + # Scene 2 + scene2_btn = QPushButton("๐ŸŽฎ Scene 2: Commitment Game") + scene2_btn.clicked.connect(lambda: self.launch("scene2_commitment.py")) + layout.addWidget(scene2_btn) + + # Scene 3 + scene3_btn = QPushButton("๐Ÿ”€ Scene 3: Bipartite Graph ZKP") + scene3_btn.clicked.connect(lambda: self.launch("scene3_bipartate.py")) + layout.addWidget(scene3_btn) + + # Style all buttons + for btn in [scene1_btn, scene2_btn, scene3_btn]: + btn.setStyleSheet("padding: 12px; font-size: 14px; background-color: #2a2a2a; border-radius: 8px;") + + self.setLayout(layout) + + def launch(self, script_path): + subprocess.Popen(["python", script_path]) + +if __name__ == "__main__": + app = QApplication(sys.argv) + launcher = ZKPSceneLauncher() + launcher.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/ZKP_Demo_Tool/tutorial/scene2_commitment.py b/ZKP_Demo_Tool/tutorial/scene2_commitment.py new file mode 100644 index 00000000..b813fbe3 --- /dev/null +++ b/ZKP_Demo_Tool/tutorial/scene2_commitment.py @@ -0,0 +1,193 @@ +import sys +import subprocess +import hashlib +import random +from PyQt5.QtWidgets import ( + QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QGraphicsScene, + QGraphicsView, QGraphicsEllipseItem, QGraphicsTextItem, QGraphicsLineItem, + QHBoxLayout, QScrollArea +) +from PyQt5.QtGui import QFont, QPen, QBrush, QColor +from PyQt5.QtCore import Qt, QPointF, QLineF + + +class ZKPNode: + def __init__(self, name, role_label, position, color): + self.name = name + self.role_label = role_label + self.position = position + self.color = color + self.nonce = str(random.randint(10000, 99999)) + self.commitment = hashlib.sha256((role_label + self.nonce).encode()).hexdigest() + self.revealed_role = role_label + self.revealed_nonce = self.nonce + + +class NarrationEngine: + @staticmethod + def format_log(node1, node2, binding_ok, hiding_ok, binding_broken=False): + log = f""" +๐ŸŽฅ Security Camera Activated: Monitoring link โ†’ {node1.name} โ†” {node2.name} + +๐Ÿ” Commitments Submitted: + โ€ข {node1.name} โ†’ SHA256(????) = {node1.commitment[:12]}... + โ€ข {node2.name} โ†’ SHA256(????) = {node2.commitment[:12]}... + +Challenge Issued โ†’ Reveal phase begins... + +๐Ÿ”“ Revealed: + โ€ข {node1.name} โ†’ \"{node1.revealed_role}\" with nonce = {node1.revealed_nonce} + โ€ข {node2.name} โ†’ \"{node2.revealed_role}\" with nonce = {node2.revealed_nonce} + +Recomputed Hashes: +""" + if binding_broken: + log += " โŒ Mismatch detected โ€” Binding broken!\n Prover tried to change their role after committing.\n\n" + else: + log += " โœ… Matches original commitments โ€” Binding held.\n\n" + + if not binding_broken and hiding_ok: + log += "โœ… Hiding held โ€” Verifier only learns that roles are different.\nZKP passed successfully.\n" + elif not binding_broken and not hiding_ok: + log += f"โš ๏ธ Hiding broken โ€” {node1.name} and {node2.name} revealed same role!\nZKP failed. Verifier now knows part of the secret mapping.\n" + + log += """ +๐Ÿ“˜ Explanation: +- Binding ensures that once a role is committed with a hash, it can't be changed. +- Hiding ensures that the hash doesn't reveal the actual role until the reveal phase. +- If two adjacent nodes share the same role, it can indicate a conflict of interest or security flaw. + For example, if an ATM and a Validator are the same role, one could validate its own transaction. + This breaks the fundamental principle of role separation in secure systems. +""" + return log + + +class SceneZKPGraph(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("๐Ÿ” ZKP Graph Simulation - Security Log Mode") + self.setGeometry(150, 100, 1100, 750) + self.setStyleSheet("background-color: #121212; color: white;") + + self.scene = QGraphicsScene() + self.view = QGraphicsView(self.scene) + self.view.setStyleSheet("background-color: #1e1e1e; border: none;") + + # Scrollable narration box + self.text_output = QLabel() + self.text_output.setWordWrap(True) + self.text_output.setFont(QFont("Courier New", 12)) + self.text_output.setStyleSheet("background-color: #1c1c1c; padding: 10px; border: 1px solid #444; color: white;") + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setWidget(self.text_output) + scroll.setMinimumHeight(250) + + self.verify_button = QPushButton("๐ŸŽฏ Simulate ZKP Verification") + self.verify_button.setFont(QFont("Arial", 14)) + self.verify_button.setStyleSheet("padding: 10px; background-color: #2d3436; color: white; border-radius: 8px;") + self.verify_button.clicked.connect(self.reveal_connection) + + self.next_button = QPushButton("โžก Next: Scene 3 - Bipartate Graph") + self.next_button.setFont(QFont("Arial", 13, QFont.Bold)) + self.next_button.setStyleSheet( + "background-color: #0055ff; color: white; padding: 10px; border-radius: 10px;" + ) + self.next_button.clicked.connect(self.go_to_next_scene) + + layout = QVBoxLayout() + layout.addWidget(self.view) + layout.addWidget(self.verify_button) + layout.addWidget(scroll) + + # Center the button using a horizontal layout + button_layout = QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(self.next_button) + button_layout.addStretch() + layout.addLayout(button_layout) + + self.setLayout(layout) + + self.nodes = [] + self.node_graphics = {} + self.edges = [] + + self.build_graph() + + def build_graph(self): + layout = [ + ("ATM", "Initiator", QPointF(150, 150), QColor("#e74c3c")), + ("Validator", "Validator", QPointF(500, 150), QColor("#f1c40f")), + ("Merchant", "Receiver", QPointF(300, 350), QColor("#2ecc71")), + ("Customer", "Initiator", QPointF(850, 150), QColor("#e74c3c")), + ("Treasury", "Receiver", QPointF(650, 400), QColor("#2ecc71")), + ("Insider", "Validator", QPointF(150, 450), QColor("#f1c40f")) + ] + + connections = [(0, 1), (1, 2), (2, 4), (3, 1), (5, 2), (0, 5)] + + for name, role, pos, color in layout: + node = ZKPNode(name, role, pos, color) + self.nodes.append(node) + + for idx, node in enumerate(self.nodes): + ellipse = QGraphicsEllipseItem(-30, -30, 60, 60) + ellipse.setBrush(QBrush(Qt.gray)) + ellipse.setPen(QPen(Qt.white, 2)) + ellipse.setPos(node.position) + self.scene.addItem(ellipse) + + lock = QGraphicsTextItem("๐Ÿ”") + lock.setFont(QFont("Arial", 16)) + lock.setDefaultTextColor(Qt.white) + lock.setPos(node.position.x() - 10, node.position.y() - 55) + self.scene.addItem(lock) + + label = QGraphicsTextItem(node.name) + label.setFont(QFont("Arial", 12)) + label.setDefaultTextColor(Qt.white) + label.setPos(node.position.x() - 30, node.position.y() + 35) + self.scene.addItem(label) + + self.node_graphics[idx] = (ellipse, lock, label) + + for a, b in connections: + p1 = self.nodes[a].position + p2 = self.nodes[b].position + line = QGraphicsLineItem(QLineF(p1, p2)) + line.setPen(QPen(Qt.white, 2)) + self.scene.addItem(line) + self.edges.append((a, b)) + + def reveal_connection(self): + a, b = random.choice(self.edges) + node1, node2 = self.nodes[a], self.nodes[b] + + self.node_graphics[a][0].setBrush(QBrush(node1.color)) + self.node_graphics[b][0].setBrush(QBrush(node2.color)) + + recomputed_hash_1 = hashlib.sha256((node1.revealed_role + node1.revealed_nonce).encode()).hexdigest() + recomputed_hash_2 = hashlib.sha256((node2.revealed_role + node2.revealed_nonce).encode()).hexdigest() + + binding_ok = (recomputed_hash_1 == node1.commitment and recomputed_hash_2 == node2.commitment) + hiding_ok = node1.revealed_role != node2.revealed_role + + log = NarrationEngine.format_log( + node1, node2, + binding_ok=binding_ok, + hiding_ok=hiding_ok, + binding_broken=not binding_ok + ) + self.text_output.setText(log) + + def go_to_next_scene(self): + subprocess.Popen(["python", "scene3_bipartate.py"]) + self.close() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = SceneZKPGraph() + window.show() + sys.exit(app.exec_()) diff --git a/ZKP_Demo_Tool/tutorial/scene3_bipartate.py b/ZKP_Demo_Tool/tutorial/scene3_bipartate.py new file mode 100644 index 00000000..c00c4e51 --- /dev/null +++ b/ZKP_Demo_Tool/tutorial/scene3_bipartate.py @@ -0,0 +1,161 @@ +import sys +import hashlib +import random +from PyQt5.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QGraphicsScene, + QGraphicsView, QGraphicsEllipseItem, QGraphicsTextItem, QGraphicsLineItem, + QHBoxLayout, QTextEdit +) +from PyQt5.QtGui import QPen, QBrush, QColor, QFont +from PyQt5.QtCore import Qt, QPointF + +class Node(QGraphicsEllipseItem): + def __init__(self, name, pos, color, node_type, description=""): + super().__init__(-30, -30, 60, 60) + self.setBrush(QBrush(color)) + self.setPen(QPen(Qt.white, 2)) + self.setPos(pos) + self.name = name + self.node_type = node_type + self.description = description + + self.label = QGraphicsTextItem(name) + self.label.setDefaultTextColor(Qt.white) + self.label.setFont(QFont("Arial", 8)) + self.label.setParentItem(self) + self.label.setPos(-30, 40) + +class Edge: + def __init__(self, scene, node1, node2): + self.line = QGraphicsLineItem(node1.x(), node1.y(), node2.x(), node2.y()) + self.line.setPen(QPen(Qt.gray, 2, Qt.SolidLine)) + scene.addItem(self.line) + self.nodes = (node1, node2) + +class ZKPVerifierExplained(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("๐Ÿ” ZKP Simulation with Verifier Explanation") + self.setGeometry(100, 100, 1300, 850) + self.setStyleSheet("background-color: #1e1e1e; color: white;") + + self.layout = QVBoxLayout() + self.setLayout(self.layout) + + self.scene = QGraphicsScene() + self.view = QGraphicsView(self.scene) + self.layout.addWidget(self.view) + + self.statement_label = QLabel("๐ŸŽ“ Knowledge Statement: 'I know (u, v) โˆˆ G such that employee u has access to account v.'") + self.statement_label.setStyleSheet("padding: 8px; font-size: 16px;") + self.layout.addWidget(self.statement_label) + + self.protocol_log = QTextEdit() + self.protocol_log.setReadOnly(True) + self.protocol_log.setStyleSheet("background-color: #111; color: #ddd; font-family: monospace;") + self.layout.addWidget(self.protocol_log) + + self.button_layout = QHBoxLayout() + self.commit_button = QPushButton("๐Ÿ” Step 1: Commit to Secret Edge") + self.challenge_account_btn = QPushButton("Step 2: Reveal Account") + self.challenge_employee_btn = QPushButton("Step 2: Reveal Employee") + self.challenge_account_btn.setEnabled(False) + self.challenge_employee_btn.setEnabled(False) + self.button_layout.addWidget(self.commit_button) + self.button_layout.addWidget(self.challenge_account_btn) + self.button_layout.addWidget(self.challenge_employee_btn) + self.layout.addLayout(self.button_layout) + + self.commit_button.clicked.connect(self.commit_phase) + self.challenge_account_btn.clicked.connect(lambda: self.reveal("account")) + self.challenge_employee_btn.clicked.connect(lambda: self.reveal("employee")) + + self.accounts = [] + self.employees = [] + self.edges = [] + self.secret_edge = None + self.nonce = None + self.commitment = None + + self.build_graph() + + def build_graph(self): + account_data = [ + ("Vault#011", "$800K"), + ("Account#992", "Suspicious Flow"), + ("Loan#743", "Offshore Link") + ] + + emp_data = [ + ("Alice", "Teller - Branch A"), + ("Bob", "Manager - HQ"), + ("Claire", "Auditor - Internal Affairs") + ] + + for i, (name, desc) in enumerate(account_data): + node = Node(name, QPointF(200, 100 + i * 200), QColor("gold"), "account", desc) + self.accounts.append(node) + self.scene.addItem(node) + + for i, (name, desc) in enumerate(emp_data): + node = Node(name, QPointF(1000, 100 + i * 200), QColor("skyblue"), "employee", desc) + self.employees.append(node) + self.scene.addItem(node) + + access_list = [ + (0, 0), + (1, 1), + (2, 2), + (0, 1) + ] + + for a_idx, e_idx in access_list: + self.edges.append(Edge(self.scene, self.accounts[a_idx], self.employees[e_idx])) + + def commit_phase(self): + self.secret_edge = random.choice(self.edges) + u, v = self.secret_edge.nodes + self.nonce = str(random.randint(100000, 999999)) + combined = u.name + v.name + self.nonce + self.commitment = hashlib.sha256(combined.encode()).hexdigest() + + self.protocol_log.clear() + self.protocol_log.append("๐Ÿ” [Commit Phase]") + self.protocol_log.append(f"Secret Edge: {u.name} โ†” {v.name} (kept hidden)") + self.protocol_log.append(f"Nonce (random salt): {self.nonce}") + self.protocol_log.append(f"Commitment = SHA256({u.name} + {v.name} + nonce)") + self.protocol_log.append(f"โ†’ {self.commitment}\n") + + self.challenge_account_btn.setEnabled(True) + self.challenge_employee_btn.setEnabled(True) + + def reveal(self, challenge_type): + u, v = self.secret_edge.nodes + self.protocol_log.append(f" [Challenge Phase] Verifier asks to reveal: {challenge_type.upper()}") + + if challenge_type == "account": + revealed = u.name + self.protocol_log.append(f"Prover reveals: {revealed} + nonce") + self.protocol_log.append("\nVerifier tries every employee to match the hash:") + for emp in self.employees: + trial = hashlib.sha256((u.name + emp.name + self.nonce).encode()).hexdigest() + match = "โœ… MATCH" if trial == self.commitment else "โŒ" + self.protocol_log.append(f" โ€ข H({u.name} + {emp.name} + {self.nonce}) โ†’ {trial[:20]}... {match}") + else: + revealed = v.name + self.protocol_log.append(f"Prover reveals: {revealed} + nonce") + self.protocol_log.append("\nVerifier tries every account to match the hash:") + for acc in self.accounts: + trial = hashlib.sha256((acc.name + v.name + self.nonce).encode()).hexdigest() + match = "โœ… MATCH" if trial == self.commitment else "โŒ" + self.protocol_log.append(f" โ€ข H({acc.name} + {v.name} + {self.nonce}) โ†’ {trial[:20]}... {match}") + + self.protocol_log.append("\n๐Ÿ›ก๏ธ If any match โ†’ verifier is convinced.") + self.challenge_account_btn.setEnabled(False) + self.challenge_employee_btn.setEnabled(False) + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = ZKPVerifierExplained() + window.show() + sys.exit(app.exec_()) diff --git a/ZKP_Demo_Tool/tutorial/tutorial_scene.py b/ZKP_Demo_Tool/tutorial/tutorial_scene.py new file mode 100644 index 00000000..2095633f --- /dev/null +++ b/ZKP_Demo_Tool/tutorial/tutorial_scene.py @@ -0,0 +1,120 @@ +import sys +import subprocess +from PyQt5.QtWidgets import ( + QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QApplication, + QGraphicsOpacityEffect, QTextEdit +) +from PyQt5.QtGui import QPixmap, QFont +from PyQt5.QtCore import Qt, QPropertyAnimation, QEasingCurve + +class ZKPTutorial(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("Scene 1: Introduction to ZKP") + self.setGeometry(300, 200, 900, 700) + self.setStyleSheet("background-color: #121212; color: white;") + + self.init_ui() + self.animate_scene() + + def init_ui(self): + self.layout = QVBoxLayout() + + # Title + self.title_label = QLabel("Scene 1: Roles in the Network + Commitment Primer") + self.title_label.setAlignment(Qt.AlignCenter) + self.title_label.setFont(QFont("Arial", 24, QFont.Bold)) + self.layout.addWidget(self.title_label) + + # Image placeholder + self.image_label = QLabel() + pixmap = QPixmap("../assets/scene1.png") + if pixmap.isNull(): + self.image_label.setText("[Visual Story: ATM, Bank, Merchant cartoon goes here]") + self.image_label.setAlignment(Qt.AlignCenter) + self.image_label.setFont(QFont("Arial", 16, QFont.Bold)) + else: + self.image_label.setPixmap(pixmap.scaled(720, 360, Qt.KeepAspectRatio)) + self.image_label.setAlignment(Qt.AlignCenter) + self.layout.addWidget(self.image_label) + + # Story-style narration + self.narration_label = QLabel() + self.narration_label.setWordWrap(True) + self.narration_label.setFont(QFont("Georgia", 15)) + self.narration_label.setAlignment(Qt.AlignLeft) + self.narration_label.setText( + "

โœจ Every adventure needs a team โ€”
" + "and in the world of secure transactions, three heroes take the stage!

" + + "

๐ŸŸฆ Initiator starts the quest:
" + "They say, โ€œI want to make a move!โ€

" + + "

๐Ÿ”บ Validator checks the map:
" + "โ€œIs this legit? Let me verify...โ€

" + + "

๐ŸŸฉ Receiver opens the treasure chest:
" + "โ€œItโ€™s real. Iโ€™m in. Transaction complete!โ€

" + + "

But here's the twistโ€ฆ
" + "What if we could prove we did everything right โ€”
" + "๐Ÿ’ก without showing the treasure itself?

" + + "

Thatโ€™s where Zero-Knowledge Proofs enter the story.
" + "Ready to see some cryptographic magic?

" + ) + self.layout.addWidget(self.narration_label) + + # Technical ZKP Explanation + self.technical_box = QTextEdit() + self.technical_box.setReadOnly(True) + self.technical_box.setStyleSheet("background-color: #1a1a1a; color: #ccc; font-family: Courier; font-size: 13px;") + self.technical_box.setText( + "Cryptographic Primer:\n\n" + "We create a commitment to the secret:\n" + " commit = H(secret || nonce)\n\n" + "This hash is like a sealed envelope.\n" + "The verifier can challenge us to reveal only part of the secret.\n\n" + "โœ… Binding: We can't change the secret after committing.\n" + "Hiding: The verifier sees only what we show โ€” never the full secret.\n" + ) + self.layout.addWidget(self.technical_box) + + # Next button + self.next_button = QPushButton("โžก Next: Scene 2 - Commitment Game") + self.next_button.setFont(QFont("Arial", 13, QFont.Bold)) + self.next_button.setStyleSheet( + "background-color: #0055ff; color: white; padding: 10px; border-radius: 10px;" + ) + self.next_button.clicked.connect(self.go_to_next_scene) + + # Center next button + button_layout = QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(self.next_button) + button_layout.addStretch() + self.layout.addLayout(button_layout) + + self.setLayout(self.layout) + + def animate_scene(self): + # Fade-in effect for narration + effect = QGraphicsOpacityEffect() + self.narration_label.setGraphicsEffect(effect) + animation = QPropertyAnimation(effect, b"opacity") + animation.setDuration(1500) + animation.setStartValue(0.0) + animation.setEndValue(1.0) + animation.setEasingCurve(QEasingCurve.InOutQuad) + animation.start() + self.fade_animation = animation # prevent garbage collection + + def go_to_next_scene(self): + subprocess.Popen(["python", "scene2_commitment.py"]) + self.close() + +if __name__ == '__main__': + app = QApplication(sys.argv) + tutorial = ZKPTutorial() + tutorial.show() + sys.exit(app.exec_()) diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index 334f522c..475d8db1 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -11,6 +11,8 @@ from pydatastructs.graphs.graph import Graph from pydatastructs.linear_data_structures.algorithms import merge_sort_parallel from pydatastructs import PriorityQueue +import hashlib +import secrets __all__ = [ 'breadth_first_search', @@ -802,6 +804,52 @@ def shortest_paths(graph: Graph, algorithm: str, "finding shortest paths in graphs."%(algorithm)) return getattr(algorithms, func)(graph, source, target) +def pedersen_commitment(graph, g, h, p, q, include_weights=True): + """ + Returns a Pedersen commitment for the given graph. + + This function creates a cryptographic commitment of the graph's structure. + The commitment hides node and edge information but allows later verification + by revealing the original graph and blinding factor. + + Parameters + ---------- + graph : Graph + The PyDataStructs graph object to commit. + + g : int + A generator of a subgroup of order q (g^q โ‰ก 1 mod p). + + h : int + A second, independent generator of the same subgroup. + + p : int + A large prime modulus (โ‰ฅ1024 bits) such that q divides p - 1. + + q : int + A prime number representing the subgroup order (โ‰ฅ160 bits). + + include_weights : bool, optional + Whether to include edge weights in the graph serialization. Default is True. + Notes + ----- + - The blinding factor `r` must be kept private. + - Changing even a single edge or vertex will yield a different commitment. + """ + if p.bit_length() < 1024: + raise ValueError("p must be a 1024-bit prime or larger.") + if q.bit_length() < 160: + raise ValueError("q must be a 160-bit prime or larger.") + if (p - 1) % q != 0: + raise ValueError("q must divide (p - 1).") + if pow(g, q, p) != 1 or pow(h, q, p) != 1: + raise ValueError("g and h must be generators of a subgroup of order q.") + data = graph.serialize_graph(graph, include_weights) + m = int(hashlib.sha256(data.encode()).hexdigest(), 16) % q + r = secrets.randbelow(q) + commitment = (pow(g, m, p) * pow(h, r, p)) % p + return commitment, r + def _bellman_ford_adjacency_list(graph: Graph, source: str, target: str) -> tuple: distances, predecessor, visited, cnts = {}, {}, {}, {} diff --git a/pydatastructs/graphs/graph.py b/pydatastructs/graphs/graph.py index 24f33a17..69435874 100644 --- a/pydatastructs/graphs/graph.py +++ b/pydatastructs/graphs/graph.py @@ -1,10 +1,46 @@ from pydatastructs.utils.misc_util import Backend, raise_if_backend_is_not_python - +from pydatastructs.utils.misc_util import GraphEdge +from pydatastructs.utils import AdjacencyListGraphNode +import hmac +import hashlib +import os +import secrets +import threading +def rotate_secret_key(): + """ Automatically rotates secret key after 30 days """ + while True: + os.environ["HMAC_SECRET_KEY"] = secrets.token_hex(32) + time.sleep(30 * 24 * 60 * 60) +def get_secret_key(): + """Gets the HMAC secret key""" + secret_key = os.getenv("HMAC_SECRET_KEY") + if secret_key is None: + try: + with open("hmac_key.txt", "r") as f: + secret_key = f.read().strip() + except FileNotFoundError: + raise RuntimeError("Secret key is missing! Set HMAC_SECRET_KEY or create hmac_key.txt.") + return secret_key.encode() + +def generate_hmac(data): + """Generating HMAC signature for integrity verification""" + return hmac.new(get_secret_key(), data.encode(),hashlib.sha256).hexdigest() +def serialize_graph(graph, include_weights=True): + """Converts a graph into a string for HMAC signing.""" + if not graph.vertices or not graph.edge_weights: + return "EMPTY_GRAPH" + vertices = sorted(graph.vertices) + if include_weights: + edges = sorted((str(k), v) for k, v in graph.edge_weights.items()) + else: + edges = sorted(str(k) for k in graph.edge_weights) + return str(vertices) + str(edges) __all__ = [ 'Graph' ] - +import copy +import time class Graph(object): """ Represents generic concept of graphs. @@ -78,16 +114,50 @@ def __new__(cls, *args, **kwargs): from pydatastructs.graphs.adjacency_list import AdjacencyList obj = AdjacencyList(*args) obj._impl = implementation - return obj elif implementation == 'adjacency_matrix': from pydatastructs.graphs.adjacency_matrix import AdjacencyMatrix obj = AdjacencyMatrix(*args) obj._impl = implementation - return obj else: raise NotImplementedError("%s implementation is not a part " "of the library currently."%(implementation)) - + obj._impl = implementation + obj.snapshots = {} + def add_snapshot(self): + """Automatically assigns timestamps using system time.""" + timestamp = int(time.time()) + snapshot_copy = self.__class__(implementation=self._impl) + for vertex_name in self.vertices: + snapshot_copy.add_vertex(AdjacencyListGraphNode(vertex_name)) + snapshot_copy.edge_weights = { + key: GraphEdge(edge.source, edge.target, edge.value) + for key, edge in self.edge_weights.items() + } + for key, edge in snapshot_copy.edge_weights.items(): + snapshot_copy.__getattribute__(edge.source.name).add_adjacent_node(edge.target.name) + snapshot_data = serialize_graph(snapshot_copy) + snapshot_signature = generate_hmac(snapshot_data) + self.snapshots[timestamp] = {"graph": snapshot_copy, "signature": snapshot_signature} + def get_snapshot(self, timestamp: int): + """Retrieves a past version of the graph if the timestamp exists.""" + if timestamp not in self.snapshots: + raise ValueError(f"Snapshot for timestamp {timestamp} does not exist. " + f"Available timestamps: {sorted(self.snapshots.keys())}") + snapshot_info = self.snapshots[timestamp] + snapshot_graph = snapshot_info["graph"] + stored_signature = snapshot_info["signature"] + snapshot_data = serialize_graph(snapshot_graph) + computed_signature = generate_hmac(snapshot_data) + if computed_signature != stored_signature: + raise ValueError("Snapshot integrity check failed! The snapshot may have been modified.") + return snapshot_graph + def list_snapshots(self): + """Returns all stored timestamps in sorted order.""" + return sorted(self.snapshots.keys()) + obj.add_snapshot = add_snapshot.__get__(obj) + obj.get_snapshot = get_snapshot.__get__(obj) + obj.list_snapshots = list_snapshots.__get__(obj) + return obj def is_adjacent(self, node1, node2): """ Checks if the nodes with the given @@ -161,3 +231,4 @@ def num_edges(self): """ raise NotImplementedError( "This is an abstract method.") +threading.Thread(target=rotate_secret_key, daemon=True).start() diff --git a/pydatastructs/graphs/tests/test_graph_snapshots.py b/pydatastructs/graphs/tests/test_graph_snapshots.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_adjacency_list.py b/tests/test_adjacency_list.py new file mode 100644 index 00000000..3dcef8a7 --- /dev/null +++ b/tests/test_adjacency_list.py @@ -0,0 +1,44 @@ +from pydatastructs.graphs import Graph +from pydatastructs.utils import AdjacencyListGraphNode +from pydatastructs.utils.raises_util import raises + +def test_adjacency_list(): + v_1 = AdjacencyListGraphNode('v_1', 1) + v_2 = AdjacencyListGraphNode('v_2', 2) + g = Graph(v_1, v_2, implementation='adjacency_list') + v_3 = AdjacencyListGraphNode('v_3', 3) + g.add_vertex(v_2) + g.add_vertex(v_3) + g.add_edge('v_1', 'v_2') + g.add_edge('v_2', 'v_3') + g.add_edge('v_3', 'v_1') + assert g.is_adjacent('v_1', 'v_2') is True + assert g.is_adjacent('v_2', 'v_3') is True + assert g.is_adjacent('v_3', 'v_1') is True + assert g.is_adjacent('v_2', 'v_1') is False + assert g.is_adjacent('v_3', 'v_2') is False + assert g.is_adjacent('v_1', 'v_3') is False + neighbors = g.neighbors('v_1') + assert neighbors == [v_2] + v = AdjacencyListGraphNode('v', 4) + g.add_vertex(v) + g.add_edge('v_1', 'v', 0) + g.add_edge('v_2', 'v', 0) + g.add_edge('v_3', 'v', 0) + assert g.is_adjacent('v_1', 'v') is True + assert g.is_adjacent('v_2', 'v') is True + assert g.is_adjacent('v_3', 'v') is True + e1 = g.get_edge('v_1', 'v') + e2 = g.get_edge('v_2', 'v') + e3 = g.get_edge('v_3', 'v') + assert (e1.source.name, e1.target.name) == ('v_1', 'v') + assert (e2.source.name, e2.target.name) == ('v_2', 'v') + assert (e3.source.name, e3.target.name) == ('v_3', 'v') + g.remove_edge('v_1', 'v') + assert g.is_adjacent('v_1', 'v') is False + g.remove_vertex('v') + assert g.is_adjacent('v_2', 'v') is False + assert g.is_adjacent('v_3', 'v') is False + + assert raises(ValueError, lambda: g.add_edge('u', 'v')) + assert raises(ValueError, lambda: g.add_edge('v', 'x')) diff --git a/tests/test_adjacency_matrix.py b/tests/test_adjacency_matrix.py new file mode 100644 index 00000000..2dace426 --- /dev/null +++ b/tests/test_adjacency_matrix.py @@ -0,0 +1,32 @@ +from pydatastructs.graphs import Graph +from pydatastructs.utils import AdjacencyMatrixGraphNode +from pydatastructs.utils.raises_util import raises + +def test_AdjacencyMatrix(): + v_0 = AdjacencyMatrixGraphNode(0, 0) + v_1 = AdjacencyMatrixGraphNode(1, 1) + v_2 = AdjacencyMatrixGraphNode(2, 2) + g = Graph(v_0, v_1, v_2) + g.add_edge(0, 1, 0) + g.add_edge(1, 2, 0) + g.add_edge(2, 0, 0) + e1 = g.get_edge(0, 1) + e2 = g.get_edge(1, 2) + e3 = g.get_edge(2, 0) + assert (e1.source.name, e1.target.name) == ('0', '1') + assert (e2.source.name, e2.target.name) == ('1', '2') + assert (e3.source.name, e3.target.name) == ('2', '0') + assert g.is_adjacent(0, 1) is True + assert g.is_adjacent(1, 2) is True + assert g.is_adjacent(2, 0) is True + assert g.is_adjacent(1, 0) is False + assert g.is_adjacent(2, 1) is False + assert g.is_adjacent(0, 2) is False + neighbors = g.neighbors(0) + assert neighbors == [v_1] + g.remove_edge(0, 1) + assert g.is_adjacent(0, 1) is False + assert raises(ValueError, lambda: g.add_edge('u', 'v')) + assert raises(ValueError, lambda: g.add_edge('v', 'x')) + assert raises(ValueError, lambda: g.add_edge(2, 3)) + assert raises(ValueError, lambda: g.add_edge(3, 2)) diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py new file mode 100644 index 00000000..fde3571d --- /dev/null +++ b/tests/test_algorithms.py @@ -0,0 +1,446 @@ +from pydatastructs import (breadth_first_search, Graph, +breadth_first_search_parallel, minimum_spanning_tree, +minimum_spanning_tree_parallel, strongly_connected_components, +depth_first_search, shortest_paths, topological_sort, +topological_sort_parallel, max_flow) +from pydatastructs.utils.raises_util import raises + +def test_breadth_first_search(): + + def _test_breadth_first_search(ds): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + + V1 = GraphNode(0) + V2 = GraphNode(1) + V3 = GraphNode(2) + + G1 = Graph(V1, V2, V3) + + assert G1.num_vertices() == 3 + + edges = [ + (V1.name, V2.name), + (V2.name, V3.name), + (V1.name, V3.name) + ] + + for edge in edges: + G1.add_edge(*edge) + + assert G1.num_edges() == len(edges) + + parent = {} + def bfs_tree(curr_node, next_node, parent): + if next_node != "": + parent[next_node] = curr_node + return True + + breadth_first_search(G1, V1.name, bfs_tree, parent) + assert (parent[V3.name] == V1.name and parent[V2.name] == V1.name) or \ + (parent[V3.name] == V2.name and parent[V2.name] == V1.name) + + V4 = GraphNode(0) + V5 = GraphNode(1) + V6 = GraphNode(2) + V7 = GraphNode(3) + V8 = GraphNode(4) + + edges = [ + (V4.name, V5.name), + (V5.name, V6.name), + (V6.name, V7.name), + (V6.name, V4.name), + (V7.name, V8.name) + ] + + G2 = Graph(V4, V5, V6, V7, V8) + + for edge in edges: + G2.add_edge(*edge) + + assert G2.num_edges() == len(edges) + + path = [] + def path_finder(curr_node, next_node, dest_node, parent, path): + if next_node != "": + parent[next_node] = curr_node + if curr_node == dest_node: + node = curr_node + path.append(node) + while node is not None: + if parent.get(node, None) is not None: + path.append(parent[node]) + node = parent.get(node, None) + path.reverse() + return False + return True + + parent.clear() + breadth_first_search(G2, V4.name, path_finder, V7.name, parent, path) + assert path == [V4.name, V5.name, V6.name, V7.name] + + _test_breadth_first_search("List") + _test_breadth_first_search("Matrix") + +def test_breadth_first_search_parallel(): + + def _test_breadth_first_search_parallel(ds): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + + V1 = GraphNode(0) + V2 = GraphNode(1) + V3 = GraphNode(2) + V4 = GraphNode(3) + V5 = GraphNode(4) + V6 = GraphNode(5) + V7 = GraphNode(6) + V8 = GraphNode(7) + + + G1 = Graph(V1, V2, V3, V4, V5, V6, V7, V8) + + edges = [ + (V1.name, V2.name), + (V1.name, V3.name), + (V1.name, V4.name), + (V2.name, V5.name), + (V2.name, V6.name), + (V3.name, V6.name), + (V3.name, V7.name), + (V4.name, V7.name), + (V4.name, V8.name) + ] + + for edge in edges: + G1.add_edge(*edge) + + parent = {} + def bfs_tree(curr_node, next_node, parent): + if next_node != "": + parent[next_node] = curr_node + return True + + breadth_first_search_parallel(G1, V1.name, 5, bfs_tree, parent) + assert (parent[V2.name] == V1.name and parent[V3.name] == V1.name and + parent[V4.name] == V1.name and parent[V5.name] == V2.name and + (parent[V6.name] in (V2.name, V3.name)) and + (parent[V7.name] in (V3.name, V4.name)) and (parent[V8.name] == V4.name)) + + _test_breadth_first_search_parallel("List") + _test_breadth_first_search_parallel("Matrix") + +def test_minimum_spanning_tree(): + + def _test_minimum_spanning_tree(func, ds, algorithm, *args): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + a, b, c, d, e = [GraphNode(x) for x in [0, 1, 2, 3, 4]] + graph = Graph(a, b, c, d, e) + graph.add_edge(a.name, c.name, 10) + graph.add_edge(c.name, a.name, 10) + graph.add_edge(a.name, d.name, 7) + graph.add_edge(d.name, a.name, 7) + graph.add_edge(c.name, d.name, 9) + graph.add_edge(d.name, c.name, 9) + graph.add_edge(d.name, b.name, 32) + graph.add_edge(b.name, d.name, 32) + graph.add_edge(d.name, e.name, 23) + graph.add_edge(e.name, d.name, 23) + mst = func(graph, algorithm, *args) + expected_mst = [('0_3', 7), ('2_3', 9), ('3_4', 23), ('3_1', 32), + ('3_0', 7), ('3_2', 9), ('4_3', 23), ('1_3', 32)] + assert len(expected_mst) == len(mst.edge_weights.items()) + for k, v in mst.edge_weights.items(): + assert (k, v.value) in expected_mst + + fmst = minimum_spanning_tree + fmstp = minimum_spanning_tree_parallel + _test_minimum_spanning_tree(fmst, "List", "kruskal") + _test_minimum_spanning_tree(fmst, "Matrix", "kruskal") + _test_minimum_spanning_tree(fmst, "List", "prim") + _test_minimum_spanning_tree(fmstp, "List", "kruskal", 3) + _test_minimum_spanning_tree(fmstp, "Matrix", "kruskal", 3) + _test_minimum_spanning_tree(fmstp, "List", "prim", 3) + +def test_strongly_connected_components(): + + def _test_strongly_connected_components(func, ds, algorithm, *args): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + a, b, c, d, e, f, g, h = \ + [GraphNode(chr(x)) for x in range(ord('a'), ord('h') + 1)] + graph = Graph(a, b, c, d, e, f, g, h) + graph.add_edge(a.name, b.name) + graph.add_edge(b.name, c.name) + graph.add_edge(b.name, f.name) + graph.add_edge(b.name, e.name) + graph.add_edge(c.name, d.name) + graph.add_edge(c.name, g.name) + graph.add_edge(d.name, h.name) + graph.add_edge(d.name, c.name) + graph.add_edge(e.name, f.name) + graph.add_edge(e.name, a.name) + graph.add_edge(f.name, g.name) + graph.add_edge(g.name, f.name) + graph.add_edge(h.name, d.name) + graph.add_edge(h.name, g.name) + comps = func(graph, algorithm) + expected_comps = [{'e', 'a', 'b'}, {'d', 'c', 'h'}, {'g', 'f'}] + assert comps == expected_comps + + scc = strongly_connected_components + _test_strongly_connected_components(scc, "List", "kosaraju") + _test_strongly_connected_components(scc, "Matrix", "kosaraju") + +def test_depth_first_search(): + + def _test_depth_first_search(ds): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + + V1 = GraphNode(0) + V2 = GraphNode(1) + V3 = GraphNode(2) + + G1 = Graph(V1, V2, V3) + + edges = [ + (V1.name, V2.name), + (V2.name, V3.name), + (V1.name, V3.name) + ] + + for edge in edges: + G1.add_edge(*edge) + + parent = {} + def dfs_tree(curr_node, next_node, parent): + if next_node != "": + parent[next_node] = curr_node + return True + + depth_first_search(G1, V1.name, dfs_tree, parent) + assert (parent[V3.name] == V1.name and parent[V2.name] == V1.name) or \ + (parent[V3.name] == V2.name and parent[V2.name] == V1.name) + + V4 = GraphNode(0) + V5 = GraphNode(1) + V6 = GraphNode(2) + V7 = GraphNode(3) + V8 = GraphNode(4) + + edges = [ + (V4.name, V5.name), + (V5.name, V6.name), + (V6.name, V7.name), + (V6.name, V4.name), + (V7.name, V8.name) + ] + + G2 = Graph(V4, V5, V6, V7, V8) + + for edge in edges: + G2.add_edge(*edge) + + path = [] + def path_finder(curr_node, next_node, dest_node, parent, path): + if next_node != "": + parent[next_node] = curr_node + if curr_node == dest_node: + node = curr_node + path.append(node) + while node is not None: + if parent.get(node, None) is not None: + path.append(parent[node]) + node = parent.get(node, None) + path.reverse() + return False + return True + + parent.clear() + depth_first_search(G2, V4.name, path_finder, V7.name, parent, path) + assert path == [V4.name, V5.name, V6.name, V7.name] + + _test_depth_first_search("List") + _test_depth_first_search("Matrix") + +def test_shortest_paths(): + + def _test_shortest_paths_positive_edges(ds, algorithm): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + vertices = [GraphNode('S'), GraphNode('C'), + GraphNode('SLC'), GraphNode('SF'), + GraphNode('D')] + + graph = Graph(*vertices) + graph.add_edge('S', 'SLC', 2) + graph.add_edge('C', 'S', 4) + graph.add_edge('C', 'D', 2) + graph.add_edge('SLC', 'C', 2) + graph.add_edge('SLC', 'D', 3) + graph.add_edge('SF', 'SLC', 2) + graph.add_edge('SF', 'S', 2) + graph.add_edge('D', 'SF', 3) + dist, pred = shortest_paths(graph, algorithm, 'SLC') + assert dist == {'S': 6, 'C': 2, 'SLC': 0, 'SF': 6, 'D': 3} + assert pred == {'S': 'C', 'C': 'SLC', 'SLC': None, 'SF': 'D', 'D': 'SLC'} + dist, pred = shortest_paths(graph, algorithm, 'SLC', 'SF') + assert dist == 6 + assert pred == {'S': 'C', 'C': 'SLC', 'SLC': None, 'SF': 'D', 'D': 'SLC'} + graph.remove_edge('SLC', 'D') + graph.add_edge('D', 'SLC', -10) + assert raises(ValueError, lambda: shortest_paths(graph, 'bellman_ford', 'SLC')) + + def _test_shortest_paths_negative_edges(ds, algorithm): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + vertices = [GraphNode('s'), GraphNode('a'), + GraphNode('b'), GraphNode('c'), + GraphNode('d')] + + graph = Graph(*vertices) + graph.add_edge('s', 'a', 3) + graph.add_edge('s', 'b', 2) + graph.add_edge('a', 'c', 1) + graph.add_edge('b', 'd', 1) + graph.add_edge('b', 'a', -2) + graph.add_edge('c', 'd', 1) + dist, pred = shortest_paths(graph, algorithm, 's') + assert dist == {'s': 0, 'a': 0, 'b': 2, 'c': 1, 'd': 2} + assert pred == {'s': None, 'a': 'b', 'b': 's', 'c': 'a', 'd': 'c'} + dist, pred = shortest_paths(graph, algorithm, 's', 'd') + assert dist == 2 + assert pred == {'s': None, 'a': 'b', 'b': 's', 'c': 'a', 'd': 'c'} + + _test_shortest_paths_positive_edges("List", 'bellman_ford') + _test_shortest_paths_positive_edges("Matrix", 'bellman_ford') + _test_shortest_paths_negative_edges("List", 'bellman_ford') + _test_shortest_paths_negative_edges("Matrix", 'bellman_ford') + _test_shortest_paths_positive_edges("List", 'dijkstra') + _test_shortest_paths_positive_edges("Matrix", 'dijkstra') + +def test_all_pair_shortest_paths(): + + def _test_shortest_paths_negative_edges(ds, algorithm): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + vertices = [GraphNode('1'), GraphNode('2'), + GraphNode('3'), GraphNode('4')] + + graph = Graph(*vertices) + graph.add_edge('1', '3', -2) + graph.add_edge('2', '1', 4) + graph.add_edge('2', '3', 3) + graph.add_edge('3', '4', 2) + graph.add_edge('4', '2', -1) + dist, next_v = shortest_paths(graph, algorithm, 's') + assert dist == {'1': {'3': -2, '1': 0, '4': 0, '2': -1}, + '2': {'1': 4, '3': 2, '2': 0, '4': 4}, + '3': {'4': 2, '3': 0, '1': 5, '2': 1}, + '4': {'2': -1, '4': 0, '1': 3, '3': 1}} + assert next_v == {'1': {'3': '1', '1': '1', '4': None, '2': None}, + '2': {'1': '2', '3': None, '2': '2', '4': None}, + '3': {'4': '3', '3': '3', '1': None, '2': None}, + '4': {'2': '4', '4': '4', '1': None, '3': None}} + +def test_topological_sort(): + + def _test_topological_sort(func, ds, algorithm, threads=None): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + vertices = [GraphNode('2'), GraphNode('3'), GraphNode('5'), + GraphNode('7'), GraphNode('8'), GraphNode('10'), + GraphNode('11'), GraphNode('9')] + + graph = Graph(*vertices) + graph.add_edge('5', '11') + graph.add_edge('7', '11') + graph.add_edge('7', '8') + graph.add_edge('3', '8') + graph.add_edge('3', '10') + graph.add_edge('11', '2') + graph.add_edge('11', '9') + graph.add_edge('11', '10') + graph.add_edge('8', '9') + if threads is not None: + l = func(graph, algorithm, threads) + else: + l = func(graph, algorithm) + assert all([(l1 in l[0:3]) for l1 in ('3', '5', '7')] + + [(l2 in l[3:5]) for l2 in ('8', '11')] + + [(l3 in l[5:]) for l3 in ('10', '9', '2')]) + + _test_topological_sort(topological_sort, "List", "kahn") + _test_topological_sort(topological_sort_parallel, "List", "kahn", 3) + + +def test_max_flow(): + def _test_max_flow(ds, algorithm): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + + a = GraphNode('a') + b = GraphNode('b') + c = GraphNode('c') + d = GraphNode('d') + e = GraphNode('e') + + G = Graph(a, b, c, d, e) + + G.add_edge('a', 'b', 3) + G.add_edge('a', 'c', 4) + G.add_edge('b', 'c', 2) + G.add_edge('b', 'd', 3) + G.add_edge('c', 'd', 1) + G.add_edge('d', 'e', 6) + + assert max_flow(G, 'a', 'e', algorithm) == 4 + assert max_flow(G, 'a', 'c', algorithm) == 6 + + a = GraphNode('a') + b = GraphNode('b') + c = GraphNode('c') + d = GraphNode('d') + e = GraphNode('e') + f = GraphNode('f') + + G2 = Graph(a, b, c, d, e, f) + + G2.add_edge('a', 'b', 16) + G2.add_edge('a', 'c', 13) + G2.add_edge('b', 'c', 10) + G2.add_edge('b', 'd', 12) + G2.add_edge('c', 'b', 4) + G2.add_edge('c', 'e', 14) + G2.add_edge('d', 'c', 9) + G2.add_edge('d', 'f', 20) + G2.add_edge('e', 'd', 7) + G2.add_edge('e', 'f', 4) + + assert max_flow(G2, 'a', 'f', algorithm) == 23 + assert max_flow(G2, 'a', 'd', algorithm) == 19 + + a = GraphNode('a') + b = GraphNode('b') + c = GraphNode('c') + d = GraphNode('d') + + G3 = Graph(a, b, c, d) + + G3.add_edge('a', 'b', 3) + G3.add_edge('a', 'c', 2) + G3.add_edge('b', 'c', 2) + G3.add_edge('b', 'd', 3) + G3.add_edge('c', 'd', 2) + + assert max_flow(G3, 'a', 'd', algorithm) == 5 + assert max_flow(G3, 'a', 'b', algorithm) == 3 + + + _test_max_flow("List", "edmonds_karp") + _test_max_flow("Matrix", "edmonds_karp") + _test_max_flow("List", "dinic") + _test_max_flow("Matrix", "dinic") diff --git a/tests/test_graph_snapshots.py b/tests/test_graph_snapshots.py new file mode 100644 index 00000000..968fb86d --- /dev/null +++ b/tests/test_graph_snapshots.py @@ -0,0 +1,33 @@ +import unittest +import time +from pydatastructs.graphs import Graph +from pydatastructs.utils import AdjacencyListGraphNode, AdjacencyMatrixGraphNode + +class TestGraphSnapshots(unittest.TestCase): + def test_snapshot_creation(self): + graph = Graph(implementation='adjacency_list') + graph.add_vertex(AdjacencyListGraphNode("A")) + graph.add_vertex(AdjacencyListGraphNode("B")) + graph.add_edge("A", "B", cost=5) + graph.add_snapshot() + + self.assertEqual(len(graph.list_snapshots()), 1) + + def test_snapshot_retrieval(self): + graph = Graph(implementation='adjacency_list') + graph.add_vertex(AdjacencyListGraphNode("A")) + graph.add_vertex(AdjacencyListGraphNode("B")) + graph.add_edge("A", "B", cost=5) + graph.add_snapshot() + snapshot_time = graph.list_snapshots()[0] + retrieved_graph = graph.get_snapshot(snapshot_time) + self.assertEqual(retrieved_graph.is_adjacent("A", "B"), True) + + def test_invalid_snapshot(self): + graph = Graph(implementation='adjacency_list') + with self.assertRaises(ValueError): + graph.get_snapshot(9999999999) + +if __name__ == '__main__': + unittest.main() + diff --git a/timestamped_graph.md b/timestamped_graph.md new file mode 100644 index 00000000..a51a10d9 --- /dev/null +++ b/timestamped_graph.md @@ -0,0 +1,77 @@ +# Timestamped Graph Snapshots + +## Overview + +The timestamped snapshot feature enables graphs to store their historical states automatically using real-time timestamps. This allows users to track the evolution of a graph over time, retrieve past states, and perform time-based analysis. + +## Why This Feature Matters + +- **Graph Evolution Tracking**: Enables users to analyze how a graph changes over time. +- **Anomaly Detection in Secure Networks**: Helps in detecting unusual patterns in cryptographic protocols and secure transactions. +- **Time-Series Graph Analysis**: Supports applications in secure financial transactions and privacy-preserving communications. +- **Cryptographic Security (Future Enhancement)**: Can be extended to sign snapshots using HMAC for integrity verification and encrypted storage. +- **Environment Variable-Based Secret Key**: Adds security by keeping cryptographic secrets out of the source code, reducing exposure to attacks. + +## How It Works + +### **Snapshot Storage & Security Enhancements** + +- When `add_snapshot()` is called, a deep copy of the graph is saved with a unique timestamp. +- Each snapshot is **serialized and cryptographically signed** using an **HMAC signature**. +- The system stores the HMAC signature alongside the snapshot to verify its integrity before retrieval. + +### **Why We Use an Environment Variable for the Secret Key** +To **prevent hardcoding secrets in the source code**, we store the **HMAC secret key in an environment variable** instead of defining it directly in the script. This offers: +1. **Better Security**: Secrets stored in environment variables are not exposed in source code repositories. +2. **Protection Against Attacks**: If an attacker gains access to the codebase, they **cannot retrieve the HMAC key** without environment access. +3. **Separation of Concerns**: The cryptographic key can be changed without modifying the code, making key rotation easier. + +## **Security Best Practices** +To ensure maximum security when handling cryptographic keys, follow these best practices: + +1. **Always set the HMAC key before running the program:** + ```bash + export HMAC_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") + +### **Retrieving Historical States** + +- Users can retrieve past versions of the graph using `get_snapshot(timestamp)`, enabling time-based queries. +- If an invalid timestamp is requested, the system raises a clear error with available timestamps. + +### **Listing Available Snapshots** + +- The `list_snapshots()` method provides a sorted list of all saved timestamps. + +## Usage Example + +```python +from pydatastructs.graphs import Graph + +graph = Graph(implementation='adjacency_list') + +graph.add_edge("A", "B", weight=5) +graph.add_snapshot() # Snapshot stored with real-time timestamp + +graph.add_edge("B", "C", weight=7) +graph.add_snapshot() + +# List stored snapshots +print(graph.list_snapshots()) # Output: [timestamp1, timestamp2] + +# Retrieve a past graph state +old_graph = graph.get_snapshot(graph.list_snapshots()[0]) +``` + +## Future Enhancements + +- **Secure Graph Snapshots for Banking & Finance**: Implement HMAC or cryptographic signing to prevent unauthorized modifications in financial transaction networks. +- **Encrypted Graph Storage for Privacy-Critical Applications**: Apply homomorphic encryption or privacy-preserving encryption to protect sensitive data, such as medical records, customer transactions, or identity graphs. +- **Efficient Storage for Large-Scale Graphs**: Introduce optimized serialization techniques to store historical snapshots with minimal overhead, making it scalable for real-world enterprise applications. +- **Integrity Verification for Regulatory Compliance**: Ensure snapshots cannot be altered without detection by integrating cryptographic hash functions. This is crucial for auditing in banking, supply chain security, and legal record-keeping. +- **Regulatory Compliance and Auditing**: Extend integrity verification using Merkle trees for large-scale verification. Implement tamper-proof logging for financial transactions. +- **Efficient storage for large graphs**: Introduce optimized serialization techniques to minimize storage costs. + +## Conclusion + +This feature lays the groundwork for advanced **cryptographic** graph analytics, allowing users to analyze, secure, and retrieve historical graph states efficiently. As future enhancements are implemented, timestamped snapshots will serve as a core foundation for **secure graph-based computations, privacy-preserving transactions, and cryptographic security in graph structures.** +