# Der Streaming-Prover

> Der Prover führt das Gastprogramm zweimal aus und hält nie den Ausführungs-Trace. Der Speicherbedarf folgt den gerade bearbeiteten Shards, nicht der Länge des Laufs, und der Beweis hängt nicht vom Scheduling ab.

Ein vollständiger Ethereum-Block umfasst etwa 200 Millionen Zyklen. Sein Trace, jede Zeile jeder Familie und jedes Speicherereignis, wäre etwa 300 Byte pro Zyklus groß: Dutzende Gigabyte, bevor eine einzige Spalte committet ist. Der Prover von Apogee baut ihn nie. Er führt das Gastprogramm (Guest) zweimal aus und hält nur die Shards, an denen er gerade arbeitet.

## Zwei Durchläufe

> Figure: Erst committen, dann beweisen. Die Speicher-Challenges müssen auf die Speicher-Commitments jedes Shards folgen; deshalb kommen die Commitments zuerst, aus einer Ausführung, und die Beweise aus einer zweiten.

**Durchlauf 1** führt das Gastprogramm aus und committet, sobald ein Shard gefüllt ist, dessen Speicherspalten, behält die Commitments und verwirft die Zeilen. Beim Exit leitet er alles Weitere, was die Aussage braucht, aus dem finalen Speicherzustand ab: die Randwerte von Registern und pc, die Liste der Speicherfenster, die der Lauf berührt hat, und die Shards der Fensterfamilien. Dann führt er das globale Transkript aus, das die Aussage einschließlich jedes Speicher-Commitments absorbiert, und zieht die Speicher-Challenges sowie den Digest, mit dem jeder Shard initialisiert wird.

**Durchlauf 2** führt erneut aus. Der Emulator ist eine reine Funktion seiner Eingabe und schneidet daher dieselben Shards, und Durchlauf 2 prüft per Assertion, dass sein Zyklenprofil, seine Fensterliste und seine Randwerte die von Durchlauf 1 sind. Jeder Shard erhält jede committete Spalte und wird bewiesen: sein Transkript, sein GKR-Durchlauf, seine Öffnung. Die Speicherspalten werden nicht erneut committet: Die Öffnung nimmt ihre Commitments aus der Aussage und ihre Werte aus Durchlauf 2; Spalten, die sich zwischen den Durchläufen unterschieden, ergäben also eine Öffnung, die der Verifier zurückweist.

Die Reihenfolge ist durch die Soundness erzwungen. Die Speicher-Challenges müssen auf jeden Wert folgen, den ein Speichertupel lesen kann; deshalb werden die Speicherspalten jedes Shards committet, bevor irgendein Shard bewiesen werden kann.

## Die Pipeline

Eine feste Zahl von Workern, `max_in_flight`, teilt sich ein Lock um den Executor. Unter dem Lock gibt ein Worker seinen fertigen Shard zurück und beansprucht den nächsten: einen gefüllten Shard, falls einer wartet, und andernfalls lässt er den Executor selbst Schritt für Schritt laufen, bis ein Puffer voll ist. Außerhalb des Locks baut er die Spalten des Shards, beweist ihn und verwirft ihn.

- **Der Executor läuft dem Bedarf nie voraus.** Höchstens ein gefüllter, nicht beanspruchter Shard pro Familie wartet, in Form von Zeilen.
- **Innerhalb eines Shards ist die Arbeit datenparallel**, über alle Kerne. Ein Worker, der in dieser Arbeit blockiert ist, nimmt keinen zweiten Shard.
- **Der Block hängt nicht vom Scheduling ab.** Der Beweis eines Shards ist eine Funktion des globalen Zustands und seiner eigenen Spalten; Beweise werden nach ihrer Position in der Aussage abgelegt. Die Bytes sind bei 1 und bei 8 gleichzeitig bearbeiteten Shards identisch.
- **Fehlschläge sind deterministisch.** Zurückgegeben wird der früheste Fehlschlag in der Befüllungsreihenfolge, bei jeder Zahl von Workern.

## Was es kostet

Der Speicher besteht aus einem Teilpuffer pro Familie, den Tabellen der letzten Zugriffe, den gerade bearbeiteten Shards und der Ausgabe. Den Speicherbedarf eines Shards bestimmt vor allem sein Vorwärtsdurchlauf, jede innere GKR-Schicht als Körperelemente: 8,4 GiB für einen `SHIFT_BITWISE`-Shard der Höhe `2^20`, 42 GiB für einen `KECCAK_F`-Shard der Höhe `2^18`. **`max_in_flight` beschränkt also, wie viele davon zusammenfallen, und die Höhen bestimmen, wie groß jeder ist.**

Gemessen an Block 257.510 (60 Transaktionen, 101,5 Mgas, 198 Mio. Zyklen, 207 Shards) auf 32 vCPUs und 247,7 GiB, mit zwölf gleichzeitig bearbeiteten Shards:

| | |
| --- | --- |
| Durchlauf 1 | 191 s; Befüllungen auf einem einzigen Thread machen 81 % seiner Shard-Sekunden aus; per Stichprobe gemessener Speicher höchstens 15,9 GiB |
| Durchlauf 2 | 2.290 s, mit etwa 12 von 12 gehaltenen Shards und 30,4 ausgelasteten vCPUs bis zum Exit des Gastprogramms, danach eine Schlussphase von 460 s |
| Speicherspitze | 173,92 GiB: die beiden `KECCAK_F`-Shards der Höhe `2^18`, gemeinsam in der Schlussphase, ohne dass sonst etwas in Bearbeitung war |

Die Spitze ergab sich aus der Höhe einer einzigen Delegationsfamilie, nicht aus den zwölf gleichzeitig bearbeiteten Shards. Das ist der Hebel: Ein Block mit weniger Keccak-Aufrufen oder mit Keccak bei geringerer Höhe erreicht eine niedrigere Spitze.

Die Spezifikation: [Der Streaming-Prover](https://apogee.gweb3networks.com/docs/auditors/spec/streaming).
