深入解析 Solana Feature Gates:如何实现无需硬分叉的特性激活

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

Image

上周,为了完成一个项目,我不得不深入研究 Solana Feature Gates(功能门) 机制。在此我想总结一下我的学习心得,希望能对大家有所帮助或启发。

Solana 的功能门机制允许验证者网络在不导致网络硬分叉的情况下,更新 Solana 运行时的行为。相比之下,以太坊对验证者客户端的更改需要进行大量的协调工作。这意味着以太坊的升级固定成本更高,从而激励开发者在一次硬分叉中包含更多改动,但这往往会导致发布过程出现更多延迟。

对比 Solana 的功能激活步骤与以太坊的硬分叉流程非常有趣;它展示了 IBRL(It'll Be Right, Lad)不仅仅是一种“Solana 文化”,它早已深深根植于协议本身。

如果说 Solana 类似于持续集成(CI),那么以太坊则更像是大型的版本化发布。

Ethereum 硬分叉

从高层次来看,以太坊硬分叉需要:

  • 就一套获批的 EIPs 达成一致,并将其纳入硬分叉中。
  • 所有以太坊客户端完成这些新 EIP 的实现*。
  • 选定一个激活日期,并将该时间戳硬编码到协议激活逻辑中。
  • 所有验证者在分叉期限到期前更新其客户端。
  • 当链达到激活时间戳时,行为发生改变。任何未更新的客户端都会创建无效区块(从而导致网络硬分叉)。

这个过程需要所有客户端实现之间的紧密协调,并要求足够数量的验证者及时升级。这会导致网络发生巨大的、不可逆转的改变,除非再次进行硬分叉。

如果把以太坊比作一名开发者,他就是那种直到追求“完美”才提交 PR 的人,一次性丢给你 5 万行代码进行审查 🤬😅。

  • 我要指出的是,以太坊有更多的客户端实现需要协调,这可以说对去中心化更有利——减少了软件供应链中的单点故障,但同时也增加了协调成本。

Solana 功能激活

与此不同,在 Solana 中,每个 SIMD 都有独立的功能激活机制。只要有足够多的客户端采用了新版本,功能就可以半自动上线。

通常,功能激活的步骤如下:

  • 编写并讨论 SIMD,并获得批准。
  • 为该 SIMD 激活生成一个公钥,并将其添加到 Solana 客户端的功能列表中,初始状态设为“非活跃(Inactive)”。
  • 将该功能的激活逻辑实现添加到客户端中。
  • 验证者更新其客户端以支持该新功能。
  • 一旦有足够数量的验证者(95% 的权益)完成更新,核心开发者会通过 CLI 命令初始化功能账户,将该功能标记为“待定(Pending)”。
  • 在下一个 Epoch 切换时,验证者会检查待激活的功能,并在内部将其标记为“活跃(Active)”。

这种架构意味着功能可以小批量实现和发布,为网络带来持续的增量变化。每次网络升级都是独立的,而不是作为一整套庞大变更的一部分。

Solana 功能激活的另一个优点是,随着 SIMD-0089 的引入,待激活的功能是可以被撤销的。

了解了这些激活机制的高层原理后,我们来看看具体代码。

功能账户

功能激活账户是由 Feature111111111111111111111111111111111111 账户拥有的简单账户,其结构定义如下:

pub struct Feature {
    pub activated_at: Option<u64>,
}

代码 在此。如果一个功能在 myFEATURE11111111111111111111111 处激活,它可能处于以下三种状态:

非活跃 (Inactive)

当账户不存在,或不属于 Feature111111111111111111111111111111111111 账户时,它被视为“非活跃”状态。在任何 Epoch 变更时,验证者都会忽略此账户,且该功能的任何运行时代码路径都不会生效。

待定 (Pending)

一旦功能账户被 Feature111111111111111111111111111111111111 拥有,如果账户数据为空或第一位为 0(即 activated_at: None),它就处于“待定”状态。一旦跨越 Epoch 边界,所有验证者都会识别到这个待定功能,并将其 activated_at 字段设置为当前 Slot。

活跃 (Active)

一旦功能账户由 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)
}

代码 在此。每个非活跃的功能账户都会被再次获取,以检查是否有新功能需要激活。这使得验证者能够立即将该功能视为活跃,并提供一个“新功能激活”列表,以便运行任何必要的一次性代码路径来确保功能正常运行。

接下来,我们通过几个例子来看看具体的功能实现。

SIMD-0194

@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 语句内部的代码仅会执行一次,随后功能便正式生效。

SIMD-0266

@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 消耗降低带来的好处。太棒了!

SIMD 检查器

我之所以开始深入研究 Solana 的功能激活机制,是因为我正在开发一个简单的工具,用于验证每个 SIMD 激活是否符合预期:simd-checker。受 @deanmlittlesimd-0194-checker 启发,我希望通过该项目编译一套简单的链上程序,以展示每个 SIMD 的运行时行为。

虽然目前只实现了少数几个 SIMD 检查,但每个测试主要包含以下部分:

Manifest

manifest 中记录条目,展示 SIMD 在各网络上的激活状态、测试程序的部署情况以及它与其他 SIMD 的关联性。

On-chain Program

一个链上程序,用于检测目标功能是否处于预期的激活状态,并允许 验证 功能的运行时数值。

RPC-layer Component

一个 RPC 层组件,用于执行一些“非运行时”检查。

当使用 surfpool 在本地测试功能时,每个功能会测试两次:一次在功能非活跃(但先前功能均活跃)的 surfnet 环境中启动,以验证未激活时的行为;另一次在功能活跃时启动,验证其启用后的状态。

正如我所说,目前仅实现了少数 SIMD。如果你对特定 SIMD 的运作方式感兴趣,欢迎提交 PR 为该工具添加测试!说不定,你能在功能正式激活前发现潜在的运行时错误。

  • 原文链接: x.com/micaiah_txtx/statu...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
micaiah_txtx
micaiah_txtx
江湖只有他的大名,没有他的介绍。