Creating ECDSA Node
Table of contents
Hello everyone, welcome back. Actually no, it's not true. The right way to say this is "I am back". I was away for a while (like, you know, 5 months) but I'm starting to share my coding experiences again. Recently I started Alchemy University Ethereum developer Bootcamp and this particular blog post is going to be about the first week's project of this Bootcamp, and this time I really hope to continue posting blogs every week for every project. After that, I will complete the "Road to Web3" and post them too.
What is this project?
In this project, we have a simple front-end application that sends funds between accounts. The thing we have to do as learners is to understand how a transaction occurs. We need to learn how to hash a message, sign it with the private key, confirm if it is the right wallet address etc and there are multiple places where we can encounter bugs like I did many times. This is the GitHub repository of the project we need to clone before implementing what we learn.
Let's Start!
There are some important points I need to say at the beginning because they might confuse you or they might seem wrong. Because you know, it happened to me :)
First of all, this is a local project and we are not connecting to a wallet. Normally, a hardware wallet or a browser wallet secures the private key of that wallet.
We need the private key to sign a transaction and we need both the signature and recovery bit to recover the public key (and address). You can check the Ethereum cryptography library here.
Because this project is not connecting any wallet we either need to know the private key and hardcode it or we have to ask the user to write the private key for signing the transaction in our app. The other option is asking the user to give us both a signature and a recovery bit.
Yes, asking for a private key or asking for the user to fill a form with a signature and recovery bit might sound wrong but as I said, we need some info because we are not connecting to any wallet. Normally our wallet secures our private key and gives signatures to applications.
The App
I think the people who are going to read this blog post are the people who also trying to complete the same Bootcamp and I believe you guys already have some idea about the app. If you don't, you can clone it from the GitHub link which is above.
The first part of the app is to generate a random private key and then extract a wallet address from it. We will use this wallet address / private key pairs (Public key/Private key pairs). We can easily generate a private key with Ethereum cryptography. You can see it belove. It's better for us to see the Hex versions of these values and our wallet address needs to start with "0x".
const privateKey = secp.utils.randomPrivateKey();
const publicKey = secp.getPublicKey(privateKey);
const address = keccak256(publicKey.slice(1)).slice(-20);
console.log("private key: ", toHex(privateKey));
console.log("public key: ", toHex(publicKey));
console.log(`address: 0x${toHex(address)}`);
In our app's front-end part we have 2 sections: Wallet and Transaction. It is basically like the screenshot above. At first, I decided to use the private key in the wallet section. The user needed to type the private key. The app gets the address from it and checks the balance. Then when the user wants to sign the transaction, the app takes the private key value from the wallet and signs it.
But then I didn't want the user to type the private key because it is not cool :/. Instead of that, I created an object with address/private key pairs. As functionality, it is the same thing as directly typing the private key but it is better not seeing it on the screen. As I mentioned above, because we are not connecting to a wallet we already know the address/private key pairs. So, how do we get these values? Like this:
// this is the backend part of the code. It is index.js file.
app.get("/balance/:address", (req, res) => {
const { address } = req.params;
const balance = balances[address] || 0;
const privateKey = privateKeys[address];
res.send({ balance, privateKey });
});
// this is the frontend part of the code. It is wallet.jsx file.
const address = evt.target.value;
if (address) {
const {
data: { balance, privateKey },
} = await server.get(`balance/${address}`);
setBalance(balance);
setPrivateKey(privateKey);
} else {
setBalance(0);
}
What is happening here? When the user types the wallet address this value goes to the backend. There are two objects which are called "balances" and "privateKeys". The backend gets the address, checks that address as the key in that two objects, and assigns the values(balance = balances[address], privateKey = privateKeys[address]
), and then sends those values to frontend again with this res.send({ balance, privateKey });
Then we set those values as our parameters.
Okay, we handled the wallet stuff, what about the transaction? What about hashing a message and signing it? Let's see what we need to do step by step, shall we?
We need a transaction message and we have to hash this message. It can be anything.
We need to sign this message. You remember that the sign function requires a message hash and private key (we will get it from the backend in this project, but normally it is secure in our wallets).
After signing the transaction, our app will send the signature and recovery bit to the server and at the backend, we will recover the signature address and check if it is the same as the sender's address.
So let's start. Now we are in our transfer.jsx file which is the client side. I wanted to write a function that hashes and signs the transaction, then I hope to extract the values like signature and recovery bit from it.
async function hashAndSign() {
try{
const transactionMessage = {
sender: address,
amount: parseInt(sendAmount),
recipient: recipient
}
// hash the transaction. You have to change the message to string first, then byte. After that hash it.
const hashedMessage = keccak256(utf8ToBytes(JSON.stringify(transactionMessage)));
const hexMessage = toHex(hashedMessage);
//Get the hex version of hashed message. We will use it when we want to recover the public key from signature (in index.js).
setHashedMessage(hexMessage);
So now, we have an object called transactionMessage.
To hash a message we will use the keccak256 function but what? We can not directly type our message as a parameter in this function, we have to convert it to bytes but what again? We can not directly type our message as a parameter in the utf8ToBytes function too. We will convert our object to a string, then to bytes, and then we will hash it. String -> Byte -> Hash.
You can see that I have hashedMessage
and hexMessage
. I spent soooo much time while I was trying to solve a bug and I want to explain it now. I can easily sign a transaction with hashedMessage
variable but the problem is when I tried to recover the public key and use that variable there was always an error. It is a quite simple thing but without the Hex version of it, that function won't work. So I learned it the hard way and obviously, it is much better to use the Hex version of a hashed message when signing and recovering. Now, the rest of the function:
// sign the message with private key. it will return an array with two elements. First one is signature. Get the hex version of it.Second one is recovery bit.
const signatureArray = await secp.sign(hexMessage, privateKey, {recovered: true});
const signature = toHex(signatureArray[0]);
setSignature(signature);
const recoveryBit = signatureArray[1];
setRecoveryBit(recoveryBit);
}
catch (error){
console.log(error);
alert(error);
}
You see the "sign" function has an optional argument which is {recovered: true}
part of the function. If it is not there, the function will return the signature. But if this argument is there, the function will return an array with two elements. The first element of that array is the signature, and the second element is the recovery bit. Maybe you realized that I converted the signature to the Hex version too because this was also the bug that I encountered.
Phew, it's a lot, huh? We are almost there. Now we need one more function which is transfer. It is something like this:
// create the transfer function. Sender-recipient and amount is for transaction.
// signature, recoverybit and hexmessage are for recovering the public key and verifying.
async function transfer(evt) {
evt.preventDefault();
try {
const {
data: { balance },
} = await server.post(`send`, {
sender: address,
amount: parseInt(sendAmount),
recipient,
signature,
recoveryBit,
hexMessage
});
setBalance(balance);
} catch (ex) {
alert(ex.response.data.message);
}
}
What are we doing? When the user clicks transfer and submits the form, this means we send these values to the backend, the backend will take those values and do some checks, and it will send new things to the front end. So, what is going on at the backend when it gets these values? This:
app.post("/send", async (req, res) => {
try {
const { signature, hexMessage, recoveryBit, sender, recipient, amount } = req.body;
// get signature, hash and recovery bit from client-side and recover the address from signature
const signaturePublicKey = secp.recoverPublicKey(hexMessage, signature, recoveryBit);
const signatureAddressNotHex = keccak256(signaturePublicKey.slice(1)).slice(-20);
const signatureAddress = "0x" + toHex(signatureAddressNotHex);
// check the signature address and the typed sender address
if (signatureAddress !== sender) {
res.status(400).send({message: "You are not the person!"})
}
// if balance is enough and signature is from same address finish the transaction and send new balances to frontend.....
Alright, that's all. Now we have a working project which hashes transaction messages, signs transactions, checks if they are valid and executes. There are many things to implement too, for example, adding nonce or transaction count, or not creating wallet/private key pairs but asking users to type their private key before signing the transaction on the client side etc.
Even if it is a simple project I spent quite some time doing it, I struggled a lot especially when I tried to recover the public key from the signature but that one too was just a small error. I learned a lot in terms of not only cryptography and blockchain but also javascript and react too because this was my first time using react and it was not easy for me to get the connection between the backend and frontend.
If you come this far, thank you for your patience and hope to see you next time. You can check this project in my GitHub too.
Oh, by the way, I want to say thank you to the Alchemy team too. Vitto, Albert Hu, you guys are amazing.
Take Home Messages
Be careful when using arguments in a function because the type of argument you use might not be the required parameter type (Remember the hashed message issue).
Convert the hashed messages and signatures to their Hex value and always try to use the Hex version of them in your functions.
Spend more time understanding backend and frontend connection and communication.