Kiểm chứng bằng chứng
Trang này chỉ cách kiểm chứng một kết quả phân giải tên mà không cần tin trình lập chỉ mục đã trả về nó. Mọi ví dụ mã ở đây đều được chạy với một trình lập chỉ mục thật trước khi xuất bản.
Việc kiểm chứng mang lại điều gì
Một kết quả phân giải trả lời «richard.pivx trỏ tới địa chỉ nào?». Kiểm chứng nó chứng minh rằng
câu trả lời đúng là câu đã được cam kết vào gốc trạng thái — và rằng gốc đó là một gốc mà hợp đồng
neo đã chấp nhận. Một trình lập chỉ mục bịa ra địa chỉ, trả về bản ghi cũ hay giấu một cái tên đều
không thể tạo ra bằng chứng vượt qua những kiểm tra này.
Có hai bước độc lập, và bạn cần cả hai:
- Bằng chứng Merkle buộc bản ghi vào một gốc trạng thái.
- Việc kiểm tra trên chuỗi buộc gốc đó vào hợp đồng neo.
Riêng bước 1 thì vô giá trị: một trình lập chỉ mục xấu có thể dựng nguyên một cây giả và trả về các bằng chứng nhất quán với gốc giả của chính nó. Chính bước 2 mới làm cho gốc có ý nghĩa.
Bước 1 — kiểm chứng bằng chứng Merkle
Cây
PiNS dùng một cây Merkle thưa nén trên không gian khóa 128 bit. Một khóa là 16 byte đầu của
SHA-256(domain_name). Khi viết bộ kiểm chứng, có hai tính chất quan trọng:
- Bằng chứng ngắn và có độ dài thay đổi. Một lá nằm ở độ sâu nông nhất mà tiền tố khóa của nó là
duy nhất, nên một bằng chứng mang
proof_depthanh em — thường chỉ vài cái, không phải 128. Đừng bao giờ giả định độ dài cố định; hãy đọcproof_depthvà kiểm tra rằng nó khớp vớimerkle_proof.length. - Một cây con rỗng là 32 byte 0 ở mọi độ cao. Không có bậc thang băm tiền tính cho các nút rỗng. Vì thế gốc của một cây rỗng toàn số 0.
Các hàm băm
Lá và nút trong được gắn thẻ để miền băm của chúng không chồng lấn. Bỏ sót byte thẻ là lỗi phổ biến nhất — nó tạo ra một băm trông hợp lý nhưng không bao giờ khớp với gốc.
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 và target_address là byte UTF-8 không có tiền tố độ dài; owner_pubkey là 32 byte thô;
price và nonce là số nguyên 64 bit không dấu dạng little-endian.
Cách duyệt
Đi từ lá ngược về gốc. Với n = proof_depth anh em, ở bước i bit quyết định bạn đang ở phía nào là
bit n - 1 - i của khóa, đếm từ bit có trọng số lớn nhất của byte 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] là anh em ở mức sâu nhất, gần lá nhất.
Terminal
Mọi bằng chứng đều mang một proof_terminal mô tả thứ nằm ở cuối đường đi:
| Terminal | Ý nghĩa |
|---|---|
Occupied | ở đó là lá của chính khóa — tên tồn tại |
Vacant | ở đó là một cây con rỗng — tên chưa được đăng ký |
Blocked | ở đó là lá của một tên khác — tên chưa được đăng ký, và phản hồi kèm bản ghi đầy đủ của tên kia |
Một kết quả phân giải thành công phải là Occupied. Vacant và Blocked là bằng chứng vắng mặt:
chúng cho phép bạn kiểm chứng rằng một cái tên thực sự chưa được đăng ký, thay vì tin lời «không tìm
thấy» của trình lập chỉ mục. Hãy từ chối mọi kết quả phân giải có terminal khác Occupied.
Mã
- 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();
}
Hãy thử bộ kiểm chứng của bạn trước hành vi sửa đổi
Một bộ kiểm chứng luôn trả về true sẽ vượt qua mọi bài kiểm tra dương tính. Trước khi tin bộ của
mình, hãy đổi một byte của target_address, owner_pubkey hoặc price trong một phản hồi thật và
xác nhận rằng giờ nó trả về false. Cả ba ví dụ ở trên đều được kiểm theo cách này.
Bước 2 — đối chiếu gốc với hợp đồng neo
Một bằng chứng Merkle đã kiểm chứng chỉ nói «bản ghi này nằm trong một cây nào đó». Để biết nó nằm trong cây thật, hãy hỏi hợp đồng neo trên BNB Smart Chain xem nó có từng chấp nhận gốc đó không.
Dùng isRootValid(bytes32) — bộ chọn 0x30ef41b4. Nó trả về một giá trị boolean mã hóa ABI duy
nhất, dễ phân tích đúng hơn nhiều so với cấu trúc rootHistory.
| Lời gọi | Bộ chọn | Trả về |
|---|---|---|
isRootValid(bytes32) | 0x30ef41b4 | bool — gốc này đã từng được chấp nhận chưa |
currentRoot() | 0xfdab463d | bytes32 — gốc được chấp nhận gần nhất |
verifyRootValidity(bytes32) | 0xc7179944 | (bool isValid, uint32 blockHeight) |
currentBlockHeight() | 0x367bf2f9 | uint32 — chiều cao PIVX của gốc gần nhất |
programVkey() | 0x09665ee7 | bytes32 — mạch mà hợp đồng bắt buộc |
- 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;
}
Diễn giải câu trả lời
isRootValid | Ý nghĩa | Nên làm gì |
|---|---|---|
true, và gốc bằng currentRoot() | trình lập chỉ mục đã đồng bộ hoàn toàn | chấp nhận |
true, nhưng gốc cũ hơn currentRoot() | trình lập chỉ mục đang tụt sau chuỗi | chấp nhận, có thể cảnh báo — bản ghi hợp lệ tại thời điểm gốc đó |
false | gốc này chưa bao giờ được chấp nhận trên chuỗi | từ chối kết quả phân giải |
Giá trị false nghĩa là cái cây mà bạn đối chiếu không tồn tại dưới góc nhìn của giao thức. Đó
chính là tình huống mà một trình lập chỉ mục do kẻ tấn công kiểm soát tạo ra, và bước này sinh ra
đúng vì lý do đó.
Kiểm chứng chính cái mạch
Hai bước ở trên chứng minh rằng một bản ghi thuộc về một gốc mà hợp đồng đã chấp nhận. Hợp đồng chỉ
chấp nhận một gốc nếu đi kèm là một bằng chứng ZK hợp lệ — và bằng chứng đó được kiểm với
programVkey, một cam kết 32 byte với đúng cái mạch đã biên dịch.
Bạn có thể xác nhận khóa đó tương ứng với mã nguồn đã công bố:
git clone https://github.com/PIVX-Name/pivx-name-prover
cd pivx-name-prover/program
cargo prove build # plain build = mainnet
So sánh khóa xác minh thu được với programVkey() (bộ chọn 0x09665ee7) trên hợp đồng neo. Nếu
chúng khớp nhau, hợp đồng đang bắt buộc đúng mã nguồn đó. Đổi một dòng duy nhất của mạch sẽ cho ra
khóa khác, nên không thể tráo một mạch đã sửa đổi mà thay đổi đó không lộ ra trên chuỗi.
Hãy biên dịch không kèm tính năng bổ sung
Một lệnh cargo prove build thuần túy tạo ra mạch mainnet mà hợp đồng đã triển khai bắt buộc. Bất
kỳ cờ tính năng nào cũng thay đổi chương trình đã biên dịch, và do đó thay đổi khóa xác minh của nó,
khiến nó không còn khớp nữa.
Kiểm chứng trực tiếp một bằng chứng SP1
Các bằng chứng là bằng chứng SP1 Groth16 tiêu chuẩn và có thể kiểm bằng SDK của 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)?;
Các giá trị công khai là mã hóa ABI của (bytes32 old_root, bytes32 new_root, uint32 end_block_height) — phép chuyển trạng thái mà lô đã thực hiện, và chiều cao khối PIVX mà nó
bao phủ.