Secure and Test the Contract With ECDSA Signature
By: Pavlo Horbonos
Tags: Smart Contracts, Ethereum, Blockchain, Solidity, Programming

One of the main goals that blockchain-based projects aim to achieve is the verification of data. For live examples, you can look at the digital identity and online documents storage and checks. Indeed, any of these cases require the action/transaction initiator verification to confirm the person or the entity. For example, if the person has the digital form of ID document, it becomes crucial to ensure ownership. Thus it is an excellent example of a problem of data verifiability. Let’s review the simplest form of the solution — a digital signature, whose testing is one of the crucial points during the smart-contract development.

What Are We Talking About

The approach is simple:

1) the system generates a message by known for everyone rules

2) the signer gets the message and adds a specific set of symbols — digital signature, code constructed from the message by a private key

3) the generated signature is now sent to the contract, where it is decomposed to retrieve the signer’s address.

Solidity offers you the ECDSA algorithm for signature generation and further decomposition. We do not need to deep dive into the algorithm itself (you can find the necessary information in the appropriate sources). All we need to know is that ECDSA is an example of asymmetrical cryptography, where the first user creates a signature with their private key, and the second user applies a standard algorithm to retrieve the public key of the signer. Thus, it can verify the source of the signature. Instead, let’s focus on the practical part — signature usage and testing.

First of all, we need to recognize a problem. For example, the contract needs to perform some action, let’s say, store the caller’s address. Though the contract should perform storage only if the caller is verified, we need to be sure that no one can use their address without permission. To retrieve the authentic caller, we need to generate some message, sign it, and decompose it within the contract.

You can find the standard solution in the Solidity documentation (for example, in the 0.8.4 — the latest stable version at the moment of the article). The docs offer us the contract, which has needed the following built-in functionality: message generation, signature splitting, and assembly code to retrieve the signer. The example shows all the necessary methods and is pretty straightforward, though it has two cons: it lacks universality, and there is no good example of solution testing. That’s why I provide my version of the code and (the actual goal) — the testing strategy of the contract.

Dive Into the Code

Sure, you can use the standard OpenZeppelin library for the ECDSA operations, but you will face the same problems again — lack of flexibility and testing approaches. So, let’s dive into my example of signature-based logic. You can find the complete working example in my GitHub, but there are few places that I want to show fully.

First of all, we will prepare the message . As you can see, it is formed from two packed and hashed wallet addresses:

The second important piece of code is hashing the message together with the standard Ethereum message:

It shows that the message was sent within the Ethereum network and has a 32-byte length, which is not a random number. After previous operations, we have the hash, which has a 32-byte length. It is essential to have the additional hashing function in such form, and we will discuss the reasoning a bit later.

Other code pieces are pretty standard. The function to split the signature is as follows:

and here’s the function to retrieve the signer:

For the external interface, we will use the custom function, which receives the signature and necessary arguments, checks if the user is already registered, forms the message, and verifies the signature:

Prepare the Test

First, we will imitate the message that needs to be signed. We will use ethers.js library, which is (together with web3) the most used and convenient library. Since it is an open source library, you are free to explore its code and docs. Also, this library gives us the perfect interface for constructing the following message:

One of the cons of both web3 and ethers libraries is that they do not have all of the functions for the local Ganache environment since both libraries are aimed to work with full Ethererum nodes. Nevertheless, there is an approach for local testing using web3-account functionality. Though you need to create an additional account, which will implement the singer functionality and provide a connection to the current web3 provider:

So, now we have the message generated and signed. But, that’s not everything; there are a few things left to talk about. Both libraries (web3 and ethers) under the hood provide additional hashing of the message before the signature creation. Also, the message is not just hashed but is combined with the standard Ethereum message we saw earlier:

And that’s why we added the additional method to the contract. So, if you want to use custom messages, skip additional hashing, etc., you need to create a custom version of the signing functionality. We have discussed the reason above — both standard libraries implement the typical approach to sign the message, which can be changed only by overriding the functionality.

As the last step, let’s run the test and check to see if it works correctly:

Run test over Ganache

We have tested the signature generation, the message generated, the signature approval, and rejection. So it is a pretty complete set of tests for the verification functionality.