2025-12-28 21:35:01
Agent Skills 是一种轻量级、开放的格式,用于扩展 AI Agent 的能力和专业知识。本质上,一个 Skill 就是一个包含 SKILL.md 文件的文件夹。
Agent Skills 的作用在于:
name 和 description,任务匹配时才加载完整指令那么这里就有个问题,为什么有了 MCP 之后还需要 Agent Skills?
这个问题其实有过很多争论,有开发者评论说:"Skill 和 MCP 是两种东西,Skill 是领域知识,告诉模型该如何做,本质上是高级 Prompt;而 MCP 对接外部工具和数据。" 也有人认为:"从 Function Call 到 Tool Call 到 MCP 到 Skill,核心大差不差,就是工程实践和表现形式的优化演进。"
其实我还是觉得要从 MCP 和 Agent Skills 设计上区分他们到底有什么不同。
MCP 其实就是指的提供了一个远程的接口,可以用这个接口来接外部世界:能读取数据库、能访问 API、能执行命令行;
Agent Skills 更像一个操作手册,主要存在本地的文件里面,不需要调用外部接口,主要是用来告诉 AI 有那些领域知识,然后教 AI 如何正确、高效地使用这些手,按照什么步骤去完成特定任务。
除此之外,Agent Skills 解决了 MCP 无法解决的三个核心问题:
节省 token
在使用 mcp 工具的时候,通常工具的定义(名字、参数、描述)全部塞进 AI 的提示词(Prompt)里,AI 才能知道怎么调用。这会极大地消耗 Token,可能占用数万个 token。据社区开发者反馈,仅加载一个 Playwright MCP 服务器就会占用 200k 上下文窗口的 8%,这在多轮对话中会迅速累积,导致成本飙升和推理能力下降。
而对于 Agent Skills 来说通过渐进式披露(Progressive Disclosure)机制,智能体按需逐步加载,既确保必要时不遗漏细节,又避免一次性将过多内容塞入上下文窗口,来解决这个问题。
解决“会用工具但不懂业务”的问题(业务流程固化)
AI 只懂 MCP 是不会理解业务的,比如 MCP 提供了 delete_database()(删除数据库)的工具。这很强大,但也危险。AI 可能因为你的一个模糊指令直接删库。这个时候就可以写一个 Skill,规定:
当用户要求删除数据库时,必须严格执行以下流程:
check_backup() 确认有备份。send_alert() 给管理员发通知。delete_database()。降低开发门槛
开发一个 MCP Server 需要后端开发能力,提供接口。Skills 只需要提供 SKILL.md 即可。比如你是资深运营,你可以写一个“小红书文案 Skill”,里面不需要代码,只需要写清楚:“第一步先分析竞品,第二步提取关键词,第三步套用这个模板…”。
所以综上 Agent Skills 至少为开发者带来三大核心价值:
能力复用:一次编写,在 Copilot、Cursor、Claude 等多个 Agent 产品中使用,还可跨团队共享或通过 GitHub 公开发布。
知识沉淀:将团队最佳实践固化为版本化的 Skills,如代码审查规范、部署流程、数据分析模板等,确保工作流程的一致性。
提升效率:通过明确的指导让 Agent 更准确地执行复杂任务,减少试错和修正,提供一致的输出质量。
Agent Skills 最核心的创新是渐进式披露(Progressive Disclosure)机制。AI 在使用 Agent Skills 的时候并没有将整个知识库加载到人工智能有限的上下文窗口中,而是以智能的、高效的层级方式加载信息。
SKILL.md 文件。该文件包含执行任务的分步指令和核心逻辑。这种分层方法使得整个 Agent Skills 系统具有极高的可扩展性。关键在于,当Agent Skills执行脚本时,代码本身永远不会进入上下文窗口;只有脚本的输出才会进入。
社区开发者分享的实践案例充分证明了渐进式披露的威力。在一个真实场景中:
一个最简单的 agent skill 其实只需要包含一个 SKILL.md 文件即可,其他的 scripts、 references、assets 都是可选的。
my-cool-skill/ <-- 技能文件夹
├── SKILL.md <-- 核心文件:包含元数据和指令(必须)
├── scripts/ <-- (可选) 包含 Python/Bash 脚本
│ └── analyze.py
└── templates/ <-- (可选) 包含模板文件
│ └── report.md
└── assets/ <-- Optional: 一些模版资源
SKILL.md
文件内容分为两部分:YAML 头信息 (Frontmatter) 和 Markdown 正文。
Frontmatter 必须用 — 包裹起来,像下面这样,最短可以只包含 name 和 description 两个字段:
---
name: skill-name
description: A description of what this skill does and when to use it.
---
name 字段规则:
a-z, -)--)description 字段规则:
包含可选字段:
---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
license: Apache-2.0
metadata:
author: example-org
version: "1.0"
---
这是 AI 加载技能后看到的具体操作指南。包括步骤、规则、输入输出格式等。
比如我想要写个示例:
构建一个“代码审查专家”技能。
假设你想创建一个技能,专门用来按你团队的风格审查 Python 代码。
那么我们可以写一个这样的 SKILL.md :
---
name: python-code-review
description: 当用户要求审查 Python 代码,或者需要检查代码质量、寻找 bug 时使用此技能。不要用于其他语言。
---
# Python Code Review Guidelines
作为 Python 代码审查专家,请遵循以下步骤审查代码:
## 1. 核心原则
- **类型提示 (Type Hints)**: 所有函数必须包含参数和返回值的类型提示。
- **文档字符串 (Docstrings)**: 使用 Google 风格的文档字符串。
- **错误处理**: 检查是否使用了裸露的 `except:`,必须捕获特定异常。
## 2. 审查清单
请按以下顺序检查代码:
1. 运行静态分析逻辑(如果在 scripts/ 文件夹中有 lint 脚本,请优先参考)。
2. 检查变量命名是否符合 snake_case。
3. 寻找潜在的 N+1 查询问题(如果涉及数据库)。
## 3. 输出格式
请以以下格式输出审查报告:
**🔍 审查摘要**
- 评分: [1-10]
- 主要问题: [摘要]
**📝 详细建议**
| 行号 | 问题 | 建议修改 | 优先级 |
|------|------|----------|--------|
| 12 | 缺少类型提示 | `def func(a: int) -> str:` | 高 |
为了让 Skill 更聪明、更好用:
精确的 Description: AI 只有在 description 与用户请求匹配时才会加载这个技能,尽量应包含:
❌ 坏的写法: "一个帮助代码的工具"
✅ 好的写法: "当用户需要根据 PEP8 标准审查 Python 代码并生成表格报告时使用。"
提供清晰的示例,在 SKILL.md 正文中提供:
原子化: 一个 Skill 最好只做一件事(例如:一个 Skill 做代码审查,另一个 Skill 做文档生成),不要把所有功能塞进一个文件。
对于复杂的 Skill,将详细文档分离,可以使用 References 目录:
data-analysis/
├── SKILL.md # 简要说明和快速开始
├── scripts/
│ └── analyze.cs
└── references/
├── REFERENCE.md # 详细 API 参考
├── examples.md # 更多示例
└── algorithms.md # 算法说明
Agent Skills 和 MCP 一样都是 anthropics 公司提出来的,所以他们也提供了很多好用的 skills 供大家使用,如果选择将官方 Skills 安装到当前项目,就在终端输入这条命令:
openskills install anthropics/skills
安装成功后,你就会在Cursor、Trae等工具的文件管理区看到 .claude/skills 的文件夹。
当然也可去下面三方的收集网站上面下载别人写好的 skills:
先在项目根目录创建一个 AGENTS.md 文件,然后运行
openskills sync
确认后按回车键,你选择的 Skills 就会写进之前空白的 AGENTS.md 文档中。它将作为 Cursor、Trae 等 Coding Agent 接下来使用 Skills 的指导文件。
Skills 是可以被自动调用的,如果你想手动调用,可以直接在提示词中指定要调用的具体 Skills,比如:
调用 frontend-design skills,用HTML开发一个视频剪辑软件的SaaS介绍页
最后在举个例子 MCP 如何协同 Agent Skills 一起完成工作。想象一下要实现一个自动化的金融分析代理:
在这种情况下,MCP 处理底层标准化的工具调用任务,而Agent Skills负责协调高层智能工作流程。 Agent Skills是一种可重用的资产,它捕捉了分析师的独特专业知识,使其能够立即扩展。这种强大的组合使开发人员能够构建健壮的系统,其中任务逻辑与其所用工具的实现完全分离。
https://www.cnblogs.com/sheng-jie/p/19381647
https://zhuanlan.zhihu.com/p/1986802048608527579
https://www.zhihu.com/question/1890546618509538123
https://agentskills.so/skills-blogs/agent-skills-compare-mcp
2025-12-20 17:50:28
这篇文章主要是看一下Rust有哪些比较有意思的设计,相比其他语言之下为什么要这么设计。
Rust 的变量在默认情况下是不可变的,但是可以通过 mut 关键字让变量变为可变的,让设计更灵活。也就是说,如果我们这么写,编译会报错:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
报错:
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
这其实和 C++ 和 Go(以及 Java、Python 等绝大多数主流语言)的设计哲学完全相反。C++ 和 Go 默认就是可变的,除非加上 const 表示是个常量。
Rust 这样做主要是为了让代码变得清晰点,降低心智负担 。一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。再来就是编译器优化,如果编译器知道一个变量绝不会变,它可以更激进地进行常量折叠、寄存器分配等优化。
在 Rust 中,可变性很简单,只要在变量名前加一个 mut 即可:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
但是 Rust 提供了 Shadowing 的功能:
fn main() {
let x = 5;
// x = x + 1; // 报错,不能修改
let x = x + 1; // 合法!这是一个全新的 x,它遮蔽了旧的 x
let x = "Hello"; // 甚至可以改变类型!
println!("{}", x);
}
这点很有意思,在很多语言是不可以这么重复声明变量的。我觉得还是和不可变性有关,既然都不可变了,重复声明也是安全的,并且复用同一个变量名,而不需要想出 x_str, x_int, x_final 这种名字,相对来说代码会简洁一些。
现在内存管理一般分为两类:
malloc/new),需要手动负责释放 (free/delete),但是这是很痛苦的,有时候忘记释放就会内存泄露,或者释放两次就会导致崩溃或为定义的行为;而 Rust 使用所有权 (Ownership)来控制。编译器在编译阶段通过一套严格的规则,自动在合适的地方插入 free 代码。没有运行时 GC,也不依赖手动管理。这个编译器定义的所有权规则有以下几条:
比如这个例子:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 赋值操作
// println!("{}, world!", s1); // ??? 这里能打印 s1 吗?
}
在 C++ 中,如果 s1 申请的是一个堆上的对象,如果是浅拷贝 (Shallow Copy),s1 和 s2 指向堆上的同一块内存。如果函数结束,析构函数执行两次,导致 Double Free 错误。
在 Rust 中,由于所有权的存在,这一行 let s2 = s1;代码执行后,s1 会当场死亡!发生 所有权转移 (Move)。Rust 认为:堆上的那块 "hello" 内存,现在归 s2 管了。所以如果你后面再用 s1,编译直接报错。
报错:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
如果你确实需要两个独立的字符串数据(深拷贝),你需要显式调用 .clone()。
let s1 = String::from("hello");
let s2 = s1.clone(); // 在堆上重新开辟内存,复制数据
println!("s1 = {}, s2 = {}", s1, s2); // s1 依然活着
除此之外,要注意栈上的数据 ,对于基本类型,基本类型(存储在栈上),Rust 会自动拷贝,其他的非基本类型会存储在堆上,不能自动拷贝。
let x = 5;
let y = x;
代码首先将 5 绑定到变量 x,接着拷贝 x 的值赋给 y,最终 x 和 y 都等于 5,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。
借用(Borrowing),就是允许你在不获取所有权 (Ownership) 的情况下访问数据。简单来说,借用就是创建数据的引用 (Reference)。
借用有两种方式,不可变借用 : &T,可变借用:&mut T。在任意给定的作用域中,你只能满足以下两个条件之一:
&T)。&mut T)。即:要么多读,要么独写,绝不能同时存在,这个规则非常像 读写锁(Read-Write Lock)。
比如下面就是合法的借用 (多读):
fn main() {
let s = String::from("hello"); // s 拥有所有权
let r1 = &s; // 不可变借用 1
let r2 = &s; // 不可变借用 2
// 可以同时存在多个读者
println!("{}, {}", r1, r2);
} // 借用结束
合法的借用 (独写):
fn main() {
let mut s = String::from("hello");
let r1 = &mut s; // 可变借用
r1.push_str(", world"); // 修改数据
println!("{}", r1);
}
非法的借用 (读写冲突):
fn main() {
let mut s = String::from("hello");
let r1 = &s; // r1 借用以此只读
let r2 = &mut s; // 错误!不能在有不可变引用的同时创建可变引用
// 因为 r2 可能会改变 s,导致 r1 看到的数据失效或不一致
println!("{}, {}", r1, r2);
}
需要注意的是,现在的 Rust 编译器非常聪明,它的“作用域”不再仅仅是花括号 {},而是看引用的最后一次使用位置,这叫做 Non-Lexical Lifetimes (NLL)。
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// --- r1 和 r2 的作用在这里就结束了!因为后面没再用过它们 ---
let r3 = &mut s; // 现在可以了!
// 因为上面的不可变引用已经不再使用了,Rust 判定冲突解除。
println!("{}", r3);
}
Rust 这么做其实也是为了安全,我们看看 C++ 中常见的坑:
// C++ 伪代码
vector<int> v = {1, 2, 3};
int& element = v[0]; // 获取第一个元素的引用
v.push_back(4);
// 危险!如果 push_back 导致 vector 扩容(重新分配内存),
// 原来的内存被释放,element 现在指向的是垃圾内存。
// 再次访问 element 会导致 Crash。
在 Rust 中:
element 是一个不可变借用 (&T)。v.push_back 需要获取 v 的可变借用 (&mut T)。& 了,不能再借出 &mut。&str 和 String
在其他很多语言用 "hello" 这种方式创建的一般就叫字符串,但在 rust 里面不一样,它实际上是申明了一个只读的字符串字面量 &str,这意味着它是不可变的,数据直接硬编码在编译后的可执行文件 (Binary) 中(静态存储区),有点像 const。
let name = "Rust"; // 类型是 &str
println!("Hello, {}", name);
// name.push_str(" World"); // 报错!&str 不能修改
在 rust 里面只有使用 String::from(...) 声明的字符串才是我们常规意义上理解的字符串,比如可以对它进行修改、拼接和传递。
修改字符串 (必须加 mut)
fn main() {
// 注意:如果要修改,必须加 mut 关键字
let mut s = String::from("Hello");
// 1. 追加字符串切片 push_str()
s.push_str(", world");
// 2. 追加单个字符 push()
s.push('!');
println!("{}", s); // 输出: Hello, world!
}
字符串拼接 (连接两个字符串),有两种主要方式:使用 + 运算符或 format! 宏。
fn main() {
let s1 = String::from("Tick");
let s2 = String::from("Tock");
// 注意细节:
// s1 必须交出所有权 (被移动了),后面不能再用了
// s2 必须传引用 (&s2)
let s3 = s1 + " " + &s2;
// 类似 C 语言的 sprintf,生成一个新的 String
let s4 = format!("{} - {}", s1, s2);
}
String 可以自动假装成 &str
fn main() {
let s = String::from("Hello World");
// 场景 1: 函数需要 String (拿走所有权)
take_ownership(s);
// println!("{}", s); // 报错,s 已经被拿走了
// --- 重新创建一个 s ---
let s = String::from("Hello Again");
// 场景 2: 函数需要 &str (只读借用) -> 【这是最常用的】
// 虽然 s 是 String,但 &s 可以被当做 &str 用
borrow_it(&s);
println!("s 还在: {}", s); // s 还在
}
fn take_ownership(input: String) {
println!("我拿到了所有权: {}", input);
} // input 在这里被释放
fn borrow_it(input: &str) {
println!("我只是借看一下: {}", input);
}
转换回切片 (Slicing)
let s = String::from("Hello World");
let hello = &s[0..5]; // 提取前5个字节
let world = &s[6..]; // 从第6个字节取到最后
需要注意的是,Rust 的字符串在底层是 UTF-8 编码的字节数组,不支持直接通过数组下标索引(Index)访问字符。比如这样是会报错的:
let s1 = String::from("hello");
let h = s1[0];
Rust 的 String 本质上是一个 Vec<u8>(字节向量)。对于纯英文: "hello",每个字母占 1 个字节。s[0] 确实是 'h'。对于中文/特殊符号: "你好"。在 UTF-8 中,’你’ 占用 3 个字节。
为了强迫开发者意识到 “字符 ≠ 字节” 这一事实,Rust 干脆在编译阶段就禁止了 String[index] 这种写法。
所以,为了获取第 N 个字符 (最常用,安全),需要使用 .chars() 迭代器:
fn main() {
let s1 = String::from("hello");
// .chars() 把字符串解析为 Unicode 字符
// .nth(0) 取出第 0 个元素
// 结果是 Option<char>,因为字符串可能是空的
match s1.chars().nth(0) {
Some(c) => println!("第一个字符是: {}", c),
None => println!("字符串是空的"),
}
}
String 是为了当你需要在运行时动态生成、修改或者持有字符串数据时使用的(比如从网络读取数据,拼接 SQL)。&str 是为了高性能传递。当你只需要“看”一下字符串,而不需要拥有它时,用 &str。因为它只是传两个整数(指针+长度),不需要拷贝堆上的数据,速度极快。Rust 的枚举和其他语言最大的不同应该就是能挂载数据。每一个枚举成员,都可以关联不同类型、不同数量的数据。
enum Message {
Quit, // 没有关联数据
Move { x: i32, y: i32 }, // 像 Struct 一样包含命名字段
Chat(String), // 包含一个 String
ChangeColor(i32, i32, i32), // 包含三个 i32
}
fn main() {
// 创建不同类型的消息
let msg1 = Message::Move { x: 10, y: 20 };
let msg2 = Message::Chat(String::from("你好"));
}
比如上面的 Message 这个枚举,Quit 成员没有挂载数据,其他三个都挂载了各不相同的数据类型。
然后我们就可以根据枚举挂载的不同数据,使用 match 来进行匹配,这有点像 C++ 里面的 switch-case:
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!("玩家退出了");
}
Message::Move { x, y } => {
println!("玩家移动到了: x={}, y={}", x, y);
}
Message::Chat(text) => {
println!("玩家发送消息: {}", text);
}
Message::ChangeColor(r, g, b) => {
println!("更改颜色为: R{} G{} B{}", r, g, b);
}
}
}
但是需要注意的是 match 会强制你处理所有可能的情况,如果你漏写了 Message::Quit,代码根本编译不过。这保证了你不会遗漏任何一种业务逻辑。如果不想在匹配时列出所有值的时候,可以使用特殊的模式 _ 替代:
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
顺带提一下,match 其实也可以用来返回赋值,这点就和很多其他语言不同:
enum IpAddr {
Ipv4,
Ipv6
}
fn main() {
let ip1 = IpAddr::Ipv6;
let ip_str = match ip1 {
IpAddr::Ipv4 => "127.0.0.1",
_ => "::1",
};
println!("{}", ip_str);
}
这里匹配到 _ 分支,所以将 "::1" 赋值给了 ip_str。
上面我们也说了枚举可以挂载数据,所以相应的 match 也能把数据解包出来:
enum Action {
Say(String),
MoveTo(i32, i32),
ChangeColorRGB(u16, u16, u16),
}
fn main() {
let actions = [
Action::Say("Hello Rust".to_string()),
Action::MoveTo(1,2),
Action::ChangeColorRGB(255,255,0),
];
for action in actions {
match action {
Action::Say(s) => {
println!("{}", s);
},
Action::MoveTo(x, y) => {
println!("point from (0, 0) move to ({}, {})", x, y);
},
Action::ChangeColorRGB(r, g, _) => {
println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored",
r, g,
);
}
}
}
}
上面当代码执行到 match action 时 Rust 发现 action 是 Action::Say 这一类,就会解构这个类型里面的 String,Action::Say(s)的意思是:“如果是 Say,把它肚子里的数据拿出来,赋值给变量 s。”,其他的同理。
Rust 的枚举类型还有一个特点就是结构比较紧凑,枚举 = 标签(tag)+ 数据(payload),比如这个枚举:
enum Message {
Quit, // 0 字节 payload
Move { x: i32, y: i32 }, // 8 字节
Write(String), // 24 字节(64位平台上,大概是 3 个指针)
}
tag 就是当前是 Quit/Move/Write 中的哪一个,数据(payload)就是容纳“所有变体里最大的那个数据”的空间,这里就是24字节的 String 类型,当然还有根据对齐要求,可能在 tag 和 payload 之间加 padding,那么一个枚举的结构就是:
size_of::<Message>() ≈ size_of::<payload最大> + size_of::<tag> + 对齐填充
所以可以看到枚举的结构比使用结构体更加紧凑。
最后,枚举甚至还能可以有自己的方法:
#![allow(unused)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// 在这里定义方法体
}
}
fn main() {
let m = Message::Write(String::from("hello"));
m.call();
}
Tony Hoare, null 的发明者,曾经说过一段非常有名的话:
我称之为我十亿美元的错误。当时,我在使用一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过在设计过程中,我未能抵抗住诱惑,引入了空引用的概念,因为它非常容易实现。就是因为这个决策,引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。
所以 Rust 只有 Option 枚举,没有 null / nil / nullptr。Rust 标准库是这样定义的:
enum Option<T> {
None, // 相当于 null
Some(T), // 包含一个值
}
Option 可以配合 match 来使用:
let some_number = Some(5);
let absent_number: Option<i32> = None;
// let sum = some_number + 1; // 报错!不能把 Option<i32> 和 i32 相加
// 必须处理为空的情况
match some_number {
Some(i) => println!("数字是: {}", i),
None => println!("没有数字"),
}
使用 Option 还有个好处就是编译器会做一种优化叫做 “空指针优化 (Null Pointer Optimization)”。
对于 Option<&T>(引用类型的 Option)或 Option<Box<T>>(堆指针的 Option)Rust 编译器在底层依然把它看作一个指针,Some(ptr) 对应非零地址,None 对应 0 (null) 地址。所以在汇编层面,Rust 的 Option<&T> 和 C++ 的 T\* 是一模一样的,内存占用也是一样的(64位机器上都是8字节)。
所以把指针包在 Option 枚举里不会增加内存开销,也不会导致运行变慢。
Rust 泛型其实是有点像C++的模版的,而不是类似 Java 或 C# 的泛型。 Java 泛型是在编译期进行检查,但在运行时被“擦除”。例如,List<String> 和 List<Integer> 在 Java 虚拟机(JVM)看来,本质上都是 List<Object>。这样做优点是节省了代码空间,缺点是牺牲了很多性能,因为在运行的时候虚拟机必须进行大量的类型转换(Casting)。
Rust 泛型则不一样,它和C++的模版是一样的,会根据你传入的具体类型(比如 int/i32 或 float/f64),生成多份不同的机器码。这样做的好处就是执行效率极高,没有 Java/Go 那种装箱(Boxing)或运行时类型断言的开销。
但是相对的,Rust 又比 C++ 模版又要使用上要舒服很多,Rust 泛型因为 Trait Bounds 的原因,所以编译器在定义阶段就能发现错误,而不是等到调用阶段。假设我们要写一个打印函数:
C++:
template <typename T>
void print_it(T value) {
// 编译器在这里不检查 value 到底有没有 .print() 方法
// 直到你在 main 函数里调用 print_it(int) 时,它才发现 int 没有 .print()
value.print();
}
Rust:
// 必须显式约束:T 必须实现了 Debug trait
fn print_it<T: std::fmt::Debug>(value: T) {
println!("{:?}", value);
}
// 如果你写成下面这样,编译器直接报错,甚至不需要你调用它:
// fn print_it<T>(value: T) {
// println!("{:?}", value); // 错误!编译器说:T 没有实现 Debug,不能打印
// }
Rust 的特征对象 (dyn Trait) ≈ C++ 的虚基类指针 (Base) ≈ Go 的 interface。
在 Rust 中,如果你想编写一个函数,或者定义一个容器(如 Vec),让它可以接受多种不同类型的数据,只要这些数据实现了同一个 Trait,你有两种选择:
fn foo<T: Draw>(x: T)
Vec 里同时存 u8 和 f64,因为 Vec 只能存一种类型。fn foo(x: &dyn Draw)
u8 还是 f64,它只关心“这东西能 Draw”。Vec<Box<dyn Draw>> 里混存 u8 和 f64。特征对象(Trait Objects)实现的方式其实和 C++ 的虚表实现很像,比如当你把一个具体类型(如 &u8)转换成特征对象(&dyn Draw)时,Rust 会生成一个胖指针(Fat Pointer)。
这个胖指针包含两部分(占用 16 字节):
data 指针:指向具体的数据(如堆上的 u8 值)。vtable 指针:指向该具体类型针对该 Trait 的虚函数表(Virtual Method Table)。当你调用 x.draw() 时,如果 x 是特征对象,机器码执行的逻辑如下:
读取 vtable:从胖指针的第二个字段找到 vtable 的地址。
查找方法:在 vtable 中找到 draw 方法对应的函数指针(比如偏移量为 0 的位置)。
跳转执行:调用该函数指针,并将胖指针的第一个字段(data 指针)作为 self 传进去。
举个例子,如果我们使用泛型来实现下面的 Screen 类,里面的 components 想要放多个元素:
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
如果你这样写,Screen 实例被创建时,T 必须被确定为某一种具体的类型。这意味着 components 里的所有元素都必须是同一个类型。如果想要混装是不行的,如下面:
let screen = Screen {
components: vec![
10u8, // 编译器推断 T 是 u8
3.14f64, // 报错!期望是 u8,但你给了 f64
],
};
// error[E0308]: mismatched types
只能全部都是同一类型:
let screen = Screen {
components: vec![10u8, 20u8, 30u8], // 全是 u8,没问题
};
如果使用特征对象(Trait Objects),就可以实现混装,在列表中存储多种不同类型的实例。
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
这样是 ok 的:
let v: Vec<Box<dyn Draw>> = vec![
Box::new(10u8),
Box::new(3.14f64),
];
生命周期就是控制有效作用域,防止 悬垂引用(Dangling Reference)的 。
fn main() {
let r; // ---------+-- r 的生命周期开始
{ // |
let x = 5; // -+-- x 的生命周期开始
r = &x; // | 试图让 r 指向 x
} // -+-- x 在这里死了(被释放)!
// |
println!("r: {}", r); // | r 依然活着,但它指向的 x 已经没了!
// | -> 报错:借用了活得不够久的值
} // ---------+
比如这段代码,Rust 编译器会拒绝编译,因为它发现 r 活得比 x 久,为了安全考虑,然后就拒绝编译。
用过 C++ 的同学知道, 在线上经常会因为这种问题而导致程序的 panic,因为C++ 编译器通常会“相信”你,比如下面的例子编译器根本不会管你:
#include <iostream>
#include <string>
// C++ 代码
const std::string& get_dangling() {
std::string s = "Hello";
return s; // 危险!s 在这里会被销毁
}
int main() {
// 这里拿到了一个引用,指向了一块已经被释放的栈内存
const std::string& ref = get_dangling();
// 运行时表现:
// 1. 可能崩溃 (Segmentation Fault)
// 2. 可能打印出乱码
// 3. 可能打印出 "Hello" (如果在内存被覆盖前运气好) -> 这是最可怕的“未定义行为”
std::cout << ref << std::endl;
}
所以相对而言,Rust 编译器的严格管控,实际上是”为了你好“。
Rust 为了实现这种严格管控,就出现了生命周期标注这种东西。生命周期不是“运行时的计时器”,也不是你手动管理内存的东西。它是编译器做静态分析时用的标记/约束,表示:
你写的 'a 这种符号,就是一种生命周期参数。大多数时候,Rust 编译器能自动推断生命周期,但在一些模糊的情况下,就需要手动标注。手动标注是以 ' 开头,名称往往是一个单独的小写字母,大多数人都用 'a 来作为生命周期的名称。
关于什么时候需要使用标注其实就一句话:输出的引用到底是从哪个输入里借来的有歧义的时候。具体来说,主要有以下 4 种场景
场景 1:结构体中包含引用
只要你的 struct 定义里包含字段是引用(而不是像 String 或 i32 这样的拥有所有权的类型),你就必须给整个结构体加上生命周期参数。
// 报错:missing lifetime specifier
struct Book {
author: &str, // 这是一个引用,编译器慌了:这个引用指向谁?能活多久?
}
// 正确
// 读作:Book 实例活多久,'a 就得活多久;author 引用的数据至少也要活 'a 这么久。
struct Book<'a> {
author: &'a str,
}
场景 2:函数有多个引用参数,且返回值也是引用
如果你有两个输入引用,且返回一个引用。编译器就蒙了:“返回的这个引用,是借用了参数 A,还是参数 B?”
// 报错
// 编译器困惑:返回的 &str 到底是谁的?
// 如果 user 活 10s,data 活 5s,我该让返回值活多久?
fn choose_one(user: &str, data: &str) -> &str {
if user.len() > 5 { user } else { data }
}
// 正确
// 显式告诉编译器:返回值的生命周期取 user 和 data 中较短的那个('a)
fn choose_one<'a>(user: &'a str, data: &'a str) -> &'a str {
if user.len() > 5 { user } else { data }
}
如果返回值只跟其中一个参数有关,只标那个参数就行:
// 这里的 'a 表示返回值只和 x 有关,和 y 无关
fn verify<'a>(x: &'a str, y: &str) -> &'a str {
x
}
当然,如果只有一个输入引用那就没有歧义:
fn capitalize(s: &str) -> &str { ... }
// 编译器自动脑补为:
// fn capitalize<'a>(s: &'a str) -> &'a str { ... }
场景 3:在 impl 块中实现方法时
这是语法要求,防止你忘记这个结构体是有“保质期”的。
struct Book<'a> {
author: &'a str,
}
// 必须写 impl<'a>,把 'a 声明出来
impl<'a> Book<'a> {
// 这里的 &self 其实隐含了生命周期
fn get_author(&self) -> &str {
self.author
}
}
注意:在方法内部,通常不需要给参数标生命周期,因为 Rust 有一条强大的规则:“如果是方法(有 &self),那么返回值的生命周期默认和 self 一样。”
2025-11-26 20:17:32
2013年底: Vitalik Buterin(V神)发布了以太坊白皮书。他的核心理念是:比特币像一个功能单一的计算器(可编程货币),而世界需要一个更通用的平台,像一台“世界计算机”(World Computer)。所以他提出了智能合约的概念,指的是运行在区块链上的、图灵完备的程序。通过使用以太坊虚拟机(EVM)提供的这样的沙盒环境,用于执行智能合约。
2015年7月"Frontier"(前线)版本上线。这是以太坊的第一个“创世区块”,标志着网络的正式启动,这是时候还仅仅是一个测试版本。采用的是工作量证明(PoW)的共识机制。
2016年初"Homestead"(家园)版本发布,这是第一个稳定版本,标志着以太坊不再是“测试版”,开始吸引DApp(去中心化应用)构建者。The DAO这个项目诞生了,它是一个去中心化的风险投资基金,通过智能合约管理,它筹集了当时价值约1.5亿美元的ETH。
同时也意味着危机,2016年6月,The DAO 合约遭到“重入攻击”(Re-entrancy Attack),导致约1/3的资金被盗。
所以这个时候社区面临一个哲学困境,是要接受损失,还是进行通过修改协议规则来回滚交易,追回被盗资金。最后社区投票支持硬分叉,追回了资金,成为了今天的主流链,也就是今天的 ETH。另一派坚持不回滚,保留了原始链,就成了另一个币 ETC。
我们接着跳过几年不这么重要的发展期来到PoS时代。
为什么要从 PoW 转向 PoS 共识证明呢?我们都知道 PoW 用“工作”来换取记账权,工作量越大,越值得信赖,这就有个问题,需要巨大的电力和硬件成本,这是极度不环保的。
而 PoS 的核心理念是用“抵押”来换取记账权,你抵押的(Stake)越多,越值得信赖。参与者不再需要购买昂贵的矿机,而是需要购买并质押(锁定)网络的原生代币,将这些代币作为“保证金”或“押金”锁在网络中。如果一个验证者试图作恶(例如,提议无效区块、双重签名),网络会自动销毁他质押的“保证金,ETH 就是这样降低了约 99.95% 的能耗。
下面说一下ETH是怎么做到的:
2020年12月, 信标链(Beacon Chain)上线,这是一条独立运行的、采用 PoS 共识的全新区块链,它唯一的任务就是让验证者质押 ETH 并就 PoS 共识达成一致。此时,ETH 质押是单向的(只能存入,不能取出)。
2022年9月15日以太坊团队将原有的 PoW 链(现在称为“执行层”)的“引擎”——即 PoW 共识——拔掉。然后,将“执行层”接入到“信标链”(现在称为“共识层”)的 PoS 引擎上。这正式标志着以太坊进入了“PoS 时代”
合并完成后,2023年4月 "Shapella"(上海 + Capella)发布启用了质押提款(EIP-4895)。验证者终于可以取出他们质押的 ETH 和奖励,这次升级引入了 withdrawalsRoot(提款树根)字段到区块头中。
当然 ETH 的迭代远没有结束,我看社区还在继续讨论新的提案出来。比如 Pectra 、The Verge 与 The Purge 等,感兴趣的可以自行取查阅一下。
这里用 BTC 和 ETH 进行对比,首先在 BTC 中,并不存在一个叫做“我的余额”的变量。它是由 UTXO 算出来的,所以假设钱包里有 5 BTC。这 5 BTC 在区块链上可能并不是一个“5”,而是:
总余额 = UTXO 1 + UTXO 2 + UTXO 3 = 5 BTC。
如果想支付 3 BTC,钱包会选择“消耗”掉 UTXO 1 (2 BTC) 和 UTXO 2 (1.5 BTC),总共 3.5 BTC。然后产生两个新的 UTXO:
旧的 UTXO 1 和 UTXO 2 就被标记为“已花费”,不能再用了。
这种模型的优点: 简单、安全、隐私性相对较好(因为找零地址可以是新地址)、易于并行处理交易。 缺点: 难以实现复杂的逻辑(例如智能合约),因为它很难跟踪一个“账户”的复杂状态。
ETH 的设计更像是传统的银行系统。每个地址都是一个独立的“账户”。如果地址有 5 ETH,那么在以太坊的“全局账本”上,地址旁边就明确写着 balance: 5。
如果要支付 3 ETH,发起一笔交易,声明:“从账户A转 3 ETH 到账户B”,网络验证账户余额(5 ETH)是否足够支付 3 ETH(以及手续费 Gas)。验证通过后,以太坊网络会:
balance 减去 3 ETH。balance 加上 3 ETH。为了防止余额数字被直接篡改,账户里面有 nonce 用来记数,每次交易完毕之后加一,防止重放攻击。
ETH 有两种账户:
外部账户 externally owned account,个人用户钱包,由私钥控制,可以发起交易;
合约账户 smart contract account 由代码(智能合约)控制,没有私钥,它不能主动发起交易,只能在被 EOA 或其他合约“调用”(发送消息)时被动执行其代码。
ETH模型的优点:使得智能合约(复杂的应用程序)成为可能。模型更直观,易于开发 DApps。缺点: 交易必须按顺序处理(因为有 nonce 机制防止重放攻击),这可能导致网络拥堵。

账户地址到账户状态的映射 , 账户地址是 160 位。
以太坊 (ETH) 的核心数据结构是 Modified Merkle Patricia Trie, 简称 MPT。我们可以把它拆解成两个关键概念的组合来理解:Merkle Tree (默克尔树) 和 Patricia Trie (帕特里夏·树,或称压缩前缀树)
Merkle Tree (默克尔树):它是一种哈希树。树底部的每个“叶子”是数据块的哈希值。相邻的哈希值两两组合再哈希,层层向上,最终汇聚成一个“根哈希” (Root Hash),如下图:
这种树有一个特点是只要树中的任何一个数据发生(哪怕是 1 bit 的)改变,最终的“根哈希”都会变得完全不同。这使得节点只需比较这一个根哈希,就能快速验证彼此是否拥有完全相同的海量数据。
+-----------------+
| Merkle Root | <- 最终的"指纹" (H_ABCD)
| (H_AB + H_CD) |
+-----------------+
/ \
/ \
+---------------+ +---------------+
| Hash_AB | | Hash_CD | <- 中间节点
| (H(T1)+H(T2)) | | (H(T3)+H(T4)) |
+---------------+ +---------------+
/ \ / \
/ \ / \
+-------+ +-------+ +-------+ +-------+
| H(T1) | | H(T2) | | H(T3) | | H(T4) | <- 叶子节点
+-------+ +-------+ +-------+ +-------+
| | | |
+-------+ +-------+ +-------+ +-------+
| T1 | | T2 | | T3 | | T4 | <- 原始数据
+-------+ +-------+ +-------+ +-------+
比如我们上图有四个数据块 T1, T2, T3, T4,然后计算哈希H(T1), H(T2), H(T3), H(T4)构成叶子节点,然后他们的父节点分别由他们拼接起来再哈希获得。如果有人把 T3 改成了 T3*,那么 H(T3) 会变,Hash_CD 也会变,最终 Merkle Root 会变得完全不同。
Patricia Trie (称压缩前缀树)
它其实是 Trie 进化而来的,可以高效地存储和查找键值对 (Key-Value),特别是当“键”(Key) 有相同前缀时,它能极大压缩存储空间。举个例子,比如我们要存储以下几个键值对(以单词为例):
"romane": (值 1)"romanus": (值 2)"romulus": (值 3)"rubens": (值 4)"ruber": (值 5) (Root)
|
"r"
/ \
/ \
"om" "ub"
/ \ / \
/ \ / \
"an" "ulus" "e" "ens" (值 4)
/ \ | |
/ \ (值 3) "r"
"e" "us" |
| | (值 5)
(值 1) (值 2)
我们可以看到这个树基本上和 Trie 类似,唯一的区别就是对路径进行了压缩,对比普通前缀树会是 R -> O -> M -> A -> N -> E。看,R->O 和 O->M 都是“单行道”,只有一个子节点。Patricia 的压缩它会把这些单行道合并。R 后面有两个分叉 ("o", "u"),所以 "r" 节点保留。但 "r" 之后的 "o" 和 "m" 都是单行道,所以它们被压缩成了 "om" 节点。同理,"rub" 被压缩成了 "ub" 节点。
使用 Patricia Trie结构对 ETH 来说主要有几点好处:
ETH 需要跟踪数以亿计的账户,每个账户都有自己的状态(余额、nonce、合约代码等)。Patricia Trie 可以方便的用来将这些数据组织成 key value 对,比如 key 存的是账户地址 (0x...),value存的是该账户的状态信息。
并且以太坊的“键”(如账户地址)非常长(160位或更长)。如果使用标准的前缀树,从根节点到每个叶子节点都会有非常多的层级,而Patricia Trie它会把所有“没有分叉的单行道”路径压缩合并成一个节点,从而节省空间。
Trie 树的最终形状和根哈希只取决于它所包含的“键值对”数据,而与插入这些数据的顺序无关。以太坊是一个全球分布式的系统。不同的节点在构建区块时,可能会以不同的顺序处理(本地缓存或插入)状态数据,所以这一点也是至关重要的。
最后就是 Patricia Trie 允许高效的状态更新,当一笔交易发生时(例如,A 转账给 B),通常只有极少数的“值”被改变了(A的余额减少,B的余额增加),种树形结构允许只更新从被修改的叶子节点到根节点的那条路径上的节点。
在 ETH 结构中,有三棵树都是使用的Patricia Trie结构:交易树 (Transaction Trie)、收据树 (Receipts Trie) 、状态树(State Trie),所以我们来看看 ETH 的 Merkel Patricia Trie 是怎么做的。

ETH 它把键视为 16 进制的 nibble(半字节)序列,用三种节点(Branch / Extension / Leaf)压缩表示键空间,并对每个节点做序列化后取哈希,最后得到整棵树的根哈希,如上图所示。
nibble (半字节)序列简单说就是将输入的 字节流 (byte stream) 转换为 16 进制字符流。因为一个字节 (Byte) 包含 8-bit,而一个半字节 (Nibble) 包含 4-bit,所以每一个字节都会被精确地拆分成两个半字节 (nibbles)。一个 8-bit 的字节,比如 0x7A,就会被拆分成了两个 nibbles:[7, a]。
下面我们看看三种节点(Branch / Extension / Leaf):
Branch 节点(branch) — 有 16 个子指针 + 一个可选值槽(用于恰好在此处结束的键):
BranchNode:
+----------------------------+
| v0 | v1 | v2 | ... | v15 | value |
+----------------------------+
value 字段用于当某个键恰好在该节点结束(即键完全耗尽)时保存对应值。Extension 节点(extension) — 用于把一段共享前缀聚合成一条边:
ExtensionNode:
+----------------------+ 指向下一级节点
| path: [nibble数组] | ---> 子节点
+----------------------+ (Branch/Leaf/Extension)
path 是一段 nibble(十六进制半字节)序列;extension 不包含值,只是压缩中间相同前缀。Leaf 节点(leaf) — 存储键的剩余部分(从分支到末尾)和对应值:
LeafNode:
+---------------------------+
| path: [剩余 nibble数组] |
| value: bytes |
+---------------------------+
比如我们把把三个键插入到空的MPT中:
[a, b, c, d] -> 值 V1
[a, b, c, e] -> 值 V2
[a, b, f] -> 值 V3
root = Extension([a,b]) -> Branch
/ | ...
c f
| \
Extension([c]) Leaf([ ] -> V3)
|
Branch
/ \
d e
| |
Leaf Leaf
(V1) (V2)
路径 [a,b] 是共有前缀,[c](成为 extension/直接连接到 branch 继续分叉指向 leaf v1 和 leaf V2,子槽 f 指向 leaf 值是 V3。
在 Block Header 里面有四棵树,状态树 (State Trie)、交易树 (Transactions Trie)、收据树 (Receipts Trie)、提款树 (Withdrawals Trie) 都是用 MPT 来构建的。
状态树 State Trie它记录了所有账户的全局状态(余额、nonce、合约代码、合约存储)。需要注意的是 这是唯一一棵持久化的树。它不只是记录这个区块发生的事,而是记录了在执行完这个区块的所有交易之后,以太坊全世界所有账户(包括智能合约)的最终状态。每个新区块都会在旧状态树的基础上进行“更新”,产生一个新的 stateRoot,其他不变的账号状态还是用原来的节点。

主要包含:
nonce(交易计数)。codeHash(代码)。storageRoot(指向它自己的存储树)。交易树 (Transactions Trie)里面包含当前区块中的所有交易。它的唯一目的就是按顺序存储仅属于这个区块的所有交易。
主要包含:
收据树 (Receipts Trie)包含当前区块中所有交易的执行回执(Receipts)。 这棵树对于 DApp 和钱包至关重要。当你想知道“我的交易成功了吗?”或者“某个智能合约是否触发了某个事件(Event)?”,你就是在这棵树里查找(或验证)这个“收据”。
主要包含:
交易 0 的结果:status: success, gasUsed: 21000, logs: [...]
交易 1 的结果:status: failure, gasUsed: 50000, logs: []
提款树 (Withdrawals Trie)这是“上海/Shapella”升级后新增的,专门用于处理从信标链(共识层)提款到执行层的操作,它为质押提款提供了可验证的记录。
主要包含:
提款 0:验证者 A 提取 X ETH 到地址 B。
提款 1:验证者 C 提取 Y ETH 到地址 D。
在 ETH 的区块头还有一个logsBloom 字段,它使用布隆过滤器(Bloom Filter)来实现的,目的是为了做“快速索引”或“摘要”。
在以太坊上,智能合约通过触发“事件”(Events/Logs)来与外界(DApp 前端、钱包)通信。例如,一个 ERC-20 代币合约在转账时会触发一个 Transfer 事件。假设你的钱包想显示你所有的 ERC-20 代币转账记录。它该如何找到这些记录?
如果没有 logsBloom ,那么钱包必须下载整条链(几 TB 的数据),然后遍历每一个区块里的每一笔交易的每一条收据(Receipt),逐一检查其 logs 字段,看看是不是你想要的 Transfer 事件。这对于轻客户端(如手机钱包)或 DApp 前端来说是绝对不可能的。
logsBloom 巧妙的使用布隆过滤器(Bloom Filter)来构建区块中的交易触发的所有事件索引 log,当一个合约触发一个事件时,例如 Transfer(address indexed from, address indexed to, uint256 value),以太坊会把触发事件的合约地址和所有被 indexed (索引) 标记的参数(比如 from 和 to 的地址)“添加”到这个布隆过滤器中。
布隆过滤器(Bloom Filter)是一种概率型数据结构,它非常节省空间,专门用来回答一个问题:“某个东西可能 在这个集合里吗?”
它的回答只有两种:
对 布隆过滤器(Bloom Filter)感兴趣的,可以去看我这篇文章:Go语言实现布谷鸟过滤器
那么有了这个 Log 我们就可以:
0xABC...”的 Transfer 事件。logsBloom 字段。logsBloom 提问:“你这里面可能包含 0xABC... 这个地址吗?”logsBloom 回答 “绝对没有”那么钱包100% 确定这个区块里没有任何一笔交易触发了与 0xABC... 相关的事件。logsBloom 回答 “可能有”钱包才会去下载这个区块的完整数据,来精确找到它要的 Transfer 事件。
我们先来说一下为什么要有PoW (工作量证明) 和 PoS (权益证明) 这种共识机制,它们都是为了解决一个在计算机科学中极其古老且棘手的问题,尤其是在一个“去中心化”和“无需信任”的环境中,用来确保即使网络中充满了互不信任的陌生人(甚至有坏人),整个系统也能安全、一致地运行。
它们具体解决了以下三个关键问题:
防止"女巫攻击" (Sybil Attack) —— 谁有资格记账?
在一个开放的网络中,一个坏人几乎可以零成本地创建一百万个“假身份”(节点)。如果“记账权”是靠“一人一票”来决定的,那么这个坏人就能轻易地用他的“百万大军”投票控制整个网络。
因为在 web2 中,是有一个去中心化的节点来控制的,所以一般是通过认证与授权 (Authentication & Authorization)来实现的,但是 web3 中,是去中心化的,所以需要设计这样的共识机制,用它增加记账的门槛,防止坏人可以低成本的记账,对网络产生影响。
防止"双花" (Double-Spending) —— 如何确保账本不可篡改?
"双花"是数字货币的“原罪”。坏人张三有 10 ETH,他先发一笔交易给李四,同时(或之后)又发一笔交易把同样的 10 ETH 发给王五。网络必须决定哪一笔交易是“唯一真实”的。
在 web2 中,是通过 一个中心化的数据库 (Single Source of Truth) 来实现的,在web3中是一个分布式的账本,我如何确保所有人都同意“张三的 10 ETH 是先给了李四,而不是先给了王五”?
那么web3中就可以共识机制就可以设计一些有成本的操作,让“作恶成本”提高,来确保账本的唯一性和不可篡改性。
激励机制 (Incentives) —— 为什么有人愿意来记账?
既然保护网络这么昂贵(要买矿机或锁定 ETH),为什么会有人愿意做这件事?
在 Web2 中,不需要通过激励“陌生人”来进行记账,银行只需要商业模式 (Business Model) 和 雇佣 (Salary)机制来保障,不需要别人来记账。
但在web3中需要奖励机制来鼓励诚实者来进行记账,诚实节点地遵循规则、打包区块、验证交易,系统就会奖励你新发行的代币(例如 ETH)和用户支付的交易费。这样ETH 越有价值,你作为奖励收到的 ETH 就越值钱,你也就越有动力去保护它。
所以这也是为什么在 web3 中需要PoW (工作量证明) 和 PoS (权益证明) 这种共识机制。下面我们来看看这两种共识机制有什么区别。
PoW (工作量证明)其实就是需要矿工投入巨额的硬件成本和电费(物理工作)来解题。第一个解出题的就拥有的记账权。但是 PoW 有个极大的问题就是它不环保,浪费了大量的电来做这个事情。
那么就有人提议,其实PoW为了能去挖矿是需要投入巨量的硬件成本和电费,最后就是谁投入的钱多,谁就拥有这个记账权,既然如此,可以不可以直接点,直接用金钱来做抵押,那么这就是PoS (权益证明)基本理念,在 PoS 机制下,网络的安全不再依赖于消耗能源,而是依赖于经济激励和惩罚。
我们下面来详细看看 PoS 是怎么做的。
在 PoS 中验证者取代了 PoW 中的“矿工”。他们是运行特定软件的节点,负责处理数据、执行交易,并将它们打包成新的区块添加到链上。要成为一个验证者,你需要向一个特殊的智能合约中质押 32 个 ETH。
质押就是将 32 ETH 锁定,这部分的资金会在惩罚的时候用到,一旦发现作恶,作恶者的一部分质押金(最多 32 ETH)将被销毁(永久消失),并且该验证者将被强制踢出网络。比如在同一个时隙提议两个不同的区块(试图分叉),抑或是提交自相矛盾的投票(例如试图支持两条不同的链),这些都是作恶的行为。
在 ETH 中,是按时间来组织打包区块的,每个 固定的 12 秒时间段被称为 Slot,理论上每个 Slot 都会产生一个新区块。
每个 Slot,系统会随机选择一个验证者作为“区块提议者”(Proposer), 32 个Slot 组成一个Epoch(约 6.4 分钟)。
ETH 的 PoS 架构分为两层:
具体步骤:
在每个 12 秒的 Slot 开始时,共识层会从所有验证者中随机抽选一个验证者,作为这个 Slot 的“提议者”(Proposer)。
提议者打包区块,被选中的 proposer:
共识层会为同一个 Slot 随机抽选一组(一个“委员会”/Committee),大约 100~几百个验证者组成的 Attestation 委员会(Committee)
委员会的工作:
这些“赞成票”会汇集到共识层。
上面的讲述中,为了防止节点作恶,PoS 设计了一套安全机制。
如果诚实地参与提议和投票,验证者会获得两种奖励:
如果节点作恶,那么就会执行相应的惩罚,这里的惩罚分为几种:
还有就是真的就是有作恶者进行了分叉选择 (Fork-Choice),提议了两个不同的区块,或者网络延迟导致出现了两个竞争的区块(分叉),怎么办?
在 ETH 中是通过 LMD-GHOST 机制来保障。节点根据“所有验证者最近一次投票(attestation)”来决定哪条分支最“重”,从而选出链头(head)。
当你的以太坊节点需要确定“主链的头部是哪个区块”时,它会执行以下操作:
Casper FFG 就是 ETH 的最终性 Finality 的机制,用来决定哪些区块被不可逆地锁定。
因为LMD-GHOST 选出的“头部”是可以改变的。如果网络延迟很严重,或者有攻击者在故意制造分叉,LMD-GHOST 选出的“最重链”可能会在 A 链和 B 链之间“摇摆不定”。这样用户没法确定这笔存款是不是100%到账了。
在 BTC 中我们知道它是通过“6 个区块确认”来确保交易的理论安全,而 FFG 是通过 Epoch 来确定。
ETH 在每个 Epoch 的第一个区块设立了Checkpoint (检查点),FFG 会通常跨越两个 Epoch(约 13 分钟)来实现最终确定。
举个例子,假设我们现在处于 Epoch 100:
第一步:标记Justification (合理化)
C(99) -> C(100) 这条链;C(100) 这个检查点区块被标记为 "Justified" (合理化),但是 Justified只是意味着“这个区块看起来非常棒,全网大部分人都同意它在主链上”,但它还不是最终的。
第二部:Finalization (最终确定)
C(100) -> C(101);C(101) 也获得了超过 2/3 的投票,它自己也变成了 "Justified";C(101) 的“来源”——C(100),因为 C(100) 本身是 "Justified" 的,所以协议在这一刻将 C(100) 的状态升级为 "Finalized" (最终确定);所以我们可以从上面看的出来 ETH 的 PoS 机制依靠验证者(质押 32 ETH)而非矿工来保护网络。其安全不靠算力,靠押金:诚实有奖,作恶(如双重签名)则被罚没 (Slashing)。
这套系统由两个协同工作的机制驱动:
简单来说:智能合约就是运行在以太坊区块链上的一段“自动执行的代码”。我们可以把它比喻成一台全自动贩卖机。
智能合约有这几个特点:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleBank {
// 1. 状态变量 (State Variable)
// 这些数据会永久写入区块链,类似于数据库中的表
mapping(address => uint256) public balances;
// 2. 事件 (Event)
// 类似于日志系统,用于前端监听
event Deposit(address indexed user, uint256 amount);
// 3. 函数:存款
// payable 关键字表示该函数可以接收 ETH
function deposit() public payable {
require(msg.value > 0, "Deposit amount must be greater than 0");
// msg.sender 是调用者的地址
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
// 4. 函数:提款
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
// 将 ETH 转回给用户
payable(msg.sender).transfer(amount);
}
}
mapping 就是 Key-Value 存储(类似于 Redis),address 是 Key,uint256 是 Value。
msg.sender 和 msg.value 是全局注入的上下文变量(Context),无法伪造。
require 类似于 Assert 或中间件校验,如果不通过,整个事务回滚。
需要注意的是,智能合约的每次执行具有原子性,比如像上面这个银行的例子,如果失败了,就整个操作进行回滚,不存在中间状态;如果所有步骤都成功,所有的状态变更(State Changes)会被一起写入区块,永久生效。其实这就有点像数据库的事务。
熟悉数据库的朋友我可以这么解释:
在 EVM(以太坊虚拟机)中,每一笔交易天然就是一个隐式的 START TRANSACTION ... COMMIT 块。你不需要显式地写 Commit,但任何未捕获的错误都会触发自动 Rollback。
原子性不单单局限于单个合约内部,而是可以跨越多个合约的调用链。
假设一笔交易的调用链是这样的:
用户 -> 合约 A -> 合约 B -> 合约 C
如果在 合约 C 的执行中出错了:
这里就有一个问题,执行失败会收 Gas 费吗?我们先看看什么是 Gas 费
Gas 费本质上等于:工作量(Gas Units)X 单价(Gas Price)。
EVM(以太坊虚拟机)执行的每一个操作码(Opcode)都有一个固定的 Gas 消耗值,操作越复杂,消耗越高。
比如下面这些操作:
计算操作(便宜):
ADD (加法): 3 GasMUL (乘法): 5 GasKECCAK256 (哈希计算): 30 Gas + 动态数据费用存储操作(极贵):
SSTORE (写入/修改状态变量): 20,000 Gas (这是最贵的操作之一)SLOAD (读取状态变量): 2,100 GasLOG (生成日志): 375 Gas基础费用:
这是由市场供需决定的变量。Gas Units 是“你需要多少升油”,Gas Price 就是“今天加油站一升油卖多少钱”。
Gas 的单位是 Gwei:
动态定价:
现在的以太坊(EIP-1559 升级后)计费变得稍微复杂了一点点,分为两部分:
Tx Fee = Gas Used X (Base Fee + Priority Fee)
举个具体的例子:
假设你要在这个拥堵的周五晚上,调用一个合约函数 buyItem()。
A. 代码层面(Gas Units) EVM 跑完你的代码,发现你做了一次加法,写了一次数据库,总共消耗了 50,000 Gas。
B. 市场层面(Gas Price) 当前的 Base Fee 是 50 Gwei,为了快点成交,你给了 2 Gwei 的小费。 总单价 = 52 Gwei。
C. 你的账单
花费 = 50000(Units)X 52(Gwei) = 2,600,000 Gwei
换算成 ETH 就是 0.0026 ETH。 假设 ETH 现价 $3000,这笔操作就要花你 $7.8 美金。
两种失败场景的区别:
在 EVM 中,交易失败主要分两种情况,扣费逻辑略有不同:
场景 A:Gas 耗尽(Out of Gas)
Gas Limit 是预算上限,假如设置了100,000Gas ,意思是“我这笔交易最多允许烧掉 100,000 Gas,再多我就不付了”。
如果代码跑到 100,000 还没跑完(Out of Gas),EVM 强制停机,这 100,000 的钱全部扣掉不退,且交易回滚。
场景 B:逻辑错误(Revert / Require 失败)
比如转账余额不足、权限不够。比如代码运行到第 10 行,触发 require(false)。这样只扣除前 10 行代码消耗的 Gas。剩余未使用的 Gas(即 Gas Limit - Gas Used)会退回到你的钱包。
为什么要这么设计?
对于去中心化网络,这是为了防止 DDoS 攻击(拒绝服务攻击)。 如果失败不收费,黑客可以写一个无限循环的恶意合约:
while(true) { i++; }
然后向网络发送几百万笔交易来调用它。如果不收费,全网节点的 CPU 就会被免费占用,导致网络瘫痪。
我们可以看出ETH的智能合约,把区块链从“账本”升级为了“通用计算平台”。没有智能合约,区块链就只能炒币(Store of Value);有了智能合约,区块链才有了应用层(Application Layer),多了更多的活力和玩法。
比如:
还有很多有意思的玩法,我这里就不一一列举了,如果要深入学习智能合约的话,可以看这几个教程:
https://cryptozombies.io/ 通过建一个“僵尸养成”区块链游戏,一步步教你写Solidity智能合约。
https://docs.soliditylang.org/ Solidity官方英文文档
https://www.wtf.academy/zh/course/solidity101 WTF Solidity极简教程
ETH 社区定义了 ERC-20 标准的 API 接口规范,任何一个智能合约,只要实现了这套规定的 API(方法和事件),它就是一个 ERC-20 代币。
我们可以把代币想象成存储在一个巨大的 Map 里面,比如这样的一个结构:
// 这是一个简化的核心存储结构
contract ERC20 {
// 1. 账本:记录 "地址 -> 余额"
mapping(address => uint256) private _balances;
// 2. 授权表:记录 "我 -> 授权给谁 -> 多少钱"
mapping(address => mapping(address => uint256)) private _allowances;
// 3. 代币总供应量
uint256 private _totalSupply;
}
当你发起一笔转账(例如 A 转给 B 100个币)时,EVM(以太坊虚拟机)到底做了什么?
transfer(B, 100) 的交易。_balances[A] 是否大于等于 100。_balances[A] = _balances[A] - 100_balances[B] = _balances[B] + 100
我这边再引用一下上面的图,合约其实在 ETH 里面也是一个账户对象,当你通过地址找到这个账户(比如那个 ERC-20 代币合约地址)时,你会得到一个包含四个字段的结构体:
Nonce: 交易计数器。
Balance: 这里存的是 ETH 的余额(比如 0.5 ETH),不是代币余额。
CodeHash: 智能合约的代码哈希(如果是普通账户则为空)。
StorageRoot (存储根): 这是一个哈希值,它指向了另一棵 MPT 树的根节点。这棵树专门属于这个合约,用来存储它所有的变量数据。
这棵树本质上是一个巨大的、持久化的 Key-Value 映射,其实也是一颗 MPT 树。
比如这样一个代币合约:
contract MyToken {
// Slot 0: 假如这里有个 owner 变量
address owner;
// Slot 1: 假如这里是总供应量
uint256 totalSupply;
// Slot 2: 这里就是代币余额的 Mapping
mapping(address => uint256) private _balances;
}
当 EVM 运行到 _balances 时,它并不会把整个 Mapping 存在 Slot 2 里面(因为 Mapping 大小是不确定的)。 它是通过哈希算法计算出具体的存储位置。
如果你想查 0xUserA 这个人的余额,数据存储在 Storage Trie 中的 Key 是这样算出来的:

_balances 变量在代码中声明的位置(假设是 Slot 2)。运算得出的结果(一个乱码一样的哈希值),就是该用户余额在底层的物理存储地址。 而这个位置对应的 Value,就是 uint256 类型的余额数值。
所以如果要查询代币的值,整个查找链条是这样的:
Block Header -> 拿到 StateRoot。
World State Trie -> 用 TokenContractAddress 查找 -> 拿到 Account Object。
Account Object -> 拿到 StorageRoot (进入该合约的私有数据库)。
Storage Trie -> 计算 keccak256(UserAddress + SlotIndex) 作为 Key -> 拿到余额数据。
2025-11-02 20:37:17
最近有点时间,想找个地方休息一下,本来只是想去熊本看看高达的,别府只是顺路去一下,没想到这个小地方还是挺惊艳到。
别府有着很多温泉资源,拥有近3,000个温泉源头,温泉涌出量位居日本第一,在全球也仅次于美国的黄石国家公园。
温泉水多到什么程度呢,别府站旁边就有个池子可以用温泉水洗手,没错,就是下面这个雕像的右边,顺带一提,这个雕像的衣服会经常换,各位如果也来别府,看看他会穿什么。

走在路上你甚至可以在街上的下水道里面看到有冒着白雾,也就是说这些下水道的水都是温泉水哦。

在别府有很多温泉,大都集中在铁轮温泉区,其中最出名的就是这个温泉,葫芦温泉,是一家百年的老店。

这个温泉固然是很好的,但是我不只是想讲这个温泉体验怎样。在泡温泉的时候,我就想泡温泉的时候就和跑步好像:
不能玩手机代表你必须要聚焦于自我的思绪当中,不受外界的信息干扰,表示你有更多的精力让你的思想放空,可以想到不一样的事情。很多时候,我都是在跑步的时候,放下手机的时候突然想起要做某件事情,然后去做,这篇文章也是一样,在泡温泉的时候,想起可以去写一篇这样的文章。
身体在承受一定的痛苦意味着身体会时不时提醒你有多久没关注过当下的生活了,你在吃什么,身边在发生什么事,经过了什么样的景色,遇到了什么样的人,似乎我们都错过了好多。但是泡温泉和跑步的时候,身体的痛苦会把你的思绪拉回来,让你记住当下的生活。
比如今天我泡温泉就记住了在一个池子里面有个老人长的挺帅的,可能有六七十岁了,戴着眼镜,有点像电影里面的老人,相信他穿着打扮一下,肯定气质不凡。
在露天温泉里有几个年轻人在我左前方一直在讲话,让我想起,要是国内也能这么方便的可以泡温泉就好了,一般我们都喜欢边吃饭边聊天,其实我们也可以把场景换一下,边泡温泉,大家赤诚相见,边泡温泉边聊天,其实也是不错的。
泡温泉和跑步有一点是不一样的,跑步都是越快越好,大家都在追求速度,但是温泉不一样,强制大家慢下来。在日本泡温泉的时候,会有告示牌告诉你,别这么急着进去泡,先慢慢洗一下身子。洗完身子之后,要缓步走到池子里,因为都是水,急的话就容易摔倒。进到池子的时候,也是要慢慢进去,因为水温很烫,身体一下钻进去肯定会把你烫出来。
我在国内生活很多年,几乎所有人都在说快一点,快一点。要快点学,赶紧考个好大学;要快点工作,好赚多点钱养家;要快点结婚生子,好繁衍子嗣;要快点工作完,好完成当下的kpi。那么什么时候慢下来呢?是必须要这么快吗?
说会到葫芦温泉,这里的水煮蛋,应该是我在日本看过最便宜的水煮蛋,只要80日元。

在别府这里还有一个温泉,别府鉄輪蒸し湯,也是一家百年的老店,体验也还不错。

这个温泉的特点仪式感很重,首先会让你进去先用温泉水淋一下,对,就是单纯的淋一下,然后穿上浴衣之后进入到蒸汽房里面,躺在草堆上,用药草蒸10分钟,到8分钟的时候门外会有人问一下你是否要提前出去,出来之后是感觉确实不同,很舒服。蒸完之后把浴衣换下之后就清洗一下身子开始泡温泉了。
汗蒸这个东西,其实很多温泉店都会有,但是这家温泉会强制顾客一定要去汗蒸一下,确实不太一样。
日本其实是一个很重仪式感的地方,它深刻地根植于日本文化的各个层面。比如泡温泉的时候,进玄关的时候会让顾客脱鞋,让顾客穿他们的浴衣,礼物本身可能不贵重,但包装一定要精美,店里面的店员一定会对顾客鞠躬问好等等。
其实我是一个不怎么喜欢仪式感的人,比如我一般在逢年过节的时候不会特意的去买各种节日食品,月饼,汤圆之类的,过生日也不喜欢搞什么生日聚会,不会买蛋糕,也不会去买礼物。
但是我的观念最近在慢慢的改变,我觉得仪式感可能没什么不好。生活本质上是充满不确定性和混乱的。仪式感通过固定的程序和可预测的步骤,为我们创造了一个“可控”的心理空间,其实是一种对抗不确定性的方式。并且仪式感可以将平凡的日常行为转变为特殊且有意义的时刻,其实也蛮有意思。
比如同样是吃饭,用你最喜欢的餐具、摆放整齐、关掉电视专注地吃,这顿饭的体验和价值感就远高于边看手机边草草了事。我们大部分时间都处在“自动驾驶”模式,脑子里想着过去或未来,想着各种事情。而仪式迫使你将注意力拉回到“此时此刻”的身体和感官上,这也是仪式的意义。
在日本比起大城市,其实我更喜欢乡下,因为没什么人,并且很干净。有时候看着这种没人的干净街道,就想一直逛下去,即使没什么好看的,也可以安安静静地走一天。

11月了草还挺绿。

我还是很喜欢海边散步的。

晚霞

写于 2025年11月2日晚,别府三日游明早就要走咯。
2025-10-30 23:08:27
加密货币(Cryptocurrency)是一种运用密码学原理来确保交易安全并控制新单位创造的数字交易介质 。
根据 Jan Lansky 所述,加密货币是满足六个条件的系统:
这些条件共同定义了一种革命性的资产形式,其价值和安全性不依赖于任何单一机构的信用背书。有些人简单的将它归结为“数字化”,其实是不对的,因为支付宝、信用卡支付等都是数字话的,但是这些传统数字金融系统的核心是建立在对中心化中介机构(如银行、支付网关)的“信任”之上,由它们来维护账本、验证交易的合法性 。加密货币的根本性突破在于,它通过密码学、分布式共识等一系列技术手段,构建了一个“去信任化”(Trustless)的系统。
所谓区块链,其实是由一系列按时间顺序连接的数据单元构成的,这些单元被称为“区块”(Block)。每一个区块包含以下内容:
链条的起点是一个特殊的区块,被称为“创世区块”(Genesis Block),因此没有指向“前一个区块”的引用。
那么在实现上,区块是如何保存交易数据和引用的呢?数据结构是怎样的?每个区块在数据结构上,包含了区块头(Block Header)和区块体(Block Body):
我们以btc为例,在btc协议中,区块头大小为80字节 。根据功能,这80字节可以被划分为六个独立的字段,共同构成了区块的元数据 。
版本(Version) – 4字节
前一区块哈希(Previous Block Hash) – 32字节。这是区块链数据结构中最关键的连接元素,它就是哈希指针。该字段存储的是其父区块(即链上的前一个区块)的区块头的SHA256(SHA256())双重哈希值 。通过这个字段,每个区块都牢固地指向其前驱,从而将独立的区块编织成一条不可分割的、按时间顺序排列的链。

Merkle Root – 32字节。btc 中使用的 merkle tree 的形式存储了区块体内所有交易,所以在头里面,还存储了区块中 merkle tree 的头节点,也就是 merkle root;
时间戳(Timestamp) – 4字节,记录了该区块被矿工创建的大致时间。
难度目标/比特(Difficulty Target / Bits) – 4字节。表示了当前区块挖矿的难度目标 ,也就是用来挖矿的。
随机数(Nonce) – 4字节。这是一个由矿工在挖矿过程中不断改变的计数器 。矿工将版本、前一区块哈希、默克尔根、时间戳、难度目标和这个随机数拼接在一起,形成80字节的区块头,然后对其进行哈希计算。如果结果不满足难度目标,矿工就将随机数加一,然后再次尝试。这个暴力枚举的过程就是工作量证明的核心。
区块体则承载了该区块内所有经过验证的交易信息,所以可以把它理解为账本。以btc为例,它的区块体的结构由两部分构成:
那么如何记录一笔交易呢?比如,我们现实生活中进行转账,A 要给 B 转账,那么对于这笔记录首先我们需要知道这个币从哪里来的,这个叫做输入(Inputs);然后需要知道这个币是转给了谁,这个叫做输出(Outputs)。在转账的过程中,由于互联网上是基于互不信任的原则,所以这笔转账的过程还需要密钥加密,这叫数字签名(Digital Signatures),数字签名由资金所有者使用其私钥生成。
A转了5个币给B,给了 5个币给 C,这个过程中不停的交易,形成的这个链就是账本。比如下图,可以看成是交易账本的简化形式。

那么交易账本的形式有了,那么如何构建安全的交易呢?首先,我们要了解一下什么是签名,上面我们也说了,在交易的过程中,A 转帐给 B,A 需要给这笔转账用 A 的私钥加密,这其实就是签名。
整个签名,其实就是非对称加密的过程,在btc钱包中,公钥相当于银行账号,私钥相当于银行密码。比如说A 要给 B 转 1 个 BTC,当 A 发起这笔交易时,他的钱包软件会做以下事情:
首先要回答 A 的“钱包”里有什么?
假设 A 的钱包里并不是一个写着“我有 1.2 BTC”的数字。实际上,他的钱包知道他拥有两笔“未花费的钱”(Unspent Transaction Output,简称 UTXO),比如:
A 要给 B 转 1 个 BTC
当 A 发起这笔交易时,他的钱包软件会做以下事情:
最后就是验证这笔交易如何验证,在 BTC 中整笔交易要达成共识,入链才算完成。
这笔交易被广播到btc网络后,每一个收到它的节点(矿工)都会进行严格的验证:
好了,到这里,一笔交易内容有什么,以及如何保证安全已经说明了,那么如何在不下载全部数据的情况下,高效地验证某一个“小数据”是否属于这个“大数据集合”?就比如,我的手机钱包,如果验证 1 笔交易,而不用下载整个区块?
这就要提到 Merkle Tree,它是交易列表构成的一个树形结构,Merkle Tree 的叶子节点存的是每一笔交易的哈希值(Transaction Hash,也叫 TXID),可以看成是下图这样的结构:

merkle tree 本质上是一棵哈希二叉树:
树的叶子节点是原始数据块(在区块链中就是一笔笔的交易)的哈希值。
树的非叶子节点(树枝和树干)是它下面两个子节点哈希值拼接后再计算出的哈希值。
这个过程不断重复,层层向上,直到只剩下一个最终的、位于最顶端的节点,这个节点被称为 merkle root。
merkle tree被发明出来主要有两个目的:
保证数据完整性(防篡改)
merkle root 是对区块内所有交易的最终摘要。任何一笔交易哪怕被改动一个字节,其对应的叶子哈希就会改变,这个改变会像多米诺骨牌一样,层层向上传导,最终导致计算出的merkle root 完全不同。
极高的验证效率(轻量级验证)
它允许在不下载整个区块数据的情况下,就能快速验证某笔交易是否存在于该区块中。
比如手机钱包知道自己的交易哈希 H3,钱包从网络上下载了该区块的区块头(只有80字节,非常小),并从中获取了正确的merkle root ,那么对于 H3来说,它的验证路径是 H3 -> H34 -> Merkle Root。也就是只需要,它的直接兄弟 H4,它的上一层节点的兄弟 H12,然后就可以通过计算 hash 进行对比验证,这个过程就叫Merkle Proof。
整个过程,手机钱包只需要下载几十字节的区块头和几十字节的 Merkle Proof,就能完成验证,而无需下载整个区块(可能好几MB)的所有交易数据,极大得节省了资源。
所以通过 merkle tree 就可以实现高效的“存在性证明”(Merkle Proof)以及保证数据完整性(防篡改)。
其中在区块链中使用 Hash 算法有其关键的作用:
collision resistance:在btc中使用的是SHA256(SHA256())双重哈希值,几乎不可能出现hash碰撞,因为如果可以轻易找到“碰撞”(两个不同输入得到相同输出),那么恶意攻击者就可能用一笔伪造的交易来替代合法的交易,从而破坏整个系统的信任;
Hiding:由于算法的不可逆,所以无法由hash值推导出原值。这个特性对于保护数据隐私至关重要。在密码学中,这也被称为抗原像性 (Pre-image Resistance)。;
puzzle friendly:如果想要找到某个特定的hash值对应的输入是什么,只能挨个去尝试,没有其他任何途径可以找到符合条件的hash值。
区块链中,由于下一个的指针是指向前一个,如果某个块的hash发生了改变,那么后续的也要改变,也就是牵一发而动全身,比如一个Merkle tree,叶子节点变了,其他节点也要变,因为其他节点是根据叶子节点算出来的。
[ Merkle Root ]
/ \
/ \
[ Hash_ABCD ] [ Hash_EFGH ]
/ \ / \
/ \ / \
[ Hash_AB ] [ Hash_CD ] [ Hash_EF ] [ Hash_GH ]
/ \ / \ / \ / \
H(A) H(B) H(C) H(D) H(E) H(F) H(G) H(H)
所以btc 中某个本地节点可以只保存最近的某些节点,如果需要前面的其他的节点可以问别人要,并且可以通过hash计算的方式来保证别人给的区块一定是正确的。就比如上图,只有几个节点,但是可以通过后面的节点计算别人给过来的前面的节点是否正确。
举例:
TX_C(交易 C 本身)H(D) (你的“兄弟”哈希)Hash_AB (你“叔叔”辈的哈希)Hash_EFGH (你“伯父”辈的哈希)TX_C 自己哈希一次,得到 H(C)。H(C) 和全节点给你的 H(D) 组合起来哈希:
H( H(C) + H(D) ) 算出了 Hash_CD
Hash_AB 和上一步算出的 Hash_CD 组合起来哈希:
H( Hash_AB + Hash_CD ) 算出了 Hash_ABCD
Hash_ABCD 和全节点给你的 Hash_EFGH 组合起来哈希:
H( Hash_ABCD + Hash_EFGH ) 算出了一个“最终的 Root”
因为哈希的collision resistance特性。如果“别人”的 TX_C 是假的,或者 H(D)、Hash_AB 中任何一个是假的,最终算出来的 Root 都不可能与“标准答案”一致。
一个交易要被认可要取得分布式共识。那么什么什么是分布式共识?
想象一下,一群好朋友(比如 100 个人)共同记一本账本,记录着大家之间谁欠谁钱。他们没有一个中心记账员(比如银行),而是每个人手上都有一本一模一样的账本。
当其中一位朋友 A 要转 100 元给朋友 B 时,他会向所有人大喊:“我要从我的账上转 100 元给 B!”。
这时候问题来了:
分布式共识 就是为了解决这个问题而设计的一套规则和方法。它的目标是:让一个分布式系统中的所有参与者(节点),在没有中央指挥的情况下,最终能够对某个状态或数值达成一致的协议。
上面我们也提到了,如果想要 A 转给 B 1 个 BTC,在 BTC 中整笔交易要达成共识,入链才算完成。这个共识算法就是工作量证明(Proof of Work, PoW)。
在 A 转 1 个 BTC 给 B 的时候,会用私钥对一笔交易进行数字签名,然后钱包会将这笔签好名的交易广播到整个区块链网络中,附近的节点会接收到这笔交易。
之后:
节点验证与传播
初步验证:
接收到交易的节点(我们称之为“矿工节点”)会立即进行验证,首先用A的公钥检查数字签名是否正确,再来就是追溯A的交易历史,确保您确实拥有足够的资金来支付这笔交易;
验证通过后,这笔交易会被放入该节点的“内存池(Mempool)”中,这是一个等待被打包的交易的临时集合;
该节点会将这笔验证过的交易继续传播给与它相连的其他节点,直到这笔交易遍布全网。
竞争记账权(挖矿)
达成共识与全网同步
由于网络问题产生分叉怎么办?比如即同时有两个矿工挖出区块,这个时候为了确保交易不可逆转,通常需要等待更多的区块在此基础上继续生成。
一般来说,在btc网络中,等待 6 个区块确认(大约 1 小时)后,该笔交易就被认为是完全被承认且不可篡改的了。
上面的工作量证明看起来实际上需要很大的计算量,需要很多计算机的算力,所以矿工做这些事情也会获得相应的报酬去激励他们继续保护和运行整个区块链网络。矿工主要有两部分收益:
区块奖励(Coinbase Reward)
这是系统凭空创造出来、作为对矿工维护网络安全奖励的新币。这部分奖励是btc(或其他加密货币)通货膨胀的主要来源。对于btc来说这个奖励的数额是协议预先规定好的,并且会定期“减半”(Halving)。例如,btc最初每个区块奖励50个btc,现在(2024年减半后)是3.125个btc;
交易手续费(Transaction Fees)
这是该矿工从他打包的那个区块中,所有交易的发起者支付的手续费的总和。用户为了让自己的交易能被矿工尽快打包,会附加一笔手续费。矿工自然会优先选择手续费高的交易来打包。这部分收益的数额是不固定的,取决于当时网络的拥堵情况和用户愿意支付的费用。
所以矿工的总收益 = 区块奖励 + 该区块内所有交易的手续费总和。
但是在 btc 中随着时间的推移,区块奖励会越来越少,直到最终变为零(预计在2140年左右)。到那时,矿工维护网络的唯一动力就将完全来自于交易手续费。这个设计确保了即使在所有币都发行完毕后,依然有经济激励促使矿工继续保护和运行整个区块链网络。
上面我们提到了,挖矿的过程其实就是改变block header 里面的 Nonce 字段,计算出一个有效的“哈希值”,计算出的哈希值必须小于或等于当前网络设定的“目标值” (Target)。
举个例子,假设目标是: 00000000000000000005a3f6d8a4c1d8d3f6a8b3c5d1e7f9a2b4c6d8e。那么,任何计算出来的哈希值,只要在数值上比上面这个小,就是有效的。比如:00000000000000000001b8d3c5d1e7f9a2b4c6d8e4a3f6d8a4c1d8 (这个值更小,所以是有效的)。
但是Nonce 只是一个 32 位的字段,目前来说矿机每秒可以执行数百亿亿次哈希运算(TH/s)。一台高端ASIC矿机(例如140 TH/s)可以在微秒级别的时间内遍历整个Nonce空间。
所以仅靠Nonce 是找不到对应难度目标(Target)的值,当然Timestamp也可以有一定的调整空间,但是比较有限,后面就演变成使用 ExtraNonce 来扩展搜索空间。ExtraNonce 指的是矿工放置并修改在铸币交易(Coinbase Transaction)的scriptSig字段(也被称为coinbase data)中的任意数据,它可以进行修改 。
scriptSig 字段通常是用来数字签名来证明所有权用的,但是Coinbase交易是凭空创造新币的,它不消耗任何已存在的UTXO。因此,其输入中的scriptSig字段无需包含任何解锁脚本或数字签名。根据btc协议,这部分空间可以由矿工自定义填充,长度限制在2到100字节之间。
那么挖矿算法的大致流程就会变成:
ExtraNonce值(如0)。hashMerkleRoot。Version、hashPrevBlock、hashMerkleRoot、当前的Time和Bits。FOR nonce FROM 0 TO 2^32:
nonce值。hash = SHA256(SHA256(header))。IF hash <= Target:IF 内层循环完成仍未找到解:
ExtraNonce的值。hashMerkleRoot。hashMerkleRoot字段。Time字段。Nonce的迭代.并且为了控制 btc 的产量,在btc网络诞生之初,矿工每成功打包一个区块,可以获得 50 BTC 的奖励。并且btc的协议规定,每产生210,000个区块,区块奖励就会减少一半(减半)。由于比特币网络的目标是平均每10分钟产生一个区块,210,000个区块大致相当于4年的时间(10分钟×210,000≈4年)。
我们可以用一个等比数列求和的公式来表示这个过程:
总供应量 = 210000×(50+25+12.5+6.25+…) = 210000×50×(1+0.5+0.25+0.125+…)=210000×50×2=2100w

由于区块奖励不断减半,奖励金额会变得越来越小。大约在第33次减半(约2140年左右)之后,区块奖励将变得微不足道(小于1聪,即比特币的最小单位)。届时,可以说几乎所有的比特币都已被挖出,矿工的收入将完全依赖于交易手续费。
这种通缩模型的设计,使得比特币具有了稀缺性,从而避免了像传统法定货币那样因无限增发而导致的通货膨胀问题。
主要是因为btc它会自动的调整难度,早期的时候参与者少,算力低在只有几 MH/s 的算力下,要遍历完这43亿种可能性需要很长时间(几百秒甚至更久)。当时网络的目标是大约10分钟产生一个区块,因此,仅仅通过不断尝试和改变Nonce,就有非常大的概率能在这10分钟内找到一个符合条件的哈希值。
现在对于一台算力为 100 TH/s(1014 次哈希/秒)的现代ASIC矿机来说,遍历完43亿的Nonce仅需要0.000043 秒,如果还是这个难度,估计用不了几秒就要被挖光了。
btc的难度调整是其协议中最优雅的设计之一,它确保了无论全网算力如何变化,新区块的产生速度都能稳定在平均10分钟一个。
btc的难度目标(Target)是个以一个256位的数字。挖矿的本质就是找到一个区块头的哈希值,使其小于或等于这个目标值。目标值越低,挖矿越难。因为一个更低的目标值意味着哈希结果的开头必须有更多的“0”,符合条件的哈希值就越少,找到它所需要的计算次数就越多。
所以为了实现大约10分钟产生一个区块这个目标,btc会动态的调整难度,难度调整的具体机制如下:
New Target = Old Target * (Actual Time / Expected Time)
Actual Time小于20160分钟(比如只用了12天),说明全网算力增强了。此时 (Actual Time / Expected Time) 这个比率小于1,New Target就会变小,从而增加挖矿难度。Actual Time大于20160分钟(比如用了16天),说明全网算力下降了。此时这个比率大于1,New Target就会变大,从而降低挖矿难度。Actual Time / Expected Time)最大不会超过4,最小不会低于1/4。我们也可以看到下面图的难度曲线的设置,是越来越陡峭的,跟价格几乎是成正比:

并且比特币矿机也不再是以前的 GPU 时代了,而是专门的专用矿卡进行挖矿。比特币矿机的演变是一场追求极致算力和能效比的“军备竞赛”:最初人们用个人电脑(CPU) 就能挖矿,很快被游戏显卡(GPU) 的高并行算力所取代;接着,更省电的FPGA(半定制芯片) 短暂出现,但最终被ASIC(专用定制芯片) 以绝对的算力和能效优势彻底统治,使挖矿从此进入了专业的工业化时代。
Progress-Free 指的是“无记忆性”(Memoryless),也就是在任何给定时刻,矿工找到下一个区块的概率与他们过去已经付出的努力无关。
在btc挖矿中,矿工们不断地进行哈希运算,尝试找到一个小于当前网络目标难度的哈希值。每一次哈希运算都是一个独立的、随机的尝试。也就是说包含两个特性:
由于这两个特性,表示过去的努力不会累积,每时每刻都是一个全新的开始,确保了挖矿的公平性,即便是算力较低的矿工,理论上也有机会在任何时刻找到区块。
指的是网络中掌握显著比例算力(或权益)的参与者(通常是大型矿池或PoS验证者)联合起来,故意“抵制”或“排斥”网络中的某些特定元素。
在这种攻击中最常见的就是一个或多个拥有大量算力的矿池故意拒绝将某些合法的交易打包到他们挖出的区块中。比如,某个矿池为了遵守其所在国的法规(如美国的 OFAC 制裁名单),宣布将“抵制”所有与黑名单地址相关的交易。
这种攻击的严重程度取决于“抵制联盟”掌握的算力。如果抵制联盟的算力低于 51%,被抵制的交易仍然可以被确认,但它们必须等待那些“不参与抵制”的矿池(例如只占 30% 算力)来挖到区块。
如果抵制联盟的算力超过 51%。他们不仅自己不打包这些交易,他们还会故意孤立(orphaning)任何包含了这些交易的诚实区块(因为他们总能挖出更长的链)。结果就是被抵制的交易将永远无法被打包上链。这等同于将某个用户或应用永久地“踢出”了网络。
这种攻击并不像“双花”那样直接窃取资产,但它直接攻击了区块链最核心的价值主张之一:抗审查性(Censorship Resistance)和中立性(Neutrality)。
当单个矿工或矿池掌握了全网超过 50% 的算力时,他们就有能力制造出一条比诚实网络更长的“分叉链”。攻击者可以在主链上将 BTC 发送给商家(例如,换取法币或商品),然后在自己的分叉链上构造一笔交易将同样的 BTC 发送回自己的地址。当他的分叉链长度超过主链并被网络接受时,之前给商家的交易就被“撤销”了,从而实现Double Spending。
但是这样的攻击需要天价的硬件和电力成本,而且成功攻击会摧毁人们对 BTC 的信心,导致币价暴跌,攻击者自身的收益也会大打折扣,,因此在经济上是不理智的。
虽然在BTC上不太可能这么做,但是这一点在那些算力较低的山寨币(altcoins)上体现得淋漓尽致。攻击者可以按小时租用强大的算力(OPEX)。对于一个小币种来说,攻击者可能只需租用比特币网络总算力的一小部分,就能轻松达到该小币种网络51%以上的算力。
攻击者,不需要 51% 的算力(理论上超过25%就有可能获利)。攻击者挖到区块后并不立即广播,而是选择不发布,并基于这个秘密区块继续挖下一个。
当诚实矿工挖到区块A时,自私矿工如果已经秘密挖到了区块A’和B’(比诚实链长),他就会立刻广播自己的A’和B’。网络会接受更长的链,导致诚实矿工的区块A作废,他们的算力被浪费。
为了防范这种攻击,在比特币中一笔交易通常需要等待6个区块的“确认”(Confirmations)后才被认为是最终、不可逆转的,这大约需要一个小时的时间。因为每增加一个确认,攻击者就需要付出更多的算力和时间来追赶并超越诚实的区块链。
中本聪的计算表明,如果一个攻击者掌握了10%的网络算力,那么在6个区块确认之后,他成功实现双花攻击的概率已经下降到了0.1%以下。这个概率被认为足够低,可以保障绝大多数商业交易的安全。“6个确认”因此成为了一个在安全性与用户体验之间取得平衡的行业“黄金标准”。
“硬”分叉 (Hard Fork) 是一种永久性的、不向后兼容的“规则升级”。当硬分叉发生时,旧版本的软件(节点)将不再接受新版本软件(节点)创建的区块,导致区块链永久性地分裂成两条不同的链。
它强制要求所有参与者(矿工、节点、交易所、钱包)必须升级到新软件。如果你不升级,你就会被“留在”旧的、即将被淘汰的链上。
硬分叉在 BTC 最著名的例子就是 Bitcoin Cash (BCH),它在 2017 年 8 月从 BTC 硬分叉出去。
这里有个有意思点,如果是在硬分叉之前拥有了某个加密货币,那么在分叉之后将同时拥有“原始货币”和所有“新分叉出来的货币”。
软分叉 (Soft Fork) 是一种向后兼容的升级,可以认为是把旧规则变得更严格。
比如,以前的规则是“区块大小不能超过 1MB”。软分叉的新规则是“区块大小不能超过 1MB,并且里面必须包含 A 数据”。旧节点(只懂旧规则)看到新区块时,会觉得:“它没超过 1MB,合法。”(它看不懂 A 数据,但不影响)。新节点会严格执行新规则。
这样只要大多数矿工升级,网络就会被“拉”到新规则上,而不会像硬分叉那样导致区块链分裂。
SegWit (Segregated Witness,隔离见证)就是btc链上一次最重大技术升级实现的软分叉。在 SegWit 之前,比特币网络面临两个主要问题:
scriptSig),而不会使交易失效,这种修改会导致交易ID(txid)发生变化。如果这个txid在交易确认前可以被篡改,就会引发严重问题,比如双花攻击的变种,或者让依赖 txid 的复杂合约(如闪电网络)变得极难实现。SegWit 通过改变交易和区块的数据结构,重新定义了一笔交易的结构。它把交易分为两个部分:
txid)只根据这部分数据来计算。scriptSig 和 scriptWitness)。由于 txid 现在只根据核心交易数据计算,而签名(scriptSig)这个唯一可以被延展(篡改)的部分已经被移到了见证数据中,不再参与 txid 的计算。因此,一旦交易发出,其 txid 就被永久固定,交易延展性问题被彻底解决。
再来就是SegWit 并没有直接把 1MB 的区块大小限制(Block Size)改掉,而是引入了一个全新的概念:区块权重 (Block Weight)。
这个权重的计算方式是:
那么:
在理想的(全是SegWit交易的)情况下,一个区块的实际物理大小可以达到接近 4MB,但其“权重”仍然是 400 万 WU。
2025-10-24 11:11:58
先摆出我的四个知识奖的奖牌镇楼,哈哈哈。该是时候总结一下自己这五年多来做了什么事情了,希望对各位能有所启发。

其实我每年都有发自己的总结文章,附上以前的一些总结:
对于我自己来说是比较喜欢研究技术,写文章,所以这五年确实沉淀了很多我以前研究的技术文章。发布的文章承蒙各位喜爱,在腾讯内网也上过很多次头条和推荐,这里我再总结一下,希望这些文章能带给大家一些启发或者帮助。

AI 相关的文章合集是当时因为图片生成爆火,然后自己顺着AI的名义给自己买了一张 4090 显卡,图片玩过几次除了生成一堆涩图以外想着是不是可以用来干点正事,于是就想着能不能学点AI 相关的知识,于是就有了这个系列的文章。
这篇文章应该是我写的最满意的文章之一了,首先里面有真实的学习步骤是怎样的,里面讲了我是怎么学习的,以及弄了一些 demo 来跑我们的模型,总而言之入门很合适了。
后面我还用强化学习,根据图片来学习怎么玩 FlappyBird 这个也很有意思。
合集里面剩下的几篇AI文章就是讲的我怎么在生活中使用 AI 的也希望能给大家一点启发。
合集:https://www.luozhiyun.com/archives/category/%E5%90%8E%E7%AB%AF/go
go相关的技术文章是写了真的不少,因为当时自己对这一块比较感兴趣,工作中也常用,所以写了很多源码研究之类的文章,很多文章即使现在来看都写的不错,诸如:
这篇文章有时候没事我会去看看的,因为细节太多,很多时候一不留神写的代码容易出错,里面的错误范例对于工作中还是很实用。
调试源码这个事情,一般是不会去做,但是如果对源码感兴趣是可以参考一下怎么去调试。
下面几篇我也觉得写的不错,就是比较底层,读起来最好自己跟着看看代码,否则会比较费劲。
合集:https://www.luozhiyun.com/archives/tag/cpp
C++ 相关的文章也是写了不少,写这些文章的时候基本上也是边写边学,写文章的同时顺带把我的疑惑点也给解答了。
合集:https://www.luozhiyun.com/archives/tag/%e8%ae%a1%e7%ae%97%e6%9c%ba%e5%9f%ba%e7%a1%80
这方面也是我自己慢慢想着要整理一下自己现有的知识体系,然后写的一些文章。
这篇文章可能在生产中还是有点用,算是科普一下SSD原理,以及如何更高效的使用。
这一篇文章也是我对知识体系的一个梳理,但是很多知识点也只是起了一个抛砖引玉的效果,更多的知识感兴趣的同学可以去看看 《深入理解计算机系统》 、《深入浅出计算机组成原理》 等书。
合集:https://www.luozhiyun.com/archives/category/%e7%bd%91%e7%bb%9c
这里面的文章还是有点意思的,写这些文章也是因为我当时在研究 k8s 相关的技术的时候,对书里面的网络感觉很多地方都不懂,然后就自己专门又找了点资料自己研究了一下。
这里就是再列举一些不知道分成什么类的文章,但是我觉得写的也不错。
这篇文章对于 RocksDB 是怎么实现的做了很详尽的分析,我觉得写的还是不错的,里面的图也画的很精美(我其他文章图也画的很精美)。
这篇文章里面讲了一下 protobuf编码原理 & 最佳实践,我觉得在工作中还是很实用的,里面的图也画的很精美。
其实我是很喜欢玩游戏的,但是不能在公司流畅的玩我想玩的游戏这点让我很难受,所以我决定自己来搞。
当时沉迷于 DNFM 无法自拔,但是我又不想每天花2个小时搬砖,所以就想有什么办法可以让电脑来自动化这件事情。
虽然我的鹅厂生涯结束了,但是我对这个世界的探索不会结束,感兴趣的可以继续关注我的公众号 & 微信 & 博客(文章最后放出)。
最近一两年我开始自己做饭,然后中午带饭去公司吃。我一开始觉得做饭是一种浪费时间的行为,但是后面觉得自己做饭一方面可以更健康的生活,另一方面做饭的时候我喜欢自己一边做饭一边听点播客,我基本什么都听,其实也算是了解其他行业的渠道。
其实就目前来看,我们的饮食里面其实配比是很不正常的,所以我会在做饭的时候会有这几点要求:
其实我们生活中很大的疲倦感除了是真正的劳累造成的以外,血糖的波动也会带来疲倦感。血糖不稳定,就像让我们的身体坐上一辆失控的“过山车”,时而冲上顶峰(高血糖),时而跌入谷底(低血糖):
吃完饭血糖飙升 → 感到困倦、没精神(高血糖疲劳) → 胰岛素过量分泌 → 血糖骤降 → 感到虚弱、无力、发慌(低血糖疲劳) → 赶紧吃东西(尤其是高糖食物) → 再次飙升……
由于我们传统的饮食结构的影响,我们食物中大多是米饭,以及面条为主食,他们都含有大量的淀粉,淀粉会消化分解为葡萄糖,这些葡萄糖分子被小肠吸收到血液中,“血糖”指的就是血液中的葡萄糖浓度。因此,大量葡萄糖涌入血液,血糖水平就会迅速飙升。
当然,我要控糖也和我的尿酸偏高有关系。果糖(Fructose),对尿酸的影响非常显著,甚至可以说是独立于“高嘌呤饮食”之外的另一个主要诱因。糖对尿酸的影响是:
我们日常吃的白砂糖(蔗糖)在体内会分解成一半葡萄糖和一半果糖。而奶茶、可乐等甜饮料中添加的“高果糖玉米糖浆”,其果糖含量更高。
所以,控糖基本上就包含了两种:
一种是少吃各种碳水,米饭,面条等等,之类的含有高淀粉的食物,用糙米、黑米、藜麦、燕麦米等全谷物来部分或全部替代白米饭;
另一方面是含有甜味的食物,如奶茶,可乐等含有果糖的食物尽量不要去吃,顺带一提,很多水果也是高果糖的,也不能多吃;
然后再来提一下蔬菜,其实我们平时的蔬菜摄入实在是太少了,根据《中国居民膳食指南(2022)》,成年人每日应摄入300至500克的蔬菜,但是很多时候蔬菜摄入实在太少了,比如一份点外卖,里面可能就几根蔬菜。所以我现在即使是早上都是粗粮比较多,比如会早上吃玉米,一份蔬菜,三个鸡蛋,然后才是两个小笼包。
蔬菜的好处有很多啦,我就不重复了,我说一点我比较关注的,就是可以帮助我们控制和稳定血糖的强大“盟友”。
蔬菜中的膳食纤维,特别是可溶性膳食纤维,扮演着“血糖缓冲器”的角色。可溶性膳’便纤维在遇水后会形成一种凝胶状物质,包裹住食物。这会显著减慢胃排空的速度和食物在肠道中的消化过程,从而使碳水化合物(糖)的吸收变得更加平缓,避免餐后血糖像坐过山车一样急剧飙升。并且纤维能够提供强烈的饱腹感,有助于控制食量。
其实我到现在为止健身超过 8 年了,从我大学时候就开始了,至今为止也练的不错,以前锻炼是想要练成super man 这样的肌肉形状,后面自己尝试过之后发现没有上科技根本不可能,现在也就保持这样的体型已经很久了,也就是其实和我的职业生涯一样,进入了“平台期”很久了。

而且随着年纪的慢慢增大,继续激烈的力量训练是很容易受伤的,近3年我就分别受伤了3次,腰部、肩膀、手腕都受伤过。手腕是最近一次去年 11月受伤,直到现在都没怎么恢复好,所以各位运动的时候一定要注意安全。
我从今年年初开始跑步,跑步真的给我很大的帮助,每次都竭尽全力完成每次计划的跑量真的很爽。然后看着自己的跑速,心肺,各项指标都在稳定的提高真的很开心,要知道人到中年能稳定提升的能力不多了。

跑步和健身很不一样,一方便跑步的时候没有组间休息,另一方面就是一旦开始了就不能停。现在我一旦心情不舒畅就会大跑一场,跑完之后你会发现自己无比淡定,再糟心的事情也不过如此,没有什么是过不去的。
一般的情况下我会选一个播客来听,这样起码让我的跑步过程不会很无聊,我一般喜欢听下面几个播客:
知行小酒馆:听的最多的应该是这个,讲如何生活,讲投资。但是有些嘉宾请的也不是很好,比如 Anker 那期,翻开评论你可以发现全是骂的;
半拿铁:里面会讲很多商业漫谈,以及商业史,但是我最喜欢的还是最喜欢他们的西游篇,把西游记从头到尾讲了一遍;
面基:这个博主主要讲投资相关的东西,有时候看到感兴趣的主题会看看;
起朱楼宴宾客:也是主要讲经济,商业,投资相关的,还不错;
The Wanderers 流浪者:这个是我最近喜欢听的,他们会对最近的实事进行总结,讲讲他们的投资理念,主理人是三个专业的投资人,讲解的话题覆盖了A股,港股,美股,加密货币;

我其实前几年开始就在想存钱,投资,然后可以产生复利,这样就可以通过投资反哺我的生活,让我有一定的被动收入,所以我也一直没买房买车,奢侈类的消费基本不参与,看准自己的目标,并为之而努力。
我的收入其实现在是分为几个部分:
1和2大概占了我总资产的三分之一,3占了三分之二。我至今为止,其实盈利还行,主要还是选股选择的都是一些可以拿的住的一些股票,大多持股超过了1年,即使在波动比较大或者出现黑天鹅的时候,我也没有选择卖出,反而进行了加仓,这也是我投资的理念,除非公司大的方向发生了转变,否则我应该还会继续持有。
我也不是交易大师,但是我觉得这些交易策略是可以通用的:
就在昨天跑步的时候,我都在想什么是财富自由?财富自由是表示自己不工作也不会饿死吗?表示自己可以一直躺平吗?我觉得不是,我觉得应该是无需为了生计而出卖自己的时间,可以自由选择自己想要的生活方式。也就是说我可以选择我的时间应该花在哪里,我可以将时间和精力投入到自己真正热爱的事业、兴趣爱好、家庭或社会贡献中,而不用被动地接受工作的束缚。
如果接受了这条假设,那么就表示代表人生不是只有一条路,而是充满了各种各样的选择和不确定性,就如同旷野一样。我可以去探索、去尝试,去定义自己的成功和幸福,而不是遵循他人的标准。当然我也需要自己去开辟道路,面对困难和迷茫。这既是挑战,也是成长的过程。
最近我用 Claude Code 越多,我越发现它在平时的 coding 中能发挥的影响力就越大,我用它不限于:
基于这几年对 ai 工具的持续使用,感觉现在真的已经慢慢的渗透到我们的日常工作中了,虽然现在很多时候仍需要手动接管,但是已经可以节省很多时间了,可以遇见在不远的未来,coding 的工作被取代也是情理之中的事情。
所以在这之前我一直在想,我能做什么?
对于我来说,我一直在审视自己的能力边界,以及后面可以持续发力的点,我不是个可以随时躺平的人,也不喜欢躺平,能找到一份热爱的事情,并持之以恒的做下去可以说是我人生的一直以来的追求。
那么对于我来说,可以一直做下去的事情莫过于有以下几种:
所以对于我来说,我现在年纪不是特别大,对生活也有激情,钱其实也不是那么缺,在我了解到我既不可能在这里一直呆到退休,也不可能直接呆到财富自由,那么我一直在对自己发出灵魂拷问:
为什么不把剩余的时间投资到更有意义的地方呢?难道一定要等自己老了,尝啥都没味道了,逛任何地方都没有意思了,眼睛里面没有光了,才能开始做自己的事情吗?

