ūüáßūüá∑ A natureza abstrata da camada de consenso da Cardano

(Postado originalmente em 28/05/2020 por Edsko de Vries, traduzido por Joselmo Cabral)

Esta nova série de postagens técnicas dos engenheiros da IOHK considera as escolhas que estão sendo feitas

Enquanto nos dirigimos para Shelley na rede principal de Cardano, queremos mant√™-lo atualizado com as √ļltimas not√≠cias. Tamb√©m queremos destacar o incans√°vel trabalho em andamento nos bastidores e a equipe de engenheiros respons√°veis.

Nesta série de blogs técnicos ocasionais do Developer Deep Dive, abrimos o espaço para os engenheiros da IOHK. Nos próximos meses, nossos desenvolvedores da Haskell oferecerão uma visão franca dos principais elementos da plataforma e dos protocolos Cardano e compartilharão insights sobre as escolhas que foram feitas.

Aqui, no primeiro da série, consideramos o uso da abstração na camada de consenso.

Introdução

A camada de consenso de Cardano tem duas responsabilidades importantes:

  • Ele executa o protocolo de consenso blockchain. No contexto de um blockchain, consenso ‚Äď ou seja, ‚Äėmaioria da opini√£o‚Äô ‚Äď significa que todos os envolvidos na execu√ß√£o do blockchain concordam com o que √© a √ļnica cadeia verdadeira. Isso significa que a camada de consenso √© respons√°vel por adotar blocos, escolher entre cadeias concorrentes, se houver, e decidir quando produzir blocos pr√≥prios.

  • √Č respons√°vel por manter todo o estado necess√°rio para tomar essas decis√Ķes. Para decidir se deve ou n√£o adotar um bloco, o protocolo precisa valid√°-lo com rela√ß√£o ao estado do ledger . Se decidir mudar para uma corrente diferente (um peda√ßo diferente de um fork na corrente), ele dever√° manter hist√≥rico suficiente para poder reconstruir o estado do ledger nessa corrente. Para poder produzir blocos, ele deve manter um conjunto de mem√≥rias de transa√ß√Ķes a serem inseridas nesses blocos.

A camada de consenso medeia entre a camada de rede abaixo dela, que lida com preocupa√ß√Ķes como protocolos de comunica√ß√£o e sele√ß√£o de pares e a camada de ledger acima dela, que especifica como √© o estado do ledger e como deve ser atualizado a cada novo bloco. A camada do ledger √© totalmente sem estado e consiste exclusivamente em fun√ß√Ķes puras. Por sua vez, a camada de consenso n√£o precisa saber a natureza exata do estado do ledger, ou mesmo o conte√ļdo dos blocos (al√©m de alguns campos de cabe√ßalho necess√°rios para executar o protocolo de consenso).

O uso extensivo é feito de abstração na camada de consenso. Isso é importante por vários motivos:

  • Ele permite que os programadores injetem falhas ao executar testes. Por exemplo, abstra√≠mos o sistema de arquivos subjacente e usamos isso para testar a camada de armazenamento e simular todo tipo de falhas no disco. Da mesma forma, abstra√≠mos com o tempo e usamos isso para verificar o que acontece com um n√≥ quando o rel√≥gio do sistema de um usu√°rio salta para frente ou para tr√°s.

  • Isso nos permite instanciar a camada de consenso com muitos tipos diferentes de ledgers. Atualmente, usamos isso para executar a camada de consenso com o ledger Byron (o ledger que est√° atualmente no blockchain Cardano), bem como o ledger Shelley para a pr√≥xima rede de testes Shelley Haskell. Al√©m disso, usamos-o para executar a camada de consenso com v√°rios tipos de ledgers especificamente projetados para teste, geralmente muito mais simples que os ledgers ‚Äėreais‚Äô, para que possamos focar nossos testes na pr√≥pria camada de consenso.

  • Ele melhora a composi√ß√£o , permitindo construir componentes maiores a partir de componentes menores. Por exemplo, o ledger de teste da Shelley cont√©m apenas o ledger de Shelley, mas quando o Shelley for liberado, a cadeia real conter√° o livro de Byron at√© o ponto do hard fork e o livro de Shelley a partir de ent√£o. Isso significa que precisamos de uma camada de ledger que alterne entre dois ledgers em um ponto espec√≠fico. Em vez de definir um novo ledger, podemos definir um combinador de hard fork que implementa precisamente essa e somente essa funcionalidade. Isso melhora a reutiliza√ß√£o do c√≥digo (n√£o precisaremos reimplementar a funcionalidade do hard fork quando o pr√≥ximo hard fork aparecer), bem como a separa√ß√£o de preocupa√ß√Ķes: o desenvolvimento e o teste do combinador de hard fork n√£o dependem das especificidades dos ledgers entre eles.

  • Intimamente relacionado ao √ļltimo ponto, o uso da abstra√ß√£o melhora a testabilidade . Podemos definir combinadores que definem pequenas varia√ß√Ķes de protocolos de consenso que nos permitem focar em aspectos espec√≠ficos. Por exemplo, temos um combinador que aceita um protocolo de consenso existente e substitui apenas a verifica√ß√£o se devemos produzir um bloco. Em seguida, podemos usar isso para gerar cen√°rios de teste nos quais muitos n√≥s produzem um bloco em um determinado slot ou, inversamente, nenhum n√≥, e verificar se a camada de consenso faz a coisa certa nessas circunst√Ęncias. Sem um combinador para substituir esse aspecto da camada de consenso, seria deixado ao acaso se tais cen√°rios surgissem. Embora possamos controlar o ‚Äėacaso‚Äô no teste (escolhendo uma semente inicial espec√≠fica para o gerador de n√ļmeros aleat√≥rios pseudo-aleat√≥rios), seria imposs√≠vel configurar cen√°rios espec√≠ficos, ou mesmo reduzir automaticamente os casos de teste com falha para um caso de teste m√≠nimo. Com esses combinadores de consenso de teste, o teste pode literalmente usar apenas uma programa√ß√£o especificando quais n√≥s devem produzir blocos em quais slots. Podemos ent√£o gerar esses agendamentos aleatoriamente, execut√°-los e, se houver um erro, reduzi-los a um caso de teste m√≠nimo que ainda aciona o bug. Um cronograma de falha t√£o m√≠nimo √© muito mais f√°cil de depurar, entender e trabalhar do que apenas uma escolha de semente aleat√≥ria inicial que faz com que ‚Ķ algo ‚Ķ aconte√ßa ‚Ķ em algum momento .

Para conseguir tudo isso, no entanto, a camada de consenso precisa fazer uso de alguns tipos sofisticados de Haskell. Nesta postagem do blog, desenvolveremos um ‚Äėmini protocolo de consenso‚Äô, explicaremos exatamente como abstra√≠mos v√°rios aspectos e como podemos fazer uso dessas abstra√ß√Ķes para definir v√°rios combinadores. O restante deste post do blog pressup√Ķe que o leitor esteja familiarizado com Haskell.

Preliminares

A camada de consenso de Cardano foi projetada para suportar a fam√≠lia de protocolos de consenso Ouroboros, todos assumindo fundamentalmente que o tempo √© dividido em slots , onde o blockchain pode ter no m√°ximo um bloco por slot. Neste artigo, definimos um n√ļmero de slot para ser apenas um Int:

Tamb√©m precisaremos de um modelo m√≠nimo de criptografia de chave p√ļblica, especificamente

A √ļnica extens√£o de idioma que precisaremos nesta postagem de blog √© TypeFamilies, embora a implementa√ß√£o real use muito mais. Se voc√™ quiser acompanhar, fa√ßa o download do c√≥digo fonte completo.

Protocolo de consenso

Conforme mencionado no início deste post, o protocolo de consenso tem três responsabilidades principais.

  • Verifica√ß√£o do l√≠der (devemos produzir um bloco?)

  • Sele√ß√£o da cadeia

  • Bloquear verifica√ß√£o

O protocolo pretende ser independente de uma escolha concreta de bloco, bem como de uma escolha concreta do ledger, de modo que um √ļnico protocolo possa ser executado com diferentes tipos de blocos e / ou registros. Portanto, cada uma dessas tr√™s responsabilidades define sua pr√≥pria ‚Äėvis√£o‚Äô sobre os dados necess√°rios.

Verificação do líder

A verifica√ß√£o do l√≠der √© executada em todos os slots e deve determinar se o n√≥ deve produzir um bloco. Em geral, a verifica√ß√£o do l√≠der pode exigir algumas informa√ß√Ķes extra√≠das do estado do ledger:

Por exemplo, no protocolo de consenso de Ouroboros Praos que inicialmente fornecerá a Shelley, a probabilidade de um nó ser eleito líder (ou seja, permitir que produza um bloco) depende de sua participação; o stake do nó, é claro, vem do estado do ledger.

Seleção da cadeia

‚ÄėSele√ß√£o de cadeias‚Äô refere-se ao processo de escolha entre duas cadeias concorrentes. O principal crit√©rio aqui √© o comprimento da cadeia, mas alguns protocolos podem ter requisitos adicionais. Por exemplo, os blocos s√£o normalmente assinados por uma chave ‚Äėquente‚Äô existente no servidor, que por sua vez √© gerada por uma chave ‚Äėfria‚Äô que nunca existe em nenhum dispositivo em rede. Quando a tecla de atalho √© comprometida, o operador do n√≥ pode gerar um novo a partir da tecla de atalho e ‚Äėdelegar‚Äô para essa nova chave. Portanto, em uma escolha entre duas cadeias de comprimento igual, ambas com uma ponta assinada pela mesma tecla de atalho, mas com uma tecla de atalho diferente, um protocolo de consenso preferir√° a tecla de atalho mais recente. Para permitir que protocolos de consenso espec√≠ficos indiquem requisitos como esse, introduzimos um SelectView, especificando quais informa√ß√Ķes o protocolo espera que estejam presentes no bloco:

Validação de cabeçalho

A valida√ß√£o de bloco √© principalmente uma preocupa√ß√£o do ledger; verifica√ß√Ķes como a verifica√ß√£o de que todas as entradas de uma transa√ß√£o est√£o dispon√≠veis para evitar gastos duplos s√£o definidas na camada do ledger. A camada de consenso n√£o tem conhecimento do que est√° dentro dos blocos; na verdade, pode at√© n√£o ser uma criptomoeda, mas uma aplica√ß√£o diferente da tecnologia blockchain.

Os blocos (mais precisamente, os cabe√ßalhos dos blocos) tamb√©m cont√™m algumas coisas especificamente para apoiar a camada de consenso. Para ficar com o exemplo do Praos, o Praos espera que v√°rias provas criptogr√°ficas estejam presentes relacionadas √† deriva√ß√£o da entropia da blockchain. A verifica√ß√£o destas √© de responsabilidade do consenso e, portanto, definimos uma terceira e √ļltima vis√£o dos campos que o consenso deve validar:

Definição de protocolo

Precisamos de mais uma fam√≠lia de tipos *: cada protocolo pode exigir informa√ß√Ķes est√°ticas para ser executado; chaves para assinar blocos, sua pr√≥pria identidade para a verifica√ß√£o de l√≠der, etc. Chamamos isso de ‚Äėconfigura√ß√£o do n√≥‚Äô e a definimos como

Agora, podemos unir tudo na definição abstrata de um protocolo de consenso. Os três métodos da classe correspondem às três responsabilidades que mencionamos; cada método recebe a configuração do nó estático, bem como a visualização específica necessária para esse método. Observe que p aqui é simplesmente uma tag de nível de tipo que identifica um protocolo específico; nunca existe no nível de valor.

Exemplo: BFT permissivo (PBFT)

O BFT permissivo √© um protocolo de consenso simples, no qual os n√≥s produzem blocos de acordo com uma programa√ß√£o de rod√≠zio: primeiro n√≥ A, depois B, depois C e depois volta para A. Chamamos o √≠ndice de um n√≥ nessa programa√ß√£o como ‚ÄėNodeId‚Äô. Este √© um conceito apenas para PBFT, n√£o algo que o restante da camada de consenso precisa estar ciente:

A configura√ß√£o do n√≥ exigida pelo PBFT √© a pr√≥pria identidade do n√≥, bem como as chaves p√ļblicas de todos os n√≥s que podem aparecer no planejamento:

Como o PBFT é uma programação simples de rodízio, ele não precisa de nenhuma informação do ledger:

A sele√ß√£o da corrente apenas analisa o comprimento da corrente; por uma quest√£o de simplicidade, assumiremos aqui que cadeias mais longas terminam em um n√ļmero maior de slots ** e, portanto, a sele√ß√£o de cadeias precisa apenas saber o n√ļmero de slot da ponta:

Por fim, ao validar blocos, devemos verificar se eles são assinados por qualquer um dos nós com permissão para produzir blocos (que podem aparecer no cronograma):

Definir o protocolo agora é simples:

A sele√ß√£o de cadeias apenas compara os n√ļmeros dos slots; a valida√ß√£o verifica se a chave √© um dos l√≠deres de slot permitidos e a sele√ß√£o de l√≠deres faz alguma aritm√©tica modular para determinar se √© a vez desse n√≥.

Observe que o cronograma round-robin √© usado apenas para verificar se somos ou n√£o um l√≠der; para blocos hist√≥ricos (ou seja, ao fazer a valida√ß√£o), apenas verificamos se o bloco foi assinado por qualquer um dos l√≠deres permitidos, e n√£o em qual ordem. Esta √© a parte ‚Äėpermissiva‚Äô da PBFT; a implementa√ß√£o real faz uma verifica√ß√£o adicional (de que n√£o h√° um √ļnico l√≠der assinando mais do que seu compartilhamento), que omitimos nesta postagem do blog.

Exemplo de combinador de protocolo: planejamento explícito do líder

Conforme mencionado na introdu√ß√£o, para testar, √© √ļtil poder decidir explicitamente a ordem na qual os n√≥s assinam blocos. Para fazer isso, podemos definir um combinador de protocolo que aceita um protocolo e simplesmente substitui a verifica√ß√£o is-leader para verificar uma programa√ß√£o fixa:

A configuração necessária para executar esse protocolo é o agendamento e o ID do nó nesse agendamento, além de qualquer configuração que o protocolo subjacente p exija:

A seleção de cadeias permanece inalterada e, portanto, também é a visão sobre os blocos necessários para a seleção de cadeias:

No entanto, precisamos substituir a valida√ß√£o do cabe√ßalho ‚Äď na verdade, desabilit√°-la completamente ‚Äď porque se o protocolo subjacente verificar que os blocos est√£o assinados pelo n√≥ correto, obviamente essa verifica√ß√£o seria incompat√≠vel com essa substitui√ß√£o. Portanto, n√£o precisamos de nada do bloco para valid√°-lo (j√° que n√£o fazemos nenhuma valida√ß√£o):

Definir o protocolo agora é trivial:

A seleção em cadeia usa apenas a seleção em cadeia do protocolo subjacente p, a validação de bloco não faz nada e a verificação is-leader refere-se à programação estática.

De blocos a protocolos

Assim como o protocolo de consenso, uma escolha específica de bloco também pode vir com seus próprios dados de configuração necessários:

Muitos blocos podem usar o mesmo protocolo, mas um tipo específico de bloco é projetado para um tipo específico de protocolo. Portanto, apresentamos uma família de tipos mapeando um tipo de bloco para o protocolo associado:

Dizemos ent√£o que um bloco suporta seu protocolo se pudermos construir as visualiza√ß√Ķes nesse bloco exigidas por seu protocolo espec√≠fico:

Exemplo: Blocos Byron (simplificados)

De forma bruta, os blocos da blockchain Byron parecem algo como

Mencionamos acima que a fam√≠lia de protocolos de consenso de Ouroboros assume que o tempo √© dividido em slots . Al√©m disso, eles tamb√©m assumem que esses slots s√£o agrupados em √©pocas . Os blocos de Byron n√£o cont√™m um n√ļmero absoluto, mas um n√ļmero de √©poca e um n√ļmero de slot relativo nessa √©poca. O n√ļmero de slots por √©poca na cadeia Byron √© fixado em um pouco acima de 10 k , mas para testar √© √ļtil poder variar k , o par√Ęmetro de seguran√ßa e, portanto, √© configur√°vel; esta √© (parte da) configura√ß√£o necess√°ria para trabalhar com os blocos Byron:

Dada a configura√ß√£o de Byron, √© simples converter o par de um n√ļmero de √©poca e um slot relativo em um n√ļmero absoluto de slot:

E o protocolo? Bem, a cadeia Byron executa PBFT, e assim podemos definir

√Č f√°cil provar que o bloco Byron suporta este protocolo:

O estado do ledger

A camada de consenso não apenas executa o protocolo de consenso, mas também gerencia o estado do ledger. No entanto, não se importa muito com a aparência específica do estado do ledger; em vez disso, assume apenas que algum tipo de estado do ledger está associado a um tipo de bloco:

Vamos precisar de uma família de tipos adicionais. Quando aplicamos um bloco ao estado do ledger, podemos obter um erro se o bloco for inválido. O tipo específico de erros de ledger é definido na camada do ledger e, é claro, é altamente específico. Por exemplo, o ledger de Shelley terá erros relacionados à aplicação de stakes, enquanto o ledger de Byron não, porque não suporta a aplicação de stakes; e ledgers que não são criptomoedas terão tipos muito diferentes de erros.

Agora, definimos duas classes de tipos. O primeiro apenas descreve a interface para a camada do ledger, dizendo que devemos poder aplicar um bloco ao estado do ledger e obter um erro (se o bloco for inv√°lido) ou o estado do ledger atualizado:

Segundo, definiremos uma classe de tipo que conecta o ledger associado a um bloco ao protocolo de consenso associado a esse bloco. Assim como o BlockSupportsProtocol fornece funcionalidade para derivar visualiza√ß√Ķes do bloco exigido pelo protocolo de consenso, o LedgerSupportsProtocol da mesma forma fornece funcionalidade para derivar visualiza√ß√Ķes do estado do ledger exigido pelo protocolo de consenso:

Veremos na pr√≥xima se√ß√£o por que √© √ļtil separar a integra√ß√£o com o ledger (UpdateLedger) de sua conex√£o com o protocolo de consenso (LedgerSupportsProtocol).

Combinadores de bloco

Como exemplo final do poder dos combinadores, mostraremos que podemos definir um combinador em blocos e seus ledgers associados. Como um exemplo de onde isso √© √ļtil, o blockchain Byron vem com uma implementa√ß√£o e uma especifica√ß√£o execut√°vel. √Č √ļtil instanciar a camada de consenso com esses dois livros, para que possamos verificar se a implementa√ß√£o concorda com a especifica√ß√£o em todos os pontos ao longo do caminho. Isso significa que os blocos nessa configura√ß√£o de ‚Äėledger duplo‚Äô s√£o, de fato, um par de blocos ***.

A definição do estado do ledger duplo e do erro do ledger duplo é semelhante:

Para aplicar um bloco duplo ao estado do ledger duplo, simplesmente aplicamos cada bloco ao seu estado associado. Esse combinador em particular assume que os dois ledgers devem sempre concordar se um bloco espec√≠fico √© ou n√£o v√°lido, o que √© adequado para comparar uma implementa√ß√£o e uma especifica√ß√£o; outras op√ß√Ķes (outros combinadores) tamb√©m s√£o poss√≠veis:

Como a inten√ß√£o do ledger duplo √© comparar duas implementa√ß√Ķes do ledger , basta que todas as preocupa√ß√Ķes de consenso sejam direcionadas pelo primeiro bloco (‚Äėprincipal‚Äô); n√£o precisamos de uma inst√Ęncia para ProtocolLedgerView para o bloco auxiliar e, de fato, em geral, talvez n√£o seja poss√≠vel fornecer uma. Isso significa que o protocolo de bloco do bloco duplo √© o protocolo de bloco do bloco principal:

A configuração de bloco que precisamos é a configuração de bloco de ambos os blocos:

Agora podemos mostrar facilmente que o bloco duplo também suporta seu protocolo:

Conclus√Ķes

A camada de consenso de Cardano foi projetada inicialmente para o blockchain Cardano, atualmente executando Byron e logo executará Shelley. Você pode argumentar que isso significa que os engenheiros da IOHK devem projetar inicialmente para essa blockchain específica e generalizar apenas mais tarde ao usar a camada de consenso para outras blockchains. No entanto, fazer isso teria desvantagens importantes:

  • Isso prejudicaria nossa capacidade de realizar testes. N√£o poder√≠amos substituir a programa√ß√£o de quais n√≥s produzem blocos quando, n√£o poder√≠amos executar com um ledger duplo, etc.

  • N√≥s envolver√≠amos coisas que s√£o logicamente independentes. Com a abordagem abstrata, o ledger Shelley consiste em tr√™s partes: uma cadeia de Byron, uma cadeia de Shelley e um combinador de fork que medeia entre esses dois. Sem abstra√ß√Ķes, essa separa√ß√£o de preocupa√ß√Ķes seria mais dif√≠cil de alcan√ßar, levando a mais dif√≠cil compreens√£o e manuten√ß√£o do c√≥digo.

  • O c√≥digo abstrato √© menos propenso a erros. Como um exemplo simples, como o combinador de ledger duplo √© polim√≥rfico nos dois ledgers combinados e eles t√™m tipos diferentes , n√£o conseguimos escrever o c√≥digo correto do tipo que, no entanto, tenta aplicar, por exemplo, o bloco principal ao ledger auxiliar.

  • Quando chegar a hora e queremos instanciar a camada de consenso para um novo blockchain (como inevitavelmente ser√°), escrev√™-la de uma maneira abstrata desde o in√≠cio nos obriga a pensar cuidadosamente sobre o design e evitar o acoplamento de coisas que n√£o devem ser acopladas , ou fazer suposi√ß√Ķes que sejam justificadas para o blockchain real em considera√ß√£o, mas podem n√£o ser verdadeiras em geral. Corrigir esses problemas ap√≥s a implanta√ß√£o do design pode ser dif√≠cil.

Obviamente, tudo isso requer uma linguagem de programação com excelentes habilidades de abstração; felizmente, Haskell se encaixa perfeitamente nessa conta.

Este é o segundo de uma série de posts do Deep Dive voltados para desenvolvedores

  • Diferente das v√°rias visualiza√ß√Ķes, o NodeConfig √© uma fam√≠lia de dados; como a configura√ß√£o do n√≥ √© passada para todas as fun√ß√Ķes, ter o NodeConfig como uma fam√≠lia de dados ajuda a digitar infer√™ncia, pois determina p. Deixar o resto como fam√≠lias de tipos pode ser conveniente e evitar a embalagem e a embalagem desnecess√°rias.

** Os n√ļmeros dos slots s√£o um proxy para o comprimento da cadeia somente se todos os slots contiverem um bloco. Isso √© verdade para PBFT apenas na aus√™ncia de parti√ß√Ķes de rede tempor√°rias e n√£o √© verdade para algoritmos probabil√≠sticos de consenso, como o Praos. Dito de outra forma, uma cadeia de densidade mais baixa pode muito bem ter um n√ļmero de slot mais alto na ponta do que outra cadeia de densidade mais curta mas mais alta.

*** O verdadeiro combinador DualBlock mant√©m uma terceira informa√ß√£o que relaciona os dois blocos (como identifica√ß√Ķes de transa√ß√£o). N√≥s o omitimos neste post por quest√£o de simplicidade.

2 Likes