第7节:世界杯竞猜(链下签名)
在区块链应用中,我们有很多需要使用链下签名的场景,例如:
什么是签名呢? 我们在使用opensea的时候,经常会提示我们进行数字签名,如下图:
用户进行sign确认,就会用自己的私钥对一段数据进行签名,得到signature,这个signature是唯一的,它可以在不暴漏你私钥的情况下,证明你是私钥的持有者。任何人都可以证明signature的有效性。
以太坊使用椭圆曲线算法进行数字签名(ECDSA),签名后的数据有如下作用:
我们在区块链中发起的每一笔交易(转账、对合约写操作)都是使用私钥签名过的,矿工会在打包前对每笔交易进行校验。
其中,V,R,S是对签名分割后得到的数据,会在后面讲解,签名示例如下:
# private
# 这个私钥事暴露的,完全是测试使用的,千万不要往里面转钱!!!
0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122
# address
0xc783df8a850f42e7F7e57013759C285caa701eB6
# message
['0xc783df8a850f42e7F7e57013759C285caa701eB6', 999]
# msgHash
0x416401c79c50b3b388890427985a289a2b8e6cd8e38949e79d5c77ec1ff88e88
# signature
0x381d3b66dbbbb2e83d054444197daa3b3309d19dcb5e81a8cc4015c4b13d8b7b79f1ce1f34b465b6cb211534869198dadf58118f0bf6208cd646d689b342af071c
签名知识点总结:
在openzeppelin标准合约中,已经实现了对ECDSA标准合约,我们拆解一下,整个签名验证过程可以分为三个阶段(详见下图)
在以太坊的ECDSA标准中,被签名的消息
为一组数据的hash值(由keccak256算法生成的byte32类型的数据),我们可以使用abi.encodePacked打包函数将任意多个参数进行打包,此处为:address和uint256类型。
function getMessageHash(
address _to,
uint _amount
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount));
}
输入参数:0xc783df8a850f42e7f7e57013759c285caa701eb6, 100
输出:0xcfb170482914a76ca8521405f52699df67c7ebb8e3899f27cc8265ebdab98a36
原始的消息
可以是能被执行的交易,也可以是其他任何形式。为了避免用户误签了恶意交易,EIP191
提倡在消息
前加上前缀prefix:"\x19Ethereum Signed Message:\n32"
字符,并再做一次keccak256
哈希,作为以太坊签名消息
。经过getEthSignedMessageHash()
函数处理后的消息,不能被用于执行交易。
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
return
keccak256(
// 这是标准字符串: \x19Ethereum Signed Message:\n
// 32表示后面的哈希内容长度
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
输入参数:0xcfb170482914a76ca8521405f52699df67c7ebb8e3899f27cc8265ebdab98a36
输出:0x60a7e355f6d1a5885594e145ce67bd165a3e63337806f576b7b417d31cdb20da
为了能够验证解析,我们需要先生成签名,有两种方式:方式1:调用metamask钱包生成;方式2:调用etherjs来生成
在metamask中导入私钥:0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122,对应地址为:0xc783df8a850f42e7F7e57013759C285caa701eB6
打开控制台F12(chrome)-> console,输入如下内容:
ethereum.enable()
account = "0xc783df8a850f42e7F7e57013759C285caa701eB6"
hash = "0xcfb170482914a76ca8521405f52699df67c7ebb8e3899f27cc8265ebdab98a36"
ethereum.request({method: "personal_sign", params: [account, hash]})
点击Sign进行签名
签名成功后,得到签名:0x96065962a0fd61b56f2791d020de3ab8bec09fe452496988f2fcfcfa056737493289f791f74507e82caf4ebb34cea0211cf52d3d89585ecd02e6352c97dcf2691b
在hardhat的test文件夹下创建sign.ts,内容如下:
const { expect } = require("chai")
const { ethers } = require("hardhat")
describe("Signature", function () {
it("signature", async function () {
// 0xc783df8a850f42e7f7e57013759c285caa701eb6
let privateKey = '0xc5e8f61d1ab959b397eecc0a37a6517b8e67a0e7cf1f4bce5591f3ed80199122'
console.log('private:', privateKey);
const signer = new ethers.Wallet(privateKey);
console.log('address :', signer.address);
const amount = 100
let msgHash = ethers.utils.solidityKeccak256(
["address", "uint256"], [signer.address, amount]
)
console.log('msgHash:', msgHash);
const sig = await signer.signMessage(ethers.utils.arrayify(msgHash))
console.log('signature:', sig);
})
})
运行单元测试:npx hardhat test,可以得到相同的签名:
此时我们已经生成了签名,签名
是由数学算法生成的。这里我们使用的是rsv签名
,签名
中包含r, s, v
三个值的信息。而后,我们可以通过r, s, v
及以太坊签名消息
来求得公钥
。下面的recoverSigner()
函数实现了上述步骤,它利用以太坊签名消息 _ethSignedMessageHash
和签名 _signature
恢复公钥
(使用了内联汇编):
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");
// 通过读取内存数据,根据规则进行截取,返回r,s,v数据
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
}
通过recoverSigner函数计算,我们恢复得到signature与签名数据对应的公钥(地址)
如果我们输入错误的signature或者签名数据,将会解析出错误的地址,即签名验证失败,如下图,我们将signature进行修改:将0x960改为0x760,效果如下,你会发现,解析出错误的地址。
接下来,我们只需要比对恢复的公钥
与签名者公钥_signer
是否相等:若相等,则签名有效;否则,签名无效:
function verify(bytes32 _msgHash, bytes memory _signature, address _signer) public pure returns (bool) {
return recoverSigner(_msgHash, _signature) == _signer;
}
效果如下,此为有效签名!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract VerifySignature {
// 1. 对真正的内容进行哈希处理,私钥最终只对这个进行签名
function getMessageHash(
address _to,
uint _amount
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount));
}
// 2. 对内容的哈希进行二次哈希,这个用于做verify处理
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
return
keccak256(
//这是标准字符串: \x19Ethereum Signed Message:\n
//32表示后面的哈希内容长度
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
// 3. 传入基础数据和签名,内部会计算出哈希值,并使用签名进行校验。
// 这个是最核心的方法,最终外部仅调用这个
function verify(bytes32 _msgHash, bytes memory _signature, address _signer) public pure returns (bool) {
return recoverSigner(_msgHash, _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 {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
}
}
单元测试:
npx hardhat test test/verifySignature.ts
核心逻辑为:
function mint(uint256 _tokenId, bytes memory _signature) external {
// 将用户地址和_tokenId打包消息
bytes32 _msgHash = getMessageHash(msg.sener, _tokenId);
// 计算以太坊签名消息
bytes32 _ethSignedMessageHash = getEthSignedMessageHash(_msgHash);
// ECDSA检验通过
require(verify(_ethSignedMessageHash, _signature), "Invalid signature");
// 地址没有mint过
require(!mintedAddress[_account], "Already minted!");
_mint(_account, _tokenId);
mintedAddress[_account] = true;
}
本文我们详细介绍了以太坊ECDSA链下签名的原理,并用代码进行了演示,链下签名与前面介绍的merkleTree都可以实现空投&白名单功能,具体选用哪一个取决于我们的业务场景,链下签名更加经济,但是更依赖中心化服务,当用户的白名单时动态产生时,使用链下签名更好;
现在我们已经有了链下签名的铺垫,下一节我们将介绍如何基于链下签名,实现多签功能,类似于genesis 多签钱包一样,敬请期待!
Wallet的signMessage和hardhat的Singer.signMessage效果相同。
加V入群:Adugii,公众号:阿杜在新加坡,一起抱团拥抱web3,下期见!
关于作者:国内第一批区块链布道者;2017年开始专注于区块链教育(btc, eth, fabric),目前base新加坡,专注海外defi,dex,元宇宙等业务方向。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!