一文读懂以太坊签名:ECDSA、RLP、EIP155、EIP191、EIP712

·

通俗拆解交易签名、消息签名、重放攻击、EIP 设计思路,并给出现成代码示例。

1. 以太坊签名核心价值

签名在链上解决三大难题:

用一句话概括:链上签名如同现实世界合同上的手写签名,但公开透明、零信任、防篡改、不可伪冒


2. 签名与验证的流程

2.1 核心算法:ECDSA

以太坊采用椭圆曲线数字签名算法(ECDSA,前文已删)。

摘要版流程:

  1. 签名:消息 + 私钥 → r、s、v
  2. 验证:消息 + 签名 → 恢复公钥 → 校验地址是否匹配

👉 查看私钥、公钥、地址三者的完整推导过程

2.2 关键参数说明

参数作用
r、s构成签名的主体,同时保证随机性
v额外 1 Byte 的恢复标志,通过它可降低 gas(以太坊 r/s 之外额外引入)
ChainId通过 EIP-155 被编码进 v,防范跨链重放

3. 交易签名实战

3.1 组成字段(RAW Transaction Fields)

const tx = {
  nonce: 5,
  gasPrice: 1e12,       // 1 Gwei
  gasLimit: 3000000,    // 上限
  to: null,             // 空:创建合约
  value: 0,
  data: "0x6080...abcd",// bytecode
  chainId: 1
};

3.2 RLP + Keccak256

  1. 按顺序 RLP 编码 原始字段。
  2. 对第 1 步结果 Keccak256 哈希 得到 32 字节消息摘要。
  3. ECDSA 签名 → 得到 v、r、s。
  4. 再次 RLP 编码 字段 + vrs 得到最终交易序列化串。

4. 超重击:重放攻击与防御

攻击防御方案
同链重放配置 唯一 nonce已用签名记录 mapping
跨链重放ChainId 写入签名并对接收链做校验
合约复用重放在签名中加入 合约地址 domainSeparator

👉 深入了解重放攻击的代码级防御脚本


5. 消息签名:链下验证的利器

链下签名不直接落区块,而是传给合约,合约再调用 ecrecover 做二次验证。最常见的三大场景:

  1. 白名单空投
  2. 代理授权(gasless)
  3. Permit(EIP-712)省去 approve 交易

5.1 通用消息格式 & 实验

MetaMask 的 personal_sign 会在消息前加固定前缀 "\x19Ethereum Signed Message:\n",这样浏览器甚至能在确认框里显示“人类可见”内容,有效防范钓鱼。

NFT 白名单举例

合约片段:

using ECDSA for bytes32;

/// @dev 链下项目方签名映射到白名单地址
function mintNFT(address account, uint256 tokenId, bytes calldata signature) external {
    bytes32 digest = keccak256(abi.encodePacked(account, tokenId)).toEthSignedMessageHash();
    require(digest.recover(signature) == _signer, "Invalid signature");
    _safeMint(account, tokenId);
}

签脚本:

const digest = ethers.utils.solidityKeccak256(
  ["address", "uint256"],
  [account, tokenId]
);
const sig = await signer.signMessage(ethers.utils.arrayify(digest));

6. EIP-191 vs EIP-712:场景决定选型

6.1 EIP-712 实战:Uniswap Permit

必填字段:

struct EIP712Domain {
    string  name;
    string  version;
    uint256 chainId;
    address verifyingContract;
}

struct Permit {
    address owner;
    address spender;
    uint256 value;
    uint256 nonce;
    uint256 deadline;
}

链下签名前端代码简写:

const domain = { name, version, chainId, verifyingContract: pairAddr };
const types = { Permit: [...] };
const value = { owner, spender, value, nonce, deadline };
const signature = await signer._signTypedData(domain, types, value);

6.2 面子里子的差异

维度通用消息EIP-712
钱包展示仅 raw表单化
重放防护需手动加域domainSeparator 天然隔离
用户体感“盲签”可视化确认,风险可察
实现成本极简需写 Domain 和 TypeHash

7. FAQ:快速排雷指南

Q1:为什么我的合约同样的输入签名每次都不同?
→ ECDSA 需要高质量的随机数 k,系统是安全的,这是预期行为。切勿复用字节级相同签名否则泄露私钥。

Q2:可以将 EIP-191 和 EIP-712 混在一起吗?
→ 不推荐。钱包只信任特定的 EIP-712 domainSeparator,混合会导致前端不一致及重放风险。

Q3:在测试网练手是否需要 ChainId?
→ 建议加。现在测试网 ID 多,省得后期跨网被撞签名。

Q4:为什么 MetaMask 的确认窗口一片乱码?
→ 没有用 EIP-712。把数据打包进 struct 并设置 domain,立即变“表格”。

Q5:如何测试“重放”漏洞?
→ 部署两份内容相同的合约 A、B,给 A 发出的签名在 B 执行即命中重放。修复后再做相同操作应被 revert。

Q6:合约升级了还能复用旧签名吗?
→ 在 domainSeparator 里加 address(this),升级后相当于不同域,旧签名失效。


8. 小结与实践清单

芯片级任务检查点
交易签名nonce、gas、ChainId、RLP、Keccak256
消息签名决定用 personal_sign 还是 EIP-712
防重放nonce、已用 mapping、domainSeparator、address、ChainId
交互钱包测试 eth_signTypedData_v4,确保人类能看懂内容

最后,祝各位开发者把“看似玄学”的以太坊签名玩得跟小孩积木一样顺手。

(完)