Roteamento Onion

Como cada hop aprende só o próprio próximo passo

Lightning · Técnico

No encaminhamento de HTLCs, um pagamento pode passar por vários canais antes de chegar ao destino. Sem uma camada de privacidade, cada hop poderia aprender origem, destino, tamanho da rota e talvez correlacionar tentativas. O roteamento onion, especificado na BOLT 4, reduz esse vazamento: cada hop aprende apenas de quem recebeu, para quem deve encaminhar, quanto deve encaminhar e qual CLTV usar.

A Lightning usa um esquema baseado em Sphinx. A origem calcula a rota inteira, cria um pacote de tamanho fixo e coloca uma camada criptografada para cada hop. O pacote viaja dentro do campo onion_routing_packet da mensagem update_add_htlc, descrita na BOLT 2.

Onion routing não torna o pagamento anônimo. Ele limita o que cada hop vê. Tráfego de rede, timing, valores incomuns, probing e observação on-chain ainda podem vazar informação.

Conteúdo

Modelo mental

Pense em uma rota simples:

Alice -> Bob -> Carol -> Dina

Alice é a origem. Dina é o destino. Bob e Carol são hops intermediários. Alice conhece a rota inteira porque ela fez a busca de caminho. Mas Bob não deve saber que Dina é o destino final, e Carol não deve saber que Alice é a origem.

O pacote onion resolve isso assim:

Pacote onion com camadas criptografadas, uma para cada hop da rota, mantendo tamanho fixo.
Cada hop remove uma camada e encaminha a cebola restante.
Descrição longa do diagrama

O diagrama mostra uma cebola com camadas externas e internas. A camada externa pertence ao primeiro hop, as camadas seguintes pertencem aos hops intermediários, e o núcleo pertence ao destino. Setas indicam que cada hop remove apenas sua camada e repassa o restante.

Pacote onion

Na BOLT 4, o pacote onion de pagamento tem 1366 bytes e quatro partes principais:

Estrutura do onion packet
Campo Tamanho Função
version 1 byte Versão do pacote onion. Na versão 0, o valor é 0x00.
public_key 33 bytes Chave pública efêmera comprimida usada pelo hop para calcular o segredo compartilhado.
hop_payloads 1300 bytes Área fixa com payloads por hop, HMACs e filler ofuscado.
hmac 32 bytes Autenticação do pacote para o hop atual.

O campo hop_payloads tem 1300 bytes. Dentro dele aparecem payloads variáveis por hop, cada um prefixado por um tamanho em BigSize, seguido do payload, de um HMAC e de filler. Esse tamanho fixo ajuda a esconder quantos hops ainda restam.

O pacote onion não substitui o HTLC. Ele viaja dentro do HTLC e diz aos hops como verificar e encaminhar o próximo HTLC.

Segredos e chaves derivadas

A origem cria uma chave efêmera e calcula um segredo compartilhado com cada hop usando ECDH em secp256k1. Cada hop também consegue calcular o próprio segredo compartilhado usando sua chave privada e a chave efêmera recebida. Os outros segredos continuam escondidos.

Origem combina uma chave efêmera com a chave pública de cada hop para derivar segredos e chaves rho, mu, um e pad.
Cada hop tem um segredo diferente, embora a origem tenha calculado todos antes de enviar.
Descrição longa do diagrama

O diagrama mostra a chave efêmera da origem sendo combinada com a chave pública de cada hop. De cada combinação sai um segredo compartilhado. Setas a partir do segredo mostram as chaves derivadas usadas para ofuscação, HMAC, erro e filler.

Todas essas chaves saem do mesmo segredo compartilhado, via HMAC-SHA256 com rótulos fixos (rho, mu, um, ammag, pad). Como cada hop tem um segredo diferente, cada hop deriva um conjunto de chaves diferente.

Chaves derivadas do segredo compartilhado
Chave Uso
rho Gerar o stream que ofusca cada hop_payload — e o filler determinístico que mantém o tamanho fixo.
mu Gerar e verificar os HMACs de integridade do pacote.
um Autenticar (HMAC) as mensagens de erro retornadas ao pagador.
ammag Gerar o stream que ofusca a failure message no caminho de volta.
pad Gerar o preenchimento pseudoaleatório inicial do pacote, antes dos payloads reais.

A chave efêmera também é cegada a cada salto. Isso significa que o próximo hop vê outra chave efêmera, impedindo que hops diferentes simplesmente comparem o mesmo ponto público para correlacionar a rota.

Payload TLV por hop

O payload moderno por hop usa o formato TLV da BOLT 1. Isso permite extensões sem mudar a estrutura inteira do pacote.

Tipos TLV usados no hop payload da BOLT 4
Tipo Nome Hop final Hop intermediário Significado
2 amt_to_forward sim sim Valor em msat que este hop deve encaminhar ou entregar.
4 outgoing_cltv_value sim sim CLTV que o HTLC de saída deve usar.
6 short_channel_id não sim Canal de saída para o próximo hop.
8 payment_data sim não payment_secret e total_msat vindos da invoice BOLT 11.
10 encrypted_recipient_data blinded blinded Dados cifrados fornecidos pelo recebedor para caminhos cegados.
12 current_path_key blinded blinded Chave de caminho usada em route blinding.
16 payment_metadata sim não Metadados da invoice, se o campo m existir.
18 total_amount_msat blinded não Valor total em msat usado em pagamentos com caminho cegado.

Para um hop intermediário fora de caminho cegado, os campos centrais são amt_to_forward, outgoing_cltv_value e short_channel_id. Para o destino, não há short_channel_id, porque não existe próximo canal. O destino recebe payment_data com o payment_secret da invoice BOLT 11 e o total_msat.

hop intermediário:
type 2  amt_to_forward       = 101000 msat
type 4  outgoing_cltv_value  = 850144
type 6  short_channel_id     = 700000x42x0

hop final:
type 2  amt_to_forward       = 100000 msat
type 4  outgoing_cltv_value  = 850018
type 8  payment_data:
        payment_secret = 32 bytes
        total_msat     = 100000

O hop usa esses dados para conferir se o HTLC recebido bate com o que a origem prometeu no onion. Por exemplo, se o HTLC recebido não tem valor suficiente para pagar a taxa e ainda encaminhar amt_to_forward, o hop deve falhar o pagamento.

Construção da cebola

A origem monta o pacote de dentro para fora, porque a camada externa precisa conter, cifrada, a camada interna pronta. O cálculo de valor e CLTV também anda de trás para frente: o destino recebe o valor final; cada hop anterior precisa receber esse valor mais sua taxa, e um CLTV de entrada maior que o CLTV de saída.

rota escolhida:
Alice -> Bob -> Carol -> Dina

1. Alice pega as chaves públicas dos hops.
2. Alice cria uma chave efêmera inicial.
3. Para cada hop, Alice calcula um segredo ECDH.
4. De cada segredo, Alice deriva rho, mu, um e pad.
5. Alice monta os payloads do destino para a origem:
   - Dina recebe payment_data e valor final.
   - Carol recebe amt_to_forward, outgoing_cltv_value e short_channel_id para Dina.
   - Bob recebe amt_to_forward, outgoing_cltv_value e short_channel_id para Carol.
6. Alice adiciona HMACs e filler para manter hop_payloads com 1300 bytes.
7. Alice envia update_add_htlc para Bob com onion_routing_packet.
8. Cada hop descasca uma camada e encaminha a cebola interna.

Esse processo conecta onion routing com gossip e busca de caminho. O gossip fornece short_channel_id, taxas e cltv_expiry_delta. A busca escolhe a rota. O onion transforma essa rota em instruções privadas por hop.

Descascando uma camada

Quando um hop recebe o HTLC, ele não conhece a rota inteira. Ele recebe um pacote onion e processa apenas a camada destinada a ele:

ao receber um onion packet:

1. calcular shared_secret = ECDH(chave_privada_do_hop, public_key_efêmera)
2. derivar rho, mu, um e pad
3. verificar o HMAC do pacote atual (com a chave mu)
4. descriptografar o primeiro hop_payload (com o stream rho)
5. validar amt_to_forward e outgoing_cltv_value contra o HTLC recebido
6. olhar o HMAC destinado ao próximo hop:
   - se for 32 bytes de zero  -> este nó é o destino final
   - se for diferente de zero  -> ainda há hops adiante
7. se ainda há hops adiante:
   - ler short_channel_id
   - cegar a public_key efêmera para o próximo hop
   - encaminhar update_add_htlc com a cebola restante
8. se este nó é o destino:
   - validar payment_secret
   - validar total_msat, metadata e CLTV final
   - cumprir ou falhar o HTLC

O passo 6 esconde um detalhe elegante do design: nada no pacote diz explicitamente quem é o destino. Cada payload carrega o HMAC do próximo hop. Quando a origem monta a camada mais interna (a do destino), não existe próximo hop, então ela coloca ali um HMAC de 32 bytes de zero. É só isso que o último nó vê: ao descascar sua camada e encontrar um HMAC todo zerado, ele conclui que é o destino final e para de encaminhar.

Um hop recebe onion packet e chave efêmera, deriva segredo, verifica HMAC, lê seu payload e encaminha a cebola restante.
O hop aprende o próximo passo, não a rota inteira.
Descrição longa do diagrama

O diagrama mostra um hop recebendo uma cebola e uma chave efêmera. O hop calcula o segredo compartilhado, verifica o HMAC, abre sua camada, lê valor, CLTV e próximo canal, cega a chave efêmera e encaminha uma cebola restante ao próximo hop.

Filler e tamanho fixo

Um pacote onion tem sempre o mesmo tamanho, tenha a rota 3 ou 20 hops. Se o pacote encolhesse a cada camada descascada, qualquer hop poderia estimar quantos saltos faltam só medindo o que recebeu. O truque que mantém os 1300 bytes constantes é o filler: um preenchimento pseudoaleatório, porém determinístico, que a origem consegue prever de antemão.

hop_payloads tem sempre 1300 bytes, em todos os hops.

construção (origem, do destino para a origem):
  para cada hop, desloca hop_payloads para a direita por
    shift = bigsize(payload) + tamanho(payload) + 32 (HMAC)
  e escreve o payload + HMAC do hop no espaço aberto.
  a origem também pré-calcula o "filler": os bytes que
  vão aparecer na cauda quando cada hop descascar sua camada.

processamento (em cada hop):
  1. anexa 1300 bytes de zero ao hop_payloads recebido
  2. aplica o stream rho (XOR) sobre os 2600 bytes
  3. lê o próprio payload e o HMAC do próximo hop
  4. os 1300 bytes seguintes já são o hop_payloads a
     encaminhar; a cauda reaparece como o filler previsto

como a origem incluiu esse mesmo filler ao calcular cada HMAC,
o HMAC do próximo hop confere sem que ninguém precise saber
quantos hops ainda faltam.

A parte contraintuitiva é por que o HMAC continua válido. Quando um hop descasca sua camada, a cauda do campo passa a conter bytes que ele nunca viu antes — o filler. Se esses bytes fossem imprevisíveis, o HMAC calculado pela origem para o próximo hop não bateria. Como o filler é gerado deterministicamente a partir da chave rho de cada hop, a origem consegue calcular exatamente os mesmos bytes na hora de montar o pacote e já os inclui no HMAC. Assim, cada hop verifica a integridade do que recebeu sem nunca saber quantas camadas existem antes ou depois dele.

Filler e pad não são a mesma coisa. O filler é a cauda que reaparece conforme os hops descascam a cebola. A chave pad gera o preenchimento inicial do pacote na origem, quando a rota é curta e sobra espaço nos 1300 bytes.

Ferramenta: construtor didático de rota onion

A ferramenta abaixo calcula valor e CLTV de trás para frente e mostra o que cada hop veria em sua camada. Ela é didática: não implementa Sphinx real, não cria HMACs reais e não deve ser usada para construir pagamentos reais. Use apenas nomes e valores fictícios.

Construtor de Rota Onion

Construtor de Rota Onion

Calcule, do destino para a origem, o valor, a taxa e o CLTV de cada hop. É uma simulação didática do payload por hop, não um pacote Sphinx real.

Resultado da rota onion

Erros onion

Quando algo falha, o erro precisa voltar para a origem sem revelar o diagnóstico aos intermediários. O hop que detecta a falha monta uma failure message, autentica com a chave um, ofusca com um stream derivado da chave ammag e devolve o pacote pelo caminho reverso. Cada hop anterior aplica sua própria camada de ofuscação ammag. No fim, só a origem consegue descascar todas as camadas, testar o HMAC de cada hop e descobrir exatamente onde a tentativa falhou.

Estrutura da failure message

O pacote de erro tem tamanho fixo, pela mesma razão do pacote onion: o tamanho não deve revelar qual erro ocorreu nem a que distância aconteceu.

failure onion (devolvido ao pagador):

  hmac         32 bytes   HMAC com a chave um do hop que falhou
  failure_len   2 bytes   tamanho da failuremsg (u16)
  failuremsg    variável  código de 2 bytes + dados do erro
  pad_len       2 bytes   tamanho do preenchimento (u16)
  pad           variável  zeros de preenchimento

  regra: failure_len + pad_len >= 256
         (o recomendado é somar exatamente 256)
         para que o tamanho não revele qual erro ocorreu.

A failuremsg começa com um código de 2 bytes. Alguns códigos carregam dados extras — por exemplo, um channel_update assinado, para que a origem atualize sua visão das taxas ou do estado do canal antes de tentar de novo.

update_fail_htlc × update_fail_malformed_htlc

Há dois jeitos de falhar um HTLC recebido, e a diferença importa:

Códigos de falha

Cada código de 2 bytes combina, no byte alto, bits de categoria que dizem como tratar a falha:

Alguns códigos de falha da BOLT 4
Categoria Código Significado
BADONION | PERM invalid_onion_hmac O HMAC do pacote onion não confere; a cebola chegou corrompida.
PERM unknown_next_peer O short_channel_id pedido não leva a nenhum peer conhecido.
UPDATE temporary_channel_failure Falha temporária no canal, por exemplo liquidez insuficiente agora.
UPDATE fee_insufficient A taxa embutida no HTLC é menor que a política atual do canal.
UPDATE incorrect_cltv_expiry O CLTV de saída não respeita o cltv_expiry_delta anunciado pelo hop.
NODE temporary_node_failure O nó está temporariamente incapaz de processar o encaminhamento.
PERM incorrect_or_unknown_payment_details O destino não reconhece o pagamento (hash, valor ou payment_secret).
BADONION | PERM invalid_onion_blinding Erro em caminho cegado; todos os hops devolvem este mesmo código para não vazar a posição da falha.

Isso permite retry inteligente. Se um canal falhou por liquidez insuficiente, política desatualizada, CLTV incorreto ou canal desativado, a carteira pode penalizar aquele trecho e tentar outra rota. Essa informação alimenta heurísticas de pathfinding, mas não vira verdade global sobre a rede.

Erros também vazam informação para a origem. Isso é necessário para retry, mas também permite probing: enviar tentativas para aprender sobre liquidez e topologia privada.

Route blinding

No onion routing clássico, a origem conhece todos os hops, inclusive o destino. Isso protege bastante contra intermediários, mas não esconde o destino da origem. Route blinding, também especificado na BOLT 4, permite que o recebedor forneça uma parte final da rota em forma cegada.

Em alto nível:

  1. O recebedor escolhe um ponto de entrada público ou alcançável.
  2. O recebedor cria uma sequência de hops cegados até ele.
  3. Para cada hop cegado, o recebedor fornece encrypted_recipient_data com instruções cifradas.
  4. O pagador monta a rota até o ponto de entrada e inclui os dados cegados no payload.
  5. Os hops cegados encaminham usando as instruções cifradas, sem revelar ao pagador a identidade real do destino final.

Em caminhos cegados, qualquer falha é convertida no mesmo código invalid_onion_blinding (e devolvida via update_fail_malformed_htlc), justamente para que a posição da falha dentro do trecho cegado não vaze.

BOLT 12 usa esse mecanismo para offers e invoices modernas. Por isso, em BOLT 12, a privacidade do recebedor melhora: o pagador não precisa aprender necessariamente o node id público final.

Privacidade e limites

O onion routing protege contra um hop individual curioso, mas não contra todos os adversários. Alguns limites reais:

Armadilhas comuns

Resumo

Mapa de dependências conceituais

Antes de ler esta página, ajuda conhecer:

Depois desta página, siga para:

Referências técnicas usadas

A seguir: como os nós anunciam canais públicos, políticas de roteamento e identidades para formar o grafo usado na busca de caminho.