En esta sección, el propósito principal es explicar cómo Optimism utiliza Libb2P para completar el establecimiento de redes P2P en Op-Node.
Las redes P2P se utilizan principalmente para pasar información en diferentes nodos, como secuenciadores después de completar la construcción de bloques de Unsafe, y propagarse a través del pub/sub del gossiphub de P2P. Libb2P también se ocupa de otras infraestructuras como redes, direccionamiento, etc. en redes P2P.
Más información sobre libp2p
libp2p (abreviado de "library peer-to-peer" o "library peer-to-peer") es un marco de red peer-to-peer (P2P) que ayuda a desarrollar aplicaciones P2P. Contiene un conjunto de protocolos, especificaciones y bibliotecas que facilitan la comunicación P2P entre los participantes de la red (también conocidos como "peers" o "peers") (fuente[2] [3])。 Originalmente parte del proyecto IPFS (InterPlanetary File), libp2p evolucionó hasta convertirse en un proyecto independiente que se convirtió en una pila de red modular (fuente) para redes distribuidas )。
libp2p es un proyecto de código abierto de la comunidad IPFS que da la bienvenida a las contribuciones de una amplia gama de comunidades, incluida la ayuda para escribir especificaciones, implementaciones de código y crear ejemplos y tutoriales[4] [5])。 libp2p se compone de varios bloques de construcción, cada uno con una interfaz muy clara, documentada y probada que los hace componibles, reemplazables y, por lo tanto, actualizables )。 La naturaleza modular de libp2p permite a los desarrolladores seleccionar y utilizar solo los componentes necesarios para sus aplicaciones, lo que promueve la flexibilidad y la eficiencia a la hora de crear aplicaciones web P2P.
Recursos relacionados
Documentación oficial para libp2p[6]
Repositorio GitHub de libp2p[7]
Introducción a libp2p en ProtoSchool[8]
La arquitectura modular y la naturaleza de código abierto de libp2p proporcionan un buen entorno para desarrollar aplicaciones P2P potentes, escalables y flexibles, lo que lo convierte en un actor importante en el campo de las redes distribuidas y el desarrollo de aplicaciones de red.
Implementación de libp2p
Al usar libp2p, deberá implementar y configurar algunos componentes principales para construir su red P2P. Estos son algunos de los principales aspectos de implementación de libp2p en la aplicación:
1. Creación y configuración de nodos:
Crear y configurar un nodo libp2p es el paso más básico, que incluye establecer la dirección de red del nodo, la identidad y otros parámetros básicos.
Códigos de uso clave:
libp2p. Nuevo()
2. Protocolo de transporte:
Elija y configure sus protocolos de transporte (por ejemplo, TCP, WebSockets, etc.) para garantizar la comunicación entre nodos.
Códigos de uso clave:
tcpTransport := tcp. NewTCPTransport()
3. Multiplexación y control de flujo:
Implementar la multiplexación para permitir que se procesen múltiples flujos de datos simultáneos en una sola conexión.
Implementar el control de flujo para administrar la velocidad de transmisión de datos y la velocidad de procesamiento.
Códigos de uso clave:
yamuxTransport := yamux. Nuevo()
4. Seguridad y encriptación:
Configurar una capa de transporte segura para garantizar la seguridad y privacidad de las comunicaciones.
Implementar mecanismos de encriptación y autenticación para proteger los datos y autenticar a las partes que se comunican.
Códigos de uso clave:
tlsTransport := tls. Nuevo()
5. Protocolos y manejo de mensajes:
Defina e implemente protocolos personalizados para manejar operaciones de red e intercambios de mensajes específicos.
Procesar los mensajes recibidos y enviar respuestas según sea necesario.
Códigos de uso clave:
Implementar un mecanismo de descubrimiento de nodos para encontrar otros nodos en la red.
Implemente la lógica de enrutamiento para determinar cómo se enrutan los mensajes a los nodos correctos de la red.
Códigos de uso clave:
dht := kaddht. NewDHT(ctx, host, almacén de datos. NewMapDatastore())
7. Comportamiento y políticas de la red:
Defina e implemente comportamientos y políticas de red, como la administración de conexiones, el control de errores y la lógica de reintentos.
Códigos de uso clave:
Administre el estado de los nodos y las redes, incluido el estado de la conexión, la lista de nodos y el almacenamiento de datos.
Códigos de uso clave:
peerstore := pstoremem. NewPeerstore()
9. Prueba y depuración:
Escriba pruebas para su aplicación libp2p para asegurar su corrección y fiabilidad.
Utilice herramientas de depuración y registros para diagnosticar y resolver problemas de red.
Códigos de uso clave:
Registro. SetLogLevel("libp2p", "DEBUG")
10. Documentación y apoyo a la comunidad:
Consulte la documentación de libp2p para obtener información sobre sus diversos componentes y API.
Comunícate con la comunidad libp2p para obtener apoyo y resolución de problemas.
}
Los anteriores son algunos de los principales aspectos a tener en cuenta e implementar al usar libp2p. La implementación específica de cada proyecto puede variar, pero estos aspectos básicos son necesarios para construir y ejecutar aplicaciones libp2p. Al implementar estas funciones, puede consultar la documentación oficial de libp2p[9] [10]y repositorios de GitHub Código de ejemplo y tutoriales.
Uso de libp2p en OP-node
Para averiguar la relación entre op-node y libp2p, tenemos que resolver varias preguntas
¿Por qué libp2p? ¿Por qué no elegir devp2p (geth usa devp2p)?
Qué datos o procesos están estrechamente relacionados con la red P2P en OP-node
Cómo se implementan estas características en la capa de código
¿Por qué los nodos de operación necesitan una red libp2p?
Primero tenemos que entender por qué Optimism requiere una red P2P ** LibbP2P es un protocolo de red modular que permite a los desarrolladores crear aplicaciones descentralizadas peer-to-peer para múltiples casos de uso (fuentes[11] [12](fuente [13])。 DevP2P, por otro lado, se utiliza principalmente en el ecosistema Ethereum y está diseñado para aplicaciones de Ethereum (Fuente )。 La flexibilidad y la amplia aplicabilidad de libp2p pueden convertirlo en la mejor opción para los desarrolladores.
Op-Node utiliza principalmente puntos de función libp2p
Utilizado por el secuenciador para pasar el bloque no seguro resultante a otros nodos que no son del secuenciador
Para otros nodos en modo no secuenciador para una sincronización rápida cuando se produce una brecha (sincronización de cadena inversa)
Un buen entorno para adoptar un sistema de reputación integral que regule el nodo general
Implementación de código
Inicialización personalizada del host
El host se puede entender como un nodo P2P, al abrir este nodo, debe realizar alguna configuración de inicialización especial para su propio proyecto
Ahora echemos un vistazo al método Host en el archivo op-node/p2p/host.go.
Esta función se utiliza principalmente para configurar el host libp2p y realizar varias configuraciones. Estas son las partes clave de la función y una descripción simple en chino de cada una:
Comprueba si P2P está desactivado
Si P2P está deshabilitado, la función regresa directamente.
Obtener ID de par de clave pública
Utilice la clave pública en la configuración para generar el ID del mismo nivel.
Inicializar Peerstore
Cree un almacén de Peerstore base.
Inicializar la extensión Peerstore
Sobre el almacén del mismo nivel base, cree un almacén del mismo nivel extendido.
Agregar claves privadas y públicas a Peerstore
Almacene las claves privadas y públicas del mismo nivel en el almacén del mismo nivel.
Inicializar el acumulador de conexiones
Se utiliza para controlar las conexiones de red.
Inicializar Connection Manager
Se utiliza para administrar conexiones de red.
Establecer dirección de transmisión y escucha
Establezca el protocolo de transporte de red y la dirección de escucha del host.
Crear host libp2p
Utilice todos los ajustes anteriores para crear un nuevo host libp2p.
Inicializar par estático
Si tiene un par estático configurado, inicialícelo.
Volver al anfitrión
Finalmente, la función devuelve el host libp2p creado.
Estas secciones clave son responsables de inicializar y configurar el host libp2p, y cada parte es responsable de un aspecto específico de la configuración del host.
func (conf *Config) Host(log log. Registrador, reportero de métricas. Reportero, métricas HostMetrics) (host. Host, error) {
Si conf. DisableP2P {
return nil, nil
}
pub := conf. Priv.GetPublic()
pid, err := par. IDFromPublicKey(pub)
if err != nil {
Retorno nulo, FMT. Errorf("no se pudo derivar la clave pública de la clave privada de la red: %w", err)
}
basePs, err := pstoreds. NewPeerstore(contexto. Antecedentes(), conf. Tienda, pstoreds. DefaultOpts())
if err != nil {
Retorno nulo, FMT. Errorf("no se pudo abrir el almacén de pares: %w", err)
}
peerScoreParams := conf. PeerScoringParams()
scoreTiempo de retención. Duración
if peerScoreParams != nil {
Use el mismo período de retención que el chisme si está disponible
scoreRetention = peerScoreParams.PeerScoring.RetainScore
} else {
Deshabilite el GC de puntuación si la puntuación del mismo nivel está deshabilitada
scoreRetention = 0
}
ps, err := tienda. NewExtendedPeerstore(context. Background(), registro, reloj. Reloj, basePs, conf. Store, scoreRetention)
if err != nil {
Retorno nulo, FMT. Errorf("no se pudo abrir el almacén de pares extendido: %w", err)
}
if err := ps. AddPrivKey(pid, conf. Priv); err != nil {
Retorno nulo, FMT. Errorf("no se pudo configurar el almacén de pares con la clave priv: %w", err)
}
if err := ps. AddPubKey(pid, pub); err != nil {
Retorno nulo, FMT. Errorf("no se pudo configurar el almacén de pares con la clave pub: %w", err)
}
connGtr gating. BlockingConnectionGater
connGtr, err = compuerta. NewBlockingConnectionGater(conf. Tienda)
if err != nil {
Retorno nulo, FMT. Errorf("no se pudo abrir el gater de conexión: %w", err)
}
connGtr = compuerta. AddBanExpiry(connGtr, ps, log, clock. Reloj, métricas)
connGtr = compuerta. AddMetering(connGtr, métricas)
connMngr, err := DefaultConnManager(conf)
if err != nil {
Retorno nulo, FMT. Errorf("no se pudo abrir el administrador de conexiones: %w", err)
}
listenAddr, err := addrFromIPAndPort(conf. ListenIP, conf. EscucharTCPPort)
if err != nil {
Retorno nulo, FMT. Errorf("error al hacer que la escucha aparezca: %w", err)
}
tcpTransport := libp2p. Transporte(
TCP. NuevoTCPTransport,
TCP. WithConnectionTimeout(tiempo. Minute*60)) // interrumpe las conexiones no utilizadas
TODO: técnicamente también podemos ejecutar el nodo en websocket y transportes QUIC ¿Quizás en el futuro?
nat lconf. NATManagerC // deshabilitado si es nulo
Si conf. NAT {
nat = basichost. NewNATManager
}
opts := []libp2p. Opción{
libp2p. Identidad(conf. Priv),
Establezca explícitamente el agente de usuario, para que podamos diferenciarnos de otros usuarios de Go libp2p.
libp2p. UserAgent(conf. UserAgent),
tcpTransport,
libp2p. WithDialTimeout(conf. TimeoutDial),
Sin servicios de retransmisión, solo conexiones directas entre pares.
libp2p. DisableRelay(),
host iniciará y escuchará la red directamente después de la construcción desde config.
libp2p. ListenAddrs(escucharAddr),
libp2p. ConnectionGater(connGtr),
libp2p. ConnectionManager(connMngr),
libp2p. ResourceManager(nil), // TODO usa la interfaz del administrador de recursos para administrar mejor los recursos por par.
libp2p. Ping(true),
libp2p. AutoNATServiceRateLimit(10, 5, time. Segundo*60),
}
Si conf. NoTransportSecurity {
} else {
opts = append(opts, conf. HostSecurity...)
}
h, err := libp2p. Nuevo(opta...)
if err != nil {
}
para i, peerAddr := range conf. StaticPeers {
if err != nil {
}
staticPeers[i] = addr
}
out := &extraHost{
Anfitrión: h,
staticPeers: staticPeers,
}
go out.monitorStaticPeers()
}
...
n.gs, err = NewGossipSub(resourcesCtx, n.host, rollupCfg, setup, n.scorer, metrics, log)
if err != nil {
}
...
Creación de autenticadores:
Generación de nombres de temas de bloques:
Genere blocksTopicName usando la función blocksTopicV1, que da formato a una cadena basada en el L2ChainID en la configuración (cfg). Las cadenas formateadas siguen una estructura específica: /optimism/{L2ChainID}/0/blocks.
llamando a ps. Join(blocksTopicName) intenta agregar un tema de chismes de bloque. Si se produce un error, devuelve un mensaje de error que indica que no se pudo unir el tema.
Se generó una nueva goroutine para registrar eventos de temas usando la función LogTopicEvents.
Se creó un suscriptor mediante la función MakeSubscriber, que encapsula un BlocksHandler que maneja los eventos OnUnsafeL2Payload de gossipIn. Se generó una nueva goroutine para ejecutar la subión proporcionada.
func JoinGossip(contexto p2pCtx. Contexto, peer.ID propio, ps *pubsub. PubSub, registro de registro. Registrador, cfg *rollup. Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, error) {
blocksTopicName := blocksTopicV1(cfg) // return fmt. Sprintf("/optimismo/%s/0/bloques", cfg. L2ChainID.String())
Retorno nulo, FMT. Errorf("error al registrar bloques tema de chismes: %w", err)
}
blocksTopic, err := ps. Join(blocksTopicName)
if err != nil {
}
if err != nil {
}
ir LogTopicEvents(p2pCtx, log. New("tema", "bloques"), blocksTopicEvents)
if err != nil {
}
return &publisher{log: log, cfg: cfg, blocksTopic: blocksTopic, runCfg: runCfg}, nil
op-node/rollup/driver/state.go
func (s *Driver) eventLoop() {
for(){
...
payload, err := s.sequencer.RunNextSequencerAction(ctx)
if err != nil {
Los errores no son lo suficientemente graves como para cambiar/detener la secuenciación, pero deben registrarse y medirse.
if err := s.network.PublishL2Payload(ctx, payload); err != nil {
}
planSequencerAction() // programa la siguiente acción del secuenciador para mantener el bucle de secuenciación
}
}
...
Cuando faltan bloques, sincronización rápida a través de P2P
Cuando los nodos se vuelven a vincular después del tiempo de inactividad debido a circunstancias especiales, como volver a vincularse después del tiempo de inactividad, es posible que algunos bloques (brechas) no estén sincronizados y, cuando se encuentran con esta situación, pueden sincronizarse rápidamente a través de la cadena inversa de la red P2P.
Echemos un vistazo a la función checkForGapInUnsafeQueue en op-node/rollup/driver/state.go
El fragmento de código define un método denominado checkForGapInUnsafeQueue, que pertenece a la estructura Driver. Su propósito es comprobar si hay brechas de datos en una cola llamada "cola no segura" e intentar recuperar las cargas útiles que faltan a través de un método de sincronización alternativo llamado altSync. El punto clave aquí es que el método es garantizar la continuidad de los datos e intentar recuperar los datos que faltan de otros métodos de sincronización cuando se detectan datos faltantes. Estos son los pasos principales de la función:
La función primero obtiene UnsafeL2Head y UnsafeL2SyncTarget de s.derivation como los puntos inicial y final del rango de comprobación.
La función comprueba si faltan bloques entre el inicio y el final, lo que se hace comparando los valores numéricos del final y el inicio.
Si se detecta una brecha de datos, la función solicitará el intervalo de datos que falta llamando a s.altSync.RequestL2Range(ctx, start, end). Si end es una referencia nula (es decir, eth. L2BlockRef{}), la función solicitará una sincronización de rango abierto, comenzando con start.
Al solicitar datos, la función registra un registro de depuración que indica qué rango de datos está solicitando.
La función finalmente devuelve un valor de error. Si no hay errores, devuelve nil
checkForGapInUnsafeQueue comprueba si hay un hueco en la cola no segura e intenta recuperar las cargas que faltan de un método alt-sync.
ADVERTENCIA: Esta es solo una señal saliente, no se garantiza que se recuperen los bloques.
Los resultados se reciben a través de OnUnsafeL2Payload.
func (s *Driver) checkForGapInUnsafeQueue(contexto ctx. Contexto) error {
start := s.derivation.UnsafeL2Head()
end := s.derivation.UnsafeL2SyncTarget()
Comprobar si nos faltan bloques entre el inicio y el final Solicítalos si los tenemos.
if end == (eth. L2BlockRef{}) {
s.log.Debug("solicitar sincronización con intervalo abierto", "inicio", inicio)
return s.altSync.RequestL2Range(ctx, start, eth. L2BlockRef{})
} else if end. Número > inicio. Número+1 {
s.log.Debug("solicitando el rango de bloques L2 no seguros que faltan", "inicio", inicio, "fin", fin, "tamaño", fin. Número-inicio.Número)
return s.altSync.RequestL2Range(ctx, start, end)
}
return nil
}
La función RequestL2Range señala el inicio y el final del bloque de solicitudes al canal de solicitudes.
A continuación, la solicitud se distribuye al canal peerRequests a través del método onRangeRequest, y el canal peerRequests es esperado por el bucle abierto por varios pares, es decir, solo un par procesará la solicitud para cada distribución.
func (s *SyncClient) onRangeRequest(contexto ctx. Context, req rangeRequest) {
...
para i := uint64(0); ; i++ {
num := req.end.Number - 1 - i
if num <= req.start {
devolución
}
Comprobar si ya tenemos algo en cuarentena
if h, ok := s.quarantineByNum[num] ; ok {
if s.trusted.Contains(h) { // si confiamos en él, intentamos promoverlo.
s.tryPromote(h)
}
No busques cosas para las que ya tenemos un candidato.
Lo desalojaremos de la cuarentena encontrando un conflicto, o si sincronizamos suficientes otros bloques
continuar
}
if _, ok := s.inFlight[num] ; ok {
.log. Debug("solicitud aún en curso, no reprogramando la solicitud de sincronización", "num", num)
continuar // solicitud aún en vuelo
}
pr := peerRequest{num: num, complete: new(atomic. Bool)}
.log. Debug("Programación de la solicitud de bloque P2P", "num", num)
Número de horario
select {
case s.peerRequests <- pr:
s.inFlight[num] = pr.completo
Caso <-CTX. Hecho():
.log. Info("no programó el rango completo de sincronización P2P", "current", num, "err", ctx. err())
devolución
default: // Es posible que todos los pares ya estén ocupados procesando solicitudes
.log. Info("no hay pares listos para manejar solicitudes de bloque para más solicitudes P2P para el historial de bloques L2", "current", num)
devolución
}
}
}
Veamos qué sucede cuando el par recibe esta solicitud.
Lo primero que debemos saber es que el enlace entre el par y el nodo solicitante, o el paso del mensaje, se pasa a través del flujo libp2p. El método de procesamiento de la secuencia lo implementa el nodo del mismo nivel receptor y el nodo emisor abre la creación de la secuencia.
Podemos ver este código en la función de inicio anterior, donde MakeStreamHandler devuelve un controlador y SetStreamHandler vincula el ID de protocolo a este controlador, por lo que cada vez que el nodo de envío crea y usa esta secuencia, se activa el controlador devuelto.
n.syncSrv = NewReqRespServer(rollupCfg, l2Chain, métricas)
Registre el protocolo de sincronización con el host libp2p
payloadByNumber := MakeStreamHandler(resourcesCtx, log. New("serve", "payloads_by_number"), n.syncSrv.HandleSyncRequest)
n.host.SetStreamHandler(PayloadByNumberProtocolID(rollupCfg.L2ChainID), payloadByNumber)
Veamos cómo se maneja dentro del controlador
En primer lugar, la función realiza comprobaciones de límites de velocidad globales e individuales para controlar la rapidez con la que se procesan las solicitudes. A continuación, lee y verifica el número de bloque solicitado, asegurándose de que se encuentra dentro de un rango razonable. A continuación, la función obtiene la carga útil del fragmento de la solicitud de la capa L2 y la escribe en el flujo de respuesta. Al escribir datos de respuesta, establece una fecha límite de escritura para evitar ser bloqueado por conexiones lentas del mismo nivel durante el proceso de escritura. Finalmente, la función devuelve el número de bloque solicitado y los posibles errores.
func (srv *ReqRespServer) handleSyncRequest(contexto ctx. Contexto, red de arroyos. Stream) (uint64, error) {
peerId := flujo. Conn(). RemotePeer()
tomar un token del limitador de velocidad global,
para asegurarse de que no haya demasiado trabajo simultáneo en el servidor entre diferentes pares.
if err := srv.globalRequestsRL.Wait(ctx); err != nil {
Retorno 0, FMT. Errorf("se agotó el tiempo de espera esperando el límite de velocidad de sincronización global: %w", err)
}
Busque datos de limitación de velocidad del par o agregue lo contrario
srv.peerStatsLock.Lock()
ps, _ := srv.peerRateLimits.Get(peerId)
if ps == nil {
ps = &peerStat{
Solicitudes: tarifa. NewLimiter(peerServerBlocksRateLimit, peerServerBlocksBurst),
}
srv.peerRateLimits.Add(peerId, ps)
P.D. Requests.Reserve() // cuenta el acierto, pero haz que retrase la siguiente solicitud en lugar de esperar inmediatamente
} else {
Solo espere si se trata de un par existente, de lo contrario, la llamada de espera de límite de velocidad instantánea siempre genera errores.
Si el solicitante cree que estamos tardando demasiado, entonces es su problema y puede desconectarse.
Nos desconectaremos solo cuando no podamos leer/escribir,
Si el trabajo no es válido (validación de rango) o cuando se agota el tiempo de espera de las subtareas individuales.
if err := ps. Requests.Wait(ctx); err != nil {
Retorno 0, FMT. Errorf("se agotó el tiempo de espera esperando el límite de velocidad de sincronización global: %w", err)
}
}
srv.peerStatsLock.Unlock()
Establezca la fecha límite de lectura, si está disponible
_ = flujo. SetReadDeadline(hora. Ahora(). Add(serverReadRequestTimeout))
Leer la solicitud
req uint64
if err := binario. Read(flujo, binario. LittleEndian, &req); err != nil {
Retorno 0, FMT. Errorf("no se pudo leer el número de bloque solicitado: %w", err)
}
if err := flujo. CloseRead(); err != nil {
Volver req, FMT. Errorf("no se pudo cerrar el lado de lectura de una llamada de solicitud de sincronización P2P: %w", err)
}
Compruebe que la solicitud está dentro del rango esperado de bloques
if req < srv.cfg.Genesis.L2.Number {
Volver req, FMT. Errorf("no se puede atender la solicitud para el bloque L2 %d antes de génesis %d: %w", req, srv.cfg.Genesis.L2.Number, invalidRequestErr)
}
max, err := srv.cfg.TargetBlockNumber(uint64(time. Ahora(). Unix()))
if err != nil {
Volver req, FMT. Errorf("no se puede determinar el número máximo de bloque de destino para verificar la solicitud: %w", invalidRequestErr)
}
if req > max {
Volver req, FMT. Errorf("no se puede atender la solicitud para el bloque L2 %d después del bloque máximo esperado (%v): %w", req, max, invalidRequestErr)
}
payload, err := srv.l2.PayloadByNumber(ctx, req)
if err != nil {
si errores. Is(err, ethereum. NotFound) {
Volver req, FMT. Errorf("bloque desconocido solicitado por el par: %w", err)
} else {
Volver req, FMT. Errorf("no se pudo recuperar la carga útil para servir al par: %w", err)
}
}
Establecemos una fecha límite de escritura, si está disponible, para escribir de forma segura sin bloquear una conexión del mismo nivel que limita
_ = flujo. SetWriteDeadline(hora. Ahora(). Add(serverWriteChunkTimeout))
0 - resultCode: éxito = 0
1:5 - Versión: 0
.tmp [5] byte
if _, err := stream. Escribir(tmp[:]); err != nil {
Volver req, FMT. Errorf("no se pudieron escribir los datos del encabezado de respuesta: %w", err)
}
w := ágil. NewBufferedWriter(flujo)
if _, err := carga útil. MariscalSSZ(w); err != nil {
Volver req, FMT. Errorf("no se pudo escribir la carga útil para sincronizar la respuesta: %w", err)
}
if err := w.Close(); err != nil {
Volver req, FMT. Errorf("no se pudo terminar de escribir la carga útil para sincronizar la respuesta: %w", err)
}
Volver req, cero
}
En este punto, se ha explicado el proceso general de solicitud y procesamiento de sincronización de cadena inversa
Sistema de reputación de puntos en nodos p2p
Con el fin de evitar que ciertos nodos realicen solicitudes y respuestas maliciosas que socaven la seguridad de toda la red, Optimism también utiliza un sistema de puntos.
Por ejemplo, en op-node/p2p/app_scores.go, hay una serie de funciones para establecer la puntuación de un par
func (s *peerApplicationScorer) onValidResponse(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementValidResponses{Cap: s.params.ValidResponseCap})
if err != nil {
s.log.Error("No se puede actualizar la puntuación del par", "peer", id, "err", err)
devolución
}
}
func (s *peerApplicationScorer) onResponseError(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementErrorResponses{Cap: s.params.ErrorResponseCap})
if err != nil {
s.log.Error("No se puede actualizar la puntuación del par", "peer", id, "err", err)
devolución
}
}
func (s *peerApplicationScorer) onRejectedPayload(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementRejectedPayloads{Cap: s.params.RejectedPayloadCap})
if err != nil {
s.log.Error("No se puede actualizar la puntuación del par", "peer", id, "err", err)
devolución
}
}
A continuación, se comprueba el estado de integración del nuevo nodo antes de añadirlo
La alta configurabilidad de libp2p hace que todo el proyecto p2p sea altamente personalizable y modular, lo anterior es la lógica principal de la implementación personalizada de libp2p de optimsim, y otros detalles se pueden aprender en detalle leyendo el código fuente en el directorio p2p.
Ver originales
Esta página puede contener contenido de terceros, que se proporciona únicamente con fines informativos (sin garantías ni declaraciones) y no debe considerarse como un respaldo por parte de Gate a las opiniones expresadas ni como asesoramiento financiero o profesional. Consulte el Descargo de responsabilidad para obtener más detalles.
El uso de libp2p en op-stack
Escrito por Joohhnnn
Aplicaciones Libp2P en Optimism
En esta sección, el propósito principal es explicar cómo Optimism utiliza Libb2P para completar el establecimiento de redes P2P en Op-Node. Las redes P2P se utilizan principalmente para pasar información en diferentes nodos, como secuenciadores después de completar la construcción de bloques de Unsafe, y propagarse a través del pub/sub del gossiphub de P2P. Libb2P también se ocupa de otras infraestructuras como redes, direccionamiento, etc. en redes P2P.
Más información sobre libp2p
libp2p (abreviado de "library peer-to-peer" o "library peer-to-peer") es un marco de red peer-to-peer (P2P) que ayuda a desarrollar aplicaciones P2P. Contiene un conjunto de protocolos, especificaciones y bibliotecas que facilitan la comunicación P2P entre los participantes de la red (también conocidos como "peers" o "peers") (fuente[2] [3])。 Originalmente parte del proyecto IPFS (InterPlanetary File), libp2p evolucionó hasta convertirse en un proyecto independiente que se convirtió en una pila de red modular (fuente) para redes distribuidas )。
libp2p es un proyecto de código abierto de la comunidad IPFS que da la bienvenida a las contribuciones de una amplia gama de comunidades, incluida la ayuda para escribir especificaciones, implementaciones de código y crear ejemplos y tutoriales[4] [5])。 libp2p se compone de varios bloques de construcción, cada uno con una interfaz muy clara, documentada y probada que los hace componibles, reemplazables y, por lo tanto, actualizables )。 La naturaleza modular de libp2p permite a los desarrolladores seleccionar y utilizar solo los componentes necesarios para sus aplicaciones, lo que promueve la flexibilidad y la eficiencia a la hora de crear aplicaciones web P2P.
Recursos relacionados
La arquitectura modular y la naturaleza de código abierto de libp2p proporcionan un buen entorno para desarrollar aplicaciones P2P potentes, escalables y flexibles, lo que lo convierte en un actor importante en el campo de las redes distribuidas y el desarrollo de aplicaciones de red.
Implementación de libp2p
Al usar libp2p, deberá implementar y configurar algunos componentes principales para construir su red P2P. Estos son algunos de los principales aspectos de implementación de libp2p en la aplicación:
1. Creación y configuración de nodos:
libp2p. Nuevo()
2. Protocolo de transporte:
Elija y configure sus protocolos de transporte (por ejemplo, TCP, WebSockets, etc.) para garantizar la comunicación entre nodos. Códigos de uso clave:
tcpTransport := tcp. NewTCPTransport()
3. Multiplexación y control de flujo:
yamuxTransport := yamux. Nuevo()
4. Seguridad y encriptación:
tlsTransport := tls. Nuevo()
5. Protocolos y manejo de mensajes:
Defina e implemente protocolos personalizados para manejar operaciones de red e intercambios de mensajes específicos.
anfitrión. SetStreamHandler("/mi-protocolo/1.0.0", miProtocolHandler)
6. Descubrimiento y enrutamiento:
dht := kaddht. NewDHT(ctx, host, almacén de datos. NewMapDatastore())
7. Comportamiento y políticas de la red:
Defina e implemente comportamientos y políticas de red, como la administración de conexiones, el control de errores y la lógica de reintentos. Códigos de uso clave:
connManager := connmgr. NewConnManager(lowWater, highWater, gracePeriod)
8. Administración de estado y almacenamiento:
Administre el estado de los nodos y las redes, incluido el estado de la conexión, la lista de nodos y el almacenamiento de datos. Códigos de uso clave:
peerstore := pstoremem. NewPeerstore()
9. Prueba y depuración:
Registro. SetLogLevel("libp2p", "DEBUG")
10. Documentación y apoyo a la comunidad:
}
Los anteriores son algunos de los principales aspectos a tener en cuenta e implementar al usar libp2p. La implementación específica de cada proyecto puede variar, pero estos aspectos básicos son necesarios para construir y ejecutar aplicaciones libp2p. Al implementar estas funciones, puede consultar la documentación oficial de libp2p[9] [10]y repositorios de GitHub Código de ejemplo y tutoriales.
Uso de libp2p en OP-node
Para averiguar la relación entre op-node y libp2p, tenemos que resolver varias preguntas
¿Por qué los nodos de operación necesitan una red libp2p?
Primero tenemos que entender por qué Optimism requiere una red P2P ** LibbP2P es un protocolo de red modular que permite a los desarrolladores crear aplicaciones descentralizadas peer-to-peer para múltiples casos de uso (fuentes[11] [12](fuente [13])。 DevP2P, por otro lado, se utiliza principalmente en el ecosistema Ethereum y está diseñado para aplicaciones de Ethereum (Fuente )。 La flexibilidad y la amplia aplicabilidad de libp2p pueden convertirlo en la mejor opción para los desarrolladores.
Op-Node utiliza principalmente puntos de función libp2p
Implementación de código
Inicialización personalizada del host
El host se puede entender como un nodo P2P, al abrir este nodo, debe realizar alguna configuración de inicialización especial para su propio proyecto
Ahora echemos un vistazo al método Host en el archivo op-node/p2p/host.go.
Esta función se utiliza principalmente para configurar el host libp2p y realizar varias configuraciones. Estas son las partes clave de la función y una descripción simple en chino de cada una:
Si P2P está deshabilitado, la función regresa directamente.
Utilice la clave pública en la configuración para generar el ID del mismo nivel.
Cree un almacén de Peerstore base.
Sobre el almacén del mismo nivel base, cree un almacén del mismo nivel extendido.
Almacene las claves privadas y públicas del mismo nivel en el almacén del mismo nivel.
Se utiliza para controlar las conexiones de red.
Se utiliza para administrar conexiones de red.
Establezca el protocolo de transporte de red y la dirección de escucha del host.
Utilice todos los ajustes anteriores para crear un nuevo host libp2p.
Si tiene un par estático configurado, inicialícelo.
Finalmente, la función devuelve el host libp2p creado.
Estas secciones clave son responsables de inicializar y configurar el host libp2p, y cada parte es responsable de un aspecto específico de la configuración del host.
Genere blocksTopicName usando la función blocksTopicV1, que da formato a una cadena basada en el L2ChainID en la configuración (cfg). Las cadenas formateadas siguen una estructura específica: /optimism/{L2ChainID}/0/blocks.
llamando a ps. Join(blocksTopicName) intenta agregar un tema de chismes de bloque. Si se produce un error, devuelve un mensaje de error que indica que no se pudo unir el tema.
Se generó una nueva goroutine para registrar eventos de temas usando la función LogTopicEvents.
Se creó un suscriptor mediante la función MakeSubscriber, que encapsula un BlocksHandler que maneja los eventos OnUnsafeL2Payload de gossipIn. Se generó una nueva goroutine para ejecutar la subión proporcionada.
func JoinGossip(contexto p2pCtx. Contexto, peer.ID propio, ps *pubsub. PubSub, registro de registro. Registrador, cfg *rollup. Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, error) {
blocksTopicName := blocksTopicV1(cfg) // return fmt. Sprintf("/optimismo/%s/0/bloques", cfg. L2ChainID.String())
Retorno nulo, FMT. Errorf("error al registrar bloques tema de chismes: %w", err)
}
blocksTopic, err := ps. Join(blocksTopicName)
if err != nil {
}
if err != nil {
}
ir LogTopicEvents(p2pCtx, log. New("tema", "bloques"), blocksTopicEvents)
if err != nil {
}
return &publisher{log: log, cfg: cfg, blocksTopic: blocksTopic, runCfg: runCfg}, nil
op-node/rollup/driver/state.go
Cuando faltan bloques, sincronización rápida a través de P2P
Cuando los nodos se vuelven a vincular después del tiempo de inactividad debido a circunstancias especiales, como volver a vincularse después del tiempo de inactividad, es posible que algunos bloques (brechas) no estén sincronizados y, cuando se encuentran con esta situación, pueden sincronizarse rápidamente a través de la cadena inversa de la red P2P.
Echemos un vistazo a la función checkForGapInUnsafeQueue en op-node/rollup/driver/state.go
El fragmento de código define un método denominado checkForGapInUnsafeQueue, que pertenece a la estructura Driver. Su propósito es comprobar si hay brechas de datos en una cola llamada "cola no segura" e intentar recuperar las cargas útiles que faltan a través de un método de sincronización alternativo llamado altSync. El punto clave aquí es que el método es garantizar la continuidad de los datos e intentar recuperar los datos que faltan de otros métodos de sincronización cuando se detectan datos faltantes. Estos son los pasos principales de la función:
La función primero obtiene UnsafeL2Head y UnsafeL2SyncTarget de s.derivation como los puntos inicial y final del rango de comprobación.
La función comprueba si faltan bloques entre el inicio y el final, lo que se hace comparando los valores numéricos del final y el inicio.
Si se detecta una brecha de datos, la función solicitará el intervalo de datos que falta llamando a s.altSync.RequestL2Range(ctx, start, end). Si end es una referencia nula (es decir, eth. L2BlockRef{}), la función solicitará una sincronización de rango abierto, comenzando con start.
Al solicitar datos, la función registra un registro de depuración que indica qué rango de datos está solicitando.
La función finalmente devuelve un valor de error. Si no hay errores, devuelve nil
checkForGapInUnsafeQueue comprueba si hay un hueco en la cola no segura e intenta recuperar las cargas que faltan de un método alt-sync.
ADVERTENCIA: Esta es solo una señal saliente, no se garantiza que se recuperen los bloques.
Los resultados se reciben a través de OnUnsafeL2Payload.
func (s *Driver) checkForGapInUnsafeQueue(contexto ctx. Contexto) error {
start := s.derivation.UnsafeL2Head()
end := s.derivation.UnsafeL2SyncTarget()
Comprobar si nos faltan bloques entre el inicio y el final Solicítalos si los tenemos.
if end == (eth. L2BlockRef{}) {
s.log.Debug("solicitar sincronización con intervalo abierto", "inicio", inicio)
return s.altSync.RequestL2Range(ctx, start, eth. L2BlockRef{})
} else if end. Número > inicio. Número+1 {
s.log.Debug("solicitando el rango de bloques L2 no seguros que faltan", "inicio", inicio, "fin", fin, "tamaño", fin. Número-inicio.Número)
return s.altSync.RequestL2Range(ctx, start, end)
}
return nil
}
La función RequestL2Range señala el inicio y el final del bloque de solicitudes al canal de solicitudes.
A continuación, la solicitud se distribuye al canal peerRequests a través del método onRangeRequest, y el canal peerRequests es esperado por el bucle abierto por varios pares, es decir, solo un par procesará la solicitud para cada distribución.
Veamos qué sucede cuando el par recibe esta solicitud.
Lo primero que debemos saber es que el enlace entre el par y el nodo solicitante, o el paso del mensaje, se pasa a través del flujo libp2p. El método de procesamiento de la secuencia lo implementa el nodo del mismo nivel receptor y el nodo emisor abre la creación de la secuencia.
Podemos ver este código en la función de inicio anterior, donde MakeStreamHandler devuelve un controlador y SetStreamHandler vincula el ID de protocolo a este controlador, por lo que cada vez que el nodo de envío crea y usa esta secuencia, se activa el controlador devuelto.
Veamos cómo se maneja dentro del controlador En primer lugar, la función realiza comprobaciones de límites de velocidad globales e individuales para controlar la rapidez con la que se procesan las solicitudes. A continuación, lee y verifica el número de bloque solicitado, asegurándose de que se encuentra dentro de un rango razonable. A continuación, la función obtiene la carga útil del fragmento de la solicitud de la capa L2 y la escribe en el flujo de respuesta. Al escribir datos de respuesta, establece una fecha límite de escritura para evitar ser bloqueado por conexiones lentas del mismo nivel durante el proceso de escritura. Finalmente, la función devuelve el número de bloque solicitado y los posibles errores.
En este punto, se ha explicado el proceso general de solicitud y procesamiento de sincronización de cadena inversa
Sistema de reputación de puntos en nodos p2p
Con el fin de evitar que ciertos nodos realicen solicitudes y respuestas maliciosas que socaven la seguridad de toda la red, Optimism también utiliza un sistema de puntos.
Por ejemplo, en op-node/p2p/app_scores.go, hay una serie de funciones para establecer la puntuación de un par
A continuación, se comprueba el estado de integración del nuevo nodo antes de añadirlo
Resumen
La alta configurabilidad de libp2p hace que todo el proyecto p2p sea altamente personalizable y modular, lo anterior es la lógica principal de la implementación personalizada de libp2p de optimsim, y otros detalles se pueden aprender en detalle leyendo el código fuente en el directorio p2p.