"Quel genre de diable devrais-je me souvenir par cĆur de tous ces fichus algorithmes et structures de donnĂ©es?"
Ă ce sujet se rĂ©sume aux commentaires de la plupart des articles sur le passage des entretiens techniques. La thĂšse principale, en rĂšgle gĂ©nĂ©rale, est que tout ce qui est utilisĂ© d'une maniĂšre ou d'une autre a dĂ©jĂ Ă©tĂ© mis en Ćuvre dix fois et il est trĂšs peu probable que ce programmeur ordinaire doive y faire face. Eh bien, dans une certaine mesure, cela est vrai. Mais, il s'est avĂ©rĂ© que tout n'Ă©tait pas implĂ©mentĂ©, et moi, malheureusement (ou heureusement?), Je devais encore crĂ©er une structure de donnĂ©es.
Mystérieuse Merkle modifiée Patricia Trie.
Puisqu'il n'y a aucune information sur cet arbre sur le habr et sur le milieu - un peu plus, je veux vous dire de quel type d'animal il s'agit et avec quoi il est mangé.

Qu'est ce que c'est
Avertissement: la principale source d'informations pour la mise en Ćuvre pour moi Ă©tait le papier jaune , ainsi que les codes source parity-ethereum et go-ethereum . Il y avait un minimum d'informations thĂ©oriques sur la justification de certaines dĂ©cisions, donc toutes les conclusions sur les raisons de prendre certaines dĂ©cisions m'appartiennent. Au cas oĂč je me tromperais sur quelque chose - je serai heureux des corrections dans les commentaires.
Un arbre est une structure de données qui est un graphe acyclique connecté. Ici, tout est simple, tout le monde le sait.
L'arbre des prĂ©fixes est l'arbre racine dans lequel les paires clĂ©-valeur peuvent ĂȘtre stockĂ©es car les nĆuds sont divisĂ©s en deux types: ceux qui contiennent une partie du chemin (prĂ©fixe) et les nĆuds feuilles qui contiennent la valeur stockĂ©e. Une valeur est prĂ©sente dans un arbre si et seulement si, Ă l'aide de la clĂ©, nous pouvons aller jusqu'Ă la racine de l'arbre et trouver un nĆud avec une valeur Ă la fin.
L'arbre PATRICIA est un arbre de prĂ©fixe dans lequel les prĂ©fixes sont binaires - c'est-Ă -dire que chaque nĆud clĂ© stocke des informations sur un bit.
L' arbre Merkle est un arbre de hachage construit sur une sorte de chaĂźne de donnĂ©es, agrĂ©geant ces mĂȘmes hachages en un seul (racine), stockant des informations sur l'Ă©tat de tous les blocs de donnĂ©es. Autrement dit, le hachage racine est une sorte de "signature numĂ©rique" de l'Ă©tat de la chaĂźne de blocs. Cette chose est utilisĂ©e activement dans la blockchain, et plus d'informations Ă ce sujet peuvent ĂȘtre trouvĂ©es ici .

Total: Merkle modifié Patricia Trie (ci-aprÚs MPT pour faire court) est un arbre de hachage qui stocke les paires clé-valeur, et les clés sont présentées sous forme binaire. Et en quoi consiste exactement «Modifié», nous le découvrirons un peu plus tard lorsque nous discuterons de l'implémentation.
Pourquoi ça?
MPT est utilisé dans le projet Ethereum pour stocker des données sur les comptes, les transactions, les résultats de leur exécution et d'autres données nécessaires au fonctionnement du systÚme.
Contrairement au Bitcoin, dans lequel l'Ă©tat est implicite et est calculĂ© par chaque nĆud indĂ©pendamment, le solde de chaque compte (ainsi que les donnĂ©es qui lui sont associĂ©es) sont stockĂ©s directement sur la blockchain en direct. De plus, l'emplacement et l'immuabilitĂ© des donnĂ©es doivent ĂȘtre fournis cryptographiquement - peu de gens utiliseront la crypto-monnaie dans laquelle le solde d'un compte alĂ©atoire peut changer sans raisons objectives.
Le principal problĂšme rencontrĂ© par les dĂ©veloppeurs d'Ethereum est la crĂ©ation d'une structure de donnĂ©es capable de stocker efficacement des paires clĂ©-valeur et en mĂȘme temps de vĂ©rifier les donnĂ©es stockĂ©es. Alors MPT est apparu.
Comment est-ce?
MPT est un arbre de préfixe PATRICIA dans lequel les clés sont des séquences d'octets.
Les bords de cet arbre sont des sĂ©quences de quartets (demi-octets). En consĂ©quence, un nĆud peut avoir jusqu'Ă seize descendants (correspondant aux branches de 0x0 Ă 0xF).
Les nĆuds sont divisĂ©s en 3 types:
- Noeud de branche. Le nĆud utilisĂ© pour la ramification. Contient jusqu'Ă 1 Ă 16 liens vers des nĆuds enfants. Peut Ă©galement contenir une valeur.
- Noeud d'extension. Un nĆud auxiliaire qui stocke une partie du chemin commun Ă plusieurs nĆuds enfants, ainsi qu'un lien vers le nĆud de branche, qui se trouve ci-dessous.
- NĆud foliaire. Un nĆud contenant une partie du chemin et la valeur stockĂ©e. C'est la fin de la chaĂźne.
Comme dĂ©jĂ mentionnĂ©, MPT est construit au-dessus d'un autre rĂ©fĂ©rentiel kv, qui stocke les nĆuds sous la forme de "lien" => "nĆud codĂ© RLP
".
Et nous arrivons ici avec un nouveau concept: RLP. En bref, il s'agit d'une méthode de codage de données représentant des listes ou des séquences d'octets. Exemple: [ "cat", "dog" ] = [ 0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' ]
. Je n'entrerai pas dans les dĂ©tails en particulier, et dans la mise en Ćuvre, j'utilise une bibliothĂšque prĂȘte Ă l'emploi, car la couverture de ce sujet gonflera Ă©galement un article dĂ©jĂ assez volumineux. Si vous ĂȘtes toujours intĂ©ressĂ©, vous pouvez en savoir plus ici . Nous nous limitons au fait que nous pouvons encoder des donnĂ©es en RLP
et les décoder à nouveau.
Un lien vers un nĆud est dĂ©fini comme suit: si la longueur du nĆud codĂ© RLP
est de 32 octets ou plus, alors le lien est un hachage keccak
de la représentation RLP
du nĆud. Si la longueur est infĂ©rieure Ă 32 octets, le lien est la reprĂ©sentation RLP
du nĆud lui-mĂȘme.
De toute Ă©vidence, dans le deuxiĂšme cas, vous n'avez pas besoin d'enregistrer le nĆud dans la base de donnĂ©es, car il sera entiĂšrement enregistrĂ© dans le nĆud parent.

La combinaison de trois types de nĆuds vous permet de stocker efficacement des donnĂ©es dans le cas oĂč il y a peu de clĂ©s (alors la plupart des chemins seront stockĂ©s dans les nĆuds d'extension et de feuille, et il y aura peu de nĆuds de branche), et dans le cas oĂč il y a beaucoup de nĆuds (les chemins ne seront pas stockĂ©s explicitement, mais ils "se rassembleront" pendant le passage Ă travers des nĆuds de branche).
Un exemple complet d'arbre utilisant toutes sortes de nĆuds:

Comme vous l'avez peut-ĂȘtre remarquĂ©, les parties stockĂ©es des chemins sont prĂ©fixĂ©es. Les prĂ©fixes sont nĂ©cessaires Ă plusieurs fins:
- Distinguer les nĆuds d'extension des nĆuds feuilles.
- Pour aligner des séquences d'un nombre impair de grignotages.
Les rÚgles de création des préfixes sont trÚs simples:
- Le préfixe prend 1 quartet. Si la longueur du chemin (à l'exclusion du préfixe) est impaire, le chemin commence immédiatement aprÚs le préfixe. Si la longueur du chemin est pair, pour aligner aprÚs le préfixe, le quartet 0x0 est ajouté en premier.
- Le préfixe est initialement 0x0.
- Si la longueur du chemin est impaire, alors 0x1 est ajouté au préfixe, si pair - 0x0.
- Si le chemin mĂšne Ă un nĆud Leaf, 0x2 est ajoutĂ© au prĂ©fixe, si 0x0 est ajoutĂ© au nĆud Extension.
Sur les beatiks, je pense que ce sera plus clair:
0b0000 => , Extension 0b0001 => , Extension 0b0010 => , Leaf 0b0011 => , Leaf
Suppression qui n'est pas une suppression
MalgrĂ© le fait que l'arborescence ait pour effet de supprimer des nĆuds, en fait, tout ce qui a Ă©tĂ© ajoutĂ© une fois reste dans l'arborescence pour toujours.
Cela est nécessaire pour ne pas créer une arborescence complÚte pour chaque bloc, mais pour ne stocker que la différence entre l'ancienne et la nouvelle version de l'arborescence.
Par conséquent, en utilisant différents hachages racine comme point d'entrée, nous pouvons obtenir l'un des états dans lesquels l'arbre a déjà été.

Ce ne sont pas toutes des optimisations. Il y a plus, mais nous n'en parlerons pas - et donc l'article est volumineux. Cependant, vous pouvez lire par vous-mĂȘme.
Implémentation
La théorie est terminée, passons à la pratique. Nous utiliserons la lingua franca du monde informatique, c'est-à -dire le python
.
Puisqu'il y aura beaucoup de code, et pour le format de l'article beaucoup devra ĂȘtre rĂ©duit et divisĂ©, je laisserai immĂ©diatement un lien vers github .
Si nécessaire, vous pouvez voir l'image entiÚre.
Tout d'abord, nous définissons l'interface d'arbre que nous voulons obtenir en conséquence:
class MerklePatriciaTrie: def __init__(self, storage, root=None): pass def root(self): pass def get(self, encoded_key): pass def update(self, encoded_key, encoded_value): pass def delete(self, encoded_key): pass
L'interface est extrĂȘmement simple. Les opĂ©rations disponibles sont l'obtention, la suppression, l'insertion et la modification (combinĂ©es dans la mise Ă jour), ainsi que l'obtention du hachage racine.
Le stockage sera transféré à la méthode __init__
- une structure de données de type dict
dans laquelle nous __init__
les nĆuds, ainsi que la root
- le "sommet" de l'arborescence. Si None
est passé en tant que root
, nous supposons que l'arborescence est vide et fonctionne à partir de zéro.
_Remarque: vous vous demandez peut-ĂȘtre pourquoi les variables dans les mĂ©thodes sont nommĂ©es en tant que encoded_key
et encoded_value
, et pas seulement key
/ value
. La rĂ©ponse est simple: selon la spĂ©cification, toutes les clĂ©s et valeurs doivent ĂȘtre encodĂ©es en RLP
. Nous ne nous en préoccuperons pas et laisserons cette occupation sur les épaules des utilisateurs de la bibliothÚque._
Cependant, avant de commencer Ă implĂ©menter l'arbre lui-mĂȘme, deux choses importantes doivent ĂȘtre faites:
- Implémentez la classe
NibblePath
, qui est une chaßne de grignotages, afin de ne pas les coder manuellement. - Pour implémenter la classe
Node
dans le cadre de cette classe - Extension
, Leaf
et Branch
.
Nibblepath
Alors, NibblePath
. Puisque nous nous dĂ©placerons activement dans l'arborescence, la base de la fonctionnalitĂ© de notre classe devrait ĂȘtre la possibilitĂ© de dĂ©finir le "dĂ©calage" depuis le dĂ©but du chemin, ainsi que de recevoir un quartet spĂ©cifique. Sachant cela, nous dĂ©finissons la base de notre classe (ainsi que quelques constantes utiles pour travailler avec les prĂ©fixes ci-dessous):
class NibblePath: ODD_FLAG = 0x10 LEAF_FLAG = 0x20 def __init__(self, data, offset=0): self._data = data
C'est assez simple, non?
Il ne reste plus qu'à écrire des fonctions d'encodage et de décodage d'une séquence de quartets.
class NibblePath:
En principe, c'est le minimum nécessaire pour un travail pratique avec des amuse-gueules. Bien sûr, dans l'implémentation actuelle, il existe encore un certain nombre de méthodes auxiliaires (telles que combine
, fusionner deux chemins en un seul), mais leur implĂ©mentation est trĂšs triviale. Si vous ĂȘtes intĂ©ressĂ©, la version complĂšte peut ĂȘtre trouvĂ©e ici .
Noeud
Comme nous le savons dĂ©jĂ , nos nĆuds sont divisĂ©s en trois types: Leaf, Extension et Branch. Tous peuvent ĂȘtre encodĂ©s et dĂ©codĂ©s, et la seule diffĂ©rence rĂ©side dans les donnĂ©es stockĂ©es Ă l'intĂ©rieur. Pour ĂȘtre honnĂȘte, c'est ce que les types de donnĂ©es algĂ©briques sont demandĂ©s, et dans Rust
, par exemple, j'écrirais quelque chose dans l'esprit:
pub enum Node<'a> { Leaf(NibblesSlice<'a>, &'a [u8]), Extension(NibblesSlice<'a>, NodeReference), Branch([Option<NodeReference>; 16], Option<&'a [u8]>), }
Cependant, il n'y a pas d'ADT en python en tant que tel, nous allons donc définir la classe Node
, et Ă l'intĂ©rieur, il y a trois classes correspondant aux types de nĆuds. Nous implĂ©mentons le codage directement dans les classes de nĆuds et le dĂ©codage dans la classe de Node
.
La mise en Ćuvre est cependant Ă©lĂ©mentaire:
Feuille:
class Leaf: def __init__(self, path, data): self.path = path self.data = data def encode(self):
Extension:
class Extension: def __init__(self, path, next_ref): self.path = path self.next_ref = next_ref def encode(self):
Succursale:
class Branch: def __init__(self, branches, data=None): self.branches = branches self.data = data def encode(self):
Tout est trĂšs simple. La seule chose qui peut poser des questions est la fonction _prepare_reference_for_encoding
.
Ensuite, je l'avoue, j'ai dû utiliser une petite béquille. Le fait est que la bibliothÚque rlp
dĂ©code les donnĂ©es de maniĂšre rĂ©cursive, et le lien vers un autre nĆud, comme nous le savons, peut ĂȘtre des donnĂ©es rlp
(dans le cas oĂč le nĆud codĂ© fait moins de 32 caractĂšres). Travailler avec des liens dans deux formats - octets de hachage et nĆud dĂ©codĂ© - est trĂšs gĂȘnant. Par consĂ©quent, j'ai Ă©crit deux fonctions qui, aprĂšs dĂ©codage du nĆud, renvoient les liens au format octet, et les dĂ©codent si nĂ©cessaire, avant d'enregistrer. Ces fonctions sont:
def _prepare_reference_for_encoding(ref):
Terminez avec les nĆuds en Ă©crivant une classe Node
. Il n'y aura que 2 mĂ©thodes: dĂ©coder le nĆud et transformer le nĆud en lien.
class Node:
Une pause
Fuh! Il y a beaucoup d'informations. Je pense qu'il est temps de se détendre. Voici un autre chat pour vous:

Milota, c'est ça? Bon, revenons à nos arbres.
MerklePatriciaTrie
Hourra - les Ă©lĂ©ments auxiliaires sont prĂȘts, nous passons aux plus dĂ©licieux. Au cas oĂč, je rappellerai l'interface de notre arbre. Dans le mĂȘme temps, nous implĂ©mentons la mĂ©thode __init__
.
class MerklePatriciaTrie: def __init__(self, storage, root=None): self._storage = storage self._root = root def root(self): pass def get(self, encoded_key): pass def update(self, encoded_key, encoded_value): pass def delete(self, encoded_key): pass
Mais avec les méthodes restantes, nous traiterons un par un.
obtenir
La méthode get
(comme, en principe, les autres mĂ©thodes) comprendra deux parties. La mĂ©thode elle-mĂȘme prĂ©parera les donnĂ©es et amĂšnera le rĂ©sultat Ă la forme attendue, tandis que le vrai travail se fera Ă l'intĂ©rieur de la mĂ©thode auxiliaire.
La mĂ©thode de base est extrĂȘmement simple:
class MerklePatriciaTrie:
Cependant, _get
pas beaucoup plus compliquĂ©: pour arriver au nĆud souhaitĂ©, nous devons aller de la racine Ă l'ensemble du chemin fourni. Si Ă la fin nous avons trouvĂ© un nĆud avec des donnĂ©es (feuille ou branche) - hourra, les donnĂ©es sont reçues. S'il n'a pas Ă©tĂ© possible de passer, la clĂ© requise est manquante dans l'arborescence.
Réalisation:
class MerklePatriciaTrie:
Eh bien, en mĂȘme temps, nous Ă©crirons des mĂ©thodes pour sauvegarder et charger des nĆuds. Ils sont simples:
class MerklePatriciaTrie:
mettre Ă jour
La méthode de update
est dĂ©jĂ plus intĂ©ressante. Il suffit d'aller jusqu'au bout et d'insĂ©rer le nĆud Feuille ne fonctionnera pas toujours. Il est probable que le point de sĂ©paration des clĂ©s se trouve quelque part Ă l'intĂ©rieur du nĆud Feuille ou Extension dĂ©jĂ enregistrĂ©. Dans ce cas, vous devrez les sĂ©parer et crĂ©er plusieurs nouveaux nĆuds.
En gĂ©nĂ©ral, toute la logique peut ĂȘtre dĂ©crite par les rĂšgles suivantes:
- Alors que le chemin coĂŻncide entiĂšrement avec les nĆuds existants, nous descendons rĂ©cursivement l'arbre.
- Si le chemin est terminĂ© et que nous sommes dans le nĆud Branche ou Feuille, cela signifie que la
update
met simplement Ă jour la valeur correspondant Ă cette clĂ©. - Si les chemins sont divisĂ©s (c'est-Ă -dire que nous ne mettons pas Ă jour la valeur, mais en insĂ©rons une nouvelle), et que nous sommes dans le nĆud Branche, crĂ©ez un nĆud Feuille et spĂ©cifiez un lien vers celui-ci dans la branche de branche branche correspondante.
- Si les chemins sont divisĂ©s et que nous sommes dans le nĆud Feuille ou Extension, nous devons crĂ©er un nĆud Branche qui sĂ©pare les chemins et, si nĂ©cessaire, un nĆud Extension pour la partie commune du chemin.
Exprimons progressivement cela dans le code. Pourquoi progressivement? Parce que la méthode est volumineuse et il sera difficile de la comprendre en vrac.
Cependant, je vais laisser un lien vers la méthode complÚte ici .
class MerklePatriciaTrie:
Il n'y a pas assez de logique générale, le plus intéressant est à l'intérieur if
art.
if type(node) == Node.Leaf
Voyons d'abord les nĆuds Leaf. Seuls 2 scĂ©narios sont possibles avec eux:
Le reste du chemin que nous suivons est exactement le mĂȘme que le chemin stockĂ© dans le nĆud Feuille. Dans ce cas, il suffit de modifier la valeur, d'enregistrer le nouveau nĆud et de lui renvoyer un lien.
Les chemins sont différents.
Dans ce cas, vous devez crĂ©er un nĆud de branche qui sĂ©pare les deux chemins.
Si l'un des chemins est vide, sa valeur sera transfĂ©rĂ©e directement au nĆud de branche.
Sinon, il faudra crĂ©er deux nĆuds Leaf raccourcis de la longueur de la partie commune des chemins + 1 quartet (ce quartet sera indiquĂ© par l'index de la branche correspondante du nĆud Branch).
Vous devrez Ă©galement vĂ©rifier s'il existe une partie commune du chemin afin de comprendre si nous devons Ă©galement crĂ©er un nĆud d'extension.
Dans le code, cela ressemblera Ă ceci:
if type(node) == Node.Leaf: if node.path == path:
La procédure _create_branch_node
est la suivante:
def _create_branch_node(self, path_a, value_a, path_b, value_b):
if type(node) == Node.Extension
Dans le cas du nĆud Extension, tout ressemble Ă un nĆud Leaf.
Si le chemin du nĆud Extension est un prĂ©fixe pour notre chemin, nous allons simplement de maniĂšre rĂ©cursive.
Sinon, nous devons faire la sĂ©paration en utilisant le nĆud Branch, comme dans le cas dĂ©crit ci-dessus.
En conséquence, le code:
elif type(node) == Node.Extension: if path.starts_with(node.path):
La procédure _create_branch_extension
logiquement équivalente à la procédure _create_branch_leaf
, mais fonctionne avec le nĆud Extension.
if type(node) == Node.Branch
Mais avec le nĆud Branch, tout est simple. Si le chemin est vide, nous enregistrons simplement la nouvelle valeur dans le nĆud Branch actuel. Si le chemin n'est pas vide, nous en «mordons» un quartet et descendons rĂ©cursivement.
Le code, je pense, n'a pas besoin de commentaires.
elif type(node) == Node.Branch: if len(path) == 0: return self._store_node(Node.Branch(node.branches, value)) idx = path.at(0) new_reference = self._update(node.branches[idx], path.consume(1), value) node.branches[idx] = new_reference return self._store_node(node)
effacer
Fuh! La derniÚre méthode reste. Il est le plus gai. La complexité de la suppression est que nous devons remettre la structure dans l'état dans lequel elle serait tombée si nous avions fait toute la chaßne de update
, à l'exclusion uniquement de la clé supprimée.
Ceci est extrĂȘmement important, car sinon une situation est possible dans laquelle le hachage racine diffĂ©rera pour deux arbres contenant les mĂȘmes donnĂ©es. Et une telle «fonctionnalité», comme vous le comprenez, effacera tout le sens de cette structure de donnĂ©es.
Cette exigence donne lieu à un assez grand nombre de scénarios d'action possibles. De plus, la fonction au N-Úme niveau d'imbrication aprÚs suppression directe devra savoir ce qui s'est passé au niveau N + 1. Pour ce faire, nous allons introduire une énumération supplémentaire - DeleteAction
que nous reviendrons en haut.
Le cadre de la méthode delete
ressemblera Ă ceci:
class MerklePatriciaTrie:
, get
update
. . .
if type(node) == Node.Leaf
. . â , , .
:
if type(node) == Node.Leaf: if path == node.path: return MerklePatriciaTrie._DeleteAction.DELETED, None else: raise KeyError
, "" â . , . .
if type(node) == Node.Extension
C Extension- :
- , Extension- . â .
_delete
, "" .- . :
- . .
- . .
- Branch-. . , Branch- . , , Leaf-. â Extension-.
:
elif type(node) == Node.Extension: if not path.starts_with(node.path): raise KeyError
if type(node) == Node.Branch
.
, . Branch-, âŠ
Pourquoi? Branch- Leaf- ( ) Extension- ( ).
, . , â Leaf-. â Extension-. , , 2 â Branch- .
? :
:
- , .
- ,
_delete
.
:
elif type(node) == Node.Branch: action = None idx = None info = None if len(path) == 0 and len(node.data) == 0: raise KeyError elif len(path) == 0 and len(node.data) != 0: node.data = b'' action = MerklePatriciaTrie._DeleteAction.DELETED else:
_DeleteAction
.
- Branch- , ( , ). .
if action == MerklePatriciaTrie._DeleteAction.UPDATED:
- ( , ), , .
. :
- . , , , . , .
- , . Leaf- . .
- , . , , .
- , , Branch- . ,
_DeleteAction
â UPDATED
.
if action == MerklePatriciaTrie._DeleteAction.DELETED: non_empty_count = sum(map(lambda x: 1 if len(x) > 0 else 0, node.branches)) if non_empty_count == 0 and len(node.data) == 0:
_build_new_node_from_last_branch
.
â Leaf Extension, , .
â Branch, Extension , , Branch.
def _build_new_node_from_last_branch(self, branches):
Le reste
. , ⊠root
.
Ici:
class MerklePatriciaTrie:
, .
⊠. , , Ethereum . , , , . , :)
, , pip install -U eth_mpt
â .

Résultats
?
, -, , - , , . â , .
-, , , â . , skip list interval tree, â , , .
-, , . , - .
-, â .
, , â !
: 1 , 2 , 3 . ! , .