While public blockchains provide transparent and auditable transaction histories, they inherently compromise user privacy. Existing privacy-enhancing protocols, such as those deployed on Ethereum, typically rely on succinct zero-knowledge proofs (zk-SNARKs) to obscure the transaction graph. However, implementing comparable cryptographic guarantees on high-throughput blockchains like Algorand is challenging due to strict per-call execution budgets and the state contention introduced by global Merkle accumulators. This work presents Obscura, a decentralized, non-custodial privacy protocol tailored for constrained smart contract environments. Obscura achieves transaction anonymity using Linkable Spontaneous Anonymous Group (LSAG) signatures over the BN254 elliptic curve, verified entirely on-chain. To overcome limitations of the Algorand Virtual Machine (AVM), we introduce a novel state model that leverages Algorand's Box Storage for
Figure 1. Obscura protocol end-to-end architecture
-
Linkable Ring Signatures: Implements Linkable Spontaneous Anonymous Group signatures (LSAG) over the BN254 elliptic curve. Verification is performed natively on-chain using AVM elliptic-curve opcodes (
EcAdd,EcScalarMul). The heavy cryptographic generation KeyGen and Sign is executed off-chain via a Python-based local prover (core/obscura_engine.py). -
$O(1)$ State Verification via Box Storage: Eliminates the need for state-heavy global Merkle accumulators. Public commitments ($P = xG$ ) and key images/nullifiers ($I = xH$ ) are stored directly in Algorand Box Storage, enabling$O(1)$ membership and double-spend checks. -
Dynamic Opcode Pooling: Bypasses the strict per-transaction execution budget of the AVM. The smart contract dynamically expands its computational headroom by issuing
$20n$ inner application calls to a stateless dummy application, allowing$O(n)$ signature verification to complete in a single epoch. -
Privacy-Hardened Client Architecture (
frontend/src/components/Obscura.tsx):- Recency-Biased Decoy Selection: Mitigates temporal intersection attacks by querying the blockchain indexer for recent deposits and drawing decoys from a bounded active window.
-
Cryptographically Secure Shuffling: Prevents positional deanonymization by permuting the anonymity set (ring) using a Fisher-Yates shuffle seeded by
crypto.getRandomValues().
- Analytical Tooling: Includes Obscura Lens (a Dash/Cytoscape transaction graph explorer) and Obscura Inspector (an indexer-backed transaction classifier) for evaluating ledger state and transaction topology.
-
Automated Bootstrapping:
contract/bootstrap_contract.pystreamlines deployment by provisioning the dummy and main applications, funding the contract, and configuring the client environment.
The Obscura ecosystem is partitioned into three distinct domains to optimize execution:
-
Client Application: The React/TypeScript frontend (
frontend/) and Pera Wallet. Responsible for querying the blockchain indexer, selecting decoys, constructing the anonymity set, and submitting signed transaction groups to the network. -
Local Prover Enclave: The Flask API and LSAG engine (
core/). Exposes stateless endpoints (/generate_proof,/compute_commitment) that perform the heavy BN254 curve arithmetic. -
Smart Contract Layer (Algorand AVM): The PyTeal contracts (
contract/). Enforces atomic state transitions, manages Box Storage ($\mathcal{B}_C$ for commitments,$\mathcal{B}_N$ for nullifiers), executes the native LSAG verification loop, and utilizes a dummy application for opcode expansion.
Figure 2. Obscura implementation architecture
The protocol operates in two primary phases: deposit and withdrawal.
-
Off-Chain: The client application samples a fresh, uniformly random secret scalar
$x$ and requests the public commitment$P = xG$ from the local prover. The secret$x$ is stored securely in local storage. -
On-Chain: The user submits an atomic transaction group:
TxGroup[Deposit(P), Pay(1 ALGO)]. The smart contract asserts that the commitment box$\mathcal{B}_C(P)$ does not exist, allocates it, writes$P$ , and increments the global deposit counter.
Figure 3. Deposit phase sequence diagram
-
Anonymity Set Construction: The client fetches recent public commitments from the indexer, selects decoys, injects its own
$P$ , and shuffles the ring$R$ . -
Off-Chain Signing: The client sends the secret
$x$ , the ring$R$ , and the recipient address$m$ to the local prover. The prover computes the key image$I = xH$ and generates the LSAG signature$\sigma$ . -
On-Chain Verification: The user submits a single application call:
Withdraw(I, \sigma, m, R). The smart contract:- Issues
$20n$ inneropupcalls to expand the opcode budget. - Asserts the nullifier box
$\mathcal{B}_N(I)$ does not exist (double-spend prevention). - Asserts all commitment boxes
$\mathcal{B}_C(P_i)$ for$P_i \in R$ exist (membership verification). - Executes the native LSAG verification algorithm.
- Allocates
$\mathcal{B}_N(I)$ to mark the deposit as spent and issues an inner payment to$m$ .
- Issues
Figure 4. Withdrawal phase sequence diagram
- Fund the Deployer: Ensure your deployer account has sufficient Testnet ALGO for app creation and box storage minimum balance requirements (MBR).
- Bootstrap: Run
python bootstrap_contract.pyin thecontract/directory. This script deploys the dummy and main applications, funds the contract, and configuresfrontend/.env. - Launch Stack: Start the Flask prover (
python backend_server.py) and the React client (npm start).
This project is experimental software designed for research and evaluation on Algorand Testnet. It should not be deployed to MainNet without:
- An independent security audit and formal verification.
- Comprehensive economic testing of fee structures and MBR dynamics.
- Legal and regulatory compliance reviews for your jurisdiction.
git clone https://github.com/n-azimi/Obscura.git
cd ObscuraThat creates the project root (the directory that contains requirements.txt, contract/, core/, and frontend/). If you use a source zip instead of git clone, cd into the unpacked folder, its name may differ (e.g. Obscura-3.6.0).
The following steps assume your shell is in that project root.
Ensure you have Node.js (v22.16.0 or compatible) and npm (v10.9.2 or compatible) installed. You can download them from the official Node.js website.
Use the same conda env anywhere you run Python: contract/, core/, tools/, and the launcher scripts (launcher_unix.py / launcher_win.py) if you use them.
First time only, from the project root:
conda create -n obscura python=3.12.12
conda activate obscuraLater sessions, open a shell, then:
conda activate obscura(If you do not use Conda, create a venv with Python 3.12 and activate it instead, then use pip as in the next step.)
From the project root, with the python environment activated:
python -m pip install -r requirements.txtFrontend (Node.js), install the React appβs packages (from the project root):
cd frontend
npm install(If the install fails on peer dependencies, try npm install --force.) Configure frontend/.env after the contract is deployed (see below). A production-like build is npm run build; local development is npm start.
After Β§3β4 (conda env, pip install -r requirements.txt, npm install in frontend/), you can open separate terminals for the stack from the repository root instead of only using manual terminals in Β§5β6 below.
Commands:
# Linux / macOS
python3 launcher_unix.py# Windows (PowerShell or cmd)
python launcher_win.py| Platform | Script | What it does |
|---|---|---|
| Linux / macOS | launcher_unix.py |
1) CONTRACT - bootstrap_contract.py then list_contracts.py (the script waits for this to finish), 2) BACKEND - python3 backend_server.py in core/, 3) FRONTEND - npm run build && npm start in frontend/. Uses gnome-terminal (typical on Ubuntu) or AppleScript + Terminal on macOS. |
| Windows | launcher_win.py |
1) CONTRACT - bootstrap_contract.py then list_contracts.py (waits for completion via a flag file), 2) BACKEND - python backend_server.py in core/, 3) FRONTEND - npm run build; npm start in frontend/. Uses separate PowerShell consoles with the obscura Conda environment automatically activated. |
Requirements: the obscura conda env, Node/npm on PATH for the frontend window, and (for the Unix script) a compatible terminal app. launcher_unix.py uses python3 for Python commands.
From Β§5 onward, the steps below assume you are running each command by hand in your own terminals (e.g. Β§5.1 contract, then Β§5.2 backend, then Β§6 frontend), instead of only using the optional launchers above. Follow that order the first time you set up, or use the same commands whenever you need finer control.
Note: You need a valid frontend/.env (from multi-terminal launch, bootstrap or a manual config) before the app can connect to the contract and the proof server.
Each tool is a separate Python process (use a different terminal for each if you run both).
Obscura Lens (Dash graph UI, default http://127.0.0.1:8050):
cd tools
python obscura_lens.pyObscura Inspector (Indexer fetch + JSON classification, CLI):
cd tools
python obscura_inspector.pyActivate your env (conda activate obscura), stay at the project root, then use terminal 1 for deployment.
bootstrap_contract.py (in contract/) deploys the dummy (opcode-budget helper) app and the main Obscura app to Algorand Testnet, funds the Obscura contract, and writes frontend/.env (deployer address, app ids, contract address, node URLs, etc.). Follow the prompts, if you generate a new deployer account, you will fund it via QR code.
# Navigate to contract directory
cd contract
# Deploy and fund the obscura smart contract
python bootstrap_contract.pyUsing a Third-Party Deployment: You can bypass the bootstrap script and manually populate
frontend/.envwith an existing deployment's configuration. However, you must verify the integrity of any third-party contract before use. Runpython verify_contract.pyin thecontract/directory to assert that the deployed on-chain bytecode exactly matches the local PyTeal source code (obscura_contract.py).
A working testnet environment configuration (e.g., 762021711.env.bak) is provided in the frontend/ directory. If you want to use the existing public deployment and bypass the bootstrap script, you can simply copy this file to .env and use it as-is without any modifications:
cd frontend
cp 762021711.env.bak .envOptional β list apps created by the deployer (list_contracts.py). With no arguments it reads REACT_APP_DEPLOYER_ADDRESS from frontend/.env (after bootstrap, that file exists). You can also pass the deployer address explicitly:
cd contract
python list_contracts.pycd contract
python list_contracts.py <DEPLOYER_58_CHAR_ADDRESS>The Flask server generates withdrawal proofs and serves /api/monitor pool stats to the frontend.
Terminal 2:
# Navigate to core directory
cd core
python backend_server.pyThe server will run on http://localhost:5000.
Terminal 3:
# Navigate to frontend directory
cd frontend
npm startThe web app will open at http://localhost:3000.
Connect your Pera Wallet (Testnet) and approve the grouped transaction: a 1 ALGO transfer to the contract plus an application call that records your commitment on-chain. Ensure you have sufficient Testnet ALGO to cover fees. Once confirmed, save the values displayed in the UI, your secret and your commitment. The contract does not store your secret.
Your secret and commitment are the only means to construct a valid withdrawal later. Back them up securely and offline (as you would a seed phrase). If you lose them, the deposit becomes permanently inaccessible. If someone else obtains them, they can spend your funds.
For withdrawals, connect a different wallet than the one used for the deposit (a βwithdrawalβ or βhandlerβ account). While the ring structure makes individual spends cryptographically ambiguous, reusing the same account across deposit and withdrawal introduces linkability. Using separate accounts, and allowing time for more deposits and withdrawals to mix into the pool, improves practical anonymity within the ring.
Ensure your withdrawal wallet has enough ALGO for transaction fees. Enter your secret, commitment, and the recipient address. The system selects decoy commitments, the core service generates the ring proof, and you sign and submit the transaction through your wallet. The blockchain records a nullifier to prevent double-spending, without revealing which ring member you were.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
An interactive Dash application (tools/obscura_lens.py) for visualizing Algorand blockchain (Testnet) activity. It fetches transaction data from the Algorand Indexer and renders a dynamic, searchable Cytoscape graph of accounts, contracts, applications, and their interactions, providing a clear view of transaction topology and flow.
- Dynamic Graph Layout: Automatically structures nodes into inbound, outbound, and central contract columns to clearly visualize the flow of funds and application calls.
- Transaction Aggregation: Groups multiple interactions between addresses, displaying aggregated transaction counts and ALGO volumes on the edges.
- Inner Transaction Tracing: Recursively unpacks and visualizes inner transactions (e.g., inner payments from the contract to a withdrawal recipient).
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
A transaction classification tool (tools/obscura_inspector.py) that fetches and normalizes full transaction histories for the Obscura smart contract using the Algorand Indexer API.
- Automated Classification: Categorizes transactions into
Deposits(grouped payment + app call),Withdrawals(app calls with the 'withdraw' argument),Fund Contract(direct ALGO payments), andOther. - Deep Data Normalization: Recursively extracts and normalizes all relevant fields (Base64, Hex, ASCII) from application arguments, box references, global state deltas, and inner transactions.
- Structured Output: Outputs a highly detailed, human-readable JSON file (
transactions.json) for deep ledger state inspection and auditing.
Obscura/
βββ contract/ # Smart contract (Algorand AVM)
β βββ bootstrap_contract.py
β βββ list_contracts.py
β βββ obscura_contract.py
β βββ verify_contract.py
βββ core/ # Local prover enclave
β βββ backend_server.py
β βββ bn_254.py
β βββ obscura_engine.py
βββ frontend/ # Client application (React/TypeScript)
β βββ public/
β βββ src/
β βββ .env
β βββ package.json
β βββ tsconfig.json
βββ img/ # Figures and images
βββ tools/ # Analytical tooling
β βββ obscura_inspector.py
β βββ obscura_lens.py
βββ launcher_unix.py # Multi-terminal launch script for Linux/macOS
βββ launcher_win.py # Multi-terminal launch script for Windows
βββ README.md # Documentation
βββ requirements.txt # Python dependenciesObscura's security guarantees are grounded in formal cryptographic properties and the AVM's execution model:
-
Signer Ambiguity: Given a ring of size
$n$ , an adversary cannot identify the true signer with probability significantly greater than$1/n$ . The commitment$P$ and key image$I$ use independent generators ($G$ and$H$ ), rendering them unconditionally unlinkable without knowledge of$x$ . - Unforgeability (EUF-CMA): Assuming the hardness of the Elliptic Curve Discrete Logarithm Problem (ECDLP) on BN254, producing a valid signature without knowing the secret scalar corresponding to a ring member is computationally infeasible.
-
Double-Spend Resistance: Linkability ensures that any two signatures generated from the same secret
$x$ yield the identical key image$I$ . The atomic assertion of$\mathcal{B}_N(I)$ guarantees deterministic double-spend prevention. -
Replay Resistance: The recipient address
$m$ is bound into the Fiat-Shamir challenge chain. Altering the payout destination breaks the signature closure, causing the transaction to revert.
- Fixed Denominations: In the current testnet implementation, transactions are strictly fixed to 1 ALGO to ensure commitment indistinguishability. Supporting arbitrary amounts would require transitioning to a full Confidential Transaction scheme, which currently exceeds AVM opcode limits.
-
Anonymity Set Size: Due to the
$O(n)$ scaling of LSAG verification and AVM execution limits, the ring size is currently capped at$n=5$ . -
Post-Quantum Vulnerability: Like all classical elliptic-curve protocols, Obscura is vulnerable to Shor's algorithm. A quantum adversary could recover
$x$ from$P = xG$ to deanonymize or forge a withdrawal for that specific deposit.
Disclaimer: This project is experimental software, aimed at research (e.g. on Algorand Testnet), and it has not been through a formal product audit. Do not deploy to mainnet or use with real funds without your own review, any audits you require, and compliance with the laws and regulations that apply to you. You are responsible for how you use the software and for any loss of funds, privacy, or data.
The integration path bridges the React frontend, the local Flask prover, and the Algorand blockchain. The specifications below map directly to contract/obscura_contract.py and core/backend_server.py.
Methods are routed via application-args[0] as a UTF-8 string (deposit, withdraw, get_count, or opup on the dummy application).
- Group: 2-transaction atomic group: (1) Payment of 1,000,000 microAlgos to the contract, (2) Application call.
-
Application Call Args:
-
args[0]:deposit -
args[1]: 64-byte public commitment$P = xG$ on BN254.
-
-
State Transition: The contract verifies the 1 ALGO payment, asserts uniqueness, and allocates a new box
$\mathcal{B}_C(P)$ keyed byc$\parallel$ P[0:32]. It also increments the global deposit counter.
- Group: Single application call. The outer transaction fee must cover the pooled execution cost.
-
Application Call Args:
-
args[0]:withdraw -
args[1]: 64-byte key image (nullifier)$I = xH$ on BN254. -
args[2]: Packed LSAG signature$\sigma$ ($96n + 33$ bytes). Layout:- 1 byte: ring size
$n$ -
$n \times 64$ bytes: ring members (public commitments$P_i \in R$ ) - 32 bytes: challenge
$c_0$ -
$n \times 32$ bytes: responses$s_i$
- 1 byte: ring size
-
args[3]: 32-byte recipient public key$m$ .
-
-
Dependencies:
- Requires
$20n$ inneropupcalls to the dummy app (passed inTxn.applications[1]) to dynamically expand the opcode budget. - Requires box references (
applbox array) for$I$ and all$P_i \in R$ to load ring members from on-chain state.
- Requires
-
State Transition: On successful verification, records the nullifier in a box
$\mathcal{B}_N(I)$ keyed byn$\parallel$ I[0:32]and issues an inner payment to$m$ (deducting network fees and storage MBR).
Optional Read-Only / Helper Methods: get_count returns the global deposit counter; opup on the dummy app is used strictly to pad the opcode budget.
| Method | Path | Body (JSON) | Returns |
|---|---|---|---|
GET |
/api/monitor |
β | Contract balance, box counts, deposit counts, and effective ring size (capped at 5). |
POST |
/compute_commitment |
secret (hex scalar |
commitment (128-hex point |
POST |
/generate_proof |
secret recipient commitments commitment |
nullifier proof |
The frontend calls /compute_commitment during the deposit phase and /generate_proof during the withdrawal phase (see frontend/src/components/Obscura.tsx).
The academic research paper detailing the cryptographic foundations, transaction architectures, and security features of Obscura is available at the links below:
If this repository or the paper has contributed to your research, please acknowledge our work by citing it:
@article{azimi2026obscura,
title={Obscura: Privacy-Preserving Protocol for the Algorand Blockchain Using LSAG Ring Signatures},
author={Azimi, Navid},
journal={arXiv preprint arXiv:2605.02077},
year={2026}
}
@misc{cryptoeprint:2026/917,
author = {Navid Azimi},
title = {Obscura: Privacy-Preserving Protocol for the Algorand Blockchain Using {LSAG} Ring Signatures},
howpublished = {Cryptology {ePrint} Archive, Paper 2026/917},
year = {2026},
doi = {10.48550/arXiv.2605.02077},
url = {https://eprint.iacr.org/2026/917}
}
Obscura is provided under the GNU General Public License v3.0 (GPL-3.0). In short: you may run, study, share, and modify the code, but if you distribute a modified version you must do so under GPL-3.0 as well, including source for your changes, and you must keep license and copyright notices intact.
Algorand Ecosystem
- Algorand Developer Portal
- Smart Contract Overview
- Algorand Virtual Machine
- Box Storage Documentation
- PyTeal
- Pera Wallet
- py-algorand-sdk
- js-algorand-sdk
Cryptographic Foundations
- Liu et al., Linkable Spontaneous Anonymous Group Signatures for Ad Hoc Groups (ACISP 2004)
- Rivest, Shamir, Tauman, How to Leak a Secret (ASIACRYPT 2001)
- Fiat, Shamir, How to Prove Yourself: Practical Solutions to Identification and Signature Problems (CRYPTO '86)
- Pertsev, Semenov, Storm, Tornado cash privacy solution version 1.4 (Tornado cash privacy solution version 1(6) 2019)

.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)
.png)