Перевірка доказів
Ця сторінка показує, як перевірити розв'язання імені, не довіряючи індексатору, який його видав. Кожен приклад коду тут перед публікацією виконується на живому індексаторі.
Що дає перевірка
Розв'язання імені відповідає на запитання «на яку адресу вказує richard.pivx?». Перевірка
доводить, що ця відповідь — саме та, яку зафіксовано в корені стану, і що цей корінь прийнято
якірним контрактом. Індексатор, який вигадає адресу, віддасть застарілий запис чи приховає ім'я,
не зможе пред'явити доказ, який проходить ці перевірки.
Кроків два, вони незалежні, і потрібні обидва:
- Доказ Меркла пов'язує запис із коренем стану.
- Перевірка в блокчейні пов'язує цей корінь із якірним контрактом.
Один лише крок 1 марний: шкідливий індексатор може побудувати ціле підроблене дерево і видавати узгоджені докази відносно власного підробленого кореня. Сенс кореню надає саме крок 2.
Крок 1 — перевірте доказ Меркла
Дерево
PiNS використовує компактне розріджене дерево Меркла над 128-бітовим простором ключів. Ключ —
це перші 16 байтів SHA-256(domain_name). При написанні верифікатора важливі дві властивості:
- Докази короткі та змінної довжини. Лист розміщується на мінімальній глибині, де префікс його
ключа унікальний, тому доказ несе
proof_depthсусідів — зазвичай кілька штук, а не 128. Ніколи не розраховуйте на фіксовану довжину; читайтеproof_depthі перевіряйте, що воно збігається зmerkle_proof.length. - Порожнє піддерево — це 32 нульові байти на будь-якій висоті. Жодних сходів заздалегідь обчислених хешів порожніх вузлів немає. Тому корінь порожнього дерева складається з самих нулів.
Хеш-функції
Листи і внутрішні вузли позначаються тегами, щоб їхні області хешування не перетиналися. Пропуск байта тега — найчастіша помилка: вона дає правдоподібний на вигляд хеш, який ніколи не збіжиться з коренем.
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 і target_address — це байти UTF-8 без префікса довжини; owner_pubkey — сирі 32 байти;
price і nonce — беззнакові 64-бітові little-endian.
Обхід
Ідіть від листа назад до кореня. За n = proof_depth сусідів на кроці i бік визначає біт
n - 1 - i ключа, рахуючи від старшого біта байта 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] — це сусід на найглибшому рівні, найближчий до листа.
Термінал
Кожен доказ несе proof_terminal, що описує, яка річ розташована в кінці шляху:
| Термінал | Значення |
|---|---|
Occupied | там власний лист ключа — ім'я існує |
Vacant | там порожнє піддерево — ім'я не зареєстроване |
Blocked | там лист іншого імені — запитане ім'я не зареєстроване, а відповідь містить повний запис того імені |
Успішне розв'язання має мати термінал Occupied. Vacant і Blocked — це докази відсутності:
вони дозволяють переконатися, що ім'я справді не зареєстроване, а не приймати на віру «не
знайдено» від індексатора. Відхиляйте будь-яке розв'язання, термінал якого відрізняється від
Occupied.
Код
- 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();
}
Перевірте свою реалізацію на підробці
Верифікатор, який завжди повертає true, проходить будь-який позитивний тест. Перш ніж довіряти
своєму, змініть один байт у target_address, owner_pubkey чи price у реальній відповіді і
переконайтеся, що тепер він повертає false. Усі три приклади вище перевірені саме так.
Крок 2 — звірте корінь із якірним контрактом
Перевірений доказ Меркла каже лише «цей запис є в якомусь дереві». Щоб дізнатися, що він у справжньому дереві, запитайте в якірного контракту в BNB Smart Chain, чи приймав він колись цей корінь.
Використовуйте isRootValid(bytes32) — селектор 0x30ef41b4. Він повертає одне логічне значення в
ABI-кодуванні, яке розібрати правильно куди простіше, ніж структуру rootHistory.
| Виклик | Селектор | Повертає |
|---|---|---|
isRootValid(bytes32) | 0x30ef41b4 | bool — чи приймався колись цей корінь |
currentRoot() | 0xfdab463d | bytes32 — останній прийнятий корінь |
verifyRootValidity(bytes32) | 0xc7179944 | (bool isValid, uint32 blockHeight) |
currentBlockHeight() | 0x367bf2f9 | uint32 — висота PIVX для останнього кореня |
programVkey() | 0x09665ee7 | bytes32 — схема, якої вимагає контракт |
- 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;
}
Як читати відповідь
isRootValid | Значення | Що робити |
|---|---|---|
true, і корінь дорівнює currentRoot() | індексатор повністю синхронізований | прийняти |
true, але корінь старіший за currentRoot() | індексатор відстає від ланцюжка | прийняти, за бажанням попередити — на момент того кореня запис був правильний |
false | цей корінь ніколи не приймався в блокчейні | відхилити розв'язання |
Значення false означає, що дерева, відносно якого ви перевіряли, з погляду протоколу не існує.
Саме такий випадок створює індексатор під контролем зловмисника, і саме заради нього потрібен цей
крок.
Перевірка самої схеми
Два кроки вище доводять, що запис належить кореню, який прийняв контракт. А корінь контракт приймає
лише разом із коректним ZK-доказом, який перевіряється відносно programVkey — 32-байтового
зобов'язання до конкретної скомпільованої схеми.
Переконатися, що цей ключ відповідає опублікованому вихідному коду, можна так:
git clone https://github.com/PIVX-Name/pivx-name-prover
cd pivx-name-prover/program
cargo prove build # plain build = mainnet
Порівняйте отриманий ключ верифікації з programVkey() (селектор 0x09665ee7) у якірному
контракті. Якщо вони збігаються, контракт вимагає саме цей вихідний код. Зміна хоча б одного рядка
схеми дає інший ключ, тому підмінити схему непомітно для блокчейну неможливо.
Збирайте без додаткових фіч
Звичайний cargo prove build дає схему основної мережі, якої вимагає розгорнутий контракт. Будь-який
прапорець фічі змінює скомпільовану програму, а отже і її ключ верифікації, який після цього не
збіжиться.
Пряма перевірка доказу SP1
Докази — це стандартні докази SP1 Groth16, і перевірити їх можна за допомогою SDK SP1:
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)?;
Публічні значення — це ABI-кодування (bytes32 old_root, bytes32 new_root, uint32 end_block_height): перехід стану, виконаний пакетом, і висота блоку PIVX, яку він покриває.