Das Modell hat Ihre Leaf-Funktion analysiert und liefert ein überzeugendes Urteil: „Das ist ChaCha20, nur die Rundenzahl wurde durch eine aus dem Schlüssel abgeleitete Variable ersetzt.“ Klingt plausibel – und der Impuls ist groß, die Reimplementierung einfach darauf aufzubauen.
Genau hier sollten Sie stoppen. Solange aus dieser Aussage kein ausführbares assert geworden ist, bleibt sie nichts weiter als ein Satz. Ein Modell mag unzählige öffentliche Implementierungen kennen, doch ob es diesen konkreten Fall richtig eingeordnet hat, ist keine Sprachfrage, sondern eine Tatsachenfrage. Beantworten lässt sie sich nur, indem beide Implementierungen mit denselben Eingaben laufen und ihre Ausgaben Byte für Byte verglichen werden.
Dieses Werkzeug heißt differenzieller Test. Es ist Stufe 3 im Vier-Stufen-Workflow und der Endpunkt jeder Hypothese aus der Suche nach Abweichungen. Das Modell liefert einen Verdacht, der differenzielle Test das Urteil.
„Kannst du bestätigen, dass das stimmt?“ führt nicht weiter
Eine typische Einsteigerfrage an ein Modell lautet: „Kannst du bestätigen, dass diese Implementierung korrekt ist?“
Das ist aus drei Gründen eine Sackgasse.
Erstens sind Modelle darauf trainiert, mit dem Nutzer mitzuschwingen. Schon die Formulierung „Ist diese Implementierung korrekt?“ enthält eine Erwartung: Sie soll wohl korrekt sein. Diese Erwartung greift das Modell häufig auf und antwortet entsprechend mit „ja“.
Zweitens fehlt ihm jede belastbare Grundlage für das Urteil. „Korrekt“ lässt sich nur anhand der Ausgaben der Black Box für denselben Eingabesatz bewerten – und diese Referenzdaten liegen nicht im Kontext. Ohne Ground Truth kann das Modell lediglich einschätzen, ob der Code „vernünftig aussieht“. Gerade das ist bei verschleiertem Code aber ein denkbar schlechter Maßstab.
Drittens richten Sie die Frage an die falsche Instanz. Korrektheit ist keine verhandelbare Meinung, sondern die Frage, ob eine Gleichung gilt. Geben Sie diese Entscheidung nicht dem Modell, sondern assert: Sagt das Modell „richtig“, aber der Test schlägt fehl, gewinnt der Test. Sagt das Modell „falsch“, aber alle Tests bestehen, gewinnt ebenfalls der Test. Wer das Urteil dem Modell überlässt, baut auf Halluzinationen.
Grenzfälle wählen, die wirklich etwas zeigen
Der Aufbau eines differenziellen Tests ist simpel: Die Originalimplementierung bleibt eine Black Box, Ihre Neufassung erhält dieselben Eingaben, und beide Ausgaben werden Fall für Fall verglichen. Entscheidend ist nicht die Schleife, sondern die Auswahl der Testdaten.
Zehntausend zufällige, gewöhnliche Strings durchzujagen und nur grüne Tests zu sehen, beweist fast nichts. Normale Eingaben nehmen den Hauptpfad; Abweichungen stecken meist an den Rändern. Aussagekräftig sind Grenzfälle:
# Boundary cases: derived from block size B
def boundary_cases(B: int) -> list[bytes]:
return [
b"", # empty: exposes initial state and padding logic
b"\x00", # single zero byte
b"\xff", # single high byte: checks sign-bit / unsigned handling
b"A" * (B - 1), # one below the block boundary
b"A" * B, # exactly one block
b"A" * (B + 1), # one above: checks carry and padding
bytes(range(256)), # full byte coverage: checks the alphabet map covers the whole domain
]
Jeder dieser Fälle zwingt einen bestimmten Zweig ans Licht. Das hohe Byte \xff legt etwa die Behandlung des Vorzeichenbits offen: Der Unterschied zwischen >>> und >> in JS oder ein fehlendes & 0xff in Python zeigt sich genau hier. Besonders scharf ist das Tripel B-1 / B / B+1, denn Block- und Padding-Logik wird erst an dieser Grenze sichtbar. Die vollständige Byte-Abdeckung eignet sich für eigene Alphabete: Ist die Mapping-Tabelle um ein Zeichen zu lang oder zu kurz, scheitert dieser Fall zuverlässig.
Der differenzielle Test selbst braucht weniger als zehn Zeilen:
# Original as black box, rewrite compared case by case
def diff_test(blackbox, rewritten, B: int) -> None:
for case in boundary_cases(B):
got, want = rewritten(case), blackbox(case)
assert got == want, f"len={len(case)} hex={case.hex()}"
Wenn Ihre Hypothese zu einem Abweichungspunkt einen Standardalgorithmus betrifft, benötigen Sie nicht immer die Original-Black-Box. Öffentliche Standards liefern oft maßgebliche Testvektoren. RFC 8439 §2.1.1 enthält beispielsweise ein festes Eingabe-/Ausgabepaar für die ChaCha20-Quarter-Round: Für a=0x11111111, b=0x01020304, c=0x9b8d6f43, d=0x01234567 erhalten Sie a=0xea2a92f4, …. Lassen Sie Ihre Neufassung zuerst den Standardvektor bestehen und vergleichen Sie sie danach mit der Ziel-Black-Box. So trennen Sie sauber zwei Fehlerklassen: „Meine ChaCha-Implementierung ist fehlerhaft“ und „Das Zielsystem hat ChaCha verändert.“
Beachten Sie, dass die assert-Meldung len(case) enthält. Das ist die nützlichste einzelne Diagnosezeile in der gesamten Suite. Scheitert von sieben Fällen nur B+1, liegt das Problem mit hoher Wahrscheinlichkeit bei Padding oder Carry. Scheitert nur die vollständige Byte-Abdeckung, steckt der Fehler in der Alphabetzuordnung. Die Länge des fehlgeschlagenen Falls weist direkt auf die fehlerhafte Schicht – ohne Rätselraten.
Dieselbe Eingabe zweimal ausführen
Beim Nachbau von Signaturlogik ist der häufigste Stolperstein nicht ein falsch identifizierter Algorithmus. Meist wurde eine Entropiequelle noch nicht aus der Implementierung herausgelöst.
Um das festzustellen, genügt eine Zeile: dieselbe Eingabe zweimal ausführen.
# Same input twice: differing results mean a hidden RNG / timestamp
def assert_deterministic(fn, case: bytes) -> None:
assert fn(case) == fn(case), "unfixed entropy source or timestamp present"
Unterscheiden sich die beiden Ergebnisse, mischt die Implementierung time.time(), eine Nonce, einen hochzählenden Zähler oder einen anderen Wert ein, der sich bei jedem Aufruf ändert. Dann ist ein differenzieller Vergleich noch gar nicht möglich: Die Black Box liefert jedes Mal eine andere Antwort – womit wollten Sie sie vergleichen?
Die Lösung besteht nicht darin, die Entropiequelle zu löschen. Dann wäre die Signatur falsch. Stattdessen ziehen Sie sie aus dem Code in einen injizierbaren Parameter und fixieren ihn während des differenziellen Tests auf einen festen Wert:
# Lift the entropy source into an injectable parameter, pin it with a stub for diffing
class Rewritten:
def __init__(self, clock=time.time, rng=os.urandom):
self._clock = clock # formerly an inline time.time(), now injected
self._rng = rng
def __call__(self, data: bytes) -> bytes:
ts = int(self._clock()) # for diffing, clock=lambda: 0
nonce = self._rng(16) # for diffing, rng=lambda n: b"\x00" * n
...
Mit der Ziel-Black-Box machen Sie dasselbe: Finden Sie die Stelle, an der Zeitstempel oder Zufallswerte eingespeist werden, und frieren Sie sie ein – etwa durch einen Hook für Date.now auf der Seite oder einen festen Seed für Node. Sobald die Entropie auf beiden Seiten fixiert ist, werden die Ausgaben wieder deterministisch und der Vergleich erhält Aussagekraft. Hat die Neufassung den End-to-End-Test bestanden, ersetzen Sie clock und rng wieder durch die echten Implementierungen.
Auch dabei hilft das Modell nicht entscheidend: Wo Entropie verborgen liegt und wie sie eingespeist wird, ist Laufzeitverhalten. Sie decken es auf, indem Sie zweimal ausführen – nicht indem Sie nur Code lesen.
Schichten einzeln prüfen statt nur das Endergebnis
Angenommen, die Gesamtausgabe Ihrer Neufassung stimmt nicht mit der Black Box überein. Starren Sie nicht auf den finalen Byte-String. Er ist das Ergebnis mehrerer ineinander verschachtelter Schichten, und zunächst ist unklar, welche davon fehlerhaft ist.
Eine Signatur ist typischerweise geschichtet: Ganz innen liegen Hash oder Blockchiffre, darum eine Kodierungsschicht wie eine Base64-Variante, Hex oder eine eigene Tabelle, und außen eine Assemblierungsschicht, die etwa Präfixe anfügt, Felder einsetzt oder einen Längen-Header ergänzt. Arbeiten Sie sich von innen nach außen vor und wechseln Sie erst zur nächsten Schicht, wenn die aktuelle besteht:
# Peel layer by layer: match the innermost first, then move outward
LAYERS = ["digest", "encode", "assemble"] # inner → outer
def first_divergent_layer(bb_dump, rw_dump, case: bytes) -> str | None:
for layer in LAYERS:
if bb_dump(case)[layer] != rw_dump(case)[layer]:
return layer # the first layer to diverge is the faulty one
return None
Das setzt voraus, dass Ihre Neufassung Zwischenstände jeder Schicht ausgeben kann und Sie die entsprechenden Werte auch aus der Black Box herausbekommen – meist durch Runtime-Instrumentierung. Der Zusatzaufwand lohnt sich: Die erste abweichende Schicht ist die fehlerhafte, und Ihr Fokus verengt sich sofort.
Das greift direkt die vier Kategorien von Abweichungspunkten auf: Weicht die Digest-Schicht ab, wurden meist Konstanten verändert oder die Rundenzahl angepasst. In der Kodierungsschicht ist häufig das Alphabet umsortiert. In der Assemblierungsschicht wurde meist Material in die Ausgabe eingebettet. Die abweichende Schicht zeigt Ihnen, in welcher Kategorie des Fingerprinting-Artikels Sie suchen müssen.
Fixtures werden mit der Zeit wertvoller
Wenn ein differenzieller Test grün wird, ist das ein gutes Gefühl. Doch zunächst ist es nur eine Momentaufnahme: Morgen kann Upstream eine neue Version veröffentlichen, und die heute validierte Implementierung liegt komplett daneben.
Der Artefaktwert, der mit der Zeit wächst, ist das Fixture: eine im Repository versionierte Tabelle bekannter Eingaben und Ausgaben.
# Fixture: known input → known output, committed to the repo
# Re-run on every upstream change; green yesterday, red today = upstream changed, not you
VECTORS = load_json("vectors.json") # [{"in": "<hex>", "out": "<hex>"}, ...]
for v in VECTORS:
got = rewritten(bytes.fromhex(v["in"])).hex()
assert got == v["out"], v["in"]
Der Nutzen zeigt sich, sobald sich Upstream verändert. Eines Tages wird CI rot, und eine Implementierung, die Sie gestern nicht angefasst haben, scheitert gegen das Fixture. Diese Information ist enorm wertvoll: Sie schließt „Ich habe einen Fehler gemacht“ aus und weist eindeutig auf „Upstream hat sich geändert“. Ohne Fixture verbringen Sie leicht einen halben Tag damit, korrekten Code zu debuggen, weil unklar bleibt, ob Ihr Code fehlerhaft ist oder sich das Ziel verschoben hat.
Für Fixtures eignen sich exakt die zuvor genannten Grenzfälle. Sie liefern bereits die höchste Abdeckung, die Sie haben.
In diesem Schritt hat das Modell genau zwei Aufgaben
Die Arbeitsteilung beim differenziellen Test ist klar: Das Modell übernimmt zwei Aufgaben – und hat beim Urteil kein Mitspracherecht.
Erstens: Testfälle erzeugen. Grenz- und Kontrollfälle für viele Primitive in großer Menge aufzustellen oder eine Serie von Eingaben zu erzeugen, die sich jeweils nur um ein Bit unterscheiden, um eine verdächtige Konstante zu untersuchen: Das ist Aufzählungsarbeit, keine anspruchsvolle Schlussfolgerung. Dafür ist ein günstiges Modell mit hoher Parallelität die wirtschaftlichste Wahl.
Zweitens: einen Diff erklären. Schlägt ein Fall fehl, geben Sie dem Modell die beiden Schicht-Dumps und lassen es anhand konkreter Bytes erklären, wo die erste Abweichung liegt und zu welcher der vier Abweichungskategorien sie wahrscheinlich gehört. Dafür braucht es mittlere Reasoning-Fähigkeiten und die Fähigkeit, „anhand der Bytes zu erklären“. Hier leistet das Modell in diesem Workflow tatsächlich Arbeit.
Das Urteil bleibt bei assert. Daran ändert sich nichts. Die Erklärung des Modells ist ein Hinweis, keine Schlussfolgerung. Hinweise, die in die falsche Richtung zeigen, sind normal – assert fängt sie ab.
Beide Aufgaben verlangen unterschiedliche Fähigkeiten. Für alles dieselbe Modellklasse zu verwenden, verschwendet entweder Geld oder Präzision:
Teilaufgabe beim differenziellen Testen | Erforderliche Fähigkeit | Empfehlung | model id |
|---|---|---|---|
Grenz- und Kontrollfälle in großen Mengen erzeugen (über viele Primitive hinweg) | Günstig, hohe Parallelität; Aufzählung benötigt kein Reasoning | Claude Sonnet 5 |
|
Einen einzelnen fehlgeschlagenen Diff lesen und anhand der Bytes erklären | Mittleres Reasoning; Zuordnung zu einer konkreten Schicht | GPT-5.6 Sol |
|
Tiefe Ursachen analysieren, wenn die Zuordnung nicht konvergiert (etwa bei veränderten Konstanten) | Starkes Reasoning; Schlussfolgerungen über Zwischenzustände mehrerer Runden hinweg | Claude Opus 5 |
|
Viele Schicht-Dumps oder ein komplettes Fixture-Batch verarbeiten, um die Abweichung zu finden | Langer Kontext | Kimi K3 |
|
Die zweite Stufe ist das Arbeitspferd. Ob sich die Wahl eines Modells speziell für die Differenzzuordnung lohnt, sehen Sie bereits nach einer eigenen Testrunde. Das Protokoll ist kurz:
Wählen Sie einen tatsächlich fehlgeschlagenen Fall aus Ihrer differenziellen Suite mitsamt den beiden Schicht-Dumps: dem der Black Box und dem Ihrer Neufassung.
Geben Sie denselben Diff getrennt an
gpt-5.6-solundclaude-opus-5. Fragen Sie nur nach zwei Dingen: In welcher Schicht liegt die erste Abweichung, und welche der vier Abweichungskategorien ist am wahrscheinlichsten?Bewerten Sie ausschließlich, ob die Zuordnung bei einem konkreten Byte und einer konkreten Schicht landet oder Ihnen nur ein vages „könnte ein Padding-Problem sein“ liefert.
Die Präzision der Zuordnung ist Ihr Auswahlkriterium. Sie bestimmt unmittelbar, wie viele Änderungsrunden nötig sind, bis dieser Fall grün wird.
Eine einzige Runde zeigt den Unterschied direkter als jedes Benchmark-Ranking.
Der Modellwechsel ist die eigentliche Hürde
Diese vier Stufen stammen von drei Anbietern: drei SDKs, drei Authentifizierungsschemata, drei Fehlerformate. Den Client dreimal anzubinden, nur um zwischen Teilaufgaben die Modellklasse zu wechseln, lohnt sich kaum. Genau deshalb verwenden viele am Ende eine einzige Stufe für alles, bekommen bei der Differenzzuordnung nur vage Aussagen und durchlaufen mehrere Änderungsrunden, ohne zu verstehen, warum.
AIReiter vereinheitlicht diese Ebene: ein Schlüssel, eine OpenAI-kompatible Schnittstelle, alle vier Stufen dahinter. Zum Wechseln ändern Sie lediglich das Feld model im Request-Body.
# Difference attribution: the mid-reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-sol",
"messages": [{"role": "user", "content": "<diff of the two layer dumps + locate the first divergent layer and deviation category>"}]
}'
# Attribution won't converge, escalate to dig the root cause: change one field
# "model": "claude-opus-5"
# Bulk-generate boundary cases:
# "model": "claude-sonnet-5"
Sie nutzen bereits das OpenAI SDK? Setzen Sie base_url auf https://aireiter.com/api/v1; mehr müssen Sie nicht ändern. Beim Anthropic SDK verwenden Sie mit demselben Schlüssel POST /api/v1/messages.
Preislich liegen Claude-Modelle 30 % unter dem Listenpreis, GPT-Modelle bei der Hälfte des Preises. In diesem Workflow trifft der Rabatt genau den dichtesten Arbeitsschritt: Die Differenzzuordnung ist der am häufigsten aufgerufene Teil des differenziellen Testens – eine Runde pro fehlgeschlagenem Fall; jede Upstream-Änderung bedeutet außerdem, Fixtures neu aufzubauen und einen neuen Satz fehlgeschlagener Fälle zuzuordnen. Das Arbeitspferd gpt-5.6-sol ist ein GPT-Modell zum halben Preis, wodurch sich der intensive Teil direkt halbiert. Die gelegentliche Eskalation zu claude-opus-5, um eine tiefe Ursache zu analysieren, umfasst zwar nur wenige Aufrufe, profitiert aber ebenfalls vom 30-%-Rabatt für Claude-Modelle.
Ohne Registrierung ausprobieren – geben Sie zunächst einen echten Diff manuell an beide Stufen, vergleichen Sie die Präzision der Zuordnung selbst und entscheiden Sie danach, was Sie anbinden.
Fazit
Reverse-Engineering-Neufassungen ruhen im Kern auf nur zwei Säulen: Das Modell liefert die Hypothese, assert das Urteil.
Ein Modell ist ein Generator für Verdachtsmomente, der jede öffentliche Implementierung gesehen haben mag. Es kann Ihnen in Sekunden sagen: „Hier könnte etwas verändert worden sein.“ Ob das diesmal stimmt, weiß es jedoch nie. Der differenzielle Test macht aus „könnte“ ein „ist“ oder „ist nicht“: Grenzfälle erzwingen die relevanten Zweige, zwei identische Läufe decken Entropiequellen auf, die Schichtanalyse isoliert die fehlerhafte Ebene, und das Fixture trennt „Ich habe es falsch implementiert“ von „Sie haben es geändert“.
Jedes Urteil aus dem Vier-Stufen-Workflow und dem Artikel zur Suche nach Abweichungen muss am Ende durch dieses Tor. Was das Modell sagt, zählt nicht. Es zählt allein, was assert sagt.
