๐Ÿ”Signature

Proof of ownership of Externally Owned Accounts (EOAs) is established through private keys and digital signatures.

Private key and Public key

The first and most crucial step in generating keys is to find a secure source of entropy, or randomness. Generating an Ethereum private key involves selecting a number between 1 and 2ยฒโตโถ.

A private key is a randomly generated 256-bit (32-byte) number.

The public key is set of x and y coordinates on an elliptic curve that satisfy the elliptic curve equation. It is derived from two numbers that are generated from the private key using elliptic curve multiplication. This process is irreversible, meaning that the private key cannot be derived from the public key.

To generate the public key, the private key is used in an equation involving elliptic curve multiplication, which is irreversible. The equation is: K = k * G, where K is the public key, k is the private key, and G is the constant point (generator point).

Ethereum uses the same elliptic curve, secp256k1, as Bitcoin.

Ethereum addresses are created by taking the Keccak-256 hash of the public key and representing it as a hexadecimal number. The last 20 bytes of the Keccak-256 hash are used to generate the address.

Example:

  • Private Key (randomly generated): 0x3a1f59e7b9a7a27b3d905a1f0ce2b1dbdb16ee1e3a78384b6a9f3de75ef1e64a

  • Public Key (derived from private key): 0x04c7b5ba59e0581ad4f3a195790fda46d4e840da00aeb0eb06802b62c0ad22a1f8d1897bcbb15a4919a242d40d38c48b4fc3a029d270066a3b7ad6cc5c00ee66d9

  • Ethereum Address (last 20 bytes of public key hash): 0x2e0e4c47e30f71667a9f211bf2e870b6c309ae14

Signature Validation Process

To prove you are the true owner of an EOA, you need to sign a message with the corresponding private key. This means that only you have access to the funds on your account. When making a transaction sending 1 Ether to a contract to mint a new NFT, under the hood, Ethereum verifies the digital signature you created (using the private key) against the corresponding accountโ€™s public key hash (the address).

As we learned, public key cryptography (also known as asymmetric encryption) is a cryptographic method that uses a key pair system. The one key, called the private key, signs the message. The other key, called the public key, verifies the signature. When we sign any message, whether a transaction on Ethereum or any form of data, we create a digital signature. This is done by hashing the message and running the ECDSA algorithm to combine the hash with the private key, producing a signature. By doing this, any changes to the message will result in a different hash value.

As we can read from the Mastering Ethereum book, โ€œA digital signature can be created to sign any message. For Ethereum transactions, the details of the transaction itself are used as the message. The mathematics of cryptography โ€” in this case, elliptic curve cryptography โ€” provides a way for the message (i.e., the transaction details) to be combined with the private key to create a code that can only be produced with knowledge of the private key. That code is called the digital signature.โ€

Above is another explanation of digital signatures, but in the context of Ethereum transactions. This explanation introduces us to another, very important subject โ€” elliptic curve cryptography.

// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.20;


/*
ethereum.enable()
account = "0x58aEdD5a35111f2DEFd03b14ae0020D8b5845902"
hash = "0x4198237100dd9ecd639492c23b05563d49df8207ee262a734b8dc52fdad61832"
ethereum.request({method: "personal_sign", params: [account, hash]})
PromiseResult is the signature (0xb031ddd7bf10556f227e93860b183368db6ccd080190c37e5b1d043e113cad0372b275f722f84ec642c9b7aeffa0c4e3d0a848b4daf4ab48a3e630bcf23eb8551b)
*/

contract VerifySignature {

    function verify(address _signer, string memory _message, bytes memory _sig)
        external pure returns (bool)
    {
        bytes32 messageHash = getMessageHash(_message);
        bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);

        return (recover(ethSignedMessageHash, _sig) == _signer);
    }

    function getMessageHash(
        string memory _message
    ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_message));
    }

    function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            _messageHash
        ));
    }

    function recover(bytes32 _ethSignedMessageHash, bytes memory _sig) public pure returns (address) {
        (bytes32 r, bytes32 s, uint8 v) = _split(_sig);
        return ecrecover(_ethSignedMessageHash, v, r, s);
    }

    function _split(bytes memory _sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
        require (_sig.length == 65, "invalid signature length");
        assembly {
            /*
            First 32 bytes stores the length of the signature

            add(sig, 32) = pointer of sig + 32
            effectively, skips first 32 bytes of signature

            mload(p) loads next 32 bytes starting at the memory address p into memory
            */

            // first 32 bytes, after the length prefix
            r := mload(add(_sig, 32))
            // second 32 bytes
            s := mload(add(_sig, 64))
            // final byte (first byte of the next 32 bytes)
            v := byte(0, mload(add(_sig, 96)))
        }
    }
}

Last updated