“Security Through Clarity” and Why Programming Languages and Architecture Matter

Decentralized Applications (DApps) atop public blockchains are the very hardest programs to write and audit: they run in an adversarial irreversible public environment; one bug, and someone may lose his shirt, with no recourse. How can we affordably write DApps that can be affordably audited, and reasonably trusted? By making their meaning so clear that misunderstanding becomes harder than understanding. We will use a simple DApp to illustrate how our Domain-Specific Language, Glow https://glow-lang.org/, we can help developers achieve clarity. Clarity can solve a lot of problems for software security and beyond. But achieving clarity is no easy feat. Clarity requires a simplicity that cannot happen by accident. Clarity requires having carefully identified the concepts that do matter, and systematically eliminated those that don’t. Clarity requires using Domain-Specific Languages that embody this selection of concepts and non-concepts.

Spot the bug!

Let’s play a game of “Spot the Bug”, using the simplest possible DApp.

The simplest possible DApp: closing a sale.

Let’s consider the simplest possible Decentralized Application (DApp): the closing of a sale. The intent of the DApp is as follows: a Buyer and a Seller have agreed to the terms of a sale. The Seller will sign a title transfer, and the Buyer will pay a price for it in tokens. The title transfer may be an electronic copy of a legal document, a transaction on another blockchain, a keycode for a hotel room, etc.

Because they want the transaction to be trustless, the two participants use a blockchain smart contract to ensure that their interaction will be “atomic”, i.e. all or nothing: either they both cooperate and the transaction happens, or one fails to cooperate and then nothing bad happens to the other participant except wasting a bit of time and a small amount of gas.

The sequence diagram for a successful interaction will be as follows:

How can we implement this DApp with standard development tools, and what kind of errors must we guard against?

Bug in a smart contract

Here is a simple smart contract for the closing, written in Solidity, today’s most used language for that purpose. Can you spot any bugs?

Note how it’s only 17-line after compressing things a bit, but would be more like 35 lines when passed through the prettier program to comply with common style guidelines.

pragma solidity ^0.8.2; // SPDX-License-Identifier: Apache2.0
contract ClosingBug { // Can you identify the bugs in this contract?
  address payable Buyer;  address payable Seller;
bytes32 digest; uint price;
constructor(address payable _Buyer, address payable _Seller,
bytes32 _digest, uint _price) payable {
Buyer = _Buyer; Seller = _Seller;
digest = _digest; price = _price;
require(msg.value == price);
}
event SignaturePublished(uint8 v, bytes32 r, bytes32 s);
function sign(uint8 v, bytes32 r, bytes32 s) public payable {
require(Seller == ecrecover(digest, v, r, s));
emit SignaturePublished(v, r, s);
selfdestruct(payable(msg.sender));
}
}

Bug #2 (This one is more subtle): The contract releases the money to the msg.sender, not the Seller, so anyone can watch the seller’s signing message, change the msg.sender on it to themselves, re-post the same signature with more GAS to get the funds (or mine it themselves), and thereby front-run the Seller’s transaction.

Note how it’s only 17-line after compressing things a bit, but would be more like 35 lines when passed through the prettier program to comply with common style guidelines.

Fixing the se two bugs result in a 24-line contract. Assuming of course you can spot the bugs. That would be 47 lines after pretty-printing. Writing the “same” contract in a straightforward way without cleverness or optimizations would take 69 lines once pretty-printed.

Bug in smart contract client

Here is a 17-line JavaScript client for the Buyer side on the same smart contract. Can you spot any bug?

async function Closing__Buyer (timeoutInBlocks, Buyer, Seller, digest, price) {
    const contract = new web3.eth.Contract(Closing.abi);
    let txHash;
    const contractInstance = await contract
        .deploy({data: Closing.bin, arguments: [timeoutInBlocks, Buyer, Seller, digest, price]})
        .send({from: Buyer}, (err, transactionHash) => {txHash = transactionHash;})
        .on("confirmation");
    const receipt = await web3.eth.getTransactionReceipt(txHash);
    await notify_other_user(Seller, ["Closing__Buyer", 1, receipt,
                                     [timeoutInBlocks, Buyer, Seller, digest, price]]);
    const address = receipt.contractAddress;
    const deadline = receipt.blockNumber + timeoutInBlocks;
    const event = await contractInstance.once("logs", {toBlock: deadline});
    const rv = event.returnValues;
    assert (check_signature(Seller, digest, rv.v, rv.r, rv.s));
    return rv;
}

Here’s one: The Buyer fails to recover funds from the contract if the Seller times out. Fixing that bug is slightly tricky due to mixing exceptions and async, but can be done in a few lines of code.

Then you have equivalent code on the Seller, which doubles your code base; and then you must make sure all these pieces fit perfectly together, and remain in this perfect fit even as the code evolves.

And for all that price, this approach offers no user-interface during the many minutes that it takes to confirm a blockchain transaction. Cue in suspenseful music in a silent submarine…

Bug in Glow application?

Now, let’s consider the very same application; but instead of writing it in Solidity, let’s write it in Glow, the Domain-Specific Language I’m working on. Can you spot any bug?

@interaction({participants=[Buyer, Seller], assets=[price]})
let buySig = (digest : Digest) => {
  deposit! Buyer -> price;


  @publicly!(Seller) let signature = sign(digest);
  withdraw! Buyer <- price;
  return signature;
};

The program has a Buyer pay a price, initially deposited in escrow, in exchange for a Seller signing some closing document, at which point the Seller receives the Buyer’s escrowed payment.

Let’s go line by line:

  1. We are going to define an interaction between two participants Buyer and Seller, who will transact about some asset named the price.
  2. The interaction function is called buySig, and it takes as parameter the cryptographic digest of a document that the Seller will sign electronically.
  3. First, the Buyer deposits the agreed-upon price in escrow.
  4. After a blank line, that makes it obvious that there is a change of active participant, and thus a second online transaction, the Seller is now active and publicly signs the digest. (Publicly signs, means that first he does it in private, then he publishes the signature, and finally everyone verifies that it checks out.)
  5. After the signature is verified by the consensus, the Buyer takes the money out of the contract.
  6. The signature is returned at the end of the interaction.
  7. The end.

So, did you find the bug?

Here it is: the payment of the price is released to the Buyer instead of the Seller. But that was pretty obvious when reading aloud what the program was doing, isn’t it?

Correct Glow application

Let’s fix that bug. Can you spot a bug now?

@interaction({participants=[Buyer, Seller], assets=[price]})
let closing = (digest : Digest) => {
  deposit! Buyer -> price;

  @publicly!(Seller) let signature = sign(digest);
  withdraw! Seller <- price;
  return signature;
};

No? Well, neither can I. The program is so compact, in 8 lines (plus 1 blank), that there’s just no space left for a bug to squeeze in unnoticed. Each line directly corresponds to some aspect of the above sequence diagram, except for the return signature that expresses the Buyer’s ultimate interest in the signature. Any line alteration would lead to a bug so obvious that either the compiler could catch it, or the human auditor probably will.

Making bugs inexpressible

Glow is a better language than Solidity, or most other languages for DApp development because it is impossible for programmers to even write those bugs. Entire classes of bugs that plague other languages are inexpressible in Glow:

  • The accounts must remain balanced, so you can’t leave the deposits and withdrawals unmatched.
  • It’s always clear who is doing what or receiving what, and the language always handles all the details of the checking for you. No mis-attribution or weird race-condition possible.
  • You can’t publicly return the signature unless it was made publicly available earlier.
  • The program is still valid if you don’t explicitly return the signature, but this will be noticed when writing the user interface for the Buyer.
  • The language automatically handles the timeouts, both in the contract and the client;, you cannot forget about it, because you don’t have to remember about it.
  • And from those 8 lines, you get both the smart contract and the client for both participants. That’s a 80-90% reduction in the total amount of code required.
  • In the future, we can also add a check that every participant gets something in exchange for participation, or the contract is rejected for obviously mis-designed incentives. — This would automatically catch the bug I introduced.

Security Through Clarity – Some larger points about security and software design

Obscurity vs Clarity

“There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult. It demands the same skill, devotion, insight, and even inspiration as the discovery of the simple physical laws which underlie the complex phenomena of nature.” — C.A.R. Hoare, Turing Award Lecture

Let’s call these two approaches “Security through Clarity” and “Security through Obscurity”.

“Security through Obscurity” often justifiably gets a bad rap; still, we ultimately rely on some version of it for e.g. passwords. A; and when you can amend your program to fix deficiencies faster than your enemies can find them, it might be good enough.

However, in situations where a security breach would be catastrophic, when it’s too late to fix your software after it’s deployed, then only the first approach will do — Security through Clarity.

KISS: Keep It Simple Stupid

The solution to Clarity is to “make things as simple as they can be, but no simpler”.

It’s easier to audit a 7-line program than a 80-line program — assuming lines of similar terseness. Simplicity wins! Obviously.

But simplicity is easier said than done. How could we keep programs simple to begin with? It’s not like others deliberately try to make their programs more complex., is it?

Simplicity isn’t automatic. On the contrary, if it isn’t specifically sought, complexity is the default.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top