Wiki ZooBC
Données et stockage

Le stockage décentralisé

Le stockage décentralisé conserve les fichiers trop volumineux pour une transaction. La chaîne n’enregistre que le manifeste et le dépôt de chaque fichier ; les fragments résident sur des nœuds enregistrés, en cinq copies chacun, placés par un tirage que chaque nœud peut refaire. Le loyer des fichiers paie les détenteurs qui prouvent, à chaque époque, qu’ils ont toujours les octets.

Fragments, racine et manifeste

Un fichier stocké est découpé en fragments de 1 Mio (1 048 576 octets). Chaque fragment est nommé par son hachage SHA3-256, et l’identité du fichier, sa racine, est un hachage calculé sur la taille des fragments, la taille totale et la liste des identifiants des fragments :

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

Les fragments sont copiés en entier ; ZooBC n’utilise pas de codage à effacement. Une transaction StoreFile (type 40) ancre le manifeste sur la chaîne : la racine, la taille totale, la taille des fragments, un dépôt d’au moins 0,01 ZBC, de 1 à 100 000 identifiants de fragments et une section d’accès facultative. Les fragments eux-mêmes ne vont jamais sur la chaîne. Une StoreFile pour une racine déjà active est une opération nulle payante ; une racine qui a été élaguée peut être stockée de nouveau.

Pour téléverser un fichier, un portefeuille l’envoie à la route POST /api/v1/storage/upload d’un nœud, qui renvoie la racine, les tailles et les identifiants des fragments destinés à la StoreFile. Le nœud refuse quand le stockage est désactivé (503), quand le corps est vide (400), quand il lui reste moins de 2 Gio libres ou qu’il détient plus de 256 Mio de fragments en attente d’ancrage (507), et quand un client dépasse 10 téléversements par 600 secondes ou 32 Mio par heure (429).

Un fichier est découpé en fragments de 1 Mio ; le manifeste va sur la chaîne, et chaque fragment va sur cinq nœuds.
Un fichier est découpé en fragments de 1 Mio ; le manifeste va sur la chaîne, et chaque fragment va sur cinq nœuds.

Qui détient un fragment

Le placement repose sur le hachage par rendez-vous. Sur le mainnet, le temps est découpé en époques de stockage de 240 blocs, soit environ une heure. Pour un fragment dans une époque, chaque nœud enregistré actif reçoit un poids :

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

La graine d’époque est le hachage du bloc qui ouvre l’époque, à la hauteur époque × 240. Les cinq nœuds aux poids les plus élevés sont les détenteurs attendus, cinq étant la valeur du paramètre de chaîne storage_replication ; avec moins de cinq nœuds, tous détiennent le fragment. Tout ce qui entre dans la formule relève des données de la chaîne, si bien que chaque nœud calcule les cinq mêmes sans aucune annonce. Le décompte qui paie les détenteurs lit le registre tel qu’il était au bloc précédent, de sorte que deux nœuds ne peuvent pas être en désaccord sur l’ensemble.

Les détenteurs changent d’une époque à l’autre, et la passation se fait avec chevauchement. À chaque époque, un nœud récupère les fragments qui lui sont attribués, d’abord auprès des détenteurs de l’époque en cours, puis auprès de ceux de l’époque précédente, puis auprès de n’importe quel nœud. Il n’accepte un fragment poussé vers lui que si le hachage concorde et qu’il en est un détenteur attendu pour cette époque ou la précédente. Il ne supprime un fragment qui ne lui est pas attribué que s’il n’en était pas non plus détenteur à l’époque précédente et que le fichier a au moins une heure.

Le stockage est activé par défaut : détenir des fragments, comme détenir les segments de l’archive, est un devoir du siège d’un nœud enregistré. Un opérateur peut le désactiver dans node.conf ([storage] enabled = false) et ne perd alors que les récompenses de stockage. Ce réglage est local et la chaîne ne peut pas le voir, si bien que le tirage n’en tient pas compte : un nœud qui a désactivé le stockage est tiré comme n’importe quel autre, mais il ne prouve jamais, ne gagne rien pour ce fragment et ne perd pas de score, et le fragment a une copie opérationnelle de moins. La réplication et la durée d’une époque proviennent toujours des paramètres de la chaîne, quoi qu’indique node.conf.

Preuves de stockage

La chaîne ne voit jamais les fragments, elle ne peut donc pas vérifier un détenteur par elle-même. À la place, à chaque époque, chaque détenteur répond à un défi que seul quelqu’un qui détient les octets peut calculer :

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

Personne ne peut connaître le défi avant le début de l’époque. Les détenteurs envoient leurs réponses dans des transactions SubmitStorageProof (type 41) : des devoirs sans frais signés avec la clé d’un nœud enregistré, une seule transaction portant tout le lot d’un nœud pour l’époque.

À chaque frontière d’époque, la chaîne compare les réponses. La valeur soumise par le plus grand nombre de nœuds distincts est tenue pour vraie, mais seulement si aucune autre valeur n’est à égalité avec elle, et elle ne compte que si au moins trois détenteurs sont d’accord (oracle_quorum). Le propriétaire de chaque détenteur concordant est alors payé depuis le fonds de stockage :

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

Un détenteur mis en minorité ne gagne rien pour cette époque, mais ne perd rien de son score de participation : comme la chaîne ne détient jamais les octets, l’accord entre détenteurs est tout ce qu’elle peut juger. Les détenteurs qui n’envoient rien sont consignés dans une table d’audit. Un nœud immature ne gagne rien, et sa récompense de stockage reste dans le fonds (maturité et gains).

Les détenteurs répondent à chaque époque à un défi nouveau ; quand au moins trois sont d’accord et qu’aucune réponse n’est à égalité, chaque détenteur concordant est payé.
Les détenteurs répondent à chaque époque à un défi nouveau ; quand au moins trois sont d’accord et qu’aucune réponse n’est à égalité, chaque détenteur concordant est payé.

Loyer

Une fois par période, soit 172 800 blocs (environ 30 jours) sur le mainnet, la chaîne transfère le loyer d’une période du dépôt du fichier vers le fonds de stockage (ZBCSTORE) :

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

Le prix est le même que pour les jeux de données, 0,05 ZBC par Mio au départ. Le loyer est facturé en mébioctets entiers, au moins un : un fichier de 42 octets paie 0,05 ZBC par période, et un fichier de 1 048 577 octets, soit un octet de plus que 1 Mio, paie 0,1 ZBC. La route GET /api/v1/storage/quote?bytes=&blocks= d’un nœud chiffre un dépôt avec la même formule.

N’importe quel compte peut ajouter de l’argent au dépôt d’un fichier actif avec FundStoredFile (type 296), par tranches d’au moins 0,01 ZBC. Lorsque le dépôt ne suffit pas à couvrir une période entière, le fichier est élagué : son enregistrement est marqué comme non stocké, et le reliquat va au fonds de longévité.

Fichiers privés

La section d’accès d’une StoreFile peut marquer un fichier comme privé (bit d’indicateur 0) et ajouter une liste d’autorisation de 1 à 16 adresses (bit d’indicateur 1). L’API d’un nœud refuse alors une requête non signée portant sur le contenu (401) et une requête signée venant de quiconque n’est ni le propriétaire ni sur la liste (403). Les fichiers privés sont exclus des listes publiques, et un fichier public qui partage un fragment avec un fichier privé actif est protégé de la même façon.

« Privé » est un mode d’accès, pas un chiffrement. Les fragments sont tout de même copiés en clair sur leurs détenteurs, et les nœuds se transmettent les fragments entre eux, si bien que l’opérateur d’un nœud détenteur peut lire les octets sur son disque. Si le contenu compte, chiffrez-le avant de le téléverser : votre clé protège alors le contenu, et l’indicateur privé ajoute une seconde couche, plus légère.

Voir aussi