Transferencia de saldos
A transferência de saldo* precisa, necessariamente, de um entendimento de como a blockchain funciona e de como essas transferências utilizam matemática segura para realizar as operações.
Tive alguns problemas com o código em si. Acho que posso ter me perdido em algum momento, mas acredito que o código abaixo esteja correto.
Como mencionei no arquivo index dessas anotações, estou aprendendo Rust e tendo meu primeiro contato com a linguagem. Este Zettelkasten tem o objetivo de mostrar meu aprendizado e armazenar as notas, e não apenas repostar o tutorial da Web3Devs.
Exercícios
- Crie uma função de transferência segura e simples no seu Pallet de Saldos.
- Crie um teste mostrando que tudo está funcionando conforme o esperado, incluindo o tratamento de erros.
No balances.rs
:
impl Pallet {
/// ... código anterior.
/// Transfere `amount` de uma conta para outra.
/// Esta função verifica se `caller` tem pelo menos `amount` de saldo para transferir,
/// e se não ocorrem overflow/underflow matemáticos.
pub fn transfer(
&mut self,
caller: String,
to: String,
amount: u128,
) -> Result<(), &'static str> {
/* TODO:
- Obter o saldo da conta `caller`.
- Obter o saldo da conta `to`.
- Usar matemática segura para calcular um `new_caller_balance`.
- Usar matemática segura para calcular um `new_to_balance`.
- Inserir o novo saldo de `caller`.
- Inserir o novo saldo de `to`.
*/
Ok(())
}
}
Tambem no balances.rs
:
mod tests {
/// ... código anterior.
#[test]
fn transfer_balance() {
/* TODO: Crie um teste que verifique o seguinte:
- Que `alice` não pode transferir fundos que ela não possui.
- Que `alice` pode transferir fundos para `bob` com sucesso.
- Que o saldo de `alice` e `bob` esteja atualizado corretamente.
*/
}
}
Meu codigo pessoalmente ficou da seguinte forma:
A função transfer
é o coração do sistema de transferências dentro dessa simulação de blockchain. Ela usa várias
características do Rust, como controle de erros, manipulação segura de valores e imutabilidade, para garantir que as
transferências de saldo entre contas sejam feitas corretamente e sem riscos de bugs comuns, como estouro de valores
(overflow
) ou subtração abaixo de zero. Vou detalhar como essa função explora o Rust para garantir uma implementação
eficiente e segura.
Estrutura da Função transfer
A função transfer
recebe três parâmetros:
caller
: A conta que está enviando o saldo.to
: A conta que está recebendo o saldo.amount
: O valor a ser transferido.
A lógica da função verifica se o saldo do caller
é suficiente e atualiza os saldos de ambas as contas, enquanto evita
erros de overflow e underflow.
Assinatura da Função
pub fn transfer(
&mut self,
caller: String,
to: String,
amount: u128,
) -> Result<(), &'static str>
&mut self
: A função modifica o estado interno doPallet
, ou seja, os saldos das contas. Por isso, recebe uma referência mutável paraself
.- Retorno: Usa o tipo
Result<(), &'static str>
, que é uma maneira comum em Rust de indicar sucesso (Ok(())
) ou erro (Err
), com uma mensagem de erro associada.
Passos da Função
1. Obtenção dos Saldos das Contas
Primeiro, a função obtém os saldos das contas de caller
e to
usando o método get_balance
, que retorna 0
se a
conta ainda não existir no mapa.
let caller_balance = self.get_balance(caller.clone());
let to_balance = self.get_balance(to.clone());
O uso de clone()
é necessário aqui porque caller
e to
são strings, e strings em Rust não implementam a trait
Copy
(não são tipos de dados triviais como inteiros), então precisamos cloná-los para que os valores possam ser
reutilizados mais tarde.
2. Verificação de Saldos com Segurança
Aqui usamos duas verificações importantes:
- Subtração segura (
checked_sub
): A função verifica se o saldo docaller
é suficiente para cobrir o valor da transferência. Se a subtração resultar em um valor negativo, a função retorna um erro de "Saldo Insuficiente". - Adição segura (
checked_add
): A função verifica se o saldo da conta receptora pode ser atualizado sem ocorrer overflow (estouro de capacidade). Se a adição resultar em um valor maior do que ou128
pode armazenar, retorna um erro de "Overflow".
let new_caller_balance = caller_balance.checked_sub(amount).ok_or("Insufficient balance")?;
let new_to_balance = to_balance.checked_add(amount).ok_or("Overflow")?;
Esses métodos (checked_sub
e checked_add
) são funções embutidas nos tipos numéricos do Rust que retornam uma
Option
. Se a operação for bem-sucedida, retorna Some(resultado)
. Se falhar (por exemplo, por underflow ou overflow),
retorna None
. Usando a função ok_or()
, podemos transformar o None
em um erro com uma mensagem específica.
3. Atualização dos Saldos
Se as verificações forem bem-sucedidas, a função atualiza o saldo de caller
e to
no BTreeMap
.
self.balances.insert(caller, new_caller_balance);
self.balances.insert(to, new_to_balance);
Aqui, estamos atualizando o mapa de saldos inserindo os novos valores calculados.
4. Retorno
Se tudo der certo, a função retorna Ok(())
para indicar que a transferência foi realizada com sucesso.
Ok(())
Exploração do Rust na Função transfer
-
Segurança de Subtração e Adição: A utilização das funções
checked_sub
echecked_add
é uma boa prática em Rust, pois evita problemas de overflow e underflow. Esses tipos de verificações automáticas garantem que o código seja mais seguro, sem a necessidade de tratamentos de erros manuais complexos. -
Sistema de Tipagem Rigoroso: Rust tem um sistema de tipos forte e rígido. Usar
u128
para os saldos garante que possamos representar grandes valores (até 340 undecilhões) sem nos preocuparmos com o estouro, a não ser que isso seja explicitamente verificado comchecked_add
echecked_sub
. -
Gerenciamento de Erros com
Result
: O uso deResult
permite que a função expresse claramente os possíveis resultados: sucesso (Ok
) ou erro (Err
). Em Rust, isso evita o uso de exceções e torna o fluxo de controle mais explícito e previsível. -
Imutabilidade por Padrão: Em Rust, as variáveis são imutáveis por padrão. O uso de
&mut self
garante que a função possa modificar o estado interno, mas esse comportamento é explícito. Sem a mutabilidade, a função não conseguiria alterar os saldos noBTreeMap
.
Testes
Os testes garantem que a função transfer
funcione conforme esperado.
1. Teste de Transferência Simples
#[test]
fn transfer_balance() {
let mut balances = Pallet::new();
balances.set_balance("Yan".to_string(), 20);
assert_eq!(
balances.transfer("Yan".to_string(), "Bruna".to_string(), 10),
Ok(())
);
assert_eq!(balances.get_balance("Yan".to_string()), 10);
assert_eq!(balances.get_balance("Bruna".to_string()), 10);
}
Aqui, o teste verifica:
- Se a função
transfer
retornaOk(())
quando a transferência é bem-sucedida. - Se o saldo do
caller
(Yan) foi devidamente reduzido. - Se o saldo do
to
(Bruna) foi atualizado corretamente.
2. Testes de Condições de Erro
Podemos adicionar testes para garantir que os erros sejam tratados adequadamente, como quando a conta caller
não tem
saldo suficiente ou quando ocorre um overflow.
#[test]
fn transfer_insufficient_balance() {
let mut balances = Pallet::new();
balances.set_balance("Yan".to_string(), 5);
assert_eq!(
balances.transfer("Yan".to_string(), "Bruna".to_string(), 10),
Err("Insufficient balance")
);
}
#[test]
fn transfer_overflow() {
let mut balances = Pallet::new();
balances.set_balance("Yan".to_string(), u128::MAX);
balances.set_balance("Bruna".to_string(), u128::MAX);
assert_eq!(
balances.transfer("Yan".to_string(), "Bruna".to_string(), 1),
Err("Overflow")
);
}
Conclusão
A função transfer
é uma ótima demonstração das capacidades do Rust em lidar com problemas comuns de sistemas
financeiros, como overflow, underflow e gestão de erros, de forma segura e eficiente. O sistema de tipos do Rust, junto
com suas funções de verificação seguras (checked_sub
, checked_add
), tornam a implementação robusta e menos propensa
a bugs. O uso de Result
para manipulação de erros também torna o código mais fácil de seguir e testar.