Working conventions for the QPrize project. Both humans and Claude instances should follow these when contributing.
- Every attempt lives in
attempts/attempt_NNN[letter]_YYYY-MM-DD_HHMM.pyNNN= zero-padded attempt number; letter suffix (B, C, ...) = refinement of same ideaYYYY-MM-DD_HHMM= actual creation date and time (usedate +%Y-%m-%d_%H%M)- Example:
attempt_004_2026-03-29_1507.py
- Every attempt file must:
- Have a header comment explaining what changed and why (vs previous attempt)
- Export
solve(num_bits: int) -> intthat synthesizes + executes + returns recovered d - Call
play_ending_sound()when finished - Use
timed()context managers around slow steps - After synthesis, save two files to
attempts/results/(create dir if needed):attempt_NNN_<bits>bit.qprog— raw Classiq circuit viaqprog.save(path)attempt_NNN_<bits>bit_meta.json— human-readable summary (see format below)
- After verifying an attempt works, append it to
attempts/registry.py(never edit past entries) - Add a row to
attempts/RESULTS.mdfor each synthesis run with: attempt name, bits, qubit width, circuit depth, CX count, success, and legitimacy - Never delete or modify old attempt files
Append one row per synthesis run (not per attempt file). Columns:
| Attempt | Bits | Width | Depth | CX | Success | Legitimacy |
|---|---|---|---|---|---|---|
| attempt_004_2026-03-29_1507 | 4 | 11 | ... | 716 | ✅ | group enum |
- Width = number of qubits (from
GeneratedCircuit.data.width) - Depth = circuit depth (from
GeneratedCircuit.data.depth) - CX = two-qubit gate count (from
GeneratedCircuit.data.transpiled_circuit.count_ops["cx"]or equivalent) - Success = ✅ if
solve()returned the correctd, ❌ otherwise - Legitimacy = shortcuts that would not work at cryptographic scale. Use these tags (comma-separate multiples):
d in oracle—dappears directly in circuit constants; circuit is a tautology, not ECDLPgroup enum— oracle constants derived by enumerating all n EC group elements; O(n) work, infeasible for cryptographic nlookup inverse— modular inverse via lookup table of size p; O(p) memory, infeasible for cryptographic p- ✅
none— no known shortcuts; would scale to cryptographic sizes
The _meta.json file must be a plain JSON object with at minimum:
{
"attempt": "attempt_008_2026-03-29_1859",
"bits": 4,
"qubits": 28,
"depth": 1050,
"cx": 716,
"gate_counts": {"cx": 716, "u3": 120, ...},
"decoded_d": 6,
"expected_d": 6,
"success": true
}Write this file immediately after synthesis (before execution) for qubits/depth/cx/gate_counts,
then update decoded_d/success after execution. Or write once at the end with all fields.
PLAN.md is the strategic document. Keep it up to date:
- When a task is completed, move it from "TODO" to "done" or update the attempt history table
- When a new idea is discovered (from papers, experiments, failures), add it under the appropriate priority section
- Note qubit/CX counts and status (Simulator / Hardware / Failed) for each attempt
- The multi-agent coordination note at the top should remain; multiple Claude instances read it
All session activity is documented in worklog/. Each session creates one file:
- Filename:
worklog/YYYY-MM-DD_HHMM_[who].md(e.g.worklog/2026-03-29_1200_dor.md) - Format: bullet list of what was done, ideas explored, decisions made, failed attempts, and open questions
- A new session should read the latest worklog file first to pick up where the previous session left off
- Also append a one-liner summary to
log.txtin the root (append-only, never edit past entries)
Template for a worklog entry:
# Session YYYY-MM-DD HH:MM — [who]
## Done
- ...
## Failed / Abandoned
- ...
## Open questions / Next steps
- ...consts.py— curve parameters only, no circuit codeutils.py— shared utilities (timed,play_ending_sound)ecc.py— classical EC arithmetic helpers (for testing/reference)attempts/— attempt files + registryworklog/— session logsPLAN.md— strategic plan and attempt historyGUIDELINE.md— this fileresearch.md— paper notes and optimization ideastest_*.py— pytest test filescode_review.md— latest agent code review output
Do not clutter the root with scratch files, temp outputs, or one-off scripts.
# attempt_NNN — YYYY-MM-DD HH:MM
# CHANGE: ...
# WHY: ...
# (optional: RESULT: ...)
import ...
from consts import PARAMS
from utils import timed, play_ending_sound
def solve(num_bits: int) -> int:
...
play_ending_sound()
return d
if __name__ == "__main__":
# Quick smoke test
result = solve(4)
print(result)- The Classiq entry-point
@qfuncmust be namedmain - Define
maininsidesolve()so it captures local constants via closure - Use
@qpermfor reversible helpers (no mid-circuit measurement),@qfuncfor the top-level - Lambda capture in loops: use default argument
lambda x=val: f(x)not barelambda: f(val)wherevalchanges in the loop allocate(size, signed, fraction_digits, reg)— usefraction_digits=sizefor fractional QNum (values in [0,1)), usefraction_digits=0for integer QNumfree(reg)is required for any register allocated inside a@qpermbut not returned
The core bottleneck is the group addition oracle, not the Hadamard or QFT layers. To keep
focus there and avoid conflating unrelated parts of the circuit, split main into three
clearly named helpers and have main call them in order:
@qfunc
def prepare_superposition(x1: Output[QArray[QBit]], x2: Output[QArray[QBit]]) -> None:
"""Allocate and Hadamard the two QPE registers."""
allocate(var_len, x1)
allocate(var_len, x2)
hadamard_transform(x1)
hadamard_transform(x2)
@qfunc # (or @qperm if reversible)
def group_add_oracle(
x1: QArray[QBit],
x2: QArray[QBit],
ecp: Output[...],
) -> None:
"""The group addition oracle — ecp = P0 + x1*G - x2*Q. This is the hard part."""
...
@qfunc
def extract_phase(x1: QArray[QBit], x2: QArray[QBit]) -> None:
"""Inverse QFT to extract the period."""
invert(lambda: qft(x1))
invert(lambda: qft(x2))
@qfunc
def main(x1: Output[QArray[QBit]], x2: Output[QArray[QBit]], ecp: Output[...]) -> None:
prepare_superposition(x1, x2)
group_add_oracle(x1, x2, ecp)
extract_phase(x1, x2)Why: Every attempt optimizes group_add_oracle. Keeping it isolated makes it easy to
swap implementations, compare CX counts, and avoids accidentally touching the Hadamard or
QFT layers when experimenting with the oracle.
The oracle is: ecp = P0 + x1*G - x2*Q
Peak condition after inverse QFT: m1*d + m2 ≡ 0 (mod n)
Standard post-processing (attempt_004+):
x1_r = round(x1_measured * n) % n # frequency m1
x2_r = round(x2_measured * n) % n # frequency m2
# filter: gcd(x1_r, n) == 1
d = (-x2_r * pow(x1_r, -1, n)) % nNote: The reference Classiq notebook uses d = -x1_r * x2_r^{-1} which is only correct for their degenerate test case (d=n-1). For the competition curves (d≠n-1), use the formula above.
- Always
git pull --rebasebeforegit push - Multiple Claude instances may be running in parallel on different machines; git is the coordination mechanism
- Check
log.txtand recentworklog/files after pulling to catch up on parallel work - Commit message format: short imperative, no "Co-Authored-By" trailers
These attempts use scalar-index encoding: EC group elements are stored as integers in Z_n (the scalar index k, where the element equals k·G). The oracle then computes:
negq_steps[i] = (n − scalar_index(2^i · Q)) % n
But scalar_index(Q) = d — the secret itself. Even deriving it "indirectly" via a
brute-force point_to_index enumeration still means d is solved classically before the
quantum circuit runs. The quantum step is then redundant.
The quantum circuit computes x1 − x2·d (mod n) — arithmetic in Z_n, not EC arithmetic.
The oracle register must hold EC coordinates (x, y) ∈ F_p × F_p. Precomputed
classical constants must be EC points derivable from G and Q without knowing d:
g_powers[i] = 2^i · G (doublings of G — uses only G)
neg_q_powers[i] = −(2^i · Q) (doublings of Q, negate y — uses only Q as a point)
The quantum circuit applies controlled EC point additions (slope formula mod p).
d is never used anywhere in circuit construction.
The oracle must never use params.d. Allowed inputs: params.G, params.Q,
params.p, params.n, params.a, params.b.
If you find yourself computing (n − d) % n, looking up d in a lookup table, or using
point_to_index to convert Q to a scalar, the attempt is NOT genuine ECDLP.
- Do not hardcode
dinto circuit constants (circular — defeats the purpose of ECDLP) - Do not use
params.din the oracle; only useparams.G,params.Q,params.n,params.p,params.a,params.b - Do not use scalar-index encoding for the oracle register (see "Genuine ECDLP" section above)
- Do not run
git pushwithout pulling first - Do not edit past entries in
registry.pyorlog.txt - Do not add
Co-Authored-Byto commit messages - Do not use the full venv path (e.g.
/path/to/venv/bin/python); activate the venv first
The oracle must be scalable to large key sizes.
Attempts 002–006 use "group-index encoding": the quantum register holds a scalar k meaning the point k·G. Deriving the oracle constant for Q = d·G requires enumerating all n group elements:
for k in range(n):
point_to_index[k * G] = k # O(n) = O(2^bits) workFor small competition sizes (n ≤ ~10^6) this is feasible, but for real cryptographic sizes (n ≈ 2^256) it is completely infeasible. This does NOT constitute a scalable quantum algorithm.
Classical precomputation must be polynomial in the key length — e.g., O(var_len) EC doublings to compute [G, 2G, 4G, ...] and [-Q, -2Q, -4Q, ...].
The quantum oracle itself may be designed in many ways — coordinate-based EC arithmetic, novel encodings, or recent algorithmic improvements (e.g., https://eprint.iacr.org/2025/1887.pdf) — as long as the circuit size grows polynomially with the key size.
Reference paper: https://inria.hal.science/hal-04848612v1/document (Chevignard, Fouque, Schrottenloher 2025 — qubit reduction via truncated output and Ekerå-Håstad sampling)