亲爱的广场用户们,新年即将开启,我们希望您也能在 Gate 广场上留下专属印记,把 2026 的第一句话,留在 Gate 广场!发布您的 #我的2026第一帖,记录对 2026 的第一句期待、愿望或计划,与全球 Web3 用户共同迎接全新的旅程,创造专属于你的年度开篇篇章,解锁广场价值 $10,000 新年专属福利!
活动时间:2025/12/31 18:00 — 2026/01/15 23:59(UTC+8)
🎁 活动奖励:多发多奖,曝光拉满!
1️⃣ 2026 幸运大奖:从全部有效帖子中随机抽取 1 位,奖励包含:
2026U 仓位体验券
Gate 新年限定礼盒
全年广场首页推荐位曝光
2️⃣ 人气新年帖 TOP 1–10:根据发帖量及互动表现综合排名,奖励包含:
Gate 新年限定礼盒
广场精选帖 5 篇推荐曝光
3️⃣ 新手首帖加成奖励:活动前未在广场发帖的用户,活动期间首次发帖即可获得:
50U 仓位体验券
进入「新年新声」推荐榜单,额外曝光加持
4️⃣ 基础参与奖励:所有符合规则的用户中随机抽取 20 位,赠送新年 F1 红牛周边礼包
参与方式:
1️⃣ 带话题 #我的2026第一条帖 发帖,内容字数需要不少于 30 字
2️⃣ 内容方向不限,可以是以下内容:
写给 2026 的第一句话
新年目标与计划
Web3 领域探索及成长愿景
注意事项
• 禁止抄袭、洗稿及违规
探讨 Ed25519 实现原理与可延展性问题
撰文:阿为
Ed25519 是一个基于椭圆曲线的数字签名算法,它高效,安全且应用广泛。TLS 1.3, SSH, Tor, ZCash, WhatsApp 和 Signal 中都使用了它。本文主要讲解以下几点:
数学要点回顾
群的定义与性质
群论是抽象代数研究的内容,但抽象代数的一些思想是程序员非常熟悉的。面向对象中的继承就是一个很好的例子,我们都知道子类继承了父类后,就能使用父类中定义的方法。可以将抽象代数理解为对一个抽象的数据结构定义了一些性质,由这些性质推导出来的定理对于所有的子类都成立。
沿用刚刚的比喻,来看看群(group)这个数据结构是如何定义的。
一个群由一个操作(任何的二元操作用 记)和一些元素构成,同时满足如下性质:
由此可以推出许多有意思的定理:
举几个例子:
被一笔带过的群论术语
拉格朗日定理
现在介绍一个非常有意思的定理,这个定理的推导在文末引用的视频中。
「群的阶能被子群的阶整除。」
为什么说这个定理有意思呢,不仅仅因为它的证明过程串起了刚刚介绍的许多知识,还因为下面的结论:
「如果一个群的阶是一个素数 ,那么根据拉格朗日定理,子群的阶一定是 或者 。除了 之外,群里的所有元素都是生成元。」
Ed25519 的实现
现在我们来讲 Ed25519,它是 EdDSA 算法的其中一种。EdDSA 有 11 个参数(这些参数的具体选择对于算法的安全和性能有很大的影响。Ed25519 的具体选择请参看链接(
另外,值得一提的是这套算法用到了一个叫 Curve25519(的椭圆曲线。对于椭圆曲线,我们只需知道,它上边有很多很多点,这些点相加能得到新的点,新的点还是在曲线上。这些点和这个加法能形成一个群。注意这里的椭圆曲线加法(是有特殊定义的。
我们约定如下记法:
这是个交互式的算法,但是没关系,有一个技巧叫做 the Fiat – Shamir heuristic(它可以把任意的交互式算法转化成非交互式的算法。最终我们会用非交互式算法。
数字签名算法都会给我们如下 API:
KeyGen 的实现
输出一个私钥和一个公钥:
pub fn generate(csprng: &mut T) -> SecretKeywhere
T: CryptoRng + RngCore,
{
let mut sk: SecretKey = SecretKey([0u8; 32]);
csprng.fill_bytes(&mut sk.0);
sk
}
pub struct ExpandedSecretKey { // xseed pub(crate) key: Scalar, // a
pub(crate) nonce: [u8; 32], // nonce
}
fn from(secret_key: &'a SecretKey) -> ExpandedSecretKey {
let mut h: Sha512 = Sha512::default();
let mut hash: [u8; 64] = [0u8; 64];
let mut lower: [u8; 32] = [0u8; 32];
let mut upper: [u8; 32] = [0u8; 32];
h.update(secret_key.as_bytes());
hash.copy_from_slice(h.finalize().as_slice());
lower.copy_from_slice(&hash[00…32]);
upper.copy_from_slice(&hash[32…64]);
// 这一步是 clamp
lower[0] &= 248;
lower[31] &= 63;
lower[31] |= 64;
ExpandedSecretKey{ key: Scalar::from_bits(lower), nonce: upper, }
}
pub struct ExpandedSecretKey { // xseed pub(crate) key: Scalar, // a
pub(crate) nonce: [u8; 32], // nonce
}
fn from(secret_key: &'a SecretKey) -> ExpandedSecretKey {
let mut h: Sha512 = Sha512::default();
let mut hash: [u8; 64] = [0u8; 64];
let mut lower: [u8; 32] = [0u8; 32];
let mut upper: [u8; 32] = [0u8; 32];
h.update(secret_key.as_bytes());
hash.copy_from_slice(h.finalize().as_slice());
lower.copy_from_slice(&hash[00…32]);
upper.copy_from_slice(&hash[32…64]);
// 这一步是 clamp
lower[0] &= 248;
lower[31] &= 63;
lower[31] |= 64;
ExpandedSecretKey{ key: Scalar::from_bits(lower), nonce: upper, }
}
Sign 的实现
这里可以提一下之前说的 Fiat Shamir 技巧,其实只需要把所有 Verifier 提供的随机数替换成一个哈希函数的结果即可。具体请看代码注释。
pub fn sign(&self, message: &[u8], public_key: &PublicKey) -> ed25519::Signature {
let mut h: Sha512 = Sha512::new();
let R: CompressedEdwardsY;
let r: Scalar;
let s: Scalar;
let k: Scalar;
h.update(&self.nonce);
h.update(&message);
r = Scalar::from_hash(h); // r 在我们交互式算法中是一个随机数,但是这里我们用了哈希。
R = (&r * &constants::ED25519_BASEPOINT_TABLE).compress(); // R = [r]B
h = Sha512::new();
h.update(R.as_bytes());
h.update(public_key.as_bytes());
h.update(&message);
k = Scalar::from_hash(h); // h = Sha512(R || A || M)
s = &(&k * &self.key) + &r; // s = r + h * a,h 原本是随机数
InternalSignature { R, s }.into()
}
Verify 的实现
impl Verifiered25519::Signature for PublicKey {
#[allow(non_snake_case)]
fn verify(
&self,
message: &[u8],
signature: &ed25519::Signature
) -> Result<(), SignatureError>
{
let signature = InternalSignature::try_from(signature)?;
let mut h: Sha512 = Sha512::new();
let R: EdwardsPoint;
let k: Scalar;
let minus_A: EdwardsPoint = -self.1;
h.update(signature.R.as_bytes());
h.update(self.as_bytes());
h.update(&message);
k = Scalar::from_hash(h); // h 的计算和 sign 中一样,h=Sha512(R||A||M)
R = EdwardsPoint::time_double_scalar_mul_basepoint(&k, &(minus_A), &signature.s); // R’ = [s]B - [h]A
if R.compress() == signature.R { // 如果 R’==R,那么验证结果为真。
Ok(())
} else {
Err(InternalError::VerifyError.into())
}
}
}
代码地址(
可延展性问题
密码学算法的实现和使用都有非常多要注意的地方。当我们说一个数字签名算法是安全的,一般指的是即使在攻击者能够获得任意消息的签名(Chosen Message Attack)的情况下,攻击者仍然不能伪造签名。Ed25519 满足这个性质,但不代表 Ed25519 是绝对安全的。在原始的论文中也提到,可延展性问题是可以接受的,且原始的算法就有这个问题。
这样,新构造的签名和旧的签名就都能被验证成功了。延展性问题告诉我们签名并不是相对 message 和公钥确定,当签名算法存在延展性问题且代码中假设签名是确定的情况下,就很有可能存在漏洞。
按照标准( 其实是没有可延展性问题的。因为在验证的过程中,我们会检查 是否小于 ,如果检查结果不为真,验证失败。但是标准(的出现晚于论文(因此在实际环境中,仍有 Ed25519 的实现存在可延展性问题,需要我们检查 的实现。
总结
致谢
感谢领先的⼀站式数字资产⾃托管服务商 Safeheron 提供的专业技术建议。