Protocolo Wire

Como mensagens Lightning são serializadas, identificadas e estendidas

Lightning · Técnico

Nas páginas anteriores, vimos que canais Lightning são atualizados por mensagens como update_add_htlc, commitment_signed e revoke_and_ack. Esta página desce um nível: antes de uma implementação entender que uma mensagem é "adicionar um HTLC", ela precisa ler bytes, descobrir o tipo da mensagem, separar o payload e validar extensões.

Esse é o papel do protocolo wire. Ele define a forma básica das mensagens que trafegam entre peers Lightning. A fonte primária desta página é a BOLT 1. Quando falamos do transporte criptografado que entrega esses bytes, a fonte é a BOLT 8. Quando falamos de negociação de funcionalidades, usamos a BOLT 9.

Conteúdo

Onde o wire entra

Uma mensagem Lightning não é enviada em texto puro na rede TCP. A conexão entre peers usa o transporte criptografado da BOLT 8. Primeiro os nós fazem um handshake criptográfico, autenticam a chave pública do peer e passam a trocar frames criptografados. Só depois da decriptação aparece a mensagem que a BOLT 1 descreve.

Separar essas camadas evita uma confusão comum:

Formato básico de uma mensagem Lightning depois da decriptação: um campo type de 2 bytes seguido pelo payload e por uma extensão TLV opcional.

Em outras palavras: o wire protocol desta página começa quando o nó já tem bytes de uma mensagem Lightning decriptada. Ele não substitui o transporte criptografado; ele fica dentro dele.

Formato básico da mensagem

Depois da decriptação, uma mensagem Lightning tem esta forma geral:

[ type: u16 big-endian ][ payload: bytes ][ extension: tlv_stream opcional ]

O campo type sempre ocupa 2 bytes em big-endian. Isso significa que o hex 0010 representa o tipo decimal 16, que é a mensagem init. O payload vem depois e seu formato depende do tipo da mensagem. Algumas mensagens também podem carregar uma extensão TLV no final. TLVs ímpares desconhecidos podem ser ignorados; TLVs pares exigem suporte, negociação ou conhecimento específico do schema da mensagem.

A BOLT 1 também define uma regra geral importante: campos numéricos são inteiros sem sinal em big-endian, salvo quando a especificação daquele campo disser outra coisa. Isso é diferente de várias estruturas do Bitcoin on-chain, onde muitos campos usam little-endian. Por isso, ao alternar entre transações Bitcoin e mensagens Lightning, não assuma a mesma ordem de bytes.

Exemplos mínimos de mensagens pelo campo type
Hex Tipo Interpretação inicial
001016init, sem payload no exemplo incompleto.
00120004000018ping, com campos de controle no payload.
0100256channel_announcement, payload definido pela BOLT 7.

O parser genérico não precisa entender todo o payload para identificar o tipo. Ele só precisa ler os dois primeiros bytes, aplicar a tabela de tipos conhecida e entregar o restante para o decodificador apropriado.

Tipos de mensagem

Os valores de type são organizados por faixas. Isso ajuda implementações, leitores e ferramentas a reconhecerem rapidamente a família de uma mensagem.

Faixas de tipos de mensagem na BOLT 1
Faixa Uso principal Exemplos
0-31Setup e controle da conexão.warning, init, error, ping, pong
32-127Abertura, fechamento e parâmetros de canal.open_channel, accept_channel, shutdown
128-255Atualizações de commitment e HTLC.update_add_htlc, commitment_signed, revoke_and_ack
256-511Gossip e descoberta de rede.channel_announcement, node_announcement, channel_update
32768-65535Mensagens customizadas.Extensões fora do conjunto base.

A mensagem plaintext tem tamanho máximo de 65535 bytes. Isso não significa que todo payload útil possa chegar perto desse limite sem cuidado. Cada mensagem específica ainda tem suas regras de tamanho, quantidade de campos e validade semântica.

A regra par/ímpar

A frase curta da Lightning para compatibilidade é "it's ok to be odd". A ideia é simples: quando um número é ímpar, ele tende a representar algo que pode ser ignorado com segurança se a implementação não entender. Quando é par, a implementação precisa entender para continuar de forma segura.

Comportamento para tipos desconhecidos
Objeto Valor par desconhecido Valor ímpar desconhecido
Tipo de mensagem O peer deve falhar a conexão. A mensagem pode ser ignorada.
Tipo TLV em extensão O parse da mensagem falha. O registro pode ser descartado.
Feature bit Funcionalidade obrigatória: se não entende, não deve aceitar. Funcionalidade opcional: se não entende, pode ignorar.

Essa regra permite evolução gradual do protocolo. Uma implementação nova pode enviar dados opcionais ímpares sem quebrar peers antigos. Para uma mudança que afeta segurança ou interpretação obrigatória, usa-se um valor par, forçando o peer a rejeitar se não souber processar.

Par/ímpar não quer dizer "seguro" ou "inseguro" por si só. Quer dizer "obrigatório entender" ou "pode ignorar". A segurança depende do contexto definido pelo BOLT daquela mensagem.

TLV e BigSize

TLV significa type-length-value: tipo, tamanho e valor. Em Lightning, esses campos usam o inteiro variável BigSize. Um stream TLV é uma sequência de registros, cada um com:

[ type: BigSize ][ length: BigSize ][ value: length bytes ]
Registro TLV com três partes: type codificado como BigSize, length codificado como BigSize e value com a quantidade de bytes indicada pelo length.

O BigSize é parecido em espírito com o CompactSize do Bitcoin, mas não é o mesmo formato. Ele usa big-endian e precisa estar em sua codificação mínima. Se um número caberia em 1 byte, ele não deve ser codificado com prefixo de 3, 5 ou 9 bytes.

Codificação BigSize
Valor Formato Exemplo
< 0xfd1 byte252 vira fc
< 0x10000fd + u16253 vira fd00fd
< 0x100000000fe + u3265536 vira fe00010000
maiorff + u644294967296 vira ff0000000100000000

Um stream TLV canônico também precisa obedecer a duas regras estruturais:

Por exemplo, o stream c9012acb0104 tem dois registros: type 201, length 1, value 2a; depois type 203, length 1, value 04. Já c90101c90102 é inválido como stream canônico porque repete o type 201.

init e feature bits

A primeira mensagem Lightning enviada por um peer depois da autenticação do transporte é init, tipo 16. Ela anuncia quais funcionalidades o nó suporta. A estrutura tradicional tem dois vetores de features e pode carregar extensões TLV:

type: 16
gflen: u16
globalfeatures: gflen bytes
flen: u16
features: flen bytes
tlvs: init_tlvs opcional

O campo globalfeatures existe por compatibilidade histórica. Implementações modernas ainda precisam lê-lo corretamente, mas o vetor features é o lugar principal para negociar funcionalidades. Ao interpretar init, o receptor combina globalfeatures e features em um mapa lógico de suporte, como um OR bit a bit para cada feature. A interpretação dos bits segue a BOLT 9: bits pares são obrigatórios e bits ímpares são opcionais.

Um init mínimo, sem features, fica assim:

0010 0000 0000
^^^^ ^^^^ ^^^^
type gflen flen

Na prática, peers reais anunciam features como option_data_loss_protect, var_onion_optin, payment_secret, option_static_remotekey, option_anchors e outras. O ponto importante para um parser é separar a leitura dos bytes da decisão de compatibilidade: primeiro parseie o vetor, depois aplique as regras de suporte obrigatório/opcional.

error, warning, ping e pong

Nem toda mensagem wire mexe em canal ou pagamento. Algumas existem para controle da conexão.

Mensagens de controle comuns
Mensagem Tipo Função
warning1Comunica problema não fatal. Por ser tipo ímpar, peers antigos que não entendem podem ignorar.
init16Negocia features logo após a autenticação do transporte.
error17Comunica falha fatal em um canal específico ou em todos os canais com o peer.
ping18Testa vivacidade e permite padding de tráfego.
pong19Responde a um ping com o tamanho esperado.

A mensagem error carrega um channel_id. Quando esse identificador é zero, o erro se aplica a todos os canais com aquele peer. O campo de dados do erro é texto arbitrário em bytes; uma ferramenta deve tratar isso como dado não confiável. Não execute, não renderize como HTML e não dependa desse texto para lógica crítica.

ping e pong parecem simples, mas são úteis para detectar peers mortos, manter a conexão ativa e testar se o canal criptografado continua respondendo. Eles também mostram um padrão que aparece em várias partes do protocolo: uma mensagem pequena, com campos fixos, pode carregar dados de padding para comportamento operacional.

Como escrever um parser seguro

Um parser wire robusto precisa ser chato no bom sentido. Ele lê tamanho, valida limite, checa codificação mínima, rejeita campos fora de ordem e só entrega dados para a próxima camada quando a estrutura estiver íntegra.

function parseLightningMessage(bytes) {
  if (bytes.length < 2) throw new Error('mensagem curta demais');
  if (bytes.length > 65535) throw new Error('mensagem grande demais');

  const type = readU16BigEndian(bytes, 0);
  const payload = bytes.slice(2);

  if (!isKnownMessageType(type)) {
    if (type % 2 === 0) throw new Error('tipo par desconhecido');
    return { type, ignored: true };
  }

  return parseKnownPayload(type, payload);
}

Para TLV, o cuidado principal é não aceitar variações equivalentes da mesma informação. Codificação mínima e ordem estrita impedem ambiguidade.

function parseTlvStream(bytes) {
  let offset = 0;
  let previousType = -1n;
  const records = [];

  while (offset < bytes.length) {
    const type = readMinimalBigSize(bytes, offset);
    offset = type.nextOffset;

    const length = readMinimalBigSize(bytes, offset);
    offset = length.nextOffset;

    if (type.value <= previousType) throw new Error('TLV fora de ordem');
    if (offset + Number(length.value) > bytes.length) throw new Error('TLV truncado');

    records.push({
      type: type.value,
      value: bytes.slice(offset, offset + Number(length.value)),
    });

    offset += Number(length.value);
    previousType = type.value;
  }

  return records;
}

Este pseudocódigo mostra a estrutura mental do parser, não uma implementação completa. Uma implementação real precisa validar limites de inteiro, campos específicos da mensagem, regras de negociação e comportamento de erro definido nos BOLTs correspondentes.

Ferramentas

Use as ferramentas abaixo com dados de teste. Comece pelos exemplos prontos: alguns mostram mensagens válidas, outros mostram erros propositais, como TLV duplicado, BigSize não mínimo e feature bits com par opcional/obrigatório ligado ao mesmo tempo. Elas rodam no navegador e não precisam de seed, chave privada, senha, macaroon, backup de nó ou dado sensível real.

Mensagens Wire e TLV

Mensagens Wire e TLV

Identifica o tipo de 2 bytes e valida a estrutura genérica de um stream TLV. Não valida o schema completo.

Tipo da mensagem

Os 2 primeiros bytes são o tipo em big-endian. O catálogo de nomes desta ferramenta é parcial.

Exemplos:

Resultado da mensagem wire

Stream TLV

Sequência type-length-value com BigSize mínimo e types em ordem crescente.

Exemplos:

Resultado do stream TLV

A primeira parte identifica o tipo de uma mensagem a partir dos dois primeiros bytes. A segunda desmonta um stream TLV didático e mostra type, length e value. Ela não conhece o schema de cada mensagem específica; por isso, use-a para entender a estrutura, não para decidir se uma mensagem real é semanticamente válida. Em um parser completo, um TLV par desconhecido falha se não houver suporte ou negociação para aquele campo.

Feature Bits BOLT 9

Bits de Funcionalidades (BOLT 9)

As mensagens init e os anúncios carregam um vetor de bits que diz o que um nó suporta. Cole o vetor em hex.

Use bytes big-endian, como aparecem na mensagem.

Exemplos:

Resultado dos feature bits

Esta ferramenta mostra quais bits estão ligados em um vetor de features e se cada bit é obrigatório ou opcional. Em uma implementação real, a decisão de aceitar ou rejeitar o peer depende das features que o seu nó entende e da etapa do protocolo onde aquele vetor aparece.

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: uma visão organizada das mensagens do protocolo, com tipo numérico, direção, BOLT relacionado e função operacional.