Obscura-PQ: Post-Quantum Privacy-Preserving Protocol for the Algorand Blockchain Using Lattice-Based Linkable Ring Signatures
Public blockchains expose the complete transaction graph, and the privacy protocols deployed to obscure it rely almost exclusively on elliptic-curve cryptography, whose discrete-logarithm foundations fall to Shor's algorithm. Because ledgers are immutable, every anonymity set published today under classical assumptions can be retroactively deanonymized by a future quantum adversary. Transitioning to post-quantum alternatives remains challenging, as the strict resource limits of smart contracts prohibit the native on-chain verification of computationally intensive post-quantum proofs. To address these challenges, we present Obscura-PQ, a decentralized, non-custodial post-quantum privacy protocol that verifies natively on the Algorand blockchain. Its core is a setup-free lattice linkable ring signature over the cyclotomic ring
Figure 1. Obscura-PQ protocol end-to-end architecture
-
Post-Quantum Lattice Linkable Ring Signatures: Implements an AOS/Borromean-style linkable ring signature over
$R_q$ ($n = 512$ ,$q = 12289$ ). The signer proves knowledge of one ring opening via a SHA-256 Fiat–Shamir challenge chain over two response-sharing linear relations,$R_{\mathsf{pk}}: t_i^{\mathsf{pk}} = a_1 z_k + a_2 z_s + a_4 z_e - c \cdot C_i$ (opens the commitment) and$R_{\mathsf{sn}}: t_i^{\mathsf{sn}} = a_3 z_k + z_e - c \cdot \mathsf{sn}$ (produces the serial number), sharing the same response triple$(z_k,z_s,z_e)$ and sparse ternary challenge$c$ (weight 48). While borrowing the response-sharing intuition of DualRing, we explicitly use a linear-size AOS chain because its sequential structure allows the heavy lattice arithmetic to be paused, persisted, and resumed on-chain. The true index closes with$z = y + c\cdot\text{secret}$ under norm-based rejection sampling; short-vector norm bounds on every response force the closer to know the coin secret. Verification is performed natively on-chain with hand-coded NTT arithmetic (table-driven negacyclic NTT,contract/ntru_teal.py) and sparse ternary challenge products. Heavy generation (CoinKeyGen, Sign) runs off-chain in a Python local prover (core/obscura_engine.py,core/ntru_ring.py). -
Frequency-Domain Wire Format: All relations are evaluated pointwise in the NTT domain, so the on-chain verifier never needs an inverse transform. Commitments are stored as
$\mathrm{NTT}(C)$ (1024 bytes), the serial number is transported as$\mathrm{NTT}(\mathsf{sn})$ , and the Fiat–Shamir chain hashes$\mathrm{NTT}(t^{\mathsf{pk}}) \parallel \mathrm{NTT}(t^{\mathsf{sn}})$ . The NTT is a bijection, so binding is preserved and$\mathsf{sn}$ remains "revealed in full". -
$O(1)$ State Verification via Box Storage: Eliminates the need for state-heavy global Merkle accumulators. Public commitments (1024-byte packed $\mathrm{NTT}(C)$) and serial-number nullifiers ($\mathsf{sn}^\ast$ ) are stored directly in Algorand Box Storage, enabling$O(1)$ membership and double-spend checks. -
Box-Streamed Proof Transport: The signature, the revealed serial number, the ring ids, and the challenge seed exceed the AVM's 2 KB argument limit, so the client streams them into an app-owned
"p"‖sn*transport box viainit_proof/put_proofapplication calls before withdrawing (payload$\mathrm{NTT}(\mathsf{sn}) \parallel \text{ring keys} \parallel c_0 \parallel r\cdot(z_k \parallel z_s \parallel z_e)$ , totalling$1056 + 3104r$ bytes). The box MBR (plus the MBR of the transient verification boxes) is fronted by the uploader and refunded at settlement; the contract deletes the consumed boxes, so permanent withdrawal state growth is the nullifier box only. -
Member-by-Member On-Chain Verification: The ring is verified sequentially, nine OpUp-pooled
prep_nttphases per ring member ($9r$ phases total): split forward NTTs of$z_{k,i}$ ,$z_{s,i}$ , and$z_{e,i}$ (with norm checks), a split NTT of the member's sparse ternary challenge, and a pointwise two-relation combine that folds the Fiat–Shamir chain update into a running-chain state box"s"‖sn*. The finalwithdrawcall asserts the chain closed back to$c_0$ and settles. Any ring size up to the protocol capMAX_RING = 10verifies on-chain; the cap is set by Algorand's 32,768-byte per-box ceiling, the transport box holds$1088 + 3104r$ bytes (32,128 B at$r = 10$ ), so$r = 11$ could not be allocated. -
Nothing-Up-My-Sleeve Public Parameters: The four public ring elements
$a_j = H_{\text{ring}}(\text{seed} \parallel j)$ , derived from the fixed domain-separation stringOBSCURA-PQ/DOM_PP/v2via SHA-256 in counter mode, have no trusted setup. Their forward NTTs (4096 bytes) are streamed into a deploy-time"pp"parameter box by the creator; their correctness is public and re-derivable. -
Dynamic Opcode Pooling: Bypasses the strict per-transaction execution budget of the AVM. The smart contract dynamically expands its computational headroom by issuing inner application calls to a stateless dummy application in every
prep_nttphase (each degree-512 NTT is split at butterfly length 32 so each half fits under the pooled budget). -
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 at random from a bounded recent pool (the 20 most recent valid decoys).
-
Cryptographically Secure Shuffling: Prevents positional deanonymization by permuting the anonymity set (ring) using a Fisher–Yates shuffle seeded by
crypto.getRandomValues(). -
Self-Healing Transport: A coin's box keys are deterministic in
$\mathsf{sn}^\ast$ , so a withdrawal that failed mid-flow leaves recognizable stranded boxes; the client detects them and issuesclear_proof(which refunds the MBR) before retrying.
- 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, uploading the public-parameter box, and configuring the client environment.
The Obscura-PQ 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, planning fees and box references, streaming the proof into the transport box, and submitting the signed transaction groups. In the current implementation the connected withdrawal wallet also acts as its own relayer: it fronts every network fee and is reimbursed the boundfeeamount from the pool at settlement. -
Local Prover (Flask): The Flask API and lattice engine (
core/). Exposes endpoints (/keygen,/compute_commitment,/generate_proof) that perform the heavy lattice arithmetic (Ring-SIS commitment, serial-number derivation, AOS/Borromean-style ring signing with rejection sampling). It runs onlocalhost:5000; the coin secret$(k,s)$ is generated here and returned to the client, which stores it in browserlocalStorage. -
Smart Contract Layer (Algorand AVM): The PyTeal contracts (
contract/, compiled to TEAL v10). Enforce atomic state transitions, manage Box Storage ($\mathcal{B}_C$ for commitments,$\mathcal{B}_N$ for serial-number nullifiers, plus proof-transport, public-parameter, and transient verification boxes), execute the native per-member two-relation verification (split NTTs, sparse ternary challenge products, challenge-chain folding, norm bounds), and use a dummy application for opcode expansion.
Figure 2. Obscura-PQ implementation architecture
The protocol operates in two primary phases: deposit and withdrawal. The denomination is fixed at 20.101 ALGO, derived in the contract from the worst-case withdrawal cost at the ring cap (
-
Off-Chain: The client requests a fresh CoinKeyGen from the local prover: a unique short secret
$k$ (the spend authority), short randomness$s$ , and the deterministic short blinding$e = H_{\mathrm{short}}(k)$ , forming the rank-one Ring-SIS commitment$C = a_1 k + a_2 s + a_4 e \bmod q$ . The packed coin secret$(k,s)$ is stored in the client's local storage ($e$ is recomputed from$k$ ); on-chain the commitment is stored as the forward NTT$\mathrm{NTT}(C)$ . -
On-Chain: The user submits an atomic transaction group:
TxGroup[Deposit(NTT(C)), Pay(20.101 ALGO)]. The smart contract asserts the payment, asserts that the commitment box$\mathcal{B}_C$ (keyed byc$\parallel$ SHA-256$(\mathrm{NTT}(C))$[0:32]) does not exist, allocates it, writes the 1024-byte packed commitment, and increments the global deposit counter.
Figure 3. Deposit phase sequence diagram
-
Anonymity Set Construction: The client fetches all commitments from the contract's boxes, orders decoys by indexer recency, selects at random from the 20 most recent, injects its own
$C$ , and shuffles the ring$R$ ($r$ up to the on-chain cap of 10). -
Off-Chain Signing: The client sends the packed coin secret
$(k,s)$ , the ring$R$ , and the full context$\mu = (\text{recipient} \parallel \text{relayer} \parallel \text{fee} \parallel \text{appID})$ to the local prover. The prover derives the blinding$e = H_{\mathrm{short}}(k)$ , computes the deterministic serial number$\mathsf{sn} = a_3 k + e$ and nullifier$\mathsf{sn}^\ast = \mathrm{SHA256}(\mathsf{pack}(\mathrm{NTT}(\mathsf{sn})))$ , and generates the AOS/Borromean-style ring signature$\sigma = (c_0, {(z_{k,i}, z_{s,i}, z_{e,i})}_{i=0}^{r-1})$ . The serial number is revealed in full (transported as $\mathrm{NTT}(\mathsf{sn})$). -
Proof Transport: The client uploads the box payload
$\mathrm{NTT}(\mathsf{sn}) \parallel \text{ring keys} \parallel c_0 \parallel r\cdot(z_{k} \parallel z_{s} \parallel z_{e})$ ($1056 + 3104r$ bytes) into the app-owned box"p"‖sn*via aninit_proofgroup (MBR payment followed by the app call) andput_proofchunk calls of at most 1,960 bytes. The payment covers the proof-box MBR plus a pre-payment for the transient verification boxes; all of it is refunded at settlement or onclear_proof. -
On-Chain Verification: The client runs
$9r$ prep_nttphases, for each member$i$ in order: split NTT of$z_{k,i}$ (with norm check), split NTT of$z_{s,i}$ (with norm check), split NTT of$z_{e,i}$ (with norm check), split NTT of the challenge$c_i = \mathrm{H2C}(\text{running chain})$ , and a pointwise two-relation combine that updates the running chain$c \leftarrow H(\mu \parallel \mathsf{sn} \parallel t_i^{\mathsf{pk}} \parallel t_i^{\mathsf{sn}} \parallel (i{+}1)\bmod r)$ in the state box. It then submitsWithdraw(sn*, ring_ids, recipient, relayer, fee). The smart contract:- Binds the nullifier to the transported serial number (
$\mathrm{SHA256}(\mathrm{NTT}(\mathsf{sn})) = \mathsf{sn}^\ast$ ) and asserts$\mathcal{B}_N(\mathsf{sn}^\ast)$ does not exist (double-spend prevention). - Asserts all
$9r$ prep phases completed and that the running Fiat–Shamir chain has returned to$c_0$ (signature validity). - For every ring member, asserts the commitment box
$\mathcal{B}_C$ exists and that its contents hash to the claimed ring id (membership verification). - Allocates
$\mathcal{B}_N(\mathsf{sn}^\ast)$ , deletes the consumed proof/verification boxes, and issues inner payments to the recipient (denom − fee − MBR) and the relayer (fee, read from the state box the signature was bound to).
- Binds the nullifier to the transported serial number (
Figure 4. Withdrawal phase sequence diagram
- Fund the Deployer: Ensure your deployer account has at least ~3.2 Testnet ALGO for app creation (the approval program needs extra pages), contract funding (2.3 ALGO, covering the ~1.640 ALGO public-parameter box MBR and a first-deposit buffer), and fees.
- Bootstrap: Run
python bootstrap_contract.pyin thecontract/directory. This script deploys the dummy and main applications, funds the contract, uploads the"pp"public-parameter box, 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-PQ.git
cd Obscura-PQThat 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-PQ-2.0.1).
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-PQ app to Algorand Testnet, funds the Obscura-PQ contract, uploads the public-parameter box, 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., 767233045.env.bak) is provided in the frontend/ directory. To use the existing public deployment and bypass the bootstrap script, copy this file to .env and use it as-is without any modifications:
cd frontend
cp 767233045.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 20.101 ALGO transfer to the contract plus an application call that records your commitment on-chain. Ensure you have sufficient Testnet ALGO to cover the denomination and 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 “relayer” 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 the fronted transaction fees (roughly 1.83 ALGO per ring member, reimbursed from the pool at settlement). 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 groups through your wallet (one signing prompt covers the transport, the
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
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-PQ 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 detailed, human-readable JSON file (
transactions.json) for deep ledger state inspection and auditing.
Obscura-PQ/
├── contract/ # Smart contract (Algorand AVM)
│ ├── bootstrap_contract.py # Deploy + fund + params upload + .env
│ ├── list_contracts.py
│ ├── ntru_teal.py # NTT tables + sparse challenge + TEAL-mirror arithmetic
│ ├── obscura_contract.py # PyTeal approval/clear programs
│ └── verify_contract.py # Deployed-bytecode vs local-source check
├── core/ # Local prover
│ ├── backend_server.py # Flask API (port 5000)
│ ├── ntru_ring.py # R_q arithmetic + Ring-SIS coin + serial number
│ └── obscura_engine.py # Lattice linkable ring signature
├── 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-PQ's security guarantees are grounded in the lattice assumptions underlying the NIST-standardized Falcon and Dilithium signatures (Ring-SIS, Ring-LWE) and the AVM's execution model. The formal security reductions hold in the classical random-oracle model (ROM):
-
Signer Ambiguity / Anonymity: Reduces to decisional Ring-LWE and an explicit decisional linking assumption in the ROM. Given a ring of size
$r$ , an adversary cannot identify the true signer with probability significantly greater than$1/r$ . After rejection sampling, the true-index response triple is distributed indistinguishably from the forward-sampled decoy responses, and the challenge chain is a random-oracle output independent of the signer index. Because the serial number error$e = H_{\mathrm{short}}(k)$ is deterministic,$\mathsf{sn}$ is formally a self-correlated-noise Ring-LWE instance. - Unforgeability / Theft Resistance: Reduces to the Ring-SIS assumption for rings of honestly generated deposits. The AOS/Borromean-style chain is special-sound: producing a verifying signature without a ring member's opening yields a relaxed opening of the commitment, which breaks Ring-SIS. The on-chain norm bound on every response is what forces the closer to know the coin secret.
-
Double-Spend Soundness & Linkability: Reduces to the Ring-SIS assumption. The opening
$(k,e)$ is unique (Ring-SIS commitment binding) and the serial number$\mathsf{sn} = a_3 k + e$ is a deterministic function of it, so any spend of a coin, honest or malicious, yields the identical$\mathsf{sn}^\ast$ . Because$e$ is bound into$C$ via$a_4 e$ , this holds even though$e = H_{\mathrm{short}}(k)$ is nominally hash-derived. Substituting a fresh$\mathsf{sn}'$ would require opening$C$ to a second short opening (a SIS collision) or forging the$R_{\mathsf{sn}}$ relation. The atomic assertion of$\mathcal{B}_N(\mathsf{sn}^\ast)$ guarantees deterministic double-spend prevention even against a malicious signer. -
Replay & Front-Running Resistance: The full context
$\mu = (\text{recipient} \parallel \text{relayer} \parallel \text{fee} \parallel \text{appID})$ is bound into every Fiat–Shamir challenge. The contract additionally settles to the recipient/relayer/fee stored in the state box that the chain was computed over, so the payout cannot deviate from what the signature authorized; altering any component breaks the closure and the transaction reverts. -
Non-Frameability / Non-Slanderability: To pre-spend or block an honest coin an adversary must produce a verifying signature carrying that coin's
$\mathsf{sn} = a_3 k + e$ ; by special soundness this requires the coin's unique opening$(k,s,e)$ (Ring-SIS). The property reduces directly to commitment binding. - Post-Quantum Security: No primitive is Shor-vulnerable; anonymity and unforgeability rest on Ring-SIS and Ring-LWE. All hashing (parameter derivation, challenges, Fiat–Shamir chain, nullifier digest) uses SHA-256.
-
Fixed Denominations: Transactions are strictly fixed to 20.101 ALGO (sized so the worst-case per-member OpUp fees plus the 0.441 ALGO storage MBR still leave a positive payout at
$r = 10$ ). Supporting arbitrary amounts would require a full Confidential Transaction scheme. -
Anonymity Set Size: Anonymity is ring-bounded, not pool-wide; the
$O(r)$ trade-off intrinsic to an LSAG-style construction. The on-chain cap is$r = 10$ , and it is a hard limit: the transport box holds$1088 + 3104r$ bytes (32,128 B at$r=10$ ), and$r = 11$ would exceed Algorand's 32,768-byte per-box ceiling, so the box cannot be allocated beyond$r = 10$ . Each member also adds nine OpUp-pooled prep phases (~1.83 ALGO) and proof-box MBR. Larger rings would require splitting the transport across multiple boxes. - Storage Cost: Lattice commitments are large (1024 bytes vs. ~32 for an elliptic-curve point); each deposit locks a ~0.425 ALGO commitment-box MBR, deducted from the payout at withdrawal. This is the principal storage cost of the protocol.
-
Opcode Budget: The table-driven NTT is costly; a single degree-512 forward NTT exceeds the ~190k pooled group budget, so every NTT is split into two phases and verification takes
$9r$ sequential groups. Nativefalcon_verifycannot accelerate the ring core (the closure is not a Falcon signature). -
Withdrawal Atomicity: The transport, prep, and withdraw groups are submitted sequentially, not as one atomic unit. A flow that fails midway leaves recoverable boxes on-chain; the client detects and clears them (
clear_proof, with full MBR refund) before retrying. -
Liveness Under Permissionless Phase Submission:
prep_nttimposes no sender check, so any account can advance a withdrawal pipeline. This can never redirect or steal funds, settlement is bound to the context folded into the challenge chain, and any mismatch makes closure fail, but a third party that races the first phase or squats the transport box can force a targeted withdrawal to abort and be restarted. The design guarantees fund safety, not censorship-resistant liveness.
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, init_params, put_params, init_proof, put_proof, clear_proof, prep_ntt, withdraw, get_count, or opup).
- Group: 2-transaction atomic group: (1) this application call, (2) payment of 20,101,000 microAlgos (the fixed denomination) to the contract.
-
Application Call Args:
-
args[0]:deposit -
args[1]: 1024-byte commitment$\mathrm{NTT}(C)$ ,$C = a_1 k + a_2 s + a_4 e \bmod q$ (512 big-endian uint16 coefficients).
-
-
State Transition: The contract verifies the payment, asserts uniqueness, and allocates a new box
$\mathcal{B}_C$ keyed byc$\parallel$ SHA-256$(\mathrm{NTT}(C))$[0:32]. It also increments the global deposit counter.
- Stream the nothing-up-my-sleeve public parameters
$a_1,a_2,a_3,a_4$ (as forward NTTs, 4096 bytes total) into the"pp"box.init_params(args[1]= total length, must equal 4096) creates the box; its ~1.640 ALGO MBR must be pre-funded, sincebox_createraises the app minimum balance during the call itself;put_params(args[1]= offset,args[2]= chunk) writes chunks. Provisioned automatically bybootstrap_contract.py.
-
Group: (1) Payment covering the transport-box MBR (
$2500 + 400 \times (33 + 32 + \text{payload})$ microAlgos) plus the 482,600 microAlgo transient verification-box MBR, followed by (2) this application call (the payment must precede the call so it credits beforebox_createraises the app minimum balance), plus optional aux calls carrying extra box references. -
Application Call Args:
args[0]:init_proof;args[1]: 32-byte$\mathsf{sn}^\ast$ ;args[2]: payload length (uint64), must equal$1024 + 32 + r\cdot(32 + 3072) = 1056 + 3104r$ for some$r \in [1, 10]$ . -
State Transition: Creates box
"p"‖sn*sized header (32 bytes, recording the funder) + payload. All fronted MBR is refunded atwithdraworclear_proof.
-
Application Call Args:
args[0]:put_proof;args[1]:$\mathsf{sn}^\ast$ ;args[2]: byte offset (uint64);args[3]: chunk bytes. Only the funder recorded in the box header may write, and only before the firstprep_nttphase freezes the box (rewriting a response after its norm check would let non-short vectors slip past verification).
-
Application Call Args:
args[0]:clear_proof;args[1]:$\mathsf{sn}^\ast$ . Abandons an upload (funder-only): deletes the transport box and any transient"v"/"w"/"s"boxes, and refunds the full fronted MBR to the funder.
-
Application Call Args:
args[0]:prep_ntt;args[1]:$\mathsf{sn}^\ast$ ;args[2]: phase (uint64)$\in [0, 9r-1]$ , where member$= \lfloor\text{phase}/9\rfloor$ and substep$= \text{phase} \bmod 9$ ; on phase 0 only,args[3..5]= recipient (32 B), relayer (32 B), fee (8 B), these seed the running-chain state box and are what settlement later pays to.Txn.applications[1]is the dummy OpUp app. -
Substeps (per member
$i$ ): 0/1 = split NTT of$z_{k,i}$ (0 also checks its norm); 2/3 = split NTT of$z_{s,i}$ (2 checks its norm); 4/5 = split NTT of$z_{e,i}$ (4 checks its norm); 6/7 = split NTT of the sparse ternary challenge$c_i = \mathrm{H2C}(\text{running chain})$ into the"w"‖sn*box; 8 = pointwise two-relation combine (reading the"pp"params box and member$i$ 's commitment box, validating its ring id) and fold of the chain update$H(\mu \parallel \mathsf{sn} \parallel t_i^{\mathsf{pk}} \parallel t_i^{\mathsf{sn}} \parallel (i{+}1)\bmod r)$ into the"s"‖sn*state box. The marker box"v"‖sn*stores the next expected phase and enforces strict in-order execution (the chain is sequential across members). - Budget: each phase issues inner OpUp calls to the dummy app (250 for NTT first halves, 175 for second halves, 128 for the combine, 1828 per member, ~1.83 ALGO of inner fees at the minimum fee).
-
Group: this application call plus aux
opupcalls carrying the box references for the proof, transient, nullifier, and ring commitment boxes. -
Application Call Args:
-
args[0]:withdraw -
args[1]: 32-byte serial-number digest / nullifier$\mathsf{sn}^\ast$ . -
args[2]: ring ids,$r \times 32$ bytes, the SHA-256$(\mathrm{NTT}(C_i))$[0:32] commitment identifiers (must equal the copy stored in the proof box). -
args[3..5]: recipient, relayer, fee (routing requires them; settlement itself pays to the values stored in the"s"state box, which the signature was bound to).
-
-
Verification: asserts the proof-box layout matches
$r$ ; binds the nullifier to the transported serial number ($\mathrm{SHA256}(\mathrm{NTT}(\mathsf{sn})) = \mathsf{sn}^\ast$ ); asserts all$9r$ prep phases completed (marker$= 9r$ ) and the running chain equals$c_0$ (Fiat–Shamir closure); asserts$\mathcal{B}_N(\mathsf{sn}^\ast)$ absent; re-asserts every ring member's commitment box exists and hashes to its ring id; asserts$\text{fee} + 441{,}000 < \text{DENOM}$ . -
State Transition: Deletes the
"v"/"w"/"s"/"p"boxes, records$\mathcal{B}_N(\mathsf{sn}^\ast)$ keyed byn$\parallel$ $\mathsf{sn}^\ast$ , and issues three inner payments:DENOM − fee − 441,000to the recipient,feeand the proof-box MBR + transient MBR refund to the (self)-relayer.
Helper Methods: get_count logs the global deposit counter; opup is an approve-only no-op used both on the dummy app (for inner OpUp budget) and on the main app (as an aux box-reference carrier).
| Method | Path | Body (JSON) | Returns |
|---|---|---|---|
GET |
/api/monitor |
— | Contract balance, commitment/nullifier box counts, unspent count, recommended/effective ring size. |
POST |
/keygen |
— |
secret (packed coin secret commitment (2048-hex packed $\mathrm{NTT}(C)$) |
POST |
/compute_commitment |
secret (packed |
commitment (2048-hex packed $\mathrm{NTT}(C)$) |
POST |
/generate_proof |
secret, recipient, relayer, fee, commitments (the ring commitment
|
nullifier sn (packed proof |
The frontend calls /keygen during the deposit phase and /api/monitor + /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-PQ 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{azimi2026obscurapq,
title={Obscura-PQ: Post-Quantum Privacy-Preserving Protocol for the Algorand Blockchain Using Lattice-Based Linkable Ring Signatures},
author={Azimi, Navid},
journal={arXiv preprint arXiv:2608.22645},
year={2026}
}
Obscura-PQ is provided under the GNU General Public License v3.0 (GPL-3.0). Under this license 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
- Azimi, Navid. "Obscura: Privacy-Preserving Protocol for the Algorand Blockchain Using LSAG Ring Signatures." arXiv preprint arXiv:2605.02077 (2026).
- Esgin, Muhammed F., et al. "MatRiCT: efficient, scalable and post-quantum blockchain confidential transactions protocol." Proceedings of the 2019 ACM SIGSAC Conference on Computer and Communications Security. 2019.
- Liu, Joseph K., Victor K. Wei, and Duncan S. Wong. "Linkable spontaneous anonymous group signature for ad hoc groups." Australasian conference on information security and privacy. Berlin, Heidelberg: Springer Berlin Heidelberg, 2004.
- Maxwell, Gregory, and Andrew Poelstra. "Borromean ring signatures." Accessed: Jun 8 (2015): 2019.
- Yuen, Tsz Hon, et al. "DualRing: Generic construction of ring signatures with efficient instantiations." Annual international cryptology conference. Cham: Springer International Publishing, 2021.
- Baum, Carsten, et al. "More efficient commitments from structured lattice assumptions." International Conference on Security and Cryptography for Networks. Cham: Springer International Publishing, 2018.
- Lyubashevsky, Vadim. "Lattice signatures without trapdoors." Annual international conference on the theory and applications of cryptographic techniques. Berlin, Heidelberg: Springer Berlin Heidelberg, 2012.
- Lyubashevsky, Vadim, Chris Peikert, and Oded Regev. "On ideal lattices and learning with errors over rings." Journal of the ACM (JACM) 60.6 (2013): 1-35.
- Abe, Masayuki, Miyako Ohkubo, and Koutarou Suzuki. "1-out-of-n signatures from a variety of keys." International conference on the theory and application of cryptology and information security. Berlin, Heidelberg: Springer Berlin Heidelberg, 2002.
- Fouque, Pierre-Alain, et al. "Falcon: Fast-Fourier lattice-based compact signatures over NTRU." Submission to the NIST’s post-quantum cryptography standardization process 36.5 (2018): 1-75.
- Fiat, Amos, and Adi Shamir. "How to prove yourself: Practical solutions to identification and signature problems." Conference on the theory and application of cryptographic techniques. Berlin, Heidelberg: Springer Berlin Heidelberg, 1986.
- Pertsev, Alexey, Roman Semenov, and Roman 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)
.png)
.png)
.png)
.png)
.png)