Dans cette section, l’objectif principal est d’expliquer comment Optimism utilise Libb2P pour compléter la mise en place de réseaux P2P dans Op-Node.
Les réseaux P2P sont principalement utilisés pour faire passer des informations dans différents nœuds, tels que les séquenceurs après avoir terminé la construction de blocs de Unsafe, et se propager via le pub/sub du gossiphub de P2P. Libb2P s’occupe également d’autres infrastructures telles que la mise en réseau, l’adressage, etc. dans les réseaux P2P.
En savoir plus sur libp2p
libp2p (abréviation de « library peer-to-peer » ou « library peer-to-peer ») est un framework de mise en réseau peer-to-peer (P2P) qui aide à développer des applications P2P. Il contient un ensemble de protocoles, de spécifications et de bibliothèques qui facilitent la communication P2P entre les participants du réseau (également appelés « pairs » ou « pairs ») (source[2] [3])。 Faisant à l’origine partie du projet IPFS (InterPlanetary File), libp2p a évolué pour devenir un projet autonome qui est devenu une pile de réseau modulaire (source) pour les réseaux distribués )。
libp2p est un projet open source de la communauté IPFS qui accueille les contributions d’un large éventail de communautés, y compris l’aide à la rédaction de spécifications, à l’implémentation de code et à la création d’exemples et de tutoriels[4] [5])。 libp2p est composé de plusieurs blocs de construction, chacun avec une interface très claire, documentée et testée qui les rend composables, remplaçables et donc évolutifs )。 La nature modulaire de libp2p permet aux développeurs de sélectionner et d’utiliser uniquement les composants nécessaires à leurs applications, ce qui favorise la flexibilité et l’efficacité lors de la création d’applications Web P2P.
Ressources connexes
Documentation officielle pour libp2p[6]
le dépôt GitHub de libp2p[7]
Introduction à libp2p sur ProtoSchool[8]
L’architecture modulaire et la nature open source de libp2p fournissent un bon environnement pour le développement d’applications P2P puissantes, évolutives et flexibles, ce qui en fait un acteur important dans le domaine des réseaux distribués et du développement d’applications réseau.
Implémentation de libp2p
Lorsque vous utilisez libp2p, vous devrez implémenter et configurer certains composants de base pour construire votre réseau P2P. Voici quelques-uns des principaux aspects de l’implémentation de libp2p dans l’application :
1. Création et configuration des nœuds :
La création et la configuration d’un nœud libp2p est l’étape la plus élémentaire, qui inclut la définition de l’adresse réseau, de l’identité et d’autres paramètres de base du nœud.
Codes d’utilisation clés :
libp2p. Nouveau()
2. Protocole de transport :
Choisissez et configurez vos protocoles de transport (par exemple, TCP, WebSockets, etc.) pour assurer la communication entre les nœuds.
Codes d’utilisation clés :
tcpTransport := tcp. NewTCPTransport()
3. Multiplexage et contrôle de flux :
Implémenter le multiplexage pour permettre le traitement de plusieurs flux de données simultanés sur une seule connexion.
Mettre en œuvre un contrôle de flux pour gérer le taux de transmission des données et le taux de traitement.
Codes d’utilisation clés :
yamuxTransport := yamux. Nouveau()
4. Sécurité et cryptage :
Configurer une couche de transport sécurisée pour assurer la sécurité et la confidentialité des communications.
Mettre en œuvre des mécanismes de cryptage et d’authentification pour protéger les données et authentifier les parties communicantes.
Codes d’utilisation clés :
tlsTransport := tls. Nouveau()
5. Protocoles et gestion des messages :
Définissez et mettez en œuvre des protocoles personnalisés pour gérer des opérations réseau et des échanges de messages spécifiques.
Traiter les messages reçus et envoyer des réponses au besoin.
Codes d’utilisation clés :
Implémenter un mécanisme de découverte de noeuds pour trouver d’autres noeuds dans le réseau.
Implémenter une logique de routage pour déterminer comment les messages sont acheminés vers les nœuds appropriés du réseau.
Codes d’utilisation clés :
dht := kaddht. NewDHT(ctx, hôte, banque de données. NewMapDatastore())
7. Comportement et politiques du réseau :
Définissez et implémentez des comportements et des stratégies réseau, tels que la gestion des connexions, la gestion des erreurs et la logique de nouvelle tentative.
Codes d’utilisation clés :
Consultez la documentation de libp2p pour en savoir plus sur ses différents composants et API.
Communiquer avec la communauté libp2p pour obtenir du soutien et résoudre des problèmes.
}
Ce qui précède sont quelques-uns des principaux aspects à prendre en compte et à implémenter lors de l’utilisation de libp2p. L’implémentation spécifique de chaque projet peut varier, mais ces aspects de base sont nécessaires pour construire et exécuter des applications libp2p. Lors de l’implémentation de ces fonctions, vous pouvez vous référer à la documentation officielle de libp2p[9] [10]et les dépôts GitHub Exemple de code et didacticiels.
Utilisation de libp2p dans le noeud OP
Afin de comprendre la relation entre op-node et libp2p, nous devons résoudre plusieurs questions
Pourquoi libp2p ? Pourquoi ne pas choisir devp2p (geth utilise devp2p)
Quelles données ou quels processus sont étroitement liés au réseau P2P dans le nœud OP
Comment ces fonctionnalités sont implémentées au niveau de la couche de code
Pourquoi les op-nodes ont besoin d’un réseau libp2p
Tout d’abord, nous devons comprendre pourquoi Optimism nécessite un réseau P2P ** LibbP2P est un protocole réseau modulaire qui permet aux développeurs de créer des applications peer-to-peer décentralisées pour de multiples cas d’utilisation (sources[11] [12])(la source [13])。 DevP2P, quant à lui, est principalement utilisé dans l’écosystème Ethereum et est adapté aux applications Ethereum (Source )。 La flexibilité et la large applicabilité de libp2p peuvent en faire un choix de premier ordre pour les développeurs.
Op-Node utilise principalement les points de fonction libp2p
Utilisé par le séquenceur pour transmettre le bloc non sécurisé résultant à d’autres nœuds non-séquenceurs
Pour les autres noeuds en mode non-séquenceur pour une synchronisation rapide en cas d’écart (synchronisation en chaîne inverse)
Un bon environnement pour l’adoption d’un système de réputation intégral pour réguler l’ensemble du nœud
Implémentation du code
Initialisation personnalisée de l’hôte
L’hôte peut être compris comme un nœud P2P, lors de l’ouverture de ce nœud, vous devez effectuer une configuration d’initialisation spéciale pour votre propre projet
Regardons maintenant la méthode Host dans le fichier op-node/p2p/host.go.
Cette fonction est principalement utilisée pour configurer l’hôte libp2p et effectuer diverses configurations. Voici les éléments clés de la fonction et une description chinoise simple de chacun d’entre eux :
Vérifiez si le P2P est désactivé
Si le P2P est désactivé, la fonction retourne directement.
Obtenir l’ID de l’homologue à partir de la clé publique
Utilisez la clé publique dans la configuration pour générer l’ID d’homologue.
Initialiser Peerstore
Créez une boutique Peerstore de base.
Initialiser l’extension Peerstore
En plus du Peerstore de base, créez un Peerstore étendu.
Ajouter des clés privées et publiques à Peerstore
Stockez les clés privées et publiques de l’homologue dans le magasin d’homologues.
Initialiser la connexion
Utilisé pour contrôler les connexions réseau.
Initialiser le Gestionnaire de connexions
Permet de gérer les connexions réseau.
Définir l’adresse de transmission et d’écoute
Définissez le protocole de transport réseau et l’adresse d’écoute de l’hôte.
Créer un hôte libp2p
Utilisez tous les paramètres précédents pour créer un nouvel hôte libp2p.
Initialiser l’homologue statique
Si vous avez configuré un homologue statique, initialisez-le.
Retour à l’hôte
Enfin, la fonction retourne l’hôte libp2p créé.
Ces sections clés sont responsables de l’initialisation et de la configuration de l’hôte libp2p, et chaque partie est responsable d’un aspect spécifique de la configuration de l’hôte.
func (conf *Config) Host(journal journal. Enregistreur, rapporteur de métriques. Rapporteur, métriques HostMetrics) (hôte. Hôte, erreur) {
Si conf. DésactiverP2P {
return nil, nil
}
pub := conf. Priv.GetPublic()
pid, err := homologue. IDFromCléPublique(pub)
if err != nil {
Retourner néant, fmt. Errorf(« Echec de dériver la clé pub à partir de la clé privée du réseau : %w », err)
}
basePs, err := pstoreds. NewPeerstore(contexte. Background(), conf. Magasin, pstockds. DefaultOpts())
if err != nil {
Retourner néant, fmt. Errorf(« échec de l’ouverture du peerstore : %w », err)
}
peerScoreParams := conf. PeerScoringParams()
scoreTemps de rétention. Durée
if peerScoreParams != nil {
Utilisez la même période de conservation que les commérages, le cas échéant.
scoreRetention = peerScoreParams.PeerScoring.RetainScore
} else {
Désactiver le score GC si le score des pairs est désactivé
scoreRetention = 0
}
ps, err := magasin. NewExtendedPeerstore(contexte. Background(), journal, horloge. Horloge, basePs, conf. Stocker, scoreRétention)
if err != nil {
Retourner néant, fmt. Errorf(« Echec de l’ouverture du peerstore étendu : %w », err)
}
if err := ps. AddPrivKey(pid, conf. Privé) ; err != nil {
Retourner néant, fmt. Errorf(« échec de la configuration du peerstore avec la clé priv : %w », err)
}
if err := ps. AddPubKey(pid, pub) ; err != nil {
Retourner néant, fmt. Errorf(« Echec de la configuration du peerstore avec la clé pub : %w », err)
}
connGtr gating. BlockingConnectionGater (en anglais seulement)
connGtr, err = gating. NewBlockingConnectionGater(conf. Magasin)
if err != nil {
Retourner néant, fmt. Errorf(« échec de l’ouverture du portail de connexion : %w », err)
}
connGtr = portail. AddBanExpiry(connGtr, ps, log, clock. Horloge, métriques)
connGtr = portail. AddMetering(connGtr, métriques)
connMngr, err := DefaultConnManager(conf)
if err != nil {
Retourner néant, fmt. Errorf(« Echec de l’ouverture du gestionnaire de connexions : %w », err)
}
listenAddr, err := addrFromIPAndPort(conf. ListenIP, conf. ListenTCPPort)
if err != nil {
Retourner néant, fmt. Errorf(« échec de l’écoute addr : %w », err)
}
tcpTransport := libp2p. Transports(
Tcp. NewTCPTransport,
Tcp. WithConnectionTimeout(heure. Minute*60)) // rompre les connexions inutilisées
TODO : techniquement, nous pouvons également exécuter le nœud sur websocket et les transports QUIC Peut-être à l’avenir ?
Nat Lconf. NATManagerC // désactivé si nul
Si conf. NAT {
nat = hôte de base. NewNATManager
}
opts := []libp2p. Option{
libp2p. Identité(conf. Priv),
Définir explicitement l’agent utilisateur, afin que nous puissions nous différencier des autres utilisateurs de Go libp2p.
libp2p. UserAgent(conf. UserAgent),
tcpTransport,
libp2p. WithDialTimeout(conf. TimeoutDial),
Pas de services de relais, connexions directes entre pairs uniquement.
libp2p. DisableRelay(),
L’hôte démarrera et écoutera le réseau directement après la construction à partir de la configuration.
libp2p. ListenAddrs(listenAddr),
libp2p. ConnectionGater(connGtr),
libp2p. ConnectionManager(connMngr),
libp2p. ResourceManager(nil), // TODO utilise l’interface du gestionnaire de ressources pour mieux gérer les ressources par pair.
libp2p. Ping(vrai),
libp2p. AutoNATServiceRateLimit(10, 5, heure. Deuxièmement*60),
}
Si conf. NoTransportSecurity {
} else {
opts = append(opts, conf. HostSecurity...)
}
h, err := libp2p. Nouveau(opts...)
if err != nil {
}
for i, peerAddr := range conf. HomologuesStatiques {
if err != nil {
}
staticPeers (Pairs statiques)[i] = addr
}
out := &extraHost{
Animateur : h,
staticPeers : staticPeers,
}
sortir.monitorStaticPeers()
}
...
n.gs, err = NewGossipSub(resourcesCtx, n.host, rollupCfg, setup, n.scorer, metrics, log)
if err != nil {
}
...
Création de l’authentificateur :
Génération de noms de thème de bloc :
Générez blocksTopicName à l’aide de la fonction blocksTopicV1, qui formate une chaîne basée sur le L2ChainID dans la configuration (cfg). Les chaînes formatées suivent une structure spécifique : /optimism/{L2ChainID}/0/blocks.
en appelant ps. Join(blocksTopicName) tente d’ajouter un thème de potins de bloc. Si une erreur se produit, un message d’erreur indique que la rubrique n’a pas pu être jointe.
Génération d’une nouvelle goroutine pour enregistrer les événements de rubrique à l’aide de la fonction LogTopicEvents.
Création d’un abonné à l’aide de la fonction MakeSubscriber, qui encapsule un BlocksHandler qui gère les événements OnUnsafeL2Payload à partir de gossipIn. Une nouvelle goroutine a été générée pour exécuter la subion fournie.
func JoinGossip(p2pCtx context. Contexte, auto peer.ID, ps *pubsub. PubSub, journal de journal. Enregistreur, cfg *rollup. Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, error) {
blocksTopicName := blocksTopicV1(cfg) // retourne fmt. Sprintf(« /optimisme/%s/0/blocs », cfg. L2ChainID.String())
Retourner néant, fmt. Errorf(« Echec de l’enregistrement des blocs, sujet des potins : %w », euh)
}
blocksTopic, err := ps. Joindre(blocksTopicName)
if err != nil {
}
if err != nil {
}
go LogTopicEvents(p2pCtx, log. Nouveau(« sujet », « blocs »), blocsÉvénementsSujet)
if err != nil {
}
return &publisher{log : log, cfg : cfg, blocksTopic : blocksTopic, runCfg : runCfg}, nil
op-node/rollup/driver/state.go
func (s *Pilote) eventLoop() {
for(){
...
charge utile, err := s.sequencer.RunNextSequencerAction(ctx)
if err != nil {
Les erreurs ne sont pas assez graves pour modifier ou arrêter le séquençage, mais elles doivent être consignées et mesurées.
if err := s.network.PublishL2Payload(ctx, charge utile) ; err != nil {
}
planSequencerAction() // planifie la prochaine action du séquenceur pour que le séquençage continue à tourner en boucle
}
}
...
Lorsqu’il y a des blocs manquants, synchronisation rapide via P2P
Lorsque les nœuds sont reliés après un temps d’arrêt en raison de circonstances particulières, telles que le relien après un temps d’arrêt, certains blocs (lacunes) peuvent ne pas être synchronisés, et lorsque vous rencontrez cette situation, ils peuvent rapidement se synchroniser via la chaîne inverse du réseau P2P.
Jetons un coup d’œil à la fonction checkForGapInUnsafeQueue dans op-node/rollup/driver/state.go
L’extrait de code définit une méthode appelée checkForGapInUnsafeQueue, qui appartient à la structure Driver. Son but est de vérifier les lacunes de données dans une file d’attente appelée « file d’attente non sécurisée » et de tenter de récupérer les charges utiles manquantes via une autre méthode de synchronisation appelée altSync. Le point clé ici est que la méthode consiste à assurer la continuité des données et à essayer de récupérer les données manquantes à partir d’autres méthodes de synchronisation lorsque des données manquantes sont détectées. Voici les principales étapes de la fonction :
La fonction obtient d’abord UnsafeL2Head et UnsafeL2SyncTarget à partir de s.derivation en tant que points de début et de fin de la plage de vérification.
La fonction vérifie les blocs manquants entre le début et la fin, ce qui se fait en comparant les valeurs numériques de fin et de début.
Si un écart de données est détecté, la fonction demandera la plage de données manquante en appelant s.altSync.RequestL2Range(ctx, start, end). Si end est une référence nulle (c’est-à-dire eth. L2BlockRef{}), la fonction demandera une synchronisation de plage ouverte, en commençant par start.
Lors de la demande de données, la fonction enregistre un journal de débogage indiquant la plage de données qu’elle demande.
La fonction finit par renvoyer une valeur d’erreur. S’il n’y a pas d’erreurs, il renvoie nil
checkForGapInUnsafeQueue vérifie s’il existe une lacune dans la file d’attente non sécurisée et tente de récupérer les charges utiles manquantes à partir d’une méthode alt-sync.
ATTENTION : Il ne s’agit que d’un signal sortant, il n’est pas garanti que les blocs soient récupérés.
Les résultats sont reçus par le biais de la charge utile OnUnsafeL2.
func (s *Pilote) checkForGapInUnsafeQueue(ctx context. Contexte) erreur {
start := s.derivation.UnsafeL2Head()
end := s.derivation.UnsafeL2SyncTarget()
Vérifiez si nous avons des blocs manquants entre le début et la fin Demandez-les si c’est le cas.
si fin == (eth. L2BlockRef{}) {
s.log.Debug(« demande de synchronisation avec plage ouverte », « start », start)
return s.altSync.RequestL2Range(ctx, start, eth. L2BlockRef{})
} sinon si fin. Nombre > départ. Nombre+1 {
s.log.Debug(« demande de plage de blocs L2 non sécurisée manquante », « start », start, « end », end, « size », end. Nombre-début.Nombre)
return s.altSync.RequestL2Range(ctx, début, fin)
}
Retour néant
}
La fonction RequestL2Range signale le début et la fin du bloc de requêtes au canal de requêtes.
La requête est ensuite distribuée au canal peerRequests via la méthode onRangeRequest, et le canal peerRequests est attendu par la boucle ouverte par plusieurs pairs, c’est-à-dire qu’un seul homologue traitera la requête pour chaque distribution.
func (s *SyncClient) onRangeRequest(ctx context. Context, req rangeRequest) {
...
pour i := uint64(0) ; ; i++ {
num := req.end.Number - 1 - i
if num <= req.start {
rendre
}
vérifier si nous avons déjà quelque chose en quarantaine
if h, ok := s.quarantineByNum[num] ; ok {
if s.trusted.Contains(h) { // si nous lui faisons confiance, essayez de le promouvoir.
s.tryPromote(h)
}
N’allez pas chercher des choses pour lesquelles nous avons déjà un candidat.
Nous l’expulserons de la quarantaine en trouvant un conflit, ou si nous synchronisons suffisamment d’autres blocs
continuer
}
if _, ok := s.inFlight[num] ; ok {
.log. Debug(« requête toujours en cours, ne replanifie pas la demande de synchronisation », « num », num)
continuer // demande toujours en cours
}
pr := peerRequest{num : num, complete : new(atomic. Booléen)}
.log. Debug(« Planification de la demande de blocage P2P », « num », num)
Numéro d’horaire
Sélectionnez {
case s.peerRequests <- pr :
s.inFlight[num] = pr.complet
Affaire <-CTX. Fait() :
.log. Info(« n’a pas planifié la plage de synchronisation P2P complète », « current », num, « err », ctx. Err())
rendre
Par défaut : // Les pairs peuvent tous être déjà occupés à traiter les requêtes
.log. Info(« aucun homologue n’est prêt à gérer les demandes de blocage pour plus de requêtes P2P pour l’historique des blocs L2 », « current », num)
rendre
}
}
}
Voyons ce qui se passe lorsque l’homologue reçoit cette demande.
La première chose que nous devons savoir est que le lien entre l’homologue et le nœud demandeur, ou le message transmis, est transmis par le flux libp2p. La méthode de traitement du flux est implémentée par le nœud homologue récepteur, et la création du flux est ouverte par le nœud émetteur.
Nous pouvons voir ce code dans la fonction d’initialisation précédente, où MakeStreamHandler renvoie un gestionnaire, et SetStreamHandler lie l’ID de protocole à ce gestionnaire, de sorte que chaque fois que le nœud émetteur crée et utilise ce flux, le gestionnaire retourné est déclenché.
n.syncSrv = NewReqRespServer(rollupCfg, l2Chain, métriques)
Enregistrez le protocole de synchronisation avec l’hôte libp2p
payloadByNumber := MakeStreamHandler(resourcesCtx, log. Nouveau(« servir », « payloads_by_number »), n.syncSrv.HandleSyncRequest)
n.host.SetStreamHandler(PayloadByNumberProtocolID(rollupCfg.L2ChainID), payloadByNumber)
Voyons comment il est géré à l’intérieur du gestionnaire
La fonction effectue d’abord des vérifications globales et individuelles de la limite de débit pour contrôler la rapidité avec laquelle les demandes sont traitées. Il lit et vérifie ensuite le numéro de bloc demandé, en s’assurant qu’il se situe dans une fourchette raisonnable. La fonction récupère ensuite la charge utile de bloc de la requête à partir de la couche L2 et l’écrit dans le flux de réponse. Lors de l’écriture des données de réponse, il définit une date limite d’écriture pour éviter d’être bloqué par des connexions homologues lentes pendant le processus d’écriture. Enfin, la fonction renvoie le numéro de bloc demandé et les erreurs possibles.
func (srv *ReqRespServer) handleSyncRequest(ctx context. Contexte, réseau de flux. Flux) (uint64, erreur) {
peerId := flux. Conn(). RemotePeer()
prendre un jeton du limiteur de débit global,
pour s’assurer qu’il n’y a pas trop de travail simultané sur le serveur entre différents homologues.
if err := srv.globalRequestsRL.Wait(ctx) ; err != nil {
Renvoie 0, fmt. Errorf(« Délai d’attente de la limite globale du débit de synchronisation : %w », err)
}
Rechercher les données de limitation de débit de l’homologue, ou ajouter autrement
srv.peerStatsLock.Lock()
ps, _ := srv.peerRateLimits.Get(peerId)
if ps == nil {
ps = &pairStat{
Demandes : tarif. NewLimiter(peerServerBlocksRateLimit, peerServerBlocksBurst),
}
srv.peerRateLimits.Add(peerId, ps)
Ps. Requests.Reserve() // compte l’accès, mais le fait retarder la requête suivante plutôt que d’attendre immédiatement
} else {
N’attendez que s’il s’agit d’un homologue existant, sinon l’appel d’attente de limite de débit instantané génère toujours des erreurs.
Si le demandeur pense que nous prenons trop de temps, c’est son problème et il peut se déconnecter.
Nous ne nous déconnecterons que lorsque nous ne parviendrons pas à lire/écrire,
si le travail n’est pas valide (validation de plage) ou lorsque des sous-tâches individuelles expirent.
if err := ps. Demandes.Attendre(ctx) ; err != nil {
Renvoie 0, fmt. Errorf(« Délai d’attente de la limite globale du débit de synchronisation : %w », err)
}
}
srv.peerStatsLock.Unlock()
Définir une date limite de lecture, le cas échéant
_ = flux. SetReadDeadline(heure. Maintenant(). Ajouter(serveurLectureRequestTimeout))
Lire la demande
req uint64
if err := binaire. Read(stream, binaire. LittleEndian, &req) ; err != nil {
Renvoie 0, fmt. Errorf(« Impossible de lire le numéro de bloc demandé : %w », err)
}
if err := flux. FermerLire() ; err != nil {
retour req, fmt. Errorf(« échec de la fermeture côté lecture d’un appel de demande de synchronisation P2P : %w », err)
}
Vérifiez que la demande se situe dans la plage de blocs attendue
if req < srv.cfg.Genesis.L2.Number {
retour req, fmt. Errorf(« Impossible de servir la demande pour le bloc L2 %d avant genesis %d : %w », req, srv.cfg.Genesis.L2.Number, invalidRequestErr)
}
max, err := srv.cfg.TargetBlockNumber(uint64(time. Maintenant(). Unix()))
if err != nil {
retour req, fmt. Errorf(« Impossible de déterminer le nombre maximal de blocs cibles pour vérifier la requête : %w », invalidRequestErr)
}
if req > max {
retour req, fmt. Errorf(« Impossible de servir la requête pour le bloc L2 %d après le bloc attendu max (%v) : %w », req, max, invalidRequestErr)
}
charge utile, err := srv.l2.PayloadByNumber(ctx, req)
if err != nil {
si des erreurs. C’est (euh, l’ethereum. NotFound) {
retour req, fmt. Errorf(« Bloc inconnu demandé par l’homologue par numéro : %w », err)
} else {
retour req, fmt. Errorf(« Echec de la récupération de la charge utile à servir à l’homologue : %w », err)
}
}
Nous définissons une date limite d’écriture, si elle est disponible, pour écrire en toute sécurité sans blocage sur une connexion homologue étranglée
_ = flux. SetWriteDeadline(heure. Maintenant(). Ajouter(serveurWriteChunkTimeout))
0 - resultCode : succès = 0
1 :5 - version : 0
.tmp [5] octet
if _, err := flux. Écrire(tmp[ :]) ; err != nil {
retour req, fmt. Errorf(« Echec de l’écriture des données d’en-tête de réponse : %w », err)
}
w := accrocheur. NewBufferedWriter(flux)
if _, err := charge utile. Maréchal SSZ(w) ; err != nil {
retour req, fmt. Errorf(« échec de l’écriture de la charge utile pour synchroniser la réponse : %w », err)
}
if err := w.Fermer() ; err != nil {
retour req, fmt. Errorf(« Echec de l’écriture de la charge utile pour synchroniser la réponse : %w », err)
}
return req, nil
}
À ce stade, le processus général de demande et de traitement de synchronisation de la chaîne inverse a été expliqué
Système de réputation des points dans les nœuds p2p
Afin d’empêcher certains nœuds de faire des requêtes et des réponses malveillantes qui compromettent la sécurité de l’ensemble du réseau, Optimism utilise également un système de points.
Par exemple, dans op-node/p2p/app_scores.go, il existe une série de fonctions pour définir le score d’un pair
func (s *peerApplicationScorer) onValidResponse(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementValidResponses{Cap : s.params.ValidResponseCap})
if err != nil {
s.log.Error(« Impossible de mettre à jour le score de l’homologue », « peer », id, « err », err)
rendre
}
}
func (s *peerApplicationScorer) onResponseError(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementErrorResponses{Cap : s.params.ErrorResponseCap})
if err != nil {
s.log.Error(« Impossible de mettre à jour le score de l’homologue », « peer », id, « err », err)
rendre
}
}
func (s *peerApplicationScorer) onRejectedPayload(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementRejectedPayloads{Cap : s.params.RejectedPayloadCap})
if err != nil {
s.log.Error(« Impossible de mettre à jour le score de l’homologue », « peer », id, « err », err)
rendre
}
}
L’état d’intégration du nouveau nœud est ensuite vérifié avant son ajout
La grande configurabilité de libp2p rend l’ensemble du projet p2p hautement personnalisable et modulaire, ce qui précède est la logique principale de l’implémentation personnalisée de libp2p d’optimsim, et d’autres détails peuvent être appris en détail en lisant le code source dans le répertoire p2p.
Voir l'original
Cette page peut inclure du contenu de tiers fourni à des fins d'information uniquement. Gate ne garantit ni l'exactitude ni la validité de ces contenus, n’endosse pas les opinions exprimées, et ne fournit aucun conseil financier ou professionnel à travers ces informations. Voir la section Avertissement pour plus de détails.
L’utilisation de libp2p dans op-stack
Écrit par Joohhnnn
Applications Libp2P dans Optimism
Dans cette section, l’objectif principal est d’expliquer comment Optimism utilise Libb2P pour compléter la mise en place de réseaux P2P dans Op-Node. Les réseaux P2P sont principalement utilisés pour faire passer des informations dans différents nœuds, tels que les séquenceurs après avoir terminé la construction de blocs de Unsafe, et se propager via le pub/sub du gossiphub de P2P. Libb2P s’occupe également d’autres infrastructures telles que la mise en réseau, l’adressage, etc. dans les réseaux P2P.
En savoir plus sur libp2p
libp2p (abréviation de « library peer-to-peer » ou « library peer-to-peer ») est un framework de mise en réseau peer-to-peer (P2P) qui aide à développer des applications P2P. Il contient un ensemble de protocoles, de spécifications et de bibliothèques qui facilitent la communication P2P entre les participants du réseau (également appelés « pairs » ou « pairs ») (source[2] [3])。 Faisant à l’origine partie du projet IPFS (InterPlanetary File), libp2p a évolué pour devenir un projet autonome qui est devenu une pile de réseau modulaire (source) pour les réseaux distribués )。
libp2p est un projet open source de la communauté IPFS qui accueille les contributions d’un large éventail de communautés, y compris l’aide à la rédaction de spécifications, à l’implémentation de code et à la création d’exemples et de tutoriels[4] [5])。 libp2p est composé de plusieurs blocs de construction, chacun avec une interface très claire, documentée et testée qui les rend composables, remplaçables et donc évolutifs )。 La nature modulaire de libp2p permet aux développeurs de sélectionner et d’utiliser uniquement les composants nécessaires à leurs applications, ce qui favorise la flexibilité et l’efficacité lors de la création d’applications Web P2P.
Ressources connexes
L’architecture modulaire et la nature open source de libp2p fournissent un bon environnement pour le développement d’applications P2P puissantes, évolutives et flexibles, ce qui en fait un acteur important dans le domaine des réseaux distribués et du développement d’applications réseau.
Implémentation de libp2p
Lorsque vous utilisez libp2p, vous devrez implémenter et configurer certains composants de base pour construire votre réseau P2P. Voici quelques-uns des principaux aspects de l’implémentation de libp2p dans l’application :
1. Création et configuration des nœuds :
libp2p. Nouveau()
2. Protocole de transport :
Choisissez et configurez vos protocoles de transport (par exemple, TCP, WebSockets, etc.) pour assurer la communication entre les nœuds. Codes d’utilisation clés :
tcpTransport := tcp. NewTCPTransport()
3. Multiplexage et contrôle de flux :
yamuxTransport := yamux. Nouveau()
4. Sécurité et cryptage :
tlsTransport := tls. Nouveau()
5. Protocoles et gestion des messages :
Définissez et mettez en œuvre des protocoles personnalisés pour gérer des opérations réseau et des échanges de messages spécifiques.
hôte. SetStreamHandler(« /mon-protocole/1.0.0 », myProtocolHandler)
6. Découverte et routage :
dht := kaddht. NewDHT(ctx, hôte, banque de données. NewMapDatastore())
7. Comportement et politiques du réseau :
Définissez et implémentez des comportements et des stratégies réseau, tels que la gestion des connexions, la gestion des erreurs et la logique de nouvelle tentative. Codes d’utilisation clés :
connManager := connmgr. NewConnManager(lowWater, highWater, gracePeriod)
8. Gestion de l’état et stockage :
Gérez l’état des nœuds et des réseaux, y compris l’état de la connexion, la liste des nœuds et le stockage des données. Codes d’utilisation clés :
peerstore := pstoremem. NewPeerstore()
9. Test et débogage :
exploitation forestière. SetLogLevel(« libp2p », « DEBUG »)
10. Documentation et soutien communautaire :
}
Ce qui précède sont quelques-uns des principaux aspects à prendre en compte et à implémenter lors de l’utilisation de libp2p. L’implémentation spécifique de chaque projet peut varier, mais ces aspects de base sont nécessaires pour construire et exécuter des applications libp2p. Lors de l’implémentation de ces fonctions, vous pouvez vous référer à la documentation officielle de libp2p[9] [10]et les dépôts GitHub Exemple de code et didacticiels.
Utilisation de libp2p dans le noeud OP
Afin de comprendre la relation entre op-node et libp2p, nous devons résoudre plusieurs questions
Pourquoi les op-nodes ont besoin d’un réseau libp2p
Tout d’abord, nous devons comprendre pourquoi Optimism nécessite un réseau P2P ** LibbP2P est un protocole réseau modulaire qui permet aux développeurs de créer des applications peer-to-peer décentralisées pour de multiples cas d’utilisation (sources[11] [12])(la source [13])。 DevP2P, quant à lui, est principalement utilisé dans l’écosystème Ethereum et est adapté aux applications Ethereum (Source )。 La flexibilité et la large applicabilité de libp2p peuvent en faire un choix de premier ordre pour les développeurs.
Op-Node utilise principalement les points de fonction libp2p
Implémentation du code
Initialisation personnalisée de l’hôte
L’hôte peut être compris comme un nœud P2P, lors de l’ouverture de ce nœud, vous devez effectuer une configuration d’initialisation spéciale pour votre propre projet
Regardons maintenant la méthode Host dans le fichier op-node/p2p/host.go.
Cette fonction est principalement utilisée pour configurer l’hôte libp2p et effectuer diverses configurations. Voici les éléments clés de la fonction et une description chinoise simple de chacun d’entre eux :
Si le P2P est désactivé, la fonction retourne directement.
Utilisez la clé publique dans la configuration pour générer l’ID d’homologue.
Créez une boutique Peerstore de base.
En plus du Peerstore de base, créez un Peerstore étendu.
Stockez les clés privées et publiques de l’homologue dans le magasin d’homologues.
Utilisé pour contrôler les connexions réseau.
Permet de gérer les connexions réseau.
Définissez le protocole de transport réseau et l’adresse d’écoute de l’hôte.
Utilisez tous les paramètres précédents pour créer un nouvel hôte libp2p.
Si vous avez configuré un homologue statique, initialisez-le.
Enfin, la fonction retourne l’hôte libp2p créé.
Ces sections clés sont responsables de l’initialisation et de la configuration de l’hôte libp2p, et chaque partie est responsable d’un aspect spécifique de la configuration de l’hôte.
Générez blocksTopicName à l’aide de la fonction blocksTopicV1, qui formate une chaîne basée sur le L2ChainID dans la configuration (cfg). Les chaînes formatées suivent une structure spécifique : /optimism/{L2ChainID}/0/blocks.
en appelant ps. Join(blocksTopicName) tente d’ajouter un thème de potins de bloc. Si une erreur se produit, un message d’erreur indique que la rubrique n’a pas pu être jointe.
Génération d’une nouvelle goroutine pour enregistrer les événements de rubrique à l’aide de la fonction LogTopicEvents.
Création d’un abonné à l’aide de la fonction MakeSubscriber, qui encapsule un BlocksHandler qui gère les événements OnUnsafeL2Payload à partir de gossipIn. Une nouvelle goroutine a été générée pour exécuter la subion fournie.
func JoinGossip(p2pCtx context. Contexte, auto peer.ID, ps *pubsub. PubSub, journal de journal. Enregistreur, cfg *rollup. Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, error) {
blocksTopicName := blocksTopicV1(cfg) // retourne fmt. Sprintf(« /optimisme/%s/0/blocs », cfg. L2ChainID.String())
Retourner néant, fmt. Errorf(« Echec de l’enregistrement des blocs, sujet des potins : %w », euh)
}
blocksTopic, err := ps. Joindre(blocksTopicName)
if err != nil {
}
if err != nil {
}
go LogTopicEvents(p2pCtx, log. Nouveau(« sujet », « blocs »), blocsÉvénementsSujet)
if err != nil {
}
return &publisher{log : log, cfg : cfg, blocksTopic : blocksTopic, runCfg : runCfg}, nil
op-node/rollup/driver/state.go
Lorsqu’il y a des blocs manquants, synchronisation rapide via P2P
Lorsque les nœuds sont reliés après un temps d’arrêt en raison de circonstances particulières, telles que le relien après un temps d’arrêt, certains blocs (lacunes) peuvent ne pas être synchronisés, et lorsque vous rencontrez cette situation, ils peuvent rapidement se synchroniser via la chaîne inverse du réseau P2P.
Jetons un coup d’œil à la fonction checkForGapInUnsafeQueue dans op-node/rollup/driver/state.go
L’extrait de code définit une méthode appelée checkForGapInUnsafeQueue, qui appartient à la structure Driver. Son but est de vérifier les lacunes de données dans une file d’attente appelée « file d’attente non sécurisée » et de tenter de récupérer les charges utiles manquantes via une autre méthode de synchronisation appelée altSync. Le point clé ici est que la méthode consiste à assurer la continuité des données et à essayer de récupérer les données manquantes à partir d’autres méthodes de synchronisation lorsque des données manquantes sont détectées. Voici les principales étapes de la fonction :
La fonction obtient d’abord UnsafeL2Head et UnsafeL2SyncTarget à partir de s.derivation en tant que points de début et de fin de la plage de vérification.
La fonction vérifie les blocs manquants entre le début et la fin, ce qui se fait en comparant les valeurs numériques de fin et de début.
Si un écart de données est détecté, la fonction demandera la plage de données manquante en appelant s.altSync.RequestL2Range(ctx, start, end). Si end est une référence nulle (c’est-à-dire eth. L2BlockRef{}), la fonction demandera une synchronisation de plage ouverte, en commençant par start.
Lors de la demande de données, la fonction enregistre un journal de débogage indiquant la plage de données qu’elle demande.
La fonction finit par renvoyer une valeur d’erreur. S’il n’y a pas d’erreurs, il renvoie nil
checkForGapInUnsafeQueue vérifie s’il existe une lacune dans la file d’attente non sécurisée et tente de récupérer les charges utiles manquantes à partir d’une méthode alt-sync.
ATTENTION : Il ne s’agit que d’un signal sortant, il n’est pas garanti que les blocs soient récupérés.
Les résultats sont reçus par le biais de la charge utile OnUnsafeL2.
func (s *Pilote) checkForGapInUnsafeQueue(ctx context. Contexte) erreur {
start := s.derivation.UnsafeL2Head()
end := s.derivation.UnsafeL2SyncTarget()
Vérifiez si nous avons des blocs manquants entre le début et la fin Demandez-les si c’est le cas.
si fin == (eth. L2BlockRef{}) {
s.log.Debug(« demande de synchronisation avec plage ouverte », « start », start)
return s.altSync.RequestL2Range(ctx, start, eth. L2BlockRef{})
} sinon si fin. Nombre > départ. Nombre+1 {
s.log.Debug(« demande de plage de blocs L2 non sécurisée manquante », « start », start, « end », end, « size », end. Nombre-début.Nombre)
return s.altSync.RequestL2Range(ctx, début, fin)
}
Retour néant
}
La fonction RequestL2Range signale le début et la fin du bloc de requêtes au canal de requêtes.
La requête est ensuite distribuée au canal peerRequests via la méthode onRangeRequest, et le canal peerRequests est attendu par la boucle ouverte par plusieurs pairs, c’est-à-dire qu’un seul homologue traitera la requête pour chaque distribution.
Voyons ce qui se passe lorsque l’homologue reçoit cette demande.
La première chose que nous devons savoir est que le lien entre l’homologue et le nœud demandeur, ou le message transmis, est transmis par le flux libp2p. La méthode de traitement du flux est implémentée par le nœud homologue récepteur, et la création du flux est ouverte par le nœud émetteur.
Nous pouvons voir ce code dans la fonction d’initialisation précédente, où MakeStreamHandler renvoie un gestionnaire, et SetStreamHandler lie l’ID de protocole à ce gestionnaire, de sorte que chaque fois que le nœud émetteur crée et utilise ce flux, le gestionnaire retourné est déclenché.
Voyons comment il est géré à l’intérieur du gestionnaire La fonction effectue d’abord des vérifications globales et individuelles de la limite de débit pour contrôler la rapidité avec laquelle les demandes sont traitées. Il lit et vérifie ensuite le numéro de bloc demandé, en s’assurant qu’il se situe dans une fourchette raisonnable. La fonction récupère ensuite la charge utile de bloc de la requête à partir de la couche L2 et l’écrit dans le flux de réponse. Lors de l’écriture des données de réponse, il définit une date limite d’écriture pour éviter d’être bloqué par des connexions homologues lentes pendant le processus d’écriture. Enfin, la fonction renvoie le numéro de bloc demandé et les erreurs possibles.
À ce stade, le processus général de demande et de traitement de synchronisation de la chaîne inverse a été expliqué
Système de réputation des points dans les nœuds p2p
Afin d’empêcher certains nœuds de faire des requêtes et des réponses malveillantes qui compromettent la sécurité de l’ensemble du réseau, Optimism utilise également un système de points.
Par exemple, dans op-node/p2p/app_scores.go, il existe une série de fonctions pour définir le score d’un pair
L’état d’intégration du nouveau nœud est ensuite vérifié avant son ajout
Résumé
La grande configurabilité de libp2p rend l’ensemble du projet p2p hautement personnalisable et modulaire, ce qui précède est la logique principale de l’implémentation personnalisée de libp2p d’optimsim, et d’autres détails peuvent être appris en détail en lisant le code source dans le répertoire p2p.