Перейти до основного вмісту

Перевірка доказів

Ця сторінка показує, як перевірити розв'язання імені, не довіряючи індексатору, який його видав. Кожен приклад коду тут перед публікацією виконується на живому індексаторі.

Що дає перевірка

Розв'язання імені відповідає на запитання «на яку адресу вказує richard.pivx?». Перевірка доводить, що ця відповідь — саме та, яку зафіксовано в корені стану, і що цей корінь прийнято якірним контрактом. Індексатор, який вигадає адресу, віддасть застарілий запис чи приховає ім'я, не зможе пред'явити доказ, який проходить ці перевірки.

Кроків два, вони незалежні, і потрібні обидва:

  1. Доказ Меркла пов'язує запис із коренем стану.
  2. Перевірка в блокчейні пов'язує цей корінь із якірним контрактом.

Один лише крок 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.

Код

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()
порада

Перевірте свою реалізацію на підробці

Верифікатор, який завжди повертає true, проходить будь-який позитивний тест. Перш ніж довіряти своєму, змініть один байт у target_address, owner_pubkey чи price у реальній відповіді і переконайтеся, що тепер він повертає false. Усі три приклади вище перевірені саме так.


Крок 2 — звірте корінь із якірним контрактом

Перевірений доказ Меркла каже лише «цей запис є в якомусь дереві». Щоб дізнатися, що він у справжньому дереві, запитайте в якірного контракту в BNB Smart Chain, чи приймав він колись цей корінь.

Використовуйте isRootValid(bytes32) — селектор 0x30ef41b4. Він повертає одне логічне значення в ABI-кодуванні, яке розібрати правильно куди простіше, ніж структуру rootHistory.

ВикликСелекторПовертає
isRootValid(bytes32)0x30ef41b4bool — чи приймався колись цей корінь
currentRoot()0xfdab463dbytes32 — останній прийнятий корінь
verifyRootValidity(bytes32)0xc7179944(bool isValid, uint32 blockHeight)
currentBlockHeight()0x367bf2f9uint32 — висота PIVX для останнього кореня
programVkey()0x09665ee7bytes32 — схема, якої вимагає контракт
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

Як читати відповідь

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, яку він покриває.