Verificar provas
Esta página mostra como verificar a resolução de um nome sem confiar no indexador que a serviu. Todos os exemplos de código aqui presentes são executados contra um indexador real antes da publicação.
O que a verificação lhe dá
Uma resolução responde a «para que endereço aponta richard.pivx?». Verificá-la prova que a
resposta é aquela com que a raiz de estado se comprometeu — e que essa raiz é uma que o contrato
âncora aceitou. Um indexador que invente um endereço, sirva um registo desatualizado ou omita um
nome não consegue produzir uma prova que passe estas verificações.
Há dois passos independentes, e precisa de ambos:
- A prova de Merkle liga o registo a uma raiz de estado.
- A verificação na cadeia liga essa raiz ao contrato âncora.
O passo 1 sozinho não vale nada: um indexador malicioso pode construir uma árvore falsa completa e servir provas coerentes com a sua própria raiz falsa. É o passo 2 que dá sentido à raiz.
Passo 1 — verificar a prova de Merkle
A árvore
O PiNS usa uma árvore de Merkle esparsa compacta sobre um espaço de chaves de 128 bits. Uma
chave são os primeiros 16 bytes de SHA-256(domain_name). Ao escrever um verificador, importam
duas propriedades:
- As provas são curtas e de comprimento variável. Uma folha fica à menor profundidade em que
o prefixo da sua chave é único, pelo que uma prova traz
proof_depthirmãos — normalmente um punhado, não 128. Nunca presuma um comprimento fixo; leiaproof_depthe confirme que corresponde amerkle_proof.length. - Uma subárvore vazia são 32 bytes a zero a qualquer altura. Não há uma escada de hashes pré-calculados para nós vazios. Por isso a raiz de uma árvore vazia é toda a zeros.
As funções de hash
As folhas e os nós internos são etiquetados para que os seus domínios de hash não se sobreponham. Omitir o byte da etiqueta é, de longe, o erro mais comum — produz um hash de aspeto plausível que nunca corresponde à raiz.
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 e target_address são bytes UTF-8 sem prefixo de comprimento; owner_pubkey são os 32
bytes em bruto; price e nonce são inteiros de 64 bits sem sinal em little-endian.
O percurso
Vá da folha de volta à raiz. Com n = proof_depth irmãos, no passo i o bit que decide de que
lado está é o bit n - 1 - i da chave, contado a partir do bit mais significativo do 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] é o irmão do nível mais profundo, o mais próximo da folha.
O terminal
Todas as provas trazem um proof_terminal que descreve o que está no fim do caminho:
| Terminal | Significado |
|---|---|
Occupied | ali está a folha própria da chave — o nome existe |
Vacant | ali está uma subárvore vazia — o nome não está registado |
Blocked | ali está a folha de um nome diferente — o nome não está registado, e a resposta inclui o registo completo desse outro nome |
Uma resolução bem-sucedida tem de ser Occupied. Vacant e Blocked são provas de ausência:
permitem verificar que um nome realmente não está registado, em vez de aceitar por fé o «não
encontrado» de um indexador. Rejeite qualquer resolução cujo terminal não seja Occupied.
Código
- 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();
}
Teste a sua implementação contra adulteração
Um verificador que devolve sempre true passa em qualquer teste positivo. Antes de confiar no
seu, altere um byte de target_address, owner_pubkey ou price numa resposta real e confirme
que agora devolve false. Os três exemplos acima são verificados desta forma.
Passo 2 — comparar a raiz com o contrato âncora
Uma prova de Merkle verificada diz apenas «este registo está em alguma árvore». Para saber que está na árvore verdadeira, pergunte ao contrato âncora na BNB Smart Chain se alguma vez aceitou essa raiz.
Use isRootValid(bytes32) — seletor 0x30ef41b4. Devolve um único booleano codificado em ABI,
bem mais fácil de interpretar corretamente do que a estrutura rootHistory.
| Chamada | Seletor | Devolve |
|---|---|---|
isRootValid(bytes32) | 0x30ef41b4 | bool — se esta raiz alguma vez foi aceite |
currentRoot() | 0xfdab463d | bytes32 — a última raiz aceite |
verifyRootValidity(bytes32) | 0xc7179944 | (bool isValid, uint32 blockHeight) |
currentBlockHeight() | 0x367bf2f9 | uint32 — altura PIVX da última raiz |
programVkey() | 0x09665ee7 | bytes32 — o circuito que o contrato impõe |
- 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;
}
Como interpretar a resposta
isRootValid | Significado | O que fazer |
|---|---|---|
true, e a raiz é igual a currentRoot() | o indexador está totalmente sincronizado | aceitar |
true, mas a raiz é anterior a currentRoot() | o indexador está atrasado face à cadeia | aceitar, opcionalmente avisar — o registo era válido nessa raiz |
false | esta raiz nunca foi aceite na cadeia | rejeitar a resolução |
Um false significa que a árvore contra a qual verificou não existe do ponto de vista do
protocolo. É exatamente o caso que um indexador controlado por um atacante produz, e é
precisamente para isso que serve este passo.
Verificar o próprio circuito
Os dois passos acima provam que um registo pertence a uma raiz que o contrato aceitou. O contrato
só aceita uma raiz se esta vier acompanhada de uma prova ZK válida — e essa prova é verificada
contra a programVkey, um compromisso de 32 bytes com o circuito compilado exato.
Pode confirmar que essa chave corresponde ao código-fonte publicado:
git clone https://github.com/PIVX-Name/pivx-name-prover
cd pivx-name-prover/program
cargo prove build # plain build = mainnet
Compare a chave de verificação resultante com programVkey() (seletor 0x09665ee7) no contrato
âncora. Se coincidirem, o contrato está a impor exatamente esse código-fonte. Alterar uma única
linha do circuito produz uma chave diferente, pelo que um circuito modificado não pode ser
substituído sem que a alteração fique visível na cadeia.
Compile sem funcionalidades adicionais
Um cargo prove build simples produz o circuito de mainnet que o contrato implantado impõe.
Qualquer flag de funcionalidade altera o programa compilado e, por conseguinte, a sua chave de
verificação, que deixará de corresponder.
Verificar diretamente uma prova SP1
As provas são provas SP1 Groth16 padrão e podem ser verificadas com o SDK do 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)?;
Os valores públicos são a codificação ABI de (bytes32 old_root, bytes32 new_root, uint32 end_block_height) — a transição de estado que o lote realizou e a altura do bloco PIVX
que cobre.