# 签署和验证以太坊签名

By [Smithereens](https://paragraph.com/@soyoung) · 2022-04-08

---

以太坊交易需要被包含在一个区块中并被挖掘，然后才能被处理并保存在区块链上。因此，链上交易需要时间和成本来补偿矿工的工作。

相比之下，链下计算可以让您立即执行操作，而无需等待交易被挖掘，并且不消耗任何气体。

在本文中，让我们看看如何使用以太坊签名执行链下计算。加密签名可用于验证消息的来源和完整性。然后，我们将研究现实生活中的链下计算用例，例如去中心化交易所、状态通道和元交易。

背景
==

在去中心化交易所中，签名和链下计算用于预先授权做市商执行任何由做市商签署的订单。制造商不是直接在链上发布买卖订单，而是使用他们的私钥签署详细说明他们订单的消息。然后，制造商将这些已签署的订单提交到链下订单簿——由第三方中继器托管在中央服务器上的数据库——供接受者浏览和填写。这些链下订单会立即在链下提交，无需支付任何 gas。

使脱链订单簿起作用的神奇成分是加密签名。

[加密签名](https://en.wikipedia.org/wiki/Digital_signature)是支持所有区块链技术的基础计算机科学原语。签名可用于代表签名者授权交易。它还可以用于向智能合约证明某个帐户批准了某个消息，以太坊这里采用的是椭圆曲线数字签名算法(**ECDSA)**

签名和验证签名
=======

以太坊网络中的每个账户都有一个公钥和一个私钥。以太坊地址本质上是公钥的散列版本。

帐户可以使用他们的私钥签署一段数据，返回该数据的签名。

![https://cdn-images-1.medium.com/max/1600/1\*7tj7YkvKNweRXILRsSD8eA.png](https://storage.googleapis.com/papyrus_images/233f5e6dc74dcf127ec2fa01eb296a335437f08b9b8c51a8626ae396a6c984ed.png)

https://cdn-images-1.medium.com/max/1600/1\\\*7tj7YkvKNweRXILRsSD8eA.png

任何人都可以验证生成的签名：

*   恢复签名者的公钥/地址，以及
    
*   验证消息的完整性，即它与签名者签署的消息相同。
    

签约
--

您可以在浏览器中完全脱链地签署消息，而无需与以太坊网络进行交互。ECDSA 签名消息的签名和验证允许在区块链之外进行防篡改通信。

我们可以`[eth_sign](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign)`通过以太坊客户端调用该方法，例如`web3.js`：

    // Create a SHA3 hash of the message 'Apples'
    const messageHash = web3.sha3('Apples');
    
    // Signs the messageHash with a given account
    const signature = await web3.eth.personal.sign(messageHash, web3.eth.defaultAccount);
    

确认
--

以太坊中的 ECDSA 签名由三个参数`r`、`s`和组成`v`。Solidity 提供了一个全局可用的方法`ecrecover`，它返回给定这三个参数的地址。如果返回的地址与签名者的地址相同，则签名有效。

生成的签名是、 和`web3.js`的串联`r`，因此必要的第一步是将这些参数拆分出来。`sv`

智能合约和以太坊客户端都具有验证 ECDSA 签名的能力。

**使用智能合约进行签名验证**

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.10;
    
    /* Signature Verification
    
    How to Sign and Verify
    # Signing
    1. Create message to sign
    2. Hash the message
    3. Sign the hash (off chain, keep your private key secret)
    
    # Verify
    1. Recreate hash from the original message
    2. Recover signer from signature and hash
    3. Compare recovered signer to claimed signer
    */
    
    contract VerifySignature {
        /* 1. Unlock MetaMask account
        ethereum.enable()
        */
    
        /* 2. Get message hash to sign
        getMessageHash(
            0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
            123,
            "coffee and donuts",
            1
        )
    
        hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
        */
        function getMessageHash(
            address _to,
            uint _amount,
            string memory _message,
            uint _nonce
        ) public pure returns (bytes32) {
            return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
        }
    
        /* 3. Sign message hash
        # using browser
        account = "copy paste account of signer here"
        ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)
    
        # using web3
        web3.personal.sign(hash, web3.eth.defaultAccount, console.log)
    
        Signature will be different for different accounts
        0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
        */
        function getEthSignedMessageHash(bytes32 _messageHash)
            public
            pure
            returns (bytes32)
        {
            /*
            Signature is produced by signing a keccak256 hash with the following format:
            "\x19Ethereum Signed Message\n" + len(msg) + msg
            */
            return
                keccak256(
                    abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
                );
        }
    
        /* 4. Verify signature
        signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
        to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
        amount = 123
        message = "coffee and donuts"
        nonce = 1
        signature =
            0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
        */
        function verify(
            address _signer,
            address _to,
            uint _amount,
            string memory _message,
            uint _nonce,
            bytes memory signature
        ) public pure returns (bool) {
            bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
            bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
    
            return recoverSigner(ethSignedMessageHash, signature) == _signer;
        }
    
        function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature)
            public
            pure
            returns (address)
        {
            (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
    
            return ecrecover(_ethSignedMessageHash, v, r, s);
        }
    
        function splitSignature(bytes memory sig)
            public
            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)))
            }
    
            // implicitly return (r, s, v)
        }
    }
    

上面定义了一个 `splitSignature` 函数：

将 拆分`signature`为其三个组件`r`, `s`, 和`v`, 这里注意 `mload(add(sig, 32))` 为啥需要 add 32 是因为 bytes 类型属于动态数组类型，前面第一个槽（32字节）存的是 bytes 的长度。

但是这里存在一个弊端，因为用户看到的都是一串哈希值，EIP712 进行了改进。

---

*Originally published on [Smithereens](https://paragraph.com/@soyoung/iUKx6Z0X7hrTzA9R1DSK)*
