Esta é a terceira parte de uma série de postagens sobre como criar um aplicativo de bate-papo com Rust na blockchain Near. Você pode encontrar os posts anteriores da série aqui e aqui .
Neste post vamos nos concentrar nas partes off-chain do código. Discutiremos a necessidade de “ indexadores ” e percorreremos algumas partes da implementação do indexador neste exemplo. Você pode encontrar o repositório completo com todo o código que discutiremos hoje no meu GitHub .
Indexadores, o que são e por que precisamos deles
No espaço blockchain, um indexador é um serviço que consome dados brutos de uma fonte (normalmente uma instância de nó completo co-localizada para esse blockchain) e os analisa em um formato que é mais útil para um aplicativo específico. Por exemplo, no caso de nosso aplicativo de bate-papo, o indexador consome um fluxo de blocos Near e produz um fluxo de eventos (por exemplo, mensagens recebidas e solicitações de contato).
Os indexadores são importantes porque os bancos de dados usados na operação do próprio blockchain geralmente não são otimizados para executar os tipos de consultas com os quais os aplicativos se preocupam. Por exemplo, obter o saldo de um usuário para um token ERC-20 no Ethereum geralmente é feito executando a consulta por meio do EVM porque essa é a única maneira pela qual as informações estão disponíveis em um nó Ethereum típico. Esta é uma operação extremamente cara em comparação com a pesquisa de uma entrada em um banco de dados relacional tradicional. Portanto, uma otimização simples para qualquer aplicativo que precise de acesso rápido aos saldos do ERC-20 seria executar um indexador nos dados brutos do Ethereum que preenche um banco de dados tradicional com os saldos importantes. Em seguida, o aplicativo usaria esse banco de dados como fonte para os saldos, em vez de um nó Ethereum diretamente. É assim que o explorador de blocos Etherscan funciona sob o capô; O Etherscan executa um indexador para preencher um banco de dados que é usado para preencher os campos nas páginas da Web que o Etherscan atende.
Os indexadores não são importantes apenas para o Ethereum, qualquer dapp de alto desempenho em qualquer blockchain precisará incluir um indexador em algum lugar de sua arquitetura. O aplicativo de bate-papo de exemplo que discutimos no Near não é exceção, então vamos nos aprofundar em como o indexador é implementado.
Obtendo os dados brutos
Os indexadores processam apenas dados brutos de blockchain em um formato que o aplicativo associado pode usar; eles não geram os dados em primeiro lugar. Portanto, a primeira pergunta que precisamos responder ao criar um indexador é: de onde vêm os dados da blockchain?
Near fornece algumas fontes de dados diferentes, conforme descrito abaixo.
Executando um nó nearcore
A melhor fonte de dados (em termos de descentralização e segurança) para qualquer blockchain é a rede peer-to-peer da própria blockchain. Para acessar essa fonte de dados, você deve executar um nó que entenda o protocolo da blockchain. No caso de Near, a implementação do nó é chamada de nearcore. Seu código-fonte está aberto no GitHub. Há documentação disponível sobre como executar seu próprio nó nearcore. A principal barreira à entrada aqui é a quantidade de espaço em disco necessária para isso; é recomendável que você tenha 1 TB de armazenamento dedicado para o seu nó e demore um pouco para sincronizar com a cadeia como resultado da necessidade de baixar todos esses dados.
Depois de configurar um nó nearcore, a Near fornece uma estrutura de indexador conveniente em Rust que pode ser usada para criar indexadores com nearcore como fonte de dados. Para um projeto real, essa seria a melhor maneira de criar um indexador. No entanto, nosso exemplo é apenas uma demonstração, portanto, não queremos gastar horas baixando dados da cadeia para um servidor dedicado de 1 TB. Felizmente existem outras opções.
Lago de dados NEAR
- Para tornar mais fácil para os desenvolvedores iniciarem seus projetos, Near criou a estrutura do data lake como uma fonte alternativa de dados para os indexadores usarem. A estrutura do data lake é construída sobre a estrutura do indexador mencionada acima, usando um nó nearcore como fonte de dados. O indexador que alimenta o data lake é trivial no sentido de que não está processando os dados para um aplicativo específico, está apenas passando os dados para serem armazenados no armazenamento AWS S3. No entanto, isso permite que os desenvolvedores tenham acesso a esses dados usando sua própria conta da AWS e criem seus próprios indexadores (não triviais) usando esse armazenamento S3 como fonte de dados.
A vantagem disso para os desenvolvedores é que esse método é muito mais rápido para começar a funcionar. A desvantagem, porém, é que os dados vêm de uma fonte centralizada e, portanto, são teoricamente mais fáceis de corromper do que usar a rede ponto a ponto diretamente.
Acessar o data lake exige que você pague pelos recursos da AWS usados para entregar esses dados a você. Mais uma vez, para fins do exemplo do aplicativo de bate-papo, não queria que as pessoas se inscrevessem na AWS e gastassem dinheiro para executar o indexador. Portanto, escolhi a opção de fonte de dados final.
Nós RPC públicos
A maneira final de acessar os dados da blockchain se você não estiver executando seu próprio nó ou acessando o armazenamento de dados pré-construído de outra pessoa é usar os nós de outra pessoa. Os nós RPC são nós na rede blockchain destinados a atender às solicitações dos usuários. Cada blockchain possui provedores de nós RPC (alguns gratuitos, outros pagos). Uma lista dos provedores RPC para Near pode ser encontrada aqui .
Essa é a maneira menos eficiente de acessar os dados da blockchain porque são necessárias várias solicitações de RPC para obter os dados que os indexadores típicos usam. Cada solicitação RPC incorre em latência de rede, tornando o indexador lento para responder a eventos que ocorrem na cadeia. A única vantagem dessa abordagem é que ela é gratuita para configurar uma demonstração, desde que haja um provedor RPC gratuito para a cadeia (que é o caso da Near). Portanto, esta é a fonte de dados que o indexador em nosso exemplo usa.
Dito isso, o próprio indexador não se importa de onde vêm seus dados. Portanto, embora nosso exemplo esteja usando a pior fonte de dados, vale a pena explorar sua implementação porque os conceitos que esse indexador usa são os mesmos de um indexador construído usando o data lake do Near ou estruturas de indexador baseadas em nós.
Implementação do indexador
Nosso indexador é construído como um tokio app em Rust. Tokio é uma estrutura Rust para escrever aplicativos de alto desempenho onde as operações de E/S são o principal gargalo. Nosso indexador é um desses aplicativos porque a computação real que ele executa é extremamente rápida em comparação com o tempo necessário para solicitar dados dos nós RPC. As principais características do tokio são que ele usa primitivos assíncronos sem bloqueio e possui multithreading integrado para permitir a execução paralela. Isso é além de estar em Rust, então naturalmente tem as garantias de segurança de simultaneidade e segurança de memória que Rust fornece.
Se tokio é o palco no qual nosso aplicativo é definido, então o que segue são os atores da peça (trocadilho intencional; este aplicativo segue o modelo de ator, mas eu escolho fazê-lo diretamente em tokio ao invés de usar uma biblioteca como actix porque eu acho que os canais de tokio fornecem digitação mais forte do que as mensagens genéricas usadas na maioria das estruturas de atores).
O indexador tem quatro papéis principais: o gerenciador, o downloader de bloco, o downloader de chunk e o manipulador de recibos.
O gerente
O processo gerenciador supervisiona todo o indexador. Ele é responsável por delegar trabalho aos outros processos e dizer-lhes para desligar quando o programa estiver sendo fechado (por exemplo, no caso de um erro ser encontrado). Por exemplo, o gerenciador lida com o balanceamento de carga dos downloaders de fragmentos percorrendo-os ao atribuir um fragmento para download.
O downloader de blocos
Como o nome indica, o objetivo do processo de download de blocos é baixar blocos. Ele pesquisa periodicamente o Near RPC para verificar se há novos blocos e, se houver, faz o download deles e os envia para o gerenciador. Se não estivéssemos usando o RPC como nossa fonte de dados, esse processo seria substituído por uma conexão com um nó próximo ou data lake.
O(s) downloader(es) de partes
Na Near, os blocos não contêm os dados sobre as transações; pedaços fazem. Os blocos fornecem apenas informações sobre quais novos blocos estão disponíveis. A razão para isso é por causa do sharding de Near (você pode ler mais sobre isso aqui). Portanto, precisamos de processos separados para baixar os dados do bloco para cada bloco. Os chunk downloaders cumprem essa função. Nosso indexador tem várias instâncias do chunk downloader para permitir o download dos chunks em paralelo.
Se não estivéssemos usando o RPC como nossa fonte de dados, dependendo de como os dados são fatorados na fonte de dados que estávamos usando, esses processos podem não precisar existir (por exemplo, a estrutura do indexador próximo inclui todos os dados de bloco e bloco em um única mensagem). Mas para o nosso caso, como estamos usando o RPC, esses processos são necessários.
O manipulador de recibos
Os pedaços contêm “recibos” que são criados quando uma transação é processada. Quando o gerente recebe um novo chunk de um chunk downloader, ele envia todos os recibos para o processo do manipulador de recibos (poderíamos ter várias instâncias do manipulador de recibos para processar recibos em paralelo, assim como temos vários downloaders de chunks, mas o processamento do recibo é rápido o suficiente para que eu não achasse que isso adicionasse muita melhoria de desempenho). Esse processo filtra os recebimentos apenas para os que nos interessam, depois baixa o resultado da execução dos recebimentos e, finalmente, processa os eventos desses resultados. No caso deste exemplo, simplesmente escreva os eventos em um arquivo (para uma demonstração ao vivo, você pode assistir ao arquivo com algo como o tail -f comando Unix para ver os eventos chegando), mas você pode imaginar que uma implementação de produção poderia encaminhar esses eventos como notificações push para uma versão móvel do aplicativo.
Observações
Você pode notar em todo o código do indexador que há alguma complexidade em relação ao envio de blocos/recibos com o hash do bloco após o bloco que incluiu esses blocos. Essa é uma peculiaridade da Near RPC, onde ele deseja saber se você está ciente dos blocos posteriores para atender ao resultado da execução. Novamente, isso seria tratado com muito mais facilidade se fosse usada uma fonte de dados melhor.
É intencional que não haja pânico em nenhuma das funções do ator. Quando encontram um erro, eles o registram e enviam uma mensagem de desligamento ao gerente (e o gerente a envia a todos os outros atores). Isso é importante porque entrar em pânico em um aplicativo multithread pode causar um comportamento inesperado (em geral, o tokio é muito bom em derrubar o aplicativo inteiro normalmente, mas ainda é melhor codificar defensivamente contra ele).
Conclusão
Nesta postagem, discutimos por que os indexadores são importantes para os dapps do mundo real e analisamos alguns dos detalhes do indexador de exemplo implementado para o chat dapp. Assim como na postagem anterior, há exercícios no código do indexador incluídos nos comentários marcados como EXERCISE. Encorajo você a experimentar esses exercícios se quiser alguma experiência prática com a base de código.
Sobre a Série
Este é o último post desta série. Na Parte 1, analisamos os princípios gerais do desenvolvimento de contratos inteligentes e como eles se aplicam a um exemplo de contrato para um chat dapp. Na Parte 2, nos aprofundamos em como near-sdk escreve contratos inteligentes para Near in Rust. Por fim, esta postagem discutiu como os indexadores são necessários para integrar os dados do blockchain com os componentes fora da cadeia do nosso aplicativo.
Uma parte final do código que não abordei é o teste de integração. O teste de integração usa a biblioteca de espaços de trabalho próximos para simular a blockchain localmente e usa o mesmo estilo Rust assíncrono do indexador. Mesmo que os testes de integração não sejam especialmente chamativos ou interessantes, eles são importantes para garantir que seu contrato funcione corretamente. Eu o encorajo a dar uma olhada nos testes de integração para o contrato de mensageiro e tentar o exercício lá para obter alguma experiência prática nessa área também.
Se você gostou desta série de postagens de blog, entre em contato conosco na consultoria Type-Driven. Temos o prazer de fornecer serviços de desenvolvimento de software para dapps, bem como materiais de treinamento para seus próprios engenheiros.
——–
2150 palavras