Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Israel Ayanwola
Israel Ayanwola

Posted on • Originally published atthecodeway.hashnode.dev on

Building an ECDSA wallet with JavaScript

Blockchain is an amazing technology to dive into. It has been around for some time now and powering very popular technologies like Ethereum.

To make this article as simple as possible, the definitions and terminologies would be summarized to just key points needed for the project you will build in this article. But links to amazing educational articles will be provided for each.

By the end of this article, you would understand Public Key Cryptography, Elliptic curve digital signatures and how they can be used to make the world a better place. I will take you one step at a time on how to build a blockchain node.

What is Cryptography

Cryptography is a way of securing a piece of information. It's the study of cryptographic hash functions.

Cryptographic hash functions are mathematical functions that take an input of any size and convert it to a fixed-sized output. It's a one-way ticket, you can't get the input from the output.

Cryptography is at the heart of blockchain and Web3. Here is alink to an online tool containing lots of hash functions you can play with.

Public Key Cryptography

It's the use of two keys, a public key (a key that is known to everyone), and a private key (a key that is kept secured). The private key can encrypt a message that can only be decrypted by the public key, verifying that the message was indeed sent by someone with the right private key and vice versa.

Public key cryptography is also calledasymmetric cryptography because it uses a pair of related keys.

RSA and ECDSA are two popular algorithms used for public cryptography. RSA is based on the idea that you can quickly find the product of two prime numbers, but extremely hard to factor out the prime numbers once you have the products. Read more about RSA onWikipedia.

ECDSA (Elliptic curve digital signature algorithm) uses elliptic curves. Read more about it onWikipedia.

Why use ECDSA

ECDSA provides the same level of security as RSA but with small key sizes. RSA keys can be very large making it take a long to transfer over a network.

ECDSA is the algorithm used by Bitcoin, which is thesecp256k1 curve.

Building an ECDSA node

This is an example of a blockchain node that uses the ECDSA signing algorithm. The project is a simple react frontend application that allows users to send money to each other. There will be a single server node, which makes this centralized but there will be an article later on how to deploy a program on Ethereum.

Goal

The server would manage the balances of the users. The goal is to enable the server to verify who is sending the money to make sure that a person can only send money from their wallet.

Project prerequisites

You will need to have a basic knowledge of React as it is used for the front end. With a basic understanding of Express.

Application Architecture

There is the front end, which has the UI part of the project that allows users to enter their wallet addresses and attempt to send money to others. The Idea here is that, when a user tries to send money, we would generate a transaction for them to sign with their private key, then input the signed transaction which would be sent to the server with their public key (wallet address), address where money should be sent, and the amount of money to be sent.

The server would take this signed transaction and the public key, then verify if the transaction was indeed signed using the right private key of the public key. If it's correct, it updates the users (sender and recipient) balances, else it returns an error message.

Client-Side React App

We would use Vite for our front-end tooling.

Create a new directory calledwallet-node and move into the directory.

Runnpm create vite@latest, enterclient as your project name,react as the framework andJavaScript as the variant.

Move into the client directory just created by Vite. Runnpm i to install the packages from the template.

Install Sass as a dev dependency using this commandnpm i -D sass. This will install the sass package as we will be using sass for our CSS styling.

The last installments are:

  1. Axios -npm i axios

  2. Ethereum Cryptography -npm i ethereum-cryptography

Now, edit the value of the title tag in the index.html file toECDSA Wallet or anything you want.

Move into thesrc directory, delete theassets directory,index.css andApp.css files.

Edit themain.jsx file, remove the line forimport './index.css'.

EditApp.jsx by removing everything in it and pasting in the following code;

import Wallet from "./Wallet";import Transfer from "./Transfer";import "./App.scss";import { useState } from "react";function App() {  const [balance, setBalance] = useState(0);  const [address, setAddress] = useState("");  return (    <div className="app">      <Wallet        balance={balance}        setBalance={setBalance}        address={address}        setAddress={setAddress}      />      <Transfer setBalance={setBalance} address={address} />    </div>  );}export default App;
Enter fullscreen modeExit fullscreen mode

We will create a Wallet and Transfer component, and ourApp.scss file for the sass styles.

From the above code, two stateful values were created,balance andaddress. Thebalance will be used to keep track of and manage an address balance, while theaddress will be used to share a user address among theWallet andTransfer components.

The transfer component accepts a propsetBalance which would be used to set the state of the user's balance after the money has been transferred successfully.

Create anApp.scss file in thesrc directory, then paste the following code;

body {  font-family: "Muli", sans-serif;  font-weight: 300;  background-color: #e2e8f0;  padding: 40px;}label {  display: flex;  flex-direction: column;  letter-spacing: 0.05rem;  font-size: .8em;  font-weight: 400;  color: #222;}.app {  display: flex;  max-width: 1400px;  flex-wrap: wrap;  gap: 12px;  margin: 0 auto;}.container {  flex-grow: 1;  margin: 0 20px;  background-color: #fff;  border: 1px solid #cbd5e0;  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);  border-radius: 0.375rem;  padding: 40px;  label {    margin: 10px 0;  }  .button {    margin-top: 10px;  }}input {  padding: 10px 0;  border-radius: 0.125rem;  border: 1px solid rgb(226, 232, 240);  background-color: #fdfdfe;  padding-inline-start: 0.75rem;  font-size: 0.875rem;}.button {  background-color: #319795;  border-radius: 0.125rem;  padding: 10px 20px;  color: white;  display: inline-flex;  text-transform: uppercase;  letter-spacing: 1px;  font-weight: 400;  font-size: .9em;  &:hover {    cursor: pointer;  }}.wallet {  display: flex;  flex-direction: column;  .balance {    text-transform: uppercase;    letter-spacing: 1px;    font-weight: 400;    font-size: .9em;    display: inline-flex;    margin-top: 10px;    padding: 0.75rem;    background-color: #f4f6f8;  }}.transfer {  display: flex;  flex-direction: column;}
Enter fullscreen modeExit fullscreen mode

Create aserver.js file and paste the code below;

import axios from "axios";const server = axios.create({  baseURL: "http://localhost:3042",});export default server;
Enter fullscreen modeExit fullscreen mode

This creates an Axios instance that useshttp://localhost:3042 as the base URL, this is the URL our server will be listening on. The Axios instance is exported to be used by other services.

Now we would need to create a very important function. Here we would use cryptographic functions 😁. Specifically, thekeccak256 hash function, which would be used to hash our transaction messages.

Create aservices.js file, and paste the code below;

import { keccak256 } from "ethereum-cryptography/keccak"import { utf8ToBytes, toHex } from "ethereum-cryptography/utils"export async function hashMessage(message){    return toHex(keccak256(utf8ToBytes(message)))}
Enter fullscreen modeExit fullscreen mode

Now let's talk about what is happening here.

We have created ahashMessage function, which accepts a message (string) and returns a hash (string).

Keccak256, the function is imported from the Ethereum cryptography package installed earlier, it accepts bytes (Uint8Array) and returns a hash of fixed-sized output. The return type of this hash is a byte. Theutf8ToBytes converts the message passed to the function to bytes which are then passed to thekeccak256 function.

Finally, the function,toHex, takes the returned bytes from thekeccak256 function and converts it to a hexadecimal string, which is then returned by thehashMessage function.

Its purpose is to encrypt a transaction message which would then be sent to the user to sign with their private key 🔑.

Creating the Wallet Component

The functionality of this component is to allow the user to input any address and see its balance. Create aWallet.jsx file in thesrc directory, then copy and paste the code below into the file.

import server from "./server";function Wallet({ address, setAddress, balance, setBalance }) {  async function onChange(evt) {    const address = evt.target.value;    setAddress(address);    if (address) {      const {        data: { balance },      } = await server.get(`balance/${address}`);      setBalance(balance);    } else {      setBalance(0);    }  }  return (    <div className="container wallet">      <h1>Your Wallet</h1>      <label>        Wallet Address        <input placeholder="Type an address, for example: 0x1" value={address} onChange={onChange}></input>      </label>      <div className="balance">Balance: {balance}</div>    </div>  );}export default Wallet;
Enter fullscreen modeExit fullscreen mode

Every time the input changes it will invoke theonChange event handler which will request the address balance from the server. If successful it will update the balance state using thesetBalance action. The Balance and Address states are created from theApp.jsx App component because their states will be used by the Transfer component too.

Transfer Component

This component will be used by the user to send any amount to any recipient. Create aTransfer.jsx file in thesrc directory, then copy and paste the code below into the file.

import { useState } from "react";import server from "./server";import { hashMessage } from "./services"function Transfer({ address, setBalance }) {  const [sendAmount, setSendAmount] = useState("");  const [recipient, setRecipient] = useState("");  const setValue = (setter) => (evt) => setter(evt.target.value);  async function getSignature(evt){    evt.preventDefault();    try {      let data = {        recipient,        amount: parseInt(sendAmount)      }      let msgHex = await hashMessage(JSON.stringify(data))      let signature = prompt(`Sign message (${msgHex}) and provide signature:`)      if (signature === null){        alert("You did not provided a signature")        return      }      await transfer(signature)    } catch (ex) {      alert(ex.response.data.message);    }  }  async function transfer(signature) {    const {      data: { balance },    } = await server.post(`send`, {      sender: address,      amount: parseInt(sendAmount),      recipient,      signature,    });    setBalance(balance);    alert("Funds transferred successfully!")  }  return (    <form className="container transfer" onSubmit={getSignature}>      <h1>Send Transaction</h1>      <label>        Send Amount        <input          placeholder="1, 2, 3..."          value={sendAmount}          onChange={setValue(setSendAmount)}        ></input>      </label>      <label>        Recipient        <input          placeholder="Type an address, for example: 0x2"          value={recipient}          onChange={setValue(setRecipient)}        ></input>      </label>      <input type="submit" className="button" value="Transfer" />    </form>  );}export default Transfer;
Enter fullscreen modeExit fullscreen mode

This is a lot, so let's break it down.

...function Transfer({ address, setBalance })...
Enter fullscreen modeExit fullscreen mode

The transfer component accepts two values;

  • address - state object which is the sender's (user's) address

  • setBalance - state action which will be used to update the user's balance

const [sendAmount, setSendAmount] = useState("");const [recipient, setRecipient] = useState("");
Enter fullscreen modeExit fullscreen mode

These states are used to set the amount to be sent and the recipient's address.

...const setValue = (setter) => (evt) => setter(evt.target.value);...
Enter fullscreen modeExit fullscreen mode

This function takes in a callable,setter as an argument and returns an event handler. The function of the event handler is to pass the target value of the event object as an argument tosetter.

It's used here;

...<label>  Send Amount  <input    placeholder="1, 2, 3..."    value={sendAmount}    onChange={setValue(setSendAmount)}  ></input></label><label>  Recipient  <input    placeholder="Type an address, for example: 0x2"    value={recipient}    onChange={setValue(setRecipient)}  ></input></label>...
Enter fullscreen modeExit fullscreen mode

The function,setValue , is used for the onChange events of both inputs above. Which updates the states of both amount and recipient with the values of their respective inputs.

Now, the function,getSignature , will be demystified. The goal here is that when the user enters a recipient address, the amount to be sent, and submits the form. A transaction object will be created which will contain the amount and recipient address, this transaction will then be displayed to the user so that they can sign it with their private key 🔑. Then the signature will be passed to the transfer function.

...async function getSignature(evt){  evt.preventDefault();  try {    let data = {      recipient,      amount: parseInt(sendAmount)    }    let msgHex = await hashMessage(JSON.stringify(data))    let signature = prompt(`Sign message (${msgHex}) and provide signature:`)    if (signature === null){      alert("You did not provided a signature")      return    }    await transfer(signature)  } catch (ex) {    alert(ex.response.data.message);  }}...
Enter fullscreen modeExit fullscreen mode

Thetransfer function;

...async function transfer(signature) {  const {    data: { balance },  } = await server.post(`send`, {    sender: address,    amount: parseInt(sendAmount),    recipient,    signature,  });  setBalance(balance);  alert("Funds transferred successfully!")}...
Enter fullscreen modeExit fullscreen mode

This sends the signature and the transaction data to the server. If the signature is valid for the transaction data, then the money will be transferred to the recipient's address.

That's all for the client-side application.

Server-Side NodeJS Application

The backend will be an express application. It's going to have two simple handlers;

  • GET /balance/:address - to get an address balance

  • POST /send - to transfer funds from one address to another.

Also, we would need to create some useful scripts for generating new random wallets and signing transactions. The latter is meant to help users sign transaction messages with their private key 🔑.

Firstly, let's set up the server directory. If not already created, create a new directory,server , in the root directory. Move into the directory, and runnpm init -y to initialize the defaultpackage.json file.

Copy and paste the content below into thepackage.json file.

{  "name": "server",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "generate": "node ./scripts/gen.js",    "sign": "node ./scripts/signer.js",    "test": "echo \"Error: no test specified\" && exit 1"  },  "keywords": [],  "author": "",  "license": "ISC",  "dependencies": {    "cors": "^2.8.5",    "ethereum-cryptography": "^1.1.2",    "express": "^4.18.1",    "yargs": "^17.6.2"  }}
Enter fullscreen modeExit fullscreen mode

Then runnpm install to install dependencies.

Before creating the scripts forgenerate andsign, we will be creatingservices.js first to define shared functions.

Services

Create a new fileservice.js in theserver directory. Copy and paste the code below into it.

const secp = require("ethereum-cryptography/secp256k1");const { keccak256 } = require("ethereum-cryptography/keccak")const { utf8ToBytes, toHex } = require("ethereum-cryptography/utils")function extractPublicKey(fullKey){    let kec = keccak256(fullKey.slice(1, fullKey.length));    return toHex(kec.slice(kec.length - 20, kec.length))}function verifySignature(sig, msg, pubKey){    const msgHash = keccak256(utf8ToBytes(msg));    let actualSignature = sig.slice(0, sig.length - 1)    let recoveryBit = parseInt(sig[sig.length - 1])    const sigPubKey = secp.recoverPublicKey(msgHash, actualSignature, recoveryBit);    const mainKey = extractPublicKey(sigPubKey);    return mainKey == pubKey}module.exports = {    verifySignature,    extractPublicKey}
Enter fullscreen modeExit fullscreen mode

The function,extractPublicKey, accepts bytes array as an argument, this is the byte format of a full public key. Then hashes it withkeccak256 and returns the hexadecimal string of the last 20 bytes of the hash.Ethereum docs explains why it's required. It's to make it shorter 😉.

The last function,verifySignature, accepts a signature, a transaction message, and the sender's public key. All this data is required to verify that the transaction message was indeed signed using the private key of the owner of the public key.

The function hashes the transaction message and used [recoverPublicKey](https://github.com/ethereum/js-ethereum-cryptography#secp256k1-curve:~:text=%3A%20boolean%0Afunction-,recoverPublicKey,-(msgHash%3A to get the public key of the signer. After extracting the short format usingextractPublicKey, it compares it will the public key,pubKey, passed to the function.

Script - generate: Generating new random public and private key pairs

const secp = require("ethereum-cryptography/secp256k1")const { toHex } = require('ethereum-cryptography/utils')const { extractPublicKey } = require('../services')let privateKey = secp.utils.randomPrivateKey();let pubKey = secp.getPublicKey(privateKey)pubKey = extractPublicKey(pubKey)console.log("Private key:", toHex(privateKey))console.log("Public key:", pubKey)
Enter fullscreen modeExit fullscreen mode

A new private key is generated using therandomPrivatekey function. The public key is also extracted from the private key. The mathematical property that made this possible is so amazing. The private key can never be known from the public key.

Script usage example:

npm run generate
Enter fullscreen modeExit fullscreen mode

Console output:

Script - sign: Signing transaction messages

const yargs = require('yargs/yargs')const { hideBin } = require('yargs/helpers')const secp = require('ethereum-cryptography/secp256k1')const { toHex } = require("ethereum-cryptography/utils")let args = yargs(hideBin(process.argv))    .option('private_key', {        alias: 'p',        type: 'string',        description: 'Your Private Key',        demandOption: true    })    .option('data', {        alias: 'd',        type: 'string',        description: 'Payload to sign',        demandOption: true    })    .parse()let privKey = args.private_keylet msgHash = args.datasecp.sign(secp.utils.hexToBytes(msgHash), privKey, { recovered: true }).then(data => {    const [signature, recovery_bit] = data    let sig = toHex(signature);    console.log("Your Signature:", sig)    console.log("Your Recovery Bit:", recovery_bit)    let fullSig = sig + recovery_bit.toString()    console.log("Copy and paste this as the full signature, this has the recovery bit attached to the end:\n", fullSig)})
Enter fullscreen modeExit fullscreen mode

This is more of a CLI tool. It accepts the private key and message hash as command-line arguments. Thesign function from the module,ethereum-cryptography/secp256k1 , is used to sign the message hash,msgHash. The result of thesignature andrecovery_bit received from the data are later concatenated to form a single string, signature, which is logged to the console.

The signature is expected in the component,Transfer , in the client-side application.

So the user can copy it from the console and paste it into the prompt.

Script usage example

npm run sign -- -p 3ebefedbd43cbd88f0504acd101df139ddce0656da699b8350c1db9eaf193484 -d 3ebefedbd43cbd88f0504acd101df139ddce0656da699b8350c1db9eaf178970
Enter fullscreen modeExit fullscreen mode

Console output:

Now create theindex.js file, which will contain our API routes and handlers, in thesrc directory.

Follow the steps below and paste the codes gradually.

const express = require("express");const app = express();const cors = require("cors");const port = 3042;const { verifySignature } = require("./services")app.use(cors());app.use(express.json());const balances = {  "KEY_A": 100,  "KEY_B": 50,  "KEY_C": 75,  // KEY_N: Any amount};
Enter fullscreen modeExit fullscreen mode

The object,balances , is currently acting as the database. The keys of the object will be public keys of different wallets, and the values will be their respective balance.

New public and private key pairs can be generated using thegenerate script. After creating any amount of key pairs, update the object,balances, with the public keys. Make sure to save their respective private keys too, so they can be used later for signing transaction messages.

GET - /balance/:address

app.get("/balance/:address", (req, res) => {  const { address } = req.params;  const balance = balances[address] || 0;  res.send({ balance });});
Enter fullscreen modeExit fullscreen mode

This is the route used to get an address balance.

POST - /send

app.post("/send", (req, res) => {  const { sender, recipient, amount, signature } = req.body;  const msg = JSON.stringify({    recipient,    amount  })  let isValid = verifySignature(signature, msg, sender);  if (isValid === false){    res.status(400).send({ message: "Invalid Signature!" })    return  }  setInitialBalance(sender);  setInitialBalance(recipient);  if (balances[sender] < amount) {    res.status(400).send({ message: "Not enough funds!" });  } else {    balances[sender] -= amount;    balances[recipient] += amount;    res.send({ balance: balances[sender] });  }});
Enter fullscreen modeExit fullscreen mode

This route verifies the amount and recipient, against the signature and public key of the sender. If the signature is valid, it tries to debit the sender, if this is successful it will update the recipient balance.

Lastly,

app.listen(port, () => {  console.log(`Listening on port ${port}!`);});function setInitialBalance(address) {  if (!balances[address]) {    balances[address] = 0;  }}
Enter fullscreen modeExit fullscreen mode

Start the server to listen on any given port.

If the port is changed, make sure it's also updated in the client application.

The function,setInitialBalance, checks if an address exists in the database. If the address does not exist, it adds the address to the database with a balance of zero. This is a very nice method, as we don't need to manually add new users to our database.

Project codes areGithub

Resources

Cryptographic tools for Ethereum -Ethereum Cryptography

Node CLI tool -Yargs

Play with Cryptographic hashes -SHA256 Online tool

Public Key Cryptography -RSA Algorithm &Elliptic Digital Curves (ECDSA)

Cryptography in HTTPS -Diffie Hellman Key Exchange &TLS handshake for HTTPS

Conclusion

The idea presented in this article can also be used in several fields on technology to maintain data integrity when data is transferred between several components through a network.

One flaw of the server built today is that it's a single node, which makes the database a centralized instance. This is where a blockchain ledger would be useful because it's managed by more nodes which are all bound by rules (consensus) making the whole system decentralized. An account with a public key,TEST, onnode A, would have the same balance onnode B.

Building applications on the blockchain will be covered later in new articles, subscribe to the newsletter to receive notifications when new articles drop.

Follow me on Twitter@netrobeweb.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Learn, Code, Build and Write.
  • Location
    Lagos, Nigeria
  • Education
    University of London, Gold Smith
  • Pronouns
    he/him
  • Work
    Cloud Operations Engineer Spacepen
  • Joined

More fromIsrael Ayanwola

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp