🎓 A1Learner
🔍
0 of 45 lessons completed

Solidity ECDSA Verification

ECDSA verification is the check that a signature was made by a claimed signer. The contract recovers the signer from the signature and compares it with the claimed signer.

The prefixed hash recovers the signer

The signer signed the prefixed hash, not the raw hash. Only the prefixed hash recovers the signer address.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Verifier {
    uint256 constant HALF_ORDER = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0;
    uint256 constant CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;

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

    function recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
        return ecrecover(digest, v, r, s);
    }

    function verify(address signer, bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (bool) {
        if (uint256(s) > HALF_ORDER) {
            return false;
        }
        return ecrecover(prefixed(digest), v, r, s) == signer;
    }
}

Only the true signer verifies

A signature checks against its own signer and its own message. A different signer or a different message fails the check.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Verifier {
    uint256 constant HALF_ORDER = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0;
    uint256 constant CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;

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

    function recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
        return ecrecover(digest, v, r, s);
    }

    function verify(address signer, bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (bool) {
        if (uint256(s) > HALF_ORDER) {
            return false;
        }
        return ecrecover(prefixed(digest), v, r, s) == signer;
    }
}

A high s value is rejected

Flipping s across the curve order makes a second signature for the same message. The low-s check rejects it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Verifier {
    uint256 constant HALF_ORDER = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0;
    uint256 constant CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;

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

    function recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
        return ecrecover(digest, v, r, s);
    }

    function verify(address signer, bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (bool) {
        if (uint256(s) > HALF_ORDER) {
            return false;
        }
        return ecrecover(prefixed(digest), v, r, s) == signer;
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Verifier {
    uint256 constant HALF_ORDER = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0;
    uint256 constant CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;

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

    function recover(bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
        return ecrecover(digest, v, r, s);
    }

    function verify(address signer, bytes32 digest, uint8 v, bytes32 r, bytes32 s) public pure returns (bool) {
        if (uint256(s) > HALF_ORDER) {
            return false;
        }
        return ecrecover(prefixed(digest), v, r, s) == signer;
    }
}

Key points

  • ECDSA: a signature scheme where a private key signs and anyone can check the signature with the public signer address.
  • ecrecover: the built-in function that returns the signer address from a digest and a signature.
  • Digest: the hash of the message that was actually signed.
  • Signed-message prefix: a fixed header added before hashing, so a signature cannot be reused as a plain transaction signature.
  • Malleability: a second valid-looking signature for the same message, made by flipping the s value.
  • Use a low-s check to reject a signature whose s value is above half the curve order.

In this example, a signer signs the prefixed hash of a message, the verifier accepts that signature, and the same signature fails when the message is changed.

Note

ecrecover returns the zero address for a malformed signature, so a check must never accept the zero address as a signer.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

A contract checks a signature with ecrecover. What does it compare the recovered address with?