本文探讨了如何利用以太坊的 ecrecover
函数验证 Schnorr 签名。通过将 Schnorr 签名的验证过程与 ecrecover
的处理相结合,提供了一种低成本的签名验证方法,并提供了具体的 Solidity 实现代码。文中还引用了 Chainlink 的相关实现与安全性的注意事项。
这个想法基于 Vitalik 在这里的帖子:https://ethresear.ch/t/you-can-kinda-abuse-ecrecover-to-do-ecmul-in-secp256k1-today/2384
然而,它不是使用 ecrecover 来执行 ECMUL,而是可以被黑客攻击以便廉价验证 Schnorr 签名。
给定消息 m
、私钥 x
和 hash-to-scalar 函数 h
,从字段中随机选择值 k
。G
是曲线生成元。
还定义函数 address()
,该函数返回给定一个点的 20 字节以太坊地址。 (注意:这仅在黑客攻击中需要)
R = G*k
e = h(address(R) || m)
s = k + x*e
签名 = (e, s) 或 (R, s)
给定签名 (R, s)
、消息 m
和公钥 P
e = h(address(R) || m)
R' = G*s - P*e
检查 R == R'
或者,给定签名 (e, s)
R = G*s - P*e
e' = h(address(R) || m)
检查 e' == e
以太坊 ecrecover
返回一个地址(公钥的哈希),给定 ECDSA 签名。
给定消息 m
和 ECDSA 签名 (v, r, s)
,其中 v
表示点的 y 坐标的奇偶性,该点的 x 坐标为 r
ecrecover(m, v, r, s):
R = 从 r 和 v 派生的点
a = -G*m
b = R*s
Qr = a + b
Q = Qr * (1/r)
Q = (1/r) * (R*s - G*m) // 恢复的公钥
以太坊的 ecrecover 返回 64 字节公钥的 keccak256 哈希的最后 20 字节(请参见 https://github.com/ethereum/go-ethereum/blob/eb948962704397bb861fd4c0591b5056456edd4d/crypto/crypto.go#L275)
给定签名 (R, s)
、消息 m
和公钥 P
,我们可以将值输入到 ecrecover 中,以便返回的地址可以用于与挑战进行比较。
计算 e = H(address(R) || m)
和 P_x = P 的 x 坐标
传入:
m = -s*P_x
v = P 的奇偶性
r = P 的 x 坐标
s = -e*P_x
然后:
ecrecover(m=-s*P_x, v=0/1, r=P_x, s=-e*P_x):
P = 从 r 和 v 派生的点(公钥)
a = -G*(-s*P_x) = G*s*P_x
b = P*(-m*P_x) = -P*e*P_x
Q = (1/P_x) (a+b)
Q = (1/P_x)(G*s*P_x - P*e*P_x)
Q = G*s - P*e // 与上面的 schnorr 验证相同
返回值是 address(Q)
。
e' = h(address(Q) || m)
e' == e
来验证签名。// SPDX-License-Identifier: LGPLv3
pragma solidity ^0.8.0;
contract Schnorr {
// secp256k1 群体顺序
uint256 constant public Q =
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
// 奇偶性 := 公钥 y 坐标奇偶性 (27或28)
// px := 公钥 x 坐标
// 消息 := 32 字节消息
// e := schnorr 签名挑战
// s := schnorr 签名
function verify(
uint8 parity,
bytes32 px,
bytes32 message,
bytes32 e,
bytes32 s
) public pure returns (bool) {
// ecrecover 输入为 (m, v, r, s);
bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
require(sp != 0);
// ecrecover 预编译实现检查 `r` 和 `s`
// 输入不为零(在这种情况下,`px` 和 `ep`),因此我们不需要
// 检查它们是否为零。
address R = ecrecover(sp, parity, px, ep);
require(R != address(0), "ecrecover 失败");
return e == keccak256(
abi.encodePacked(R, uint8(parity), px, message)
);
}
}
Chainlink 已经在这里实现了类似的想法:https://github.com/smartcontractkit/chainlink/blob/bb214c5d7ec172de400a72a1d8851ff639c979d2/evm/v0.5/contracts/dev/SchnorrSECP256K1.sol,但是他们的实现使用的不止 ecrecover
和 keccak256
;它还检查签名公钥的 x 坐标是否小于半序列,等等。 但是,我认为 s < Q
检查是没有必要的;它似乎出现在黄皮书中,但在以太坊实际使用的 ecrecover 实现中没有。
如果你在生产环境中使用此内容,你可能需要将 block.chainId
添加到挑战中以防止重放攻击。
规范的 ecrecover 实现:
- 原文链接: hackmd.io/@nZ-twauPRISEa...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!