Pular para o conteúdo principal

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:

  1. A prova de Merkle liga o registo a uma raiz de estado.
  2. 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_depth irmãos — normalmente um punhado, não 128. Nunca presuma um comprimento fixo; leia proof_depth e confirme que corresponde a merkle_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:

TerminalSignificado
Occupiedali está a folha própria da chave — o nome existe
Vacantali está uma subárvore vazia — o nome não está registado
Blockedali 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

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()
dica

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.

ChamadaSeletorDevolve
isRootValid(bytes32)0x30ef41b4bool — se esta raiz alguma vez foi aceite
currentRoot()0xfdab463dbytes32 — a última raiz aceite
verifyRootValidity(bytes32)0xc7179944(bool isValid, uint32 blockHeight)
currentBlockHeight()0x367bf2f9uint32 — altura PIVX da última raiz
programVkey()0x09665ee7bytes32 — o circuito que o contrato impõe
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

Como interpretar a resposta

isRootValidSignificadoO que fazer
true, e a raiz é igual a currentRoot()o indexador está totalmente sincronizadoaceitar
true, mas a raiz é anterior a currentRoot()o indexador está atrasado face à cadeiaaceitar, opcionalmente avisar — o registo era válido nessa raiz
falseesta raiz nunca foi aceite na cadeiarejeitar 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.

aviso

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.