
Abhängigkeitsfreier statischer Analysator für Soundness-Fehler in zk-Schaltkreisen bei o1js/Mina zkApps und Noir-Schaltkreisen
Community-Paket:
o1js-scanist im offiziellen Verzeichnis o1js Community Packages gelistet.
Neueste Version: 0.20.0 — der Analyzer liest jetzt Contracts, die
extends TokenContractverwenden. Bis zu diesem Release erfasste das Contract-Gate nurSmartContract, sodass jeder Fungible Token, jede NFT-Sammlung und jeder AMM-Pool im Ökosystem als "no findings" gescannt wurde. Wenn Sie einen Token-Contract vor 0.20.0 gescannt haben, scannen Sie ihn erneut. Siehe .
Ein schneller, abhängigkeitsfreier statischer Analyzer für zk-Circuit-Soundness-Bugs in:
.ts / .js) — Kimchi-Circuits aus @method-Bodies.nr) — Aztecs Rust-ähnliche ZK-DSL (einschließlich aztec-nr-förmiger Muster)Die sicherheitskritischen Bugs liegen meist nicht im Proving-System — sie liegen in den
eigenen Constraints der Anwendung: Witnesses, die der Prover kontrolliert, aber der Circuit
niemals bindet. o1js-scan ist der Under-Constrained-Signal-Scanner für die Cousins von Circom
in den Ökosystemen Mina und Noir.```bash
pip install o1js-scan
o1js-scan path/to/zkapp # o1js + Noir (auto) noir-scan path/to/circuits # same binary — Noir-friendly alias noir-scan . --lang noir --fail-on high --sarif noir.sarif
### Beispiel
Gegeben sei ein Vault, dessen `withdraw`-Betrag ein vom Prover kontrollierter Witness ist, der
niemals an den On-Chain-Zustand gebunden wird:```console
$ o1js-scan examples/vulnerable_vault.ts --include-examples
LOW O1JS_UNCONSTRAINED_RECIPIENT vulnerable_vault.ts:23 fn=withdraw Recipient `to` is prover-chosen in `withdraw`
HIGH O1JS_UNCONSTRAINED_WITNESS vulnerable_vault.ts:23 fn=withdraw Unconstrained witness `amount` flows to send_amount in `withdraw`
o1js-scan: 2 finding(s) [1 high, 1 low] in 1 of 1 file(s) — fails (--fail-on high)
$ echo $?
1
--include-examples wird hier nur benötigt, weil die Demo-Datei unter
examples/ liegt, was der Pfad-Klassifizierer standardmäßig herabstuft, damit
der Beispielcode eines Repos seinen eigenen Build nicht fehlschlagen lässt. Derselbe Vertrag in deinem src/ meldet
HIGH ohne Flag.
Der HIGH-Fund ist der ausleerbare Bug. Der korrigierte Vertrag
(examples/safe_vault.ts) verwirft ihn und beendet mit 0, wobei nur der informative
LOW beim vom Prover gewählten Empfänger erhalten bleibt:```console
$ o1js-scan examples/safe_vault.ts --include-examples
LOW O1JS_UNCONSTRAINED_RECIPIENT safe_vault.ts:23 fn=withdraw Recipient to is prover-chosen in withdraw
o1js-scan: 1 finding(s) [1 low] in 1 of 1 file(s) — passes (--fail-on high)
$ echo $?
0
Siehe [`examples/`](https://github.com/auditinfra-io/o1js-scan/blob/main/examples) für die o1js- und Noir-Paare aus verwundbaren/fixed Versionen.
## Inhalt
- [Installation](#install)
- [Verwendung](#usage) · [Einen Befund unterdrücken](#suppressing-a-reviewed-finding)
- [GitHub Action](#github-action)
- [Was es erkennt — o1js](#what-it-detects-o1js) · [Noir](#what-it-detects-noir)
- [Bekannte Einschränkungen](#known-limitations) · [Wo dieses Tool an seine Grenzen stößt](#where-this-tool-stops)
- [Datenschutz und privater Code](#privacy-and-private-code)
- [Post-Quanten-Überprüfung](#post-quantum-review)
- [Kompatibilität](#compatibility) · [Funktionsweise](#how-it-works)
- [Mitwirken](#roadmap--contributing)
## Installation```bash
pip install o1js-scan
Für eine isolierte globale CLI-Installation verwenden Sie pipx:```bash
pipx install o1js-scan
Für Node/npm-basierte Noir-, Aztec- oder o1js-App-Repositories installieren Sie den npm-Wrapper:```bash
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high
Das npm-Paket ist ein dünner Wrapper um denselben Python-Analyzer und erfordert
Python 3.8+ im PATH (python3 oder python). Setzen Sie O1JS_SCAN_PYTHON, um einen
bestimmten Interpreter auszuwählen.
Oder aus dem Quellcode:```bash git clone https://github.com/auditinfra-io/o1js-scan cd o1js-scan pip install -e .
Keine Python-Abhängigkeiten von Drittanbietern. Python 3.8+. Das Konsolenskript `noir-scan` wird
zusammen mit `o1js-scan` installiert (gleicher Einstiegspunkt), auch über den npm-
Wrapper.
## Verwendung```bash
# scan a directory (recursively; skips node_modules, target/, .git, …)
o1js-scan path/to/project
# Noir-only / o1js-only
noir-scan circuits --lang noir
o1js-scan src --lang o1js
# scan a single file
o1js-scan src/MyContract.ts
noir-scan src/main.nr
# machine-readable output for CI
o1js-scan src --json
# SARIF 2.1.0 for GitHub code scanning (writes o1js-scan.sarif by default)
o1js-scan src --sarif
noir-scan . --lang noir --sarif noir.sarif
# choose which severity fails CI (critical|high|medium|low|none; default high)
o1js-scan src --fail-on medium
# progressive/power-user gate (equivalent to --fail-on medium)
o1js-scan src --strict
# test code is excluded by default (both backends); opt back in
o1js-scan src --include-tests
# example code is downgraded to LOW by default; keep original severity
o1js-scan src --include-examples
o1js-scan --version
Der Exit-Code ist 1, wenn ein Finding auf oder über der --fail-on-Stufe (Standard
high) vorliegt, und andernfalls 0 — du kannst ihn also direkt in CI einbinden.
Mit dem Standard lässt ein Low-/Medium-Finding (einschließlich der unten
beschriebenen informativen Empfänger-Regel) den Build nicht fehlschlagen; verwende --fail-on none, um nur zu berichten,
oder --strict (eine Kurzform für --fail-on medium), um strenger zu filtern und
Findings mit niedrigem Schweregrad weiterhin als Hinweis zu behandeln. Die beiden Optionen schließen sich gegenseitig aus, damit die CI-Konfiguration nicht mehrdeutig sein kann. Ein fehlender Scan-Pfad beendet mit 2
und einer Fehlermeldung auf stderr, sodass ein Tippfehler nicht stillschweigend als sauberer Lauf durch die CI gehen kann. Jeder Lauf
gibt eine einzeilige Zusammenfassung (Anzahl nach Schweregrad und das Gate-Urteil) auf stderr aus.
Testcode ist standardmäßig ausgeschlossen — bei beiden Backends. Tests bauen absichtlich ungültige Werte und fehlerhafte Transaktionen, um zu belegen, dass die Asserts sie ablehnen; ein Finding dort ist also der Sinn des Tests und kein Circuit-Bug. Eine Datei gilt als Testcode, wenn:
*.test.ts / *.spec.ts entspricht (und den .js/.jsx/.tsx/.mjs
/.cjs-Varianten) oder *_test.nr / test_*.nr;test/, tests/, __tests__/, spec/ oder __mocks__/
liegt;#[test] / #[test(...)]-Attribut
trägt oder sich innerhalb eines mod test { … } / mod tests { … }-Blocks befindet —
blockbezogen, sodass ein Testmodul am Ende einer Produktionsdatei nicht den Rest davon
stummschaltet.Übergib --include-tests, um sie zu melden.
Beispielcode wird herabgestuft, nicht verworfen. Ein Finding in einem examples/- oder
example/-Verzeichnis oder in einer Datei namens *.eg.ts (.nr und die anderen JS/TS-
Erweiterungen ebenfalls) wird auf LOW herabgestuft, mit einem Hinweis — weiterhin gemeldet, aber nicht mehr
in der Lage, einen Build fehlschlagen zu lassen. Beispielcode ist absichtlich vereinfacht, und die eigenen Beispiele eines
Frameworks als Schwachstellen zu markieren, ist Rauschen; aber er wird in die Produktion kopiert
weit häufiger als Testcode, weshalb er herabgestuft statt verborgen wird. Übergib --include-examples, um den ursprünglichen Schweregrad beizubehalten.
Wann immer eine der beiden Richtlinien greift, gibt der Lauf eine Zeile auf stderr aus, die dies mitteilt —
z. B. 6 file(s) skipped as test code, 1 finding(s) downgraded as examples — damit
ein ruhiger Scan niemals stillschweigend ruhig ist. Die Zählungen erscheinen auch in SARIF unter
invocation.properties. Beachte den Kompromiss: Die Erkennung ist nur pfadbasiert
(kein describe(/it(-Parsing), sodass ein Produktions-Circuit, der unter tests/ gespeichert ist,
übersprungen wird — die stderr-Zeile ist, wie du es bemerkst.
Verzeichnisse, die beim Durchlaufen eines Baums übersprungen werden: node_modules, target (nargo),
.git, dist, build, __pycache__, .venv, venv.
Schalte ein Finding, das du triagiert hast, stumm, ohne das Gate zu lockern, mit einem Inline- Kommentar auf — oder in der Zeile über — der markierten Zeile:```ts this.send({ to, amount }); // o1js-scan-disable-line O1JS_UNCONSTRAINED_WITNESS
// o1js-scan-disable-next-line this.send({ to, amount });
Ihre Anfrage konnte nicht verarbeitet werden, da kein zu übersetzender Inhalt bereitgestellt wurde. Bitte senden Sie den Markdown-Text, den Sie übersetzen möchten.```nr
let inv = unsafe { hint(x) }; // o1js-scan-disable-line NOIR_UNCONSTRAINED_WITNESS
Eine oder mehrere Regel-IDs auflisten, um nur diese zu unterdrücken; eine reine Direktive (ohne IDs) unterdrückt jede Regel in der Zielzeile.
Als Bibliothek:```python from o1js_scan import analyze_file, analyze_project
for path, finding in analyze_project("src", lang="auto"): print(path, finding.rule_id, finding.severity.value, finding.title)
## GitHub Action
Fügen Sie den Scanner in wenigen Zeilen zur CI hinzu. Befunde erscheinen als Annotationen im PR-Diff und als Warnungen im Tab **Security → Code scanning** des Repositorys.```yaml
# .github/workflows/o1js-scan.yml
name: o1js-scan
on: [push, pull_request]
permissions:
contents: read
security-events: write # required to upload SARIF to code scanning
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: auditinfra-io/[email protected]
with:
path: src # optional, defaults to the repo root
lang: auto # auto | o1js | noir
# version: 0.20.0 # optional, pin the scanner version
# fail-on: high # optional, fail the job on high/critical
Empfohlen für Noir-Projekte, die Code-Scanning-Warnungen und ein Gate für hohe Schweregrade wünschen:```yaml
Oder ohne die Action:```bash
pip install o1js-scan
noir-scan . --lang noir --fail-on high --sarif noir.sarif
Inputs: `path` (Standard `.`), `lang` (`auto`|`o1js`|`noir`, Standard `auto`),
`version` (zu installierende PyPI-Version, Standard neueste), `upload-sarif` (Standard
`true`), `fail-on` (`critical`|`high`|`medium`|`low`|`none`, Standard `none`),
`fail-on-findings` (veraltet, Standard `false`), `include-tests` (Standard
`false`), `include-examples` (Standard `false`). Ausgabe: `sarif-file`. SARIF-
Upload benötigt `security-events: write` und aktiviertes Code Scanning.
Der Bericht und das Gate werden aus einem Argument-Array erstellt, sodass `include-tests`
und `include-examples` auf beide angewendet werden — das SARIF, das Sie lesen, und der Exit-Code, auf den Sie
gaten, beschreiben immer denselben Quellsatz. Der Reporting-Durchlauf läuft mit
`--fail-on none`, sodass Befunde den SARIF-Upload nie blockieren, aber ein operativer
Fehler (ein Pfad, der nicht existiert, ein CLI-Nutzungsfehler) lässt den Schritt
weiterhin fehlschlagen, anstatt als sauberer Scan gemeldet zu werden.
`fail-on-findings: true` bleibt aus Kompatibilitätsgründen erhalten und wird auf `fail-on: high`
abgebildet, wenn `fail-on` auf `none` belassen wird; es gibt eine Veraltungswarnung aus. Bevorzugen Sie
`fail-on`, das bei jeder Schwere gate kann.
## Was es erkennt (o1js)
### Unterstützte Regeln auf einen Blick
<!-- BEGIN GENERATED RULE SUMMARY -->
| Backend | Regeln | High-fähig | Medium-fähig | Low-fähig |
|---------|------:|-------------:|---------------:|------------:|
| o1js | 18 | 11 | 12 | 2 |
| Noir | 11 | 4 | 9 | 1 |
| **Gesamt** | **29** | **15** | **21** | **3** |
<!-- END GENERATED RULE SUMMARY -->
Die Zählungen sind unterschiedliche Regel-IDs, die von jedem Backend unterstützt werden. Eine Regel, die
Schwere je nach Kontext zuweist (zum Beispiel high für einen Werttransfer und
medium für einen State-Write), erscheint in mehr als einer Schwere-Spalte, sodass die
Schwere-Spalten absichtlich nicht zur Regel-Gesamtzahl addieren. Derzeit gibt es keine
Critical- oder Info-Schwere-Regeln. Die vollständigen Beschreibungen und
False-Positive-Guards folgen unten.
<!-- BEGIN GENERATED O1JS RULE TABLE -->
| Regel | Schwere | Was sie bedeutet |
|------|----------|---------------|
| `O1JS_MISSING_STATE_PRECONDITION` | high | `this.x.get()`-Lesen ohne passendes `requireEquals(...)` / `getAndRequireEquals()`. Ein bloßes `get()` fügt **keine** Account-Vorbedingung hinzu, sodass der Proof `x` nicht an seinen On-Chain-Wert bindet — ein Prover kann jeden Wert einsetzen. |
| `O1JS_UNCONSTRAINED_WITNESS` | high / medium | Ein `@method`-Argument (ein prover-kontrollierter privater Witness) fließt in einen Send-**Betrag** (`this.send(...)` oder ein `AccountUpdate.create*(...).send(...)` derselben Methode) oder ein State-`.set(...)` und wird **nie** zugesichert. Direktes Analogon zu einem unterbeschränkten Circom-Signal. High, wenn es einen Werttransfer erreicht. |
| `O1JS_UNCONSTRAINED_PROVABLE_WITNESS` | high / medium / low | Eine lokale `Provable.witness(...)`-Variable fließt in einen Send-/State-Effekt mit **keiner** In-Circuit-Zusicherung. Der Witness-Callback läuft *außerhalb* des Circuits (er ist nur ein Prover-Hinweis), sodass das Ergebnis ein frischer prover-kontrollierter Wert ist — die andere Witness-Quelle neben `@method`-Argumenten. Er muss neu abgeleitet und zugesichert (`x.assertEquals(<recomputed>)`) oder an State gebunden werden. High bei einem Send-Betrag (`this.send(...)` oder `AccountUpdate.create*` derselben Methode). |
| `O1JS_UNCONSTRAINED_RECIPIENT` | low | Ein `@method`-Argument wird **nur** als `to:`-Empfänger eines Sends verwendet. Dies ist normalerweise beabsichtigt (ein Nutzer benennt sein eigenes Auszahlungsziel) und ist informativ — es ist nur relevant, wenn das Ziel eine feste Treasury oder eine im State erfasste Adresse sein soll. Löst **nicht** das CI-Exit-Code-Gate aus. |
| `O1JS_WITNESS_NOT_BOUND_TO_STATE` | medium | Ein Witness ist nur *trivial* beschränkt (z. B. `> 0` oder gegen eine Konstante verglichen), bevor ein Effekt erfolgt — nie an On-Chain-State gebunden. Bestätigen Sie, dass die Off-Chain-Orchestrierung dies sicher macht, oder das Guthaben ist bis zu seinem Bestandswert abziehbar. |
| `O1JS_STALE_MERKLE_ROOT` | high | Eine Methode berechnet einen Merkle-Root aus einem prover-gelieferten Witness (`computeRootAndKey` / `calculateRoot`) neu, bindet aber **keinen** der neu berechneten Roots an den aktuellen On-Chain-Root. Ohne ein `this.root.requireEquals(...)` / `assertEquals` gegen den Live-Root kann ein Prover einen Witness für einen fabrizierten oder veralteten Baum übergeben — Mitgliedschaft fälschen oder alten State wiedergeben. Die Bindung kann in einem undekorierten Helper derselben Klasse (`this.verifyX(witness)`) liegen; die Helper-Propagierung deckt das ab. |
| `O1JS_UNVERIFIED_PROOF` | high | Ein `@method`-Parameter vom Typ `Proof<...>` / `SelfProof<...>` / `DynamicProof<...>` wird nie `.verify()`'d, bevor seine öffentlichen Felder verwendet werden. Das Übergeben eines Proofs verifiziert ihn nicht — ohne explizites Verify kann der Prover ein beliebiges Proof-Objekt liefern, und jede Verwendung seines `publicOutput` ist unbeschränkt. Löst auch aus, wenn `.verifyIf(flag)` durch ein unbeschränktes `@method`-Argument gegated wird und die öffentlichen Felder des Proofs gelesen werden, weil der Prover die Bedingung falsch machen kann. |
| `O1JS_UNASSERTED_BOOL` | high / medium | Ein o1js-Prädikat (`equals` / `lessThanOrEqual` / …) gibt ein `Bool` zurück und fügt **keine** Beschränkung hinzu, es sei denn, das Ergebnis wird zugesichert oder verwendet. HIGH, wenn der Aufruf eine bloße verworfene Anweisung ist; MEDIUM, wenn er einer lokalen Variable zugewiesen wird, die nie wieder referenziert wird. |
| `O1JS_UNCONSTRAINED_SENDER` | high / medium | `this.sender.getUnconstrained()` gibt den Tx-Sender zurück, ohne ihn zu beweisen. HIGH, wenn dieser Wert (oder eine lokale Variable daraus) in ein Assert / State-`.set` / `send` fließt (leere Prüfung); andernfalls MEDIUM. Bevorzugen Sie `this.sender.getAndRequireSignature()` oder das erweiterte Idiom `AccountUpdate.createSigned(sender)`. **Bleibt still, wenn** (1) dieselbe `@method` auch irgendwo `this.sender.getAndRequireSignature()` aufruft (Signaturanforderung ist methodenbezogen), oder (2) der gewitness-te Sender-Wert das Argument für `AccountUpdate.createSigned(...)` / ein `AccountUpdate.create(...).requireSignature()` auf demselben Schlüssel ist (Argumentidentität erforderlich — ein `createSigned` auf einem anderen Schlüssel unterdrückt nicht). |
| `MissingRangeCheck` | high | Ein rohes `Field` (nicht das range-geprüfte `UInt64`/`UInt32`) wird als Transferbetrag verwendet. Ein `Field` ist ein Element mod p und nicht range-beschränkt. |
| `O1JS_WEAK_PERMISSIONS` | high / medium | `editState` / `send` auf `proofOrSignature()` oder `none()` gesetzt, wodurch der zkApp-Account-Schlüssel den Circuit durch Signieren umgehen kann. Markiert auch `setVerificationKey` / `setPermissions`, die auf `signature` / `proofOrSignature` / `none` belassen wurden (Minas dokumentierte Upgrade-Trainingsräder); HIGH in Kombination mit einem schwachen `editState`/`send` im selben `permissions.set`. |
| `O1JS_LOGIC_OUTSIDE_PROOF` | high | Sicherheitslogik (Assert / Approve / Send / State-`.set`) innerhalb von `Provable.asProver(...)` oder einem `Provable.witness*`-Callback. Diese Callbacks laufen *außerhalb* des Circuits — ein bösartiger Prover kann sie löschen und dennoch einen verifizierenden Proof erzeugen. |
| `O1JS_APPROVE_WITHOUT_BINDING` | medium | Eine `@method` ruft `approve` / `approveAccountUpdate` / `approveBase` auf, ohne `balanceChange` / `publicKey` zu lesen und ohne `assertCanMint` / `assertCanBurn` / eine `forEachUpdate`-Erhaltungsprüfung — der Mina FlawedTokenContract-Archetyp. |
| `O1JS_VACUOUS_ASSERT` | high / medium | Ein Assert, das konstruktionsbedingt erfüllt ist: `x.assertEquals(x)`, `x.equals(x).assertTrue()` oder `Bool(true).assertTrue()`. HIGH für Selbstvergleiche (fast immer ein Tippfehler); MEDIUM für konstante Bool-Asserts. |
| `O1JS_CONDITIONAL_ASSERT` | medium | Ein Assert innerhalb von `if <flag> { ... }`, wobei `<flag>` ein prover-kontrolliertes `@method`-`Bool` ist (oder eine lokale Variable aus `.toBoolean()`). Ein JS-Bedingung beschränkt den Circuit nicht so, wie es `Provable.if` tut. Inline-Vergleiche bleiben aus Präzisionsgründen ungemeldet. |
| `O1JS_GUARDED_INVERSE` | medium | Ein `.div()` / `.inv()` / `.sqrt()` innerhalb eines `Provable.if`-Zweigs, geschützt durch eine Bedingung auf genau den Wert, bei dem es fehlschlägt. Beide Zweige werden in-circuit ausgewertet, und diese Aufrufe sichern bedingungslos zu, dass die Inverse oder Wurzel existiert, sodass der Guard die Zusicherung nicht überspringt — der Circuit ist für genau die Eingabe unerfüllbar, für die der Guard geschrieben wurde, und die Methode kann dafür nie bewiesen werden. Gemeldet von Veridise als `V-O1J-VUL-060`. Berechnen Sie zuerst einen sicheren Divisor (`Provable.if(isZero, Field(1), d)`) und wählen Sie das Ergebnis danach aus. **Bleibt still, wenn** der Guard nichts über den Divisor aussagt, sodass ein unabhängiges `Provable.if` um eine sichere Division nicht markiert wird. |
| `O1JS_PRECONDITION_OVERWRITTEN` | medium | Zwei oder mehr `requireEquals` / `requireBetween` / `requireNothing`-Aufrufe auf derselben Eigenschaft in einer Methode mit unterschiedlichen Argumenten. Vorbedingungen werden auf dem AccountUpdate *gesetzt* statt akkumuliert, sodass jeder Aufruf den vorherigen überschreibt und nur der letzte durchgesetzt wird — anders als In-Circuit-Zusicherungen, die sich zusammensetzen. `a.requireEquals(b)` dann `a.requireEquals(c)` impliziert `a === c`, nicht `a === b`. Gemeldet von Veridise als `V-O1J-VUL-012`. **Bleibt still, wenn** die Argumente identisch sind (idempotent, nichts geht verloren), bei `getAndRequireEquals()` (eine andere Methode, sodass wiederholte State-Reads in Ordnung sind) und wenn die Aufrufe in sich gegenseitig ausschließenden JS-Zweigen liegen, die zur Circuit-Build-Zeit aufgelöst werden. Diese letzte Ausnahme kann ein echtes Overwrite verbergen, das einen unabhängigen `if`/`else` überspannt. |
| `O1JS_STATE_READ_AFTER_WRITE` | medium | Ein `@state`-Feld wird gelesen (`get()` / `getAndRequireEquals()`), nachdem ein `set(...)` auf demselben Feld in derselben Methode abgeschlossen ist. `set()` zeichnet die Änderung auf dem AccountUpdate auf, schreibt aber nicht durch zu `get()`, sodass das Lesen noch den Wert von vor dem Schreiben beobachtet und jede darauf aufgebaute Arithmetik still um dieses Schreiben daneben liegt. Gemeldet von Veridise als `V-O1J-VUL-030`. Behalten Sie den neuen Wert in einer lokalen Variable, anstatt den State zurückzulesen. **Bleibt still, wenn** das Lesen innerhalb der eigenen Argumente des Schreibens verschachtelt ist (das Read-Modify-Write-Idiom `this.x.set(this.x.getAndRequireEquals().add(1))`, das korrekt ist), und wenn Schreiben und Lesen in sich gegenseitig ausschließenden JS-Zweigen liegen. Auf eine einzelne Methode beschränkt — der Cross-Method-Caching-Fall, den Veridise ebenfalls beschreibt, benötigt Call-Graph-Wissen, das diese Regel nicht hat. |
<!-- END GENERATED O1JS RULE TABLE -->
### False-Positive-Guards (o1js)
Der Analyzer ist darauf ausgelegt, bei korrektem Code still zu bleiben:
- **Signatur-gegatete Methoden werden übersprungen.** Eine `@method`, die
`this.requireSignature()` (oder `getAndRequireSignature`, `AccountUpdate.createSigned`,
`Signature.verify`) aufruft, ist owner/admin-gegatet — ihre Argumente werden vom Schlüssel-
Inhaber gewählt, nicht von einem beliebigen Prover — sodass ihre Witnesses nicht markiert werden. Dies ist das
o1js-Äquivalent zu `onlyOwner`.
- **State-gebundene Witnesses werden übersprungen.** Ein Argument, das als gleich zu (oder
beschränkt durch einen Ordnungsvergleich gegen) einen `getAndRequireEquals()`-abgeleiteten
Wert zugesichert wird, ist solide und wird nicht gemeldet. Dies deckt sowohl die direkte Form ab —
`amount.assertLessThanOrEqual(bal)` — als auch die verkettete Form
`amount.lessThanOrEqual(bal).assertTrue()`. Bindung, die in einem
undekorierten Helper derselben Klasse (`this.verifyX(arg)`) liegt, wird ebenfalls erkannt,
einschließlich durch eine Kette solcher Helper.
- **Verifizierte Proofs werden übersprungen.** Ein `Proof` / `SelfProof` / `DynamicProof` /
`*Proof`-typisiertes Argument, auf dem `.verify()` aufgerufen wird, ist durch den
verifizierten Circuit beschränkt — Witness-Befunde darauf (und auf sein `publicOutput` /
`publicInput`) werden unterdrückt. Ein `.verifyIf(flag)` wird nur angerechnet, wenn die
Bedingung kein unbeschränktes Methodenargument ist oder selbst zugesichert wird. Dasselbe gilt für den kanonischen OffchainState-Wrapper
`this.offchainState.settle(proof)` (das Framework verifiziert innerhalb von `settle`).
Ein handgeschriebenes `.settle(proof)` wird
**nicht** als verifizierend angenommen. Der umgekehrte Fall (proof-typisiertes Argument nie verifiziert
und nicht OffchainState-gesettlet) wird als `O1JS_UNVERIFIED_PROOF` gemeldet.
- **Zugesicherte / verwendete Bools werden übersprungen.** Ein Prädikat, das mit
`.assertTrue()` / `.assertFalse()` verkettet, in `Provable.if(...)` verschachtelt oder
einer lokalen Variable zugewiesen wird, die später referenziert wird, wird nicht als
`O1JS_UNASSERTED_BOOL` gemeldet.
- **Authentifizierte Sender werden übersprungen.** `this.sender.getUnconstrained()`
löst nicht aus, wenn dieselbe `@method` auch
`this.sender.getAndRequireSignature()` aufruft, oder wenn dieser gewitness-te Wert
an `AccountUpdate.createSigned(...)` übergeben / über `.requireSignature()` auf einem daraus erstellten AccountUpdate authentifiziert wird (Argument-
identität erforderlich).
- Kommentare und String-Literale werden vor der Analyse entfernt, sodass ein `assert`
innerhalb eines Strings kein falsches Ergebnis erzeugen kann.
## Was es erkennt (Noir)
Dieselbe Soundness-Idee — unterbeschränkte Witnesses — gilt für
[Noir](https://noir-lang.org) (`.nr`)-Circuits. Richten Sie den Scanner auf `.nr`-
Dateien (oder verwenden Sie `--lang noir`), und er analysiert sie mit dem Noir-Regelsatz.
Derselbe lexikalische, abhängigkeitsfreie Ansatz. Kalibriert gegen aztec-nr-Oracle- /
`unsafe`-Idiome — siehe [`docs/noir_calibration.md`](https://github.com/auditinfra-io/o1js-scan/blob/main/docs/noir_calibration.md).
<!-- BEGIN GENERATED NOIR RULE TABLE -->
| Regel | Schwere | Was sie bedeutet |
|------|----------|---------------|
| `NOIR_UNCONSTRAINED_WITNESS` | high | Ein Wert, der aus einem `unsafe { ... }`-Block gebunden wird — das Ergebnis einer `unconstrained fn` (Oracle- / Brillig-Hinweis) —, der nie durch ein `assert` / `assert_eq` (oder einen bestätigenden Helper / Merkle-Check) neu beschränkt wird. Der Hinweis läuft **außerhalb** des Circuits. Analogon zu `O1JS_UNCONSTRAINED_PROVABLE_WITNESS`. |
| `NOIR_UNCONSTRAINED_INPUT` | medium | Ein privater (Witness-)Input von `fn main`, der in **kein** `assert` / `assert_eq` fließt und **nicht** Teil des öffentlichen Outputs ist. Analogon zu `O1JS_UNCONSTRAINED_WITNESS`. |
| `NOIR_UNCONSTRAINED_PUBLIC_INPUT` | medium | Ein **öffentlicher** Input von `fn main`, der keine Beschränkung und keinen Output erreicht — der Circuit liest ihn nie. Das *Dual* der Private-Witness-Regel: Der Verifier liefert den Wert und glaubt, die Aussage handelt von ihm, während der Circuit ihn ignoriert (z. B. ein `merkle_root: pub Field`, das nie geprüft wird, sodass Mitgliedschaft nie tatsächlich bewiesen wurde). MEDIUM, weil ein absichtlich ungenutzter öffentlicher Input auch ein legitimes Idiom ist, um einen Proof an einen Kontext (Nonce / Chain-ID / Empfänger) zu binden, was lexikalisch nicht unterscheidbar ist — daher gatet es CI nicht beim Standard `--fail-on high`. |
| `NOIR_UNCHECKED_CAST` | medium | Ein prover-kontrollierter Wert, der auf einen schmalen unsigned Typ (`as u8`/`u16`/`u32`) gecastet wird, mit **keiner** Range-Zusicherung. Analogon zu o1js `MissingRangeCheck`. |
| `NOIR_UNCONSTRAINED_ARRAY_INDEX` | medium | Ein prover-kontrollierter Wert, der als Array-Index (`arr[i]`) verwendet wird, mit **keiner** Prüfung jeglicher Art darauf. Noirs implizite Bounds-Prüfung stellt nur fest, dass der Index *im Bereich* liegt — nicht, dass er der *korrekte* Index ist —, sodass der Prover frei bleibt, jedes Element auszuwählen und dennoch einen verifizierenden Proof zu erzeugen. Dies ist der Selector-Freedom-Bug hinter Merkle-Pfadpositionen, Note-Auswahl und Allow-List-Mitgliedschaft. Unterdrückt, wenn der Index range-beschränkt, durch eine Gleichheit gepinnt, vor einem Cast beschränkt (`index.assert_max_bit_size::<8>(); let i = index as u32;`) oder wenn der zurückgelesene Wert selbst durch ein `assert_eq` gepinnt ist. |
| `NOIR_UNASSERTED_BOOL` | high / medium | Ein Vergleich, dessen `bool`-Ergebnis **verworfen** wird. Analogon zu o1js `O1JS_UNASSERTED_BOOL`. |
| `NOIR_CONDITIONAL_ASSERT` | medium | Ein `assert` innerhalb von `if <flag> { ... }`, wobei `<flag>` ein prover-kontrolliertes bloßes `bool` oder eine lokale Variable aus prover-kontrollierten Werten ist. Eine Beschränkung innerhalb eines Conditionals gilt nur, wenn die Bedingung wahr ist, sodass ein prover-gewählter Zweig die Prüfung überspringen kann. Inline-Vergleiche (`if x != 0`) werden aus Präzisionsgründen in Ruhe gelassen; das Zuweisen des Guards an eine lokale Variable (`let gate = x != 0; if gate`) wird gemeldet, es sei denn, `gate` wird selbst zugesichert. |
| `NOIR_CONDITIONAL_CONSTRAIN` | medium | Ein `constrain_*` / `confirm_*` / `verify_*`-Aufruf nur unter einem prover-kontrollierten `if`, während ein `unsafe`-Hinweis noch den Output erreicht. |
| `NOIR_UNUSED_CHECK_RESULT` | high / medium | Ein `check_*` / `confirm_*` / `verify_*` / `constrain_*`-Ergebnis wird verworfen (bloßer Aufruf) oder zugewiesen und nie zugesichert — die Prüfung bindet den Circuit nicht. |
| `NOIR_VACUOUS_CONSTRAINT` | high / medium | Eine Beschränkung, die konstruktionsbedingt erfüllt ist: ein Selbstvergleich (`assert(x == x)`, `assert_eq(x, x)`, `x >= x`) oder eine konstante Bedingung (`assert(true)`). Sie fügt keine Einschränkung hinzu, aber die Zeile *liest sich* wie eine Prüfung — was sie gefährlicher macht als eine fehlende Beschränkung, weil die Überprüfung dort aufhört. HIGH für einen Selbstvergleich (fast immer ein Tippfehler für eine echte Prüfung: `assert(computed == expected)` falsch getippt als `assert(expected == expected)`); MEDIUM für eine Konstante, die häufiger ein Platzhalter ist. `x != x` wird **nicht** markiert — das ist unerfüllbar, ein Liveness-Bug statt eines stillen Soundness-Lochs. |
| `NOIR_UNSAFE_MISSING_SAFETY` | low | Ein `unsafe { ... }`-Block ohne angrenzenden `// Safety:`-Kommentar. Informativ; lässt CI beim Standard `--fail-on high` nicht fehlschlagen. |
<!-- END GENERATED NOIR RULE TABLE -->
### False-Positive-Guards (Noir)
- **Assert / Let-Hop / Same-File-Confirm-Helper** binden `unsafe`-Hinweise.
- **Call-Site-Namen** `constrain_*` / `confirm_*` / `verify_*` /
`check_(non_)membership*` / `public_data_storage_read` rechnen Argumente an (mit
Unused-Result-Erkennung für verworfene Prüfungen).
- **Dokumentiert absichtlich unbeschränkt** (erfordert angrenzendes `// Safety:`):
`random()`, `avm::…` und Kernel/Rollup/Discovery-Deferred-Formulierungen.
- **Tuple-`let` + zugesicherte Flags** binden Merkle-Witnesses, die in Membership-Prüfungen übergeben werden.
Beispiel:```console
$ noir-scan examples/noir_unconstrained.nr --include-examples
HIGH NOIR_UNCONSTRAINED_WITNESS noir_unconstrained.nr:16 fn=main Unconstrained `unsafe` result `inv` in `main`
LOW NOIR_UNSAFE_MISSING_SAFETY noir_unconstrained.nr:16 fn= `unsafe` block without a `// Safety:` comment
noir-scan: 2 finding(s) [1 high, 1 low] in 1 of 1 file(s) — fails (--fail-on high)
$ noir-scan examples/noir_constrained.nr --include-examples
noir-scan: no findings in 1 o1js or Noir file(s) — passes (--fail-on high)
Wie beim o1js-Beispiel oben ist --include-examples nur deshalb erforderlich, weil diese Demo-Dateien unter examples/ liegen.
Der Analyzer ist ein abhängigkeitsfreies lexikalisches Frontend plus eine leichtgewichtige semantische Schicht, die Alias-Verfolgung und interprozedurale Propagation durch Hilfsfunktionen derselben Klasse durchführt. Er ist kein TypeScript-Compiler-Frontend, kein Type-Checker und keine Ganzprogramm-Datenfluss-Engine, und es gibt keine SMT- oder Formalbeweis-Schicht in diesem Scanner. Behalten Sie diese blinden Flecken beim Triage im Hinterkopf — sie sind für dieses abhängigkeitsfreie Design bekannt und beabsichtigt, keine Bugs:
Nur einfache Aliase werden verfolgt. Die Witness-Verfolgung folgt einfachen
Aliasen innerhalb derselben Methode wie const q = qty, aber nicht abgeleiteten Ausdrücken oder
Destrukturierung: ```ts
const q = qty; this.send({ to: dest, amount: q }); // followed
const q = qty.add(1); this.send({ to: dest, amount: q }); // not followed
const slot = this.root; slot.get(); // missing precondition missed
Cross-Method-Bindung deckt nur Same-Class-Helper-Ketten ab. Ein
undekorierter Same-Class-Helper, der als this.verifyX(arg) aufgerufen wird, kann
ein Argument des Aufrufers state-binden, und seit 0.19.0 werden Ketten davon
(@method → Helper A → Helper B) bis zu einem Fixpunkt verfolgt. Der
Helper→Helper-Schritt bildet nur eine bloße Parameterreferenz ab, daher
wird helperA(x.add(1)) nicht propagiert. Freie und importierte Funktionen
werden weiterhin nicht verfolgt, und lokale Variablen-Aliasing des
Helper-Arguments bleibt eine dokumentierte Einschränkung.
Unasserted-Bool-Erkennung ist statement-förmig. Tier A markiert nur bloße
Expression-Statements, deren äußerster Aufruf ein Bool-Prädikat ist, an das
nichts angehängt ist. Prädikate, die in Provable.if(...) verschachtelt oder
zugewiesen und später verwendet werden, werden nicht markiert. Komplexe
Kontrollfluss-Verwendungen eines Bool-Lokals können weiterhin übersehen werden,
wenn der Name nie referenziert wird (Fehlermodus: Übersehen, kein falsch
Positiver).
Signature-Gating ist methodenbezogen und substring-basiert.
_method_is_signature_gated behandelt eine ganze @method als owner-gated, wenn sie
ein Signatur-Idiom enthält, und erkennt einen Verifier nur, wenn der
Receiver-Name wörtlich signature enthält — daher wird sig.verify(admin, msg)
nicht als Gating erkannt, während eine unabhängige Signaturprüfung an anderer
Stelle in einer großen Methode übermäßig unterdrücken kann. Es ist alles-oder-nichts pro Methode.
Sender-Authentifizierung ist namensbasiert und nur innerhalb derselben Methode.
O1JS_UNCONSTRAINED_SENDER unterdrückt, wenn this.sender.getAndRequireSignature()
oder AccountUpdate.createSigned(<that sender>) im selben @method-Body
erscheint. Eine Signaturanforderung, die nur in einem Helper lebt
(this.requireSenderSig() → getAndRequireSignature darin), wird nicht
verfolgt — der Fehlermodus ist ein falsch Positiver bei korrektem Code, der das
Idiom kapselt, kein übersehener echter Bug.
Noir Cross-Crate-Helper werden nur durch Namenskonvention erkannt (keine
Nargo.toml / Import-Auflösung). Übersehen wird einem falsch Positiven vorgezogen.
Diese sind der Grund, warum Findings ein Ausgangspunkt für menschliche Überprüfung sind, nicht Beweise. Ein datenflussbewusster Rewrite liegt bewusst außerhalb des Umfangs des lexikalischen Analysators.
o1js-scan ist bewusst ein flacher, dateiweiser lexikalischer Durchlauf — kein Parser, kein Datenfluss, kein Solver. Das macht es abhängigkeitsfrei und sofort in CI einsetzbar, und es ist zugleich eine harte Obergrenze. Die obigen Einschränkungen sind kein Backlog; sie sind Konsequenzen des Designs.
Daher lohnt es sich, explizit zu sagen, was dieses Tool Ihnen sagen kann und was nicht:
Dieser Kompromiss ist der richtige für einen Linter, den Sie bei jedem Commit ausführen. Wenn Sie an etwas arbeiten, wo der Unterschied zählt — ein Protokoll, das echten Wert hält, ein Circuit, bei dem Sie sich keinen Fehler leisten können —, behandeln Sie dies als den ersten Durchlauf und planen Sie eine echte Überprüfung ein.
Für eine tiefere Analyse wird der separate vollständige Scanner im
audit-engine-cli-Repository gepflegt.
o1js-scan ist der bewusst leichte, offene Scanner; das proprietäre Erkennungswissen
und die Implementierungsdetails des vollständigen Scanners werden hier nicht reproduziert. Für Zugang oder eine vollständigere Circuit-Überprüfung wenden Sie sich an:
[email protected].
Die installierte CLI analysiert Dateien lokal. Sie hat keine Telemetrie, keinen Netzwerk-
Client, kein Konto und keinen Upload-Schritt, und ihre Python-Laufzeit hat keine Drittanbieter-
Abhängigkeiten. Das Ausführen von o1js-scan path/to/private-repo sendet den Quellcode
oder Findings nirgendwohin.
Wie Compiler-Logs können Scanner-Ausgaben Pfade, Identifikatoren und Quellcode- Fragmente enthalten. SARIF identifiziert zudem exakte Repository-Positionen, und die GitHub- Action lädt es zum GitHub-Code-Scanning hoch. Verwenden Sie dieselben Repository- und CI-Zugriffs- kontrollen, die Sie bereits für den gescannten Quellcode verwenden.
Möchten Sie einen nützlichen Falsch-Positiv- oder Übersehen-Bericht beitragen, ohne eine Anwendung zu teilen? Reproduzieren Sie die Syntax mit erfundenen Namen und Konstanten, entfernen Sie Geschäftslogik Statement für Statement, und verifizieren Sie, dass das synthetische Snippet weiterhin dieselbe Regel auslöst, bevor Sie es posten. Der datenschutzsichere Beitragsleitfaden enthält eine konkrete Checkliste und mehrere Möglichkeiten, der o1js-Community zu helfen, ohne einen privaten Circuit offenzulegen.
Diese Grenze hindert den offenen Scanner nicht daran, besser zu werden. Öffentliche o1js-Dokumentation und -Repositories können neue Regeln und Kompatibilitäts- Fixtures unterstützen; synthetische Beispiele können falsch Positive und übersehene Constraints testen; und Parser-Resilienz, Diagnostik, SARIF, Performance, Packaging und Kalibrierung können alle verbessert werden, ohne eine private Audit-Technik oder Client-Code zu veröffentlichen. Der offene Scanner sollte unabhängig erklärbare Aussagen machen; private Forschung kann im separaten Audit-Engine verbleiben.
Quantenrisiko hängt mit Circuit-Sicherheit zusammen, ist aber keine Regel für fehlende
Constraints. o1js-scan bestimmt nicht, ob eine Signatur, ein Hash, ein Commitment, das
Kimchi-Proof-System oder Mina selbst ein Post-Quanten-Sicherheitsziel erfüllt. Diese
Antworten hängen vom konkreten Primitiv und den Parametern, Plattformannahmen,
der erforderlichen Lebensdauer des Deployments und seinem Migrationsplan ab — nicht bloß von einem
TypeScript-Identifikator, den ein lexikalischer Scanner sehen kann.
Inspiriert von O(1) Labs' Qubit or Not Qubit, macht der Post-Quanten-Überprüfungsleitfaden diese Grenze zu einem o1js-spezifischen Inventar und einer Krypto-Agilitäts-Checkliste. Verwenden Sie ihn zusammen mit diesem Scanner, statt einen sauberen Scan als Post-Quanten- Bewertung zu interpretieren.
Funktioniert mit o1js 1.x, 2.x und 3.x, einschließlich des Mesa-Hardforks, auf den o1js
3.0.0 abzielt. o1js-scan analysiert TypeScript-Quellcode als Text und hat keine Laufzeit-
Abhängigkeit von o1js — nichts ist versionsgebunden. Es setzt auf die moderne
require*-Vorbedingungs-API (getAndRequireEquals, requireEquals,
requireSignature, getAndRequireSignature), die
@method / @method() / @method.returns(...)-Dekoratoren, annotierte @state-
Felder, this.send({...}), Low-Level-AccountUpdate.balance.subInPlace(...)-
Transfers und Permissions.*. Die etablierten Formen bleiben über
die Grenzen 1.x → 2.x → 3.x hinweg kompatibel, während der Scanner auch die neu
dokumentierten Dekorator- und Low-Level-Transfer-Varianten akzeptiert.
Das 2.x-Owner-Auth-Idiom this.sender.getAndRequireSignature() wird
als Signature-Gating erkannt. (Legacy-assertEquals-Vorbedingungen werden weiterhin akzeptiert,
sodass älterer Code ebenfalls nicht bricht.)
Mesas Breaking Changes sind alle auf Laufzeit- und Protokollebene — die Entfernung von
Transaction.setFeePerSnarkCost() und den TransactionCost.*-Konstanten, die
neue VerificationKey.toJSON()-Form, neu generierte Verification Keys,
MAX_ZKAPP_STATE_FIELDS von 8 auf 32 erhöht und das mina-signer-v4-
Transaktionsformat. Keine davon benennt eine API um, auf die dieser Scanner matcht, daher hat sich keine
Regel für Mesa geändert, und das ist verifiziert statt behauptet.
scripts/o1js_release_matrix.sh scannt zwei gepinnte o1js-Releases, die die
Protokollgrenze überspannen — 2.15.0 (9620ef08, das letzte 2.x-Release) und
3.0.0 (cc18a919, Mesa) — und vergleicht jedes Finding mit
tests/fixtures/o1js_release_matrix.json:
| Release | Findings | HIGH | MEDIUM | LOW | Dateien |
|---|---|---|---|---|---|
| o1js 2.15.0 | 36 | 8 | 26 | 2 | 18 |
| o1js 3.0.0 (Mesa) | 39 | 8 | 29 | 2 | 19 |
33 Findings sind über die Grenze hinweg identisch, keine gingen verloren, und alle drei
neuen befinden sich in src/examples/zkapps/big-state-zkapp.ts — das 32-State-Field-
Beispiel, das nur existiert, weil Mesa MAX_ZKAPP_STATE_FIELDS erhöht hat. Dieses
Delta ist durch einen Test gepinnt, sodass es nicht stillschweigend driften kann. Die Matrix läuft bei jedem
CI-Build; der wöchentliche o1js-upstream-canary-Job verfolgt o1js zusätzlich am
HEAD, vor jedem Release.
Äquivalente Constraint-Schreibweisen werden für die Analyse normalisiert: Instanz-
assertEquals(...), statisches Provable.assertEqual(Type, ...) und
equals(...).assertTrue()-Gleichheitsketten binden alle dieselben Operanden. Die Methoden-
Extraktion ist klammerbalanciert nach längenerhaltender Kommentar- und String-
Maskierung und akzeptiert mehrzeilige Dekoratoren, verschachtelte callback-förmige Parameter-
Typen, TypeScript-Zugriffsmodifikatoren und mehrzeilige Identitäts-Aliase (einschließlich
parenthesierter und as Type-Formen).
Die Noir-Analyse zielt auf Noir-Syntax ab, die von Aztec- / nargo-Projekten verwendet wird (.nr); sie
ruft nargo nicht auf und kompiliert keine Circuits.
Es ist ein lexikalischer Analysator, kein vollständiger TypeScript- oder Noir-Parser — o1js- und Noir- Quellen sind klammerbegrenzt und regex-tauglich, und die Ausgabe ist dafür gedacht, von einem Menschen triagiert zu werden. Das hält es abhängigkeitsfrei und sofort in CI ausführbar. Findings sind ein Ausgangspunkt für die Überprüfung, keine Beweise.
Beiträge willkommen — neue Regelfamilien, mehr FP-Guards und realweltliche
Kalibrierungs-Archetypen sind alle wertvoll. Siehe CONTRIBUTING.md.
Für einen vorgeschlagenen Weg von der Community-Packages-Listung zu einer Advisory-Prüfung im o1js-Repository siehe den versandfertigen o1js-Upstream-Integrationsvorschlag.
Führen Sie die Tests und den Linter aus mit:```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only
## Lizenz
Apache-2.0. Siehe [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE).