WEB3DEV

Cover image for Tutorial - Crie um programa de eleição em Solana usando o Anchor
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Tutorial - Crie um programa de eleição em Solana usando o Anchor

01 de setembro de 2022

Dois dos requisitos mais importantes para as eleições são a segurança e a transparência. A tecnologia blockchain oferece ambos, tornando-a mais adequada para a condução de eleições. Neste tutorial, criaremos um programa de eleição completo em Solana. Por que Solana? Bem, é a blockchain que mais cresce, com a atividade do desenvolvedor apenas em segundo lugar, depois da Ethereum. Além disso, ela oferece alta taxa de transferência e velocidade de transação.

Existem alguns tutoriais já disponíveis sobre aplicativos de votação. Deixe que eu liste-os abaixo:

Portas x Rodas

Como construir em Solana?

Exemplo de votação on-chai com o Anchor

Porém, os tutoriais acima permitem apenas que os usuários votem em algumas opções pré-determinadas. As eleições verdadeiras são bem diferentes disso. No momento da criação das eleições, não conseguimos determinar a identidade e o número de candidatos. Assim, nosso programa de eleições deve ter as seguintes características:

  1. permitir que qualquer número de candidatos se inscreva;
  2. permitir que qualquer um se inscreva;
  3. ter diferentes estágios - candidatura, votação e estágio fechado;
  4. declarar um ou muitos (no caso de eleições de conselho) vencedores.

Tendo em mente os recursos acima, vamos começar a criar nosso aplicativo de eleições descentralizado em Solana. Você pode encontrar o código completo no GitHub: aqui.

Parte I - Criação da conta de eleição

O primeiro passo é criar uma conta de eleição. Chamaremos a pessoa que cria a instância de eleição de ‘iniciador’. O fluxo do processo eleitoral será assim:

  1. O iniciador cria a instância de eleição.
  2. Os candidatos podem começar a se inscrever.
  3. O iniciador fecha o estágio de inscrição e a eleição vai para o estágio de votação.
  4. Os eleitores votam nos candidatos.
  5. O iniciador fecha o estágio de votação e os resultados são declarados.

No cenário ideal, o iniciador não deveria ter nenhuma autoridade após a criação da eleição. Isso pode ser alcançado tornando a eleição baseada no tempo. Por exemplo, o estágio de inscrição se encerra automaticamente após 7 dias da criação da eleição. No entanto, para simplificar este tutorial, concederemos o poder de fechar o aplicativo e os estágios de votação ao iniciador.

Nossa conta de eleição terá os seguintes campos:

  1. Candidatos - o número de candidatos que se inscreveram até o momento.
  2. Estágio - em que estágio está a eleição atualmente (candidatura, votação, encerrado).
  3. Iniciador - chave pública da pessoa que cria a eleição.
  4. Winners_num - o número de vencedores a serem escolhidos (mínimo 1).
  5. Winners_id e Winners_votes - explicarei esses campos mais tarde.

Avançando para a parte de codificação. Este tutorial pressupõe que você tenha uma compreensão básica do Anchor e dos principais conceitos de Solana, como PDAs (Program Derived Addresses - endereços derivados de programa). Caso contrário, você deve primeiro procurá-los e depois continuar em frente.

use anchor_lang::prelude::*;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod election {
    use super::*;
    pub fn create_election(ctx: Context<CreateElection>,winners:u8) -> Result<()> {
        require!(winners > 0,ElectionError::WinnerCountNotAllowed);
        let election = &mut ctx.accounts.election_data;
        election.candidates = 0; 
        election.stage = ElectionStage::Application;
        election.initiator = ctx.accounts.signer.key();
        election.winners_num = winners;
        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(winners:u8)]
pub struct CreateElection<'info> {
    #[account(
        init,
        payer=signer,
        space= 8 + 8 + 2 + 32 + 1 + 2 * (4 + winners as usize * 8)
    )]
    pub election_data: Account<'info,ElectionData>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info,System>
}

#[account]
pub struct ElectionData {
    pub candidates: u64,
    pub stage: ElectionStage,
    pub initiator: Pubkey,
    pub winners_num: u8,
    pub winners_id: Vec<u64>,
    pub winners_votes: Vec<u64>,
}

#[derive(AnchorDeserialize,AnchorSerialize,PartialEq,Eq,Clone)]
pub enum ElectionStage {
    Application,
    Voting,
    Closed,
}

#[error_code]
pub enum ElectionError {
    WinnerCountNotAllowed
}
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, o campo ‘candidates’ (candidatos) é um inteiro u64 inicialmente definido como 0.

O campo ‘stage’ (estágio) é definido como ElectionStage::Application para que a eleição vá para o estágio de aplicação imediatamente após a criação. ElectionStage é uma enumeração personalizada com três opções possíveis - Application (inscrição), Voting (votação) e Closed (encerrado).

O campo ’initiator’ (iniciador) guarda a chave pública de quem cria a eleição e será utilizado posteriormente para garantir que apenas o iniciador possa mudar os estágios.

O campo ’Winners_num’ é um inteiro u8 que será inserido pelo iniciador. Como deve haver pelo menos um vencedor, adicionamos um requisito de declaração e ele emitirá o erro personalizado WinnerCountNotAllowed se o iniciador tentar inserir o ’winners_num’ abaixo de 1. Então, efetivamente, os vencedores podem estar no intervalo de 1 a 255. Você pode definir esse campo para u16 ou mais se precisar de mais vencedores.

Tanto ’winners_id’ e ‘winners_votes’_ são vetores u64 que gravarão os ids e os votos dos vencedores, respectivamente. Discutiremos a necessidade desses campos nas seções posteriores.

Em Solana, temos que definir o espaço requerido pela conta no momento da criação do programa. Para a cona de eleição, definimos o espaço como:

8 + 8 + 2 + 32 + 1 + 2*(4 + vencedores conforme o tamanho u*8)

Os primeiros 8 bytes são para o discriminador interno do Anchor. Os próximos 43 bytes são para os campos candidatos, estágio, iniciador e winners_num. Porém, o tamanho dos campos winners_id e winners_votes não serão os mesmos para todas as instâncias de eleição. O número de vencedores pode estar no intervalo de 1 a 255, tornando o tamanho desses campos variável. Portanto, o tamanho desses campos é derivado do argumento ’winners’ passado pelo iniciador na instrução. Se o iniciador escolher 2 possíveis vencedores, o winners_id e o winners_votes podem conter apenas 2 elementos cada.

Como alternativa, você pode mencionar estaticamente o tamanho máximo possível, ou seja, 8 + 8 + 2 + 32 + 1 + 2 (4 + 255 8) para o espaço. Mas o espaço da conta tem um valor de aluguel, por isso é aconselhável manter o tamanho da conta o menor possível. Mais sobre espaços aqui: Referência de espaço.

Com isso, completamos nossa primeira parte - a conta de eleição. Agora, vamos seguir adiante para a configuração do candidato.

Parte II - Criação da conta do candidato

Lembra-se do fluxo do processo de eleição que discutimos acima? Concluímos a primeira etapa, ou seja, o iniciador cria a instância de eleição. Agora, seguimos para a segunda etapa, ou seja, os candidatos começam a se inscrever. A maneira mais simples de realizar isso é criar uma lista e adicionar a chave pública a ela toda vez que um candidato se inscrever. Mas, optar por essa solução, nos custará muito espaço de aluguel. O número de candidatos pode ser grande.

Para resolver o problema, usaremos os endereços derivados de programa, ou PDAs (Program Derived Addresses). Os PDAs são uma das características distintivas da Solana e oferecem toneladas de utilidades. Sempre que um candidato se inscrever, criaremos uma conta que armazenará os votos desse candidato. Em seguida, transferiremos a titularidade dessa conta para o PDA. Portanto, em vez de uma única conta contendo as chaves públicas e votos para todos os candidatos, criaremos contas separadas para cada candidato e o candidato pagará o aluguel pela criação.

Vejamos primeiro o código:

pub fn apply(ctx: Context<Apply>) -> Result<()> {
    let election = &mut ctx.accounts.election_data;

    require!(election.stage == ElectionStage::Application,ElectionError::ApplicationIsClosed);

    election.candidates += 1;
    ctx.accounts.candidate_identity.id = election.candidates;
    ctx.accounts.candidate_identity.pubkey = ctx.accounts.signer.key();
    Ok(())
}

pub fn register(ctx: Context<Register>) -> Result<()> {
    let candidate = &mut ctx.accounts.candidate_data;

    candidate.votes = 0;
    candidate.pubkey = ctx.accounts.signer.key();
    candidate.id = ctx.accounts.candidate_identity.id;

    Ok(())
}

#[derive(Accounts)]
pub struct Apply<'info> {
    #[account(
        init,
        payer=signer,
        space=8+8+32,
        seeds=[
            b"candidate",
            signer.key().as_ref(),
            election_data.key().as_ref()
        ],
        bump
    )]
    pub candidate_identity: Account<'info,CandidateIdentity>,
    #[account(mut)]
    pub election_data: Account<'info,ElectionData>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info,System>
}

#[derive(Accounts)]
pub struct Register<'info> {
    #[account(
        init,
        payer=signer,
        space=8+8+8+32,
        seeds=[
            &(candidate_identity.id).to_be_bytes(),
            election_data.key().as_ref()
        ],
        bump
    )]
    pub candidate_data: Account<'info,CandidateData>,
    pub election_data: Account<'info,ElectionData>,
    pub candidate_identity: Account<'info,CandidateIdentity>,
    #[account(mut,address=candidate_identity.pubkey @ ElectionError::WrongPublicKey)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info,System>
}

#[account]
pub struct CandidateData {
    pub votes: u64,
    pub id: u64,
    pub pubkey: Pubkey,
}

#[account]
pub struct CandidateIdentity {
    pub id: u64,
    pub pubkey: Pubkey,
}
Enter fullscreen mode Exit fullscreen mode

Estamos usando um modelo de conta dupla, em que uma conta ’CandidateIdentity’ impõe que o candidato possa se inscrever apenas uma vez, enquanto a outra conta ’CandidateData’ mantém a contagem de votos.

Você pode estar se perguntando por que precisamos de duas contas. Embora uma única conta possa cumprir ambos os propósitos, se tornará extremamente difícil manter uma contagem de votos. As coisas ficarão mais claras quando você ler adiante.

Todo o modelo de inscrição do candidato funciona assim:

Cada candidato terá um ID exclusivo, que é a posição em que se inscreveram. Por exemplo, um candidato que se inscrever depois de seis candidatos terá um ID = 7.

Existem dois pontos de extremidade (endpoints) - apply() e register(). O candidato chamará primeiro o ponto de extremidade apply(), que incrementará o número de candidatos na ‘Conta de eleição’ por um e criará uma conta ’CandidateIdentity’ que tem dois campos - id e pubkey. O campo ’id’ é igual ao campo ’candidates’ da conta de eleição, ou seja, o número de candidatos inscritos até agora. O campo ‘pubkey’ grava a chave pública do candidato.

O endereço da conta ‘CandidateData’ é atribuído ao PDA, que é derivado da semente: o id do “candidato” + a chave pública do candidato + a chave pública da conta de eleição. Como o PDA só pode ser atribuído a uma única conta, a semente acima garante que o mesmo candidato não possa se inscrever novamente na mesma instância eleitoral.

Em seguida, o candidato chamará o ponto de extremidade register(), que criará uma nova conta chamada ’CandidateData’. Essa conta tem três campos - ’votes’ (votos), ‘id’ (identidade) e ‘pubkey’ (chave pública). Os campos ’id’ e ’pubkey’ são iguais aos da conta ‘CandidateIdentity’. O campo ’votes’ é um inteiro u64 que é inicialmente definido como 0.

O endereço da conta ’CandidateData’ é atribuído ao PDA derivado da semente: o id do candidato + a chave pública da conta eleitoral. Como um id é exclusivo para cada candidato, apenas uma conta pode ser associada a um candidato.

Pata votar em um candidato, o eleitor terá apenas o documento de identificação do candidato. O PDA da conta ’CandidateData’ pode ser gerado apenas com o ID. Essa é a razão pela qual estamos usando contas duplas. Na ausência de contas duplas, o eleitor precisaria da chave pública do candidato e o programa teria que salvar as chaves públicas no vetor winner_ids ao invés do ID, exigindo mais espaço. Além disso, escolher os vencedores se tornaria extremamente difícil e consumiria muita memória.

Outra pergunta que você pode fazer, por que estamos usando dois pontos de extremidade (endpoints)? A Solana exige que a transação inclua todas as contas que estão sendo lidas ou escritas. Não sabemos o ID do candidato antes de chamar o ponto de extremidade apply(), então não podemos gerar o PDA da conta ’CandidateData’ antes que a conta ‘CandidateIdentity’ seja criada. Portanto, o ponto de extremidade apply() primeiro cria a conta ‘CandidateIdentity’ cujos campos são usados para criar a conta ‘CandidateData’ no ponto de extremidade register().

Isso é tudo para a Parte II. Estamos usando dois novos erros personalizados - ApplicationIsClosed e WrongPublicKey. O primeiro é usado na declaração de requisição para garantir que os candidatos só possam se inscrever durante o estágio de inscrição. Enquanto o último é usado na declaração de validação da conta para garantir que apenas o usuário correto crie a conta ’CandidateData’. Certifique-se de adicionar esses dois erros personalizados à sua enumeração ElectionError.

Parte III - Mudança de estágio da eleição

O iniciador terá a autoridade de encerrar os estágios de inscrição/votação. Existem três estágios possíveis:

  1. Inscrição - este estágio é ativado imediatamente após a criação da eleição. Os candidatos podem se inscrever nesse estágio, mas não é permitido votar.
  2. Votação - este estágio é ativado imediatamente após o iniciador encerrar o estágio de inscrição. Os candidatos não podem mais se inscrever. Somente a votação pode acontecer durante esse estágio.
  3. Encerrado - este estágio é ativado imediatamente após o iniciador fechar o estágio de votação. Nenhuma inscrição ou votação pode acontecer. A conta da eleição se tornará imutável e nenhuma outra alteração poderá ser feita. Os IDs no campo ’Winners_id’ da conta de eleição são os vencedores finais.

Usaremos um único ponto de extremidade para lidar com todas as mudanças de estágio. Vamos dar uma olhada no código primeiro:

pub fn change_stage(ctx: Context<ChangeStage>,new_stage: ElectionStage) -> Result<()> {
    let election = &mut ctx.accounts.election_data;

    require!(election.stage != ElectionStage::Closed,ElectionError::ElectionIsClosed);

    match new_stage {
        ElectionStage::Voting => {
            return election.close_application();
        },
        ElectionStage::Closed => {
            return election.close_voting();
        },
        ElectionStage::Application => {
            return Err(ElectionError::PrivilegeNotAllowed.into());
        }
    }
}

#[derive(Accounts)]
pub struct ChangeStage<'info> {
    #[account(mut)]
    pub election_data: Account<'info,ElectionData>,
    #[account(mut,address=election_data.initiator @ ElectionError::PrivilegeNotAllowed)]
    pub signer: Signer<'info>
}

impl ElectionData {
    pub fn close_application(&mut self) -> Result<()> {
        require!(self.stage == ElectionStage::Application,ElectionError::ApplicationIsClosed);

        if self.candidates <= self.winners_num as u64 {
            for i in 1..self.candidates + 1 {
                self.winners_id.push(i);
                self.stage = ElectionStage::Closed;
            }
        } else {
            self.stage = ElectionStage::Voting;
        }
        Ok(())
    }

    pub fn close_voting(&mut self) -> Result<()> {
        require!(self.stage == ElectionStage::Voting,ElectionError::NotAtVotingStage);
        self.stage = ElectionStage::Closed;
        Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

Em primeiro lugar, estamos validando se o signatário da transação é o ’initiator’ (iniciador). Caso contrário, o programa retornará um erro personalizado: PrivilegeNotAllowed.

A seguir, na lógica do programa, verificamos se a eleição está no estágio ‘closed’ (encerrado). Se estiver encerrado, não permitimos que ninguém faça mais alterações na conta. Portanto, o programa retorna um erro personalizado ’ElectionIsClosed’.

Em seguida, uma declaração de correspondência lida com o argumento ’new_stage’. O argumento ’new_stage’ é do tipo de enumeração de ElectionStage e pode ter três valores - inscrição, votação e encerrado. Não queremos que o iniciador reverta as eleições para o estágio de inscrição. Portanto, o programa retorna um erro PrivilegeNotAllowed se o iniciador tentar mudar o estágio para ‘inscrição’.

Os casos de ‘votação’ e ‘encerrado’ da declaração de correspondência retornam os métodos na estrutura ‘conta de eleição’. Como não estamos criando nenhuma conta nesta parte, é melhor lidar com a mudança através de métodos.

Para o caso de ‘votação’, o ponto de extremidade chama o método close_application(). Primeiro, o método verifica se a eleição está no estágio de inscrição. Caso contrário, o programa retornará um erro ApplicationIsClosed. Se a eleição estiver no estágio de inscrição, o método irá ainda verificar se o número de candidatos é maior que o ’winner_num’. Se não for, então não faz sentido ir para a votação. Assim, o método mudará diretamente o estágio para ‘encerrado’ e colocará os IDs de todos os candidatos no vetor ’Winners_id’.

Para o caso 'encerrado', o ponto de extremidade chama o método close_voting(). Esse método verificará primeiro se a eleição está no estágio de votação. Se não estiver, ele retorna um erro NotAtVoting. Caso contrário, o método mudará o estágio para ‘ElectionStage::Closed’.

Parte IV - Criação da conta de voto (parte final do quebra-cabeças)

A seguir, criaremos o ponto de extremidade de voto. O código dessa seção é:

pub fn vote(ctx: Context<Vote>) -> Result<()> {
    let election = &mut ctx.accounts.election_data;

    require!(election.stage == ElectionStage::Voting,ElectionError::NotAtVotingStage);

    let candidate = &mut ctx.accounts.candidate_data;
    let my_vote = &mut ctx.accounts.my_vote;

    candidate.votes += 1;
    my_vote.id = candidate.id;

    Ok(())
}

#[derive(Accounts)]
pub struct Vote<'info> {
    #[account(
        init,
        payer=signer,
        space=8+8,
        seeds=[
            b"voter",
            signer.key().as_ref(),
            election_data.key().as_ref()
        ],
        bump
    )]
    pub my_vote: Account<'info,MyVote>,
    #[account(mut)]
    pub candidate_data: Account<'info,CandidateData>,
    #[account(mut)]
    pub signer: Signer<'info>,
    #[account(mut)]
    pub election_data: Account<'info,ElectionData>,
    pub system_program: Program<'info,System>
}

#[account]
pub struct MyVote {
    pub id: u64,
}
Enter fullscreen mode Exit fullscreen mode

A lógica do programa começa com a verificação do estágio da eleição. Se não estiver no estágio de votação, ele retornará um erro ’NotAtVotingStage’.

Em seguida, estamos criando uma nova conta ’MyVote’ com um único campo ’id’, que salva o ID do candidato votado. O endereço dessa conta é atribuído a um PDA derivado da seguinte semente:

literal do “eleitor” + a chave pública do eleitor + a chave pública da conta de eleição

Esse PDA garante que o eleitor pode votar apenas uma vez. Você pode desejar permitir que os eleitores votem em múltiplos candidatos. Se esse é o caso, use a seguinte semente:

literal do “eleitor” + a chave pública do eleitor + PDA da conta candidate_data

A semente acima garante que o eleitor pode dar votos a mais de um candidato, mas apenas um voto por candidato.

Com isso, completamos toda a estrutura de eleição. Criamos a conta da eleição, a conta do candidato e a conta do voto. Além disso, criamos um ponto de extremidade (endpoint) para alterar o estágio da eleição. Resta apenas a declaração dos vencedores.

Se você se lembra da Parte I, adicionamos dois campos - ’winners_id’ e ’winners_votes’ na conta da eleição. Esses campos são os vetores, que estão vazios por enquanto. O objetivo é preencher esses campos com os RGs e os votos dos vencedores após as eleições. Atualmente, os votos são guardados em uma conta separada para cada candidato e não têm relação direta com a conta de eleição. Portanto, não é possível escolher os vencedores sem passar pelas contas ’CandidateData’ de todos os candidatos em uma declaração. Mas o número de candidatos pode ser muito grande e só podemos enviar no máximo 1232 bytes de dados em uma transação. Portanto, precisamos de uma solução alternativa e consegui isso usando o seguinte algoritmo:

pub fn vote(ctx: Context<Vote>) -> Result<()> {
    ....
    election.record_vote(candidate.id,candidate.votes);
    Ok(())
}

impl ElectionData {
    pub fn record_vote(&mut self,id: u64,votes: u64) {
        if !self.winners_id.contains(&id) {
            if self.winners_id.len() < self.winners_num as usize {
                self.winners_id.push(id);
                self.winners_votes.push(votes);            
            } else {
                let current_last_winner = (self.winners_num - 1) as usize;

                if votes > self.winners_votes[current_last_winner] {
                    self.winners_id[current_last_winner] = id;
                    self.winners_votes[current_last_winner] = votes;
                } else {
                    return;
                }
            }
        } else {
            let index = self.winners_id.iter().position(|&r| r == id).unwrap();
            self.winners_votes[index] += 1;
        }

        //sorting votes in descending order if winners' votes are changed
        let mut j = self.winners_id.iter().position(|&r| r == id).unwrap();

        while j > 0 && self.winners_votes[j] > self.winners_votes[j-1] {

            let vote_holder = self.winners_votes[j-1];
            let id_holder = self.winners_id[j-1];

            self.winners_votes[j-1] = self.winners_votes[j];
            self.winners_votes[j] = vote_holder;

            self.winners_id[j-1] = self.winners_id[j];
            self.winners_id[j] = id_holder;

            j -= 1;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Criamos outro método na estrutura ElectionData - _’record_vote’. Ele leva dois argumentos - id e votos. Em seguida, colocamos esse método por último no ponto de extremidade de voto e passamos o ID e os votos do candidato votado para ele.

Esse método atualizará os vetores ’winners_id’ e ’winners_votes’ após cada votação. Se o ID já existir em ’winners_id’, ele incrementará os votos adjacentes em ‘winners_votes’ em 1. Caso contrário, compararmos seus votos com os votos mais baixos em ’winners_votes’. Se o candidato tiver mais votos, colocaremos seu ID e votos nos vetores.

Vamos dar um exemplo para entender melhor: suponha que cinco candidatos - A, B, C, D e E - se inscreveram para a eleição e queremos selecionar dois vencedores entre eles. O primeiro voto é lançado para A. O algoritmo acima colocará A no ’winners_id’ e 1 no ’winners_votes’. O segundo voto é lançado para B. Da mesma forma, o algoritmo irá forçar B para o ’winners_id’ e 1 para o ’winner_votes’. O terceiro voto é lançado para C, mas nós já temos 2 candidatos no vetor ’winners_id’ com votos iguais, então nenhuma mudança será feita em ambos os vetores. O quarto voto é lançado para C novamente. C agora tem 2 votos, portanto, nosso algoritmo irá compará-lo com o menor número de votos no ’winner_votes’, que é 1. Como C tem mais votos, o algoritmo substituirá B por C no ’winners_id’. Então, o vetor classifica e A, que tem menos votos, será deslocado para o final do ’winners_id’.

O algoritmo é executado após cada voto, então o ’winners_id’ mostra os líderes de votação em tempo real. Quando o iniciador muda o estágio para ‘encerrado’, o ’winners_id’ não pode ser alterado e os líderes se tornarão os vencedores finais.

O tutorial termina aqui. Você pode encontrar o código completo junto com os testes aqui no GitHub.

Você pode fazer outras melhorias no programa se quiser: atualmente, qualquer pessoa pode se inscrever e votar nas eleições. Você pode querer manter o acesso à comunidade específica com base na retenção de tokens ou na lista de permissões (whitelisting) de endereços.

Obrigado por ler! Se você encontrar algum bug ou melhorias no programa, por favor, escreva para mim. Você pode me encontrar no Twitter: 0xShuk.

Esse artigo foi escrito por 0xShuk e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Latest comments (0)