Wiki di ZooBC
Dati e archiviazione

Il pool di archiviazione decentralizzata

L'archiviazione decentralizzata conserva i file troppo grandi per una transazione. La catena registra solo il manifest e il deposito di ogni file; i frammenti vivono sui nodi registrati, in cinque copie ciascuno, collocati con un'estrazione che ogni nodo può ripetere. Il canone dei file paga i detentori che dimostrano, a ogni epoca, di avere ancora i byte.

Frammenti, radice e manifest

Un file archiviato viene diviso in frammenti da 1 MiB (1.048.576 byte). Ogni frammento prende il nome dal suo hash SHA3-256, e l'identità del file, la sua radice, è un hash calcolato sulla dimensione dei frammenti, sulla dimensione totale e sulla lista degli id dei frammenti:

piece id = SHA3-256(piece)
root     = SHA3-256(piece size (u32) ‖ total size (u64) ‖ piece ids)

I frammenti vengono copiati per intero; ZooBC non usa codici di cancellazione (erasure coding). Una transazione StoreFile (tipo 40) ancora il manifest sulla catena: la radice, la dimensione totale, la dimensione dei frammenti, un deposito di almeno 0,01 ZBC, da 1 a 100.000 id di frammenti e un blocco di accesso facoltativo. I frammenti stessi non vanno mai sulla catena. Uno StoreFile per una radice già attiva è una no-op pagata; una radice che è stata eliminata può essere archiviata di nuovo.

Per caricare un file, un wallet lo invia a POST /api/v1/storage/upload di un nodo, che restituisce la radice, le dimensioni e gli id dei frammenti per lo StoreFile. Il nodo rifiuta quando l'archiviazione è disattivata (503), quando il corpo è vuoto (400), quando ha meno di 2 GiB liberi o più di 256 MiB di frammenti non ancora ancorati (507), e quando un client supera 10 caricamenti ogni 600 secondi o 32 MiB all'ora (429).

Un file viene diviso in frammenti da 1 MiB; il manifest va sulla catena, e ogni frammento va a cinque nodi.
Un file viene diviso in frammenti da 1 MiB; il manifest va sulla catena, e ogni frammento va a cinque nodi.

Chi detiene un frammento

Il posizionamento usa il rendezvous hashing. Il tempo è diviso in epoche di archiviazione di 240 blocchi, circa un'ora, su mainnet. Per un frammento in un'epoca, ogni nodo registrato attivo riceve un peso:

weight = SHA3-256(piece id ‖ node public key ‖ epoch seed)

Il seed dell'epoca è l'hash del blocco che apre l'epoca, all'altezza epoca × 240. I cinque nodi con i pesi più alti sono i detentori attesi, dove cinque è il parametro della catena storage_replication; con meno di cinque nodi, il frammento lo detengono tutti. Tutto nella formula è un dato della catena, quindi ogni nodo calcola gli stessi cinque senza alcun annuncio. Il conteggio che paga i detentori legge il registro com'era al blocco precedente, quindi due nodi non possono essere in disaccordo sull'insieme.

I detentori cambiano da un'epoca all'altra, e il passaggio di consegne si sovrappone. A ogni epoca un nodo scarica i frammenti assegnatigli, prima dai detentori attuali, poi da quelli precedenti, poi da qualsiasi nodo. Accetta un frammento inviatogli solo se l'hash corrisponde ed è un detentore atteso in questa epoca o nella precedente. Cancella un frammento non assegnato solo se non ne era detentore nemmeno nell'epoca precedente e il file ha almeno un'ora.

L'archiviazione è attiva per impostazione predefinita: detenere frammenti, come detenere le porzioni dell'archivio, è un compito del seggio di un nodo registrato. Un operatore può disattivarla in node.conf ([storage] enabled = false), e in tal caso perde solo le ricompense di archiviazione. L'interruttore è un'impostazione locale che la catena non può vedere, quindi l'estrazione dei detentori non ne tiene conto: un nodo disattivato viene estratto come qualsiasi altro, ma non dimostra mai, non guadagna nulla per quel frammento e non perde punteggio, e il frammento ha una copia funzionante in meno. La replica e la durata dell'epoca provengono sempre dai parametri della catena, qualunque cosa dica node.conf.

Prove di archiviazione

La catena non vede mai i frammenti, quindi non può verificare da sola un detentore. Al loro posto, a ogni epoca, ogni detentore risponde a una sfida che solo chi possiede i byte può calcolare:

challenge = SHA3-256(epoch seed ‖ piece id ‖ epoch (u64 LE))
proof     = SHA3-256(piece bytes ‖ challenge)

Nessuno può conoscere la sfida prima che l'epoca inizi. I detentori inviano le loro risposte in transazioni SubmitStorageProof (tipo 41): compiti esenti da commissioni firmati con la chiave di un nodo registrato, con un'unica transazione che contiene l'intero batch di un nodo per l'epoca.

A ogni fine epoca la catena confronta le risposte. Il valore inviato dal maggior numero di nodi distinti viene preso come verità, ma solo se nessun altro valore è in parità con esso, e conta solo quando almeno tre detentori sono d'accordo (oracle_quorum). Il proprietario di ciascun detentore concorde viene quindi pagato dal pool di archiviazione:

reward = the active files' rent for one epoch ÷ the expected proofs (pieces × 5)
paid   = min(reward, storage pool balance)

Un detentore messo in minoranza non guadagna nulla per quell'epoca ma non perde punteggio di partecipazione: poiché la catena non detiene mai i byte, l'accordo tra i detentori è tutto ciò che può giudicare. I detentori che non inviano nulla vengono registrati in una tabella di audit. Un nodo non ancora maturo non guadagna nulla, e la sua ricompensa di archiviazione resta nel pool (maturità e guadagni).

I detentori rispondono a una nuova sfida a ogni epoca; quando almeno tre sono d'accordo e nessuna risposta è in parità, ogni detentore concorde viene pagato.
I detentori rispondono a una nuova sfida a ogni epoca; quando almeno tre sono d'accordo e nessuna risposta è in parità, ogni detentore concorde viene pagato.

Canone

Una volta per periodo, 172.800 blocchi (circa 30 giorni) su mainnet, la catena sposta il canone di un periodo dal deposito del file al pool di archiviazione (ZBCSTORE):

rent = max(1, ceil(size ÷ 1,048,576)) × price per MiB

Il prezzo è lo stesso dei dataset, 0,05 ZBC per MiB all'inizio. Il canone si addebita in mebibyte interi, almeno uno: un file di 42 byte paga 0,05 ZBC per periodo, e un file di 1.048.577 byte, un byte oltre 1 MiB, paga 0,1 ZBC. GET /api/v1/storage/quote?bytes=&blocks= di un nodo calcola il prezzo di un deposito con la stessa formula.

Qualsiasi account può aggiungere denaro al deposito di un file attivo con FundStoredFile (tipo 296), almeno 0,01 ZBC alla volta. Quando il deposito non riesce a coprire un periodo intero, il file viene eliminato: il suo record viene segnato come non archiviato, e il resto va al pool di longevità.

File privati

Il blocco di accesso di uno StoreFile può contrassegnare un file come privato (bit di flag 0) e aggiungere una lista di accesso da 1 a 16 indirizzi (bit di flag 1). L'API di un nodo rifiuta allora una richiesta non firmata del contenuto (401) e una richiesta firmata da chiunque non sia il proprietario o nella lista (403). I file privati vengono esclusi dagli elenchi pubblici, e un file pubblico che condivide un frammento con un file privato attivo viene protetto allo stesso modo.

«Privato» è una modalità di accesso, non una cifratura. I frammenti vengono comunque copiati non cifrati presso i loro detentori, e i nodi si servono i frammenti a vicenda, quindi l'operatore di un nodo detentore può leggere i byte sul proprio disco. Se il contenuto è importante, cifralo prima di caricarlo: così è la tua chiave a proteggere il contenuto, e il flag privato aggiunge un secondo livello, più leggero.

Vedi anche