Jito-Solana的工作原理 - 深入探讨

  • thogiti
  • 发布于 2025-01-01 14:13
  • 阅读 48

本文深入分析了Jito在Solana平台上的架构及其运作方式,重点解释了Jito Relayer、Block Engine以及交易打包(Bundle)的处理流程。通过介绍MEV(最大化可提取价值)的捕获机制,Jito在提高交易效率与利润分配方面的作用得到了详细阐述,为开发者和验证者提供了清晰的参考资源。

介绍

Solana REV; Jito Tips以绿色标识。来源:Blockworks。

图:Solana REV; Jito Tips以绿色标识。来源:Blockworks。

过去30天趋势,Solana REV

图:过去30天趋势,Solana REV; Jito Tips以绿色标识。来源:Jito.network。

在过去的一年里,Solana的实际经济价值(REV)——交易费用加上协议外的MEV小费——飙升至14亿美元的历史最高值。大部分增长来自Jito小费,单独占据了Solana 2024年12月REV的约一半。尽管逐月数据可能会波动,但从更大的角度来看,与去年的水平相比增长了近50倍。在过去30天中,Jito小费几乎占据了总费用和小费的三分之二。这一爆炸性趋势突显了Jito在推动Solana的费用收入和MEV活动中的关键作用。

鉴于Jito作为“领先的MEV基础设施团队”的地位,理解其基础架构至关重要——特别是随着网络的经济激励越来越多地围绕MEV捕获。然而,将Jito的技术(如Jito区块引擎、Jito-Solana客户端等)拼凑在一起理解其实际工作原理可能会很困难。它们代表了Solana“模块化堆栈”中一些最重要的基础设施,而Jito的MEV优化验证器客户端已经保护了超过90%的Solana活跃股份

在本文中,我们将提供关于Jito内在工作机制的完整说明。我们将重点介绍MEV小费是如何处理的,这些优化对验证器和用户的意义,以及Jito生态系统如何适应Solana的更大战略路线图。我们希望提供一个完整且详细的参考资料,以帮助开发者、验证器和质押者充分理解在高交易量环境中运行Jito的优势和挑战。

什么是Jito?

Jito是一个经过修改的Solana客户端,能够实现更高效、更有利可图且更公平的MEV价值捕获。它在标准的Solana验证器基础上构建,增加了额外的链下组件(中继器和区块引擎)以及两个核心链上程序(小费支付程序和小费分配程序)。

Jito的最终目标是实现MEV价值捕获——在Solana区块内的可货币化机会——同时将这些收益重新分配给验证器和委托人,且所有操作都尽量减少信任假设。最终,Tip Rewards NCN(节点共识网络)将进一步分散奖励分配,消除当前存在的单点故障。

Jito Solana客户端架构

以下是Jito系统架构的高层次图示。它展示了用户交易如何流入网络,Jito的链下组件(中继器、区块引擎)如何处理它们,以及链上程序如何最终完成MEV小费的分配。

Jito-Solana-Client-Architecture 来源:Jito-Solana, https://docs.Jito.wtf/

Jito-client-architecture

来源:https://www.bee.com/15574.html

组件

  • 验证器(Jito-Solana Client):一个经过修改的Solana验证器,增加了额外的“阶段”来处理MEV捆绑。Jito验证器运行额外组件,包括RelayerStage、BlockEngineStage和BundleStage。
  • Relayer:它持有交易200毫秒,并将其转发给验证器。这有助于创建包形成机会。
  • Block Engine:处理交易摄取、MEV捆绑模拟和捆绑选择。
  • 小费支付程序(链上):作为每个验证器的MEV小费的链上储蓄罐。
  • 小费分配程序(链上):将累积的小费分配给验证器和质押者。

典型流:

  • 中继器将用户交易延迟200毫秒,以形成捆绑机会。其中一些交易可能会引起搜索者的兴趣,用作额外的MEV价值捕获捆绑。
  • 区块引擎模拟带有搜索者捆绑的交易。它选择最佳捆绑,并将其流送至验证器的BlockEngineStage
  • 验证器将它们放入“捆绑流水线”(BundleStage)。
  • 与此同时,来自中继器的常规交易也会进来。它们要么跳过签名验证,要么通过签名验证,然后转发给Jito-Solana验证器的BankingStage
  • 区块被划分为两个部分:
    • 在PoHtick的前80%期间,计算限制部分减少,但如果没有捆绑到达,正常交易仍可以使用它。在插槽的80%之后,成本限制恢复为最大。
  • 在领导插槽中(Jito验证器),验证器协调最终块:
    • 最高价/最有利可图的交易可能来自区块引擎。
    • 捆绑以原子方式执行。
  • 区块正常广播。

在深入之前,让我们首先了解Jito捆绑的工作原理。

Jito捆绑如何工作

捆绑是一组必须以原子性和顺序执行的交易(最多五个交易)。这意味着如果一个交易失败,整个捆绑将被丢弃。Jito通过专门的BundleStage扩展了Solana的验证器流水线,该阶段处理这些捆绑。此捆绑的主要目标是捕获套利机会、清算或其他需要及时执行的盈利策略。

关键约束

  • 一切或不成(原子执行)。
  • 严格的执行顺序。
  • 捆绑不能跨越多个插槽。
  • 捆绑不能修改共识关键帐户。
  • 必须通过成本/QoS检查;如果任何单个交易成本过高,则整个捆绑将被拒绝。

捆绑流程和执行

sequenceDiagram
    participant User
    participant Relayer
    participant Searcher
    participant BlockEngine
    participant JitoValidator
    participant TipPaymentProgram

    User->>Relayer: 提交含有MEV小费的交易通过拍卖
    Relayer->>JitoValidator: 持有交易200毫秒然后发送
    Searcher->>BlockEngine: 创建并提交捆绑
    BlockEngine->>BlockEngine: 模拟并选择最佳捆绑
    JitoValidator->>BlockEngine: 发送交易(gRPC)
    BlockEngine->>JitoValidator: 发送选择的捆绑(gRPC)
    JitoValidator->>JitoValidator: 确保tip_receiver = TDA 为本Epoch
    JitoValidator->>BundleStage: 原子执行捆绑
    BundleStage->>BankingStage: 如果成功则提交
    BankingStage->>TipPaymentProgram: 执行小费支付交易,存入lamports于小费PDA
    JitoValidator->>JitoValidator: 最终确定区块
  • 区块引擎接收来自用户提交的正常交易加上“MEV”小费交易。
  • 区块引擎模拟捆绑,为它们按潜在利润排名。
    • 捆绑执行模拟的输出包括:
    • 交易是否成功执行
    • 任何执行错误
    • 执行时间指标
    • 交易结果
  • 区块引擎将一份有利可图的捆绑清单发送给验证器。
  • 验证器在其BundleStage中执行这些捆绑,将小费收集到小费支付程序PDA中。

Jito捆绑拍卖

Jito捆绑拍卖简化了Solana上的区块空间分配,使得这一过程更加高效和有利可图。

通过频繁的拍卖及利用并行锁定模式,确保高价值交易被优先处理。这样的设置不仅增加了验证器获得的小费,还帮助搜索者和交易者更快地执行他们的策略。在Jito生态系统中,捆绑拍卖在提升Solana网络的整体性能和盈利能力方面发挥着至关重要的作用。

Jito捆绑拍卖决定哪些捆绑将在当前区块中执行。通过频繁的拍卖和利用并行锁定模式,确保高价值交易被优先处理。这样的设置不仅增加了验证器获得的小费,也帮助搜索者和交易者更快地执行策略。

  • 搜索者/交易者的捆绑提交
    • 搜索者和交易者提交交易的捆绑。每个捆绑包含搜索者希望被包含在区块中的一组交易(通常每个捆绑4-5个交易,并且小费指令是最后一笔交易)。
  • 优先级拍卖机制
    • 由于区块空间和机会是稀缺资源,因此需要拍卖机制以优先考虑哪些捆绑被包含在块中。
    • 拍卖的目的是最大化在块中可收集的总小费(交易费用)。这确保验证器有动力将最有利可图的交易包含在内。
  • 锁定模式中的并行性
    • 捆绑内的交易可能对账户具有不同的锁定模式。这些模式可以是:
    • 写-写(w,w):两个交易都写入同一账户。
    • 读-写(r,w):一个交易从一个账户读取,而另一个向其写入。
    • 写-读(w,r):一个交易写入一个账户,而另一个从中读取。
    • 读-读(r,r):两个交易均仅从同一账户读取。
    • 具有相互锁定模式(w,w),(r,w)或(w,r)特定账户的捆绑在一次拍卖中运行。否则的捆绑,若不触及相同账户或具备非相互锁定模式(r,r),则在不同拍卖中运行。这样可以实现并行性和高效的区块使用。
  • 在200毫秒tick时拍卖执行
    • 并行拍卖以200毫秒的间隔(tick)运行。这确保拍卖过程足够快速,以跟上Solana区块链的高吞吐量和区块时间。
    • 频繁的拍卖tick允许实时处理和优先考虑捆绑,确保最有利可图的交易被包含在下一个区块中。
  • 捆绑排序和优先级
    • 在单次拍卖中,捆绑按以下标准排序和优先:
    • 请求的小费:搜索者愿意支付的小费(交易费用)金额。
    • CU请求的效率:捆绑的计算单位(CU)效率,即与提供的小费相比所需的计算量。
    • 拍卖算法选取可以适应区块计算单元(CU)限制的最高支付捆绑组合。计算限制在PoHtick的前80%期间部分减少,但如果没有捆绑到达,常规交易仍可以使用它。在插槽的80%之后,成本限制恢复为最大。这确保区块被填充最有利可图的交易,而不超出计算能力。
  • 提交给验证器
    • Jito将最高支付捆绑的组合提交给验证器,直到达到区块的CU限制。
    • 验证器将这些捆绑包含在下一个区块中,确保最有利可图的交易被及时执行。

Jito中继器

中继器是Jito Solana架构中的重要组成部分,作为进入系统的交易网关。它是一个独立的服务,Jito验证器通过gRPC与之连接,有效地充当交易“网关”或“代理”。中继器延迟交易200毫秒,确保形成捆绑的机会。它与Jito-Solana验证器的集成使得MEV价值捕获和区块生成无缝对接。

关键功能:

  • 在TPU层拦截交易,防止其到达标准的Solana验证器。
  • 平衡进入交易的负载,防止区块引擎在高负载情况下出现瓶颈。确保系统能够处理高吞吐量的工作负载。

关键代码库:

fn start_consuming_relayer_packets(
    mut client: RelayerClient<...>,
    packet_tx: &Sender<PacketBatch>,
    ...
) -> Result<(), ProxyError> {
    let mut packet_stream = client.subscribe_packets(...).await?.into_inner();
    while let Some(message) = packet_stream.message().await? {
        match message.msg {
            Some(relayer::subscribe_packets_response::Msg::Batch(proto_batch)) => {
                let packet_batch = PacketBatch::new(
                    proto_batch.packets.iter().map(proto_packet_to_packet).collect()
                );
                // 可能信任或不信任
                if trust_packets {
                    banking_packet_sender.send(...)?
                } else {
                    packet_tx.send(packet_batch)?;
                }
            }
            Some(relayer::subscribe_packets_response::Msg::Heartbeat(_)) => {
                // 触发心跳频道
            }
            None => { ... }
        }
    }
    Ok(())
}

典型流:

  • 一旦验证器连接到中继器,验证器将更新其广告的gossip协议,使“TPU”(交易处理单元)的地址与中继器的IP/端口匹配。换句话说,任何通过UDP发送到验证器的常规客户端交易可能会发现中继器的端点。
  • 客户或搜索者将常规Solana交易提交给中继器。中继器会进行一些额外的交易处理,例如去重、过滤等。
  • 中继器持有交易短暂时间(约200毫秒),为Jito系统或区块引擎提供足够的时间来进行拍售搜索者提交的捆绑
  • 最终,中继器通过gRPC将它们发送给验证器。验证器要么将其通过正常的sigverify→BankingStage渠道传递,或者,如果验证器认为这些是“受信任的”,可能会跳过重新验证直接将其送到banking_packet_sender
  • 每500ms(大约),中继器发送一个“心跳”以表明它仍然活着。验证器期望这样。如果它在大约1.5秒内没有看到它,它可以恢复到直接的QUIC使用,以避免验证器在中继器消失时饥饿。

与其他组件的集成

中继器与Jito Solana架构中的其他多个组件集成:

  • TPU层:
    • 在交易到达标准的Solana验证器之前拦截交易。
  • Jito-Solana验证器:
    • 确保选择的捆绑以原子方式执行并包含在区块链中。

挑战和优化

中继器面临着几个挑战,包括:

  • 高吞吐量
    • 中继器从多个来源(搜索者、零售钱包、聚合服务)接收大量交易。它需要快速验证、批处理并将其转发到正确的领导者或区块引擎,而不会形成瓶颈。
  • 延迟
    • Solana的slot大约为400毫秒,因此中继器必须快速处理入口交易——如果它转发交易的时间过长,搜索者可能会错过插槽窗口或无法意识到他们的MEV机会。

为了应对这些挑战,中继器实施了几项优化:

  • 批处理
    • 这里的批处理意味着快速连续到达的交易被分组在一起。这种方法减少了开销(减少gRPC或网络调用)并允许向量化操作(如签名检查或模拟请求)。
    • 当区块引擎收到更大的批次时,能够更轻松地进行流水线或并行处理签名检查和模拟。
  • 异步I/O
    • 中继器通常使用Rust的Tokio异步框架中的非阻塞事件循环。这确保中继器在等待I/O时(例如gRPC发送或接收、网络写入),仍可以并行处理其他任务。
    • 通过异步或事件驱动的风格,短时间延迟不会阻止整个中继器流水线。

Jito区块引擎

区块引擎是Jito Solana MEV价值捕获系统的核心。区块引擎是另一个gRPC服务,专门用于捆绑和交易流程。它通常与中继器并行使用。它位于中继器和Jito-Solana验证器之间,充当决策层,处理、模拟并选择MEV捆绑。下面我们将探讨其功能的详细分解。

区块引擎工作流程

flowchart LR
    A[中继器] --> B[交易队列]
    B --> C[模拟引擎]
    C --> D[捆绑选择]
    D --> E[转发模块]
    E --> F[验证器]

关键责任:

  • 接受来自搜索者或聚合器的正常交易和特别是捆绑
  • 可能重新模拟或重新排序以实现最高的盈利能力或MEV价值捕获。
  • 提供给验证器。
  • 维护区块生成者费用信息,让验证器知道如何进行小费分配。

关键代码片段:

pub struct BlockEngineStage {
    t_hdls: Vec<JoinHandle<()>>,
}

impl BlockEngineStage {
    pub fn new(
        block_engine_config: Arc<Mutex<BlockEngineConfig>>,
        bundle_tx: Sender<Vec<PacketBundle>>,
        packet_tx: Sender<PacketBatch>,
        banking_packet_sender: BankingPacketSender,
        exit: Arc<AtomicBool>,
        block_builder_fee_info: Arc<Mutex<BlockBuilderFeeInfo>>,
    ) -> Self {
        let thread = Builder::new().name("block-engine-stage".to_string())
            .spawn(move || {
                let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
                rt.block_on(Self::start(...));
            }).unwrap();

        Self { t_hdls: vec![thread] }
    }
}
let subscribe_bundles_stream = client
    .subscribe_bundles(SubscribeBundlesRequest {})
    .await?
    .into_inner();

while let Some(resp) = subscribe_bundles_stream.message().await? {
    let bundles = resp.bundles.into_iter()
        .map(|bundle_proto| PacketBundle {
            batch: PacketBatch::new(
                bundle_proto.packets.into_iter().map(proto_packet_to_packet).collect()
            ),
            bundle_id: bundle_proto.uuid,
        })
        .collect();

    // 转发到bundle_stage:
    bundle_tx.send(bundles)?;
}
  • Jito使用BlockEngineValidatorClient订阅两个主要流:
    • subscribe_bundles — 入站捆绑(每个“PacketBundle”是一组交易,通常每个捆绑4-5个交易)。
    • 小费指令是最后一笔交易(简单的转账到一个地址——随后从Jito发送给EOA)。
  • BlockBuilderFeeInfoRequest,用来让验证器查询区块引擎当前区块构建者的公钥和佣金。

典型流:

  • 搜索者将捆绑推送至区块引擎。区块引擎会进行重新检查或离线模拟。
  • 验证器有一个BlockEngineStage与区块引擎连接。通过gRPC,区块引擎同时传递正常交易(几乎就像第二个流水线)和捆绑。
  • 如果gRPC消息被识别为“捆绑”,它会将其重新组合为PacketBundles并将其发送至bundle_tx => consumedBundleStage处理。
  • 为了提高性能,正常交易与中继器的处理方式相同:如果来自受信任的来源(trust_packets == true),则绕过签名验证。
  • 区块引擎还会告诉验证器有关区块生成者的佣金和公钥。验证器使用这些信息来调整小费分配。
  • 最后,区块引擎将最佳或最高支付的捆绑传递给验证器,以原子方式执行。

捆绑阶段

BundleStage是Jito-solana验证器中的新流水线,处理来自区块引擎的捆绑。捆绑具有所有包含的交易必须以原子方式成功执行或完全失败的特性——对于MEV流程来说至关重要。通过设计,BundleStage作为现有Solana交易流程(如BankingStage)的并行对应物,但它对捆绑执行强制要求更严格的原子保证。一旦捆绑被接受,该捆绑中所有交易成功落入一个区块,或者整个捆绑被拒绝。

关键责任:

  • 每个捆绑要完全执行或完全丢弃。
  • 捆绑内的交易必须按搜索者提供的严格顺序执行,确保没有与BankingStage的无关交易部分重叠或交错。

关键代码片段:

  • bundle_stage.rs和捆绑阶段模块
  • bundle_stage.rs和bundle_consumer.rs协调整个流水线:接收、路由到专门的逻辑以进行模拟、原子锁定、最终提交等。
  • bundle_account_locker.rs确保捆绑一组交易所用的所有账户从系统的其余部分锁定,直到捆绑完成。如果多个捆绑到达,这些锁定可以防止冲突修改。
  • bundle_packet_receiver.rs接收来自区块引擎或中继器的入站gRPC基础或本地PacketBundle流,并将其发送至下一个阶段。

典型流:

  • 捆绑接收
// 来源于`bundle_packet_receiver.rs`的粗略代码片段
while let Ok(packet_bundles) = bundle_receiver.recv_timeout(recv_timeout) {
    // ... 将其收集到内部结构中 ...
}

BundleStage拥有一个BundleReceiver(在bundle_packet_receiver.rs中)负责拾取捆绑。这些捆绑不是通过通常的原始UDP或QUIC流量传入,而是从区块引擎或中继器通过可靠的gRPC通道到达。

  • 反序列化和过滤
/// 一个捆绑有如下要求:
  /// - 所有交易必须可清洗
  /// - 无重复签名
  /// - 不得包含黑名单帐户
  /// - 不能已经被处理过或包含无效的块哈希
  pub fn build_sanitized_bundle(
      &self,
      bank: &Bank,
      blacklisted_accounts: &HashSet<Pubkey>,
      transaction_error_metrics: &mut TransactionErrorMetrics,
  ) -> Result<SanitizedBundle, DeserializedBundleError> {
      if bank.vote_only_bank() {
          return Err(DeserializedBundleError::VoteOnlyMode);
      }

      let transactions: Vec<SanitizedTransaction> = self
          .packets
          .iter()
          .filter_map(|p| {
              p.build_sanitized_transaction(
                  bank.vote_only_bank(),
                  bank,
                  bank.get_reserved_account_keys(),
              )
          })
          .collect();

      if self.packets.len() != transactions.len() {
          return Err(DeserializedBundleError::FailedToSerializeTransaction);
      }

      let unique_signatures: HashSet<&Signature, RandomState> =
          HashSet::from_iter(transactions.iter().map(|tx| tx.signature()));
      if unique_signatures.len() != transactions.len() {
          return Err(DeserializedBundleError::DuplicateTransaction);
      }

      let contains_blacklisted_account = transactions.iter().any(|tx| {
          tx.message()
              .account_keys()
              .iter()
              .any(|acc| blacklisted_accounts.contains(acc))
      });

      if contains_blacklisted_account {
          return Err(DeserializedBundleError::BlacklistedAccount);
      }

      // 假设所有内容均成功锁定,以检查是否有已经处理的交易或过期/无效块哈希
      let lock_results: Vec<_> = repeat(Ok(())).take(transactions.len()).collect();
      let check_results = bank.check_transactions(
          &transactions,
          &lock_results,
          MAX_PROCESSING_AGE,
          transaction_error_metrics,
      );

      if check_results.iter().any(|r| r.is_err()) {
          return Err(DeserializedBundleError::FailedCheckTransactions);
      }

      Ok(SanitizedBundle {
          transactions,
          bundle_id: self.bundle_id.clone(),
      })
  }

Jito包括一个ImmutableDeserializedBundle逻辑(在 immutable_deserialized_bundle.rs中)以解包每个捆绑。它检查签名,丢弃无效或重复的交易,并确保整个捆绑可以被处理。失败的交易会让整个捆绑被丢弃。

  • 账户锁定
pub struct LockedBundle<'a, 'b> {
    bundle_account_locker: &'a BundleAccountLocker,
    sanitized_bundle: &'b SanitizedBundle,
    bank: Arc<Bank>,
}

impl<'a, 'b> LockedBundle<'a, 'b> {
    pub fn new(
        bundle_account_locker: &'a BundleAccountLocker,
        sanitized_bundle: &'b SanitizedBundle,
        bank: &Arc<Bank>,
    ) -> Self {
        Self {
            bundle_account_locker,
            sanitized_bundle,
            bank: bank.clone(),
        }
    }

    pub fn sanitized_bundle(&self) -> &SanitizedBundle {
        self.sanitized_bundle
    }
}

// 自动解锁捆绑帐户时调用
impl<'a, 'b> Drop for LockedBundle<'a, 'b> {
    fn drop(&mut self) {
        let _ = self
            .bundle_account_locker
            .unlock_bundle_accounts(self.sanitized_bundle, &self.bank);
    }
}

impl BundleAccountLocks {
    pub fn read_locks(&self) -> HashSet<Pubkey> {
        self.read_locks.keys().cloned().collect()
    }

    pub fn write_locks(&self) -> HashSet<Pubkey> {
        self.write_locks.keys().cloned().collect()
    }

    pub fn lock_accounts(
    ...
    }
...

一旦验证通过,BundleStage将会锁定捆绑的整个集合所有使用的账户(bundle_account_locker.rs)。这是一个重要步骤:不允许部分或并行使用这些账户,从而确保原子和无冲突的使用。

  • 模拟与执行

bundle_consumer.rs执行捆绑中的交易,检查一系列潜在的失败:无效的块哈希、lamports不足或程序错误。如果任何交易的模拟失败,整个捆绑将被丢弃。如果全部通过:

  • 系统会将交易加载到一个顺序批次中,提交给PoH(账本记录)。

  • 由于交易被锁定且成本模型事先计算,系统知道可以安全地应用它们。

  • 原子提交

    fn execute_record_commit_bundle(
        committer: &Committer,
        recorder: &TransactionRecorder,
        log_messages_bytes_limit: &Option<usize>,
        max_bundle_retry_duration: Duration,
        sanitized_bundle: &SanitizedBundle,
        bank_start: &BankStart,
    ) -> ExecuteRecordCommitResult {
        let transaction_status_sender_enabled = committer.transaction_status_sender_enabled();

        let mut execute_and_commit_timings = LeaderExecuteAndCommitTimings::default();

        debug!("捆绑: {} 正在执行", sanitized_bundle.bundle_id);
        let default_accounts = vec![None; sanitized_bundle.transactions.len()];
        let mut bundle_execution_results = load_and_execute_bundle(
        ...

        )
        ...

        }

如果最终执行和记录步骤成功,所有交易将被添加到账本(通过PoHRecorder)。如果有些意外失败(例如突然出现的AccountInUse错误),Jito会强制回滚整个集合。

  • 小费管理
/// 验证器需要管理与两个小费相关程序的状态
fn handle_tip_programs(
    bundle_account_locker: &BundleAccountLocker,
    tip_manager: &TipManager,
    // ...
    bank_start: &BankStart,
    bundle_stage_leader_metrics: &mut BundleStageLeaderMetrics,
) -> Result<(), BundleExecutionError> {
    // 1) 初始化小费支付和小费分配程序(如有需要)
    if let Some(initialize_tip_programs_bundle) =
        tip_manager.get_initialize_tip_programs_bundle(&bank_start.working_bank, &keypair)
    {
        // 锁定账户,然后执行和提交...
    }

    // 2) 创建并发送"小费刻度捆绑"(如小费接收者或块生成者发生更改时)
    if let Some(tip_crank_bundle) = tip_manager.get_tip_programs_crank_bundle(
        &bank_start.working_bank,
        &kp,
        &block_builder_fee_info.lock().unwrap(),
    )? {
        // 锁定账户,然后执行和提交...
    }

    Ok(())
}
  • 一旦验证器成为领导者(并准备处理引用小费账户的捆绑),BundleStage将检查小费支付和小费分配程序是否已完全初始化。如果没有,它将自动生成(或重新初始化)它们,通过发送一个短初始化捆绑的交易。
  • 在程序本身设立后,验证器必须确认每轮的“小费储蓄罐”准备好。Jito称其为TipDistributionAccount。如果一个新的轮次开始(或小费分配账户需要变更),验证器将构建另一个小捆绑——"小费刻度捆绑"。这确保小费接收者更新为正确的位置,以便验证器能够获得预期的MEV小费股份。
  • 每次用户的交易或MEV策略引用小费支付PDA时,Jito验证器需要确认小费接收者指向当前轮次的验证器短暂TipDistributionAccount。如果链上状态与预期不同,BundleStage自动生成内部交易(如"清理旧小费"或"更改块生成者+小费接收者"),首先提交这些交易,然后再处理用户捆绑。
  • 与任何其他捆绑一样,这些内部小费捆绑会被锁定、模拟,然后作为一个原子集合执行。如果某个步骤失败——例如小费程序的余额不足——BundleStage将撤销整个小费更新。这避免在插槽中对小费账户进行部分或相互矛盾的更改。
  • 丢弃与清理

一旦验证器的领导插槽结束,BundleStage会冲洗剩余或部分处理的捆绑。这可以防止过期状态干扰下一个领导者插槽。

区块引擎与其他组件的集成

区块引擎与Jito Solana架构中的几个其他组件进行集成:

  • 中继器:
    • 将交易延迟约200毫秒,以便形成捆绑。
    • 确保交易及时转发到验证器。
  • Jito-Solana验证器:
    • 将选择的捆绑和交易转发到验证器。
    • 确保验证器具有执行捆绑所需的上下文(例如账户状态)。
  • 小费支付程序:
    • 跟踪与捆绑相关的小费。
    • 确保小费存储在PDA中,以便稍后分配。
  • 小费分配程序:
    • 根据对网络的贡献,将MEV小费分配给验证器和质押者。

挑战与优化

区块引擎面临几个挑战,包括:

  • 可扩展性
    • 区块引擎负责从多个中继器、搜索者和直接客户端连接摄取和处理大量交易和MEV捆绑。Solana的交易吞吐量可能非常高,因此粗略的方法可能难以应对排队。这尤其是一个非常难的问题,因为:
    • Solana的快速区块时间意味着新交易不断以流的形式到达。
    • 大型捆绑必须快速(在200毫秒内)模拟,以免阻塞。
    • 该引擎必须与验证器协调,以确保不处理过期或重复的数据。
    • 有三个主要的瓶颈领域:
    • 网络I/O:基于gRPC的流如果没有小心安排,容易饱和。
    • CPU:快速模拟大型多交易的捆绑需要有效的并发。
    • 内存:维持多个正在进行的模拟或大型队列缓冲区会突然增加内存使用量。
  • 原子性
    • Jito中的捆绑必须一起执行,或者根本不执行。验证器的捆绑执行代码(BundleStage)强制执行这一顺序、切忌菜多菜少的属性。区块引擎必须准备这些捆绑,并以验证器可以容易地接受或拒绝的方式呈现它们。这导致有趣的难题,因为:
    • 如果捆绑中间的某个交易失败,验证器无法提交同一捆绑的较早或较晚部分。
    • 多个捆绑可能会引用同一账户或地址表。引擎必须尽早检测并处理这些冲突,以避免浪费时间在不可行的合并上。
    • 一捆绑的特定交易可能依赖于先前捆绑的输出。处理链式依赖可能会变得复杂。
    • 如果区块引擎试图在插槽结束前排程某个捆绑,整捆可能会无效,如果未能按时完成。
  • 盈利能力
    • Jito生态系统专注于每个插槽最大化MEV。区块引擎的工作是选择出最佳的交易(无论是否付小费)以应付即将到来的区块。这是一个困难问题,因为:
    • 随着时间的推移,新的高支付交易可能会到达而排队。
    • 即使某个捆绑提供了巨额小费,也可能会消耗太多的计算,阻止其他高支付交易填充到区块中。
    • 对许多搜索者来说,有联接可获得公平的通道而选取最有利可图的单个捆绑(贪婪集合)之间存在紧张关系。

为了应对这些挑战,区块引擎实施了几项优化:

  • 并行模拟
    • 区块引擎生成多个工作任务或线程,以模拟和验证某个捆绑是否可以成功在当前帐本状态下执行。每个工作线程模拟单一捆绑(或小子集)。
    • 引擎缓存账户状态,以减少从链上存储进行重复读操作。一旦某个状态被一个模拟所更新,如果该模拟稍后被认为是正确的,它可以逐步更新缓存。
    • 如果模拟耗时过长(例如年长捆绑,因大重量),引擎会撤销或优先处理它,以集中注意力于更靠近、有更高价值的请求。
  • 贪婪选择
    • 区块引擎通常有更多模拟的捆绑比能在区块中容纳的捆绑多。必须选择出最大总小费(以获得验证器的报酬的捆绑)组合。
    • 模拟后,它知道每个捆绑可能的净小费。它将按收益降序排列。
    • 如果下一个“最有利可图”的捆绑在被锁定的账户或地址表上存在冲突,则跳过它或重新排序以找到下一个最优良集合。
  • Gas优化
    • 即使某个捆绑报告了非常高的小费,它也可能需要大量计算——填满整区块。这可能会挤走其他有利于交易的机会,从而导致总利润减少。
    • 每个模拟的捆绑都会得到总计算使用量的估计。
    • 引擎对每个捆绑计算一个“每个计算单位的小费”指标。
    • 如果某个捆绑的(小费/计算单位)比率较低,则它可能在总体小费更高的小捆绑后提前。

Jito小费支付程序

Jito小费支付程序是Jito Solana架构中另一个关键组件,负责存储和管理MEV小费。通过将小费安全存储在PDA中,并向小费分配程序提供必要的数据,它确保搜索者有动力提交有利可图的MEV捆绑,而验证器和质押者则得到了公平的报酬。

关键责任:

  • 在PDA中存储MEV小费。每个PDA与特定捆绑或搜索者相关联。
  • 维护每个捆绑或搜索者的小费记录。
  • 确保小费能够准确处理、追踪,并在需要时能够被检索。
  • 为小费分配程序提供必要的数据(例如小费余额),以便将小费分配给验证器和质押者。

典型流程

用户支付额外的MEV小费以获得交易的优先权。这些小费是存入多达八个静态PDAs (例如,PDA0…PDA7)的lamports。配置PDA (CFG_PDA)将它们全部联系在一起,允许验证器在每个轮次中设置正确的tip_receiver

小费支付流程:

  • changeTipReceiver指令将当前轮次的TDA(小费分配账户)设置为tip_receiver
  • 每当小费支付交易执行时,它会将lamports存入一个小费PDA中。
  • 到达轮次末尾,这些小费PDA会倾倒到TDA中进行分配。

与其他组件的集成

小费支付程序与Jito Solana架构中的多个其他组件进行集成:* 区块引擎:

  • 接收区块引擎的提示支付,当捆绑包被选为包含在区块中时。
  • 确保提示存储在正确的 PDA 中。
    • 提示分发程序:
  • 为提示分发程序提供提示余额数据,以分配提示给验证者和质押者。
    • Jito-Solana 验证者:
  • 确保提示被安全存储,并在需要时可以检索。

挑战与优化

提示支付程序面临几个挑战,包括:

  • 安全性
    • 该程序必须确保只有 Jito-Solana 验证者可以更新“提示”账户。这意味着普通用户不能操控提示余额或不正确地从提示账户中提取 lamports。
  • 可扩展性
    • 如果验证者接收许多小额提示转账,一个一个处理可能效率低下,特别是当你必须对每个区块或每个纪元进行多次链上更新时。

为了解决这些挑战,提示支付程序实施了多项优化:

  • PDA 安全性
    • 提示支付程序使用由已知种子 (TIP_ACCOUNT_SEED_0..7, CONFIG_ACCOUNT_SEED) 键控的 PDAs,以确保只有授权用户可以重新分配或从这些提示账户中提取。
  • 批处理处理
    • 许多提示更新指令会被聚合到一个交易或少数几个交易中—例如,initialize_tip_payment_program_txinitialize_tip_distribution_account_tx,或同时管理多个 PDAs 的“crank”捆绑包,而不是逐一处理。
  • 异步更新
    • Jito-Solana 验证者定期插入特殊交易(“crank”),以最终确定或轮换提示 PDAs,或将提示分配给实际的验证者或质押者。这是在后台循环中完成的—而不是在每个涉及提示程序的用户交易上的同步开销。

Jito 提示分发程序

提示分发程序管理每个纪元的提示收集。每个验证者都有一个 TDA: PDA(vote_key, epoch)。它累积了该纪元的所有提示。

在纪元结束时,一个链下(或未来的去中心化 NCN)过程计算一个 Merkle 根,定义 TDA 中的提示是如何在验证者及其委托者(或质押者)之间按股份比例分配的。当前的提示分配是集中的,新的 JIP 提示奖励 NCN 将去中心化这个过程。

关键功能:

  • 从提示支付程序中检索每个捆绑包或搜索者的提示余额。
  • 根据验证者和质押者对网络的贡献计算分配份额。
  • 根据他们计算的份额将提示分配给验证者和质押者。
  • 更新验证者和质押者的余额,添加他们的提示份额。
  • 确保分配在区块链状态中准确反映。

与其他组件的集成

提示分发程序与 Jito Solana 架构中的几个其他组件集成:

  • 提示支付程序:
    • 从提示支付程序中检索提示余额以进行分配。
    • 确保要分配的总提示金额准确计算。
  • Jito-Solana 验证者:
    • 根据其对网络的贡献分配提示给验证者。
    • 确保验证者为他们的工作得到公平补偿。
  • 质押者:
    • 根据他们在网络中的股份分配提示给质押者。
    • 确保质押者为其贡献得到公平补偿。

纪元结束分配流程(当前)

在纪元结束时,一个链下机构计算分配。Merkle 根上传到链上以便质押者索取。

sequenceDiagram
    participant OffChain
    participant JitoValidator
    participant TipDistributionProgram
    participant Staker

    OffChain->>OffChain: 计算 TDA 的 Merkle 根(验证者和质押者的份额)
    OffChain->>JitoValidator: 提交交易在链上上传 Merkle 根
    JitoValidator->>TipDistributionProgram: 用 Merkle 根更新 TDA
    Staker->>TipDistributionProgram: 提交带有 Merkle 证明的索赔
    TipDistributionProgram->Staker: 转移质押者的份额

在当前设置(预 NCN)中,由一个集中或“许可”方负责在每个纪元结束时确定奖励:

  • 计算链下的 Merkle 根

一个链下服务(通常由验证者或指定的权威机构运行)根据验证者及其质押者在该纪元获得了多少提示来计算 Merkle 根。

  • 上传 Merkle 根

该服务然后向验证者发送交易,验证者将 Merkle 根传递给链上的 TipDistributionProgram

  • 基于 Merkle 的索赔

一旦 Merkle 根被存储,每个质押者提供 Merkle 证明以索取其提示份额。

  • 质押者接收份额

在提供有效证明后,TipDistributionProgram 将正确的 lamports 部分转移到质押者的账户中。

Merkl 树中存储的内容

##[derive(Clone, Eq, Debug, Hash, PartialEq, Deserialize, Serialize)]
pub struct TreeNode {
    // 要索取提示的权益账户或验证者投票账户。
    pub claimant: Pubkey,

    // ClaimStatus PDA 的 Pubkey(用于防止双重索赔)。
    pub claim_status_pubkey: Pubkey,

    // 上述 claim_status_pubkey 的 Bump 种子。
    pub claim_status_bump: u8,

    // 通常来自质押账户的质押者和提取者字段。
    pub staker_pubkey: Pubkey,
    pub withdrawer_pubkey: Pubkey,

    // 当前纪元的提示应支付给该索赔者的 lamports。
    pub amount: u64,

    // 从该叶子到根的 Merkle 证明(树构建后填充)。
    pub proof: Option<Vec<[u8; 32]>>,
}

备注:

  • 这个过程使得信任集中在计算和上传 Merkle 根的人身上。
  • 质押者必须信任该计算是公平的。
  • 这个方法更简单,但透明度较低,并引入了单点故障。

使用提示奖励 NCN 的去中心化分发

(这是一项正在进行中的工作,所有具体细节现在都处于研发阶段。)

Jito 提示奖励 NCN 是一个去中心化框架,旨在确保在 Jito-Solana 生态系统中公平高效的奖励分配。通过结合链上程序与链下过程,它提供了一种强大的机制来处理奖励,确保所有参与者都有动力为网络的安全性和效率做出贡献。该系统专注于共识和削减机制,确保奖励分配的公平,并确保运营者对他们的参与承担责任。

graph TD
    A[纪元开始] --> B[存入奖励]
    B --> C[奖励 Dropbox]
    C --> D[转移到 EpochRewardMerkleRoot]
    D --> E[创建 EpochRewardMerkleRoot]
    E --> F[运营者创建 Merkle 树]
    F --> G[提交 Merkle 根票据]
    G --> H[对 Merkle 根进行投票]
    H --> I{达成共识了吗?}
    I -->|是| J[最终确定 Merkle 根]
    I -->|否| K[处理失信纪元]
    J --> L[分配奖励]
    L --> M[接受者收到奖励]
    K --> N[将奖励转移到当前纪元]
    N --> L
    H --> O[削减条件]
    O --> P{运营者投票了吗?}
    P -->|否| Q[削减运营者]
    P -->|是| R{运营者达成共识了吗?}
    R -->|否| Q
    R -->|是| S[运营者获得奖励]
    Q --> T[运营者被削减]
    S --> L
    T --> L

关键组件:

  • 纪元奖励 Merkle 树:
    • 每个节点维护其自己纪元奖励的 Merkle 树,其中包含给定纪元要分配的奖励。
    • 每个 Merkle 树的根哈希用于验证奖励数据的完整性。
  • Jito 奖励 NCN:
    • Jito 奖励 NCN 通过验证其纪元奖励 Merkle 树的根哈希,确保参与 NCN 之间的一致性。
    • 它还处理奖励的分配,并对于不合规的运营者强制执行削减条件。

奖励分配流程涉及几个步骤,从存入奖励到分配给接受者。以下是每个步骤的详细解释,以及相关代码段。

存入奖励

pub fn process_dropbox_to_latest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // 在此添加代码
    todo!();
}

奖励可以存入 EpochRewardMerkleRoot 账户或 RewardDropbox PDA。使用 dropbox_to_latest 指令转移奖励从 RewardDropbox 到当前纪元的 EpochRewardMerkleRoot

投票与共识

pub fn process_upload_and_vote(program_id: &Pubkey, accounts: &[AccountInfo], root: MerkleRoot) -> ProgramResult {
    // 在此添加代码
    todo!();
}

运营者在投票窗口内创建并对其 EpochRewardMerkleRootTicket 投票。使用 upload_and_vote 指令根据运营者的投票更新根。

分配奖励

pub fn process_distribute_crank(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // 在此添加代码
    todo!();
}

如果达成共识,则使用 distribute_crank 指令根据共识根哈希作为证明分配来自有效 EpochRewardMerkleRoot 的奖励。

处理失信纪元

pub fn process_delinquent_to_latest(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // 在此添加代码
    todo!();
}

如果不达成共识,则奖励可以使用 delinquent_to_latest 指令转移到当前纪元的奖励。

削减不合规的运营者

pub fn process_slash(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    // 在此添加代码
    todo!();
}

未在投票窗口内投票或未达成共识的运营者可以使用 slash 指令进行削减。

结束纪元分配流程(与 TipRouter NCN 一起)

NCN 节点的运营者各自计算 Merkle 根。一旦达成三分之二的共识,该根将被发布到链上,奖励分配会自动包含 DAO 和 NCN 参与者的 3% 费用。

sequenceDiagram
    participant OffChainSoftware
    participant Operators (NCN)
    participant TipRouterNCN
    participant TipDistributionProgram
    participant DAO
    participant Staker

    OffChainSoftware->>Operators: 计算 TDA 的 Merkle 根
    Operators->>TipRouterNCN: 对 Merkle 根进行投票,争取 66% 的共识
    TipRouterNCN->>TipDistributionProgram: 一旦达成共识,上传 Merkle 根到链上
    TipDistributionProgram->>DAO: 扣除 3% 费用(2.7% 给 DAO,0.3% 给 NCN 参与者)
    TipDistributionProgram->Staker: 在索赔时,用证明支付相关部分

TipRouter NCN 正式上线时,分配通过一网络的运营者去中心化:

  • 集体计算

链下软件首先将所有的提示数据汇总为候选 Merkle 根。每个运营者(NCN 节点)计算或验证同一个 Merkle 根。

  • 链上投票

这些运营者通过 TipRouterNCN 智能合约提交对 Merkle 根的投票。如果三分之二的股权投票赞成,该 Merkle 根会最终确定。

  • 自动上传

共识达成后,NCN 程序自动将 Merkle 根发布到链上的 TipDistributionProgram

  • 费用扣除和分配

奖励的 3% 分成归 DAO 财库(2.7%)和 NCN 参与者(0.3%)。其余则根据 Merkle 根的规定支付给验证者和质押者。

  • 质押者索取奖励

最后,每个质押者使用 Merkle 证明索取他们的部分,确保分配的可信度更高。

备注:

  • 这个过程消除了单点故障;没有单一方可以更改或扣留 Merkle 根。
  • 添加削减条件来惩罚违规行为,从而协调运营者的激励。
  • 改善了 MEV 奖励分配的整体透明度和公平性。

挑战与优化

提示分发程序面临几个挑战,包括:

  • 公平性:确保提示在验证者和质押者之间公平分配。
  • 效率:高效处理大量分配。
  • 安全性:确保提示安全分配,防止篡改。

为了解决这些挑战,提示分发程序可能实施几项优化:

  • 预定义公式:使用预定义公式计算分配份额,以确保公平性。
  • 批处理中处理:以批处理方式处理分配,提高效率。
  • 安全分配:使用安全的链上机制分配提示,防止篡改。

结论

总之,在 Solana 上的 MEV 是巨大的——无论在规模上还是在如何塑造用户体验和验证者经济上。超过 90% 的 Solana 股权选择进入 Jito 网络,理解 Jito 在很大程度上就是理解 Solana 如何日常运作。我们对 Jito 中继器、区块引擎、捆绑阶段及周边系统架构的深入探讨,应该为分析和交互 Solana 上的交易和 MEV 提供坚实基础。

在 Eclipse,我们相信开放研究可以为类似 Solana 的前沿区块链协议的演变带来更高透明度——最终建立链上机制、数据分析和最佳实践之间的桥梁。我们将继续发布作品,解析关键 Solana 基础设施的内在工作,以便生态系统中的每个人都能从经过深思熟虑的深入视角中受益。

通过阐明 Jito 的架构和 MEV 过程,我们希望更多的利益相关者——搜索者、交易者、验证者和普通用户——能拥有必要的知识,负责地参与和优化 Solana 网络。如果你有疑问或见解,欢迎合作,我们期待着在 Solana 新兴的“模块化”栈周围促进一个越来越丰富的研究社区。

参考资料

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

0 条评论

请先 登录 后评论
thogiti
thogiti
https://thogiti.github.io/