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:
- BOLT 8: define handshake, criptografia, autenticação e framing criptografado.
- BOLT 1: define como interpretar a mensagem plaintext depois que o transporte criptografado entregou os bytes.
- BOLTs específicos: definem o conteúdo do payload de cada mensagem, como canais na BOLT 2, onion na BOLT 4 e gossip na BOLT 7.
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.
| Hex | Tipo | Interpretação inicial |
|---|---|---|
0010 | 16 | init, sem payload no exemplo incompleto. |
001200040000 | 18 | ping, com campos de controle no payload. |
0100 | 256 | channel_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.
| Faixa | Uso principal | Exemplos |
|---|---|---|
| 0-31 | Setup e controle da conexão. | warning, init, error, ping, pong |
| 32-127 | Abertura, fechamento e parâmetros de canal. | open_channel, accept_channel, shutdown |
| 128-255 | Atualizações de commitment e HTLC. | update_add_htlc, commitment_signed, revoke_and_ack |
| 256-511 | Gossip e descoberta de rede. | channel_announcement, node_announcement, channel_update |
| 32768-65535 | Mensagens 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.
| 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 ]
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.
| Valor | Formato | Exemplo |
|---|---|---|
| < 0xfd | 1 byte | 252 vira fc |
| < 0x10000 | fd + u16 | 253 vira fd00fd |
| < 0x100000000 | fe + u32 | 65536 vira fe00010000 |
| maior | ff + u64 | 4294967296 vira ff0000000100000000 |
Um stream TLV canônico também precisa obedecer a duas regras estruturais:
- os tipos aparecem em ordem estritamente crescente;
- não há dois registros com o mesmo tipo.
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.
| Mensagem | Tipo | Função |
|---|---|---|
warning | 1 | Comunica problema não fatal. Por ser tipo ímpar, peers antigos que não entendem podem ignorar. |
init | 16 | Negocia features logo após a autenticação do transporte. |
error | 17 | Comunica falha fatal em um canal específico ou em todos os canais com o peer. |
ping | 18 | Testa vivacidade e permite padding de tráfego. |
pong | 19 | Responde 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
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
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
- Tratar o wire protocol como se fosse TCP puro. A BOLT 1 opera depois da decriptação feita pela BOLT 8.
- Ler o
typeem little-endian por hábito vindo de transações Bitcoin. - Aceitar
BigSizenão mínimo, como codificar 252 com prefixofd. - Permitir TLVs fora de ordem ou duplicados.
- Ignorar uma mensagem de tipo par desconhecido em vez de falhar a conexão.
- Renderizar o texto de
errorcomo HTML confiável. - Confundir feature opcional com feature automaticamente suportada.
- Validar apenas a estrutura genérica e esquecer as regras do BOLT específico da mensagem.
Resumo
- BOLT 8 entrega bytes decriptados; BOLT 1 interpreta a estrutura básica da mensagem.
- Toda mensagem começa com
typede 2 bytes em big-endian. - O payload é definido pelo BOLT específico daquele tipo de mensagem.
- TLV usa
BigSize, codificação mínima e tipos em ordem estritamente crescente. - Valores pares desconhecidos exigem entendimento; valores ímpares desconhecidos podem ser ignorados quando a regra do contexto permite.
initanuncia features, e a BOLT 9 define a semântica dos bits pares e ímpares.
Mapa de dependências conceituais
Antes de ler esta página, ajuda conhecer:
Depois desta página, siga para:
Referências técnicas usadas
- BOLT 1 — Base Protocol
- BOLT 8 — Encrypted and Authenticated Transport
- BOLT 9 — Assigned Feature Flags
- BOLT 2 — Peer Protocol for Channel Management
- BOLT 7 — P2P Node and Channel Discovery
A seguir: uma visão organizada das mensagens do protocolo, com tipo numérico, direção, BOLT relacionado e função operacional.