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.**
+