İspatları doğrulama
Bu sayfa, bir isim çözümlemesini onu sunan indeksleyiciye güvenmeden nasıl doğrulayacağınızı gösterir. Buradaki her kod örneği, yayımlanmadan önce çalışan bir indeksleyiciye karşı çalıştırılır.
Doğrulama size ne kazandırır
Bir çözümleme şu soruyu yanıtlar: "richard.pivx hangi adrese işaret ediyor?" Onu doğrulamak,
yanıtın durum köküne taahhüt edilmiş yanıt olduğunu ve o kökün çıpa sözleşmesinin kabul ettiği
bir kök olduğunu kanıtlar. Adres uyduran, eskimiş bir kayıt sunan ya da bir ismi gizleyen bir
indeksleyici, bu denetimleri geçen bir ispat üretemez.
Birbirinden bağımsız iki adım vardır ve ikisine de ihtiyacınız olur:
- Merkle ispatı, kaydı bir durum köküne bağlar.
- Zincir üzerindeki denetim, o kökü çıpa sözleşmesine bağlar.
Tek başına 1. adım işe yaramaz: kötü niyetli bir indeksleyici, baştan sona sahte bir ağaç kurup kendi sahte köküyle tutarlı ispatlar sunabilir. Köke anlam kazandıran 2. adımdır.
1. adım — Merkle ispatını doğrulayın
Ağaç
PiNS, 128 bitlik bir anahtar uzayı üzerinde kompakt seyrek Merkle ağacı kullanır. Bir anahtar,
SHA-256(domain_name) değerinin ilk 16 baytıdır. Doğrulayıcı yazarken iki özellik önemlidir:
- İspatlar kısadır ve uzunlukları değişkendir. Bir yaprak, anahtar önekinin benzersiz olduğu
en sığ derinlikte durur; dolayısıyla bir ispat
proof_depthkadar kardeş taşır — genellikle bir avuç, 128 değil. Asla sabit bir uzunluk varsaymayın;proof_depthdeğerini okuyun vemerkle_proof.lengthile eşleştiğini doğrulayın. - Boş bir alt ağaç her yükseklikte 32 sıfır bayttır. Boş düğümler için önceden hesaplanmış hash merdiveni yoktur. Bu yüzden boş bir ağacın kökü tamamen sıfırdır.
Hash fonksiyonları
Yapraklar ve iç düğümler, hash alanları çakışmasın diye etiketlenir. Etiket baytını atlamak, açık ara en yaygın hatadır — makul görünen ama kökle asla eşleşmeyen bir hash üretir.
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 ve target_address, uzunluk öneki olmayan UTF-8 baytlarıdır; owner_pubkey ham 32
bayttır; price ve nonce ise işaretsiz 64 bit little-endian değerlerdir.
Dolaşım
Yapraktan köke doğru ilerleyin. n = proof_depth kardeşle, i adımında hangi tarafta
olduğunuzu belirleyen bit, 0. baytın en anlamlı bitinden sayılarak anahtarın n - 1 - i
numaralı bitidir:
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], en derin seviyedeki, yaprağa en yakın kardeştir.
Terminal
Her ispat, yolun sonunda ne olduğunu anlatan bir proof_terminal taşır:
| Terminal | Anlamı |
|---|---|
Occupied | orada anahtarın kendi yaprağı var — isim mevcut |
Vacant | orada boş bir alt ağaç var — isim kayıtlı değil |
Blocked | orada başka bir ismin yaprağı var — isim kayıtlı değil ve yanıt o ismin tam kaydını içeriyor |
Başarılı bir çözümleme Occupied olmalıdır. Vacant ve Blocked, yokluk ispatlarıdır: bir
indeksleyicinin "bulunamadı" demesine inanmak yerine, bir ismin gerçekten kayıtlı olmadığını
doğrulamanızı sağlarlar. Terminali Occupied olmayan her çözümlemeyi reddedin.
Kod
- 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();
}
Uygulamanızı kurcalamaya karşı test edin
Her zaman true döndüren bir doğrulayıcı, tüm olumlu testleri geçer. Kendi doğrulayıcınıza
güvenmeden önce, gerçek bir yanıttaki target_address, owner_pubkey veya price alanının bir
baytını değiştirin ve artık false döndürdüğünü doğrulayın. Yukarıdaki üç örnek de bu yöntemle
denetlenmiştir.
2. adım — kökü çıpa sözleşmesiyle karşılaştırın
Doğrulanmış bir Merkle ispatı yalnızca "bu kayıt bir ağaçta" der. Gerçek ağaçta olduğunu bilmek için, BNB Smart Chain üzerindeki çıpa sözleşmesine o kökü hiç kabul edip etmediğini sorun.
isRootValid(bytes32) kullanın — seçici 0x30ef41b4. ABI kodlu tek bir boole değeri döndürür;
bunu doğru ayrıştırmak rootHistory yapısına göre çok daha kolaydır.
| Çağrı | Seçici | Döndürdüğü |
|---|---|---|
isRootValid(bytes32) | 0x30ef41b4 | bool — bu kök hiç kabul edilmiş mi |
currentRoot() | 0xfdab463d | bytes32 — kabul edilen en son kök |
verifyRootValidity(bytes32) | 0xc7179944 | (bool isValid, uint32 blockHeight) |
currentBlockHeight() | 0x367bf2f9 | uint32 — en son kökün PIVX yüksekliği |
programVkey() | 0x09665ee7 | bytes32 — sözleşmenin dayattığı devre |
- 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;
}
Yanıtı yorumlama
isRootValid | Anlamı | Ne yapmalı |
|---|---|---|
true ve kök currentRoot() ile aynı | indeksleyici tümüyle senkronize | kabul edin |
true ama kök currentRoot() değerinden eski | indeksleyici zincirin gerisinde | kabul edin, isterseniz uyarın — kayıt o kök itibarıyla geçerliydi |
false | bu kök zincir üzerinde hiç kabul edilmedi | çözümlemeyi reddedin |
false yanıtı, karşılaştırdığınız ağacın protokol açısından var olmadığı anlamına gelir. Bu, tam
da saldırganın denetimindeki bir indeksleyicinin ürettiği durumdur ve bu adım tam olarak bunun
içindir.
Devrenin kendisini doğrulamak
Yukarıdaki iki adım, bir kaydın sözleşmenin kabul ettiği bir köke ait olduğunu ispatlar. Sözleşme
ise bir kökü ancak yanında geçerli bir ZK ispatı varsa kabul eder — ve o ispat, tam olarak
derlenmiş devreye yapılmış 32 baytlık bir taahhüt olan programVkey karşısında denetlenir.
Bu anahtarın yayımlanmış kaynak koda karşılık geldiğini doğrulayabilirsiniz:
git clone https://github.com/PIVX-Name/pivx-name-prover
cd pivx-name-prover/program
cargo prove build # plain build = mainnet
Elde ettiğiniz doğrulama anahtarını, çıpa sözleşmesindeki programVkey() (seçici 0x09665ee7)
değeriyle karşılaştırın. Eşleşiyorlarsa sözleşme tam olarak o kaynak kodu dayatıyordur. Devrenin
tek bir satırını değiştirmek farklı bir anahtar üretir; dolayısıyla değiştirilmiş bir devre,
değişiklik zincir üzerinde görünmeden yerine konulamaz.
Ek özellik olmadan derleyin
Düz bir cargo prove build, yayımlanmış sözleşmenin dayattığı mainnet devresini üretir. Herhangi
bir özellik bayrağı derlenen programı, dolayısıyla doğrulama anahtarını değiştirir ve bu anahtar
artık eşleşmez.
Bir SP1 ispatını doğrudan doğrulamak
İspatlar standart SP1 Groth16 ispatlarıdır ve SP1 SDK ile denetlenebilir:
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)?;
Açık değerler (bytes32 old_root, bytes32 new_root, uint32 end_block_height) yapısının ABI kodlamasıdır — toplu işin gerçekleştirdiği durum geçişi
ve kapsadığı PIVX blok yüksekliği.