El almacenamiento descentralizado
El almacenamiento descentralizado guarda archivos demasiado grandes para una transacción. La cadena solo registra el manifiesto y el depósito de cada archivo; los fragmentos viven en nodos registrados, cinco copias de cada uno, colocados mediante un sorteo que cualquier nodo puede repetir. El alquiler de los archivos paga a los poseedores que demuestran, en cada época, que siguen teniendo los bytes.
Fragmentos, raíz y manifiesto
Un archivo almacenado se corta en fragmentos de 1 MiB (1 048 576 bytes). Cada fragmento se nombra por su hash SHA3-256, y la identidad del archivo, su raíz, es un hash sobre el tamaño de fragmento, el tamaño total y la lista de ids de fragmentos:
piece id = SHA3-256(piece)
root = SHA3-256(piece size (u32) ‖ total size (u64) ‖ piece ids)
Los fragmentos se copian enteros; ZooBC no usa códigos de borrado. Una transacción StoreFile (tipo 40) ancla el manifiesto en la cadena: la raíz, el tamaño total, el tamaño de fragmento, un depósito de al menos 0.01 ZBC, de 1 a 100 000 ids de fragmentos y un bloque de acceso opcional. Los fragmentos en sí nunca van a la cadena. Un StoreFile para una raíz que ya está activa es una operación nula pagada; una raíz que se podó puede volver a almacenarse.
Para subir un archivo, una billetera lo envía a POST /api/v1/storage/upload de un nodo, que devuelve la raíz, los tamaños y los ids de fragmentos para el StoreFile. El nodo lo rechaza cuando el almacenamiento está desactivado (503), cuando el cuerpo está vacío (400), cuando tiene menos de 2 GiB libres o más de 256 MiB de fragmentos aún sin anclar (507), y cuando un cliente supera 10 subidas cada 600 segundos o 32 MiB por hora (429).
Quién guarda un fragmento
La colocación usa hashing de encuentro (rendezvous hashing). El tiempo se divide en épocas de almacenamiento de 240 bloques, aproximadamente una hora, en mainnet. Para un fragmento en una época, cada nodo registrado activo recibe un peso:
weight = SHA3-256(piece id ‖ node public key ‖ epoch seed)
La semilla de la época es el hash del bloque que abre la época, en la altura época × 240. Los cinco nodos con los pesos más altos son los poseedores esperados, siendo cinco el parámetro de la cadena storage_replication; con menos de cinco nodos, todos guardan el fragmento. Todo lo que hay en la fórmula son datos de la cadena, así que todos los nodos calculan los mismos cinco sin ningún anuncio. El recuento que paga a los poseedores lee el registro tal como estaba en el bloque anterior, de modo que no hay dos nodos que puedan discrepar sobre el conjunto.
Los poseedores cambian de una época a otra, y el relevo se solapa. En cada época un nodo descarga los fragmentos que tiene asignados, primero de los poseedores actuales, después de los anteriores y después de cualquier nodo. Solo acepta un fragmento que le envían si el hash coincide y es un poseedor esperado en esta época o en la anterior. Solo borra un fragmento no asignado si tampoco era poseedor en la época anterior y el archivo tiene al menos una hora.
El almacenamiento está activado por defecto: guardar fragmentos, como guardar los trozos del archivo histórico, es un deber de la plaza de un nodo registrado. Un operador puede desactivarlo en node.conf ([storage] enabled = false) y entonces solo pierde las recompensas de almacenamiento. El interruptor es un ajuste local que la cadena no ve, así que el sorteo no lo consulta: un nodo con el almacenamiento desactivado se sortea como cualquier otro, pero nunca demuestra nada, no gana nada por ese fragmento ni pierde puntuación, y el fragmento queda con una copia operativa menos. La replicación y la duración de la época siempre vienen de los parámetros de la cadena, diga lo que diga node.conf.
Pruebas de almacenamiento
La cadena nunca ve los fragmentos, así que no puede comprobar a un poseedor por sí misma. En su lugar, en cada época, cada poseedor responde a un desafío que solo puede calcular quien tenga los bytes:
challenge = SHA3-256(epoch seed ‖ piece id ‖ epoch (u64 LE))
proof = SHA3-256(piece bytes ‖ challenge)
Nadie puede conocer el desafío antes de que empiece la época. Los poseedores envían sus respuestas en transacciones SubmitStorageProof (tipo 41): deberes sin comisión firmados con la clave de un nodo registrado, en los que una sola transacción lleva todo el lote de un nodo para la época.
En cada límite de época, la cadena compara las respuestas. El valor enviado por más nodos distintos se toma como el verdadero, pero solo si ningún otro valor empata con él, y solo cuenta cuando al menos tres poseedores coinciden (oracle_quorum). Al propietario de cada poseedor que coincide se le paga entonces desde el fondo de almacenamiento:
reward = the active files' rent for one epoch ÷ the expected proofs (pieces × 5)
paid = min(reward, storage pool balance)
Un poseedor que queda en minoría no gana nada en esa época, pero no pierde puntuación de participación: como la cadena nunca tiene los bytes, el acuerdo entre poseedores es lo único que puede juzgar. Los poseedores que no envían nada quedan anotados en una tabla de auditoría. Un nodo que todavía no es maduro no gana nada, y su recompensa de almacenamiento se queda en el fondo (madurez e ingresos).
Alquiler
Una vez por periodo, 172 800 bloques (unos 30 días) en mainnet, la cadena pasa el alquiler de un periodo del depósito del archivo al fondo de almacenamiento (ZBCSTORE):
rent = max(1, ceil(size ÷ 1,048,576)) × price per MiB
El precio es el mismo que para los datasets, 0.05 ZBC por MiB al principio. El alquiler se cobra en mebibytes enteros, al menos uno: un archivo de 42 bytes paga 0.05 ZBC por periodo, y un archivo de 1 048 577 bytes, un byte más que 1 MiB, paga 0.1 ZBC. GET /api/v1/storage/quote?bytes=&blocks= de un nodo calcula el precio de un depósito con la misma fórmula.
Cualquier cuenta puede añadir dinero al depósito de un archivo activo con FundStoredFile (tipo 296), al menos 0.01 ZBC cada vez. Cuando el depósito no alcanza para cubrir un periodo completo, el archivo se poda: su registro se marca como no almacenado, y el resto va al fondo de longevidad.
Archivos privados
El bloque de acceso de un StoreFile puede marcar un archivo como privado (bit indicador 0) y añadir una lista de permitidos de 1 a 16 direcciones (bit indicador 1). La API de un nodo rechaza entonces una petición del contenido sin firmar (401) y una petición firmada de cualquiera que no sea el propietario o alguien de la lista (403). Los archivos privados quedan fuera de los listados públicos, y un archivo público que comparte un fragmento con un archivo privado activo queda protegido de la misma manera.
«Privado» es un modo de acceso, no un cifrado. Los fragmentos se siguen copiando sin cifrar a sus poseedores, y los nodos se sirven fragmentos entre sí, así que el operador de un nodo poseedor puede leer los bytes de su disco. Si el contenido importa, cífralo antes de subirlo: así tu clave protege el contenido, y el indicador de privado añade una segunda capa, más ligera.