BOLT 4:洋葱路由协议

  • lightning
  • 发布于 2025-04-17 13:13
  • 阅读 15

本文档描述了v0版本的 Oninon 路由协议,该协议构建了一种 Onion 路由数据包,用于将付款从起始节点路由到最终节点,数据包通过多个中间节点(称为跃点)进行路由。消息在每个Hop上都会混淆,以确保网络级别的攻击者无法将属于同一路由的数据包关联起来。该路由由始发节点构建,该节点知道每个中间节点和最终节点的公钥,并使用 ECDH 算法为每个中间节点和最终节点创建共享密钥。

BOLT #4:洋葱路由协议

概述

本文档描述了洋葱路由数据包的构建,该数据包用于将付款从源节点路由到最终节点。该数据包通过多个中间节点进行路由,这些节点称为跳(hop)

路由方案基于 [Sphinx][sphinx] 结构,并扩展了每个跳的 payload。

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

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

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

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

节点:

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

目录

约定

本文档中遵守了许多约定:

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

密钥生成

许多加密和验证密钥是从共享密钥派生的:

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

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

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

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

伪随机字节流

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

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

数据包结构

数据包由四个部分组成:

  • 一个 version 字节
  • 一个 33 字节的压缩 secp256k1 public_key,用于在共享密钥生成期间使用
  • 一个 1300 字节的 hop_payloads,由多个可变长度的 hop_payload payload 组成
  • 一个 32 字节的 hmac,用于验证数据包的完整性

数据包的网络格式包括将各个部分序列化为一个连续的字节流,然后将其传输给数据包接收者。由于数据包的大小固定,因此在连接上传输时无需在其前面加上其长度。

数据包的总体结构如下:

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

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

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

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

其中,lengthpayloadhmac 对于每个跳重复;并且其中 filler 由混淆的、确定性生成的填充组成,如 填充生成 中所述。 此外,hop_payloads 在每个跳中都会逐渐混淆。

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

使用这种端到端身份验证,每个跳都可以将 HTLC 参数与 payload 的指定值进行交叉检查,并确保发送对等节点没有转发错误制作的 HTLC。

由于没有任何 payload TLV 值可以短于 2 个字节,因此保留了长度值为 0 和 1。(0 表示不再支持的旧格式,1 保留供将来使用)。

payload 格式

这根据 BOLT #1 中定义的类型-长度-值格式进行格式化。

1. `tlv_stream`: `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. 类型: 10 (`encrypted_recipient_data`)
    2. 数据:
        * [`...*byte`:`encrypted_recipient_data`]
    1. 类型: 12 (`current_path_key`)
    2. 数据:
        * [`point`:`path_key`]
    1. 类型: 16 (`payment_metadata`)
    2. 数据:
        * [`...*byte`:`payment_metadata`]
    1. 类型: 18 (`total_amount_msat`)
    2. 数据:
        * [`tu64`:`total_msat`]

short_channel_id 是用于路由消息的出站通道的 ID;接收对等节点应在该通道的另一端操作。

amt_to_forward 是要转发到路由信息中指定的下一个接收对等节点或最终目的地的金额,以毫聪(millisatoshi)为单位。

对于非最终节点,这包括源节点为接收对等节点计算的费用,该费用根据接收对等节点公布的费用方案计算(如 BOLT #7 中所述)。

outgoing_cltv_value 是携带数据包的出站 HTLC 应具有的 CLTV 值。 包含此字段允许跳验证源节点指定的信息以及转发的 HTLC 的参数,并确保源节点使用当前的 cltv_expiry_delta 值。

如果这些值不对应,则表明转发节点已篡改了预期的 HTLC 值,或者源节点具有过时的 cltv_expiry_delta 值。

这些要求确保了在响应意外的 outgoing_cltv_value 时的一致性,无论它是最终节点还是非最终节点,以避免泄漏其在路由中的位置。

要求

encrypted_recipient_data 的创建者(通常,付款的接收者):

  • 必须为盲化路由中的每个节点(包括其自身)创建 encrypted_data_tlv
  • 必须为每个非最终节点包含 encrypted_data_tlv.payment_relay
  • 必须为每个非最终节点包含恰好一个 encrypted_data_tlv.short_channel_idencrypted_data_tlv.next_node_id
  • 必须为每个非最终节点设置 encrypted_data_tlv.payment_constraints,并且可以为最终节点设置:
    • max_cltv_expiry 设置为允许使用该路由的最大区块高度,从最终节点选择的该路由应过期的 max_cltv_expiry 高度开始,加上最终节点的 min_final_cltv_expiry_delta,然后加上每个跳的 encrypted_data_tlv.payment_relay.cltv_expiry_delta
    • htlc_minimum_msat 设置为节点将允许的最大最小 HTLC 值。
  • 如果它设置 encrypted_data_tlv.allowed_features
    • 必须将其设置为空数组。
  • 必须按如下方式计算路由的总费用和 CLTV delta,并将其传达给发送者:
    • total_fee_base_msat(n+1) = (fee_base_msat(n+1) * 1000000 + total_fee_base_msat(n) * (1000000 + fee_proportional_millionths(n+1)) + 1000000 - 1) / 1000000
    • total_fee_proportional_millionths(n+1) = ((total_fee_proportional_millionths(n) + fee_proportional_millionths(n+1)) * 1000000 + total_fee_proportional_millionths(n) * fee_proportional_millionths(n+1) + 1000000 - 1) / 1000000
    • total_cltv_delta = cltv_delta(0) + cltv_delta(1) + ... + cltv_delta(n) + min_final_cltv_expiry_delta
  • 必须按照 路由盲化 中的要求从 encrypted_data_tlv 创建 encrypted_recipient_data

TLV payload 的写入者:

  • 对于盲化路由中的每个节点:
    • 必须包含接收者提供的 encrypted_recipient_data
    • 对于盲化路由中的第一个节点:
      • 必须在 current_path_key 中包含接收者提供的 path_key
    • 如果它是最终节点:
      • 必须包含 amt_to_forwardoutgoing_cltv_valuetotal_amount_msat
      • outgoing_cltv_value 设置的值:
      • 必须使用当前区块高度作为基线值。
      • 如果添加了随机偏移量以提高隐私性:
        • 应该将偏移量添加到基线值。
    • 不得包含任何其他 tlv 字段。
  • 对于盲化路由之外的每个节点:
    • 必须包含 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
      • 除了固定洋葱大小所暗示的限制之外,不得对 payment_metadata 的大小施加任何限制

读取者:

  • 如果存在 encrypted_recipient_data
    • 如果在传入的 update_add_htlc 中设置了 path_key
      • 如果存在 current_path_key,则必须返回错误。
      • 必须将该 path_key 用作解密的 path_key
    • 否则:
      • 如果 current_path_key 不存在,则必须返回错误。
      • 必须将该 current_path_key 用作解密的 path_key
      • 应该在返回错误之前添加随机延迟。
    • 如果 encrypted_recipient_data 未使用 path_key(如 路由盲化 中所述)解密,则必须返回错误。
    • 如果存在 payment_constraints
      • 如果发生以下情况,则必须返回错误:
      • expiry 大于 encrypted_recipient_data.payment_constraints.max_cltv_expiry
      • 金额低于 encrypted_recipient_data.payment_constraints.htlc_minimum_msat
    • 如果缺少 allowed_features
      • 必须按照存在并包含空数组的方式处理消息。
    • 如果发生以下情况,则必须返回错误:
      • encrypted_recipient_data.allowed_features.features 包含未知的 feature bit(即使它是奇数)。
      • encrypted_recipient_data 同时包含 short_channel_idnext_node_id
      • 付款使用 encrypted_recipient_data.allowed_features.features 中未包含的 feature。
    • 如果它不是最终节点:
      • 如果 payload 包含 encrypted_recipient_datacurrent_path_key 以外的其他 tlv 字段,则必须返回错误。
      • 如果 encrypted_recipient_data 不包含 short_channel_idnext_node_id 中的任何一个,则必须返回错误。
      • 如果 encrypted_recipient_data 不包含 payment_relay,则必须返回错误。
      • 必须使用 encrypted_recipient_data.payment_relay 中的值来计算 amt_to_forwardoutgoing_cltv_value,如下所示:
      • amt_to_forward = ((amount_msat - fee_base_msat) * 1000000 + 1000000 + fee_proportional_millionths - 1) / (1000000 + fee_proportional_millionths)
      • outgoing_cltv_value = cltv_expiry - payment_relay.cltv_expiry_delta
    • 如果它是最终节点:
      • 如果 payload 包含除 encrypted_recipient_datacurrent_path_keyamt_to_forwardoutgoing_cltv_valuetotal_amount_msat 以外的其他 tlv 字段,则必须返回错误。
      • 如果 amt_to_forwardoutgoing_cltv_valuetotal_amount_msat 不存在,则必须返回错误。
      • 如果 amt_to_forward 低于它期望的付款金额,则必须返回错误。
      • 如果传入的 cltv_expiry < outgoing_cltv_value,则必须返回错误。
      • 如果传入的 cltv_expiry < current_block_height + min_final_cltv_expiry_delta,则必须返回错误。
  • 否则(它不是盲化路由的一部分):
    • 如果在传入的 update_add_htlc 中设置了 path_key 或存在 current_path_key,则必须返回错误。
    • 如果 amt_to_forwardoutgoing_cltv_value 不存在,则必须返回错误。
    • 如果它不是最终节点:
      • 如果发生以下情况,则必须返回错误:
      • short_channel_id 不存在,
      • 它无法将 HTLC 转发到通道 short_channel_id 指示的对等节点。
      • 传入的 amount_msat - fee < amt_to_forward(其中 feeBOLT #7 中描述的公布费用)
      • cltv_expiry - cltv_expiry_delta < outgoing_cltv_value
  • 如果它是最终节点:
    • 如果 total_msat 不存在,则必须将 total_msat 视为等于 amt_to_forward
    • 如果发生以下情况,则必须返回错误:
      • 传入的 amount_msat < amt_to_forward
      • 传入的 cltv_expiry < outgoing_cltv_value
      • 传入的 cltv_expiry < current_block_height + min_final_cltv_expiry_delta

有关多部分付款的其他要求在此处指定。

基本多部分付款

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

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

请注意,有两个不同的 tlv 字段可用于传输 total_msat。最后一个 total_amount_msat 是随着盲化路径引入的,对于盲化路径来说,payment_secret 没有意义。

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

要求

写者:

  • 如果 invoice 提供了 basic_mpp feature:
    • 可以发送多个 HTLC 来支付 invoice。
    • 必须在集合中的所有 HTLC 上使用相同的 payment_hash
    • 应该在大致相同的时间发送所有付款。
    • 应该尝试为每个 HTLC 使用不同的接收者路径。
    • 应该重试和/或重新划分失败的 HTLC。
    • 如果 invoice 指定了 amount
      • 必须将 total_msat 设置为至少该 amount,且小于或等于 amount 的两倍。
    • 否则:
      • 必须将 total_msat 设置为其希望支付的金额。
    • 必须确保到达收款人的 HTLC 集合的总 amt_to_forward 等于或大于 total_msat
    • 如果 HTLC 集合的总 amt_to_forward 已经大于或等于 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 集合中。
    • 如果 total_msat 对于集合中的所有 HTLC 都不相同,则应该使整个 HTLC 集合失败。
    • 如果此 HTLC 集合的总 amt_to_forward 等于或大于 total_msat
      • 应该履行 HTLC 集合中的所有 HTLC
    • 否则,如果此 HTLC 集合的总 amt_to_forward 小于 total_msat
      • 不得履行 HTLC 集合中的任何 HTLC
      • 在一个合理的超时后,必须使 HTLC 集合中的所有 HTLC 失败。
      • 应该在初始 HTLC 之后至少等待 60 秒。
      • 应该对失败消息使用 mpp_timeout
      • 必须要求集合中的所有 HTLC 都具有 payment_secret
    • 如果它履行了 HTLC 集合中的任何 HTLC:
      • 必须履行整个 HTLC 集合。
基本原理

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

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

因为一个节点可能需要支付超过其所需金额(由于所需路径中通道的 htlc_minimum_msat 值所致),因此允许节点支付超过其指定的 total_msat。否则,节点在沿着特定路径重试付款时将受到可以采取的路径的约束。但是,任何单个 HTLC 的金额都不得小于已付总额与 total_msat 之间的差额。

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

实现可以选择不履行其他方面满足金额标准的 HTLC 集合(例如,某些其他失败或 invoice 超时),但是如果它仅履行其中一些 HTLC,则中间节点可以简单地认领剩余的部分。

路由盲化

1. 子类型: `blinded_path`
2. 数据:
   * [`sciddir_or_pubkey`:`first_node_id`]
   * [`point`:`first_path_key`]
   * [`byte`:`num_hops`]
   * [`num_hops*blinded_path_hop`:`path`]
1. 子类型: `blinded_path_hop`
2. 数据:
    * [`point`:`blinded_node_id`]
    * [`u16`:`enclen`]
    * [`enclen*byte`:`encrypted_recipient_data`]

盲化路径包括:

  1. 一个初始引入点 (first_node_id)
  2. 一个与第一个 node_id (first_path_key) 共享密钥的初始密钥
  3. 一系列调整后的节点 ID (path.blinded_node_id)
  4. 一系列加密到节点的二进制 blob (path.encrypted_recipient_data) 告诉他们下一个跳。

例如,Dave 希望 Alice 通过公共节点 Bob,然后是 Carol 联系他。他创建了一个 Bob、Carol 最终是他自己的公钥链(“path_keys”),以便他可以与他们每个人共享一个秘密。这些密钥是一个简单的链,因此每个节点都可以派生出下一个 path_key,而无需明确告知。

从这些共享秘密中,Dave 创建并加密了三个 encrypted_data_tlv

  1. encrypted_data_bob:用于告诉 Bob 转发给 Carol
  2. encrypted_data_carol:用于告诉 Carol 转发给他
  3. encrypted_data_dave:用于他自己表明使用了该路径,以及他想要的任何元数据。

为了掩盖节点 ID,他还从共享秘密中派生出三个致盲因子,这些致盲因子将 Bob 变成 Bob',Carol 变成 Carol',Dave 变成 Dave'。

所以这是他交给 Alice 的 blinded_path

  1. first_node_id:Bob
  2. first_path_key:Bob 的第一个 path key
  3. path:[Bob',encrypted_data_bob],[Carol',encrypted_data_carol],[Dave',encrypted_data_dave]

Alice 有两种不同的方式来构建一个到达 Bob 的 onion(因为他可能不是她的直接对等节点),这两种方式在下面的要求中描述。

但是在 Bob 之后,路径始终相同:他会将他派生的 path_key 连同 onion 一起发送给 Carol。她将使用 path_key 来派生 onion 的调整(Alice 为 Carol' 而不是 Carol 加密),以便她可以解密它,并且还可以派生密钥来解密 encrypted_data_tlv,这将告诉她转发给 Dave(以及 Dave 指定的可能附加限制)。

要求

请注意,盲化路径的创建者(即接收者)正在为发送者创建它以用来创建一个 onion,并为中间节点读取指令,因此这里有两个读取者部分。

blinded_path 的写入者:

  • 必须创建一条可行的到达自身的路径 ($N_r$),即 $N_0 \rightarrow N_1 \rightarrow ... \rightarrow N_r$。
  • 必须将 first_node_id 设置为 $N_0$
  • 必须使用以下算法为路由中的每个节点创建一系列 ECDH 共享秘密:
    • $e_0 \leftarrow \{0;1\}^{256}$($e_0$ 应该通过 CSPRNG 获得)
    • $E_0 = e_0 \cdot G$
    • 对于路由中的每个节点:
    • 令 $N_i = k_i * G$ 为 node_id($k_i$ 是 $N_i$ 的私钥)
    • $ss_i = SHA256(e_i * N_i) = SHA256(k_i * E_i)$(只有 $N_r$ 和 $N_i$ 知道的 ECDH 共享秘密)
    • $rho_i = HMAC256(\text{"rho"}, ss_i)$($N_r$ 用于加密 $N_i$ 的 encrypted_recipient_data 的密钥)
    • $e_{i+1} = SHA256(E_i || ss_i) * e_i$(只有 $N_r$ 知道的临时私有路径密钥)
    • $E_{i+1} = SHA256(E_i || ss_i) * E_i$ (path_key。注意:$N_i$ 不得学习 $e_i$)
  • 必须将 first_path_key 设置为 $E_0$
  • 必须使用以下算法为每个节点创建一系列盲化节点 ID $B_i$:
    • $B_i = HMAC256(\text{"blinded\_node\_id"}, ss_i) * N_i$($N_i$ 的盲化 node_id,只有 $N_i$ 知道私钥)
    • 必须将 path 中每个 blinded_path_hopblinded_node_id 设置为 $B_i$
  • 可以用不同的值替换 $E_{i+1}$,但是如果这样做:
    • 必须将 encrypted_data_tlv[i].next_path_key_override 设置为 $E_{i+1}$
  • 可以在 encrypted_data_tlv[r].path_id 中存储私有数据,以验证该路由是否在正确的上下文中使用并由他们创建
  • 应该添加填充数据以确保所有 encrypted_data_tlv[i] 具有相同的长度
  • 必须使用 ChaCha20-Poly1305,使用相应的 $rho_i$ 密钥和一个全零 nonce 加密每个 encrypted_data_tlv[i],以产生 encrypted_recipient_data[i]
  • 可以在路径的末尾添加其他“虚拟”跳(它们将在接收时被忽略)以模糊路径长度。

blinded_path 的读取者:

  • 必须在其自己的 onion payload 前面加上内容以到达 first_node_id
  • 必须在 path 中每个 onion payload 中包含相应的 encrypted_recipient_data
  • 对于 path 中的第一个条目:
    • 如果它正在发送付款:
    • 应该创建一个未盲化的 onion 付款到 first_node_id,并将 first_path_key 作为 current_path_key 包含在内。
    • 否则:
    • 必须将第一个盲化路径 onion blinded_node_id 加密。
    • 必须将先前 onion payload 中的 next_path_key_override 设置为 first_path_key
  • 对于 path 中的每个后续条目:
    • 必须将 onion 加密到相应的 blinded_node_id

encrypted_recipient_data 的读取者:

  • 必须计算:
    • $ss_i = SHA256(k_i * E_i)$ (标准 ECDH)
    • $b_i = HMAC256(\text{"blinded\_node\_id"}, ss_i) * k_i$
    • $rho_i = HMAC256(\text{"rho"}, ss_i)$
  • 必须使用 ChaCha20-Poly1305 和全零 nonce 密钥,使用 $rho_i$ 作为密钥解密 encrypted_recipient_data 字段。
  • 如果 encrypted_recipient_data 字段缺失、无法解密为 encrypted_data_tlv 或包含未知的 偶数 字段:
    • 必须返回错误
  • 如果 encrypted_data_tlv 包含 next_path_key_override:
    • 必须将其用作下一个 path_key
  • 否则:
    • 必须使用 $E_{i+1} = SHA256(E_i || ss_i) * E_i$ 作为下一个 path_key
  • 必须转发 onion,并在闪电消息中包含下一个 path_key,发给下一个节点
  • 如果是最终接收者:
    • 如果 path_id 与其为此目的创建的 盲化路由 不匹配,则必须忽略该消息

原理

路由 盲化 是一种提供接收者匿名性的轻量级技术。它比 会合路由 更灵活,因为它只是用随机公钥替换路由中节点的公钥,同时允许发送者选择他们放入每个跳的 onion 中的数据。盲化路由 在某些情况下也是可重用的(例如 onion 消息)。

盲化路由 中的每个节点都需要接收 $E_i$ 才能解密 onion 和 encrypted_recipient_data 载荷

当连接由不同节点生成的两个 盲化路由 时,第一个路由的最后一个节点需要知道第二个路由的第一个 path_key:必须使用 next_path_key_override 字段来传输此信息。从理论上讲,此方法可用于支付(不仅仅是 onion 消息),但我们建议使用非 盲化 路径来连接到 first_node_id 并在那里使用 current_path_key:这意味着该节点可以判断它是否被用作 引入点,但也无需节点支持 盲化 路径即可到达该点,并给出关于支付的非 盲化 部分的有意义的错误。

最终接收者必须验证 盲化路由 是否在正确的上下文中使用(例如,对于特定支付)并且是由他们创建的。否则,恶意发送者可以创建不同的 盲化路由 到他们怀疑可能是真正接收者的所有节点,并尝试它们,直到其中一个接受该消息。接收者可以通过存储 $E_r$ 和上下文(例如 payment_hash)来防止这种情况,并在收到 onion 时验证它们是否匹配。否则,为了避免额外的存储成本,它可以将一些私有上下文信息放入 path_id 字段(例如 payment_preimage)中并在收到 onion 时验证它。请注意,在这种情况下,使用私有信息非常重要,发送者无法访问这些信息。

每当 引入点 收到来自 盲化路由 的失败时,它应该在转发错误之前添加一个随机延迟。失败很可能是 探测 尝试,消息计时可能有助于攻击者推断其与最终接收者的距离。

可以使用 padding 字段来确保所有 encrypted_recipient_data 具有相同的长度。当在 盲化路由 的末尾添加 跳时,这尤其有用,以防止发送者确定哪个节点是最终接收者。

当路由 盲化 用于支付时,接收者指定 盲化节点 应该应用于支付的 费用过期时间,而不是让发送者配置它们。接收者还在可以通过该路由的支付中添加额外的 约束,以防止 探测 攻击,这些攻击会让恶意节点 去盲化 盲化节点 的身份。它应该设置 payment_constraints.max_cltv_expiry 以限制 盲化路由 的生命周期,并降低 中间节点 更新其 费用 并拒绝支付的风险(这可以用于 盲化 路由内的节点)。

encrypted_recipient_data 内部:encrypted_data_tlv

encrypted_recipient_data 是一个 TLV 流,为给定的 盲化节点 加密,该流可能包含以下 TLV 字段:

  1. tlv_stream: encrypted_data_tlv
  2. 类型:
    1. 类型:1 (padding)
    2. 数据:
      • [...*byte:padding]
    3. 类型:2 (short_channel_id)
    4. 数据:
      • [short_channel_id:short_channel_id]
    5. 类型:4 (next_node_id)
    6. 数据:
      • [point:node_id]
    7. 类型:6 (path_id)
    8. 数据:
      • [...*byte:data]
    9. 类型:8 (next_path_key_override)
    10. 数据:
      • [point:path_key]
    11. 类型:10 (payment_relay)
    12. 数据:
      • [u16:cltv_expiry_delta]
      • [u32:fee_proportional_millionths]
      • [tu32:fee_base_msat]
    13. 类型:12 (payment_constraints)
    14. 数据:
      • [u32:max_cltv_expiry]
      • [tu64:htlc_minimum_msat]
    15. 类型:14 (allowed_features)
    16. 数据:
      • [...*byte:features]
原理

加密的接收者数据由最终接收者创建并提供给发送者,其中包含有关节点如何处理消息的指令(它也可以由发送者自己创建:转发节点无法分辨)。它用于支付 onion 和 onion 消息 onion 中。请参阅 路由 盲化

接受和转发支付

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

非严格转发

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

原理

如果两个对等方具有多个通道,则下游节点将能够解密下一跳 载荷,而不管数据包通过哪个通道发送。

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

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

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

建议

使用非严格转发的实施应考虑对具有相同对等方的所有通道应用相同的 费用安排,因为发送者可能会选择导致总体成本最低的通道。拥有不同的策略可能会导致转发节点根据发送者最理想的 费用安排 接受费用,即使它们在与同一对等方的所有通道上提供聚合带宽。

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

最后一个节点的 载荷

在构建路由时,源节点必须使用以下值的最终节点的 载荷

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

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

如果不是为了以上原因,由于它不需要转发付款,因此最终节点可以简单地丢弃其 载荷

共享密钥

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

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

临时 Onion 密钥进行 盲化

为了确保沿路由的多个跳无法通过他们看到的 临时公钥 链接,密钥在每个跳上都是 盲化 的。 盲化 以一种确定性的方式完成,该方式允许发送者在数据包构建期间计算相应的 盲化私钥

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 跳,发送者然后迭代计算下一个跳的 共享密钥 ss_k临时密钥 ek_{k+1},如下所示:

  • 发送者使用跳的 公钥临时私钥 执行 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 载荷。 构造返回一个 1366字节 的数据包以及第一个接收对等方的地址。

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

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

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

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

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

Onion 解密

我们使用两种 onion_packet

  1. update_add_htlc 中的 onion_routing_packet,用于付款,其中包含一个 payload TLV(请参阅添加 HTLC
  2. onion_message 中的 onion_message_packet,用于消息,其中包含一个 onionmsg_tlv TLV(请参阅 Onion 消息

这些部分指定了要使用的 associated_datapath_key(如果有)、提取的 载荷 格式和处理(包括如何确定下一个对等点,如果有),以及如何处理错误。 处理本身是相同的。

要求

读者:

  • 如果 version 不是 0:
    • 必须中止处理数据包并失败。
  • 如果 public_key 不是有效的 公钥
    • 必须中止处理数据包并失败。
  • 如果 onion 用于付款:
    • 如果之前已收到 hmac
      • 如果预 图像 已知:
      • 可以立即使用预 图像 兑换 HTLC。
      • 否则:
      • 必须中止处理数据包并失败。
  • 如果指定了 path_key
    • 计算 blinding_ss 为 ECDH(path_key, node_privkey)。
    • 或者:
      • 通过乘以 $HMAC256(\text{"blinded\_node\_id"}, blinding\_ss)$ 来调整 public_key
    • 或者(等效地):
      • 通过乘以 $HMAC256(\text{"blinded\_node\_id"}, blinding\_ss)$ 来调整其自己的 node_privkey(如下)。
  • 共享密钥 ss 导出为 ECDH(public_key, node_privkey)(请参阅共享密钥)。
  • mu 导出为 $HMAC256(\text{"mu"}, ss)$(请参阅密钥生成)。
  • 将 HMAC 导出为 $HMAC256(mu, hop\_payloads || associated\_data)$。
  • 必须使用 恒定时间 比较计算出的 HMAC 和 hmac
  • 如果计算出的 HMAC 和 hmac 不同:
    • 必须中止处理数据包并失败。
  • rho 导出为 $HMAC256(\text{"rho"}, ss)$(请参阅密钥生成)。
  • 使用 rho 导出 hop_payloads 长度两倍的 bytestream(请参阅伪随机字节流)。
  • unwrapped_payloads 设置为 hop_payloadsbytestream 的 XOR。
  • unwrapped_payloads 的前面删除一个 bigsize,作为 payload_length。 如果该值格式不正确:
    • 必须中止处理数据包并失败。
  • 如果 payload_length 小于 2:
    • 必须中止处理数据包并失败。
  • 如果 unwrapped_payloads 中剩余的字节数少于 payload_length
    • 必须中止处理数据包并失败。
  • unwrapped_payloads 的前面删除 payload_length 字节,作为当前的 payload
  • 如果 unwrapped_payloads 中剩余的字节数少于32个:
    • 必须中止处理数据包并失败。
  • 删除 32 个字节作为 next_hmac,从 unwrapped_payloads 的前面。
  • 如果 unwrapped_payloads 小于 hop_payloads
    • 必须中止处理数据包并失败。
  • 如果 next_hmac 不是全零(不是最终节点):
    • blinding_tweak 导出为 $SHA256(public\_key || ss)$(请参阅 盲化 临时 Onion 密钥)。
    • 应该将 onion 转发到下一个对等点,其中:
      • version 设置为 0。
      • public_key 设置为传入的 public_key 乘以 blinding_tweak
      • hop_payloads 设置为 unwrapped_payloads,截断为传入的 hop_payloads 大小。
      • hmac 设置为 next_hmac
    • 如果无法转发:
      • 必须失败。
  • 否则(全零 next_hmac):
    • 这是 onion 的最终目的地。

原理

在使用 盲化 路径的情况下,发送者实际上并没有为我们的 node_id 加密此 onion,而是为一个 调整后的 版本:我们可以从 path_key 中导出使用的 调整,该 调整 与 onion 一起给出。 然后,我们要么以相同的方式 调整 我们的 节点私钥 以解密 onion,要么 调整 到数学上等效的 onion 临时密钥

填充符生成

收到数据包后,处理节点会从路由信息和 逐跳载荷 中提取目标信息。 提取是通过 取消混淆左移 字段来完成的。 这会使每个跳的字段变短,从而使攻击者可以推断出路由长度。 因此,在转发之前对字段进行 预填充。 由于填充是 HMAC 的一部分,因此源节点将必须 预生成 相同的填充(与每个跳将生成的填充相同),以便为每个跳正确计算 HMAC。 填充符 也用于填充字段长度,以防所选路由短于 1300字节

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

为了计算正确的 HMAC,源节点必须为每个跳 预生成 hop_payloads,包括每个跳添加的 增量混淆填充。 此 增量混淆填充 称为 filler

以下示例代码显示了如何在 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 &lt; 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
    // 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 关联。

带有 path_key 的 HTLC 的错误处理尤其令人担忧,因为实施(或版本)中的差异可能会被用来 取消匿名化 盲化 路径的元素。 因此,该决定将每个错误都转换为 invalid_onion_blinding引入点 会将其转换为普通 onion 错误。

要求

出错的节点

  • 必须设置 pad,以使 failure_len 加上 pad_len 至少为 256。
  • 应该设置 pad,以使 failure_len 加上 pad_len 等于 256。 偏离 此值可能会导致较旧的节点无法解析返回消息。

源节点

  • 一旦返回消息被解密:
    • 应该存储消息的副本。
    • 应该继续解密,直到循环重复 27 次(tlv 载荷 类型的最大路由长度)。
    • 应该使用 恒定 ammagum 密钥来 混淆 路由长度。

原理

源节点的要求应该有助于隐藏付款发送者。 通过继续解密 27 次(找到错误后进行虚拟解密循环),如果发送者要多次重试同一路由,则出错节点无法通过执行 时序分析 来了解其在路由中的相对位置。

失败消息

封装在 failuremsg 中的失败消息具有与普通消息相同的格式:一个 2字节 的类型 failure_code,后跟适用于该类型的数据。 消息数据之后是一个可选的 TLV 流

以下是当前支持的 failure_code 值的列表,以及它们的使用案例要求。

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

failure_code高位字节 可以读取为一组标志:

  • 0x8000 (BADONION): 发送对等点 加密的 onion 无法解析
  • 0x4000 (PERM): 永久性 故障(否则为 瞬态
  • 0x2000 (NODE): 节点 故障(否则为 通道
  • 0x1000 (UPDATE): 违反了 通道转发参数

定义了以下 failure_code

  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:onion_的_sha256]

当 onion 到达处理节点时,onion 的 HMAC 不正确。

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

临时密钥不能被处理节点解析。

  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,该 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 的值必须至少是在最后一个 hop onion payload 中指定的值。 因此,它对发送者没有任何实质性的信息价值(尽管可能表明倒数第二个节点收取的费用低于预期)。 倒数第二个 hop 发送的金额或到期日对于 htlc 来说太低,将通过 final_incorrect_cltv_expiryfinal_incorrect_htlc_amount 来处理。

height 参数由最终节点设置为接收 htlc 时最知名的区块高度。 发送者可以使用它来区分发送具有错误最终 CLTV 过期时间的付款和中间 hop 延迟付款,从而不再满足接收者的发票 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:disabled_flags]
    • [u16:len]
    • [len*byte:channel_update]

来自处理节点的通道已被禁用。 目前未定义 disabled_flags 的标志,因此目前始终是两个零字节。

  1. 类型: 21 (expiry_too_far)

HTLC 中的 CLTV 过期时间太远,超出了未来。

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

解密的 onion per-hop payload 无法被处理节点理解或不完整。 如果故障可以缩小到 payload 中的特定 tlv 类型,则出错节点可以在解密的字节流中包含该 type 及其字节 offset

  1. 类型: 23 (mpp_timeout)

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

  1. 类型: BADONION|PERM|24 (invalid_onion_blinding)
  2. 数据:
    • [sha256:onion_的_sha256]

在盲化路径中发生错误。

要求

一个 出错节点

  • 如果在传入的 update_add_htlc 中设置了 path_key
    • 必须返回一个 invalid_onion_blinding 错误。
  • 如果在 onion payload 中设置了 current_path_key 并且它不是最终节点:
    • 必须返回一个 invalid_onion_blinding 错误。
  • 否则:
    • 在创建错误消息时必须选择上述错误代码之一。
    • 必须包含该特定错误类型的适当数据。
    • 如果有多个错误:
      • 应该从上面的列表中选择它遇到的第一个错误。

一个 出错节点 可以:

  • 如果 onion 中的 per-hop payload 无效(例如,它不是有效的 tlv 流) 或者缺少所需信息(例如,未指定金额):
    • 返回一个 invalid_onion_payload 错误。
  • 如果整个节点发生其他未指定的瞬时错误:
    • 返回一个 temporary_node_failure 错误。
  • 如果整个节点发生其他未指定的永久性错误:
    • 返回一个 permanent_node_failure 错误。
  • 如果一个节点在其 node_announcement features 中公布了要求, 但这些要求未包含在 onion 中:
    • 返回一个 required_node_feature_missing 错误。

一个 转发节点 必须:

  • 如果在传入的 update_add_htlc 中设置了 path_key
    • 返回一个 invalid_onion_blinding 错误。
  • 如果在 onion payload 中设置了 current_path_key 并且它不是最终节点:
    • 返回一个 invalid_onion_blinding 错误。
  • 否则:
    • 在创建错误消息时选择上述错误代码之一。

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

  • 如果 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 在未来超过 max_htlc_cltv
    • 返回一个 expiry_too_far 错误。
  • 如果通道已禁用:
    • 报告传出通道的当前通道设置。
    • 返回一个 channel_disabled 错误。

一个 中间 hop 不得,但 最终节点

  • 如果支付哈希已支付:
    • 可以将支付哈希视为未知。
    • 可以成功接受 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 错误。
  • 如果最终节点的 HTLC 中的 cltv_expiry 低于 outgoing_cltv_value
    • 必须返回 final_incorrect_cltv_expiry 错误。
  • 如果最终节点的 HTLC 中的 amount_msat 低于 amt_to_forward
    • 必须返回一个 final_incorrect_htlc_amount 错误。
  • 如果它返回一个 channel_update
    • 必须将 short_channel_id 设置为传入 onion 使用的 short_channel_id

基本原理

在存在多个 short_channel_id 别名的情况下,channel_update short_channel_id 应该引用原始发送者期望的那个,以避免混淆并避免泄漏有关其他别名(或通道 UTXO 的真实位置)的信息。

channel_update 字段过去在 failure_code 包含 UPDATE 标志的消息中是强制性的。 但是,由于节点将 onion 中包含的更新应用于其 gossip 数据是一种大规模的指纹识别漏洞,因此 channel_update 字段不再是强制性的,并且预计节点会过渡到不包含它。 不提供 channel_update 的节点应该将 channel_update len 字段设置为零。

但是,某些节点可能仍将 channel_update 用于重试同一付款。

接收失败代码

要求

一个 原始节点

  • 必须忽略 failuremsg 中的任何额外字节。
  • 如果 最终节点 返回错误:
    • 如果设置了 PERM 位:
      • 应该拒绝付款。
    • 否则:
      • 如果错误代码被理解且有效:
      • 可以重试付款。 特别是,如果自发送以来区块高度已更改,则可能发生 final_expiry_too_soon,在这种情况下,temporary_node_failure 可能会在几秒钟内解决。
  • 否则, 中间 hop 返回错误:
    • 如果设置了 NODE 位:
      • 应该从考虑中删除与出错节点连接的所有通道。
    • 如果未设置 PERM 位:
      • 应该在从其对等方收到新的 channel_update 时恢复通道。
    • 否则:
      • 如果设置了 UPDATE,并且 channel_update 有效且比用于发送付款的 channel_update 更新:
      • 可以在计算重试失败付款的路由时考虑 channel_update
      • 必须不以任何其他方式向第三方公开 channel_update,包括将 channel_update 应用于本地网络图、将 channel_update 作为 gossip 发送给对等方等。
    • 然后应重试路由并发送付款。
  • 可以使用各种失败类型中指定的数据进行调试 目的。

Onion 消息

Onion 消息允许对等方使用现有连接来查询发票(请参阅 BOLT 12)。 与 gossip 消息一样,它们不与特定的本地通道相关联。 与 HTLC 一样,它们使用 onion 消息 协议进行端到端加密。

Onion 消息使用与 HTLC onion_packet 相同的格式,但格式略有不同:payload 长度不是 1300 字节,而是由总长度隐含(减去标头和尾随字节的 66 字节)。 onionmsg_payloads 本身与 hop_payloads 格式相同,只是没有“传统”长度:长度为 0 表示空的 onionmsg_payload

Onion 消息是不可靠的:特别是,它们被设计为处理起来很便宜,并且不需要存储来转发。 因此,中间节点不会返回错误。

为了一致性,所有 onion 消息都使用 路由盲化

onion_message 消息

  1. 类型: 513 (onion_message) (option_onion_messages)

  2. 数据:

    • [point:path_key]
    • [u16:len]
    • [len*byte:onion_message_packet]
  3. 类型: onion_message_packet

  4. 数据:

    • [byte:version]
    • [point:public_key]
    • [...*byte:onionmsg_payloads]
    • [32*byte:hmac]
  5. 类型: onionmsg_payloads

  6. 数据:

    • [bigsize:length]
    • [length*u8:onionmsg_tlv]
    • [32*byte:hmac]
    • ...
    • filler

onionmsg_tlv 本身是一个 TLV:中间节点期望一个 encrypted_recipient_data,它可以使用与 onion 消息一起传递的 path_key 将其解密为 encrypted_data_tlv

数字 64 及以上的字段保留给最终 hop 的 payload,尽管非最终 hop 不会明确拒绝它们(当然,除非是偶数!)。

  1. tlv_stream: onionmsg_tlv
  2. 类型:
    1. 类型: 2 (reply_path)
    2. 数据:
      • [blinded_path:path]
    3. 类型: 4 (encrypted_recipient_data)
    4. 数据:
      • [...*byte:encrypted_recipient_data]
    5. 类型: 64 (invoice_request)
    6. 数据:
      • [tlv_invoice_request:invreq]
    7. 类型: 66 (invoice)
    8. 数据:
      • [tlv_invoice:inv]
    9. 类型: 68 (invoice_error)
    10. 数据:
      • [tlv_invoice_error:inverr]
要求

encrypted_recipient_data 的创建者(通常是 onion 的接收者):

  • 必须按照 路由盲化 中的要求从 encrypted_data_tlv 创建 encrypted_recipient_data
  • 不得在任何 encrypted_data_tlv 中包含 payment_relaypayment_constraints
  • 必须为每个非最终节点在 encrypted_data_tlv 中包含 next_node_idshort_channel_id

写入者:

  • 必须将 onion_message_packet version 设置为 0。
  • 必须使用 Sphinx 按照上述详细说明构造 onion_message_packet onionmsg_payloads
  • 不得在 Sphinx 构造中使用任何 associated_data
  • 应该将 onion_message_packet len 设置为 1366 或 32834。
  • 如果它期望收到回复并且在合理的时间段后未收到回复,则应该通过不同的路径重试。
  • 对于非最终节点的 onionmsg_tlv
    • 不得设置 encrypted_recipient_data 以外的字段。
  • 对于最终节点的 onionmsg_tlv
    • 如果允许最终节点回复:
    • 必须将 reply_path path_key 设置为 first_node_id 的初始路径密钥
    • 必须将 reply_path first_node_id 设置为回复路径中第一个节点的未盲化节点 id。
    • 对于每个 reply_path path
      • 必须将 blinded_node_id 设置为要加密 onion hop 的盲化节点 id。
      • 必须将 encrypted_recipient_data 设置为有效的加密 encrypted_data_tlv 流,该流在使用时满足 onionmsg_tlv 的要求。
      • 可以使用 path_id 包含一个密钥,以便它可以识别此 reply_path 的使用。
    • 否则:
    • 不得设置 reply_path

读者:

  • 应该接受来自没有已建立通道的对等方的 onion 消息。
  • 可以通过丢弃消息来限制消息速率。
  • 必须使用 Onion 解密 中描述的空 associated_datapath_key 解密 onion_message_packet,以提取 onionmsg_tlv
  • 如果解密失败,结果不是有效的 onionmsg_tlv,或者它包含未知的偶数类型:
    • 必须忽略该消息。
  • 如果 encrypted_data_tlv 包含 allowed_features
    • 如果满足以下条件,则必须忽略该消息:
    • encrypted_data_tlv.allowed_features.features 包含未知的特征位(即使它是奇数)。
    • 该消息使用了未包含在 encrypted_data_tlv.allowed_features.features 中的特征。
  • 如果根据 onion 加密,它不是最终节点:
    • 如果 onionmsg_tlv 包含 encrypted_recipient_data 以外的其他 tlv 字段:
    • 必须忽略该消息。
    • 如果 encrypted_data_tlv 包含 path_id
    • 必须忽略该消息。
    • 否则:
    • 如果存在 next_node_id
      • 下一个对等方**是具有该节点 id 的对等方。
    • 否则,如果存在 short_channel_id 并且对应于已公布的 short_channel_id 或通道的本地别名:
      • 下一个对等方**是该通道另一端的对等方。
    • 否则:
      • 必须忽略该消息。
    • 应该使用 onion_message 将消息转发到 下一个对等方**。
    • 如果它转发该消息:
      • 必须将转发的 onion_message 中的 path_key 设置为 路由盲化 中计算的下一个 path_key
  • 否则(它是最终节点):
    • 如果设置了 path_id 并且对应于读者先前在 reply_path 中发布过的路径:
    • 如果 onion 消息不是对先前 onion 的回复:
      • 必须忽略 onion 消息
    • 否则(未知或未设置 path_id):
    • 如果 onion 消息是对包含 path_id 的 onion 消息的回复:
      • 必须完全按照它没有发送初始 onion 消息的方式做出响应(或不响应)。
    • 如果 onionmsg_tlv 包含多个 payload 字段:
    • 必须忽略该消息。
    • 如果它想发送回复:
    • 必须使用 reply_path 创建一个 onion 消息。
    • 必须通过 onion_message 将回复发送到 first_node_id 指示的节点,使用 reply_path path_key 沿着 reply_path path 发送。
基本原理

必须注意仅使用给定的确切 reply_path 接受回复,否则可能会进行探测。 这意味着检查 两种方式:非回复不使用回复路径,回复总是 使用回复路径。

要求丢弃带有 onionmsg_tlv 字段的消息 并非严格要求确保当前和 未来实施之间的一致性。 甚至是奇数字段也可能是一个问题,因为它们 会被理解它们的节点解析(因此可能会被拒绝!), 并被不理解它们的节点忽略。

所有 onion 消息都是盲化的,即使这种开销并不 总是必要的(此处为 33 个字节,onion 中每个 encrypted_data_tlv 的 16 字节 MAC)。 这种盲化允许节点使用他人提供的路径 不知道其内容。 普遍使用它可以简化 实现一点,并使其更难以区分 onion 消息。

len 允许发送比标准 1300 字节更大的消息 允许用于 HTLC onion,但应谨慎使用,因为它 降低了匿名集,因此建议它看起来 就像一个 HTLC onion,或者如果更大,则采用固定大小。

Onion 消息没有明确要求通道,但对于 垃圾邮件减少,节点可以选择对这些对等方进行速率限制,尤其是 要求其转发的消息。

max_htlc_cltv 选择

max_htlc_cltv 值定义为 2016 个区块,基于 Lightning 实施部署的历史值。

测试向量

返回错误

测试向量使用以下参数:

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

nhops = 5
sessionkey = 0x4141414141414141414141414141414141414141414141414141414141414141

failure_source  = node 4
failure_message = `incorrect_or_unknown_payment_details`
      htlc_msat = 100
      height    = 800000
      tlv data
        type  = 34001
        value = [128, 128, ..., 128] (300 bytes)

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

创建错误消息

encoded_failure_message = 400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
shared_secret = b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328
payload = 0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
um_key = 4da7f2923edce6c2d85987d1d9fa6d88023e6c3a9c3d20f07d3b10b61a78d646
raw_error_packet = fda7e11974f78ca6cc456f2d17ae54463664696e93842548245dd2a2c513a6260140400f0000000000000064000c3500fd84d1fd012c80808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808002c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

# 转发错误包
shared_secret = b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328
ammag_key = 2f36bb8822e1f0d04c27b7d8bb7d7dd586e032a3218b8d414afbba6f169a4d68
stream = e9c975b07c9a374ba64fd9be3aae955e917d34d1fa33f2e90f53bbf4394713c6a8c9b16ab5f12fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4f5984bc119af09d471a61f39e9e389c4120cadabc5d9b7b1355a8ccef050ca8ad72f642fc26919927b347808bade4b1c321b08bc363f20745ba2f97f0ced2996a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad3248e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7dbc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d86d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2265a658d9f2a007229a5293b67ec91c84c4b4407c228434bad8a815ca9b256c776bd2c9f
error packet for node 4: 146e94a9086dbbed6a0ab6932d00c118a7195dbf69b7d7a12b0e6956fc54b5e0a989f165b5f12fd45edd73a5b0c48630ff5be69500d3d82a29c0803f0a0679a6a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e332a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e63a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f12909cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b38392a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c531eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e70d04a0a57afe4af42e99912fbb4f8883a5ecb9cb29b883cb6bfa0f4db2279ff8c6d2b56a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fcb4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0ecacb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b8285727735ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb2fe0329666477ee7cb7ebd6b8```
shared_secret = 3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc
    ammag_key = 1bf08df8628d452141d56adfd1b25c1530d7921c23cecfc749ac03a9b694b0d3
    stream = 6149f48b5a7e8f3d6f5d870b7a698e204cf64452aab4484ff1dee671fe63fd4b5f1b78ee2047dfa61e3d576b149bedaf83058f85f06a3172a3223ad6c4732d96b32955da7d2feb4140e58d86fc0f2eb5d9d1878e6f8a7f65ab9212030e8e915573ebbd7f35e1a430890be7e67c3fb4bbf2def662fa625421e7b411c29ebe81ec67b77355596b05cc155755664e59c16e21410aabe53e80404a615f44ebb31b365ca77a6e91241667b26c6cad24fb2324cf64e8b9dd6e2ce65f1f098cfd1ef41ba2d4c7def0ff165a0e7c84e7597c40e3dffe97d417c144545a0e38ee33ebaae12cc0c14650e453d46bfc48c0514f354773435ee89b7b2810606eb73262c77a1d67f3633705178d79a1078c3a01b5fadc9651feb63603d19decd3a00c1f69af2dab2595931ca50d8280758b1cc91ba2dc43dbbc3d91bf25c08b46c2ecef7a32cec64d4b61ee3a629ef563afe058b71e71bcb69033948bc8728c5ebe65ec596e4f305b9fc159d53f723dfc95b57f3d51717f1c89af97a6d587e89e62efcc92198a1b2bd66e2d875505ea4046c04389f8cb0ee98f0af03af2652e2f3d9a9c48430f2891a4d9b16e7d18099e4a3dd334c24aba1e2450792c2f22092c170da549d43a440021e699bd6c20d8bbf1961100a01ebcce06a4609f5ad93066287acf68294cfa9ea7cea03a508983b134a9f0118b16409a61c06aaa95897d2067cb7cd59123f3e2ccf0e16091571d616c44818f118bb7835a679f5c0eea8cf1bd5479882b2c2a341ec26dbe5da87b3d37d66b1fbd176f71ab203a3b6eaf7f214d579e7d0e4a3e59089ebd26ba04a62403ae7a793516ec16d971d51c5c0107a917d1a70221e6de16edca7cb057c7d06902b5191f298aa4d478a0c3a6260c257eae504ebbf2b591688e6f3f77af770b6f566ae9868d2f26c12574d3bf9323af59f0fe0072ff94ae597c2aa6fbcbf0831989e02f9d3d1b9fd6dd97f509185d9ecbf272e38bd621ee94b97af8e1cd43853a8f6aa6e8372585c71bf88246d064ade524e1e0bd8496b620c4c2d3ae06b6b064c97536aaf8d515046229f72bee8aa398cd0cc21afd5449595016bef4c77cb1e2e9d31fe1ca3ffde06515e6a4331ccc84edf702e5777b10fc844faf17601a4be3235931f6feca4582a8d247c1d6e4773f8fb6de320cf902bbb1767192782dc550d8e266e727a2aa2a414b816d1826ea46af71701537193c22bbcc0123d7ff5a23b0aa8d7967f36fef27b14fe1866ff3ab215eb29e07af49e19174887d71da7e7fe1b7aa1b3c805c063e0fafedf125fa6c57e38cce33a3f7bb35fd8a9f0950de3c22e49743c05f40bc55f960b8a8b5e2fde4bb229f125538438de418cb318d13968532499118cb7dcaaf8b6d635ac4001273bdafd12c8ea0702fb2f0dac81dbaaf68c1c32266382b293fa3951cb952ed5c1bdc41750cdbc0bd62c51bb685616874e251f031a929c06faef5bfcb0857f815ae20620b823f0abecfb5
    error packet for node 2: 145bc1c63058f7204abbd2320d422e69fb1b3801a14312f81e5e29e6b5f4774cfed8a25241d3dfb7466e749c1b3261559e49090853612e07bd669dfb5f4c54162fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0eef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a921a2c931500ab08e686cffb2d87258d254fb17843959cccd265a57ba26c740f0f231bb76df932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb31820064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a150776bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db121a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec8768848333156dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2caf8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b948d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee694ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b446c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab0946534e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef4341b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b761d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb89574a6bdca1185b40f8aa54026d5926725f99ef028da1be892e3586361efe15f4a148ff1bc9
    # forwarding error packet
    shared_secret = a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae
    ammag_key = 59ee5867c5c151daa31e36ee42530f429c433836286e63744f2020b980302564
    stream = 0f10c86f05968dd91188b998ee45dcddfbf89fe9a99aa6375c42ed5520a257e048456fe417c15219ce39d921555956ae2ff795177c63c819233f3bcb9b8b28e5ac6e33a3f9b87ca62dff43f4cc4a2755830a3b7e98c326b278e2bd31f4a9973ee99121c62873f5bfb2d159d3d48c5851e3b341f9f6634f51939188c3b9ff45feeb11160bb39ce3332168b8e744a92107db575ace7866e4b8f390f1edc4acd726ed106555900a0832575c3a7ad11bb1fe388ff32b99bcf2a0d0767a83cf293a220a983ad014d404bfa20022d8b369fe06f7ecc9c74751dcda0ff39d8bca74bf9956745ba4e5d299e0da8f68a9f660040beac03e795a046640cf8271307a8b64780b0588422f5a60ed7e36d60417562938b400802dac5f87f267204b6d5bcfd8a05b221ec294d883271b06ca709042ed5dbb64a7328d2796195eab4512994fb88919c73b3e5dd7bf68b2136d34cff39b3be266b71e004509bf975a240800bb8ae5eed248423a991ae80ef751b2d03b67fb93ffdd7969d5b500fe446a4ffb4cd04d0767a5d367ebd3f8f260f38ae1e9d9f9a7bd1a99ca1e10ee36bd241f06fc2b481c9b7450d9c9704204666807783264a0e93468e22db4dc4a7a4db2963ddf4366d08e225cf94848aac794bcecb7e850113e38cc3647a03a5dfaa3442b1bb58b1de7fa7f436feb4d7c23cbd2de6d55d4025fcd383cc9d49c0b130e2fd5a9097c216683c842f898a8a2159761cca9aa1c818194e3b7bea6da6652d5189f3b6b0ca1d5398b6d14e311d9c7f00399c29e94deb98496f4cd97c5d7d6a65cabc3791f60d728d6422a422c0cff5f7dfd4ce2d7e8d38dd71ae18763acc832c57275497f61b2620cca13cc64c0c48353f3817016f91448d6fc1cc451ee1f4a429e43292bbcd54fcd807e2c47675bac1781d9d81e9e6dc69028d428f5ee261750f626bcaf416a0e7badadf73fe1922207ae6c5209d16849e4a108f4a6f38694075f55177105ac4c2b97f6a474b94c03257d8d12b0196e2905d914b8c2213a1b9dc9608e1a2a1e03fe0820a813275de83be5e9734875787a9e006eb8574c23ddd49e2347d1ecfcedf3caa0a5dd45666368525b48ac14225d6422f82dbf59860ee4dc78e845d3c57668ce9b9e7a8d012491cef242078b458a956ad67c360fb6d8b86ab201d6217e49b55fa02a1dea2dbe88d0b08d30670d1b93c35cc5e41e088fccb267e41d6151cf8560496e1beeefe680744d9dabb383a4957466b4dc3e2bce7b135211da483d998a22fa687cc609641126c5dee3ed87291067916b5b065f40582163291d48e81ecd975d0d6fd52a31754f8ef15e43a560bd30ea5bf21915bd2e7007e607abbc6261edc8430cc7f789675b1fe83e807c5c475bd5178eba2fc40674706b0a68c6a428e5dec36e413e653c6db1178923ff87e2389a78bf9e93b713de4f4753f9f9d6a361369b609e1970c91ff9bd191c472e0bf2e8681412260ad0ef5855dc39f2084d45
    error packet for node 1: 1b4b09a935ce7af95b336baae307f2b400e3a7e808d9b4cf421cc4b3955620acb69dcdb656128dae8857adbd4e6b37fbb1be9c1f2f02e61e9e59a630c4c77cf383cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd2111e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f360d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b03eba82b5d7cb78e34dc961948d6161eadd7cf5d95d9c56df2ff5faa6ccf85eacdc9ff2fc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ffe31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9fe79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c9762de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bce49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2ff45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b142444aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f35c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641cddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d25b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaaeb3bd7fc02ec02066b324cb72a09b6b43dec1097f49d69d3c138ce6f1a6402898baf7568c
    # forwarding error packet
    shared_secret = 53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66
    ammag_key = 3761ba4d3e726d8abb16cba5950ee976b84937b61b7ad09e741724d7dee12eb5
    stream = 3699fd352a948a05f604763c0bca2968d5eaca2b0118602e52e59121f050936c8dd90c24df7dc8cf8f1665e39a6c75e9e2c0900ea245c9ed3b0008148e0ae18bbfaea0c711d67eade980c6f5452e91a06b070bbde68b5494a92575c114660fb53cf04bf686e67ffa4a0f5ae41a59a39a8515cb686db553d25e71e7a97cc2febcac55df2711b6209c502b2f8827b13d3ad2f491c45```markdown
为节点0发送的错误数据包:2dd2f49c1f5af0fcad371d96e8cddbdcd5096dc309c1d4e110f955926506b3c03b44c192896f45610741c85ed4074212537e0c118d472ff3a559ae244acd9d783c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04ca86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf979738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c780486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de0822669861dafcd29de74eac48b64411987285491f98d78584d0c2a163b7221ea796f9e8671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa02afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4ed3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae28227b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1beab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bdd4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26add1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b55c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73af0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b97978ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d16fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b3e546df4
```## 参考文献

[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

## 作者

[ FIXME: ]

![Creative Commons License](https://i.creativecommons.org/l/by/4.0/88x31.png "License CC-BY")
&lt;br>
本作品采用 [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/) 许可。

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

0 条评论

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