BOLT 4:洋葱路由协议

  • lightning
  • 发布于 2025-04-17 12:48
  • 阅读 18

该文档描述了洋葱路由协议的构建方法,用于将支付从发起节点路由到最终节点。数据包通过多个中间节点(称为“跳”)进行路由。路由方案基于 Sphinx 构造,并扩展了每个跳的有效负载。中间节点可以验证数据包的完整性,并了解应将数据包转发到哪个节点。该协议使用共享密钥生成伪随机字节流来混淆数据包,并使用密钥来加密有效负载和计算 HMAC,以确保每个跳的数据包完整性。

BOLT #4: Onion 路由协议

概述

本文档描述了 onion 路由数据包的构建,该数据包用于将付款从起始节点路由到最终节点。该数据包通过多个中间节点路由,这些节点称为

路由模式基于 [Sphinx][sphinx] 结构,并扩展为每个跳的 payload。

转发消息的中间节点可以验证数据包的完整性,并且可以了解应该将数据包转发到哪个节点。他们无法了解除其前任或后继者之外的哪些其他节点是数据包路由的一部分;他们也无法了解路由的长度或它们在其中的位置。每个跳都会对数据包进行混淆,以确保网络级别的攻击者无法关联属于同一路由的数据包(即,属于同一路由的数据包不共享任何相关信息)。请注意,这并不排除攻击者通过流量分析关联数据包的可能性。

路由由起始节点构建,该节点知道每个中间节点和最终节点的公钥。了解每个节点的公钥允许起始节点为每个中间节点和最终节点创建一个共享密钥(使用 ECDH)。然后,共享密钥用于生成字节的伪随机流(用于混淆数据包)和多个密钥(用于加密 payload 和计算 HMAC)。然后,HMAC 又用于确保每个跳处数据包的完整性。

路由上的每个跳只看到起始节点的临时密钥,以隐藏发送者的身份。临时密钥在转发到下一个节点之前,会被每个中间跳进行盲化,从而使 onion 在路由上不可链接。

本规范描述了数据包格式和路由机制的 版本 0

节点:

  • 收到比其实现的版本更高的版本的数据包时:
    • 必须向起始节点报告路由失败。
    • 必须丢弃该数据包。

目录

约定

本文档中遵循许多约定:

  • HMAC:数据包的完整性验证基于密钥哈希消息认证码,如 [FIPS 198 标准][fips198]/[RFC 2104][RFC2104] 所定义,并使用 SHA256 哈希算法。
  • 椭圆曲线:对于涉及椭圆曲线的所有计算,都使用 Bitcoin 曲线,如 [secp256k1][sec2] 中所指定
  • 伪随机流:[ChaCha20][rfc8439] 用于生成伪随机字节流。对于其生成,使用固定的 96 位 null-nonce(0x000000000000000000000000),以及从共享密钥派生的密钥和所需输出大小的 0x00 字节流作为消息。
  • 术语 起始节点最终节点 分别指初始数据包发送者和最终数据包接收者。
  • 术语 节点 有时可以互换使用,但 通常指路由中的中间节点,而不是终端节点。 起始节点 --> --> ... --> --> 最终节点
  • 术语 处理节点 指沿路由的特定节点,该节点当前正在处理转发的数据包。
  • 术语 对等节点 仅指直接邻居(在覆盖网络中):更具体地说,发送对等节点 将数据包转发到 接收对等节点
  • 路由中的每个跳都有一个可变长度的 hop_payload 或一个固定大小的旧版 hop_data payload。
    • 旧版 hop_data 由单个 0x00 字节前缀标识
    • 可变长度的 hop_payload 以一个 bigsize 编码为前缀,该编码以字节为单位对长度进行编码,不包括前缀和尾随 HMAC。

密钥生成

从共享密钥派生出多个加密和验证密钥:

  • rho:用作生成伪随机字节流时的密钥,该字节流用于混淆每个跳的信息
  • mu:在 HMAC 生成期间使用
  • um:在错误报告期间使用
  • pad:用于为起始 mix-header 数据包生成随机填充字节

密钥生成函数接受一个密钥类型(rho=0x72686Fmu=0x6d75um=0x756dpad=0x706164)和一个 32 字节的密钥作为输入,并返回一个 32 字节的密钥。

密钥通过计算 HMAC(以 SHA256 作为哈希算法)生成,使用适当的密钥类型(即 rhomuumpad)作为 HMAC 密钥,并使用 32 字节的共享密钥作为消息。然后,生成的 HMAC 作为密钥返回。

请注意,密钥类型不包括 C 风格的 0x00 终止字节,例如 rho 密钥类型的长度为 3 个字节,而不是 4 个字节。

伪随机字节流

伪随机字节流用于在路径的每个跳处混淆数据包,以便每个跳只能恢复下一个跳的地址和 HMAC。伪随机字节流通过加密(使用 ChaCha20)一个 0x00 字节流生成,该字节流具有所需的长度,并使用从共享密钥派生的密钥和一个 96 位零 nonce(0x000000000000000000000000)进行初始化。

使用固定 nonce 是安全的,因为密钥永远不会重用。

数据包结构

数据包由四个部分组成:

  • 一个 version 字节
  • 一个 33 字节的压缩 secp256k1 public_key,用于共享密钥生成期间
  • 一个 1300 字节的 hop_payloads,由多个可变长度的 hop_payload payload 或最多 20 个固定大小的旧版 hop_data payload 组成。
  • 一个 32 字节的 hmac,用于验证数据包的完整性

数据包的网络格式由序列化为连续字节流的各个部分组成,然后传输给数据包接收者。由于数据包的大小是固定的,因此在通过连接传输时,无需以其长度作为前缀。

数据包的总体结构如下:

1. 类型:`onion_packet`
2. 数据:
   * [`byte`:`version`]
   * [`point`:`public_key`]
   * [`1300*byte`:`hop_payloads`]
   * [`32*byte`:`hmac`]

对于此规范(版本 0),version 具有常量值 0x00

hop_payloads 字段是一个结构,它保存混淆的路由信息和关联的 HMAC。它长 1300 字节,具有以下结构:

1. 类型:`hop_payloads`
2. 数据:
   * [`bigsize`:`length`]
   * [`hop_payload_length`:`hop_payload`]
   * [`32*byte`:`hmac`]
   * ...
   * `filler`

其中,lengthhop_payload(其内容取决于 length)和 hmac 针对每个跳重复;其中,filler 由混淆的、确定性生成的填充组成,如 填充生成 中所述。此外,hop_payloads 在每个跳都会递增地被混淆。

使用 hop_payload 字段,起始节点能够指定在每个跳转发的 HTLC 的路径和结构。由于 hop_payload 受数据包范围的 HMAC 保护,因此它包含的信息通过 HTLC 发送者(起始节点)和路径中的每个跳之间的每个成对关系进行完全身份验证。

通过这种端到端身份验证,每个跳都能够使用 hop_payload 的指定值交叉检查 HTLC 参数,并确保发送对等节点没有转发构造不当的 HTLC。

length 字段确定 hop_payload 字段的长度和格式;定义了以下格式:

  • 旧版 hop_data 格式,由长度的单个 0x00 字节标识。在这种情况下,hop_payload_length 定义为 32 字节。
  • tlv_payload 格式,由任何超过 1 的长度标识。在这种情况下,hop_payload_length 等于 length 的数值。
  • 长度的单个 0x01 字节保留供将来使用,以表示不同的 payload 格式。这是安全的,因为没有 TLV 值可以短于 2 个字节。在这种情况下,hop_payload_length 必须在使用此 length 的未来规范中定义。

旧版 hop_data payload 格式

hop_data 格式由单个 0x00 字节长度标识,用于向后兼容。 它的 payload 定义为:

1. 类型:`hop_data`(对于 `realm` 0)
2. 数据:
   * [`short_channel_id`:`short_channel_id`]
   * [`u64`:`amt_to_forward`]
   * [`u32`:`outgoing_cltv_value`]
   * [`12*byte`:`padding`]

字段描述:

  • short_channel_id:用于路由消息的传出通道的 ID;接收对等节点应操作此通道的另一端。

  • amt_to_forward:要转发到路由信息中指定的下一个接收对等节点的金额,以毫聪为单位。

    对于非最终节点,此值 金额 必须包括起始节点为接收对等节点计算的 费用。在处理传入的 Sphinx 数据包及其封装的 HTLC 消息时,如果以下不等式不成立,则应拒绝该 HTLC,因为它表明之前的跳已偏离指定的参数:

      incoming_htlc_amt - fee >= amt_to_forward

    其中 fee 根据接收对等节点公布的费用方案计算(如 BOLT #7 中所述)。

    对于最终节点,此值必须与传入的 htlc 金额完全相等,否则应拒绝该 HTLC。

  • outgoing_cltv_value:携带数据包的 传出 HTLC 应具有的 CLTV 值。

      cltv_expiry - cltv_expiry_delta >= outgoing_cltv_value

    包含此字段允许一个跳验证起始节点指定的信息和转发的 HTLC 的参数,并确保起始节点正在使用当前的 cltv_expiry_delta 值。如果不存在下一跳,则 cltv_expiry_delta 为 0。如果这些值不对应,则应失败并拒绝该 HTLC,因为这表明转发节点已篡改预期的 HTLC 值,或者起始节点具有过时的 cltv_expiry_delta 值。无论该跳是最终节点还是非最终节点,都必须对意外的 outgoing_cltv_value 做出一致的响应,以避免泄露其在路由中的位置。

  • padding:此字段供将来使用,也用于确保将来的非 0-realm hop_data 不会更改整体 hop_payloads 大小。

转发 HTLC 时,节点必须按照上面 hop_data 中指定的构造传出的 HTLC;否则,偏离指定的 HTLC 参数可能会导致不必要的路由失败。

tlv_payload 格式

这是一种更灵活的格式,它避免了最终节点的冗余 short_channel_id 字段。 它按照 BOLT #1 中定义的类型-长度-值格式进行格式化。

1. `tlv_stream`: `tlv_payload`
2. 类型:
    1. 类型: 2 (`amt_to_forward`)
    2. 数据:
        * [`tu64`:`amt_to_forward`]
    1. 类型: 4 (`outgoing_cltv_value`)
    2. 数据:
        * [`tu32`:`outgoing_cltv_value`]
    1. 类型: 6 (`short_channel_id`)
    2. 数据:
        * [`short_channel_id`:`short_channel_id`]
    1. 类型: 8 (`payment_data`)
    2. 数据:
        * [`32*byte`:`payment_secret`]
        * [`tu64`:`total_msat`]
    1. 类型: 16 (`payment_metadata`)
    2. 数据:
        * [`...*byte`:`payment_metadata`]

要求

编写者:

  • 除非 node_announcementinit 消息或 BOLT #11 提供 var_onion_optin 功能:
    • 必须改用旧版 payload 格式。
  • 对于每个节点:
    • 必须包括 amt_to_forwardoutgoing_cltv_value
  • 对于每个非最终节点:
    • 必须包括 short_channel_id
    • 不得包括 payment_data
  • 对于最终节点:
    • 不得包括 short_channel_id
    • 如果接收者提供了 payment_secret
      • 必须包括 payment_data
      • 必须将 payment_secret 设置为提供的那个
      • 必须将 total_msat 设置为将发送的总金额
    • 如果接收者提供了 payment_metadata
      • 必须为每个 HTLC 包括 payment_metadata
      • 除了固定 onion 大小所暗示的限制外,不得对 payment_metadata 的大小应用任何限制

读取者:

  • 如果 amt_to_forwardoutgoing_cltv_value 不存在,则必须返回错误。
  • 如果它是最终节点:
    • 如果 total_msat 不存在,则必须将其视为等于 amt_to_forward

这些字段内容的具体要求在 上方下方 指定。

基本多部分支付

HTLC 可能是更大的“多部分”支付的一部分:这种“基本”原子多路径支付将对所有路径使用相同的 payment_hash

请注意,amt_to_forward 仅是此 HTLC 的金额:包含更大值的 total_msat 字段是最终发送者承诺其余付款将通过后续 HTLC 进行;我们将这些具有相同 preimage 的未完成 HTLC 称为“HTLC 集”。

payment_metadata 将包含在每个支付部分中,以便可以尽早检测到无效的支付详细信息。

要求

编写者:

  • 如果发票提供 basic_mpp 功能:
    • 可以发送多个 HTLC 来支付发票。
    • 必须对集合中的所有 HTLC 使用相同的 payment_hash
    • 应大约在同一时间发送所有付款。
    • 应尝试为每个 HTLC 使用到接收者的不同路径。
    • 应重试和/或重新划分失败的 HTLC。
    • 如果发票指定了 amount
      • 必须将 total_msat 设置为至少该 amount,并且小于或等于 amount 的两倍。
    • 否则:
      • 必须将 total_msat 设置为它希望支付的金额。
    • 必须确保到达收款人的 HTLC 集的总 amount_msat 等于 total_msat
    • 如果 HTLC 集的总 amount_msat 已经大于或等于 total_msat,则不得发送另一个 HTLC。
    • 必须包括 payment_secret
  • 否则:
    • 必须将 total_msat 设置为等于 amt_to_forward

最终节点:

  • 如果 失败消息 下的要求指示,则必须使 HTLC 失败
    • 注意:此处指定的“已支付金额”是 total_msat 字段。
  • 如果它不支持 basic_mpp
    • 如果 total_msat 不完全等于 amt_to_forward,则必须使 HTLC 失败。
  • 否则,如果它支持 basic_mpp
    • 必须将其添加到对应于该 payment_hash 的 HTLC 集中。
    • 如果所有 HTLC 的 total_msat 不相同,则应使整个 HTLC 集失败。
    • 如果此 HTLC 集的总 amount_msat 等于 total_msat
      • 应满足 HTLC 集中所有 HTLC
    • 否则,如果此 HTLC 集的总 amount_msat 小于 total_msat
      • 不得满足 HTLC 集中任何 HTLC
      • 在经过一段合理的超时后,必须使 HTLC 集中所有 HTLC 失败。
      • 应在初始 HTLC 之后至少等待 60 秒。
      • 应使用 mpp_timeout 作为失败消息。
      • 必须要求集合中所有 HTLC 具有 payment_secret
    • 如果它满足 HTLC 集中任何 HTLC:
      • 必须满足整个 HTLC 集。
理由

如果存在 basic_mpp,它会导致延迟,以允许其他部分付款合并。总金额必须足以支付所需的付款,就像单个付款一样。但是,这必须合理地限定范围,以避免拒绝服务。

由于发票不一定指定金额,并且由于付款人可以向最终金额添加噪声,因此必须明确发送总金额。这些要求允许稍微超过这一点,因为它简化了在拆分时向金额添加噪声的情况,以及发送者真正独立的情况(例如,朋友分摊账单)。

一旦集合超过约定的总金额,就限制发送 HTLC,以防止在所有部分付款到达之前发布 preimage:这将允许任何中间节点立即索取任何未完成的部分付款。

实现可以选择不满足否则符合金额标准的 HTLC 集(例如,其他一些失败或发票超时),但是,如果它仅满足其中的一些,则中间节点可以简单地索取剩余的那些。

接受和转发付款

一旦节点解码了 payload,它要么在本地接受付款,要么将其转发到 payload 中指示的对等节点作为下一个跳。

非严格转发

节点可以将 HTLC 沿着与 short_channel_id 指定的传出通道不同的传出通道转发,只要接收者具有 short_channel_id 预期的相同节点公钥即可。因此,如果 short_channel_id 连接节点 A 和 B,则 HTLC 可以通过连接 A 和 B 的任何通道转发。如果不遵守,将导致接收者无法解密 onion 数据包中的下一个跳。

理由

如果两个对等节点具有多个通道,则无论数据包通过哪个通道发送,下游节点都将能够解密下一个跳 payload。

实现非严格转发的节点能够实时评估与特定对等节点的通道带宽,并使用本地最优的通道。

例如,如果在转发时连接 A 和 B 的 short_channel_id 指定的通道没有足够的带宽,则 A 可以使用另一个通道。这可以减少付款延迟,防止 HTLC 由于 short_channel_id 的带宽限制而失败,而发送者仅在 A 和 B 之间的通道上尝试相同的路由,但仅在通道上有所不同。

非严格转发允许节点利用连接它们到接收节点的私有通道,即使该通道在公共通道图中未知。

建议

使用非严格转发的实现应考虑对具有相同对等节点的所有通道应用相同的费用计划,因为发送者可能会选择导致总成本最低的通道。具有不同的策略可能会导致转发节点根据发送者的最佳费用计划接受费用,即使它们在具有相同对等节点的所有通道上提供聚合带宽也是如此。

或者,实现可以选择仅将非严格转发应用于类似策略通道,以确保他们预期的费用收入不会因使用备用通道而偏离。

最后一个节点的 Payload

在构建路由时,起始节点必须使用具有以下值的最终节点的 payload:

  • payment_secret:设置为接收者指定的支付密钥(例如,来自 BOLT #11 支付发票的 payment_secret
  • outgoing_cltv_value:设置为接收者指定的最终到期时间(例如,来自 BOLT #11 支付发票的 min_final_cltv_expiry
  • amt_to_forward:设置为接收者指定的最终金额(例如,来自 BOLT #11 支付发票的 amount

这允许最终节点检查这些值并在需要时返回错误,但它也消除了倒数第二个节点进行探测攻击的可能性。否则,此类攻击可能会尝试通过重新发送具有不同金额/到期时间的 HTLC 来发现接收对等节点是否是最后一个节点。 最终节点将从收到的 HTLC 中提取其 onion payload,并将其值与 HTLC 的值进行比较。有关更多详细信息,请参见下面的 返回错误 部分。

如果不是为了上述情况,由于它不需要转发付款,因此最终节点可以简单地丢弃其 payload。

共享密钥

起始节点使用椭圆曲线 Diffie-Hellman 在该跳的发送者的临时密钥和跳的节点 ID 密钥之间建立与路由上每个跳的共享密钥。生成的曲线点被序列化为压缩格式,并使用 SHA256 进行哈希处理。哈希输出用作 32 字节的共享密钥。

椭圆曲线 Diffie-Hellman (ECDH) 是对 EC 私钥和 EC 公钥进行的操作,输出一个曲线点。对于此协议,使用 libsecp256k1 中实现的 ECDH 变体,该变体定义在 secp256k1 椭圆曲线上。在数据包构建期间,发送者使用临时私钥和跳的公钥作为 ECDH 的输入,而在数据包转发期间,跳使用临时公钥及其自身的节点 ID 私钥。由于 ECDH 的属性,它们都将派生相同的值。

盲化临时密钥

为了确保路由上的多个跳不能通过它们看到的临时公钥链接,密钥在每个跳处被盲化。盲化以确定性的方式完成,允许发送者在数据包构建期间计算相应的盲化私钥。

EC 公钥的盲化是 EC 点的单个标量乘法,该点表示具有 32 字节盲化因子的公钥。由于标量乘法的可交换性,盲化的私钥是输入对应的私钥与相同盲化因子的乘积。

盲化因子本身是计算为临时公钥和 32 字节共享密钥的函数。具体来说,它是以压缩格式序列化的公钥和共享密钥的串联的 SHA256 哈希值。

数据包构建

在以下示例中,假设 发送节点(起始节点)n_0 想要将数据包路由到 接收节点(最终节点)n_r。首先,发送者计算一条路由 {n_0, n_1, ..., n_{r-1}, n_r},其中 n_0 是发送者本身,n_r 是最终接收者。所有节点 n_in_{i+1} 必须是覆盖网络路由中的对等节点。然后,发送者收集 n_1n_r 的公钥,并生成一个随机的 32 字节 sessionkey。或者,发送者可能会传入 关联数据,即数据包提交的数据,但不包含在数据包本身中。关联数据将包含在 HMAC 中,并且必须与每个跳的完整性验证期间提供的关联数据匹配。

为了构建 onion,发送者将第一个跳 ek_1 的临时私钥初始化为 sessionkey,并通过乘以 secp256k1 基点从中推导出对应的临时公钥 epk_1。对于路由上的每个 k 跳,发送者然后迭代地计算下一个跳 ek_{k+1} 的共享密钥 ss_k 和临时密钥,如下所示:

  • 发送者使用跳的公钥和临时私钥执行 ECDH,以获得曲线点,该曲线点使用 SHA256 哈希处理以生成共享密钥 ss_k
  • 盲化因子是临时公钥 epk_k 和共享密钥 ss_k 之间的串联的 SHA256 哈希。
  • 通过将当前临时私钥 ek_k 乘以盲化因子来计算下一个跳 ek_{k+1} 的临时私钥。
  • 通过乘以基点从临时私钥 ek_{k+1} 导出下一个跳 epk_{k+1} 的临时公钥。

一旦发送者拥有上面所有必需的信息,就可以构建数据包。构建通过 r 跳路由的数据包需要 r 个 32 字节的临时公钥、r 个 32 字节的共享密钥、r 个 32 字节的盲化因子和 r 个可变长度的 hop_payload payload。 该构建返回一个 1366 字节的数据包以及第一个接收对等节点的地址。

数据包构建以与路由相反的顺序执行,即首先应用最后一个跳的操作。

数据包使用从 CSPRNG (ChaCha20) 派生的 1300 个 随机 字节初始化。上面引用的 pad 密钥用于从 ChaCha20 流中提取额外的随机字节,将其用作此目的的 CSPRNG。获得 paddingKey 后,ChaCha20 将与全零 nonce 一起使用,以生成 1300 个随机字节。这些随机字节然后用作要创建的混音头部的起始状态。

使用共享密钥生成填充(请参见 填充生成)。

对于路由中的每个跳(以相反的顺序),发送者应用以下操作:

  • 使用跳的共享密钥生成 rho-密钥和 mu-密钥。
  • shift_size 定义为 hop_payload 的长度加上长度的 bigsize 编码和该 HMAC 的长度。因此,如果 payload 长度为 l,则对于 l < 253shift_size1 + l + 32,否则由于 l 的 bigsize 编码,shift_size3 + l + 32
  • hop_payload 字段右移 shift_size 字节,丢弃超过其 1300 字节大小的最后 shift_size 字节。
  • bigsize 序列化的长度、序列化的 hop_payloadhmac 被复制到以下 shift_size 字节中。
  • rho-密钥用于生成 1300 字节的伪随机字节流,然后使用 XOR 应用于 hop_payloads 字段。
  • 如果这是最后一个跳,即第一次迭代,则 hop_payloads 字段的尾部将被路由信息 filler 覆盖。
  • 下一个 HMAC 是在串联的 hop_payloads 和关联数据上计算的(以 mu-密钥作为 HMAC 密钥)。

生成的最终 HMAC 值将是由路由中的第一个接收对等节点使用的 HMAC。

数据包生成返回一个序列化的数据包,其中包含 version 字节、第一个跳的临时公钥、第一个跳的 HMAC 和混淆的 hop_payloads

以下 Go 代码是数据包构建的示例实现:

func NewOnionPacket(paymentPath []*btcec.PublicKey, sessionKey *btcec.PrivateKey,
    hopsData []HopData, assocData []byte) (*OnionPacket, error) {

    numHops := len(paymentPath)
    hopSharedSecrets := make([][sha256.Size]byte, numHops)

    // Initialize ephemeral key for the first hop to the session key.
    var ephemeralKey big.Int
    ephemeralKey.Set(sessionKey.D)

    for i := 0; i < numHops; i++ {
        // Perform ECDH and hash the result.
        ecdhResult := scalarMult(paymentPath[i], ephemeralKey)
        hopSharedSecrets[i] = sha256.Sum256(ecdhResult.SerializeCompressed())

        // Derive ephemeral public key from private key.
        ephemeralPrivKey := btcec.PrivKeyFromBytes(btcec.S256(), ephemeralKey.Bytes())
        ephemeralPubKey := ephemeralPrivKey.PubKey()

        // Compute blinding factor.
        sha := sha256.New()
        sha.Write(ephemeralPubKey.SerializeCompressed())
        sha.Write(hopSharedSecrets[i])

        var blindingFactor big.Int
        blindingFactor.SetBytes(sha.Sum(nil))

        // Blind ephemeral key for next hop.
        ephemeralKey.Mul(&ephemeralKey, &blindingFactor)
        ephemeralKey.Mod(&ephemeralKey, btcec.S256().Params().N)
    }

    // Generate the padding, called "filler strings" in the paper.
    filler := generateHeaderPadding("rho", numHops, hopDataSize, hopSharedSecrets)

    // Allocate and initialize fields to zero-filled slices
    var mixHeader [routingInfoSize]byte
    var nextHmac [hmacSize]byte

        // Our starting packet needs to be filled out with random bytes, we
        // generate some determinstically using the session private key.
        paddingKey := generateKey("pad", sessionKey.Serialize()
        paddingBytes := generateCipherStream(paddingKey, routingInfoSize)
        copy(mixHeader[:], paddingBytes)

    // Compute the routing information for each hop along with a
    // MAC of the routing information using the shared key for that hop.
    for i := numHops - 1; i >= 0; i-- {
        rhoKey := generateKey("rho", hopSharedSecrets[i])
        muKey := generateKey("mu", hopSharedSecrets[i])

        hopsData[i].HMAC = nextHmac

        // Shift and obfuscate routing information
        streamBytes := generateCipherStream(rhoKey, numStreamBytes)

        rightShift(mixHeader[:], hopDataSize)
        buf := &bytes.Buffer{}
        hopsData[i].Encode(buf)
        copy(mixHeader[:], buf.Bytes())
        xor(mixHeader[:], mixHeader[:], streamBytes[:routingInfoSize])

        // These need to be overwritten, so every node generates a correct padding
        if i == numHops-1 {
            copy(mixHeader[len(mixHeader)-len(filler):], filler)
        }

packet := append(mixHeader[:], assocData...) nextHmac = calcMac(muKey, packet) }

packet := &OnionPacket{
    Version:      0x00,
    EphemeralKey: sessionKey.PubKey(),
    RoutingInfo:  mixHeader,
    HeaderMAC:    nextHmac,
}
return packet, nil

}


## 数据包转发

本规范仅限于 `version` `0` 的数据包;未来版本的结构可能会发生变化。

接收到数据包后,处理节点会将数据包的版本字节与其自身支持的版本进行比较,如果数据包指定的版本号不受支持,则中止连接。
对于具有受支持版本号的数据包,处理节点首先将数据包解析为各个字段。

接下来,处理节点使用与其自身公钥相对应的私钥和数据包中的临时密钥来计算共享密钥,如 [共享密钥](#shared-secret) 中所述。

上述要求可防止路由上的任何节点多次重试付款,试图通过流量分析来跟踪付款进度。请注意,可以使用先前共享密钥或 HMAC 的日志来禁用此类探测,一旦 HTLC 无论如何都不会被接受时(即在 `outgoing_cltv_value` 过去之后),就可以忘记这些日志。此类日志可以使用概率数据结构,但它必须根据需要限制提交速率,以约束此日志的最坏情况存储要求或误报。

接下来,处理节点使用共享密钥计算一个 **mu**-key,然后使用该密钥计算 `hop_payloads` 的 HMAC。然后将生成的 HMAC 与数据包的 HMAC 进行比较。

计算出的 HMAC 和数据包的 HMAC 的比较必须是时间恒定的,以避免信息泄漏。

此时,处理节点可以生成一个 **rho**-key。

然后对路由信息进行去混淆,并提取有关下一个跃点的信息。为此,处理节点复制 `hop_payloads` 字段,附加 1300 个 `0x00` 字节,生成 `2*1300` 个伪随机字节(使用 **rho**-key),然后使用 `XOR` 将结果应用于 `hop_payloads` 的副本。前几个字节对应于 `hop_payload` 的 bigsize 编码长度 `l`,然后是 `l` 字节的生成的路由信息成为 `hop_payload`,以及 32 字节的 HMAC。接下来的 1300 个字节是传出数据包的 `hop_payloads`。

32 个 `0x00` 字节的特殊 `hmac` 值表示当前处理的跃点是预期的接收者,并且不应转发数据包。

如果 HMAC 未指示路由终止,并且下一个跃点是处理节点的对等方;则会组装新数据包。数据包组装是通过使用处理节点的公钥以及共享密钥来盲化临时密钥,并通过序列化 `hop_payloads` 来完成的。然后将生成的数据包转发到寻址的对等方。

### 要求

处理节点:
  - 如果临时公钥不在 `secp256k1` 曲线:
    - 必须中止数据包处理。
    - 必须向原始节点报告路由失败。
  - 如果数据包先前已转发或在本地赎回,即数据包包含与先前接收的数据包重复的路由信息:
    - 如果知道 preimage:
      - 可以使用 preimage 立即赎回 HTLC。
    - 否则:
      - 必须中止处理并报告路由失败。
  - 如果计算出的 HMAC 与数据包的 HMAC 不同:
    - 必须中止处理。
    - 必须报告路由失败。
  - 如果 `realm` 未知:
    - 必须丢弃数据包。
    - 必须发出路由失败信号。
  - 必须将数据包寻址到另一个作为其直接邻居的对等方。
  - 如果处理节点没有具有匹配地址的对等方:
    - 必须丢弃数据包。
    - 必须发出路由失败信号。

## 填充生成

接收到数据包后,处理节点从路由信息和每跃点有效负载中提取目标为该节点的信息。
提取是通过解混淆和左移字段来完成的。
这将使每个跃点的字段更短,从而允许攻击者推断出路由长度。因此,在转发之前预先填充该字段。
由于填充是 HMAC 的一部分,因此原始节点必须预先生成相同的填充(每个跃点将生成的填充),以便为每个跃点正确计算 HMAC。
如果所选路由短于 1300 字节,则填充还用于填充字段长度。

在解混淆 `hop_payloads` 之前,处理节点用 1300 个 `0x00` 字节填充它,以使其总长度为 `2*1300`。
然后,它生成匹配长度的伪随机字节流,并使用 `XOR` 将其应用于 `hop_payloads`。
这会对其目标信息进行解混淆,同时对末尾添加的 `0x00` 字节进行混淆。

为了计算正确的 HMAC,原始节点必须为每个跃点预先生成 `hop_payloads`,包括每个跃点添加的递增混淆填充。此递增混淆填充称为 `filler`。

以下示例代码显示了在 Go 中如何生成填充:

```Go
func generateFiller(key string, numHops int, hopSize int, sharedSecrets [][sharedSecretSize]byte) []byte {
    fillerSize := uint((numMaxHops + 1) * hopSize)
    filler := make([]byte, fillerSize)

    // The last hop does not obfuscate, it's not forwarding anymore. // 最后一个跃点不会进行混淆,它不再进行转发。
    for i := 0; i < numHops-1; i++ {

        // Left-shift the field // 左移字段
        copy(filler[:], filler[hopSize:])

        // Zero-fill the last hop // 零填充最后一个跃点
        copy(filler[len(filler)-hopSize:], bytes.Repeat([]byte{0x00}, hopSize))

        // Generate pseudo-random byte stream // 生成伪随机字节流
        streamKey := generateKey(key, sharedSecrets[i])
        streamBytes := generateCipherStream(streamKey, fillerSize)

        // Obfuscate // 混淆
        xor(filler, filler, streamBytes)
    }

    // Cut filler down to the correct length (numHops+1)*hopSize // 将填充减少到正确的长度 (numHops+1)*hopSize
    // bytes will be prepended by the packet generation. // 字节将由数据包生成预先添加。
    return filler[(numMaxHops-numHops+2)*hopSize:]
}

请注意,此示例实现仅用于演示目的;filler 的生成效率要高得多。 最后一个跃点不需要混淆 filler,因为它不会进一步转发数据包,因此也不需要提取 HMAC。

返回错误

Onion 路由协议包括一个简单的机制,用于将加密的错误消息返回到原始节点。 返回的错误消息可能是任何跃点(包括最终节点)报告的失败。 正向数据包的格式不适用于返回路径,因为除了原始节点之外,没有其他跃点可以访问生成所需的信信息。 请注意,这些错误消息不可靠,因为它们没有放置在链上,因为存在跃点失败的可能性。

中间跃点存储来自正向路径的共享密钥,并在每个跃点期间重复使用它来混淆任何相应的返回数据包。 此外,每个节点在本地存储有关其自身在路由中的发送对等方的数据,因此它知道将任何最终返回数据包返回转发到哪里。 生成错误消息的节点(出错节点)构建一个由以下字段组成的返回数据包:

  1. 数据:
    • [32*byte:hmac]
    • [u16:failure_len]
    • [failure_len*byte:failuremsg]
    • [u16:pad_len]
    • [pad_len*byte:pad]

其中 hmac 是对数据包剩余部分进行身份验证的 HMAC,使用使用上述过程生成的密钥,密钥类型为 umfailuremsg 定义如下,pad 是用于隐藏长度的额外字节。

然后,出错节点使用密钥类型 ammag 生成一个新密钥。 然后,此密钥用于生成伪随机流,该伪随机流又使用 XOR 应用于数据包。

混淆步骤由返回路径上的每个跃点重复。 接收到返回数据包后,每个跃点都会生成其 ammag,生成伪随机字节流,并将结果应用于返回数据包,然后再进行返回转发。

原始节点能够检测到它是返回消息的预定最终接收者,因为当然,它是相应正向数据包的发起者。 当原始节点接收到与它发起的传输匹配的错误消息时(即,它无法进一步返回转发错误),它会为路由中的每个跃点生成 ammagum 密钥。 然后,它使用每个跃点的 ammag 密钥迭代解密错误消息,并使用每个跃点的 um 密钥计算 HMAC。 原始节点可以通过将 hmac 字段与计算出的 HMAC 进行匹配来检测错误消息的发送者。

正向和返回数据包之间的关联在此 onion 路由协议之外处理,例如,通过与支付通道中的 HTLC 的关联。

要求

出错节点

  • 应设置 pad,使 failure_len 加上 pad_len 等于 256。
    • 注意:此值比当前定义的最长消息长 118 字节。

原始节点

  • 一旦返回消息被解密:
    • 应存储消息的副本。
    • 应继续解密,直到循环重复 20 次。
    • 应使用恒定的 ammagum 密钥来混淆路由长度。

失败消息

failuremsg 中封装的失败消息具有与正常消息相同的格式:一个 2 字节类型 failure_code,后跟适用于该类型的数据。以下是当前支持的 failure_code 值的列表,后跟它们的使用案例要求。

请注意,failure_code 与其他 BOLT 中定义的其他消息类型不是同一类型,因为它们不是直接在传输层上发送的,而是包装在返回数据包中。因此,failure_code 的数值可以重用也分配给其他消息类型的值,而不会有引起冲突的任何危险。

failure_code 的高字节可以读作一组标志:

  • 0x8000 (BADONION): 由发送对等方加密的无法解析的 onion
  • 0x4000 (PERM): 永久性失败(否则为瞬时性)
  • 0x2000 (NODE): 节点失败(否则为通道)
  • 0x1000 (UPDATE): 包含新的通道更新

请注意,channel_update 字段在 failure_code 包含 UPDATE 标志的消息中是强制性的。

定义了以下 failure_code

  1. 类型:PERM|1 (invalid_realm)

处理节点不理解 realm 字节。

  1. 类型:NODE|2 (temporary_node_failure)

处理节点的一般临时故障。

  1. 类型:PERM|NODE|2 (permanent_node_failure)

处理节点的一般永久故障。

  1. 类型:PERM|NODE|3 (required_node_feature_missing)

处理节点具有此 onion 中没有的所需功能。

  1. 类型:BADONION|PERM|4 (invalid_onion_version)
  2. 数据:
    • [sha256:sha256_of_onion]

处理节点不理解 version 字节。

  1. 类型:BADONION|PERM|5 (invalid_onion_hmac)
  2. 数据:
    • [sha256:sha256_of_onion]

onion 的 HMAC 在到达处理节点时是不正确的。

  1. 类型:BADONION|PERM|6 (invalid_onion_key)
  2. 数据:
    • [sha256:sha256_of_onion]

处理节点无法解析临时密钥。

  1. 类型:UPDATE|7 (temporary_channel_failure)
  2. 数据:
    • [u16:len]
    • [len*byte:channel_update]

来自处理节点的通道无法处理此 HTLC,但可能能够在以后处理它或其他 HTLC。

  1. 类型:PERM|8 (permanent_channel_failure)

来自处理节点的通道无法处理任何 HTLC。

  1. 类型:PERM|9 (required_channel_feature_missing)

来自处理节点的通道需要 onion 中不存在的功能。

  1. 类型:PERM|10 (unknown_next_peer)

onion 指定了一个 short_channel_id,它与来自处理节点的任何通道都不匹配。

  1. 类型:UPDATE|11 (amount_below_minimum)
  2. 数据:
    • [u64:htlc_msat]
    • [u16:len]
    • [len*byte:channel_update]

HTLC 金额低于来自处理节点的通道的 htlc_minimum_msat

  1. 类型:UPDATE|12 (fee_insufficient)
  2. 数据:
    • [u64:htlc_msat]
    • [u16:len]
    • [len*byte:channel_update]

费用金额低于处理节点的通道要求的金额。

  1. 类型:UPDATE|13 (incorrect_cltv_expiry)
  2. 数据:
    • [u32:cltv_expiry]
    • [u16:len]
    • [len*byte:channel_update]

cltv_expiry 不符合处理节点的通道要求的 cltv_expiry_delta:它不满足以下要求:

    cltv_expiry - cltv_expiry_delta >= outgoing_cltv_value
  1. 类型:UPDATE|14 (expiry_too_soon)
  2. 数据:
    • [u16:len]
    • [len*byte:channel_update]

CLTV 到期时间太接近当前区块高度,无法由处理节点安全处理。

  1. 类型:PERM|15 (incorrect_or_unknown_payment_details)
  2. 数据:
    • [u64:htlc_msat]
    • [u32:height]

最终节点不知道 payment_hashpayment_secretpayment_hash 不匹配,该 payment_hash 的金额不正确,htlc 的 CLTV 到期时间太接近当前区块高度,无法安全处理,或者 payment_metadata 不存在,而它应该存在。

htlc_msat 参数是多余的,但为了向后兼容而保留。htlc_msat 的值始终与最终跃点 onion 有效负载中指定的金额匹配。因此,它对发送者没有任何信息价值。发送不同金额或 htlc 到期时间的倒数第二个跃点通过 final_incorrect_cltv_expiryfinal_incorrect_htlc_amount 处理。

height 参数由最终节点设置为接收 htlc 时已知的最佳区块高度。发送者可以使用它来区分发送具有错误最终 CLTV 到期时间的付款与中间跃点延迟付款,因此不再满足接收者的发票 CLTV delta 要求。

注意:最初 PERM|16 (incorrect_payment_amount) 和 17 (final_expiry_too_soon) 用于区分不正确的 htlc 参数与未知的支付哈希。遗憾的是,发送此响应允许进行探测攻击,即接收用于转发 HTLC 的节点可以通过向潜在目的地发送具有相同哈希但值或到期高度低得多的付款并检查响应来检查对其最终目的地的猜测。实施必须小心区分以前非永久性的 final_expiry_too_soon (17) 情况与现在由 incorrect_or_unknown_payment_details (PERM|15) 表示的其他永久性故障。

  1. 类型:18 (final_incorrect_cltv_expiry)
  2. 数据:
    • [u32:cltv_expiry]

HTLC 中的 CLTV 到期时间与 onion 中的值不匹配。

  1. 类型:19 (final_incorrect_htlc_amount)
  2. 数据:
    • [u64:incoming_htlc_amt]

HTLC 中的金额与 onion 中的值不匹配。

  1. 类型:UPDATE|20 (channel_disabled)
  2. 数据:
    • [u16:flags]
    • [u16:len]
    • [len*byte:channel_update]

来自处理节点的通道已被禁用。

  1. 类型:21 (expiry_too_far)

HTLC 中的 CLTV 到期时间太远。

  1. 类型:PERM|22 (invalid_onion_payload)
  2. 数据:
    • [bigsize:type]
    • [u16:offset]

处理节点不理解已解密的 onion 每跃点有效负载,或者该有效负载不完整。如果失败可以缩小到有效负载中的特定 tlv 类型,则出错节点可以在解密的字节流中包含该 type 及其字节 offset

  1. 类型:23 (mpp_timeout)

未在合理的时间内收到多部分付款的全部金额。

要求

出错节点

  • 在创建错误消息时,必须选择上述错误代码之一。
  • 必须包含该特定错误类型的相应数据。
  • 如果存在多个错误:
    • 应从上面的列表中选择它遇到的第一个错误。

任何 出错节点 可以:

  • 如果 realm 字节未知:
    • 返回 invalid_realm 错误。
  • 如果 onion 中的每跃点有效负载无效(例如,它不是有效的 tlv 流) 或者缺少所需信息(例如,未指定金额):
    • 返回 invalid_onion_payload 错误。
  • 如果整个节点发生其他未指定的瞬态错误:
    • 返回 temporary_node_failure 错误。
  • 如果整个节点发生其他未指定的永久性错误:
    • 返回 permanent_node_failure 错误。
  • 如果节点在其 node_announcement features 中声明了要求, 但这些要求未包含在 onion 中:
    • 返回 required_node_feature_missing 错误。

转发节点 可以,但 最终节点 不得:

  • 如果 onion version 字节未知:
    • 返回 invalid_onion_version 错误。
  • 如果 onion HMAC 不正确:
    • 返回 invalid_onion_hmac 错误。
  • 如果 onion 中的临时密钥无法解析:
    • 返回 invalid_onion_key 错误。
  • 如果在转发到其接收对等方的过程中,传出通道中发生其他未指定的瞬态错误(例如,达到通道容量、 飞行中的 HTLC 过多等):
    • 返回 temporary_channel_failure 错误。
  • 如果在转发到其接收对等方的过程中发生其他未指定的永久性错误(例如,通道最近已关闭):
    • 返回 permanent_channel_failure 错误。
  • 如果传出通道在其 channel_announcementfeatures 中声明了要求, 但这些要求未包含在 onion 中:
    • 返回 required_channel_feature_missing 错误。
  • 如果未知 onion 指定的接收对等方:
    • 返回 unknown_next_peer 错误。
  • 如果 HTLC 金额小于当前指定的最小金额:
    • 报告传出 HTLC 的金额和传出通道的当前通道设置。
    • 返回 amount_below_minimum 错误。
  • 如果 HTLC 未支付足够的费用:
    • 报告传入 HTLC 的金额和传出通道的当前通道设置。
    • 返回 fee_insufficient 错误。
    • 如果传入 cltv_expiry 减去 outgoing_cltv_value 低于 传出通道的 cltv_expiry_delta
    • 报告传出 HTLC 的 cltv_expiry 和传出通道的当前通道设置。
    • 返回 incorrect_cltv_expiry 错误。
  • 如果 cltv_expiry 非常接近当前时间:
    • 报告传出通道的当前通道设置。
    • 返回 expiry_too_soon 错误。
  • 如果 cltv_expiry 在未来非常遥远:
    • 返回 expiry_too_far 错误。
  • 如果通道被禁用:
    • 报告传出通道的当前通道设置。
    • 返回 channel_disabled 错误。

中间跃点 不得,但 最终节点 必须:

  • 如果支付哈希已支付:
    • 可以将支付哈希视为未知。
    • 可以成功接受 HTLC。
  • 如果 payment_secret 与该 payment_hash 的预期值不匹配, 或者需要 payment_secret 但不存在:
    • 必须使 HTLC 失败。
    • 必须返回 incorrect_or_unknown_payment_details 错误。
  • 如果支付的金额小于预期金额:
    • 必须使 HTLC 失败。
    • 必须返回 incorrect_or_unknown_payment_details 错误。
  • 如果支付哈希未知:
    • 必须使 HTLC 失败。
    • 必须返回 incorrect_or_unknown_payment_details 错误。
  • 如果支付的金额是预期金额的两倍以上:
    • 应使 HTLC 失败。
    • 应返回 incorrect_or_unknown_payment_details 错误。
      • 注意:这允许原始节点通过更改金额来减少信息泄漏,同时不允许意外的重大超额支付。
  • 如果 cltv_expiry 值非常接近现在:
    • 必须使 HTLC 失败。
    • 必须返回 incorrect_or_unknown_payment_details 错误。
  • 如果 outgoing_cltv_value 与 最终节点的 HTLC 中的 cltv_expiry 不对应:
    • 必须返回 final_incorrect_cltv_expiry 错误。
  • 如果 amt_to_forward 与来自 最终节点的 HTLC 的 incoming_htlc_amt 不对应:
    • 必须返回 final_incorrect_htlc_amount 错误。

接收失败代码

要求

原始节点

  • 必须忽略 failuremsg 中的任何额外字节。
  • 如果 最终节点 返回错误:
    • 如果设置了 PERM 位:
      • 应使付款失败。
    • 否则:
      • 如果错误代码被理解并且有效:
      • 可以重试付款。特别是,如果自发送以来区块高度已更改,则可能发生 final_expiry_too_soon,在这种情况下,temporary_node_failure 可能会在几秒钟内解决。
  • 否则,中间跃点 返回错误:
    • 如果设置了 NODE 位:
      • 应从考虑中删除与出错节点连接的所有通道。
    • 如果未设置 PERM 位:
      • 应在收到新的 channel_update 时还原通道。
    • 否则:
      • 如果设置了 UPDATE,并且 channel_update 有效且比用于发送付款的 channel_update 更新:
      • 如果 channel_update 不应导致失败:
        • 可以将 channel_update 视为无效。
      • 否则:
        • 应应用 channel_update
      • 可以将 channel_update 排队以进行广播。
      • 否则:
      • 应从考虑中消除从出错节点发出的通道。
      • 如果未设置 PERM 位:
        • 应在收到新的 channel_update 时还原通道。
    • 然后应重试路由并发送付款。
  • 可以使用各种失败类型中指定的数据进行调试。

测试向量

返回错误

测试向量使用以下参数:

pubkey[0] = 0x02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619
pubkey[1] = 0x0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c
pubkey[2] = 0x027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007
pubkey[3] = 0x032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991
pubkey[4] = 0x02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145

nhops = 5/20
sessionkey = 0x4141414141414141414141414141414141414141414141414141414141414141
associated data = 0x4242424242424242424242424242424242424242424242424242424242424242

以下是错误消息创建示例的深入跟踪:

failure_message = 2002
# 创建错误消息
shared_secret = b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328
payload = 0002200200fe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
um_key = 4da7f2923edce6c2d85987d1d9fa6d88023e6c3a9c3d20f07d3b10b61a78d646
raw_error_packet = 4c2fc8bc08510334b6833ad9c3e79cd1b52ae59dfe5c2a4b23ead50f09f7ee0b0002200200fe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
# 转发错误数据包
shared_secret = b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328
 ammag_key = 2f36bb8822e1f0d04c27b7d8bb7d7dd586e032a3218b8d414afbba6f169a4d68
stream = e9c975b07c9a374ba64fd9be3aae955e917d34d1fa33f2e90f53bbf4394713c6a8c9b16ab5f12fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4
error packet for node 4: a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4
# 转发错误数据包
shared_secret = 21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d
ammag_key = cd9ac0e09064f039fa43a31dea05f5fe5f6443d40a98be4071af4a9d704be5ad
stream = 617ca1e4624bc3f04fece3aa5a2b615110f421ec62408d16c48ea6c1b7c33fe7084a2bd9d4652fc5068e5052bf6d0acae2176018a3d8c75f37842712913900263cff92f39f3c18aa1f4b20a93e70fc429af7b2b1967ca81a761d40582daf0eb49cef66e3d6fbca0218d3022d32e994b41c884a27c28685ef1eb14603ea80a204b2f2f474b6ad5e71c6389843e3611ebeafc62390b717ca53b3670a33c517ef28a659c251d648bf4c966a4ef187113ec9848bf110816061ca4f2f68e76ceb88bd6208376460b916fb2ddeb77a65e8f88b2e71a2cbf4ea4958041d71c17d05680c051c3676fb0dc8108e5d78fb1e2c44d79a202e9d14071d536371ad47c39a05159e8d6c41d17a1e858faaaf572623aa23a38ffc73a4114cb1ab1cd7f906c6bd4e21b29694
error packet for node 3: c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270
# 转发错误数据包
shared_secret = 3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc
ammag_key = 1bf08df8628d452141d56adfd1b25c1530d7921c23cecfc749ac03a9b694b0d3
stream = 6149f48b5a7e8f3d6f5d870b7a698e204cf64452aab4484ff1dee671fe63fd4b5f1b78ee2047dfa61e3d576b149bedaf83058f85f06a3172a3223ad6c4732d96b32955da7d2feb4140e58d86fc0f2eb5d9d1878e6f8a7f65ab9212030e8e915573ebbd7f35e1a430890be7e67c3fb4bbf2def662fa625421e7b411c29ebe81ec67b77355596b05cc155755664e59c16e21410aabe53e80404a615f44ebb31b365ca77a6e91241667b26c6cad24fb2324cf64e8b9dd6e2ce65f1f098cfd1ef41ba2d4c7def0ff165a0e7c84e7597c40e3dffe97d417c144545a0e38ee33ebaae12cc0c14650e453d46bfc48c0514f354773435ee89b7b2810606eb73262c77a1d67f3633705178d79a1078c3a01b5fadc9651feb63603d19decd3a00c1f69af2dab259593
error packet for node 2: a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3
# 转发错误数据包
shared_secret = a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae
ammag_key = 59ee5867c5c151daa31e36ee42530f429c433836286e63744f2020b980302564
stream = 0f10c86f05968dd91188b998ee45dcddfbf89fe9a99aa6375c42ed5520a257e048456fe417c15219ce39d921555956ae2ff795177c63c819233f3bcb9b8b28e5ac6e33a3f9b87ca62dff43f4cc4a2755830a3b7e98c326b278e2bd31f4a9973ee99121c62873f5bfb2d159d3d48c5851e3b341f9f6634f51939188c3b9ff45feeb11160bb39ce3332168b8e744a92107db575ace7866e4b8f390f1edc4acd726ed106555900a0832575c3a7ad11bb1fe388ff32b99bcf2a0d0767a83cf293a220a983ad014d404bfa20022d8b369fe06f7ecc9c74751dcda0ff39d8bca74bf9956745ba4e5d299e0da8f68a9f660040beac03e795a046640cf8271307a8b64780b0588422f5a60ed7e36d60417562938b400802dac5f87f267204b6d5bcfd8a05b221ec2
error packet for node 1: aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921
# 转发错误数据包
shared_secret = 53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66
ammag_key = 3761ba4d3e726d8abb16cba5950ee976b84937b61b7ad09e741724d7dee12eb5
stream = 3699fd352a948a05f604763c0bca2968d5eaca2b0118602e52e59121f050936c8dd90c24df7dc8cf8f1665e39a6c75e9e2c0900ea245c9ed3b0008148e0ae18bbfaea0c711d67eade980c6f5452e91a06b070bbde68b5494a92575c114660fb53cf04bf686e67ffa4a0f5ae41a59a39a8515cb686db553d25e71e7a97cc2febcac55df2711b6209c502b2f8827b13d3ad2f491c45a0cafe7b4d8d8810e805dee25d676ce92e0619b9c206f922132d806138713a8f69589c18c3fdc5acee41c1234b17ecab96b8c56a46787bba2c062468a13919afc18513835b472a79b2c35f9a91f38eb3b9e998b1000cc4a0dbd62ac1a5cc8102e373526d7e8f3c3a1b4bfb2f8a3947fe350cb89f73aa1bb054edfa9895c0fc971c2b5056dc8665902b51fced6dff80c
error packet for node 0: 9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d

## References

[sphinx]: http://www.cypherpunks.ca/~iang/pubs/Sphinx_Oakland09.pdf
[RFC2104]: https://tools.ietf.org/html/rfc2104
[fips198]: http://csrc.nist.gov/publications/fips/fips198-1/FIPS-198-1_final.pdf
[sec2]: http://www.secg.org/sec2-v2.pdf
[rfc8439]: https://tools.ietf.org/html/rfc8439

## Authors

[ FIXME: ]

![Creative Commons License](https://i.creativecommons.org/l/by/4.0/88x31.png "License CC-BY")
<br>
This work is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).

>- 原文链接: [github.com/lightning/bol...](https://github.com/lightning/bolts/blob/2e8f2095a36afb9de38da0f3f0051c7dc16dfc36//04-onion-routing.md)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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