Saltar al contenido principal

Verificar pruebas

Esta página muestra cómo verificar la resolución de un nombre sin confiar en el indexador que la sirvió. Todos los ejemplos de código que aparecen aquí se ejecutan contra un indexador real antes de publicarse.

Qué le aporta la verificación

Una resolución responde a «¿a qué dirección apunta richard.pivx?». Verificarla demuestra que la respuesta es la comprometida en la raíz de estado, y que esa raíz es una que el contrato ancla aceptó. Un indexador que invente una dirección, sirva un registro obsoleto u omita un nombre no puede producir una prueba que supere estas comprobaciones.

Hay dos pasos independientes, y necesita ambos:

  1. La prueba de Merkle vincula el registro con una raíz de estado.
  2. La comprobación en la cadena vincula esa raíz con el contrato ancla.

El paso 1 por sí solo no vale nada: un indexador malicioso puede construir un árbol falso completo y servir pruebas coherentes con su propia raíz falsa. El paso 2 es lo que da sentido a la raíz.


Paso 1 — verificar la prueba de Merkle

El árbol

PiNS usa un árbol de Merkle disperso compacto sobre un espacio de claves de 128 bits. Una clave son los primeros 16 bytes de SHA-256(domain_name). Al escribir un verificador importan dos propiedades:

  • Las pruebas son cortas y de longitud variable. Una hoja se sitúa a la menor profundidad en la que el prefijo de su clave es único, así que una prueba lleva proof_depth hermanos: por lo general un puñado, no 128. Nunca dé por supuesta una longitud fija; lea proof_depth y compruebe que coincide con merkle_proof.length.
  • Un subárbol vacío son 32 bytes a cero a cualquier altura. No hay una escalera de hashes precalculados para nodos vacíos. Por eso la raíz de un árbol vacío es toda ceros.

Las funciones hash

Las hojas y los nodos internos van etiquetados para que sus dominios de hash no se solapen. Olvidar el byte de etiqueta es, con diferencia, el error más frecuente: produce un hash de aspecto plausible que jamás coincide con la raíz.

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 y target_address son bytes UTF-8 sin prefijo de longitud; owner_pubkey son los 32 bytes en bruto; price y nonce son enteros de 64 bits sin signo en little-endian.

El recorrido

Vaya de la hoja de vuelta a la raíz. Con n = proof_depth hermanos, en el paso i el bit que decide de qué lado está es el bit n - 1 - i de la clave, contando desde el bit más significativo del 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] es el hermano del nivel más profundo, el más cercano a la hoja.

El terminal

Toda prueba lleva un proof_terminal que describe qué hay al final del camino:

TerminalSignificado
Occupiedahí está la hoja propia de la clave: el nombre existe
Vacantahí hay un subárbol vacío: el nombre no está registrado
Blockedahí está la hoja de un nombre distinto: el nombre no está registrado, y la respuesta incluye el registro completo de ese otro nombre

Una resolución correcta debe ser Occupied. Vacant y Blocked son pruebas de ausencia: permiten verificar que un nombre realmente no está registrado, en lugar de aceptar por fe el «no encontrado» de un indexador. Rechace toda resolución cuyo terminal no sea 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()
tip

Pruebe su implementación frente a la manipulación

Un verificador que siempre devuelve true supera cualquier prueba positiva. Antes de fiarse del suyo, cambie un byte de target_address, owner_pubkey o price en una respuesta real y confirme que ahora devuelve false. Los tres ejemplos anteriores se comprueban así.


Paso 2 — contrastar la raíz con el contrato ancla

Una prueba de Merkle verificada solo dice «este registro está en algún árbol». Para saber que está en el árbol auténtico, pregunte al contrato ancla en BNB Smart Chain si alguna vez aceptó esa raíz.

Use isRootValid(bytes32) — selector 0x30ef41b4. Devuelve un único booleano codificado en ABI, mucho más fácil de interpretar correctamente que la estructura rootHistory.

LlamadaSelectorDevuelve
isRootValid(bytes32)0x30ef41b4bool — si esta raíz se aceptó alguna vez
currentRoot()0xfdab463dbytes32 — la última raíz aceptada
verifyRootValidity(bytes32)0xc7179944(bool isValid, uint32 blockHeight)
currentBlockHeight()0x367bf2f9uint32 — altura de PIVX de la última raíz
programVkey()0x09665ee7bytes32 — el circuito que impone el contrato
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

Cómo interpretar la respuesta

isRootValidSignificadoQué hacer
true, y la raíz es igual a currentRoot()el indexador está completamente sincronizadoaceptar
true, pero la raíz es anterior a currentRoot()el indexador va por detrás de la cadenaaceptar, opcionalmente avisar: el registro era válido en esa raíz
falseesta raíz nunca se aceptó en la cadenarechazar la resolución

Un false significa que el árbol contra el que verificó no existe a efectos del protocolo. Ese es exactamente el caso que produce un indexador controlado por un atacante, y justo para eso sirve este paso.


Verificar el propio circuito

Los dos pasos anteriores demuestran que un registro pertenece a una raíz que el contrato aceptó. El contrato solo acepta una raíz si la acompañaba una prueba ZK válida, y esa prueba se comprueba contra programVkey, un compromiso de 32 bytes con el circuito compilado exacto.

Puede confirmar que esa clave corresponde al código fuente publicado:

git clone https://github.com/PIVX-Name/pivx-name-prover
cd pivx-name-prover/program
cargo prove build # plain build = mainnet

Compare la clave de verificación resultante con programVkey() (selector 0x09665ee7) en el contrato ancla. Si coinciden, el contrato está imponiendo exactamente ese código fuente. Cambiar una sola línea del circuito produce una clave distinta, así que no puede sustituirse un circuito modificado sin que el cambio se vea en la cadena.

aviso

Compile sin características adicionales

Un cargo prove build sin más produce el circuito de mainnet que impone el contrato desplegado. Cualquier indicador de característica cambia el programa compilado y, por tanto, su clave de verificación, que ya no coincidirá.

Verificar una prueba SP1 directamente

Las pruebas son pruebas SP1 Groth16 estándar y pueden comprobarse con el SDK de 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)?;

Los valores públicos son la codificación ABI de (bytes32 old_root, bytes32 new_root, uint32 end_block_height): la transición de estado que realizó el lote y la altura de bloque de PIVX que cubre.