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:
- Der Merkle-Beweis bindet den Datensatz an einen Zustands-Root.
- 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_depthGeschwister — typischerweise eine Handvoll, nicht 128. Nehmen Sie nie eine feste Länge an; lesen Sieproof_depthund prüfen Sie, dass es zumerkle_proof.lengthpasst. - 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:
| Terminal | Bedeutung |
|---|---|
Occupied | dort steht das eigene Blatt des Schlüssels — der Name existiert |
Vacant | dort steht ein leerer Teilbaum — der Name ist nicht registriert |
Blocked | dort 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
- Python
- PHP
- JavaScript
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()
<?php
const LEAF_TAG = "\x00";
const NODE_TAG = "\x01";
const KEY_LEN = 16;
const MAX_DEPTH = 128;
function hashLeaf(string $domain, string $ownerPubkeyHex, string $targetAddress, int $price, int $nonce): string
{
return hash('sha256',
LEAF_TAG
. $domain
. hex2bin($ownerPubkeyHex)
. $targetAddress
. pack('P', $price) // u64 little-endian
. pack('P', $nonce),
true);
}
function hashNode(string $left, string $right): string
{
return hash('sha256', NODE_TAG . $left . $right, true);
}
function keyOf(string $domain): string
{
return substr(hash('sha256', $domain, true), 0, KEY_LEN);
}
function keyBit(string $key, int $i): int
{
return (ord($key[intdiv($i, 8)]) >> (7 - ($i % 8))) & 1;
}
function fold(string $key, string $start, array $siblings): string
{
$h = $start;
$n = count($siblings);
foreach ($siblings as $i => $sib) {
$h = keyBit($key, $n - 1 - $i) === 1 ? hashNode($sib, $h) : hashNode($h, $sib);
}
return $h;
}
function verifyResolution(array $entry, string $expectedRoot): bool
{
if ($entry['proof_terminal'] !== 'Occupied') {
throw new RuntimeException('a resolution must carry an Occupied terminal');
}
$siblings = array_map('hex2bin', $entry['merkle_proof']);
if (count($siblings) !== (int)$entry['proof_depth']) {
throw new RuntimeException('proof_depth does not match the sibling count');
}
if ((int)$entry['proof_depth'] > MAX_DEPTH) {
throw new RuntimeException('proof depth exceeds MAX_DEPTH');
}
$leaf = hashLeaf($entry['domain_name'], $entry['owner_pubkey'], $entry['target_address'],
(int)$entry['price'], (int)$entry['nonce']);
return bin2hex(fold(keyOf($entry['domain_name']), $leaf, $siblings)) === strtolower($expectedRoot);
}
import crypto from "node:crypto";
const LEAF_TAG = 0x00;
const NODE_TAG = 0x01;
const KEY_LEN = 16;
const MAX_DEPTH = 128;
const sha256 = (...parts) =>
crypto.createHash("sha256").update(Buffer.concat(parts)).digest();
const u64le = (v) => {
const b = Buffer.alloc(8);
b.writeBigUInt64LE(BigInt(v));
return b;
};
function hashLeaf(domain, ownerPubkeyHex, targetAddress, price, nonce) {
return sha256(
Buffer.from([LEAF_TAG]),
Buffer.from(domain, "utf8"),
Buffer.from(ownerPubkeyHex, "hex"),
Buffer.from(targetAddress, "utf8"),
u64le(price),
u64le(nonce),
);
}
const hashNode = (l, r) => sha256(Buffer.from([NODE_TAG]), l, r);
const keyOf = (domain) =>
crypto.createHash("sha256").update(domain, "utf8").digest().subarray(0, KEY_LEN);
const keyBit = (key, i) => (key[Math.floor(i / 8)] >> (7 - (i % 8))) & 1;
function fold(key, start, siblings) {
let h = start;
const n = siblings.length;
for (let i = 0; i < n; i++) {
h = keyBit(key, n - 1 - i) ? hashNode(siblings[i], h) : hashNode(h, siblings[i]);
}
return h;
}
export function verifyResolution(entry, expectedRoot) {
if (entry.proof_terminal !== "Occupied") {
throw new Error("a resolution must carry an Occupied terminal");
}
const siblings = entry.merkle_proof.map((s) => Buffer.from(s, "hex"));
if (siblings.length !== entry.proof_depth) {
throw new Error("proof_depth does not match the sibling count");
}
if (entry.proof_depth > MAX_DEPTH) {
throw new Error("proof depth exceeds MAX_DEPTH");
}
const leaf = hashLeaf(
entry.domain_name,
entry.owner_pubkey,
entry.target_address,
Number(entry.price),
Number(entry.nonce),
);
return fold(keyOf(entry.domain_name), leaf, siblings).toString("hex")
=== expectedRoot.toLowerCase();
}
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.
| Aufruf | Selektor | Rückgabe |
|---|---|---|
isRootValid(bytes32) | 0x30ef41b4 | bool — wurde dieser Root je angenommen |
currentRoot() | 0xfdab463d | bytes32 — der zuletzt angenommene Root |
verifyRootValidity(bytes32) | 0xc7179944 | (bool isValid, uint32 blockHeight) |
currentBlockHeight() | 0x367bf2f9 | uint32 — PIVX-Höhe des letzten Roots |
programVkey() | 0x09665ee7 | bytes32 — die Schaltung, die der Vertrag durchsetzt |
- Python
- PHP
- JavaScript
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
<?php
function isRootValid(string $rpcUrl, string $contractAddress, string $smtRoot): bool
{
$clean = str_pad(str_replace('0x', '', strtolower($smtRoot)), 64, '0', STR_PAD_LEFT);
$payload = [
'jsonrpc' => '2.0',
'method' => 'eth_call',
'params' => [['to' => $contractAddress, 'data' => '0x30ef41b4' . $clean], 'latest'],
'id' => 1,
];
$ch = curl_init($rpcUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 15,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
if (isset($data['error'])) {
throw new RuntimeException('EVM RPC error: ' . $data['error']['message']);
}
$result = $data['result'] ?? '0x';
if ($result === '' || $result === '0x') {
return false;
}
// A bool is ABI-encoded as a full 32-byte word: 0x00..01 for true.
return hexdec(substr($result, -1)) === 1;
}
async function isRootValid(rpcUrl, contractAddress, smtRoot) {
const clean = smtRoot.replace(/^0x/, "").toLowerCase().padStart(64, "0");
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_call",
params: [{ to: contractAddress, data: `0x30ef41b4${clean}` }, "latest"],
id: 1,
}),
});
if (!response.ok) throw new Error(`RPC request failed: ${response.statusText}`);
const json = await response.json();
if (json.error) throw new Error(`EVM RPC error: ${json.error.message}`);
const result = json.result;
if (!result || result === "0x") return false;
// A bool is ABI-encoded as a full 32-byte word: 0x00..01 for true.
return BigInt(result) === 1n;
}
Die Antwort deuten
isRootValid | Bedeutung | Was zu tun ist |
|---|---|---|
true, und der Root entspricht currentRoot() | der Indexer ist vollständig synchronisiert | annehmen |
true, aber der Root ist älter als currentRoot() | der Indexer hinkt der Kette hinterher | annehmen, optional warnen — der Datensatz war zu jenem Root gültig |
false | dieser Root wurde on-chain nie angenommen | die 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.
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.