Conteúdo no Blog: Tutorial Marlowe 3.0 (Parte 11) – Educação Cardano
11. Potenciais problemas com contratos
A linguagem Marlowe foi projetada para ter o mínimo possível de armadilhas e truques, para que os contratos possam ser escritos intuitivamente, evitando surpresas. No entanto, é impossível, por design, excluir todos os contratos que não devem ser escritos, sem tornar o Marlowe muito mais difícil de usar. Além disso, mesmo quando um contrato é bem escrito, ainda é possível que seus usuários interajam com ele de formas inválidas, emitindo transações inválidas.
Em todos os casos, quando esses efeitos indesejados acontecem, o Marlowe é projetado para se comportar da maneira mais intuitiva e conservadora possível. No entanto, vale a pena conhecer esses possíveis problemas e revisar como Marlowe se comporta nessas situações. Esse é o assunto deste tutorial.
11.1 Advertências
Os avisos de Marlowe são indicações de que um contrato foi escrito incorretamente. Um contrato bem escrito nunca deve emitir um aviso, não importa como os usuários interajam com ele. Idealmente, gostaríamos de proibir que contratos que possam emitir avisos sejam sempre escritos, mas isso exigiria que os contratos da Marlowe fossem digitados com dependência, e escrever expressões com digitação dependente é muito mais complicado.
Em vez disso, Marlowe permite que contratos que emitem avisos sejam escritos, e fornecemos ferramentas de análise estática que permitem que os desenvolvedores de contratos verifiquem se um contrato específico pode emitir avisos. Além disso, fornecemos comportamentos de reserva para quando um contrato produz um aviso, apesar de nossos conselhos. Nós fornecemos comportamentos de reserva porque reconhecemos que a análise de grandes contratos pode ser muito computacionalmente cara e porque erros podem ser cometidos. Queremos que contratos mal escritos falhem da maneira mais inofensiva possível, isto é, conservadoramente.
Pagamentos não positivos
Quando um contrato deve pagar uma quantia em dinheiro que seja menor que uma unidade de uma moeda ou token, ele emitirá um aviso NonPositivePay e não transferirá nenhum dinheiro.
Os pagamentos negativos devem ser implementados como depósitos positivos (ao pagar um participante) ou pagamentos positivos na direção oposta (ao pagar entre contas).
Depósitos não positivos
Quando um contrato deve esperar uma quantia em dinheiro que seja menor que uma unidade de uma moeda de token, ele ainda aguardará uma transação IDeposit, mas essa transação não precisa transferir nenhum dinheiro para o contrato e nenhum dinheiro é transferido para o participante que emite a transação. Depois que esse depósito “falso” for bem-sucedido, o contrato emitirá um aviso NonPositiveDeposit.
Depósitos negativos sempre devem ser implementados como pagamentos positivos.
Pagamento parcial
Quando um contrato deve pagar uma quantia maior que a quantia existente na conta de origem, ele transferirá apenas o que estiver disponível nessa conta, mesmo que haja dinheiro suficiente em todas as contas da conta de origem. contrato e emitirá um aviso PartialPay.
Pagamentos parciais devem ser evitados porque um contrato que nunca produz um pagamento parcial é um contrato explícito. Contratos explícitos garantem a seus usuários que eles serão executáveis e que, em qualquer parte do contrato, diz que um pagamento ocorrerá, isso realmente acontecerá.
Sombreamento Let (não coberto pela análise estática)
Quando um contrato atinge uma construção Let que redefine um valor com um identificador que já foi definido por uma Let externa, o contrato emitirá um aviso de Shadowing (Sombreamento) e substituirá a definição anterior.
O sombreamento é uma prática ruim de programação porque leva à confusão. O uso do mesmo identificador para mais de uma coisa pode induzir os desenvolvedores ou usuários a pensarem que um uso de Use será avaliado em uma quantidade, enquanto na verdade será avaliado em outra quantidade.
11.2 Cheiros ruins
Existem outros “maus cheiros” que indicam que um contrato provavelmente foi mal projetado.
Esses contratos são válidos, no sentido de que não causam necessariamente avisos e fazem o que dizem que fazem, mas têm características que sugerem que o desenvolvedor do contrato não estava totalmente ciente das consequências do contrato, ou que o desenvolvedor propositalmente escreveu o contrato de uma maneira que foi confusa para o leitor.
Uso indefinido do Let (deve ser um aviso)
Quando uma construção Use usa um identificador que ainda não foi definido, ele será avaliado como o valor padrão de 0. Nenhum aviso será emitido, mas, novamente, essa é uma prática ruim, pois pode ser enganosa. (Constant 0) deve ser usado, pois torna explícita a quantidade em questão.
Partes inacessíveis de um contrato
Este é o principal mau cheiro nos contratos da Marlowe. Se parte do contrato é inacessível, por que teria sido incluído em primeiro lugar?
Esse mau cheiro assume várias formas.
O subcontrato não está acessível
Por exemplo:
O contrato anterior é equivalente ao contract2. Em geral, você nunca deve usar FalseObs e deve usar TrueObs apenas como a observação raiz de uma construção Case.
A Observation é sempre curta
Por exemplo:
A observação anterior é equivalente à observation1. Novamente, você só deve usar TrueObs como a observação raiz de uma construção Case.
Ramo When está inacessível
Por exemplo:
contract2 é inacessível, todo o Case pode ser removido do contrato e o comportamento seria o mesmo.
O Value nunca é avaliado
Por exemplo:
value1 nunca será avaliado. Mas, neste caso, não temos escolha, a não ser escrever algo no valor padrão de ChoiceValue
Nas versões futuras do Marlowe, podemos remover o valor padrão para ChoiceValue e introduzir uma construção Value especial para esse tipo de situação.
Tempo limite aninhado sem aumento
Por exemplo:
contract1 está inacessível: após o bloco 10, o contrato evoluirá diretamente para contract2. O When interior não faz nenhuma diferença para o contrato.
11.3 Problemas de usabilidade
Mesmo que um contrato evite avisos e não tenha um código inacessível, ele ainda poderá permitir que usuários mal-intencionados forçam outros usuários a situações indesejáveis que não foram originalmente planejadas pelo desenvolvedor do contrato.
Momento incorreto das construções When
Considere o seguinte contrato:
Em princípio, não há nada de errado com este contrato, mas se (Papel “alice”) fizer sua escolha no bloco 9, será praticamente impossível para bob fazer sua escolha no prazo e receber o reembolso do dinheiro em sua conta (AccountId 1 (Papel “bob”)). A menos que isso faça parte de um jogo e que seja um efeito pretendido, é provável que seja um contrato injusto para (Papel “bob”).
Em geral, é uma boa prática garantir que as construções When tenham tempos limite crescentes e que o aumento entre os tempos limite seja razoável para as diferentes partes emitirem e conseguirem que suas transações sejam aceitas pelo blockchain. Há muitas razões pelas quais a participação de uma parte pode ser adiada: uma falha no fornecimento de energia, um pico repentino no número de transações pendentes no blockchain, ataques à rede etc. Por isso, é importante permitir bastante tempo e estar generoso com tempos limite e com aumentos nos tempos limites.
11.4 Erros
Finalmente, mesmo que um contrato esteja perfeitamente escrito. Os usuários podem usá-lo incorretamente e chamamos esses erros de uso incorreto.
Em todos os casos, sempre que uma transação causar um erro, a transação não terá efeito no Contract ou em seu State. De fato, a carteira de um usuário saberá com antecedência se uma transação produzirá um erro, porque as transações são determinísticas, portanto, os usuários nunca precisarão enviar uma transação incorreta para o blockchain.
Intervalo ambíguo
Quando uma transação atinge um tempo limite, seu intervalo de slot deve ser inequívoco sobre se o tempo limite passou ou não. Por exemplo, se o limite máximo de When de um contrato tiver tempo limite 10 e uma transação com intervalo de espaço [6, 14] for emitida, a transação causará um erro AmbiguousSlotIntervalError, porque é impossível saber se o tempo limite passou apenas olhando para a transação. Para evitar isso, a transação deve ser dividida em duas transações separadas:
Um com intervalo de slot [6, 9].
Outro com intervalo de fenda [10, 14].
Aplicar sem correspondência
Se uma transação não fornecer as entradas esperadas pelo Contract, o contrato emitirá um erro NoMatchError e a transação inteira será descartada.
Transação inútil
Se uma transação não afetar o Contract ou o State, isso resultará em um erro Transaction Useless e a transação inteira será descartada. A razão pela qual descartamos transações inúteis é que elas abrem a porta para ataques de negação de serviço (DoS), porque um invasor em potencial pode inundar o contrato com transações desnecessárias e impedir transações necessárias de ingressar no blockchain.