Nesta seção, o principal objetivo é explicar como a Optimism usa o Libb2P para concluir o estabelecimento de redes P2P em Op-Node.
As redes P2P são usadas principalmente para passar informações em diferentes nós, como sequenciadores depois de completar a construção do bloco de Unsafe, e propagando-se através do pub/sub do gossiphub do P2P. Libb2P também lida com outras infraestruturas, como redes, endereçamento, etc. em redes P2P.
Saiba mais sobre libp2p
libp2p (abreviado de "biblioteca peer-to-peer" ou "biblioteca peer-to-peer") é uma estrutura de rede peer-to-peer (P2P) que ajuda a desenvolver aplicativos P2P. Ele contém um conjunto de protocolos, especificações e bibliotecas que facilitam a comunicação P2P entre os participantes da rede (também conhecidos como "pares" ou "pares") (fonte[2] [3])。 Originalmente parte do projeto IPFS (InterPlanetary File), o libp2p evoluiu para um projeto independente que se tornou uma pilha de rede modular (fonte) para redes distribuídas )。
libp2p é um projeto de código aberto da comunidade IPFS que acolhe contribuições de uma ampla gama de comunidades, incluindo ajudar a escrever especificações, implementações de código e criar exemplos e tutoriais[4] [5])。 libp2p é composto de vários blocos de construção, cada um com uma interface muito clara, documentada e testada que os torna composable, substituíveis e, portanto, atualizáveis )。 A natureza modular da libp2p permite que os desenvolvedores selecionem e usem apenas os componentes necessários para suas aplicações, promovendo flexibilidade e eficiência ao construir aplicações web P2P.
Recursos relacionados
Documentação oficial para libp2p[6]
repositório GitHub da libp2p[7]
Introdução ao libp2p na ProtoSchool[8]
A arquitetura modular e a natureza de código aberto da libp2p fornecem um bom ambiente para o desenvolvimento de aplicações P2P poderosas, escaláveis e flexíveis, tornando-a um ator importante no campo das redes distribuídas e do desenvolvimento de aplicações de rede.
Implementação libp2p
Ao usar libp2p, você precisará implementar e configurar alguns componentes principais para construir sua rede P2P. Aqui estão alguns dos principais aspetos de implementação da libp2p na aplicação:
1. Criação e configuração de nós:
Criar e configurar um nó libp2p é a etapa mais básica, que inclui a definição do endereço de rede, identidade e outros parâmetros básicos do nó.
Códigos de utilização das chaves:
libp2p. Novo()
2. Protocolo de Transporte:
Escolha e configure seus protocolos de transporte (por exemplo, TCP, WebSockets, etc.) para garantir a comunicação entre nós.
Códigos de utilização das chaves:
tcpTransport := tcp. NewTCPTransport()
3. Multiplexação e Controlo de Fluxo:
Implementar a multiplexação para permitir que vários fluxos de dados simultâneos sejam processados em uma única conexão.
Implementar controle de fluxo para gerenciar a taxa de transmissão de dados e taxa de processamento.
Códigos de utilização das chaves:
yamuxTransport := yamux. Novo()
4. Segurança e encriptação:
Configurar uma camada de transporte segura para garantir a segurança e privacidade das comunicações.
Implementar mecanismos de encriptação e autenticação para proteger os dados e autenticar as partes comunicantes.
Códigos de utilização das chaves:
tlsTransport := tls. Novo()
5. Protocolos e Tratamento de Mensagens:
Definir e implementar protocolos personalizados para lidar com operações de rede específicas e trocas de mensagens.
Processar mensagens recebidas e enviar respostas conforme necessário.
Códigos de utilização das chaves:
Implementar um mecanismo de descoberta de nós para encontrar outros nós na rede.
Implementar lógica de roteamento para determinar como as mensagens são roteadas para os nós corretos na rede.
Códigos de utilização das chaves:
dht := kaddht. NewDHT(ctx, host, armazenamento de dados. NewMapDatastore())
7. Comportamento e políticas de rede:
Defina e implemente comportamentos e políticas de rede, como gerenciamento de conexões, tratamento de erros e lógica de novas tentativas.
Códigos de utilização das chaves:
Consulte a documentação do libp2p para saber mais sobre seus vários componentes e APIs.
Comunicar com a comunidade libp2p para apoio e resolução de problemas.
}
O acima são alguns dos principais aspetos a considerar e implementar ao usar libp2p. A implementação específica de cada projeto pode variar, mas esses aspetos básicos são necessários para construir e executar aplicativos libp2p. Ao implementar essas funções, você pode consultar a documentação oficial do libp2p[9] [10]e repositórios GitHub Código de exemplo e tutoriais.
Uso de libp2p no nó OP
A fim de descobrir a relação entre op-node e libp2p, temos que descobrir várias perguntas
Porquê libp2p? Por que não escolher devp2p (geth usa devp2p)
Que dados ou processos estão intimamente relacionados com a rede P2P no nó OP
Como esses recursos são implementados na camada de código
Por que os nós operacionais precisam de uma rede libp2p
Primeiro precisamos entender por que o Optimism requer uma rede P2P ** LibbP2P é um protocolo de rede modular que permite aos desenvolvedores construir aplicativos peer-to-peer descentralizados para vários casos de uso (fontes[11] [12])(fonte [13])。 O DevP2P, por outro lado, é usado principalmente no ecossistema Ethereum e é adaptado para aplicações Ethereum (Fonte )。 A flexibilidade e a ampla aplicabilidade do libp2p podem torná-lo uma excelente escolha para desenvolvedores.
Op-Node usa principalmente pontos de função libp2p
Usado pelo sequenciador para passar o bloco inseguro resultante para outros nós não sequenciadores
Para outros nós no modo não-sequenciador para sincronização rápida quando ocorre uma lacuna (sincronização de cadeia reversa)
Um bom ambiente para a adoção de um sistema de reputação integral para regular o nó geral
Implementação de código
inicialização personalizada do host
Host pode ser entendido como um nó P2P, ao abrir este nó, você precisa fazer alguma configuração de inicialização especial para o seu próprio projeto
Agora vamos examinar o método Host no arquivo op-node/p2p/host.go.
Esta função é usada principalmente para configurar o host libp2p e fazer várias configurações. Aqui estão as partes-chave da função e uma descrição chinesa simples de cada uma:
Verifique se o P2P está desativado
Se P2P estiver desativado, a função retornará diretamente.
Obter ID de Peer da Chave Pública
Use a chave pública na configuração para gerar o ID de mesmo nível.
Inicializar Peerstore
Crie um repositório Peerstore base.
Inicialize a extensão Peerstore
Além do Peerstore base, crie um Peerstore estendido.
Adicione chaves privadas e públicas ao Peerstore
Armazene as chaves pública e privada do peer no peerstore.
Inicializar Gater de Conexão
Usado para controlar conexões de rede.
Inicializar o Gerenciador de Conexões
Usado para gerenciar conexões de rede.
Definir endereço de transmissão e audição
Defina o protocolo de transporte de rede e o endereço de escuta do host.
Criar host libp2p
Use todas as configurações anteriores para criar um novo host libp2p.
Inicializar par estático
Se você tiver um par estático configurado, inicialize-o.
Voltar ao Anfitrião
Finalmente, a função retorna o host libp2p criado.
Essas seções principais são responsáveis por inicializar e configurar o host libp2p, e cada parte é responsável por um aspeto específico da configuração do host.
func (conf *Config) Host (log log. Logger, métricas do repórter. Reporter, métricas HostMetrics) (host. Host, erro) {
se conf. DisableP2P {
retorno nulo, nulo
}
pub := conf. Priv.GetPublic()
pid, err := peer. IDFromPublicKey(pub)
if err != nil {
retorno nulo, fmt. Errorf("falha ao derivar pubkey da chave priv de rede: %w", err)
}
basePs, err := pstoreds. NewPeerstore(contexto. Antecedentes(), conf. Loja, pstoreds. DefaultOpts())
if err != nil {
retorno nulo, fmt. Errorf("falha ao abrir peerstore: %w", err)
}
peerScoreParams := conf. PeerScoringParams()
scoreTempo de retenção. Duração
if peerScoreParams != nil {
Use o mesmo período de retenção que a fofoca, se disponível
scoreRetention = peerScoreParams.PeerScoring.RetainScore
} else {
Desativar a pontuação GC se a pontuação por pares estiver desativada
scoreRetention = 0
}
ps, err := loja. NewExtendedPeerstore(contexto. Background(), log, relógio. Relógio, basePs, conf. Loja, scoreRetention)
if err != nil {
retorno nulo, fmt. Errorf("falha ao abrir peerstore estendido: %w", erro)
}
se errar := ps. AddPrivKey(pid, conf. Priv); err != nil {
retorno nulo, fmt. Errorf("falha ao configurar peerstore com chave priv: %w", err)
}
se errar := ps. AddPubKey (pid, pub); err != nil {
retorno nulo, fmt. Errorf("falha ao configurar peerstore com chave pub: %w", err)
}
nGtr datação. BlockingConnectionGater
connGtr, err = datação. NewBlockingConnectionGater(conf. Loja)
if err != nil {
retorno nulo, fmt. Errorf("falha ao abrir o gater de conexão: %w", erro)
}
connGtr = datação. AddBanExpiry(connGtr, ps, log, relógio. Relógio, métricas)
connGtr = datação. AddMetering(connGtr, métricas)
connMngr, err := DefaultConnManager(conf)
if err != nil {
retorno nulo, fmt. Errorf("falha ao abrir o gerenciador de conexões: %w", erro)
}
listenAddr, err := addrFromIPAndPort(conf. ListenIP, conf. ListenTCPPort)
if err != nil {
retorno nulo, fmt. Errorf("falha ao fazer ouvir addr: %w", err)
}
tcpTransport := libp2p. Transportes(
TCP. NewTCPTransport,
TCP. WithConnectionTimeout(tempo. Minuto*60)) // quebrar ligações não utilizadas
TODO: tecnicamente também podemos executar o nó em transportes websocket e QUIC Talvez no futuro?
Nat Lconf. NATManagerC // desativado se nulo
se conf. NAT {
nat = basichost. NewNATManager
}
opts := []libp2p. Opção{
libp2p. Identidade(conf. Priv),
Defina explicitamente o user-agent, para que possamos diferenciar de outros usuários do Go libp2p.
libp2p. UserAgent(conf. UserAgent),
tcpTransport,
libp2p. WithDialTimeout(conf. TimeoutDial),
Sem serviços de retransmissão, apenas conexões diretas entre pares.
libp2p. DisableRelay(),
O host iniciará e ouvirá a rede diretamente após a construção a partir da configuração.
libp2p. ListenAddrs(listenAddr),
libp2p. ConnectionGater (connGtr),
libp2p. ConnectionManager(connMngr),
libp2p. ResourceManager(nil), // TODO usa a interface do gerenciador de recursos para gerenciar melhor os recursos por par.
libp2p. Ping(verdadeiro),
libp2p. AutoNATServiceRateLimit(10, 5, tempo. Segundo*60),
}
se conf. NoTransportSecurity {
} else {
opts = apêndice(opts, conf. HostSecurity...)
}
h, err := libp2p. Novo(opts...)
if err != nil {
}
para i, peerAddr := range conf. StaticPeers {
if err != nil {
}
staticPeers[i] = addr
}
out := &extraHost{
Anfitrião: h,
staticPeers: staticPeers,
}
sair.monitorStaticPeers()
}
...
n.gs, err = NewGossipSub(resourcesCtx, n.host, rollupCfg, setup, n.scorer, metrics, log)
if err != nil {
}
...
Criação do autenticador:
Geração de nomes de temas de bloco:
Gere blocksTopicName usando a função blocksTopicV1, que formata uma string com base no L2ChainID na configuração (cfg). As cadeias de caracteres formatadas seguem uma estrutura específica: /optimism/{L2ChainID}/0/blocks.
função chamando ps. Join(blocksTopicName) tenta adicionar um tema de fofoca de bloco. Se ocorrer um erro, ele retornará uma mensagem de erro indicando que o tópico não pôde ser associado.
Gerou uma nova rotina para registrar eventos de tópicos usando a função LogTopicEvents.
Criou um assinante usando a função MakeSubscriber, que encapsula um BlocksHandler que lida com eventos OnUnsafeL2Payload de gossipIn. Uma nova rotina foi gerada para executar o subião fornecido.
func (s *Driver) eventLoop() {
for(){
...
carga útil, err := s.sequencer.RunNextSequencerAction(ctx)
if err != nil {
Os erros não são graves o suficiente para alterar/interromper o sequenciamento, mas devem ser registrados e medidos.
if err := s.network.PublishL2Payload(ctx, payload); err != nil {
}
planSequencerAction() // agendar a próxima ação do sequenciador para manter o looping de sequenciamento
}
}
...
Quando há blocos ausentes, sincronização rápida via P2P
Quando os nós são religados após o tempo de inatividade devido a circunstâncias especiais, como a religação após o tempo de inatividade, alguns blocos (lacunas) podem não estar sincronizados e, ao encontrar esta situação, podem sincronizar rapidamente através da cadeia inversa da rede P2P.
Vamos dar uma olhada na função checkForGapInUnsafeQueue em op-node/rollup/driver/state.go
O trecho de código define um método chamado checkForGapInUnsafeQueue, que pertence à estrutura Driver. Seu objetivo é verificar lacunas de dados em uma fila chamada "fila insegura" e tentar recuperar cargas úteis ausentes por meio de um método de sincronização alternativo chamado altSync. O ponto-chave aqui é que o método é garantir a continuidade dos dados e tentar recuperar dados ausentes de outros métodos de sincronização quando dados ausentes são detetados. Aqui estão os principais passos da função:
A função primeiro obtém UnsafeL2Head e UnsafeL2SyncTarget de s.derivation como os pontos de início e fim do intervalo de verificação.
A função verifica se há blocos ausentes entre o início e o fim, o que é feito comparando os valores numéricos de fim e início.
Se uma lacuna de dados for detetada, a função solicitará o intervalo de dados ausente chamando s.altSync.RequestL2Range(ctx, start, end). Se end é uma referência nula (ou seja, eth. L2BlockRef{}), a função solicitará uma sincronização de intervalo aberto, começando com start.
Ao solicitar dados, a função registra um log de depuração indicando qual intervalo de dados está solicitando.
A função eventualmente retorna um valor de erro. Se não houver erros, ele retorna zero
checkForGapInUnsafeQueue verifica se há uma lacuna na fila insegura e tenta recuperar as cargas úteis ausentes de um método alt-sync.
AVISO: Este é apenas um sinal de saída, os blocos não têm garantia de serem recuperados.
Os resultados são recebidos através de OnUnsafeL2Payload.
func (s *Driver) checkForGapInUnsafeQueue(contexto ctx. Contexto) erro {
início := s.derivation.UnsafeL2Head()
fim := s.derivation.UnsafeL2SyncTarget()
Verifique se faltam blocos entre o início e o fim Solicite-os se o fizermos.
se fim == (eth. L2BlockRef{}) {
s.log.Debug("solicitando sincronização com intervalo aberto", "start", start)
return s.altSync.RequestL2Range(ctx, start, eth. L2BlockRef{})
} senão se terminar. Número > início. Número+1 {
s.log.Debug("solicitando intervalo de blocos L2 inseguros ausentes", "start", start, "end", end, "size", end. Número-início.Número)
retornar s.altSync.RequestL2Range (ctx, início, fim)
}
retorno nulo
}
A função RequestL2Range sinaliza o início e o fim do bloco de solicitações para o canal de solicitações.
A solicitação é então distribuída para o canal peerRequests através do método onRangeRequest, e o canal peerRequests é aguardado pelo loop aberto por vários pares, ou seja, apenas um par processará a solicitação para cada distribuição.
func (s *SyncClient) onRangeRequest(contexto ctx. Contexto, req rangeRequest) {
...
para i := uint64(0); ; i++ {
num := req.end.Number - 1 - i
if num <= req.start {
regresso
}
verificar se já temos algo em quarentena
se h, ok := s.quarantineByNum[num] ; ok {
if s.trusted.Contains(h) { // se confiarmos nele, tente promovê-lo.
s.tryPromote(h)
}
Não busquem coisas para as quais já temos candidato.
Vamos eliminá-lo da quarentena encontrando um conflito ou se sincronizarmos outros blocos suficientes
continuar
}
if _, ok := s.inFlight[num] ; ok {
.log. Debug("solicitação ainda em andamento, não reagendando solicitação de sincronização", "num", num)
continuar // solicitar ainda em voo
}
pr := peerRequest{num: num, complete: new(atomic. Bool)}
.log. Debug("Agendando solicitação de bloco P2P", "num", num)
número da agenda
selecione {
caso s.peerRequests <- pr:
s.inVoo[num] = pr.completo
Caso <-CTX. Feito():
.log. Info("não programou intervalo de sincronização P2P completo", "current", num, "err", ctx. Err())
regresso
padrão: // os pares podem estar todos ocupados processando solicitações já
.log. Info("sem pares prontos para lidar com solicitações de bloco para mais solicitações P2P para histórico de blocos L2", "atual", num)
regresso
}
}
}
Vamos ver o que acontece quando o par recebe essa solicitação.
A primeira coisa que precisamos saber é que o link entre o peer e o nó solicitante, ou a mensagem que passa, é passada através do fluxo libp2p. O método de processamento do fluxo é implementado pelo nó de mesmo nível de recebimento e a criação do fluxo é aberta pelo nó de envio.
Podemos ver esse código na função init anterior, onde MakeStreamHandler retorna um manipulador e SetStreamHandler vincula a ID do protocolo a esse manipulador, portanto, sempre que o nó de envio cria e usa esse fluxo, o manipulador retornado é acionado.
n.syncSrv = NewReqRespServer(rollupCfg, l2Chain, métricas)
Registrar o protocolo de sincronização com o host libp2p
payloadByNumber := MakeStreamHandler(resourcesCtx, log. New("serve", "payloads_by_number"), n.syncSrv.HandleSyncRequest)
n.host.SetStreamHandler(PayloadByNumberProtocolID(rollupCfg.L2ChainID), payloadByNumber)
Vamos ver como ele é tratado dentro do manipulador
A função primeiro executa verificações de limite de taxa global e individual para controlar a rapidez com que as solicitações são processadas. Em seguida, lê e verifica o número de bloco solicitado, garantindo que está dentro de um intervalo razoável. Em seguida, a função obtém a carga útil da solicitação da camada L2 e a grava no fluxo de resposta. Ao gravar dados de resposta, ele define um prazo de gravação para evitar ser bloqueado por conexões de pares lentas durante o processo de gravação. Finalmente, a função retorna o número de bloco solicitado e possíveis erros.
func (srv *ReqRespServer) handleSyncRequest(contexto ctx. Contexto, rede de fluxo. Stream) (uint64, erro) {
peerId := fluxo. Conn(). RemotePeer()
pegue um token do limitador de taxa global,
para garantir que não haja muito trabalho de servidor simultâneo entre diferentes pares.
se err := srv.globalRequestsRL.Wait(ctx); err != nil {
retorno 0, fmt. Errorf("timed out waiting for global sync rate limit: %w", err)
}
encontrar dados de limitação de taxa de peer, ou adicionar de outra forma
srv.peerStatsLock.Lock()
ps, _ := srv.peerRateLimits.Get(peerId)
if ps == nil {
ps = &peerStat{
Pedidos: taxa. NewLimiter(peerServerBlocksRateLimit, peerServerBlocksBurst),
}
srv.peerRateLimits.Add(peerId, ps)
ps. Requests.Reserve() // contar o acerto, mas fazê-lo atrasar a próxima solicitação em vez de esperar imediatamente
} else {
Aguarde apenas se for um par existente, caso contrário, a chamada de espera de limite de taxa instantânea sempre erros.
Se o solicitante acha que estamos demorando muito, então o problema é dele e ele pode se desconectar.
Desligamo-nos apenas quando não conseguimos ler/escrever,
se o trabalho for inválido (validação de intervalo) ou quando as subtarefas individuais expirarem.
se errar := ps. Requests.Wait(ctx); err != nil {
retorno 0, fmt. Errorf("timed out waiting for global sync rate limit: %w", err)
}
}
srv.peerStatsLock.Unlock()
Definir prazo de leitura, se disponível
_ = fluxo. SetReadDeadline(tempo. Agora(). Add(serverReadRequestTimeout))
Leia o pedido
req uint64
if err := binário. Leitura(fluxo, binário. LittleEndian, &req); err != nil {
retorno 0, fmt. Errorf("falha ao ler o número do bloco solicitado: %w", erro)
}
se errar := fluxo. CloseRead(); err != nil {
retorno req, fmt. Errorf("falha ao fechar o lado de leitura de uma chamada de solicitação de sincronização P2P: %w", err)
}
Verifique se a solicitação está dentro do intervalo esperado de blocos
if req < srv.cfg.Genesis.L2.Number {
retorno req, fmt. Errorf("cannot serve request for L2 block %d before genesis %d: %w", req, srv.cfg.Genesis.L2.Number, invalidRequestErr)
}
max, err := srv.cfg.TargetBlockNumber(uint64(time. Agora(). Unix()))
if err != nil {
retorno req, fmt. Errorf("não é possível determinar o número máximo do bloco de destino para verificar a solicitação: %w", invalidRequestErr)
}
if req > max {
retorno req, fmt. Errorf("cannot serve request for L2 block %d after max expected block (%v): %w", req, max, invalidRequestErr)
}
carga útil, err := srv.l2.PayloadByNumber(ctx, req)
if err != nil {
se erros. É(err, ethereum. NotFound) {
retorno req, fmt. Errorf("peer solicitado bloco desconhecido por número: %w", err)
} else {
retorno req, fmt. Errorf("falha ao recuperar a carga útil para servir ao ponto: %w", err)
}
}
Definimos o prazo de gravação, se disponível, para escrever com segurança sem bloquear uma conexão de peer de limitação
_ = fluxo. SetWriteDeadline(tempo. Agora(). Add(serverWriteChunkTimeout))
0 - resultCode: sucesso = 0
1:5 - Versão: 0
.tmp [5] byte
if _, err := fluxo. Escrever(tmp[:]); err != nil {
retorno req, fmt. Errorf("falha ao gravar dados do cabeçalho de resposta: %w", erro)
}
w := rápido. NewBufferedWriter(fluxo)
if _, err := carga útil. Marechal SSZ(w); err != nil {
retorno req, fmt. Errorf("falha ao gravar a carga útil para sincronizar a resposta: %w", err)
}
se errar := w.Close(); err != nil {
retorno req, fmt. Errorf("falha ao terminar a gravação da carga útil para sincronizar a resposta: %w", err)
}
retorno req, nil
}
Neste ponto, o processo geral de solicitação e processamento de sincronização de cadeia reversa foi explicado
Sistema de reputação de pontos em nós p2p
A fim de evitar que certos nós façam solicitações maliciosas e respostas que minam a segurança de toda a rede, o Optimism também usa um sistema de pontos.
Por exemplo, em op-node/p2p/app_scores.go, há uma série de funções para definir a pontuação de um 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("Não é possível atualizar a pontuação de pares", "peer", id, "err", err)
regresso
}
}
func (s *peerApplicationScorer) onResponseError(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementErrorResponses{Cap: s.params.ErrorResponseCap})
if err != nil {
s.log.Error("Não é possível atualizar a pontuação de pares", "peer", id, "err", err)
regresso
}
}
func (s *peerApplicationScorer) onRejectedPayload(id peer.ID) {
_, err := s.scorebook.SetScore(id, store. IncrementRejectedPayloads{Cap: s.params.RejectedPayloadCap})
if err != nil {
s.log.Error("Não é possível atualizar a pontuação de pares", "peer", id, "err", err)
regresso
}
}
O status de integração do novo nó é verificado antes de ser adicionado
A alta configurabilidade do libp2p torna todo o projeto p2p altamente personalizável e modular, o acima é a lógica principal da implementação personalizada do libp2p da optimsim, e outros detalhes podem ser aprendidos em detalhes lendo o código-fonte no diretório p2p.
Ver original
Esta página pode conter conteúdos de terceiros, que são fornecidos apenas para fins informativos (sem representações/garantias) e não devem ser considerados como uma aprovação dos seus pontos de vista pela Gate, nem como aconselhamento financeiro ou profissional. Consulte a Declaração de exoneração de responsabilidade para obter mais informações.
O uso de libp2p em op-stack
Escrito por Joohhnnn
Aplicações Libp2P em Otimismo
Nesta seção, o principal objetivo é explicar como a Optimism usa o Libb2P para concluir o estabelecimento de redes P2P em Op-Node. As redes P2P são usadas principalmente para passar informações em diferentes nós, como sequenciadores depois de completar a construção do bloco de Unsafe, e propagando-se através do pub/sub do gossiphub do P2P. Libb2P também lida com outras infraestruturas, como redes, endereçamento, etc. em redes P2P.
Saiba mais sobre libp2p
libp2p (abreviado de "biblioteca peer-to-peer" ou "biblioteca peer-to-peer") é uma estrutura de rede peer-to-peer (P2P) que ajuda a desenvolver aplicativos P2P. Ele contém um conjunto de protocolos, especificações e bibliotecas que facilitam a comunicação P2P entre os participantes da rede (também conhecidos como "pares" ou "pares") (fonte[2] [3])。 Originalmente parte do projeto IPFS (InterPlanetary File), o libp2p evoluiu para um projeto independente que se tornou uma pilha de rede modular (fonte) para redes distribuídas )。
libp2p é um projeto de código aberto da comunidade IPFS que acolhe contribuições de uma ampla gama de comunidades, incluindo ajudar a escrever especificações, implementações de código e criar exemplos e tutoriais[4] [5])。 libp2p é composto de vários blocos de construção, cada um com uma interface muito clara, documentada e testada que os torna composable, substituíveis e, portanto, atualizáveis )。 A natureza modular da libp2p permite que os desenvolvedores selecionem e usem apenas os componentes necessários para suas aplicações, promovendo flexibilidade e eficiência ao construir aplicações web P2P.
Recursos relacionados
A arquitetura modular e a natureza de código aberto da libp2p fornecem um bom ambiente para o desenvolvimento de aplicações P2P poderosas, escaláveis e flexíveis, tornando-a um ator importante no campo das redes distribuídas e do desenvolvimento de aplicações de rede.
Implementação libp2p
Ao usar libp2p, você precisará implementar e configurar alguns componentes principais para construir sua rede P2P. Aqui estão alguns dos principais aspetos de implementação da libp2p na aplicação:
1. Criação e configuração de nós:
libp2p. Novo()
2. Protocolo de Transporte:
Escolha e configure seus protocolos de transporte (por exemplo, TCP, WebSockets, etc.) para garantir a comunicação entre nós. Códigos de utilização das chaves:
tcpTransport := tcp. NewTCPTransport()
3. Multiplexação e Controlo de Fluxo:
yamuxTransport := yamux. Novo()
4. Segurança e encriptação:
tlsTransport := tls. Novo()
5. Protocolos e Tratamento de Mensagens:
Definir e implementar protocolos personalizados para lidar com operações de rede específicas e trocas de mensagens.
anfitrião. SetStreamHandler("/my-protocol/1.0.0", myProtocolHandler)
6. Descoberta e roteamento:
dht := kaddht. NewDHT(ctx, host, armazenamento de dados. NewMapDatastore())
7. Comportamento e políticas de rede:
Defina e implemente comportamentos e políticas de rede, como gerenciamento de conexões, tratamento de erros e lógica de novas tentativas. Códigos de utilização das chaves:
connManager := connmgr. NewConnManager (lowWater, highWater, gracePeriod)
8. Gestão e Armazenamento do Estado:
Gerencie o status de nós e redes, incluindo status de conexão, lista de nós e armazenamento de dados. Códigos de utilização das chaves:
peerstore := pstoremem. NewPeerstore()
9. Teste e depuração:
exploração madeireira. SetLogLevel("libp2p", "DEPURAR")
10. Documentação e Suporte da Comunidade:
}
O acima são alguns dos principais aspetos a considerar e implementar ao usar libp2p. A implementação específica de cada projeto pode variar, mas esses aspetos básicos são necessários para construir e executar aplicativos libp2p. Ao implementar essas funções, você pode consultar a documentação oficial do libp2p[9] [10]e repositórios GitHub Código de exemplo e tutoriais.
Uso de libp2p no nó OP
A fim de descobrir a relação entre op-node e libp2p, temos que descobrir várias perguntas
Por que os nós operacionais precisam de uma rede libp2p
Primeiro precisamos entender por que o Optimism requer uma rede P2P ** LibbP2P é um protocolo de rede modular que permite aos desenvolvedores construir aplicativos peer-to-peer descentralizados para vários casos de uso (fontes[11] [12])(fonte [13])。 O DevP2P, por outro lado, é usado principalmente no ecossistema Ethereum e é adaptado para aplicações Ethereum (Fonte )。 A flexibilidade e a ampla aplicabilidade do libp2p podem torná-lo uma excelente escolha para desenvolvedores.
Op-Node usa principalmente pontos de função libp2p
Implementação de código
inicialização personalizada do host
Host pode ser entendido como um nó P2P, ao abrir este nó, você precisa fazer alguma configuração de inicialização especial para o seu próprio projeto
Agora vamos examinar o método Host no arquivo op-node/p2p/host.go.
Esta função é usada principalmente para configurar o host libp2p e fazer várias configurações. Aqui estão as partes-chave da função e uma descrição chinesa simples de cada uma:
Se P2P estiver desativado, a função retornará diretamente.
Use a chave pública na configuração para gerar o ID de mesmo nível.
Crie um repositório Peerstore base.
Além do Peerstore base, crie um Peerstore estendido.
Armazene as chaves pública e privada do peer no peerstore.
Usado para controlar conexões de rede.
Usado para gerenciar conexões de rede.
Defina o protocolo de transporte de rede e o endereço de escuta do host.
Use todas as configurações anteriores para criar um novo host libp2p.
Se você tiver um par estático configurado, inicialize-o.
Finalmente, a função retorna o host libp2p criado.
Essas seções principais são responsáveis por inicializar e configurar o host libp2p, e cada parte é responsável por um aspeto específico da configuração do host.
Gere blocksTopicName usando a função blocksTopicV1, que formata uma string com base no L2ChainID na configuração (cfg). As cadeias de caracteres formatadas seguem uma estrutura específica: /optimism/{L2ChainID}/0/blocks.
função chamando ps. Join(blocksTopicName) tenta adicionar um tema de fofoca de bloco. Se ocorrer um erro, ele retornará uma mensagem de erro indicando que o tópico não pôde ser associado.
Gerou uma nova rotina para registrar eventos de tópicos usando a função LogTopicEvents.
Criou um assinante usando a função MakeSubscriber, que encapsula um BlocksHandler que lida com eventos OnUnsafeL2Payload de gossipIn. Uma nova rotina foi gerada para executar o subião fornecido.
func JoinGossip(p2pCtx contexto. Contexto, auto peer.ID, ps *pubsub. PubSub, log de log. Logger, cfg *rollup. Config, runCfg GossipRuntimeConfig, gossipIn GossipIn) (GossipOut, erro) {
blocksTopicName := blocksTopicV1(cfg) // retorno fmt. Sprintf("/optimism/%s/0/blocks", cfg. L2ChainID.String())
retorno nulo, fmt. Errorf("falha ao registrar bloqueia tópico de fofoca: %w", err)
}
blocksTopic, err := ps. Join(blocksTopicName)
if err != nil {
}
if err != nil {
}
vá LogTopicEvents(p2pCtx, log. New("tópico", "blocos"), blocksTopicEvents)
if err != nil {
}
return &publisher{log: log, cfg: cfg, blocksTopic: blocksTopic, runCfg: runCfg}, nil
op-node/rollup/driver/state.go
Quando há blocos ausentes, sincronização rápida via P2P
Quando os nós são religados após o tempo de inatividade devido a circunstâncias especiais, como a religação após o tempo de inatividade, alguns blocos (lacunas) podem não estar sincronizados e, ao encontrar esta situação, podem sincronizar rapidamente através da cadeia inversa da rede P2P.
Vamos dar uma olhada na função checkForGapInUnsafeQueue em op-node/rollup/driver/state.go
O trecho de código define um método chamado checkForGapInUnsafeQueue, que pertence à estrutura Driver. Seu objetivo é verificar lacunas de dados em uma fila chamada "fila insegura" e tentar recuperar cargas úteis ausentes por meio de um método de sincronização alternativo chamado altSync. O ponto-chave aqui é que o método é garantir a continuidade dos dados e tentar recuperar dados ausentes de outros métodos de sincronização quando dados ausentes são detetados. Aqui estão os principais passos da função:
A função primeiro obtém UnsafeL2Head e UnsafeL2SyncTarget de s.derivation como os pontos de início e fim do intervalo de verificação.
A função verifica se há blocos ausentes entre o início e o fim, o que é feito comparando os valores numéricos de fim e início.
Se uma lacuna de dados for detetada, a função solicitará o intervalo de dados ausente chamando s.altSync.RequestL2Range(ctx, start, end). Se end é uma referência nula (ou seja, eth. L2BlockRef{}), a função solicitará uma sincronização de intervalo aberto, começando com start.
Ao solicitar dados, a função registra um log de depuração indicando qual intervalo de dados está solicitando.
A função eventualmente retorna um valor de erro. Se não houver erros, ele retorna zero
checkForGapInUnsafeQueue verifica se há uma lacuna na fila insegura e tenta recuperar as cargas úteis ausentes de um método alt-sync.
AVISO: Este é apenas um sinal de saída, os blocos não têm garantia de serem recuperados.
Os resultados são recebidos através de OnUnsafeL2Payload.
func (s *Driver) checkForGapInUnsafeQueue(contexto ctx. Contexto) erro {
início := s.derivation.UnsafeL2Head()
fim := s.derivation.UnsafeL2SyncTarget()
Verifique se faltam blocos entre o início e o fim Solicite-os se o fizermos.
se fim == (eth. L2BlockRef{}) {
s.log.Debug("solicitando sincronização com intervalo aberto", "start", start)
return s.altSync.RequestL2Range(ctx, start, eth. L2BlockRef{})
} senão se terminar. Número > início. Número+1 {
s.log.Debug("solicitando intervalo de blocos L2 inseguros ausentes", "start", start, "end", end, "size", end. Número-início.Número)
retornar s.altSync.RequestL2Range (ctx, início, fim)
}
retorno nulo
}
A função RequestL2Range sinaliza o início e o fim do bloco de solicitações para o canal de solicitações.
A solicitação é então distribuída para o canal peerRequests através do método onRangeRequest, e o canal peerRequests é aguardado pelo loop aberto por vários pares, ou seja, apenas um par processará a solicitação para cada distribuição.
Vamos ver o que acontece quando o par recebe essa solicitação.
A primeira coisa que precisamos saber é que o link entre o peer e o nó solicitante, ou a mensagem que passa, é passada através do fluxo libp2p. O método de processamento do fluxo é implementado pelo nó de mesmo nível de recebimento e a criação do fluxo é aberta pelo nó de envio.
Podemos ver esse código na função init anterior, onde MakeStreamHandler retorna um manipulador e SetStreamHandler vincula a ID do protocolo a esse manipulador, portanto, sempre que o nó de envio cria e usa esse fluxo, o manipulador retornado é acionado.
Vamos ver como ele é tratado dentro do manipulador A função primeiro executa verificações de limite de taxa global e individual para controlar a rapidez com que as solicitações são processadas. Em seguida, lê e verifica o número de bloco solicitado, garantindo que está dentro de um intervalo razoável. Em seguida, a função obtém a carga útil da solicitação da camada L2 e a grava no fluxo de resposta. Ao gravar dados de resposta, ele define um prazo de gravação para evitar ser bloqueado por conexões de pares lentas durante o processo de gravação. Finalmente, a função retorna o número de bloco solicitado e possíveis erros.
Neste ponto, o processo geral de solicitação e processamento de sincronização de cadeia reversa foi explicado
Sistema de reputação de pontos em nós p2p
A fim de evitar que certos nós façam solicitações maliciosas e respostas que minam a segurança de toda a rede, o Optimism também usa um sistema de pontos.
Por exemplo, em op-node/p2p/app_scores.go, há uma série de funções para definir a pontuação de um par
O status de integração do novo nó é verificado antes de ser adicionado
Resumo
A alta configurabilidade do libp2p torna todo o projeto p2p altamente personalizável e modular, o acima é a lógica principal da implementação personalizada do libp2p da optimsim, e outros detalhes podem ser aprendidos em detalhes lendo o código-fonte no diretório p2p.