耿腾的博客

常期望安定,还期望即兴。

0%

Pubkey

1
pub struct Pubkey(pub(crate) [u8; 32]);

find_program_address

1
2
3
4
pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
Self::try_find_program_address(seeds, program_id)
.unwrap_or_else(|| panic!("Unable to find a viable program address bump seed"))
}

try_find_program_address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> {
// Perform the calculation inline, calling this from within a program is
// not supported
#[cfg(not(target_os = "solana"))]
{
let mut bump_seed = [std::u8::MAX]; // 从 std::u8::MAX 开始尝试
for _ in 0..std::u8::MAX {
{
let mut seeds_with_bump = seeds.to_vec();
seeds_with_bump.push(&bump_seed); // 将 bump 作为种子的最后一部分
match Self::create_program_address(&seeds_with_bump, program_id) {
Ok(address) => return Some((address, bump_seed[0])),
Err(PubkeyError::InvalidSeeds) => (),
_ => break,
}
}
bump_seed[0] -= 1; // 尝试失败了就减 1 再尝试
}
None
}
// Call via a system call to perform the calculation
#[cfg(target_os = "solana")]
{
// ...
}
}

create_program_address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
pub fn create_program_address(
seeds: &[&[u8]],
program_id: &Pubkey,
) -> Result<Pubkey, PubkeyError> {
if seeds.len() > MAX_SEEDS { // 种子最多有 16 个
return Err(PubkeyError::MaxSeedLengthExceeded);
}
for seed in seeds.iter() {
if seed.len() > MAX_SEED_LEN { // 每个种子最长 32
return Err(PubkeyError::MaxSeedLengthExceeded);
}
}

// Perform the calculation inline, calling this from within a program is
// not supported
#[cfg(not(target_os = "solana"))]
{
let mut hasher = crate::hash::Hasher::default(); // 就是一个 Sha256
for seed in seeds.iter() {
hasher.hash(seed);
}
// PDA_MARKER 是一个 21 字节长的字符串
// const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress";
hasher.hashv(&[program_id.as_ref(), PDA_MARKER]);
let hash = hasher.result();

if bytes_are_curve_point(hash) { // PDA 账户需要确保不在爱德华曲线上
return Err(PubkeyError::InvalidSeeds);
}

Ok(Pubkey::from(hash.to_bytes()))
}
// Call via a system call to perform the calculation
#[cfg(target_os = "solana")]
{
let mut bytes = [0; 32];
let result = unsafe {
crate::syscalls::sol_create_program_address(
seeds as *const _ as *const u8,
seeds.len() as u64,
program_id as *const _ as *const u8,
&mut bytes as *mut _ as *mut u8,
)
};
match result {
crate::entrypoint::SUCCESS => Ok(Pubkey::from(bytes)),
_ => Err(result.into()),
}
}
}

bytes_are_curve_point

1
2
3
4
5
6
7
8
9
10
pub fn bytes_are_curve_point<T: AsRef<[u8]>>(_bytes: T) -> bool {
#[cfg(not(target_os = "solana"))]
{
curve25519_dalek::edwards::CompressedEdwardsY::from_slice(_bytes.as_ref())
.decompress()
.is_some()
}
#[cfg(target_os = "solana")]
unimplemented!();
}

总结

整体流程

  1. [std::u8::MAX] (即 [255]) 开始作为 bump 尝试,值依次递减至 0,bump 就是最后一个种子。
  2. 检查种子数是否不超过 16 (包含 bump,也就是用户能输入的只有 15 个),所有种子长度不超过 32.
  3. 使用所有种子依次输入 Sha256 进行哈希,然后将 program_id 及一个 21 字节的常量 PDA_MARKER (b"ProgramDerivedAddress") 输入进行哈希。
  4. 计算哈希结果并判断是否在 Curve25519 上,如果不在,表示是合法的 PDA 地址,否则 bump 自减 1 从第二步开始重试。
  5. 如果从 [255][0] 都找不到,则返回 None / 报错。

Curve25519 是一种特定的爱德华曲线,它设计用于实现高效、安全的 Diffie-Hellman 密钥交换。

AccountInfo

solana_program::account_info::AccountInfo

账户信息(AccountInfo),合约中对于账户基本信息的引用(余额和数据可变)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// Account information
#[derive(Clone)]
#[repr(C)]
pub struct AccountInfo<'a> {
/// Public key of the account
pub key: &'a Pubkey,
/// The lamports in the account. Modifiable by programs.
pub lamports: Rc<RefCell<&'a mut u64>>,
/// The data held in this account. Modifiable by programs.
pub data: Rc<RefCell<&'a mut [u8]>>,
/// Program that owns this account
pub owner: &'a Pubkey,
/// The epoch at which this account will next owe rent
pub rent_epoch: Epoch,
/// Was the transaction signed by this account's public key?
pub is_signer: bool,
/// Is the account writable?
pub is_writable: bool,
/// This account's data contains a loaded program (and is now read-only)
pub executable: bool,
}

Account

anchor_lang::accounts::account::Account

账户(Account),对 AccountInfo 的包装,并能够自动反序列化 T

1
2
3
4
5
#[derive(Clone)]
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> {
account: T,
info: &'info AccountInfo<'info>,
}

基本功能

  1. 检查 T::owner() 是否等于 info.owner,这就要求 T: Owner。通常使用 #[account] 宏来自动为 T 实现 Owner,并使用 crate::ID() 也就是合约地址作为它的 owner。
  2. 此外,它还会保证 !(Account.info.owner == SystemProgram && Account.info.lamports() == 0),即账户所有者不是系统程序(必须是非系统合约),并且账户的 SOL 余额不能为 0。

AccountLoader

anchor_lang::accounts::account_loader::AccountLoader

账户加载器,用于按需零拷贝反序列化的提取器,类型 T 需要实现 ZeroCopy

1
2
3
4
5
#[derive(Clone)]
pub struct AccountLoader<'info, T: ZeroCopy + Owner> {
acc_info: &'info AccountInfo<'info>,
phantom: PhantomData<&'info T>,
}

基本功能

load_init: 在初始化时(只能)运行一次,得到一个 RefMut<T>,可读写。
load: 加载只读引用 Ref<T>
load_mut: 加载读写引用 RefMut<T>

InterfaceAccount

anchor_lang::accounts::interface_account::InterfaceAccount

接口账户,用来支持 T: Owners 的情况。即有多个程序拥有这种类型的账户数据,引入时是为了支持 token-2022 #2386

1
2
3
4
5
6
7
#[derive(Clone)]
pub struct InterfaceAccount<'info, T: AccountSerialize + AccountDeserialize + Clone> {
account: Account<'info, T>,
// The owner here is used to make sure that changes aren't incorrectly propagated
// to an account with a modified owner
owner: Pubkey,
}

基本功能

  1. 检查所有者,即 T::owners().contains(InterfaceAccount.info.owner)
  2. Account 的第二项。

Interface

anchor_lang::accounts::interface::Interface

接口,用来表示实现了某种接口的程序中的某一个。

1
2
#[derive(Clone)]
pub struct Interface<'info, T>(Program<'info, T>);

TODO: 例子。

Program

anchor_lang::accounts::program::Program

程序,表示一个程序/合约。

1
2
3
4
5
#[derive(Clone)]
pub struct Program<'info, T> {
info: &'info AccountInfo<'info>,
_phantom: PhantomData<T>,
}

Signer

anchor_lang::accounts::signer::Signer

签名者,检查 Signer.info.is_signer == true

1
2
3
4
#[derive(Debug, Clone)]
pub struct Signer<'info> {
info: &'info AccountInfo<'info>,
}

SystemAccount

anchor_lang::accounts::system_account::SystemAccount

系统账户,检查账户的拥有者是不是系统程序(即 SystemAccount.info.owner == SystemProgram)。

1
2
3
4
#[derive(Debug, Clone)]
pub struct SystemAccount<'info> {
info: &'info AccountInfo<'info>,
}

Sysvar

anchor_lang::accounts::sysvar::Sysvar

系统变量,检查账户是否是系统变量。

1
2
3
4
5

pub struct Sysvar<'info, T: solana_program::sysvar::Sysvar> {
info: &'info AccountInfo<'info>,
account: T,
}

TODO: 写一篇 post 单独介绍实现了 solana_program::sysvar::Sysvar 的类型。

UncheckedAccount

强调不做任何检查的账户,需要手动在合约指令中做检查。

1
2
#[derive(Debug, Clone)]
pub struct UncheckedAccount<'info>(&'info AccountInfo<'info>);

以下是各种索引算法的简要对比表,包括其原理、性能优势、适用场景和缺点。

索引算法 原理介绍 为什么性能高 适用场景 缺点/性能差的场景
B+ 树 B+ 树是一种平衡多叉树,数据仅存储在叶节点,所有叶节点按顺序连接,适合范围查询和顺序访问。 结构层次分明,叶节点通过链表相连,实现快速范围查找和顺序遍历。 适用于数据库索引、大量顺序读取或范围查询的场景。 高维数据或频繁更新的场景性能较差。插入、删除操作复杂,维护成本高。
倒排索引 建立词项到文档的映射关系,将每个词项关联到出现该词的文档列表,通常用于全文搜索。 通过直接访问词项位置,可快速找到包含该词的所有文档。 适用于文本搜索、日志检索等全文检索场景。 对于非文本数据无效,处理频繁更新的文档集时性能较差,占用内存大。
HNSW HNSW 基于分层的小世界图结构,通过近似最近邻的方法实现高效向量检索。 通过多层图索引和邻居节点搜索,实现快速的近似最近邻查找。 适用于向量相似度检索、推荐系统、图像搜索等高维向量场景。 构建和维护图的成本较高,尤其是频繁更新和插入向量的情况下。对高精度要求场景不适合。
跳表(Skip List) 通过多级链表实现快速搜索,每级链表跳过部分节点,提高查找速度。 基于多级链表,可在 O(log n) 时间内查找目标节点。 适用于键值存储、顺序查询需求的场景,作为简单替代结构。 在超大数据集或高频率更新的场景下不够高效,性能会下降。
LSM 树 LSM 树通过分层排序和合并,将数据写入磁盘前缓存在内存,适合高写入量的场景。 数据批量写入减少随机写的开销,分层合并提升读取效率。 适合写密集型的键值数据库,如 RocksDB、LevelDB。 读性能可能较差,特别是大量读操作时,需通过多层合并查询。
Trie 树 用树结构保存字符串的前缀,每个节点代表一个字符,用于高效的前缀匹配。 可以快速定位字符串的前缀路径,适合匹配和自动补全。 适用于自动补全、前缀匹配等需要快速定位前缀的场景。 占用内存较大,无法很好地处理非字符串数据,不适合范围查询。
Annoy 使用多棵随机 KD 树构建索引,用于近似最近邻查询,查询时从多棵树中查找相似点。 随机生成多棵树,提高查找相似度的效率,查询速度快。 适用于低维度、高性能要求的向量检索,推荐系统等。 处理高维数据时性能较差,存储空间需求大。更新不便。
LSH(Locality-Sensitive Hashing) 将相似数据哈希到相同的桶中,快速找到相似数据的近似最近邻索引方法。 通过哈希实现分桶,减少相似数据查询范围,查询速度快。 适用于低维向量的相似性搜索,处理文本等场景。 高维度数据上效率低,近似度精度差,占用较多内存。

简要总结

  • B+ 树倒排索引 是经典的数据库和全文搜索引擎中常用的索引结构,适合关系型数据和文本数据的检索。
  • HNSWAnnoy 是面向高维向量的近似最近邻索引结构,常用于推荐系统、语义搜索等场景。
  • LSM 树 适合写密集的场景,如键值数据库。
  • Trie 树跳表 更适合简单的键值存储和字符串匹配,但不适合复杂的数据查询和高维向量检索。
  • LSH 在维度较低的数据上表现较好,但对高维数据表现有限。

Why ZKP?

零知识证明 Zero-knowledge Proofs

其中的 Proof($\pi$) 必须是个有用的:$Theorem / Statement / Proposition$

  1. 不要把 Proof 这个词忘掉,到底证明了什么?
  2. 两个角色,证明者(prover)、验证者(verifier),是双方的。写代码的时候要时刻关注写的到底是哪部分,是证明者的代码,还是验证者的代码。

Prover Code: 需要关注,有没有产生一个有效的证明,产生这个证明的时候有没有泄漏不该泄漏的信息。
Verifier Code:关注,这个证明是不是有效的。

写代码的时候容易忘掉。
这是一个 Two-party interaction

  1. 关注以下三个性质:
  • 完备性(Completeness):诚实的证明者(honest prover)一定能成功证明。
  • 可靠性(Soundness):恶意的证明者(malicious prover)一定通不过验证。很难测试,需要非常认真、仔细、深入的考虑这个问题:如何保证 verifier 不会被欺骗。
  • 零知识(Zero-knowledge):恶意的验证者(malicious verifier)无法获得有用的知识。

关注 Meta Theorems (Completeness and Soundness)的证明
如何保证我们写的 babyplonk 是 sound 的。

  • Prover 和 Verifier 之间有大量的交互。Verifier 会发送大量的 challenge 给 Prover。challenge 越多,Proof Size 就越大。
  • 交互过程中的步骤顺序不能调换。
  • 实际项目中如果有安全漏洞是非常严重的。
  • 攻击本身也可能是 ZK 的,不知道谁干的。

Non-interactive

BlockChain 中经常使用 Non-interactive ZKP。 Fiat-Shamir Transformation 能把 public-coin protocol 转换成 non-interactive protocol。

public-coin 指的是掷硬币,公开的发随机数 challenge。
借助 Random Oracle (RO) 工具来辅助 Prover 来产生这些硬币。随机数不一定是由 verifier 作为来源,由可信第三方来提供也可以。
Prover 与 RO 交互,产生证明 $\pi$ 直接发送给 Verifier。不需要等待 Verifier 回复,Verifier 可以在未来异步验证这个证明。
通常使用一个足够安全的哈希函数(Cryptographic Hash Function)来替代 RO 的角色。没有严格的证明,大概率不会出问题。

Commitment

  1. $O(1)$ Size。无论数据多大,比如 10 GB 的文件,都可以产生一个很小的串来代表它。
  2. Binding。$CM(D1)\not ={CM(D2)} \iff D1\not ={D2}$。
  3. Hiding。能够保护数据,可以混入随机数。同一个文件两次 Commit 可以得到不同的 Commitment。

Blockchain: Put commitment on chain (EIP-4844,Rollup-blob)

Commit-and-Prove

alt text

Why zkSNARK

这是结诚浩的《图解密码技术》一书的读书笔记,我是在去北京的火车上把其中感兴趣的部分草草过完的。World of Z2O-K7E 只是建议过一下,我就只是过一下,感觉还是很有帮助的,至少看过了之后,我能感觉自己的大脑切换到了“密码学的上下文”,接收零知识证明相关的知识似乎更高效了。

基本常识

  1. 不要使用保密的密码算法(即隐蔽式的安全性),应该使用得到广为验证的公开的密码算法,而保护好密钥。
  2. 使用低强度的密码会给人一种错误的安全感,比不进行加密更危险。
  3. 任何密码总有一天会被破解,除了不实用的一次性密码本和传说中的量子密码。
  4. 密码只是信息安全的一部分,提防“社会工程学”。

对称密码

DES / 3DES

一种将 64 位明文加密为 64 位密文的对称密码算法,需要以分组为单位对输入进行处理(这样的密码算法称为分组密码,block cipher)。DES 已经可以在现实时间内被破解。

三重DES(3DES)是为了增加 DES 的强度设计的。三重 DES 并不是三轮加密,而是加密、解密、加密。如果三轮的密钥都相同,就跟 DES 一样了。

第一轮和第三轮使用同样的密钥则称为 DES-EDE2。
三轮都不同则称为 DES-EDE3。

AES

Rijndael 算法在 2000 年被选为 AES。

分组长度固定为 128 比特,密钥长度有 128、192、256 比特三种。

加密过程:SubBytes -> ShiftRows -> MixColumns -> AddRoundKey

解密过程: AddRoundKey -> InvMixColumns -> InvShiftRows -> InvSubBytes

如何选择

不用 DES,少用 3DES,尽量用 AES。

分组密码的模式

  • ECB(Electronic CodeBook,电子密码本):简单,快速,支持并行计算;明文中重复排列会反映在密文中,并且通过删除、替换密文分组可以对明文进行操作,对包含某些比特错误的密文解密时对应的分组会出错,不能抵御重放攻击。不要用。
  • CBC(Cipher Book Chaining,密文分组连接模式):推荐使用,重复的明文不会导致重复的密文,可以解密任意密文分组。必须是先加密再 XOR,如果反过来,则其效果等同于 ECB,可以直接从 IV (初始化向量)开始,依次将前后两组密文执行 XOR,得到的每组结果恰好就等于 ECB 每组的结果。
  • CFB / OFB:不需要填充(padding)。
  • CTR(CounTeR,计数器模式):推荐使用,主动攻击者反转密文分组中的某些比特时,明文分组中相对应的比特也会被反转。

公钥密码

RSA 的基本步骤:

  1. 取两个比较大的质数 p 和 q。
  2. 求两者的乘积 $N = p \times q$。
  3. ,即 p 和 q 的最小公倍数;有的资料说的是求欧拉函数值 $\phi = (p-1) \times (q-1)$,以下统称 $\phi$。

GPT: 两者都有各自的使用场景,但在实际使用中,求欧拉函数值 ($\phi$) 更为常见。

  1. 选择公钥 $E$,$E$ 与 $\phi$ 互质并且 $1 < E < \phi$。
  2. 求私钥 $D$,$D \times E \equiv 1$ (mod $\phi$),即 D 为 E 的乘法逆元。
  3. 这样得到公钥为 $(E, N)$,私钥为 $(D, N)$。

一个使用小质数实现的简单的 RSA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
use num::BigInt;
use rand::seq::index::sample;
use rand::{thread_rng, Rng};

const SMALL_PRIMES: [u64; 50] = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193,
197, 199, 211, 223, 227, 229,
];

#[derive(Debug)]
struct KeyPair {
private_key: u64,
public_key: u64,
modulus: u64,
}

impl KeyPair {
pub fn generate() -> Self {
let mut rng = thread_rng();
let indices = sample(&mut rng, SMALL_PRIMES.len(), 2);
// use crate rand to select two random prime numbers
let p = SMALL_PRIMES[indices.index(0)];
let q = SMALL_PRIMES[indices.index(1)];
let modulus = p * q;
let phi = (p - 1) * (q - 1);
let public_key = loop {
let public_key = SMALL_PRIMES[thread_rng().gen_range(0..SMALL_PRIMES.len())];
if public_key < phi && gcd(public_key, phi) == 1 {
break public_key;
}
};

let mut private_key = 1;
while (public_key * private_key) % phi != 1 {
private_key += 1;
}

Self {
private_key,
public_key,
modulus,
}
}

pub fn encrypt(&self, message: BigInt) -> BigInt {
message.pow(self.public_key as u32) % self.modulus
}

pub fn decrypt(&self, encrypted_message: BigInt) -> BigInt {
encrypted_message.pow(self.private_key as u32) % self.modulus
}

// getters

pub fn get_private_key(&self) -> (u64, u64) {
(self.private_key, self.modulus)
}

pub fn get_public_key(&self) -> (u64, u64) {
(self.public_key, self.modulus)
}

pub fn get_modulus(&self) -> u64 {
self.modulus
}
}

fn gcd(p0: u64, p1: u64) -> u64 {
let mut p0 = p0;
let mut p1 = p1;
while p1 != 0 {
let temp = p1;
p1 = p0 % p1;
p0 = temp;
}
p0
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_rsa() {
for _ in 0..100 {
let key_pair = KeyPair::generate();
let message = BigInt::from(rand::thread_rng().gen_range(6..key_pair.get_modulus()));
let encrypted_message = key_pair.encrypt(message.clone());
let decrypted_message = key_pair.decrypt(encrypted_message.clone());
assert_eq!(decrypted_message, message);
println!(
"public key: ({}, {}), private key: ({}, {}), message: {}, encrypted: {}, decrypted: {}",
key_pair.get_public_key().0, key_pair.get_public_key().1,
key_pair.get_private_key().0, key_pair.get_private_key().1,
message,
encrypted_message,
decrypted_message
)
}
}
}

单向散列函数

单向散列函数(也称为消息摘要函数、哈希函数、杂凑函数),用来保证消息的完整性(integrity),也称为一致性。

输入的消息也称为原像(pre-image)。

输出的散列值也称为消息摘要(message digest)或者指纹(fingerprint)。

难以发现碰撞的性质称为抗碰撞性(collision resistance)。

算法选择

MD5 是不安全的。

SHA-1 不应该用于新用途。

SHA-2 (SHA-256以上) 、SHA-3 是安全的,可以使用。

消息认证码

消息认证码(Message Authentication Code)是一种确认完整性并进行认证的技术。

实现方法

  • 单向散列函数,例如 HMAC。
  • 分组密码,例如 AES-CMAC。
  • 其他,例如流密码和公钥密码。

数字签名

使用公钥密码(比如 RSA、椭圆曲线)来实现。

  • 绝对不要对意思不清楚的消息进行签名,可能无意中解密了公钥加密的密文。
  • 依赖公钥基础设施,公钥必须属于真正的发送者,即使用证书来确认。

密钥交换

Diffie-Hellman

一个使用小质数实现的简单的 Diffie-Hellman:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
use num::BigInt;
use rand::thread_rng;
use rand::Rng;

const P_AND_G: [(u64, u64); 20] = [
(7, 3),
(11, 2),
(13, 2),
(17, 3),
(19, 2),
(23, 5),
(29, 2),
(31, 3),
(37, 2),
(41, 6),
(43, 3),
(47, 5),
(53, 2),
(59, 2),
(61, 2),
(67, 2),
(71, 7),
(73, 5),
(79, 3),
(83, 2),
];

// prime P and generator G
#[derive(Debug)]
pub struct PnG {
prime: u64,
generator: u64,
}

impl PnG {
pub fn generate() -> Self {
let (prime, generator) = P_AND_G[thread_rng().gen_range(0..P_AND_G.len())];
Self { prime, generator }
}

pub fn get_prime(&self) -> u64 {
self.prime
}

pub fn get_generator(&self) -> u64 {
self.generator
}

pub fn generate_random_number(&self) -> u64 {
// generate a number in the range of 1 to prime - 2
thread_rng().gen_range(1..self.prime - 2)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_diffie_hellman() {
for round in 0..10 {
let pg = PnG::generate();
println!(
"{round}) Prime: {}, Generator: {}",
pg.get_prime(),
pg.get_generator()
);
let alice_number = pg.generate_random_number();
let bob_number = pg.generate_random_number();
println!(
"{round}) Alice number: {}, Bob number: {}",
alice_number, bob_number
);
let alice_partial_key =
BigInt::from(pg.get_generator()).pow(alice_number as u32) % pg.get_prime();
let bob_partial_key =
BigInt::from(pg.get_generator()).pow(bob_number as u32) % pg.get_prime();
println!(
"{round}) Alice partial key: {}, Bob partial key: {}",
alice_partial_key, bob_partial_key
);
let alice_full_key =
BigInt::from(bob_partial_key).pow(alice_number as u32) % pg.get_prime();
let bob_full_key =
BigInt::from(alice_partial_key).pow(bob_number as u32) % pg.get_prime();
println!(
"{round}) Alice full key: {}, Bob full key: {}",
alice_full_key, bob_full_key
);
assert_eq!(alice_full_key, bob_full_key);
}
}
}

椭圆曲线密码

椭圆曲线密码是利用“椭圆曲线上的离散对数问题”的复杂度来实现的。

使用了椭圆曲线的 schnorr 的一个简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
use k256::{NonZeroScalar, ProjectivePoint, PublicKey};
use rand::rngs::OsRng;

fn main() {
// 使用操作系统级别的 RNG
let mut rng = OsRng;

// Alice 拥有一个私钥 a,计算公钥 aG
// 私钥 a
let a = NonZeroScalar::random(&mut rng);
// 公钥 aG
let a_g = ProjectivePoint::from(PublicKey::from_secret_scalar(&a));

// 知道 G 和 aG,求 a,属于椭圆曲线上的离散对数问题,目前没有有效的解法
let r = NonZeroScalar::random(&mut rng);
// Alice 生成一个随机数 r,并计算 R = rG
let r_g = ProjectivePoint::from(PublicKey::from_secret_scalar(&r));

// Bob 提供一个挑战 c
let c = NonZeroScalar::random(&mut rng);

// Alice 计算响应 z = r + a * c (mod q)
let ac = a * c; // 计算 a * c
let z = r.add(&ac);

let Some(z) = NonZeroScalar::new(z).into() else {
eprintln!("计算 z 为零。");
return;
};

// Alice 将 z 发送给 Bob,Bob 验证 zG ?= R + c*PK
let z_g = ProjectivePoint::from(PublicKey::from_secret_scalar(&z));

// 计算 c * PK
let c_pk = a_g * *c;

// 计算 rG + c*PK
let r_plus_cpk = r_g + c_pk;

// 验证 zG ?= rG + c*PK
// c * PK 与 a * c 的关系是?
// 为什么这个证明是成立的
// 为什么这个证明是不可伪造的
// 回答:
// c * PK = c * a * G = a * c * G = a * c * G = ac * G
// PK = aG ?
if z_g == r_plus_cpk {
println!("验证成功,Alice 证明了她拥有私钥.");
} else {
println!("验证失败.");
}
}

// 与 GPT 的交谈:
// 为什么 Alice 要先生成一个随机数?
// 答:为了证明她拥有私钥,她需要一个随机数 r,然后计算 R = rG,然后计算 z = r + a * c (mod q)
// 可以让 Bob 先生成随机数并发送给 Alice 吗?(然后 Alice 再生成 r ,发送 rG)
// 答:可以,但是这样会暴露 Bob 的私钥,因为 Bob 会发送 c * PK,PK = aG,所以 Bob 的私钥就会暴露
// 不对啊,Bob 只需要发送 c,不会暴露私钥啊
// 答:对,Bob 只需要发送 c,不会暴露私钥,但是这样会暴露 Bob 的公钥,Bob 的公钥是 aG,这样会暴露 Bob 的身份
// 不对啊,aG 是 Alice 使用私钥 a 计算出来的公钥,怎么会是 Bob 的公钥呢?
// 答:对,我理解错了,Bob 只需要发送 c,不会暴露私钥,也不会暴露公钥,这样是可以的
// Bob 先发送随机数给 Alice 真的没问题吗?Alice 不会获得没有 a 但是证明她拥有 a 的超能力吗?
// 答:Alice 不会获得没有 a 但是证明她拥有 a 的超能力,因为 Alice 需要 a 来计算 z = r + a * c (mod q),如果没有 a,Alice 就无法计算 z
// Alice 不能用假的 a' 来计算 z 吗?一直用假的 a', 是否也能通过验证?
// 答:不能,因为 Bob 会用 a' 来计算 c * PK,然后验证 zG ?= R + c * PK,如果 a' 是错误的,那么 c * PK 也是错误的,验证就会失败
// PK 和 zG 都是用 a' 计算出来的,所以 Bob 永远都会验证成功,不管 Alice 用了什么 a',验证都会成功,这样 Alice 是否就获得的超能力
// 答:不会,因为 Alice 无法计算出正确的 z,验证会失败
// z' = r + a' * c (mod q),所以 z'G = R + c * a' * G 验证肯定成功吧?
// 答:不会,因为 c * a' * G != c * a * G,验证会失败
// 你好像没有理解,如果 Bob 实现发送了挑战 c,Alice 可以使用任何除了 a 之外的密钥替换 a(比如 a'),这样整个流程只是证明了 Alice 拥有一个密钥(这个密钥并不一定是 a)
// 答:对,我理解错了,如果 Bob 先发送了挑战 c,Alice 可以使用任何除了 a 之外的密钥替换 a(比如 a'),这样整个流程只是证明了 Alice 拥有一个密钥(这个密钥并不一定是 a)
// 所以 Alice 即使没有任何密钥,她临时编造一个,在这种情况下,Bob 永远都无法验证 Alice 是否拥有私钥,这样 Alice 就获得了超能力
// 所以顺序不能调换,对吗?
// 答:对,顺序不能调换,Alice 必须先生成一个随机数 r,然后计算 R = rG,然后计算 z = r + a * c (mod q),然后发送 z 给 Bob,Bob 验证 zG ?= R + c * PK

目标

从今天开始,学习零知识证明相关知识,暂定 3 个月(到2024年7月26日完成)的学习目标为:

  1. 完成零知识证明常见算法、实现的学习,至少能够知道它们解决的是什么问题,分别适用于什么业务场景。
  2. 学会并熟练掌握使用至少一个流行的 Rust 零知识证明库。
  3. 参与至少一个零知识证明的(开源?)项目,需要提交有质量的代码。

初步计划

V20240426

  1. 第一个月: World of Z2O-K7E 的学习;产出为“能够在朋友、同事甚至是小学生(如果我有幸能认识一个愿意听我讲的)面前把学到的哲学、原理、应用讲出来,讲清楚”。
  2. 第二个月: 完成 World of Z2O-K7E 的学习并开始学习 Rust 相关库,写 demo;学习产出参考上一个月,写 demo 的产出为能够全面反映该 Rust 库的各种功能的一个 repo。
  3. 第三个月: 继续写 demo,看相关开源项目源码,参与项目;产出物为 项目地址 / commit / PR 记录等等。

介于本人在该领域是完全的“零知识”,以上仅为初步计划,如果有任何修改(即使是放弃)都需要更新本文。

20240530

一个多月了,公司的事有点烦。实际上我话太少,并没有找人验证过我是否能证明我能讲出所有我学到的东西,目前学习进度并不快,只是能够把 RSA 和 schnorr 的流程完整复述出来(并用小质数作为例子介绍)。报了一个课,希望能学到我想学的东西,后面打算整理这个课的笔记了。继续加油!

定义

  • $S$、$R$ 为关系模式,$s$、$r$ 为关系实例。
  • 要计算关系除法,需要满足 $S ⊆ R$。
  • 给定一个元组 $t$,令 $t[S]$ 代表元组 $t$ 在 $S$ 中属性上的投影;那么,$r ÷ s$ 是 $R-S$ 模式的一个关系。
  • 元组 $t$ 在 $r ÷ s$ 中的充要条件是满足以下两个
    1. $t$ 在 $Π_{R-S}(r)$ 中
    2. 对于 $s$ 中的每个元组 $t_s$,在 $r$ 中存在一个元组 $t_r$ 且满足:
      • $t_r[S] = t_s[S]$
      • $t_r[R-S] = t$
  • 例子:

$r$:

A1 A2
a e
a f
b e
b g
c e
c f
d f

$s$:

A2
e
f

$r ÷ s$:

A1
a
c

公式

关系代数除法可以使用其它关系代数运算替代:

用上一节的例子就是:

A1
a
b
c
d

A1 A2
a e
a f
b e
b f
c e
c f
d e
d f

公式中是为了计算差($-$)的时候两个操作数模式相同,对 $r$ 做了一下模式的颠倒,其实还是 $r$。

A1 A2
a e
a f
b e
b g
c e
c f
d f

A1 A2
b f
d e

A1
b
d

A1
a
c

想写这样一篇总结也是因为在互联网上看了太多乱七八糟的说法,往往让读者越看越糊涂。这些说法往往给我这样的感觉:

  1. 没有先讲清楚在形容什么:同步、异步、阻塞、非阻塞通常作为形容词使用,无论是用在日常语境,还是用在编程领域内部,如果你不知道这些形容词是放在什么名词之前就开始讲什么意思,甚至一段话中掺杂多个名词去形容而不说明,那就会给人不知所云的感觉。
  2. 概念与实现混为一谈:同步阻塞、异步非阻塞是非常常见的组合方式,所以在讲同步、异步的概念时,它们忍不住拿这些具体实现来讲概念,也就顶多能让读者理解这些固定搭配,难以活学活用(其实能不能做到前者都存疑,甚至作者自己真的懂吗?)。
  3. 不恰当的比喻:比喻的使用是为了帮助表达,本体和喻体之间当然是不同的事物(否则就不是比喻了),但是讲了个生动的比喻,却没有把本体与喻体之间的对应关系讲清楚,或者没有讲清楚这个比喻的边界,给话题引入了不必要的复杂性,起到了相反的作用。

所以,我打算好好聊一下我对这个话题的理解,尽可能不要犯我认为别人犯过的这些错误,欢迎读者监督。

同步与异步

同步这个词在日常语境下与计算机是有着一些不同的。日常语境下,同步经常用在同时进行的事情上,比如“多个平台同步直播卖货”,“推动新时代四化同步发展”,当然它们之间是经过协调一致的,不是无组织无纪律各行其是的。

而在编程领域,“同步”和“异步”通常用来是形容“任务”、“函数”等可执行对象的(以下统一称作“任务”);当然也有用来形容“API”的,此时它们表示的是一种具体实现的风格,即“同步风格的API”、“异步风格的API”,它跟前者有关系也有区别。

那它们究竟是什么意思呢?

无论是同步还是异步,都一定是用来形容多个任务的,我们可以说这两个任务或者这三个函数是同步或者异步执行的,但是一个任务不存在同步或者异步。同步指的是多个任务一个一个执行,不存在时间重叠的情况,异步指的是多个任务不一个一个执行,可能存在时间重叠的情况。

有的说法硬要在讲清楚这个概念之前,先把这个任务假设成一个异步函数,再把它分成调用和获取结果两个步骤,开始谈什么消息,打比方讲什么先打了个电话订餐,又接了个电话通知,然后告诉你这叫异步,这是很容易造成误解的。即便要拆成俩,单个任务的调用和获取结果也一定是一前一后,不可能先获取结果再调用,也不可能同时进行,这两个步骤是同步的,不以具体实现为转移,它们只是可能跟另一个任务是异步的,拆成俩徒增复杂度,没有任何必要。所以干脆不要拆,也不要聊具体实现,我们说一个任务,它就是一个任务,就是执行一段时间拿到结果,不要拆开。比如“吃外卖”,就等于“下单-收到电话-取餐-吃”这整个过程,反正经过了一段时间,不要在意内部细节

那么,同步和异步应该怎么使用呢,比如;

  1. 我先吃了个外卖,然后打了一会游戏,吃外卖和打游戏这两个任务就是同步的。
  2. 我先吃了个外卖,然后打了一会游戏,又写了会代码,这三个任务也是同步的。
  3. 在 2. 的基础上,我在吃外卖的过程中还听了一段时间的音乐,那么听音乐和吃外卖是异步的(不要管我什么时候开的音乐,什么时候关的音乐)。
  4. 在 2. 的基础上,打游戏期间都是保证把音乐关掉了的,但是吃外卖和写代码期间都听了一会音乐,那么听音乐和玩游戏是同步的,和吃外卖、写代码是异步的。

是不是很简单,只要按照那个定义去套就行了。但是有一点要注意,4. 里面的 “打游戏期间都是保证把音乐关掉了” 的 “保证” 二字是很关键的,它确保了听音乐和玩游戏之间的 “一个一个执行” 的条件,这需要我在玩游戏之前确保音乐是关掉的,又保证玩游戏期间没有突然去把音乐打开,如果你只是凭运气,只是偶然造成了 “玩游戏期间音乐是关掉的” 的结果,那么它仍然是异步的,因为异步的定义里说的是“可能存在时间重叠”,你没有确保完全排除这个可能,它们就是异步的。

我们再回到日常语境下的“同步”,会发现它跟编程的“同步”是相通的。比如“多个平台同步直播卖货”,虽然整体上各平台是同时在直播一个流媒体源,看上去更符合异步的定义,但其实把“直播”拆分成“录制”、“拉流”、“转场”、“互动”、“上链接”等任务时,会发现它们之间还是存在一些比较严格的先后顺序的,比如你不可能在媒体源录制之前拉流,也不可能在某个重要互动之前就享受某些优惠。也就是说,日常语境下的同步,更多的是描述两件相关的大任务,它们的子任务只要存在一定程度的“保证”就可以说“同步”,并没有编程领域这么严格。

阻塞与非阻塞

这确实是一对很难脱离具体实现讲的概念。

不过相比同步与异步,阻塞与非阻塞更简单,是用来形容单个可执行对象(以下仍统一称作“任务”)的执行状态的。

可能被形容的有线程、协程、某一段业务逻辑等等:

  1. 我在某线程用 BIO 接口读一个文件,造成该线程无法继续运行,这就是线程阻塞了
  2. 我在某协程内使用该语言/框架提供的 sleep 方法,造成协程无法继续运行,这就是协程阻塞了(所在线程并没有阻塞)
  3. 我写了一段代码,反正调用 IO 或者 sleep 了,我也不知道运行在哪个线程上,哪个协程上,或者只是一个异步的 setTimeout, 反正就是在等,这就是业务逻辑阻塞了(可能线程和协程都没有被阻塞)

但其实这样说有点扩大概念外延的意思,99.9% 的情况下,我们说阻塞指的都是线程。而非阻塞,通常指的也是某个任务不会造成线程阻塞。

为什么线程阻塞是最常讨论的话题呢?因为:

  1. 线程的调度和空间占用,都是比较消耗系统资源的。一个 I/O 密集型业务(比如最常见的 Web 服务),如果会因为 I/O 阻塞线程,那么为了支持多个并发同时进行,就需要为每个独立的并发单独开一个线程。在并发较高时,就需要开大量的线程,占用大量 CPU 和内存资源,所以我们有了 C10K 问题。
  2. 这是一个已经被用各种方式花式解决了的问题了。Linux 上的 epoll、Windows 上的 IOCP、MacOS 上的 kqueue(先不要管它们是异步/同步、阻塞/非阻塞,总之 —>),都提供了可以让调用者不必开一堆线程而只需要复用固定数量的线程即可支撑较高并发的接口。不管是 Nodejs,还是 Vert.X 的异步回调,Go 的有栈协程,Rust 的无栈协程,甭管它们封装成异步风格还是同步风格的API,是甜还是咸,都是建立在前者基础上的。简单说,目的只有一个:不阻塞调用者的线程,或者说线程复用

业务要你等3秒,网络数据包要等几百毫秒才能到,硬盘读写要有时间,这些都会造成业务逻辑的阻塞,如果使用 BIO 接口,则业务逻辑的阻塞也会导致线程的阻塞。而使用了上述的框架/语言,让你不用在这些时候阻塞自己的线程,被阻塞的就只有当前业务逻辑,线程可以去跑别的业务逻辑,实现了线程复用。

组合起来

现在,单讲同步、异步、阻塞、非阻塞,应该没什么问题了,我们再聊聊那些常见的搭配。

我们经常听说同步阻塞、异步非阻塞这种搭配方式,而如果按照前文的定义,这种搭配方式是很奇怪的——同步和异步是用来形容多个任务的,阻塞和非阻塞时是说单个任务的,这样组合在一起是在干嘛?

一般来讲,它们是用来形容某种 API 的风格和实现的。

同步和异步是表示这种 API 的接口风格。比如常见的 Linux 的 BIO 接口、Go 的 IO 接口、Rust 里的 async 函数,这些都是同步风格的接口,先后调用这样的两个接口,它们默认是同步执行的,你要异步执行,需要另外开线程或者协程;Node.js、Vert.X 的绝大部分接口,都是异步风格的接口,先后调用这样的两个接口,它们默认是异步执行的,你要同步执行,需要把第二个接口写到前一个接口的回调函数里。

阻塞和非阻塞是表示这种 API 需要调用者配合使用的线程模型。比如 Linux 的 BIO 接口会阻塞调用者的线程,它就是阻塞的,而 Go 的 IO 接口、Rust 的 async 函数、Node.js 和 Vert.X 里的异步接口,都不会阻塞调用者的线程,它们就是非阻塞的。

在没有协程的年代,同步风格的 BIO 接口就是会导致线程阻塞,它意味着心智负担小,开发方便,但是吃资源;而不会阻塞线程的 NIO/AIO 接口往往是异步风格(或者封装为异步风格),代码写起来心智负担重,性能好,比如 Node.js 和 Vert.X。所以经常有人拿同步代指阻塞,拿异步代指非阻塞,它们成为了同义词两两绑定在一起,即“同步阻塞”和“异步非阻塞”。

而 Go 、Rust 等语言提供的协程,相当于是对“异步非阻塞”的一种高级封装,可以像写同步代码那样写不阻塞线程的代码,让你即拥有高性能,又没有很高的心智负担。但是也很少见他们讨论自己是同步还是异步,阻塞还是非阻塞,因为风格上确实是同步的,封装的实际上是异步常用的非阻塞接口,确实不会阻塞线程,但是协程是可能被阻塞。所以不要套概念,理解原理,理解同步具体指的是谁和谁,异步具体指的是谁和谁,阻塞和非阻塞指的具体是谁,搞清楚对象,套定义就可以了。

总结

在写这篇文章的时候,我并没有查询很多资料,但自认为这算是一个可以简化问题,帮助理解的模型。

希望能给读者一些启发,也欢迎批评指正。

使用数组结构表示堆结构,元素从索引 0 开始,则索引 i 位置:

  • 父节点索引为 $(i - 1) / 2$
  • 左孩子索引为 $2 \times i + 1$
  • 右孩子索引为 $2 \times i + 2$

实现

  • Java 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class Heap {

public static void main(String[] args) {
Heap heap = new Heap(1000);
heap.push(-6);
heap.push(3);
heap.push(-1);
heap.push(0);
heap.push(9);
heap.push(-4);
heap.push(5);
heap.push(7);
heap.push(-2);
heap.push(9);
heap.push(13);

System.out.println(heap);

while (!heap.isEmpty()) {
System.out.println(heap.pop());
System.out.println(heap);
}
}

private final int[] data;
private final int limit;
private int heapSize;

public Heap(int limit) {
this.limit = limit;
this.data = new int[limit];
this.heapSize = 0;
}

public void push(int value) {
if (heapSize >= limit) {
throw new RuntimeException("heapSize must not exceed limit");
}

this.data[heapSize++] = value;
heapInsert(heapSize - 1);
}

public boolean isEmpty() {
return heapSize == 0;
}

public Integer pop() {
if (isEmpty()) {
return null;
}

swap(--heapSize, 0);
int result = this.data[heapSize];
this.data[heapSize] = 0;
heapify(0);
return result;
}

private void heapInsert(int index) {
while (index > 0 && this.data[index] > this.data[(index - 1) / 2]) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}

private void swap(int i, int j) {
if (i != j) {
this.data[i] = this.data[i] ^ this.data[j];
this.data[j] = this.data[i] ^ this.data[j];
this.data[i] = this.data[i] ^ this.data[j];
}
}

private void heapify(int index) {
int leftIndex = 2 * index + 1;

while (leftIndex < heapSize) {
int rightIndex = leftIndex + 1;
int greatestIndex = rightIndex < heapSize && this.data[rightIndex] > this.data[leftIndex] ? rightIndex : leftIndex;

if (this.data[greatestIndex] <= this.data[index]) {
break;
}

swap(index, greatestIndex);
index = greatestIndex;
leftIndex = 2 * index + 1;
}
}

@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; i<heapSize; i++) {
b.append(data[i]);
if (i != heapSize - 1)
b.append(", ");
}
b.append(']');

return b.toString();
}
}

输出内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[13, 9, 5, 3, 9, -4, -1, -6, -2, 0, 7]
13
[9, 9, 5, 3, 7, -4, -1, -6, -2, 0]
9
[9, 7, 5, 3, 0, -4, -1, -6, -2]
9
[7, 3, 5, -2, 0, -4, -1, -6]
7
[5, 3, -1, -2, 0, -4, -6]
5
[3, 0, -1, -2, -6, -4]
3
[0, -2, -1, -4, -6]
0
[-1, -2, -6, -4]
-1
[-2, -4, -6]
-2
[-4, -6]
-4
[-6]
-6
[]
  • Rust 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
pub struct Heap<T>(Vec<T>);

impl<T: Ord> Heap<T> {
pub fn new() -> Self {
Self(Vec::new())
}

pub fn push(&mut self, value: T) {
self.0.push(value);
self.heap_insert();
}

pub fn pop(&mut self) -> Option<T> {
if self.0.is_empty() {
return None;
}

let last = self.0.len() - 1;
self.0.swap(0, last);
let result = self.0.pop();
self.heapify();
result
}

fn heapify(&mut self) {
let len = self.0.len();

let mut index = 0;
let mut left = index * 2 + 1;

while left < len {
let right = left + 1;
let greatest = if right < len && self.0[left] < self.0[right] {
right
} else {
left
};

if self.0[greatest] <= self.0[index] {
break;
}

self.0.swap(index, greatest);
index = greatest;
left = index * 2 + 1;
}
}

fn heap_insert(&mut self) {
if self.0.is_empty() {
return;
}

let mut index = self.0.len() - 1;
if index == 0 {
return;
}
let mut parent = (index - 1) / 2;

while self.0[parent] < self.0[index] {
self.0.swap(index, parent);
index = parent;
if index == 0 {
break;
}
parent = (index - 1) / 2;
}
}
}

impl<T: Ord> Iterator for Heap<T> {
type Item = T;

fn next(&mut self) -> Option<Self::Item> {
self.pop()
}
}

#[test]
fn test_heap() {
for _ in 0..1000 {
let array: [u8; 32] = rand::random();
let sorted = {
let mut s = array.clone();
s.sort_by(|a, b| b.cmp(a));
s
};

let mut heap = Heap::new();

for a in array {
heap.push(a);
}

let heap_sorted: Vec<u8> = heap.collect();

assert_eq!(heap_sorted, sorted);
}
}