文章详细对比了 Solana 的特性激活机制与 Ethereum 的硬分叉过程,强调 Solana 如何通过无需硬分叉实现协议的持续更新。它深入解释了 Solana 特性激活的步骤、特性账户的状态,并提供了 Rust 代码示例,展示了特性如何在运行时被激活和实施,最后介绍了 SIMD 检查工具。

上周,为了完成一个项目,我不得不深入研究 Solana Feature Gates(功能门) 机制。在此我想总结一下我的学习心得,希望能对大家有所帮助或启发。
Solana 的功能门机制允许验证者网络在不导致网络硬分叉的情况下,更新 Solana 运行时的行为。相比之下,以太坊对验证者客户端的更改需要进行大量的协调工作。这意味着以太坊的升级固定成本更高,从而激励开发者在一次硬分叉中包含更多改动,但这往往会导致发布过程出现更多延迟。
对比 Solana 的功能激活步骤与以太坊的硬分叉流程非常有趣;它展示了 IBRL(It'll Be Right, Lad)不仅仅是一种“Solana 文化”,它早已深深根植于协议本身。
如果说 Solana 类似于持续集成(CI),那么以太坊则更像是大型的版本化发布。
从高层次来看,以太坊硬分叉需要:
这个过程需要所有客户端实现之间的紧密协调,并要求足够数量的验证者及时升级。这会导致网络发生巨大的、不可逆转的改变,除非再次进行硬分叉。
如果把以太坊比作一名开发者,他就是那种直到追求“完美”才提交 PR 的人,一次性丢给你 5 万行代码进行审查 🤬😅。
与此不同,在 Solana 中,每个 SIMD 都有独立的功能激活机制。只要有足够多的客户端采用了新版本,功能就可以半自动上线。
通常,功能激活的步骤如下:
这种架构意味着功能可以小批量实现和发布,为网络带来持续的增量变化。每次网络升级都是独立的,而不是作为一整套庞大变更的一部分。
Solana 功能激活的另一个优点是,随着 SIMD-0089 的引入,待激活的功能是可以被撤销的。
了解了这些激活机制的高层原理后,我们来看看具体代码。
功能激活账户是由 Feature111111111111111111111111111111111111 账户拥有的简单账户,其结构定义如下:
pub struct Feature {
pub activated_at: Option<u64>,
}
代码 在此。如果一个功能在 myFEATURE11111111111111111111111 处激活,它可能处于以下三种状态:
当账户不存在,或不属于 Feature111111111111111111111111111111111111 账户时,它被视为“非活跃”状态。在任何 Epoch 变更时,验证者都会忽略此账户,且该功能的任何运行时代码路径都不会生效。
一旦功能账户被 Feature111111111111111111111111111111111111 拥有,如果账户数据为空或第一位为 0(即 activated_at: None),它就处于“待定”状态。一旦跨越 Epoch 边界,所有验证者都会识别到这个待定功能,并将其 activated_at 字段设置为当前 Slot。
一旦功能账户由 Feature111111111111111111111111111111111111 拥有且 activated_at 字段已填充,该功能即进入活跃状态,新行为随之生效。
这三种状态引出了一些问题:验证者如何知道要在 Epoch 边界检查新功能 myFEATURE11111111111111111111111?新行为在客户端层面又是如何具体实现的?
当功能作者实现一个功能时,首先会生成该功能的公钥(例如 myFEATURE11111111111111111111111)。然后,他们会将其添加到 Agave 客户端的 feature-set crate 中。他们需要提供新功能的公钥,将其添加到 FEATURE_NAMES 列表,并包含在尚未全集群激活的 FeatureSnapshot 中:
pub mod my_new_feature_does_great_things_i_promise {
solana_pubkey::declare_id!("myFEATURE11111111111111111111111");
}
...
pub static FEATURE_NAMES: LazyLock<AHashMap<Pubkey, &'static str>> = LazyLock::new(|| {
[
...
(
my_new_feature_does_great_things_i_promise::id(),
"SIMD_XXXX: GREAT THINGS"
)
]
.iter()
.cloned()
.collect()
});
...
pub struct FeatureSnapshot {
...
pub my_new_feature_does_great_things_i_promise_enabled: bool
}
在内部,验证者维护着一个 FeatureSet 结构体:
pub struct FeatureSet {
active: AHashMap<Pubkey, u64>,
inactive: AHashSet<Pubkey>,
snapshot: FeatureSnapshot,
}
凭借这些信息,验证者客户端就能识别所有的功能账户。启动时,它会将所有功能视为非活跃,随后获取 FEATURE_NAMES 中每个账户的状态,以确定其是否活跃。
在处理新的 Epoch 时,可以再次迭代这些账户以查找待激活的功能:
fn compute_active_feature_set(&self, include_pending: bool) -> (FeatureSet, AHashSet<Pubkey>) {
let mut active = self.feature_set.active().clone();
let mut inactive = AHashSet::new();
let mut pending = AHashSet::new();
let slot = self.slot();
for feature_id in self.feature_set.inactive() {
let mut activated = None;
if let Some(account) = self.get_account_with_fixed_root(feature_id) {
if let Some(feature) = feature::state::from_account(&account) {
match feature.activated_at {
None if include_pending => {
// 功能激活待定
pending.insert(*feature_id);
activated = Some(slot);
}
Some(activation_slot) if slot >= activation_slot => {
// 功能已激活
activated = Some(activation_slot);
}
_ => {}
}
}
}
if let Some(slot) = activated {
active.insert(*feature_id, slot);
} else {
inactive.insert(*feature_id);
}
}
(FeatureSet::new(active, inactive), pending)
}
代码 在此。每个非活跃的功能账户都会被再次获取,以检查是否有新功能需要激活。这使得验证者能够立即将该功能视为活跃,并提供一个“新功能激活”列表,以便运行任何必要的一次性代码路径来确保功能正常运行。
接下来,我们通过几个例子来看看具体的功能实现。
@deanmlittle 最近在主网激活了 SIMD-0194。这个 SIMD 的实现非常简洁。它展示了新激活功能如何在 Epoch 边界执行更改:
if new_feature_activations.contains(&feature_set::deprecate_rent_exemption_threshold::id())
{
self.rent_collector.rent.lamports_per_byte_year =
(self.rent_collector.rent.lamports_per_byte_year as f64
* self.rent_collector.rent.exemption_threshold) as u64;
self.rent_collector.rent.exemption_threshold = 1.0;
self.update_rent();
}
如果该功能刚被激活,则执行内部逻辑设置相关数值。此 if 语句内部的代码仅会执行一次,随后功能便正式生效。
@0x_febo 提出的 SIMD-0266 引入了 p-token,显著提升了 SPL Token 指令的计算单元(CU)效率。在了解了这项改动的巨大规模和影响后,看到实际激活功能的代码如此简单,几乎让人感到不可思议:
if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) {
if let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_program(
&feature_set::replace_spl_token_with_p_token::SPL_TOKEN_PROGRAM_ID,
&feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER,
self.feature_set
.snapshot()
.relax_programdata_account_check_migration,
"replace_spl_token_with_p_token",
) {
warn!(
"未能将 SPL Token 替换为 p-Token 缓冲区 '{}': {e}",
feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER,
);
}
}
代码 在此。它的逻辑非常直接:“如果要激活此功能,就将 SPL Token 程序升级并指向一个新程序”。这个新的 Token 程序可以完全独立地编写、测试和部署(部署到 PTOKEN_PROGRAM_BUFFER 账户)。待功能准备就绪,只需简单替换,全网即可享受到 CU 消耗降低带来的好处。太棒了!
我之所以开始深入研究 Solana 的功能激活机制,是因为我正在开发一个简单的工具,用于验证每个 SIMD 激活是否符合预期:simd-checker。受 @deanmlittle 的 simd-0194-checker 启发,我希望通过该项目编译一套简单的链上程序,以展示每个 SIMD 的运行时行为。
虽然目前只实现了少数几个 SIMD 检查,但每个测试主要包含以下部分:
在 manifest 中记录条目,展示 SIMD 在各网络上的激活状态、测试程序的部署情况以及它与其他 SIMD 的关联性。
一个链上程序,用于检测目标功能是否处于预期的激活状态,并允许 验证 功能的运行时数值。
一个 RPC 层组件,用于执行一些“非运行时”检查。
当使用 surfpool 在本地测试功能时,每个功能会测试两次:一次在功能非活跃(但先前功能均活跃)的 surfnet 环境中启动,以验证未激活时的行为;另一次在功能活跃时启动,验证其启用后的状态。
正如我所说,目前仅实现了少数 SIMD。如果你对特定 SIMD 的运作方式感兴趣,欢迎提交 PR 为该工具添加测试!说不定,你能在功能正式激活前发现潜在的运行时错误。
- 原文链接: x.com/micaiah_txtx/statu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!