Structures de données exotiques: Merkle modifiée Patricia Trie

"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é.


KDPV


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 .


Le travail acharné est ...


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.


Les nƓuds sont diffĂ©rents


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:


L'arbre est plein mais pas épais


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:


  1. Distinguer les nƓuds d'extension des nƓuds feuilles.
  2. 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é.


Qu'est-ce qui est écrit avec un stylo ...


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:


  1. Implémentez la classe NibblePath , qui est une chaßne de grignotages, afin de ne pas les coder manuellement.
  2. 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 # ,   . self._offset = offset #      def consume(self, amount): # "" N      . self._offset += amount return self def at(self, idx): #      idx = idx + self._offset #    ,   ,    , #   ,    -      . byte_idx = idx // 2 nibble_idx = idx % 2 #   . byte = self._data[byte_idx] #      . nibble = byte >> 4 if nibble_idx == 0 else byte & 0x0F return nibble 

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: # ... def decode_with_type(data): #   : # ,     ,    . is_odd_len = data[0] & NibblePath.ODD_FLAG == NibblePath.ODD_FLAG is_leaf = data[0] & NibblePath.LEAF_FLAG == NibblePath.LEAF_FLAG #    ,     #    . offset  , #       "" . offset = 1 if is_odd_len else 2 return NibblePath(data, offset), is_leaf def encode(self, is_leaf): output = [] #    ,       . nibbles_len = len(self._data) * 2 - self._offset is_odd = nibbles_len % 2 == 1 #  . prefix = 0x00 #    ,    . #      (self.at(0))     . #           (0x0). prefix += self.ODD_FLAG + self.at(0) if is_odd else 0x00 #  ,  Leaf node,  . prefix += self.LEAF_FLAG if is_leaf else 0x00 output.append(prefix) # ,      ,  . pos = nibbles_len % 2 #          , #     2 ,    , #     , #    . while pos < nibbles_len: byte = self.at(pos) * 16 + self.at(pos + 1) output.append(byte) pos += 2 return bytes(output) 

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): #    --    , #   -  ,   -  . return rlp.encode([self.path.encode(True), self.data]) 

Extension:


 class Extension: def __init__(self, path, next_ref): self.path = path self.next_ref = next_ref def encode(self): #    --    , #   -  ,   -    . next_ref = _prepare_reference_for_encoding(self.next_ref) return rlp.encode([self.path.encode(False), next_ref]) 

Succursale:


 class Branch: def __init__(self, branches, data=None): self.branches = branches self.data = data def encode(self): #    --    ,  #  16 -     (  ), #   -   (  ). branches = list(map(_prepare_reference_for_encoding, self.branches)) return rlp.encode(branches + [self.data]) 

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): #    ( ,   ) --  . #       :) if 0 < len(ref) < 32: return rlp.decode(ref) return ref def _prepare_reference_for_usage(ref): #     -   . #          . if isinstance(ref, list): return rlp.encode(ref) return 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: # class Leaf(...) # class Extension(...) # class Branch(...) def decode(encoded_data): data = rlp.decode(encoded_data) # 17  -  Branch . if len(data) == 17: branches = list(map(_prepare_reference_for_usage, data[:16])) node_data = data[16] return Node.Branch(branches, node_data) #    17,   2.   - . #      ,     . path, is_leaf = NibblePath.decode_with_type(data[0]) if is_leaf: return Node.Leaf(path, data[1]) else: ref = _prepare_reference_for_usage(data[1]) return Node.Extension(path, ref) def into_reference(node): #    . #      32 , #   -   . #       . encoded_node = node.encode() if len(encoded_node) < 32: return encoded_node else: return keccak_hash(encoded_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:


Vous pouvez prendre une bouchée pendant la pause


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: # ... def get(self, encoded_key): if not self._root: raise KeyError path = NibblePath(encoded_key) #       #  ,    ,    . result_node = self._get(self._root, path) if type(result_node) is Node.Extension or len(result_node.data) == 0: raise KeyError return result_node.data 

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: # ... def _get(self, node_ref, path): #      . node = self._get_node(node_ref) #    --   . #   ,      . if len(path) == 0: return node if type(node) is Node.Leaf: #     Leaf-,     , #      . if node.path == path: return node elif type(node) is Node.Extension: #    -- Extension,    . if path.starts_with(node.path): rest_path = path.consume(len(node.path)) return self._get(node.next_ref, rest_path) elif type(node) is Node.Branch: #    -- Branch,     . #   ,           #  :      . branch = node.branches[path.at(0)] if len(branch) > 0: return self._get(branch, path.consume(1)) #    ,        , #     . raise KeyError 

Eh bien, en mĂȘme temps, nous Ă©crirons des mĂ©thodes pour sauvegarder et charger des nƓuds. Ils sont simples:


 class MerklePatriciaTrie: # ... def _get_node(self, node_ref): raw_node = None if len(node_ref) == 32: raw_node = self._storage[node_ref] else: raw_node = node_ref return Node.decode(raw_node) def _store_node(self, node): reference = Node.into_reference(node) if len(reference) == 32: self._storage[reference] = node.encode() return reference 

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:


  1. Alors que le chemin coĂŻncide entiĂšrement avec les nƓuds existants, nous descendons rĂ©cursivement l'arbre.
  2. 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Ă©.
  3. 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.
  4. 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: # ... def update(self, encoded_key, encoded_value): path = NibblePath(encoded_key) result = self._update(self._root, path, encoded_value) self._root = result def _update(self, node_ref, path, value): #       (,   ), #       . if not node_ref: return self._store_node(Node.Leaf(path, value)) #          #    . node = self._get_node(node_ref) if type(node) == Node.Leaf: ... elif type(node) == Node.Extension: ... elif type(node) == Node.Branch: ... 

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:


  1. 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.


  2. 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: #  .       . node.data = value return self._store_node(node) #    . #    . common_prefix = path.common_prefix(node.path) #      . path.consume(len(common_prefix)) node.path.consume(len(common_prefix)) #  Branch . branch_reference = self._create_branch_node(path, value, node.path, node.data) # ,    Extension-. if len(common_prefix) != 0: return self._store_node(Node.Extension(common_prefix, branch_reference)) else: return branch_reference 

La procédure _create_branch_node est la suivante:


 def _create_branch_node(self, path_a, value_a, path_b, value_b): #    Branch-. branches = [b''] * 16 # ,     Branch- . branch_value = b'' if len(path_a) == 0: branch_value = value_a elif len(path_b) == 0: branch_value = value_b #    Leaf-,  . self._create_branch_leaf(path_a, value_a, branches) self._create_branch_leaf(path_b, value_b, branches) #  Branch-     . return self._store_node(Node.Branch(branches, branch_value)) def _create_branch_leaf(self, path, value, branches): # ,     Leaf-. if len(path) > 0: #    ( ). idx = path.at(0) #  Leaf-   ,     . leaf_ref = self._store_node(Node.Leaf(path.consume(1), value)) branches[idx] = leaf_ref 

if type(node) == Node.Extension

Dans le cas du nƓud Extension, tout ressemble à un nƓud Leaf.


  1. Si le chemin du nƓud Extension est un prĂ©fixe pour notre chemin, nous allons simplement de maniĂšre rĂ©cursive.


  2. 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): #         . new_reference = \ self._update(node.next_ref, path.consume(len(node.path)), value) return self._store_node(Node.Extension(node.path, new_reference)) #  Extension-. #     . common_prefix = path.common_prefix(node.path) #  . path.consume(len(common_prefix)) node.path.consume(len(common_prefix)) #  Branch- ,  ,    . branches = [b''] * 16 branch_value = value if len(path) == 0 else b'' #     Leaf-  Extension- . self._create_branch_leaf(path, value, branches) self._create_branch_extension(node.path, node.next_ref, branches) branch_reference = self._store_node(Node.Branch(branches, branch_value)) # ,    Extension-. if len(common_prefix) != 0: return self._store_node(Node.Extension(common_prefix, branch_reference)) else: return branch_reference 

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 - DeleteActionque nous reviendrons en haut.


Le cadre de la méthode deleteressemblera à ceci:


 class MerklePatriciaTrie: # ... # Enum, ,         . class _DeleteAction(Enum): #    . #     , #        (_DeleteAction, None). DELETED = 1, #    (,    ). #     ,    #    : (_DeleteAction, ___). UPDATED = 2, #    Branch-  .   -- #    : # (_DeleteAction, (___, ___)) USELESS_BRANCH = 3 def delete(self, encoded_key): if self._root is None: return path = NibblePath(encoded_key) action, info = self._delete(self._root, path) if action == MerklePatriciaTrie._DeleteAction.DELETED: #   . self._root = None elif action == MerklePatriciaTrie._DeleteAction.UPDATED: #   . new_root = info self._root = new_root elif action == MerklePatriciaTrie._DeleteAction.USELESS_BRANCH: #   . _, new_root = info self._root = new_root def _delete(self, node_ref, path): node = self._get_node(node_ref) if type(node) == Node.Leaf: pass elif type(node) == Node.Extension: pass elif type(node) == Node.Branch: pass 

, 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- :


  1. , Extension- . — .
  2. _delete , "" .
  3. . :

  • . .
  • . .
  • Branch-. . , Branch- . , , Leaf-. — Extension-.

:


 elif type(node) == Node.Extension: if not path.starts_with(node.path): raise KeyError #   . #       . action, info = self._delete(node.next_ref, path.consume(len(node.path))) if action == MerklePatriciaTrie._DeleteAction.DELETED: return action, None elif action == MerklePatriciaTrie._DeleteAction.UPDATED: #    ,     . child_ref = info new_ref = self._store_node(Node.Extension(node.path, child_ref)) return action, new_ref elif action == MerklePatriciaTrie._DeleteAction.USELESS_BRANCH: #     Branch-. stored_path, stored_ref = info # ,     Branch-. child = self._get_node(stored_ref) new_node = None if type(child) == Node.Leaf: #  branch-  . #     Leaf-  Extension. path = NibblePath.combine(node.path, child.path) new_node = Node.Leaf(path, child.data) elif type(child) == Node.Extension: #  Branch-  Extension-. #       . path = NibblePath.combine(node.path, child.path) new_node = Node.Extension(path, child.next_ref) elif type(child) == Node.Branch: #  Branch-      Branch-. #    Extension-    . path = NibblePath.combine(node.path, stored_path) new_node = Node.Extension(path, stored_ref) new_reference = self._store_node(new_node) return MerklePatriciaTrie._DeleteAction.UPDATED, new_reference 

if type(node) == Node.Branch


.


, . Branch-, 



Pourquoi? Branch- Leaf- ( ) Extension- ( ).
, . , — Leaf-. — Extension-. , , 2 — Branch- .


? :


:


  1. , .
  2. , _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: #   ,    . #    . idx = path.at(0) if len(node.branches[idx]) == 0: raise KeyError action, info = self._delete(node.branches[idx], path.consume(1)) #  ,   ,  . #      -    #    . node.branches[idx] = b'' 

_DeleteAction .


  1. Branch- , ( , ). .

 if action == MerklePatriciaTrie._DeleteAction.UPDATED: #   . next_ref = info node.branches[idx] = next_ref reference = self._store_node(node) return MerklePatriciaTrie._DeleteAction.UPDATED, reference elif action == MerklePatriciaTrie._DeleteAction.USELESS_BRANCH: #    . _, next_ref = info node.branches[idx] = next_ref reference = self._store_node(node) return MerklePatriciaTrie._DeleteAction.UPDATED, reference 

  1. ( , ), , .

. :


  • . , , , . , .
  • , . 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: # Branch- ,  . return MerklePatriciaTrie._DeleteAction.DELETED, None elif non_empty_count == 0 and len(node.data) != 0: #  ,   . path = NibblePath([]) reference = self._store_node(Node.Leaf(path, node.data)) return MerklePatriciaTrie._DeleteAction.USELESS_BRANCH, (path, reference) elif non_empty_count == 1 and len(node.data) == 0: #  ,   . return self._build_new_node_from_last_branch(node.branches) else: #  1+   ,  2+ . # Branch-  ,   - UPDATED. reference = self._store_node(node) return MerklePatriciaTrie._DeleteAction.UPDATED, reference 

_build_new_node_from_last_branch .


— Leaf Extension, , .


— Branch, Extension , , Branch.


 def _build_new_node_from_last_branch(self, branches): #    . idx = 0 for i in range(len(branches)): if len(branches[i]) > 0: idx = i break #     . prefix_nibble = NibblePath([idx], offset=1) #     child = self._get_node(branches[idx]) path = None node = None #   . if type(child) == Node.Leaf: path = NibblePath.combine(prefix_nibble, child.path) node = Node.Leaf(path, child.data) elif type(child) == Node.Extension: path = NibblePath.combine(prefix_nibble, child.path) node = Node.Extension(path, child.next_ref) elif type(child) == Node.Branch: path = prefix_nibble node = Node.Extension(path, branches[idx]) #  . reference = self._store_node(node) return MerklePatriciaTrie._DeleteAction.USELESS_BRANCH, (path, reference) 

Le reste


. , 
 root .


Ici:


 class MerklePatriciaTrie: # ... def root(self): return self._root 

, .



 . , , Ethereum . , , , . , :)


, , pip install -U eth_mpt — .


That's all folks!


Résultats


?


, -, , - , , . — , .


-, , , — . , skip list interval tree, — , , .


-, , . , - .


-, — .


, , — !



: 1 , 2 , 3 . ! , .

Source: https://habr.com/ru/post/fr446558/


All Articles