签署和验证以太坊签名

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

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

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

背景

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

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

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

签名和验证签名

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

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

https://cdn-images-1.medium.com/max/1600/1\*7tj7YkvKNweRXILRsSD8eA.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 签名由三个参数rs和组成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为其三个组件rs, 和v, 这里注意 mload(add(sig, 32)) 为啥需要 add 32 是因为 bytes 类型属于动态数组类型,前面第一个槽(32字节)存的是 bytes 的长度。

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