Skip to main content

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

Гэтая старонка паказвае, як праверыць вырашэнне імя, не давяраючы індэксатару, які яго выдаў. Кожны прыклад кода тут перад публікацыяй выконваецца на жывым індэксатары.

Што дае праверка

Вырашэнне імя адказвае на пытанне «на які адрас указвае 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()
tip

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

Верыфікатар, які заўсёды вяртае 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, якую ён пакрывае.