Праверка доказаў
Гэтая старонка паказвае, як праверыць вырашэнне імя, не давяраючы індэксатару, які яго выдаў. Кожны прыклад кода тут перад публікацыяй выконваецца на жывым індэксатары.
Што дае праверка
Вырашэнне імя адказвае на пытанне «на які адрас указвае 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, якую ён пакрывае.