Zum Hauptinhalt springen

Ein App-spezifisches Plasma schreiben, das die Privatsphäre wahrt

Zero-Knowledge
Server
offchain
Privatsphäre
Experte
Ori Pomerantz
15. Oktober 2025
32 Minuten Lesezeit

Einführung

Im Gegensatz zu Rollups nutzen Plasmas das Ethereum Mainnet für die Integrität, aber nicht für die Verfügbarkeit. In diesem Artikel schreiben wir eine Anwendung, die sich wie ein Plasma verhält, wobei Ethereum die Integrität garantiert (keine unautorisierten Änderungen), aber nicht die Verfügbarkeit (eine zentralisierte Komponente kann ausfallen und das gesamte System lahmlegen).

Die Anwendung, die wir hier schreiben, ist eine Bank, die die Privatsphäre wahrt. Verschiedene Adressen haben Konten mit Guthaben, und sie können Geld (ETH) an andere Konten senden. Die Bank veröffentlicht Hashes des Zustands (Konten und deren Guthaben) und der Transaktionen, behält aber die tatsächlichen Guthaben offchain, wo sie privat bleiben können.

Design

Dies ist kein produktionsreifes System, sondern ein Lehrmittel. Daher wurde es mit einigen vereinfachenden Annahmen geschrieben.

  • Fester Kontenpool. Es gibt eine bestimmte Anzahl von Konten, und jedes Konto gehört zu einer vorgegebenen Adresse. Dies macht das System viel einfacher, da es schwierig ist, Datenstrukturen variabler Größe in Zero-Knowledge-Beweisen zu handhaben. Für ein produktionsreifes System können wir die Merkle-Wurzel als Zustands-Hash verwenden und Merkle-Beweise für die erforderlichen Guthaben bereitstellen.

  • Speicherung im Arbeitsspeicher. In einem Produktionssystem müssen wir alle Kontostände auf die Festplatte schreiben, um sie im Falle eines Neustarts zu erhalten. Hier ist es in Ordnung, wenn die Informationen einfach verloren gehen.

  • Nur Transfers. Ein Produktionssystem würde eine Möglichkeit erfordern, Vermögenswerte bei der Bank einzuzahlen und abzuheben. Aber der Zweck hier ist nur, das Konzept zu veranschaulichen, daher ist diese Bank auf Transfers beschränkt.

Zero-Knowledge-Beweise

Auf einer grundlegenden Ebene zeigt ein Zero-Knowledge-Beweis, dass der Beweiser einige Daten, Dataprivate, kennt, sodass eine Beziehung Relationship zwischen einigen öffentlichen Daten, Datapublic, und Dataprivate besteht. Der Verifizierer kennt Relationship und Datapublic.

Um die Privatsphäre zu wahren, müssen die Zustände und die Transaktionen privat sein. Um jedoch die Integrität zu gewährleisten, muss der kryptografische Hash (opens in a new tab) der Zustände öffentlich sein. Um den Personen, die Transaktionen einreichen, zu beweisen, dass diese Transaktionen wirklich stattgefunden haben, müssen wir auch Transaktions-Hashes veröffentlichen.

In den meisten Fällen ist Dataprivate die Eingabe für das Zero-Knowledge-Beweis-Programm und Datapublic die Ausgabe.

Diese Felder in Dataprivate:

  • Staten, der alte Zustand
  • Staten+1, der neue Zustand
  • Transaction, eine Transaktion, die vom alten Zustand in den neuen wechselt. Diese Transaktion muss folgende Felder enthalten:
    • Zieladresse, die den Transfer empfängt
    • Betrag, der transferiert wird
    • Nonce, um sicherzustellen, dass jede Transaktion nur einmal verarbeitet werden kann. Die Quelladresse muss nicht in der Transaktion enthalten sein, da sie aus der Signatur wiederhergestellt werden kann.
  • Signature, eine Signatur, die autorisiert ist, die Transaktion durchzuführen. In unserem Fall ist die einzige Adresse, die zur Durchführung einer Transaktion autorisiert ist, die Quelladresse. Da unser Zero-Knowledge-System so funktioniert, wie es funktioniert, benötigen wir zusätzlich zur Ethereum-Signatur auch den öffentlichen Schlüssel des Kontos.

Dies sind die Felder in Datapublic:

  • Hash(Staten), der Hash des alten Zustands
  • Hash(Staten+1), der Hash des neuen Zustands
  • Hash(Transaction), der Hash der Transaktion, die den Zustand von Staten zu Staten+1 ändert.

Die Beziehung überprüft mehrere Bedingungen:

  • Die öffentlichen Hashes sind tatsächlich die korrekten Hashes für die privaten Felder.
  • Die Transaktion führt, wenn sie auf den alten Zustand angewendet wird, zum neuen Zustand.
  • Die Signatur stammt von der Quelladresse der Transaktion.

Aufgrund der Eigenschaften kryptografischer Hashfunktionen reicht der Beweis dieser Bedingungen aus, um die Integrität sicherzustellen.

Datenstrukturen

Die primäre Datenstruktur ist der Zustand, der vom Server gehalten wird. Für jedes Konto verfolgt der Server den Kontostand und eine Nonce (opens in a new tab), die verwendet wird, um Replay-Angriffe (opens in a new tab) zu verhindern.

Komponenten

Dieses System erfordert zwei Komponenten:

  • Der Server, der Transaktionen empfängt, sie verarbeitet und Hashes zusammen mit den Zero-Knowledge-Beweisen auf der Chain veröffentlicht.
  • Ein Smart Contract, der die Hashes speichert und die Zero-Knowledge-Beweise verifiziert, um sicherzustellen, dass Zustandsübergänge legitim sind.

Daten- und Kontrollfluss

Auf diese Weise kommunizieren die verschiedenen Komponenten, um von einem Konto auf ein anderes zu transferieren.

  1. Ein Webbrowser reicht eine signierte Transaktion ein, die um einen Transfer vom Konto des Unterzeichners auf ein anderes Konto bittet.

  2. Der Server verifiziert, dass die Transaktion gültig ist:

    • Der Unterzeichner hat ein Konto bei der Bank mit ausreichendem Guthaben.
    • Der Empfänger hat ein Konto bei der Bank.
  3. Der Server berechnet den neuen Zustand, indem er den transferierten Betrag vom Guthaben des Unterzeichners abzieht und ihn dem Guthaben des Empfängers hinzufügt.

  4. Der Server berechnet einen Zero-Knowledge-Beweis, dass die Zustandsänderung gültig ist.

  5. Der Server reicht bei Ethereum eine Transaktion ein, die Folgendes enthält:

    • Den neuen Zustands-Hash
    • Den Transaktions-Hash (damit der Sender der Transaktion weiß, dass sie verarbeitet wurde)
    • Den Zero-Knowledge-Beweis, der beweist, dass der Übergang in den neuen Zustand gültig ist
  6. Der Smart Contract verifiziert den Zero-Knowledge-Beweis.

  7. Wenn der Zero-Knowledge-Beweis erfolgreich ist, führt der Smart Contract diese Aktionen aus:

    • Aktualisierung des aktuellen Zustands-Hashes auf den neuen Zustands-Hash
    • Ausgabe eines Log-Eintrags mit dem neuen Zustands-Hash und dem Transaktions-Hash

Werkzeuge

Für den clientseitigen Code werden wir Vite (opens in a new tab), React (opens in a new tab), Viem (opens in a new tab) und Wagmi (opens in a new tab) verwenden. Dies sind branchenübliche Werkzeuge; wenn Sie nicht mit ihnen vertraut sind, können Sie dieses Tutorial nutzen.

Der Großteil des Servers ist in JavaScript unter Verwendung von Node (opens in a new tab) geschrieben. Der Zero-Knowledge-Teil ist in Noir (opens in a new tab) geschrieben. Wir benötigen Version 1.0.0-beta.10, also führen Sie, nachdem Sie Noir wie angewiesen installiert haben (opens in a new tab), Folgendes aus:

noirup -v 1.0.0-beta.10

Die Blockchain, die wir verwenden, ist anvil, eine lokale Test-Blockchain, die Teil von Foundry (opens in a new tab) ist.

Implementierung

Da dies ein komplexes System ist, werden wir es in Phasen implementieren.

Phase 1 - Manuelles Zero-Knowledge

Für die erste Phase werden wir eine Transaktion im Browser signieren und die Informationen dann manuell für den Zero-Knowledge-Beweis bereitstellen. Der Zero-Knowledge-Code erwartet diese Informationen in server/noir/Prover.toml (dokumentiert hier (opens in a new tab)).

Um es in Aktion zu sehen:

  1. Stellen Sie sicher, dass Sie Node (opens in a new tab) und Noir (opens in a new tab) installiert haben. Installieren Sie diese vorzugsweise auf einem UNIX-System wie macOS, Linux oder WSL (opens in a new tab).

  2. Laden Sie den Code für Phase 1 herunter und starten Sie den Webserver, um den Client-Code bereitzustellen.

    git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk
    cd 250911-zk-bank
    cd client
    npm install
    npm run dev
    

    Der Grund, warum Sie hier einen Webserver benötigen, ist, dass viele Wallets (wie MetaMask) zur Verhinderung bestimmter Arten von Betrug keine Dateien akzeptieren, die direkt von der Festplatte bereitgestellt werden.

  3. Öffnen Sie einen Browser mit einer Wallet.

  4. Geben Sie in der Wallet eine neue Passphrase ein. Beachten Sie, dass dadurch Ihre bestehende Passphrase gelöscht wird, stellen Sie also sicher, dass Sie ein Backup haben.

    Die Passphrase lautet test test test test test test test test test test test junk, die Standard-Test-Passphrase für Anvil.

  5. Navigieren Sie zu dem clientseitigen Code (opens in a new tab).

  6. Verbinden Sie sich mit der Wallet und wählen Sie Ihr Zielkonto und den Betrag aus.

  7. Klicken Sie auf Sign und signieren Sie die Transaktion.

  8. Unter der Überschrift Prover.toml finden Sie Text. Ersetzen Sie server/noir/Prover.toml durch diesen Text.

  9. Führen Sie den Zero-Knowledge-Beweis aus.

    cd ../server/noir
    nargo execute
    

    Die Ausgabe sollte ähnlich aussehen wie

ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute

[zkBank] Circuit witness successfully solved [zkBank] Witness saved to target/zkBank.gz [zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85)

Die Nachricht liegt im Textformat vor, was es dem Benutzer leicht macht, sie zu verstehen (was beim Signieren notwendig ist), und dem Noir-Code, sie zu parsen. Der Betrag wird in Finney angegeben, um einerseits gebrochene Transfers zu ermöglichen und andererseits leicht lesbar zu sein. Die letzte Zahl ist die Nonce (opens in a new tab).

Der String ist 100 Zeichen lang. Zero-Knowledge-Beweise können mit Daten variabler Größe nicht gut umgehen, daher ist es oft notwendig, Daten aufzufüllen (Padding).

pubKeyX=["0x83",...,"0x75"]
pubKeyY=["0x35",...,"0xa5"]
signature=["0xb1",...,"0x0d"]

Diese drei Parameter sind Byte-Arrays mit fester Größe.

Dies ist die Methode, um ein Array von Strukturen anzugeben. Für jeden Eintrag geben wir die Adresse, den Kontostand (in milliETH, auch bekannt als Finney (opens in a new tab)) und den nächsten Nonce-Wert an.

client/src/Transfer.tsx

Diese Datei (opens in a new tab) implementiert die clientseitige Verarbeitung und generiert die Datei server/noir/Prover.toml (diejenige, die die Zero-Knowledge-Parameter enthält).

Hier ist die Erklärung der interessanteren Teile.

export default attrs =>  {

Diese Funktion erstellt die React-Komponente Transfer, die von anderen Dateien importiert werden kann.

  const accounts = [
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
    "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
    "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
  ]

Dies sind die Kontoadressen, die durch die Passphrase test ... test junk erstellt wurden. Wenn Sie Ihre eigenen Adressen verwenden möchten, ändern Sie einfach diese Definition.

  const account = useAccount()
  const wallet = createWalletClient({
    transport: custom(window.ethereum!)
  })

Diese Wagmi-Hooks (opens in a new tab) ermöglichen uns den Zugriff auf die Viem (opens in a new tab)-Bibliothek und die Wallet.

  const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")

Dies ist die Nachricht, aufgefüllt mit Leerzeichen. Jedes Mal, wenn sich eine der Variablen useState (opens in a new tab) ändert, wird die Komponente neu gezeichnet und message wird aktualisiert.

  const sign = async () => {

Diese Funktion wird aufgerufen, wenn der Benutzer auf die Schaltfläche Sign klickt. Die Nachricht wird automatisch aktualisiert, aber die Signatur erfordert die Zustimmung des Benutzers in der Wallet, und wir möchten diese nicht anfordern, es sei denn, es ist erforderlich.

    const signature = await wallet.signMessage({
        account: fromAccount,
        message,
    })

Bitten Sie die Wallet, die Nachricht zu signieren (opens in a new tab).

    const hash = hashMessage(message)

Holen Sie sich den Nachrichten-Hash. Es ist hilfreich, ihn dem Benutzer zum Debuggen (des Noir-Codes) zur Verfügung zu stellen.

    const pubKey = await recoverPublicKey({
        hash,
        signature
    })

Holen Sie sich den öffentlichen Schlüssel (opens in a new tab). Dies ist für die Funktion Noir ecrecover (opens in a new tab) erforderlich.

    setSignature(signature)
    setHash(hash)
    setPubKey(pubKey)

Setzen Sie die Zustandsvariablen. Dadurch wird die Komponente neu gezeichnet (nachdem die Funktion sign beendet ist) und dem Benutzer werden die aktualisierten Werte angezeigt.

    let proverToml = `

Der Text für Prover.toml.

message="${message}"

pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
pubKeyY=${hexToArray(pubKey.slice(4+2*32))}

Viem stellt uns den öffentlichen Schlüssel als 65-Byte-Hexadezimalstring zur Verfügung. Das erste Byte ist 0x04, eine Versionsmarkierung. Darauf folgen 32 Bytes für das x des öffentlichen Schlüssels und dann 32 Bytes für das y des öffentlichen Schlüssels.

Noir erwartet diese Informationen jedoch als zwei Byte-Arrays, eines für x und eines für y. Es ist einfacher, dies hier auf dem Client zu parsen, anstatt als Teil des Zero-Knowledge-Beweises.

Beachten Sie, dass dies im Allgemeinen eine gute Praxis bei Zero-Knowledge ist. Code innerhalb eines Zero-Knowledge-Beweises ist teuer, daher sollte jede Verarbeitung, die außerhalb des Zero-Knowledge-Beweises durchgeführt werden kann, auch außerhalb des Zero-Knowledge-Beweises erfolgen.

signature=${hexToArray(signature.slice(2,-2))}

Die Signatur wird ebenfalls als 65-Byte-Hexadezimalstring bereitgestellt. Das letzte Byte ist jedoch nur erforderlich, um den öffentlichen Schlüssel wiederherzustellen. Da der öffentliche Schlüssel dem Noir-Code bereits zur Verfügung gestellt wird, benötigen wir ihn nicht zur Verifizierung der Signatur, und der Noir-Code erfordert ihn nicht.

${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}
`

Stellen Sie die Konten bereit.

    setProverToml(proverToml)
  }

  return (
    <>
        <h2>Transfer</h2>

Dies ist das HTML-Format (genauer gesagt JSX (opens in a new tab)) der Komponente.

server/noir/src/main.nr

Diese Datei (opens in a new tab) ist der eigentliche Zero-Knowledge-Code.

use std::hash::pedersen_hash;

Der Pedersen-Hash (opens in a new tab) wird mit der Noir-Standardbibliothek (opens in a new tab) bereitgestellt. Zero-Knowledge-Beweise verwenden diese Hashfunktion häufig. Sie ist innerhalb von arithmetischen Schaltkreisen (opens in a new tab) im Vergleich zu Standard-Hashfunktionen viel einfacher zu berechnen.

use keccak256::keccak256;
use dep::ecrecover;

Diese beiden Funktionen sind externe Bibliotheken, die in Nargo.toml (opens in a new tab) definiert sind. Sie tun genau das, wonach sie benannt sind: eine Funktion, die den Keccak-256-Hash (opens in a new tab) berechnet, und eine Funktion, die Ethereum-Signaturen verifiziert und die Ethereum-Adresse des Unterzeichners wiederherstellt.

global ACCOUNT_NUMBER : u32 = 5;

Noir ist von Rust (opens in a new tab) inspiriert. Variablen sind standardmäßig Konstanten. Auf diese Weise definieren wir globale Konfigurationskonstanten. Insbesondere ist ACCOUNT_NUMBER die Anzahl der Konten, die wir speichern.

Datentypen mit dem Namen u<number> haben diese Anzahl von Bits, vorzeichenlos. Die einzigen unterstützten Typen sind u8, u16, u32, u64 und u128.

global FLAT_ACCOUNT_FIELDS : u32 = 2;

Diese Variable wird für den Pedersen-Hash der Konten verwendet, wie unten erklärt.

global MESSAGE_LENGTH : u32 = 100;

Wie oben erklärt, ist die Nachrichtenlänge fest. Sie wird hier angegeben.

global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;

EIP-191-Signaturen (opens in a new tab) erfordern einen Puffer mit einem 26-Byte-Präfix, gefolgt von der Nachrichtenlänge in ASCII und schließlich der Nachricht selbst.

struct Account {
    balance: u128,
    address: Field,
    nonce: u32,
}

Die Informationen, die wir über ein Konto speichern. Field (opens in a new tab) ist eine Zahl, typischerweise bis zu 253 Bits, die direkt in dem arithmetischen Schaltkreis (opens in a new tab) verwendet werden kann, der den Zero-Knowledge-Beweis implementiert. Hier verwenden wir das Field, um eine 160-Bit-Ethereum-Adresse zu speichern.

struct TransferTxn {
    from: Field,
    to: Field,
    amount: u128,
    nonce: u32
}

Die Informationen, die wir für eine Transfer-Transaktion speichern.

fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {

Eine Funktionsdefinition. Der Parameter sind Account-Informationen. Das Ergebnis ist ein Array von Field-Variablen, dessen Länge FLAT_ACCOUNT_FIELDS beträgt.

let flat = [
        account.address,
        ((account.balance << 32) + account.nonce.into()).into(),
    ];

Der erste Wert im Array ist die Kontoadresse. Der zweite enthält sowohl den Kontostand als auch die Nonce. Die .into()-Aufrufe ändern eine Zahl in den Datentyp, den sie haben muss. account.nonce ist ein u32-Wert, aber um ihn zu account.balance << 32, einem u128-Wert, hinzuzufügen, muss er ein u128 sein. Das ist das erste .into(). Das zweite konvertiert das u128-Ergebnis in ein Field, damit es in das Array passt.

flat
}

In Noir können Funktionen nur am Ende einen Wert zurückgeben (es gibt kein vorzeitiges Zurückgeben). Um den Rückgabewert anzugeben, werten Sie ihn direkt vor der schließenden Klammer der Funktion aus.

fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {

Diese Funktion wandelt das Konten-Array in ein Field-Array um, das als Eingabe für einen Pedersen-Hash verwendet werden kann.

let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];

So geben Sie eine veränderbare Variable an, also keine Konstante. Variablen in Noir müssen immer einen Wert haben, daher initialisieren wir diese Variable mit lauter Nullen.

for i in 0..ACCOUNT_NUMBER {

Dies ist eine for-Schleife. Beachten Sie, dass die Grenzen Konstanten sind. Bei Noir-Schleifen müssen die Grenzen zur Kompilierzeit bekannt sein. Der Grund dafür ist, dass arithmetische Schaltkreise keine Flusskontrolle unterstützen. Bei der Verarbeitung einer for-Schleife fügt der Compiler den darin enthaltenen Code einfach mehrmals ein, einmal für jede Iteration.

Schließlich sind wir bei der Funktion angelangt, die das Konten-Array hasht.

fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {
    let mut account : u32 = ACCOUNT_NUMBER;

    for i in 0..ACCOUNT_NUMBER {
        if accounts[i].address == address {
            account = i;
        }
    }

Diese Funktion findet das Konto mit einer bestimmten Adresse. Diese Funktion wäre in Standardcode furchtbar ineffizient, da sie über alle Konten iteriert, selbst nachdem sie die Adresse gefunden hat.

In Zero-Knowledge-Beweisen gibt es jedoch keine Flusskontrolle. Wenn wir jemals eine Bedingung überprüfen müssen, müssen wir sie jedes Mal überprüfen.

Ähnliches passiert mit if-Anweisungen. Die if-Anweisung in der obigen Schleife wird in diese mathematischen Anweisungen übersetzt.

conditionresult = accounts[i].address == address // eins, wenn sie gleich sind, andernfalls null

accountnew = conditionresult*i + (1-conditionresult)*accountold

    assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");

    account
}

Die Funktion assert (opens in a new tab) führt dazu, dass der Zero-Knowledge-Beweis abstürzt, wenn die Behauptung falsch ist. In diesem Fall, wenn wir kein Konto mit der entsprechenden Adresse finden können. Um die Adresse zu melden, verwenden wir einen Format-String (opens in a new tab).

fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {

Diese Funktion wendet eine Transfer-Transaktion an und gibt das neue Konten-Array zurück.

    let from = find_account(accounts, txn.from);
    let to = find_account(accounts, txn.to);

    let (txnFrom, txnAmount, txnNonce, accountNonce) =
        (txn.from, txn.amount, txn.nonce, accounts[from].nonce);

Wir können in Noir nicht auf Strukturelemente innerhalb eines Format-Strings zugreifen, daher erstellen wir eine nutzbare Kopie.

    assert (accounts[from].balance >= txn.amount,
        f"{txnFrom} does not have {txnAmount} finney");

    assert (accounts[from].nonce == txn.nonce,
        f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}");

Dies sind zwei Bedingungen, die eine Transaktion ungültig machen könnten.

    let mut newAccounts = accounts;

    newAccounts[from].balance -= txn.amount;
    newAccounts[from].nonce += 1;
    newAccounts[to].balance += txn.amount;

    newAccounts
}

Erstellen Sie das neue Konten-Array und geben Sie es dann zurück.

fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field

Diese Funktion liest die Adresse aus der Nachricht.

{
    let mut result : Field = 0;

    for i in 7..47 {

Die Adresse ist immer 20 Bytes (also 40 hexadezimale Ziffern) lang und beginnt bei Zeichen #7.

Lesen Sie den Betrag und die Nonce aus der Nachricht.

{
    let mut amount : u128 = 0;
    let mut nonce: u32 = 0;
    let mut stillReadingAmount: bool = true;
    let mut lookingForNonce: bool = false;
    let mut stillReadingNonce: bool = false;

In der Nachricht ist die erste Zahl nach der Adresse der Betrag in Finney (also ein Tausendstel eines ETH), der transferiert werden soll. Die zweite Zahl ist die Nonce. Jeglicher Text dazwischen wird ignoriert.

Die Rückgabe eines Tupels (opens in a new tab) ist die Noir-Methode, um mehrere Werte aus einer Funktion zurückzugeben.

Diese Funktion konvertiert die Nachricht in Bytes und konvertiert dann die Beträge in ein TransferTxn.

// Das Äquivalent zu Viems hashMessage
// https://viem.sh/docs/utilities/hashMessage#hashmessage
fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {

Wir konnten den Pedersen-Hash für die Konten verwenden, da sie nur innerhalb des Zero-Knowledge-Beweises gehasht werden. In diesem Code müssen wir jedoch die Signatur der Nachricht überprüfen, die vom Browser generiert wird. Dafür müssen wir dem Ethereum-Signaturformat in EIP-191 (opens in a new tab) folgen. Das bedeutet, dass wir einen kombinierten Puffer mit einem Standardpräfix, der Nachrichtenlänge in ASCII und der Nachricht selbst erstellen und den Ethereum-Standard Keccak-256 verwenden müssen, um ihn zu hashen.

Um Fälle zu vermeiden, in denen eine Anwendung den Benutzer auffordert, eine Nachricht zu signieren, die als Transaktion oder für einen anderen Zweck verwendet werden kann, legt EIP-191 fest, dass alle signierten Nachrichten mit dem Zeichen 0x19 (kein gültiges ASCII-Zeichen) beginnen, gefolgt von Ethereum Signed Message: und einem Zeilenumbruch.

Behandeln Sie Nachrichtenlängen bis zu 999 und schlagen Sie fehl, wenn sie größer sind. Ich habe diesen Code hinzugefügt, obwohl die Nachrichtenlänge eine Konstante ist, weil es so einfacher ist, sie zu ändern. Auf einem Produktionssystem würden Sie wahrscheinlich einfach davon ausgehen, dass sich MESSAGE_LENGTH aus Gründen der besseren Leistung nicht ändert.

    keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
}

Verwenden Sie die Ethereum-Standardfunktion keccak256.

fn signatureToAddressAndHash(
        message: str<MESSAGE_LENGTH>, 
        pubKeyX: [u8; 32],
        pubKeyY: [u8; 32],
        signature: [u8; 64]
    ) -> (Field, Field, Field)   // Adresse, erste 16 Bytes des Hashs, letzte 16 Bytes des Hashs        
{

Diese Funktion verifiziert die Signatur, was den Nachrichten-Hash erfordert. Sie liefert uns dann die Adresse, die sie signiert hat, und den Nachrichten-Hash. Der Nachrichten-Hash wird in zwei Field-Werten geliefert, da diese im Rest des Programms einfacher zu verwenden sind als ein Byte-Array.

Wir müssen zwei Field-Werte verwenden, da Feldberechnungen modulo (opens in a new tab) einer großen Zahl durchgeführt werden, diese Zahl jedoch typischerweise weniger als 256 Bits beträgt (andernfalls wäre es schwierig, diese Berechnungen in der EVM durchzuführen).

    let hash = hashMessage(message);

    let mut (hash1, hash2) = (0,0);

    for i in 0..16 {
        hash1 = hash1*256 + hash[31-i].into();
        hash2 = hash2*256 + hash[15-i].into();
    }

Geben Sie hash1 und hash2 als veränderbare Variablen an und schreiben Sie den Hash Byte für Byte in sie hinein.

    (
        ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash), 

Dies ist ähnlich wie Soliditys ecrecover (opens in a new tab), mit zwei wichtigen Unterschieden:

  • Wenn die Signatur nicht gültig ist, schlägt der Aufruf bei einem assert fehl und das Programm wird abgebrochen.
  • Während der öffentliche Schlüssel aus der Signatur und dem Hash wiederhergestellt werden kann, ist dies eine Verarbeitung, die extern durchgeführt werden kann und sich daher nicht lohnt, innerhalb des Zero-Knowledge-Beweises durchgeführt zu werden. Wenn jemand versucht, uns hier zu betrügen, wird die Signaturverifizierung fehlschlagen.

Schließlich erreichen wir die Funktion main. Wir müssen beweisen, dass wir eine Transaktion haben, die den Hash der Konten gültig vom alten Wert auf den neuen ändert. Wir müssen auch beweisen, dass sie diesen spezifischen Transaktions-Hash hat, damit die Person, die sie gesendet hat, weiß, dass ihre Transaktion verarbeitet wurde.

{
    let mut txn = readTransferTxn(message);

Wir benötigen, dass txn veränderbar ist, da wir die Absenderadresse nicht aus der Nachricht lesen, sondern aus der Signatur.

Phase 2 - Hinzufügen eines Servers

In der zweiten Phase fügen wir einen Server hinzu, der Transfer-Transaktionen vom Browser empfängt und implementiert.

Um es in Aktion zu sehen:

  1. Stoppen Sie Vite, falls es läuft.

  2. Laden Sie den Branch herunter, der den Server enthält, und stellen Sie sicher, dass Sie alle erforderlichen Module haben.

    git checkout 02-add-server
    cd client
    npm install
    cd ../server
    npm install
    

    Es ist nicht nötig, den Noir-Code zu kompilieren, es ist derselbe Code, den Sie für Phase 1 verwendet haben.

  3. Starten Sie den Server.

    npm run start
    
  4. Führen Sie in einem separaten Befehlszeilenfenster Vite aus, um den Browser-Code bereitzustellen.

    cd client
    npm run dev
    
  5. Navigieren Sie zum Client-Code unter http://localhost:5173 (opens in a new tab)

  6. Bevor Sie eine Transaktion ausgeben können, müssen Sie die Nonce sowie den Betrag kennen, den Sie senden können. Um diese Informationen zu erhalten, klicken Sie auf Update account data und signieren Sie die Nachricht.

    Wir haben hier ein Dilemma. Einerseits möchten wir keine Nachricht signieren, die wiederverwendet werden kann (ein Replay-Angriff (opens in a new tab)), weshalb wir überhaupt eine Nonce wollen. Wir haben jedoch noch keine Nonce. Die Lösung besteht darin, eine Nonce zu wählen, die nur einmal verwendet werden kann und die wir bereits auf beiden Seiten haben, wie zum Beispiel die aktuelle Uhrzeit.

    Das Problem bei dieser Lösung ist, dass die Zeit möglicherweise nicht perfekt synchronisiert ist. Stattdessen signieren wir also einen Wert, der sich jede Minute ändert. Das bedeutet, dass unser Zeitfenster für die Anfälligkeit gegenüber Replay-Angriffen höchstens eine Minute beträgt. In Anbetracht der Tatsache, dass die signierte Anfrage in der Produktion durch TLS geschützt wird und dass die andere Seite des Tunnels – der Server – den Kontostand und die Nonce bereits offenlegen kann (er muss sie kennen, um zu funktionieren), ist dies ein akzeptables Risiko.

  7. Sobald der Browser den Kontostand und die Nonce zurückerhält, zeigt er das Transferformular an. Wählen Sie die Zieladresse und den Betrag aus und klicken Sie auf Transfer. Signieren Sie diese Anfrage.

  8. Um den Transfer zu sehen, klicken Sie entweder auf Update account data oder schauen Sie in das Fenster, in dem Sie den Server ausführen. Der Server protokolliert den Zustand jedes Mal, wenn er sich ändert.

ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start

server@1.0.0 start node --experimental-json-modules index.mjs

Listening on port 3000 Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 64000 (1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 100000 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 56800 (2) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 53800 (3) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 139000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)


#### `server/index.mjs` \{#server-index-mjs-1\}

[Diese Datei](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/index.mjs) enthält den Serverprozess und interagiert mit dem Noir-Code unter [`main.nr`](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/noir/src/main.nr). Hier ist eine Erklärung der interessanten Teile.

```js
import { Noir } from '@noir-lang/noir_js'

Die Bibliothek noir.js (opens in a new tab) bildet die Schnittstelle zwischen JavaScript-Code und Noir-Code.

const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))
const noir = new Noir(circuit)

Laden Sie den arithmetischen Schaltkreis – das kompilierte Noir-Programm, das wir in der vorherigen Phase erstellt haben – und bereiten Sie dessen Ausführung vor.

// Wir stellen Kontoinformationen nur als Antwort auf eine signierte Anfrage bereit
const accountInformation = async signature => {
    const fromAddress = await recoverAddress({
        hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
        signature
    })

Um Kontoinformationen bereitzustellen, benötigen wir nur die Signatur. Der Grund dafür ist, dass wir bereits wissen, wie die Nachricht lauten wird, und somit auch den Nachrichten-Hash kennen.

const processMessage = async (message, signature) => {

Verarbeiten Sie eine Nachricht und führen Sie die darin codierte Transaktion aus.

    // Den öffentlichen Schlüssel abrufen
    const pubKey = await recoverPublicKey({
        hash,
        signature
    })

Da wir nun JavaScript auf dem Server ausführen, können wir den öffentlichen Schlüssel dort anstatt auf dem Client abrufen.

noir.execute führt das Noir-Programm aus. Die Parameter entsprechen denen, die in Prover.toml (opens in a new tab) bereitgestellt werden. Beachten Sie, dass lange Werte als Array von hexadezimalen Strings (["0x60", "0xA7"]) bereitgestellt werden, nicht als einzelner hexadezimaler Wert (0x60A7), wie es Viem tut.

    } catch (err) {
        console.log(`Noir error: ${err}`)
        throw Error("Invalid transaction, not processed")
    }

Wenn ein Fehler auftritt, fangen Sie ihn ab und leiten Sie dann eine vereinfachte Version an den Client weiter.

    Accounts[fromAccountNumber].nonce++
    Accounts[fromAccountNumber].balance -= amount
    Accounts[toAccountNumber].balance += amount

Wenden Sie die Transaktion an. Wir haben dies bereits im Noir-Code getan, aber es ist einfacher, es hier noch einmal zu tun, anstatt das Ergebnis von dort zu extrahieren.

let Accounts = [
    {
        address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        balance: 5000,
        nonce: 0,
    },

Die anfängliche Accounts-Struktur.

Phase 3 - Ethereum Smart Contracts

  1. Stoppen Sie die Server- und Client-Prozesse.

  2. Laden Sie den Branch mit den Smart Contracts herunter und stellen Sie sicher, dass Sie alle erforderlichen Module haben.

    git checkout 03-smart-contracts
    cd client
    npm install
    cd ../server
    npm install
    
  3. Führen Sie anvil in einem separaten Befehlszeilenfenster aus.

  4. Generieren Sie den Verifizierungsschlüssel und den Solidity-Verifizierer und kopieren Sie dann den Verifizierer-Code in das Solidity-Projekt.

    cd noir
    bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak
    bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol
    cp target/Verifier.sol ../../smart-contracts/src
    
  5. Gehen Sie zu den Smart Contracts und setzen Sie die Umgebungsvariablen, um die anvil-Blockchain zu verwenden.

    cd ../../smart-contracts
    export ETH_RPC_URL=http://localhost:8545
    ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    
  6. Stellen Sie Verifier.sol bereit und speichern Sie die Adresse in einer Umgebungsvariablen.

    VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'`
    echo $VERIFIER_ADDRESS
    
  7. Stellen Sie den Vertrag ZkBank bereit.

    ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'`
    echo $ZKBANK_ADDRESS
    

    Der Wert 0x199..67b ist der Pedersen-Hash des anfänglichen Zustands von Accounts. Wenn Sie diesen anfänglichen Zustand in server/index.mjs ändern, können Sie eine Transaktion ausführen, um den anfänglichen Hash zu sehen, der vom Zero-Knowledge-Beweis gemeldet wird.

  8. Starten Sie den Server.

    cd ../server
    npm run start
    
  9. Führen Sie den Client in einem anderen Befehlszeilenfenster aus.

    cd client
    npm run dev
    
  10. Führen Sie einige Transaktionen aus.

  11. Um zu überprüfen, ob sich der Zustand Onchain geändert hat, starten Sie den Serverprozess neu. Sie werden sehen, dass ZkBank keine Transaktionen mehr akzeptiert, da der ursprüngliche Hash-Wert in den Transaktionen von dem Onchain gespeicherten Hash-Wert abweicht.

    Dies ist die Art von Fehler, die erwartet wird.

ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start

server@1.0.0 start node --experimental-json-modules index.mjs

Listening on port 3000 Verification error: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason: Wrong old state hash

Contract Call: address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 function: processTransaction(bytes _proof, bytes32[] _publicInputs) args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000

Wir müssen das Barretenberg-Paket (opens in a new tab) verwenden, um den eigentlichen Beweis zu erstellen, der Onchain gesendet werden soll. Wir können dieses Paket entweder über die Befehlszeilenschnittstelle (bb) oder über die JavaScript-Bibliothek bb.js (opens in a new tab) verwenden. Die JavaScript-Bibliothek ist viel langsamer als die native Ausführung von Code, daher verwenden wir hier exec (opens in a new tab), um die Befehlszeile zu nutzen.

Beachten Sie, dass Sie, wenn Sie sich für die Verwendung von bb.js entscheiden, eine Version verwenden müssen, die mit der von Ihnen verwendeten Noir-Version kompatibel ist. Zum Zeitpunkt des Schreibens verwendet die aktuelle Noir-Version (1.0.0-beta.11) die bb.js-Version 0.87.

const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"

Die Adresse hier ist diejenige, die Sie erhalten, wenn Sie mit einem sauberen anvil beginnen und den obigen Anweisungen folgen.

const walletClient = createWalletClient({ 
    chain: anvil, 
    transport: http(), 
    account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
})

Dieser private Schlüssel gehört zu einem der standardmäßig vorfinanzierten Konten in anvil.

const generateProof = async (witness, fileID) => {

Generieren Sie einen Beweis mit der ausführbaren Datei bb.

    const fname = `witness-${fileID}.gz`    
    await fs.writeFile(fname, witness)

Schreiben Sie den Zeugen in eine Datei.

    await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`)

Erstellen Sie nun den eigentlichen Beweis. Dieser Schritt erstellt auch eine Datei mit den öffentlichen Variablen, aber diese benötigen wir nicht. Wir haben diese Variablen bereits von noir.execute erhalten.

    const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")

Der Beweis ist ein JSON-Array von Field-Werten, die jeweils als hexadezimaler Wert dargestellt werden. Wir müssen ihn jedoch in der Transaktion als einzelnen bytes-Wert senden, den Viem durch einen großen hexadezimalen String darstellt. Hier ändern wir das Format, indem wir alle Werte verketten, alle 0x entfernen und dann am Ende eines hinzufügen.

    await execPromise(`rm -r ${fname} ${fileID}`)

    return proof
}

Räumen Sie auf und geben Sie den Beweis zurück.

const processMessage = async (message, signature) => {
    .
    .
    .

    const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0"))

Die öffentlichen Felder müssen ein Array von 32-Byte-Werten sein. Da wir jedoch den Transaktions-Hash auf zwei Field-Werte aufteilen mussten, erscheint er als 16-Byte-Wert. Hier fügen wir Nullen hinzu, damit Viem versteht, dass es sich tatsächlich um 32 Bytes handelt.

    const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)

Jede Adresse verwendet jede Nonce nur einmal, sodass wir eine Kombination aus fromAddress und nonce als eindeutigen Bezeichner für die Zeugendatei und das Ausgabeverzeichnis verwenden können.

Senden Sie die Transaktion an die Chain.

smart-contracts/src/ZkBank.sol

Dies ist der Onchain-Code, der die Transaktion empfängt.

Der Onchain-Code muss zwei Variablen verfolgen: den Verifizierer (ein separater Vertrag, der von nargo erstellt wird) und den aktuellen Zustands-Hash.

    event TransactionProcessed(
        bytes32 indexed transactionHash,
        bytes32 oldStateHash,
        bytes32 newStateHash
    );

Jedes Mal, wenn sich der Zustand ändert, geben wir ein TransactionProcessed-Ereignis aus.

    function processTransaction(
        bytes calldata _proof,
        bytes32[] calldata _publicFields
    ) public {

Diese Funktion verarbeitet Transaktionen. Sie erhält den Beweis (als bytes) und die öffentlichen Eingaben (als bytes32-Array) in dem Format, das der Verifizierer benötigt (um die Onchain-Verarbeitung und damit die Gaskosten zu minimieren).

        require(_publicInputs[0] == currentStateHash,
            "Wrong old state hash");

Der Zero-Knowledge-Beweis muss belegen, dass die Transaktion von unserem aktuellen Hash zu einem neuen wechselt.

        myVerifier.verify(_proof, _publicFields);

Rufen Sie den Verifizierer-Vertrag auf, um den Zero-Knowledge-Beweis zu verifizieren. Dieser Schritt macht die Transaktion rückgängig, wenn der Zero-Knowledge-Beweis falsch ist.

Wenn alles in Ordnung ist, aktualisieren Sie den Zustands-Hash auf den neuen Wert und geben Sie ein TransactionProcessed-Ereignis aus.

Missbrauch durch die zentralisierte Komponente

Informationssicherheit besteht aus drei Eigenschaften:

  • Vertraulichkeit, Benutzer können keine Informationen lesen, für die sie nicht autorisiert sind.
  • Integrität, Informationen können nur von autorisierten Benutzern auf autorisierte Weise geändert werden.
  • Verfügbarkeit, autorisierte Benutzer können das System nutzen.

In diesem System wird die Integrität durch Zero-Knowledge-Beweise gewährleistet. Die Verfügbarkeit ist viel schwerer zu garantieren, und Vertraulichkeit ist unmöglich, da die Bank den Kontostand jedes Kontos und alle Transaktionen kennen muss. Es gibt keine Möglichkeit, eine Entität, die über Informationen verfügt, daran zu hindern, diese Informationen weiterzugeben.

Es könnte möglich sein, eine wirklich vertrauliche Bank mithilfe von Stealth-Adressen (opens in a new tab) zu erstellen, aber das würde den Rahmen dieses Artikels sprengen.

Falsche Informationen

Eine Möglichkeit, wie der Server die Integrität verletzen kann, besteht darin, falsche Informationen bereitzustellen, wenn Daten angefordert werden (opens in a new tab).

Um dies zu lösen, können wir ein zweites Noir-Programm schreiben, das die Konten als private Eingabe und die Adresse, für die Informationen angefordert werden, als öffentliche Eingabe erhält. Die Ausgabe ist der Kontostand und die Nonce dieser Adresse sowie der Hash der Konten.

Natürlich kann dieser Beweis nicht onchain verifiziert werden, da wir Nonces und Kontostände nicht onchain veröffentlichen wollen. Er kann jedoch durch den Client-Code verifiziert werden, der im Browser ausgeführt wird.

Erzwungene Transaktionen

Der übliche Mechanismus zur Gewährleistung der Verfügbarkeit und zur Verhinderung von Zensur auf L2s sind erzwungene Transaktionen (opens in a new tab). Aber erzwungene Transaktionen lassen sich nicht mit Zero-Knowledge-Beweisen kombinieren. Der Server ist die einzige Entität, die Transaktionen verifizieren kann.

Wir können smart-contracts/src/ZkBank.sol so modifizieren, dass es erzwungene Transaktionen akzeptiert und den Server daran hindert, den Zustand zu ändern, bis sie verarbeitet sind. Dies macht uns jedoch anfällig für einen einfachen Denial-of-Service-Angriff. Was ist, wenn eine erzwungene Transaktion ungültig und daher unmöglich zu verarbeiten ist?

Die Lösung besteht darin, einen Zero-Knowledge-Beweis dafür zu haben, dass eine erzwungene Transaktion ungültig ist. Dies gibt dem Server drei Optionen:

  • Die erzwungene Transaktion verarbeiten und einen Zero-Knowledge-Beweis dafür liefern, dass sie verarbeitet wurde, sowie den neuen Zustands-Hash.
  • Die erzwungene Transaktion ablehnen und dem Vertrag einen Zero-Knowledge-Beweis dafür liefern, dass die Transaktion ungültig ist (unbekannte Adresse, falsche Nonce oder unzureichender Kontostand).
  • Die erzwungene Transaktion ignorieren. Es gibt keine Möglichkeit, den Server zu zwingen, die Transaktion tatsächlich zu verarbeiten, aber das bedeutet, dass das gesamte System nicht verfügbar ist.

Verfügbarkeitskautionen

In einer realen Implementierung gäbe es wahrscheinlich ein Profitmotiv, um den Server am Laufen zu halten. Wir können diesen Anreiz verstärken, indem der Server eine Verfügbarkeitskaution hinterlegt, die jeder verbrennen kann, wenn eine erzwungene Transaktion nicht innerhalb eines bestimmten Zeitraums verarbeitet wird.

Schlechter Noir-Code

Normalerweise laden wir den Quellcode in einen Block-Explorer (opens in a new tab) hoch, damit die Leute einem Smart Contract vertrauen. Im Falle von Zero-Knowledge-Beweisen ist das jedoch unzureichend.

Verifier.sol enthält den Verifizierungsschlüssel, der eine Funktion des Noir-Programms ist. Dieser Schlüssel sagt uns jedoch nicht, was das Noir-Programm war. Um tatsächlich eine vertrauenswürdige Lösung zu haben, müssen Sie das Noir-Programm (und die Version, die es erstellt hat) hochladen. Andernfalls könnten die Zero-Knowledge-Beweise ein anderes Programm widerspiegeln, eines mit einer Hintertür.

Bis Block-Explorer es uns ermöglichen, Noir-Programme hochzuladen und zu verifizieren, sollten Sie dies selbst tun (vorzugsweise auf IPFS). Dann können erfahrene Benutzer den Quellcode herunterladen, ihn selbst kompilieren, Verifier.sol erstellen und verifizieren, dass er mit dem onchain identisch ist.

Fazit

Plasma-artige Anwendungen erfordern eine zentralisierte Komponente als Informationsspeicher. Dies eröffnet potenzielle Schwachstellen, ermöglicht es uns aber im Gegenzug, die Privatsphäre auf eine Art und Weise zu wahren, die auf der Blockchain selbst nicht verfügbar ist. Mit Zero-Knowledge-Beweisen können wir die Integrität sicherstellen und es für den Betreiber der zentralisierten Komponente möglicherweise wirtschaftlich vorteilhaft machen, die Verfügbarkeit aufrechtzuerhalten.

Hier finden Sie weitere meiner Arbeiten (opens in a new tab).

Danksagungen

  • Josh Crites hat einen Entwurf dieses Artikels gelesen und mir bei einem kniffligen Noir-Problem geholfen.

Alle verbleibenden Fehler liegen in meiner Verantwortung.