Contenção de atualização de estado
Os contratos inteligentes do Bitcoin armazenam estados nas saídas de uma cadeia de transações. A transição de estado ocorre quando uma transação gasta a saída contendo o estado antigo e cria uma saída contendo o novo estado. A qualquer momento, uma única saída/UTXO na ponta da cadeia de transações tem o estado mais recente.
Um problema surge quando várias transações competem para atualizar o estado compartilhado ao mesmo tempo. Para ver o porquê, vamos considerar um simples contrato inteligente de token ERC-20 com o seguinte estado¹:
Figura 1: estado de um contrato de token fungível
O estado registra quantos tokens cada usuário possui. Suponha que Alice envie uma transação para transferir 5 tokens para Charlie, atualizando o estado para state3A, enquanto Bob envia outra transação para transferir 10 tokens para Dave simultaneamente, atualizando-o para state3B. Uma das transações falhará porque estão gastando duas vezes um único UTXO contendo state2.
Suponha que a transação de Bob falhe. Ele precisa criar uma nova transação gastando o novo UTXO state3A, em vez de state2, e tentar novamente. Não é difícil ver quando há muitos usuários tentando atualizar ao mesmo tempo, pode levar muitas tentativas para que a transação de atualização seja bem-sucedida, causando atrasos imprevisíveis e degradando a experiência do usuário.
Para evitar a contenção, uma abordagem ingênua é enviar todas as transações de atualização para um coordenador intermediário, chamado sequenciador, que as ordena e as transmite para a blockchain.
Infelizmente, essa abordagem não funciona porque as transações em lote podem gastar um único UTXO, como fazem as transações de Alice e Bob na Fig. 2. Quando o sequenciador os reordena sequencialmente em uma cadeia para evitar gastos duplos como na Fig. 3, a assinatura na transação original de Bob, state3B, torna-se invalidada. Se o sequenciador precisar pedir a um usuário que assine novamente toda vez que uma transação for reordenada, haverá um atraso imprevisível novamente e voltaremos à estaca zero.
Assinaturas de pré-autorização
As assinaturas são usadas para pré-autorizar atualizações de estado. Precisamos de uma maneira de assinar cada transação, que não seria invalidada mesmo que o sequenciador a reordene alterando sua entrada. Nenhum dos sinalizadores SIGHASH permite isso.
Inspirados em nosso trabalho anterior de emular SIGHASH_NOINPUT, que exclui da assinatura a entrada que está sendo gasta, assinamos apenas uma mensagem contendo detalhes específicos sobre a ação que está sendo autorizada. A assinatura é verificada usando nossa biblioteca ECDSA na Linha 17.
Em nosso contrato de token, assinamos apenas o destinatário e o valor. Por exemplo, Alice assinaria para autorizar a transferência de 5 tokens para Charlie.
+ import "ec.scrypt";
// um token fungível básico do tipo ERC20
contract ERC20 {
PubKey minter;
@state
HashedMap<PubKey, int> balances;
//transfere tokens do remetente para o destinatário
public function transferFrom(PubKey sender, PubKey receiver, int amount, Sig senderSig, int senderBalance, int senderKeyIndex, int receiverBalance, int receiverKeyIndex, SigHashPreimage preimage) {
// autentica
- require(checkSig(senderSig, sender));
+ bytes msg = receiver + pack(amount);
+ // verifica a assinatura em relação à nova mensagem
+ require(EC.verifySig(msg, sig, sender));
Com a assinatura de Alice, qualquer pessoa, inclusive o sequenciador, pode criar uma transação para transferir 5 tokens de Alice para Charlie. Um atacante não pode roubar redirecionando a transferência para ele ou alterando o valor da transferência, o que invalidaria a assinatura de Alice.
Nesta abordagem, o sequenciador pode atualizar o estado tão rápido quanto ele pode criar transações, que podem ser milhões de transações por segundo usando hardware comum. Ele não está mais bloqueado esperando que os usuários assinem novamente as transações reordenadas.
Além disso, embora a criação de transações seja sequencial off chain, o que é super fácil e rápido, os contratos inteligentes neles podem ser processados em paralelo pelos mineradores, graças ao modelo UTXO do Bitcoin.
Ataques de repetição
Observe que os tokens podem ser transferidos desde que uma assinatura válida seja fornecida. Não há nada na mensagem assinada de Alice que possa impedir que a mesma assinatura seja usada repetidamente.
Bob (na verdade qualquer um) poderia reutilizar a mesma assinatura e enviar para si mesmo outros 5 tokens de Alice. Ele pode até repetir isso muitas vezes até que o saldo de Alice se esgote.
Para combater o ataque de repetição, podemos usar um nonce no nível do aplicativo. “Nonce” é uma abreviação de “número usado uma vez” em criptografia. Podemos usar um nonce para cada assinatura e armazenar o próximo nonce dentro do contrato.
+ struct Value {
+ int balance;
+ int nonce;
+ }
// um token fungível básico do tipo ERC20
contract ERC20 {
PubKey minter;
@state
- HashedMap<PubKey, int> balances;
+ HashedMap<PubKey, Value> balances;
// transfere tokens do remetente para o destinatário
public function transferFrom(PubKey sender, PubKey receiver, int amount, Sig senderSig, int senderBalance, int senderNonce, int senderKeyIndex, int receiverBalance, int receiverKeyIndex, SigHashPreimage preimage) {
// autentica
- bytes msg = receiver + pack(amount);
+ bytes msg = receiver + pack(amount) + pack(senderNonce);
// verifica a assinatura em relação à nova mensagem
require(EC.verifySig(msg, sig, sender));
- require(this.balances.canGet(sender, senderBalance, sendererKeyIndex));
+ require(this.balances.canGet(sender, {senderBalance, senderNonce}, sendererKeyIndex));
require(senderBalance >= amount);
- require(this.balances.set(sender, senderBalance - amount, senderKeyIndex));
+ require(this.balances.set(sender, {senderBalance - amount, ++senderNonce}, senderKeyIndex));
Se dois contratos usam a mesma codificação de mensagens (por exemplo, há outro contrato de token fungível), uma assinatura usada por um contrato também pode ser válida para o outro, mesmo com nonce. Precisamos de algumas informações de identificação sobre o contrato para evitar esse tipo de ataque de repetição.
Nós estipulamos que nenhuma chave/endereço público usado em um contrato com estado pode ser reutilizado em outros contratos. Isso é consistente com a prática padrão de gerar um novo endereço para cada nova transação de bitcoin.
Resistente à censura
Se um sequenciador censura a transação de um usuário, o usuário sempre pode enviá-la diretamente ao contrato com estado na cadeia (on chain).
Além disso, pode haver vários sequenciadores para um determinado contrato com estado, um usuário pode submeter a sequenciadores alternativos se algum se recusar a processar sua transação. Esses sequenciadores podem ser coordenados em uma rede de sobreposição fora da rede do minerador. Técnicas de agendamento padrão, como round-robin, podem ser empregadas para resolver a disputa entre eles ao acessar o estado mais recente.
[1] Um estado pode ser compactado armazenando cada entrada da tabela em uma árvore Merkle e armazenando apenas a raiz da árvore como o estado no contrato inteligente.
Esse artigo é uma tradução de sCrypt feita por @bananlabs. Você pode encontrar o artigo original aqui
Oldest comments (0)