Zum Hauptinhalt springen

Beweise prüfen

Diese Seite zeigt, wie Sie eine Namensauflösung prüfen, ohne dem Indexer zu vertrauen, der sie geliefert hat. Jedes Codebeispiel hier wird vor der Veröffentlichung gegen einen laufenden Indexer ausgeführt.

Was die Prüfung Ihnen bringt

Eine Auflösung beantwortet: „Auf welche Adresse zeigt richard.pivx?“ Die Prüfung belegt, dass die Antwort diejenige ist, auf die sich der Zustands-Root festgelegt hat — und dass der Root einer ist, den der Anchor-Vertrag angenommen hat. Ein Indexer, der eine Adresse erfindet, einen veralteten Datensatz liefert oder einen Namen unterschlägt, kann keinen Beweis vorlegen, der diese Prüfungen besteht.

Es gibt zwei unabhängige Schritte, und Sie brauchen beide:

  1. Der Merkle-Beweis bindet den Datensatz an einen Zustands-Root.
  2. Die On-Chain-Prüfung bindet diesen Root an den Anchor-Vertrag.

Schritt 1 allein ist wertlos: Ein bösartiger Indexer kann einen kompletten gefälschten Baum bauen und dazu passende Beweise gegen seinen eigenen gefälschten Root liefern. Erst Schritt 2 macht den Root aussagekräftig.


Schritt 1 — den Merkle-Beweis prüfen

Der Baum

PiNS nutzt einen kompakten Sparse Merkle Tree über einem 128-Bit-Schlüsselraum. Ein Schlüssel sind die ersten 16 Bytes von SHA-256(domain_name). Beim Schreiben eines Verifizierers sind zwei Eigenschaften wichtig:

  • Beweise sind kurz und von variabler Länge. Ein Blatt sitzt auf der geringsten Tiefe, in der sein Schlüsselpräfix eindeutig ist, ein Beweis trägt also proof_depth Geschwister — typischerweise eine Handvoll, nicht 128. Nehmen Sie nie eine feste Länge an; lesen Sie proof_depth und prüfen Sie, dass es zu merkle_proof.length passt.
  • Ein leerer Teilbaum ist auf jeder Höhe 32 Nullbytes. Es gibt keine Leiter vorberechneter Hashes für leere Knoten. Der Root eines leeren Baums besteht daher aus lauter Nullen.

Die Hashfunktionen

Blätter und innere Knoten werden markiert, damit sich ihre Hash-Domänen nicht überschneiden. Das Tag-Byte wegzulassen ist der mit Abstand häufigste Fehler — er erzeugt einen plausibel aussehenden Hash, der nie zum Root passt.

leaf = SHA-256( 0x00 ‖ domain ‖ owner_pubkey ‖ target_address ‖ price_le64 ‖ nonce_le64 )
node = SHA-256( 0x01 ‖ left ‖ right )
key = SHA-256( domain )[0..16]

domain und target_address sind UTF-8-Bytes ohne Längenpräfix; owner_pubkey sind die rohen 32 Bytes; price und nonce sind vorzeichenlose 64-Bit-Werte in Little-Endian.

Der Durchlauf

Gehen Sie vom Blatt zurück zum Root. Bei n = proof_depth Geschwistern entscheidet in Schritt i das Bit n - 1 - i des Schlüssels darüber, auf welcher Seite Sie stehen, gezählt ab dem höchstwertigen Bit von Byte 0:

h = leaf
for i in 0..n:
h = key_bit(key, n-1-i) ? node(sibling[i], h) : node(h, sibling[i])

merkle_proof[0] ist das Geschwister auf der tiefsten Ebene, dem Blatt am nächsten.

Das Terminal

Jeder Beweis trägt ein proof_terminal, das beschreibt, was am Ende des Pfads steht:

TerminalBedeutung
Occupieddort steht das eigene Blatt des Schlüssels — der Name existiert
Vacantdort steht ein leerer Teilbaum — der Name ist nicht registriert
Blockeddort steht das Blatt eines anderen Namens — der Name ist nicht registriert, und die Antwort enthält den vollständigen Datensatz jenes Namens

Eine erfolgreiche Auflösung muss Occupied sein. Vacant und Blocked sind Abwesenheits-Beweise: Sie erlauben es, zu prüfen, dass ein Name wirklich nicht registriert ist, statt einem „nicht gefunden“ des Indexers zu glauben. Weisen Sie jede Auflösung zurück, deren Terminal nicht Occupied ist.

Code

import hashlib
import struct

LEAF_TAG, NODE_TAG = b"\x00", b"\x01"
KEY_LEN, MAX_DEPTH = 16, 128


def hash_leaf(domain, owner_pubkey_hex, target_address, price, nonce):
h = hashlib.sha256()
h.update(LEAF_TAG)
h.update(domain.encode())
h.update(bytes.fromhex(owner_pubkey_hex))
h.update(target_address.encode())
h.update(struct.pack("<Q", price))
h.update(struct.pack("<Q", nonce))
return h.digest()


def hash_node(left, right):
h = hashlib.sha256()
h.update(NODE_TAG)
h.update(left)
h.update(right)
return h.digest()


def key_of(domain):
return hashlib.sha256(domain.encode()).digest()[:KEY_LEN]


def key_bit(key, i):
return (key[i // 8] >> (7 - (i % 8))) & 1


def fold(key, start, siblings):
h, n = start, len(siblings)
for i, sib in enumerate(siblings):
h = hash_node(sib, h) if key_bit(key, n - 1 - i) else hash_node(h, sib)
return h


def verify_resolution(entry, expected_root):
if entry["proof_terminal"] != "Occupied":
raise ValueError("a resolution must carry an Occupied terminal")

siblings = [bytes.fromhex(s) for s in entry["merkle_proof"]]
if len(siblings) != entry["proof_depth"]:
raise ValueError("proof_depth does not match the sibling count")
if entry["proof_depth"] > MAX_DEPTH:
raise ValueError("proof depth exceeds MAX_DEPTH")

leaf = hash_leaf(
entry["domain_name"],
entry["owner_pubkey"],
entry["target_address"],
int(entry["price"]),
int(entry["nonce"]),
)
root = fold(key_of(entry["domain_name"]), leaf, siblings)
return root.hex() == expected_root.lower()
tipp

Testen Sie Ihre Implementierung gegen Manipulation

Ein Verifizierer, der immer true zurückgibt, besteht jeden positiven Test. Bevor Sie Ihrem vertrauen: Ändern Sie in einer echten Antwort ein Byte von target_address, owner_pubkey oder price und bestätigen Sie, dass er nun false liefert. Alle drei Beispiele oben sind so geprüft.


Schritt 2 — den Root gegen den Anchor-Vertrag prüfen

Ein verifizierter Merkle-Beweis sagt nur: „Dieser Datensatz liegt in irgendeinem Baum.“ Um zu wissen, dass er im echten Baum liegt, fragen Sie den Anchor-Vertrag auf der BNB Smart Chain, ob er diesen Root je angenommen hat.

Verwenden Sie isRootValid(bytes32) — Selektor 0x30ef41b4. Er liefert einen einzelnen ABI-kodierten Boolean, der sich deutlich leichter korrekt auswerten lässt als die rootHistory-Struktur.

AufrufSelektorRückgabe
isRootValid(bytes32)0x30ef41b4bool — wurde dieser Root je angenommen
currentRoot()0xfdab463dbytes32 — der zuletzt angenommene Root
verifyRootValidity(bytes32)0xc7179944(bool isValid, uint32 blockHeight)
currentBlockHeight()0x367bf2f9uint32 — PIVX-Höhe des letzten Roots
programVkey()0x09665ee7bytes32 — die Schaltung, die der Vertrag durchsetzt
import requests


def is_root_valid(rpc_url, contract_address, smt_root):
"""Returns True if the anchor contract has ever accepted this root."""
clean = smt_root.replace("0x", "").lower().rjust(64, "0")
payload = {
"jsonrpc": "2.0",
"method": "eth_call",
"params": [{"to": contract_address, "data": f"0x30ef41b4{clean}"}, "latest"],
"id": 1,
}
r = requests.post(rpc_url, json=payload, timeout=15).json()
if "error" in r:
raise RuntimeError(f"EVM RPC error: {r['error']['message']}")

result = r.get("result", "0x")
# A bool is ABI-encoded as a full 32-byte word: 0x00..01 for true.
return int(result, 16) == 1 if result not in ("", "0x") else False

Die Antwort deuten

isRootValidBedeutungWas zu tun ist
true, und der Root entspricht currentRoot()der Indexer ist vollständig synchronisiertannehmen
true, aber der Root ist älter als currentRoot()der Indexer hinkt der Kette hinterherannehmen, optional warnen — der Datensatz war zu jenem Root gültig
falsedieser Root wurde on-chain nie angenommendie Auflösung zurückweisen

Ein false bedeutet, dass der Baum, gegen den Sie geprüft haben, aus Sicht des Protokolls nicht existiert. Genau diesen Fall erzeugt ein von einem Angreifer kontrollierter Indexer, und genau dafür ist dieser Schritt da.


Die Schaltung selbst prüfen

Die beiden Schritte oben belegen, dass ein Datensatz zu einem Root gehört, den der Vertrag angenommen hat. Der Vertrag nimmt einen Root nur an, wenn ein gültiger ZK-Beweis dabei war — und dieser Beweis wird gegen programVkey geprüft, eine 32-Byte-Festlegung auf die exakt kompilierte Schaltung.

Dass dieser Schlüssel dem veröffentlichten Quellcode entspricht, können Sie bestätigen:

git clone https://github.com/PIVX-Name/pivx-name-prover
cd pivx-name-prover/program
cargo prove build # plain build = mainnet

Vergleichen Sie den entstandenen Verification Key mit programVkey() (Selektor 0x09665ee7) im Anchor-Vertrag. Stimmen sie überein, setzt der Vertrag genau diesen Quellcode durch. Eine einzige geänderte Zeile der Schaltung ergibt einen anderen Schlüssel, eine veränderte Schaltung lässt sich also nicht unterschieben, ohne dass die Änderung on-chain sichtbar wird.

warnung

Ohne zusätzliche Features bauen

Ein einfaches cargo prove build erzeugt die Mainnet-Schaltung, die der bereitgestellte Vertrag durchsetzt. Jedes Feature-Flag verändert das kompilierte Programm und damit seinen Verification Key, der dann nicht mehr passt.

Einen SP1-Beweis direkt prüfen

Die Beweise sind gewöhnliche SP1-Groth16-Beweise und lassen sich mit dem SP1-SDK prüfen:

use sp1_sdk::{ProverClient, SP1ProofWithPublicValues};

let client = ProverClient::from_env();
let (_, vk) = client.setup(ELF);
let proof = SP1ProofWithPublicValues::load("proof.bin")?;
client.verify(&proof, &vk)?;

Die öffentlichen Werte sind die ABI-Kodierung von (bytes32 old_root, bytes32 new_root, uint32 end_block_height) — der vom Batch ausgeführte Zustandsübergang und die PIVX-Blockhöhe, die er abdeckt.