该文档描述了洋葱路由协议的构建方法,用于将支付从发起节点路由到最终节点。数据包通过多个中间节点(称为“跳”)进行路由。路由方案基于 Sphinx 构造,并扩展了每个跳的有效负载。中间节点可以验证数据包的完整性,并了解应将数据包转发到哪个节点。该协议使用共享密钥生成伪随机字节流来混淆数据包,并使用密钥来加密有效负载和计算 HMAC,以确保每个跳的数据包完整性。
本文档描述了 onion 路由数据包的构建,该数据包用于将付款从起始节点路由到最终节点。该数据包通过多个中间节点路由,这些节点称为跳。
路由模式基于 [Sphinx][sphinx] 结构,并扩展为每个跳的 payload。
转发消息的中间节点可以验证数据包的完整性,并且可以了解应该将数据包转发到哪个节点。他们无法了解除其前任或后继者之外的哪些其他节点是数据包路由的一部分;他们也无法了解路由的长度或它们在其中的位置。每个跳都会对数据包进行混淆,以确保网络级别的攻击者无法关联属于同一路由的数据包(即,属于同一路由的数据包不共享任何相关信息)。请注意,这并不排除攻击者通过流量分析关联数据包的可能性。
路由由起始节点构建,该节点知道每个中间节点和最终节点的公钥。了解每个节点的公钥允许起始节点为每个中间节点和最终节点创建一个共享密钥(使用 ECDH)。然后,共享密钥用于生成字节的伪随机流(用于混淆数据包)和多个密钥(用于加密 payload 和计算 HMAC)。然后,HMAC 又用于确保每个跳处数据包的完整性。
路由上的每个跳只看到起始节点的临时密钥,以隐藏发送者的身份。临时密钥在转发到下一个节点之前,会被每个中间跳进行盲化,从而使 onion 在路由上不可链接。
本规范描述了数据包格式和路由机制的 版本 0。
节点:
本文档中遵循许多约定:
SHA256
哈希算法。secp256k1
][sec2] 中所指定ChaCha20
][rfc8439] 用于生成伪随机字节流。对于其生成,使用固定的 96 位 null-nonce(0x000000000000000000000000
),以及从共享密钥派生的密钥和所需输出大小的 0x00
字节流作为消息。hop_payload
或一个固定大小的旧版 hop_data
payload。
hop_data
由单个 0x00
字节前缀标识hop_payload
以一个 bigsize
编码为前缀,该编码以字节为单位对长度进行编码,不包括前缀和尾随 HMAC。从共享密钥派生出多个加密和验证密钥:
密钥生成函数接受一个密钥类型(rho=0x72686F
,mu=0x6d75
,um=0x756d
或 pad=0x706164
)和一个 32 字节的密钥作为输入,并返回一个 32 字节的密钥。
密钥通过计算 HMAC(以 SHA256
作为哈希算法)生成,使用适当的密钥类型(即 rho、mu、um 或 pad)作为 HMAC 密钥,并使用 32 字节的共享密钥作为消息。然后,生成的 HMAC 作为密钥返回。
请注意,密钥类型不包括 C 风格的 0x00
终止字节,例如 rho 密钥类型的长度为 3 个字节,而不是 4 个字节。
伪随机字节流用于在路径的每个跳处混淆数据包,以便每个跳只能恢复下一个跳的地址和 HMAC。伪随机字节流通过加密(使用 ChaCha20
)一个 0x00
字节流生成,该字节流具有所需的长度,并使用从共享密钥派生的密钥和一个 96 位零 nonce(0x000000000000000000000000
)进行初始化。
使用固定 nonce 是安全的,因为密钥永远不会重用。
数据包由四个部分组成:
version
字节secp256k1
public_key
,用于共享密钥生成期间hop_payloads
,由多个可变长度的 hop_payload
payload 或最多 20 个固定大小的旧版 hop_data
payload 组成。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`
其中,length
、hop_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_announcement
、init
消息或 BOLT #11 提供 var_onion_optin
功能:
amt_to_forward
和 outgoing_cltv_value
。short_channel_id
payment_data
short_channel_id
payment_secret
:
payment_data
payment_secret
设置为提供的那个total_msat
设置为将发送的总金额payment_metadata
:
payment_metadata
读取者:
amt_to_forward
或 outgoing_cltv_value
不存在,则必须返回错误。total_msat
不存在,则必须将其视为等于 amt_to_forward
。HTLC 可能是更大的“多部分”支付的一部分:这种“基本”原子多路径支付将对所有路径使用相同的 payment_hash
。
请注意,amt_to_forward
仅是此 HTLC 的金额:包含更大值的 total_msat
字段是最终发送者承诺其余付款将通过后续 HTLC 进行;我们将这些具有相同 preimage 的未完成 HTLC 称为“HTLC 集”。
payment_metadata
将包含在每个支付部分中,以便可以尽早检测到无效的支付详细信息。
编写者:
basic_mpp
功能:
payment_hash
。amount
:
total_msat
设置为至少该 amount
,并且小于或等于 amount
的两倍。total_msat
设置为它希望支付的金额。amount_msat
等于 total_msat
。amount_msat
已经大于或等于 total_msat
,则不得发送另一个 HTLC。payment_secret
。total_msat
设置为等于 amt_to_forward
。最终节点:
total_msat
字段。basic_mpp
:
total_msat
不完全等于 amt_to_forward
,则必须使 HTLC 失败。basic_mpp
:
payment_hash
的 HTLC 集中。total_msat
不相同,则应使整个 HTLC 集失败。amount_msat
等于 total_msat
:
amount_msat
小于 total_msat
:
mpp_timeout
作为失败消息。payment_secret
。如果存在 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:
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_i
和 n_{i+1}
必须是覆盖网络路由中的对等节点。然后,发送者收集 n_1
到 n_r
的公钥,并生成一个随机的 32 字节 sessionkey
。或者,发送者可能会传入 关联数据,即数据包提交的数据,但不包含在数据包本身中。关联数据将包含在 HMAC 中,并且必须与每个跳的完整性验证期间提供的关联数据匹配。
为了构建 onion,发送者将第一个跳 ek_1
的临时私钥初始化为 sessionkey
,并通过乘以 secp256k1
基点从中推导出对应的临时公钥 epk_1
。对于路由上的每个 k
跳,发送者然后迭代地计算下一个跳 ek_{k+1}
的共享密钥 ss_k
和临时密钥,如下所示:
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 个随机字节。这些随机字节然后用作要创建的混音头部的起始状态。
使用共享密钥生成填充(请参见 填充生成)。
对于路由中的每个跳(以相反的顺序),发送者应用以下操作:
shift_size
定义为 hop_payload
的长度加上长度的 bigsize 编码和该 HMAC 的长度。因此,如果 payload 长度为 l
,则对于 l < 253
,shift_size
为 1 + l + 32
,否则由于 l
的 bigsize 编码,shift_size
为 3 + l + 32
。hop_payload
字段右移 shift_size
字节,丢弃超过其 1300 字节大小的最后 shift_size
字节。hop_payload
和 hmac
被复制到以下 shift_size
字节中。XOR
应用于 hop_payloads
字段。hop_payloads
字段的尾部将被路由信息 filler
覆盖。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 路由协议包括一个简单的机制,用于将加密的错误消息返回到原始节点。 返回的错误消息可能是任何跃点(包括最终节点)报告的失败。 正向数据包的格式不适用于返回路径,因为除了原始节点之外,没有其他跃点可以访问生成所需的信信息。 请注意,这些错误消息不可靠,因为它们没有放置在链上,因为存在跃点失败的可能性。
中间跃点存储来自正向路径的共享密钥,并在每个跃点期间重复使用它来混淆任何相应的返回数据包。 此外,每个节点在本地存储有关其自身在路由中的发送对等方的数据,因此它知道将任何最终返回数据包返回转发到哪里。 生成错误消息的节点(出错节点)构建一个由以下字段组成的返回数据包:
32*byte
:hmac
]u16
:failure_len
]failure_len*byte
:failuremsg
]u16
:pad_len
]pad_len*byte
:pad
]其中 hmac
是对数据包剩余部分进行身份验证的 HMAC,使用使用上述过程生成的密钥,密钥类型为 um
,failuremsg
定义如下,pad
是用于隐藏长度的额外字节。
然后,出错节点使用密钥类型 ammag
生成一个新密钥。
然后,此密钥用于生成伪随机流,该伪随机流又使用 XOR
应用于数据包。
混淆步骤由返回路径上的每个跃点重复。
接收到返回数据包后,每个跃点都会生成其 ammag
,生成伪随机字节流,并将结果应用于返回数据包,然后再进行返回转发。
原始节点能够检测到它是返回消息的预定最终接收者,因为当然,它是相应正向数据包的发起者。
当原始节点接收到与它发起的传输匹配的错误消息时(即,它无法进一步返回转发错误),它会为路由中的每个跃点生成 ammag
和 um
密钥。
然后,它使用每个跃点的 ammag
密钥迭代解密错误消息,并使用每个跃点的 um
密钥计算 HMAC。
原始节点可以通过将 hmac
字段与计算出的 HMAC 进行匹配来检测错误消息的发送者。
正向和返回数据包之间的关联在此 onion 路由协议之外处理,例如,通过与支付通道中的 HTLC 的关联。
出错节点:
pad
,使 failure_len
加上 pad_len
等于 256。
原始节点:
ammag
和 um
密钥来混淆路由长度。failuremsg
中封装的失败消息具有与正常消息相同的格式:一个 2 字节类型 failure_code
,后跟适用于该类型的数据。以下是当前支持的 failure_code
值的列表,后跟它们的使用案例要求。
请注意,failure_code
与其他 BOLT 中定义的其他消息类型不是同一类型,因为它们不是直接在传输层上发送的,而是包装在返回数据包中。因此,failure_code
的数值可以重用也分配给其他消息类型的值,而不会有引起冲突的任何危险。
failure_code
的高字节可以读作一组标志:
请注意,channel_update
字段在 failure_code
包含 UPDATE
标志的消息中是强制性的。
定义了以下 failure_code
:
invalid_realm
)处理节点不理解 realm
字节。
temporary_node_failure
)处理节点的一般临时故障。
permanent_node_failure
)处理节点的一般永久故障。
required_node_feature_missing
)处理节点具有此 onion 中没有的所需功能。
invalid_onion_version
)sha256
:sha256_of_onion
]处理节点不理解 version
字节。
invalid_onion_hmac
)sha256
:sha256_of_onion
]onion 的 HMAC 在到达处理节点时是不正确的。
invalid_onion_key
)sha256
:sha256_of_onion
]处理节点无法解析临时密钥。
temporary_channel_failure
)u16
:len
]len*byte
:channel_update
]来自处理节点的通道无法处理此 HTLC,但可能能够在以后处理它或其他 HTLC。
permanent_channel_failure
)来自处理节点的通道无法处理任何 HTLC。
required_channel_feature_missing
)来自处理节点的通道需要 onion 中不存在的功能。
unknown_next_peer
)onion 指定了一个 short_channel_id
,它与来自处理节点的任何通道都不匹配。
amount_below_minimum
)u64
:htlc_msat
]u16
:len
]len*byte
:channel_update
]HTLC 金额低于来自处理节点的通道的 htlc_minimum_msat
。
fee_insufficient
)u64
:htlc_msat
]u16
:len
]len*byte
:channel_update
]费用金额低于处理节点的通道要求的金额。
incorrect_cltv_expiry
)u32
:cltv_expiry
]u16
:len
]len*byte
:channel_update
]cltv_expiry
不符合处理节点的通道要求的 cltv_expiry_delta
:它不满足以下要求:
cltv_expiry - cltv_expiry_delta >= outgoing_cltv_value
expiry_too_soon
)u16
:len
]len*byte
:channel_update
]CLTV 到期时间太接近当前区块高度,无法由处理节点安全处理。
incorrect_or_unknown_payment_details
)u64
:htlc_msat
]u32
:height
]最终节点不知道 payment_hash
,payment_secret
与 payment_hash
不匹配,该 payment_hash
的金额不正确,htlc 的 CLTV 到期时间太接近当前区块高度,无法安全处理,或者 payment_metadata
不存在,而它应该存在。
htlc_msat
参数是多余的,但为了向后兼容而保留。htlc_msat
的值始终与最终跃点 onion 有效负载中指定的金额匹配。因此,它对发送者没有任何信息价值。发送不同金额或 htlc 到期时间的倒数第二个跃点通过 final_incorrect_cltv_expiry
和 final_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) 表示的其他永久性故障。
final_incorrect_cltv_expiry
)u32
:cltv_expiry
]HTLC 中的 CLTV 到期时间与 onion 中的值不匹配。
final_incorrect_htlc_amount
)u64
:incoming_htlc_amt
]HTLC 中的金额与 onion 中的值不匹配。
channel_disabled
)u16
:flags
]u16
:len
]len*byte
:channel_update
]来自处理节点的通道已被禁用。
expiry_too_far
)HTLC 中的 CLTV 到期时间太远。
invalid_onion_payload
)bigsize
:type
]u16
:offset
]处理节点不理解已解密的 onion 每跃点有效负载,或者该有效负载不完整。如果失败可以缩小到有效负载中的特定 tlv 类型,则出错节点可以在解密的字节流中包含该 type
及其字节 offset
。
mpp_timeout
)未在合理的时间内收到多部分付款的全部金额。
出错节点:
任何 出错节点 可以:
realm
字节未知:
invalid_realm
错误。invalid_onion_payload
错误。temporary_node_failure
错误。permanent_node_failure
错误。node_announcement
features
中声明了要求,
但这些要求未包含在 onion 中:
required_node_feature_missing
错误。转发节点 可以,但 最终节点 不得:
version
字节未知:
invalid_onion_version
错误。invalid_onion_hmac
错误。invalid_onion_key
错误。temporary_channel_failure
错误。permanent_channel_failure
错误。channel_announcement
的 features
中声明了要求,
但这些要求未包含在 onion 中:
required_channel_feature_missing
错误。unknown_next_peer
错误。amount_below_minimum
错误。fee_insufficient
错误。cltv_expiry
减去 outgoing_cltv_value
低于
传出通道的 cltv_expiry_delta
:cltv_expiry
和传出通道的当前通道设置。incorrect_cltv_expiry
错误。cltv_expiry
非常接近当前时间:
expiry_too_soon
错误。cltv_expiry
在未来非常遥远:
expiry_too_far
错误。channel_disabled
错误。中间跃点 不得,但 最终节点 必须:
payment_secret
与该 payment_hash
的预期值不匹配,
或者需要 payment_secret
但不存在:
incorrect_or_unknown_payment_details
错误。incorrect_or_unknown_payment_details
错误。incorrect_or_unknown_payment_details
错误。incorrect_or_unknown_payment_details
错误。
cltv_expiry
值非常接近现在:
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
中的任何额外字节。final_expiry_too_soon
,在这种情况下,temporary_node_failure
可能会在几秒钟内解决。channel_update
时还原通道。channel_update
有效且比用于发送付款的 channel_update
更新:channel_update
不应导致失败:
channel_update
视为无效。channel_update
。channel_update
排队以进行广播。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: ]

<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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!