# 签署和验证以太坊签名 **Published by:** [Smithereens](https://paragraph.com/@soyoung/) **Published on:** 2022-04-08 **URL:** https://paragraph.com/@soyoung/iUKx6Z0X7hrTzA9R1DSK ## Content 以太坊交易需要被包含在一个区块中并被挖掘,然后才能被处理并保存在区块链上。因此,链上交易需要时间和成本来补偿矿工的工作。 相比之下,链下计算可以让您立即执行操作,而无需等待交易被挖掘,并且不消耗任何气体。 在本文中,让我们看看如何使用以太坊签名执行链下计算。加密签名可用于验证消息的来源和完整性。然后,我们将研究现实生活中的链下计算用例,例如去中心化交易所、状态通道和元交易。 背景 在去中心化交易所中,签名和链下计算用于预先授权做市商执行任何由做市商签署的订单。制造商不是直接在链上发布买卖订单,而是使用他们的私钥签署详细说明他们订单的消息。然后,制造商将这些已签署的订单提交到链下订单簿——由第三方中继器托管在中央服务器上的数据库——供接受者浏览和填写。这些链下订单会立即在链下提交,无需支付任何 gas。 使脱链订单簿起作用的神奇成分是加密签名。 加密签名是支持所有区块链技术的基础计算机科学原语。签名可用于代表签名者授权交易。它还可以用于向智能合约证明某个帐户批准了某个消息,以太坊这里采用的是椭圆曲线数字签名算法(ECDSA) 签名和验证签名 以太坊网络中的每个账户都有一个公钥和一个私钥。以太坊地址本质上是公钥的散列版本。 帐户可以使用他们的私钥签署一段数据,返回该数据的签名。 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 进行了改进。 ## Publication Information - [Smithereens](https://paragraph.com/@soyoung/): Publication homepage - [All Posts](https://paragraph.com/@soyoung/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@soyoung): Subscribe to updates - [Twitter](https://twitter.com/stoneworld1991): Follow on Twitter