Перейти к основному содержимому

Проверка доказательств

Эта страница показывает, как проверить разрешение имени, не доверяя выдавшему его индексатору. Каждый пример кода здесь перед публикацией выполняется на живом индексаторе.

Что даёт проверка

Разрешение имени отвечает на вопрос «на какой адрес указывает 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) в якорном контракте. Если они совпадают, контракт требует именно этот исходный код. Изменение хотя бы одной строки схемы даёт другой ключ, поэтому подменить схему незаметно для блокчейна невозможно.

warning

Собирайте без дополнительных фич

Обычный 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, которую он покрывает.