惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Register - Security
The Register - Security
V
Visual Studio Blog
aimingoo的专栏
aimingoo的专栏
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
量子位
C
Check Point Blog
博客园 - 【当耐特】
小众软件
小众软件
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Azure Blog
Microsoft Azure Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Engineering at Meta
Engineering at Meta
Recorded Future
Recorded Future
The Last Watchdog
The Last Watchdog
博客园 - Franky
N
Netflix TechBlog - Medium
Webroot Blog
Webroot Blog
A
About on SuperTechFans
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
D
Docker
S
Security Affairs
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
V2EX - 技术
V2EX - 技术
Jina AI
Jina AI
Help Net Security
Help Net Security
L
LangChain Blog
P
Proofpoint News Feed
The Cloudflare Blog
WordPress大学
WordPress大学
Google DeepMind News
Google DeepMind News
Schneier on Security
Schneier on Security
Recent Announcements
Recent Announcements
Attack and Defense Labs
Attack and Defense Labs
云风的 BLOG
云风的 BLOG
V
Vulnerabilities – Threatpost
Microsoft Security Blog
Microsoft Security Blog
H
Heimdal Security Blog
P
Proofpoint News Feed
O
OpenAI News
H
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
爱范儿
爱范儿
Security Archives - TechRepublic
Security Archives - TechRepublic

追梦人物的博客

0x05:Merkle Tree & Patricia Trie 0x04:ECDSA - 以太坊设计与实现 LeetCode 105 从前序与中序遍历构造二叉树 迭代算法原理 + 完整证明 0x03:Address - 以太坊设计与实现 0x00:专栏开篇 - 以太坊设计与实现 0x01:RLP 编码 - 以太坊设计与实现 Bellman-Ford 算法原理及其在 DeFi 套利中的应用 - 追梦人物的博客 迪杰斯特拉(Dijkstra)最短路径算法原理、实现与证明 - 追梦人物的博客 实战:CEX-DEX 稳定币套利监控程序开发 - 追梦人物的博客 CEX-DEX 稳定币套利模型 - 追梦人物的博客 Uniswap 手续费和协议费机制剖析 - 追梦人物的博客 Uniswap 流动性机制及相关数学原理分析 - 追梦人物的博客 uv 替代 pyenv + pipx + poetry 环境管理实践 Rust 项目从创建到发布 - 追梦人物的博客 VS Code 调试 Python - 追梦人物的博客 比特币跨市场套利的数学模型 - 追梦人物的博客 ERC-20 相关知识点总结 - 追梦人物的博客 Django 老项目如何从 SQLite 迁到 PostgreSQL 自动生成接口文档 - HelloDjango - django REST framework 教程 单元测试 - HelloDjango - django REST framework 教程 限制接口访问频率 - HelloDjango - django REST framework 教程 API 版本管理 - HelloDjango - django REST framework 教程 如何在 Windows 下搭建高效的 django 开发环境 拓展Python Markdown - 追梦人物的博客 加缓存为接口提速 - HelloDjango - django REST framework 教程 基于 drf-haystack 实现文章搜索接口 - HelloDjango 评论接口 - HelloDjango - django REST framework 教程 实现分类、标签、归档日期接口 - HelloDjango - django REST framework 教程 在接口返回Markdown解析后的内容 - HelloDjango - django REST framework 教程 文章详情接口 - HelloDjango - django REST framework 教程 分页 - HelloDjango - django REST framework 教程 使用视图集简化代码 - HelloDjango - django REST framework 教程 用类视图实现首页 API - HelloDjango - django REST framework 教程 实现博客首页文章列表 API - HelloDjango - django REST framework 教程 初始化 RESTful API 风格的博客系统 - HelloDjango django-rest-framework 是什么鬼? - HelloDjango - django REST framework 教程 结束 or 开始? - HelloDjango Coverage.py 统计测试覆盖率 - HelloDjango - Django博客教程(第二版) 单元测试:测试评论应用 - HelloDjango - Django博客教程(第二版) 单元测试:测试 blog 应用 - HelloDjango Django Haystack 全文检索与关键词高亮 - HelloDjango Django 博客实现简单的全文搜索 - HelloDjango - Django博客教程(第二版) 开启 Django 博客的 RSS 功能 - HelloDjango 统计各个分类和标签下的文章数 - HelloDjango - Django博客教程(第二版) 稳定易用的 Django 分页库,完善分页功能 - HelloDjango 通过 Django Pagination 实现简单分页 - HelloDjango 在脚本中使用 ORM:Faker 批量生成测试数据 - HelloDjango Django 官方推荐的姿势:类视图 - HelloDjango - Django博客教程(第二版) Django 使用 union 合并不同模型(Model) 的查询集(QuerySet) 开发博客文章阅读量统计功能 - HelloDjango - Django博客教程(第二版) 使用 Docker 让部署 Django 项目更加轻松 - HelloDjango 使用 Certbot 向 Let's Encrypt 免费申请 HTTPS 证书 - HelloDjango 使用 Fabric 自动化部署 - HelloDjango 博客代码开源啦 - 追梦人物的博客 (赠书)推荐一本django书籍:Django企业开发实战 - 追梦人物的博客 Nginx+Gunicorn+Supervisor 部署 Django 博客应用 - HelloDjango 优化博客功能细节,提升使用体验 - HelloDjango - Django博客教程(第二版) 交流的桥梁:评论功能 - HelloDjango - Django博客教程(第二版) 分类、归档和标签页 - HelloDjango - Django博客教程(第二版) 页面侧边栏:使用自定义模板标签 - HelloDjango - Django博客教程(第二版) 自动生成文章摘要 - HelloDjango - Django博客教程(第二版) Markdown 文章自动生成目录,提升阅读体验 - HelloDjango - Django博客教程(第二版) 让博客支持 Markdown 语法和代码高亮 - HelloDjango 开发博客文章详情页 - HelloDjango - Django博客教程(第二版) 创作后台开启,请开始你的表演 - HelloDjango - Django博客教程(第二版) 博客从“裸奔”到“有皮肤” - HelloDjango - Django博客教程(第二版) Django 的接客之道 - HelloDjango - Django博客教程(第二版) Django 迁移、操作数据库 - HelloDjango - Django博客教程(第二版) 创建 Django 博客的数据库模型 - HelloDjango 开始进入 django 开发之旅 - HelloDjango "空空如也"的博客应用 - HelloDjango - Django博客教程(第二版) 一种自顶而下的Python装饰器设计方法 - 追梦人物的博客 Python提取支付宝和微信支付二维码 - 追梦人物的博客 2018 - 我的学生生涯最后一年回顾 - 追梦人物的博客 批量清除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 筛选练习参考答案 - Vue 2.x Todo 教程练习参考答案 删除todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 编辑todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 添加todo练习参考答案 - Vue 2.x Todo 教程练习参考答案 标为完成练习参考答案 - Vue 2.x Todo 教程练习参考答案 入门仪式_Hello_Vue练习参考答案 - Vue 2.x Todo 教程练习参考答案 组件化todo应用 - Vue 2.x Todo 教程 批量清除todo - Vue 2.x Todo 教程 本地存储 - Vue 2.x Todo 教程 筛选 - Vue 2.x Todo 教程 还剩多少todo未完成 - Vue 2.x Todo 教程 自定义指令实现自动聚焦 - Vue 2.x Todo 教程 全部标为完成 - Vue 2.x Todo 教程 删除todo - Vue 2.x Todo 教程 编辑todo - Vue 2.x Todo 教程 添加todo - Vue 2.x Todo 教程 标为完成 - Vue 2.x Todo 教程 UI - Vue 2.x Todo 教程 显示todo列表 - Vue 2.x Todo 教程 入门仪式:Hello Vue - Vue 2.x Todo 教程 在学习django-rest-framework时收集的学习资料推荐 - 追梦人物的博客 区块链理论与应用研究小组成员招募书 - 追梦人物的博客 Python界网红,豆瓣工程师董伟明加了我的QQ后 - 追梦人物的博客 招募Django学习小组项目组核心成员 - 追梦人物的博客
0x02:Secp256k1 - 以太坊设计与实现
2026-04-01 · via 追梦人物的博客

如果你使用过加密货币钱包,在创建钱包时,它通常会给你生成 12 个单词,称为“助记词”,它会强制让你用笔抄下来(不允许你截图),这就是你钱包的“口令”,持有此口令者即可随意操作对应的钱包,因此绝对不能泄露。钱包创建后会显示一个地址,其他人可以向这个地址转账。

这里面实际涉及到这样一个转换过程:助记词 -> 私钥 -> 公钥 -> 地址。

其中助记词 -> 私钥的转换过程并不包含在以太坊的核心实现中,而是由一个外部规范定义,主要由钱包应用实现。私钥 -> 公钥 -> 地址的转换过程属于以太坊的核心实现,私钥 -> 公钥是本篇关注的内容,公钥 -> 地址的转换将在下一篇详细介绍。

可以看到,私钥 -> 公钥这个链条必须是单向转换的,因为公钥会被分享出去,如果公钥可以反过来推出私钥,那就毫无安全可言了。提到单向转换,可能最容易想到的就是哈希函数。但是私钥和公钥在以太坊中的一个重要功能是私钥签名,公钥验证,哈希函数不具备这样的性质。需要选择一个数学性质更为良好的工具来做这个单向转换,比特币选择了 Secp256k1,以太坊则直接沿用了它前辈的选择。

本篇我们来探索私钥 -> 公钥转换涉及的 Secp256k1 相关概念,彻底理解这一过程。

群和域

为了更好的理解 Secp256k1,我们需要先来回顾一些抽象代数的基础概念。虽然这门课程很多专业都要到研究生阶段才会开设,但这里我们只需要了解基础的概念就行了,即使以前没有接触过,用小学学过的整数加减乘除类比就能理解。

群(Group)是抽象代数中最基础的代数结构之一,由非空集合和一个二元运算构成,满足四条公理,具体定义如下:

设非空集合为 \(G\)\(+\) 是定义在 \(G\) 上的二元运算,若 \((G,+)\) 满足以下四条群公理,则称 \((G,+)\) 为一个

  1. 封闭性:对任意的 \(a,b\in G\)\(a+b\in G\)
  2. 结合律:对任意的 \(a,b,c\in G\)\((a+b)+c = a+(b+c)\)
  3. 存在单位元:存在 \(e\in G\),使得对任意的 \(a\in G\)\(e+a = a+e = a\),该元素 \(e\) 称为群 \(G\) 的单位元
  4. 存在逆元:对任意的 \(a\in G\),存在 \(a^{-1}\in G\),使得 \(a+a^{-1} = a^{-1}+a = e\),元素 \(a^{-1}\) 称为元素 \(a\) 的逆元

例如整数和加法便构成一个群,称为整数加群,其中 0 是单位元,\(a\) 的逆元为其相反数 \(-a\)

特殊的,如果群还满足交换律(任意 \(a,b \in G\)\(a + b=b + a\)),则称为交换群(阿贝尔群,Abelian Group),显然整数加群是一个交换群。

由于结合律,\(a+a+...+a\) 结果无歧义,因此可将这种同一元素多次相加的形式简记为乘法形式 \(k{a}\),且规定若 \(k=0\)\(ka\) 的结果为单位元。

若群中存在元素 \(g \in G\),使得 \(G\) 中任意元素均可表示为 \(kg\)\(k\) 为整数),则称 \((G,+)\) 为循环群,元素 \(g\) 称为 \(G\) 的生成元(generator),记为 \(G=⟨g⟩\)。整数加群是一个循环群,其生成元是 1。

域(Field)是比群更复杂的代数结构,由非空集合和两个二元运算(分别记作加法和乘法)构成,其中加法构成交换群,非零元的乘法也构成交换群,且乘法对加法满足分配律,具体定义如下:

设非空集合为 \(F\),在 \(F\) 上定义两个二元运算:\(+\)(加法)和 \(\cdot\)(乘法),若 \((F, + , \cdot)\) 满足以下三组公理,则称 \(F\) 为一个

  1. 加法交换群\((F,+)\) 是交换群
  2. 非零元乘法交换群:设 \(F^* = F \setminus \{e\}\)\(F\) 中去掉加法单位元 \(e\)),则 \((F^*, \cdot)\) 是交换群
  3. 乘法对加法的分配律:对任意 \(a,b,c \in F\),有\(a \cdot (b+c) = a \cdot b + a \cdot c\)\((b+c) \cdot a = b \cdot a + c \cdot a\)(左右分配律)。

注意这里加法和乘法只是对两种运算的抽象称呼,加法和乘法是两种独立运算,不要将乘法和上一节中介绍的多个相同群元素相加的乘法简记形式(\(a+a+...+a = ka\))混淆。

若域中元素个数 \(q\) 有限,则称为有限域(伽罗瓦域,Galois Field),记为 \(GF(q)\)\(F_q\)\(q\) 为域的阶。

和本篇内容息息相关的一个域叫做 \(p\) 阶素数域 \(F_p\)。集合 \(F_p = \{0, 1, 2, ..., p-1\}\),其中 \(p\) 为素数。域上的加法和乘法运算都是模 \(p\) 的:\(a + b \equiv (a + b) \pmod p\)\(a \times b \equiv (a \times b) \pmod p\)。可以证明以上集合和运算构成一个域(套入域的三组公理可以验证,除了乘法逆元不那么显然以外,其他性质均继承自整数的加法和乘法,关于乘法逆元的存在性可问下 AI,这里不再占用过多篇幅进行说明)。

椭圆曲线

定义

Secp256k1 的椭圆曲线定义为:

$\(y^2 \equiv x^3 + 7 \pmod p\)$
其中 \(\equiv\) 是同余符号,可以理解为 \(y^2\)\(x^3 + 7\)\(p\) 的余数相同。椭圆曲线上的有效点是所有满足方程 \(y^2 \equiv x^3 + 7 \pmod p\) 的坐标对 \((x, y)\),其中 \(x, y \in F_p\)\(F_p\) 为上一节提到的 \(p\) 阶素数域。

数学家们发现的椭圆曲线的一些性质:

  1. 关于 x 轴对称
  2. 非 x 轴对称的两点之连线(可为同一个点,此时退化为切线),必与曲线上某点相交

以下是一个椭圆曲线的可视化示例:

elliptic_curve.png

给定点 \((x, y)\),很容易验证其是否在椭圆曲线上。👇下面是 Python 写的验算程序:

# secp256k1 曲线参数
p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
a = 0
b = 7

# 生成点 G 的坐标
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8


def is_on_curve(x, y, p, a, b):
    """验证点 (x, y) 是否满足椭圆曲线方程 y² ≡ x³ + ax + b (mod p)"""
    left = (y * y) % p
    right = (x * x * x + a * x + b) % p
    return left == right


# 验证生成点 G
print(f"生成点 G ({hex(Gx)}, {hex(Gy)}) 在曲线上: {is_on_curve(Gx, Gy, p, a, b)}")

x = 0x6E145CCEF1033DEA239875DD00DFB4FEE6E3348B84985C92F103444683BAE07B
y = 0x83B5C38E5E2B0C8529D7FA3F64D46DAA1ECE2D9AC14CAB9477D042C84C32CCD0
print(f"验证点 ({hex(x)}, {hex(y)}) 在曲线上: {is_on_curve(x, y, p, a, b)}")

加法运算

给定两个不同的点 \(P = (x_1, y_1)\)\(Q = (x_2, y_2)\),且 \(P \neq -Q\),它们的和 \(R = P + Q = (x_3, y_3)\) 定义如下:

几何规则:通过 \(P\)\(Q\) 画一条直线,这条直线与椭圆曲线相交于第三个点,取该点关于 x 轴的对称点,即为 \(R\)

elliptic_curve_add_p1_p2.png

给定两个相同的点 \(P = (x_1, y_1)\),它们的和 \(R = P + P = 2P = (x_3, y_3)\) 定义如下:

几何规则:在 \(P\) 点处作椭圆曲线的切线,这条切线与椭圆曲线相交于第二个点,取该点关于 x 轴的对称点,即为 \(2P\)

elliptic_curve_add_p1_p1.png
如果两个点关于 x 轴对称,从图上看没有交点,但是我们特殊规定其与曲线交于无穷远点,记作 \(O\)

elliptic_curve_add_vertically.png

另特殊规定,对于任意点 \(P\),其和无穷远点相加都有:

\[P + O = O + P = P\]

点的逆元

\(P = (x, y)\) 关于 x 轴的对称点 \(-P\) 是其逆元。在椭圆曲线上,逆元的定义非常简单:

\[-P = (x, -y \pmod p) = (x, p - y)\]

点与它的逆元相加,结果是无穷远点:

\[P + (-P) = O\]

群结构

根据前面的分析:

  1. 椭圆曲线(含无穷远点)上两个点相加,结果仍在曲线上,满足封闭性
  2. 无穷远点充当了单位元的功能,即存在单位元
  3. \(P\) 关于 x 轴对称的点 \(-P\) 是其逆元,即存在逆元
  4. 加法满足结合律(较为复杂,此处略过)和交换律

因此椭圆曲线上的点(包括无穷远点)构成一个加法交换群。进一步地还可以证明,这是一个循环群。

曲线参数

以下是椭圆曲线 secp256k1 的一些参数值,包括素数 \(p\)、阶 \(N\) 和生成点 \(G\) 的坐标。

素数 \(p\)(定义域的阶):

\(p = 2^{256} - 2^{32} - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1\)

十进制: 115792089237316195423570985008687907853269984665640564039457584007908834671663

十六进制:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F

\(N\)(椭圆曲线所有点含无穷远点的总数):
十进制:

115792089237316195423570985008687907852837564279074904382605163141518161494337

十六进制:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

生成点 \(G\)(基点,Base Point)的坐标:

Gx (十六进制):0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798

Gx (十进制): 55066263022277343669578718895168534326250603453777594175500187360389116729240

Gy (十六进制):0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8

Gy (十进制): 32670510020758816978083085130507043184471273380659243275938904335757337482424

生成点 G 是椭圆曲线的基点,可以生成曲线上的所有其它点。

私钥

私钥是一个在特定范围内的随机整数。具体来说,私钥 \(d\) 满足:

\[1 \leq d < N\]

其中 \(N\) 是 secp256k1 椭圆曲线的阶,即椭圆曲线所有点(含无穷远点)的总数。

可见私钥并无任何特殊性,它仅仅只是一个取值范围为 \([1, N)\) 的整数。但是私钥的选择必须极其讲究,不能选择容易被人猜到或者瞎撞就能撞到的值。目前被广泛采用的一种标准是由 BIP39 定义的助记词方案,你在加密钱包中看到的 12 个单词助记词就是这个标准规定的的产物,它在保证私钥熵源的不可猜测性(高安全性)的同时,兼顾了人类可记忆性、易备份性与输入容错性。

公钥

从私钥 \(d\) 到公钥 \(Q\) 的转换非常简单:

\[ Q = d \times G \]

其中,\(G\) 是 secp256k1 的生成点。乘法是在此前关于群的章节中定义的乘法,\(d \times G\) 即点 \(G\) 相加 \(d\) 次。

前面已经说过,secp256k1 椭圆曲线的点构成循环群,所以公钥也是椭圆曲线上的一个点 \((x, y)\),其中 \(x, y \in F_p\)。在实际应用中,公钥有两种主要的序列化格式:未压缩格式(uncompressed)和压缩格式(compressed)。

未压缩格式占 65 字节,格式为 0x04 前缀 + 32 字节 x 坐标 + 32 字节 y 坐标。例如生成点 G 的未压缩格式为:

0479BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8

压缩格式占 33 字节,格式为前缀 0x020x03 + 32 字节 x 坐标。前缀 0x02 表示 y 坐标是偶数,0x03 表示 y 坐标是奇数。压缩格式的原理是:已知椭圆曲线方程 \(y^2 \equiv x^3 + 7 \pmod p\),如果知道 x 坐标,可以解出 \(y \equiv \pm\sqrt{x^3 + 7} \pmod p\)。对于每个 x,最多有两个对应的 y 值:\(y\)\(-y\)。这两个值一个是偶数,一个是奇数(因为 \(p\) 是奇数),因此只需要一个前缀位来区分。

压缩格式将公钥大小从 65 字节减少到 33 字节,节省了近 50% 的存储空间和传输带宽。以太坊的私钥导入格式(如 Keystore 文件)中通常存储未压缩公钥,但在交易签名和地址生成时使用压缩公钥。

go-ethereum 中的实现

本节阅读提示

本篇重点主要还是理解 secp256k1 椭圆曲线的数学性质和私钥到公钥的转换机制,具体的代码实现细节偏密码学,了解有哪些实用的函数和接口就好,对理解以太坊本身没有太大帮助,所以看看就好。

go-ethereum 以太坊客户端中包含了完整的 secp256k1 椭圆曲线实现。这些实现位于 crypto/secp256k1 包中,提供了私钥生成、公钥生成、点运算等核心功能。

实现概述

go-ethereum 提供了两种实现方式:
- Go 纯实现:完全用 Go 语言编写的实现,位于 curve.goscalar_mult_nocgo.go
- C 优化实现:调用 libsecp256k1 库的优化实现,位于 scalar_mult_cgo.go

外部统一使用 Go 接口,内部会根据编译配置自动选择最优实现。

曲线参数定义

crypto/secp256k1/curve.go 文件中定义了 secp256k1 的所有参数:

// BitCurve 表示一条 Koblitz 曲线(a=0 的椭圆曲线)
type BitCurve struct {
    P       *big.Int // 有限域的素数
    N       *big.Int // 曲线的阶(order)
    B       *big.Int // 椭圆曲线方程的常数项(y² = x³ + b)
    Gx, Gy  *big.Int // 生成点 G 的 (x, y) 坐标
    BitSize int      // 位的长度(256位)
}

// secp256k1 曲线参数(在包初始化时设置)
var theCurve = new(BitCurve)

func init() {
    // 素数 p:定义有限域 F_p
    theCurve.P, _ = new(big.Int).SetString(
        "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 0)

    // 阶 N:曲线上有效点的总数
    theCurve.N, _ = new(big.Int).SetString(
        "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 0)

    // 常数 b = 7(曲线方程 y² = x³ + 7)
    theCurve.B, _ = new(big.Int).SetString(
        "0x0000000000000000000000000000000000000000000000000000000000000007", 0)

    // 生成点 G 的 x 坐标
    theCurve.Gx, _ = new(big.Int).SetString(
        "0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 0)

    // 生成点 G 的 y 坐标
    theCurve.Gy, _ = new(big.Int).SetString(
        "0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 0)

    // 位长度:256位
    theCurve.BitSize = 256
}

// S256 返回 secp256k1 椭圆曲线实例
func S256() *BitCurve {
    return theCurve
}

这些参数与我们在前面的章节中讨论的完全一致。S256() 函数是获取 secp256k1 曲线实例的入口,所有椭圆曲线操作都通过这个实例进行。

私钥生成

私钥生成在 crypto/crypto.go 中实现,使用 Go 标准库的密码学安全随机数生成器:

import (
    "crypto/ecdsa"
    "crypto/rand"
    "github.com/ethereum/go-ethereum/crypto/secp256k1"
)

// GenerateKey 生成一个新的 secp256k1 私钥
func GenerateKey() (*ecdsa.PrivateKey, error) {
    // 使用 S256() 获取 secp256k1 曲线
    // rand.Reader 提供密码学安全的随机数
    return ecdsa.GenerateKey(S256(), rand.Reader)
}

公钥生成

从私钥生成公钥通过标量乘法实现。crypto/secp256k1/curve.go 中的 ScalarBaseMult 函数:

// ScalarBaseMult 计算 k × G,其中 G 是生成点,k 是标量(私钥)
// 参数 k 是大端序(big-endian)字节数组
// 返回值是公钥的 (x, y) 坐标
func (bitCurve *BitCurve) ScalarBaseMult(k []byte) (*big.Int, *big.Int) {
    // 调用通用标量乘法函数,以生成点 G 为基点
    return bitCurve.ScalarMult(bitCurve.Gx, bitCurve.Gy, k)
}

ScalarBaseMultScalarMult 的特例,专门用于基点 G 的标量乘法。这个函数会被编译器优化,因为基点是固定的,可以使用预计算表加速。

通用的标量乘法函数 ScalarMult 实现了完整的 \(k \times P\) 运算(P 可以是任意点)。go-ethereum 提供了两个版本:Go 版本(scalar_mult_nocgo.go)和 C 优化版本(scalar_mult_cgo.go)。C 版本使用了 libsecp256k1 库的高性能实现。

点运算

点运算包括点加法(Add)和点加倍(Double),是椭圆曲线运算的基础。

点加法

// Add 计算 (x1, y1) + (x2, y2)
// 返回和点的 (x, y) 坐标
func (bitCurve *BitCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) {
    // 处理无穷远点(单位元)
    // 如果一个点是无穷远点,返回另一个点
    if x1.Sign() == 0 && y1.Sign() == 0 {
        return x2, y2
    }
    if x2.Sign() == 0 && y2.Sign() == 0 {
        return x1, y1
    }

    z := new(big.Int).SetInt64(1) // 仿射坐标的 z = 1

    // 如果两个点相同,调用点加倍
    if x1.Cmp(x2) == 0 && y1.Cmp(y2) == 0 {
        return bitCurve.affineFromJacobian(bitCurve.doubleJacobian(x1, y1, z))
    }

    // 否则调用 Jacobian 坐标系下的点加法
    return bitCurve.affineFromJacobian(bitCurve.addJacobian(x1, y1, z, x2, y2, z))
}

点加法的实现中,有一个重要的优化:Jacobian 坐标系

Jacobian 坐标系优化

在仿射坐标(affine coordinates)中,椭圆曲线的点用 \((x, y)\) 表示。在 Jacobian 坐标系中,点用 \((x, y, z)\) 表示,其中:

\[x_{\text{affine}} = \frac{x}{z^2} \pmod p\]

\[y_{\text{affine}} = \frac{y}{z^3} \pmod p\]

Jacobian 坐标系的优势在于:点加法和点加倍运算不需要进行昂贵的模逆运算(modular inversion)。模逆运算的计算复杂度远高于模乘法,是椭圆曲线运算的主要性能瓶颈。

在 Jacobian 坐标系下完成一系列运算后,通过 affineFromJacobian 函数转换回仿射坐标:

// affineFromJacobian 将 Jacobian 坐标转换为仿射坐标
// 输入:(x, y, z),输出:(xOut, yOut)
func (bitCurve *BitCurve) affineFromJacobian(x, y, z *big.Int) (xOut, yOut *big.Int) {
    // 如果 z = 0,表示无穷远点
    if z.Sign() == 0 {
        return new(big.Int), new(big.Int)
    }

    // 计算 z^(-1) mod p
    zinv := new(big.Int).ModInverse(z, bitCurve.P)

    // 计算 z^(-2)
    zinvsq := new(big.Int).Mul(zinv, zinv)

    // xOut = x * z^(-2) mod p
    xOut = new(big.Int).Mul(x, zinvsq)
    xOut.Mod(xOut, bitCurve.P)

    // yOut = y * z^(-3) mod p
    zinvsq.Mul(zinvsq, zinv)
    yOut = new(big.Int).Mul(y, zinvsq)
    yOut.Mod(yOut, bitCurve.P)

    return
}

点加倍

// Double 计算 2 × (x, y) = (x, y) + (x, y)
func (bitCurve *BitCurve) Double(x, y *big.Int) (*big.Int, *big.Int) {
    z := new(big.Int).SetInt64(1) // 仿射坐标的 z = 1
    // 在 Jacobian 坐标系下执行点加倍,然后转换回仿射坐标
    return bitCurve.affineFromJacobian(bitCurve.doubleJacobian(x, y, z))
}

doubleJacobian 函数实现了 Jacobian 坐标系下的点加倍运算,使用了优化的算法减少模运算次数。

公钥序列化

go-ethereum 提供了公钥的序列化和反序列化函数,支持未压缩格式:

// Marshal 将点 (x, y) 序列化为未压缩格式
// 格式:0x04 + x(32字节) + y(32字节),共65字节
func (bitCurve *BitCurve) Marshal(x, y *big.Int) []byte {
    byteLen := (bitCurve.BitSize + 7) >> 3 // 计算字节长度:(256+7)/8 = 32
    ret := make([]byte, 1+2*byteLen)
    ret[0] = 4 // 未压缩格式的前缀

    // 将 x 和 y 写入字节数组(大端序)
    math.ReadBits(x, ret[1:1+byteLen])
    math.ReadBits(y, ret[1+byteLen:])
    return ret
}

// Unmarshal 将序列化的数据解析为点 (x, y)
func (bitCurve *BitCurve) Unmarshal(data []byte) (x, y *big.Int) {
    byteLen := (bitCurve.BitSize + 7) >> 3

    // 验证数据长度和前缀
    if len(data) != 1+2*byteLen {
        return
    }
    if data[0] != 4 { // 必须是未压缩格式的前缀
        return
    }

    // 解析 x 和 y
    x = new(big.Int).SetBytes(data[1 : 1+byteLen])
    y = new(big.Int).SetBytes(data[1+byteLen:])
    return
}

这些函数与之前讨论的公钥格式完全对应。

总结

  • secp256k1 椭圆曲线定义在 \(p\) 阶素数域上,方程为 \(y^2 \equiv x^3 + 7 \pmod p\),曲线上的点构成加法群,且是循环群,阶为 \(N\),选择群中的一个点作为生成点 \(G\)
  • 私钥 \(d\)\((0,N]\) 的一个整数,私钥的选择要尽可能随机,不能被爆破
  • 公钥 \(Q = d \times G\)

-- EOF --