Data Layouts
O SDK Wormhole e o Sistema de Layout
O Wormhole SDK utiliza o pacote layout para definir, serializar e desserializar estruturas de dados de maneira eficiente. Esse sistema modular garante formatação consistente de dados e compatibilidade entre ambientes, sendo ideal para projetos que exigem um gerenciamento robusto de dados estruturados.
Benefícios do Sistema de Layout
Compreender o mecanismo de layout permite que você:
Defina estruturas de dados: como números, arrays e tipos personalizados.
Serialização e desserialização eficientes: converta dados estruturados para binário e vice-versa.
Lide com layouts específicos do protocolo: ideal para aplicações que envolvam comunicação cross-chain.
Essa abordagem é particularmente útil para desenvolvedores que desejam integrar o Wormhole em suas aplicações, especialmente ao trabalhar com payloads complexos ou comunicação entre blockchains.
Conceitos-Chave
Itens de Layout
Um layout define como estruturas de dados devem ser:
Serializadas: convertidas para um formato binário.
Desserializadas: reconstruídas para sua estrutura original.
Um layout é composto por itens de layout, que especificam campos individuais ou conjuntos de campos na sua estrutura de dados. Cada item de layout inclui:
name
: Nome do campo.binary
: Tipo de dado (e.g.,uint
,bytes
).size
: Tamanho fixo em bytes (para tipos comouint
ebytes
).
Exemplo de Layout
Abaixo, um exemplo de layout para serializar uma mensagem no protocolo Wormhole:
const exampleLayout = [
{ name: 'sourceChain', binary: 'uint', size: 2 },
{ name: 'orderSender', binary: 'bytes', size: 32 },
{ name: 'redeemer', binary: 'bytes', size: 32 },
{ name: 'redeemerMessage', binary: 'bytes', lengthSize: 4 },
] as const;
Neste exemplo:
sourceChain
: Inteiro não assinado de 2 bytes, identifica a blockchain de origem.orderSender
: Array de bytes fixo (32 bytes), representa o endereço do remetente.redeemer
: Outro array de 32 bytes, usado para o endereço do recebedor.redeemerMessage
: Sequência de bytes de comprimento variável, especificada por um inteiro de 4 bytes.
Essa definição garante que todos os campos necessários sejam codificados de maneira consistente e interpretados corretamente.
Serialização e Desserialização
Serialização
Converte dados estruturados em formato binário. Use a função serializeLayout
:
const examplePayload = {
sourceChain: 6,
orderSender: new Uint8Array(32),
redeemer: new Uint8Array(32),
redeemerMessage: new Uint8Array([0x01, 0x02, 0x03]),
};
const serializedData = serializeLayout(exampleLayout, examplePayload);
Resultado: serializedData
será um Uint8Array
representando os dados em binário.
Desserialização
Reconstrói objetos estruturados a partir de binário. Use a função deserializeLayout
:
const deserializedPayload = deserializeLayout(exampleLayout, serializedData);
Resultado: Um objeto reconstruído, facilitando o trabalho com dados transmitidos entre blockchains.
Conversões Personalizadas
Os layouts permitem mapeamentos personalizados para tipos complexos. Isso é útil ao lidar com dados que não se encaixam em tipos simples como inteiros ou arrays de bytes.
Exemplo: Conversão personalizada para um Chain ID:
const chainCustomConversion = {
to: (chainId: number) => toChain(chainId),
from: (chain: Chain) => chainToChainId(chain),
} satisfies CustomConversion<number, Chain>;
Essa configuração permite conversões entre formatos legíveis e dados codificados usados em payloads.
Tratamento de Erros
O sistema de layout realiza verificações durante a serialização/desserialização. Erros são lançados se os dados estiverem no formato ou tamanho errado.
Exemplo de tratamento de erro:
try {
deserializeLayout(fillLayout, corruptedData);
} catch (error) {
console.error('Erro durante a desserialização:', error.message);
}
Aplicações de Layouts
Definição de Layouts
Estruturas são definidas como listas de campos:
const exampleLayout = [
{ name: 'sourceChain', binary: 'uint', size: 2 },
{ name: 'orderSender', binary: 'bytes', size: 32 },
{ name: 'redeemer', binary: 'bytes', size: 32 },
{ name: 'redeemerMessage', binary: 'bytes', lengthSize: 4 },
] as const;
Campos de Comprimento Variável
O layout suporta arrays e sequências de bytes com comprimento variável, usando lengthSize
para especificar o tamanho do campo:
{ name: 'message', binary: 'bytes', lengthSize: 4 }
O sistema de layout do Wormhole SDK oferece uma maneira eficiente de gerenciar dados estruturados para comunicação cross-chain, garantindo consistência e suporte a casos de uso complexos, como payloads de comprimento variável e conversões personalizadas.
Layouts Aninhados e Tipagem Forte O Wormhole SDK simplifica o manuseio de estruturas complexas ao adotar layouts aninhados e tipagem forte. Enquanto os layouts aninhados organizam dados hierárquicos de forma clara, a tipagem forte garante consistência dos dados e identifica erros durante o desenvolvimento.
Layouts Aninhados
Em protocolos complexos, estruturas aninhadas são comuns. Esses layouts permitem organizar dados hierárquicos (como transações ou mensagens multipartes) em formatos estruturados.
Exemplo de layout aninhado onde uma mensagem possui campos aninhados:
const nestedLayout = [
{
name: 'source',
binary: 'bytes',
layout: [
{ name: 'chainId', binary: 'uint', size: 2 },
{ name: 'sender', binary: 'bytes', size: 32 },
],
},
{
name: 'redeemer',
binary: 'bytes',
layout: [
{ name: 'address', binary: 'bytes', size: 32 },
{ name: 'message', binary: 'bytes', lengthSize: 4 },
],
},
] as const satisfies Layout;
Aqui:
source
contém dois campos:chainId
esender
.redeemer
possuiaddress
e uma mensagem com comprimento prefixado.
Tipagem Forte
Ao usar TypeScript, o Wormhole SDK implementa tipagem forte para garantir que dados serializados/deserializados sigam a estrutura esperada, minimizando erros.
Com o utilitário LayoutToType
, cria-se uma estrutura tipada a partir de um layout:
type NestedMessage = LayoutToType<typeof nestedLayout>;
const message: NestedMessage = {
source: {
chainId: 6,
sender: new Uint8Array(32),
},
redeemer: {
address: new Uint8Array(32),
message: new Uint8Array([0x01, 0x02, 0x03]),
},
};
Qualquer dado com tipo incorreto resulta em erro em tempo de compilação.
Serialização e Desserialização
Com layouts aninhados, a serialização e desserialização seguem o mesmo padrão:
const serializedNested = serializeLayout(nestedLayout, message);
const deserializedNested = deserializeLayout(nestedLayout, serializedNested);
A tipagem forte em TypeScript assegura que os objetos de mensagem estejam em conformidade com o layout.
Layouts Usados Comumente
O SDK inclui layouts otimizados para campos padrão como IDs de blockchain, endereços e assinaturas.
Chain ID Layouts
Os layouts de ID de blockchain derivam do chainItemBase
, que define a representação binária de um ID como um inteiro não assinado de 2 bytes:
const chainItemBase = { binary: 'uint', size: 2 } as const;
Dynamic Chain ID Layout: Permite validação em tempo de execução e suporte a valores nulos.
export const chainItem = <const C extends readonly Chain[] = typeof chains>(
opts?: { allowedChains?: C; allowNull?: boolean },
) => ({
...chainItemBase,
custom: {
to: (val: number): AllowNull<C[number], N> => { ... },
from: (val: AllowNull<C[number], N>): number => { ... },
},
});
Fixed Chain ID Layout: Rígido, ideal para um único ID de blockchain.
export const fixedChainItem = <const C extends Chain>(chain: C) => ({
...chainItemBase,
custom: {
to: chain,
from: chainToChainId(chain),
},
});
Layouts de Assinatura
O layout de assinatura no Wormhole define como serializar/desserializar assinaturas criptográficas.
const signatureLayout = [
{ name: 'r', binary: 'uint', size: 32 },
{ name: 's', binary: 'uint', size: 32 },
{ name: 'v', binary: 'uint', size: 1 },
] as const satisfies Layout;
Custom Conversion: Integra dados binários brutos com objetos de assinatura de alto nível.
export const signatureItem = {
binary: 'bytes',
layout: signatureLayout,
custom: {
to: (val: LayoutToType<typeof signatureLayout>) =>
new Signature(val.r, val.s, val.v),
from: (val: Signature) => ({ r: val.r, s: val.s, v: val.v }),
},
} satisfies BytesLayoutItem;
Essa integração melhora a confiabilidade no processamento de dados de assinatura.
VAAs e Protocolos Wormhole
As VAAs (Verified Action Approval) são mensagens assinadas que conectam cadeias distintas. O layout para VAAs organiza dados em três componentes principais:
Header: Metadados como índice do conjunto de guardiões e assinaturas.
Envelope: Detalhes específicos da cadeia, como ID de emissor e sequência.
Payload: Dados de aplicação.
Header Example:
export const headerLayout = [
{ name: 'version', binary: 'uint', size: 1, custom: 1, omit: true },
{ name: 'guardianSet', ...guardianSetItem },
{
name: 'signatures',
binary: 'array',
lengthSize: 1,
layout: guardianSignatureLayout,
},
] as const satisfies Layout;
A arquitetura modular do Wormhole SDK facilita a interoperabilidade entre cadeias, garantindo consistência nos dados.
Como funciona?
Internamente, a função serialize
combina dinamicamente o baseLayout
(cabeçalho e envelope) com o layout do payload definido pelo payloadLiteral
. O layout completo é então passado para a função serializeLayout
, que converte os dados no formato binário.
const layout = [
...baseLayout, // Layout do cabeçalho e envelope
payloadLiteralToPayloadItemLayout(vaa.payloadLiteral), // Layout do payload
] as const;
return serializeLayout(layout, vaa as LayoutToType<typeof layout>);
Desserializando os dados do VAA
O SDK Wormhole fornece a função deserialize
para analisar um VAA no formato binário de volta para um objeto estruturado. Esta função usa o baseLayout
e a lógica do discriminador de payload para garantir que o VAA seja interpretado corretamente.
import { deserialize } from '@wormhole-foundation/sdk-core/vaa/functions';
const serializedVAA = new Uint8Array([
/* Dados binários do VAA serializado */
]);
const vaaPayloadType = 'SomePayloadType'; // Tipo de payload esperado para este VAA
const deserializedVAA = deserialize(vaaPayloadType, serializedVAA);
Como funciona?
Internamente, a função deserialize
usa o baseLayout
(cabeçalho e envelope) para analisar a estrutura principal do VAA. Ela então identifica o layout do payload apropriado usando o tipo de payload ou discriminador fornecido.
const [header, envelopeOffset] = deserializeLayout(headerLayout, data, {
consumeAll: false,
});
const [envelope, payloadOffset] = deserializeLayout(envelopeLayout, data, {
offset: envelopeOffset,
consumeAll: false,
});
const [payloadLiteral, payload] =
typeof payloadDet === 'string'
? [
payloadDet as PayloadLiteral,
deserializePayload(payloadDet as PayloadLiteral, data, payloadOffset),
]
: deserializePayload(
payloadDet as PayloadDiscriminator,
data,
payloadOffset
);
return {
...header,
...envelope,
payloadLiteral,
payload,
} satisfies VAA;
Registrando Payloads Personalizados
No SDK Wormhole, os payloads dependem de layouts para definir sua estrutura binária, garantindo consistência e segurança de tipo entre os protocolos. Payloads personalizados expandem essa funcionalidade, permitindo que os desenvolvedores tratem características específicas de protocolo ou casos de uso únicos.
Para aprender como definir e registrar payloads usando layouts, consulte a página "Building Protocols and Payloads" para um guia detalhado.
Erros Comuns e Melhores Práticas
Ao trabalhar com o sistema de layout do SDK Wormhole, é importante estar ciente de alguns problemas comuns que podem surgir. Abaixo estão alguns erros a evitar e melhores práticas para garantir uma integração tranquila.
Erros a Evitar
Definindo Tamanhos para Tipos de Dados
Ao definir os tamanhos para cada tipo de dado, certifique-se de que o comprimento real dos dados corresponda ao tamanho especificado para evitar erros de serialização e desserialização:
uint
eint
- o tamanho especificado deve ser grande o suficiente para acomodar o valor dos dados. Por exemplo, armazenar um valor maior que 255 em um único byte (uint8
) falhará, pois excede a capacidade do byte. Da mesma forma, um inteiro subdimensionado (por exemplo, especificar 2 bytes para um inteiro de 4 bytes) pode resultar em perda de dados ou falha na desserialização.bytes
- os dados devem corresponder ao comprimento de byte especificado no layout. Por exemplo, definir um campo como 32 bytes (tamanho: 32) exige que os dados fornecidos tenham exatamente 32 bytes de comprimento; caso contrário, a serialização falhará.
// Erro: Descompasso entre o tamanho dos dados e o tamanho definido no layout
{ name: 'orderSender', binary: 'bytes', size: 32 }
// Se os dados fornecidos não tiverem exatamente 32 bytes, isso falhará
Arrays Definidos Incorretamente
Arrays podem ser de comprimento fixo ou prefixados por comprimento, então é importante defini-los corretamente. Arrays de comprimento fixo devem corresponder ao comprimento especificado, enquanto arrays prefixados por comprimento necessitam de um campo lengthSize
.
// Erro: O comprimento do array não corresponde ao tamanho esperado
{ name: 'redeemerMessage', binary: 'bytes', lengthSize: 4 }
Melhores Práticas
Essas melhores práticas e erros comuns podem ajudar a prevenir bugs e melhorar a confiabilidade de sua implementação ao trabalhar com layouts no SDK Wormhole.
Reutilize Itens de Layout Predefinidos
Em vez de definir manualmente os tamanhos ou tipos, reutilize os itens de layout predefinidos fornecidos pelo SDK Wormhole. Esses itens garantem formatação consistente e aplicam tipagem forte.
Por exemplo, use o layout chainItem
para IDs de cadeia ou universalAddressItem
para endereços de blockchain:
import {
chainItem,
universalAddressItem,
} from '@wormhole-foundation/sdk-core/layout-items';
const exampleLayout = [
{ name: 'sourceChain', ...chainItem() }, // Use o layout predefinido para ID da cadeia
{ name: 'senderAddress', ...universalAddressItem }, // Use o layout para endereço universal
] as const;
Ao aproveitar os itens de layout predefinidos, você reduz redundâncias, mantém a consistência e garante compatibilidade com os padrões do Wormhole.
Use Instâncias de Classe
Sempre que possível, converta os dados desserializados em instâncias de classes de nível superior. Isso facilita a validação, manipulação e interação com dados estruturados. Por exemplo, a classe UniversalAddress
garante o manuseio consistente de endereços:
import { UniversalAddress } from '@wormhole-foundation/sdk-core';
const deserializedAddress = new UniversalAddress(someBinaryData);
Focar em reutilizar itens de layout predefinidos e converter dados desserializados em abstrações de nível superior pode garantir uma implementação mais robusta e de fácil manutenção.
Tratamento Consistente de Erros
Sempre trate os erros durante as operações de serialização e desserialização. Capturar exceções permite registrar ou resolver problemas de forma elegante ao trabalhar com dados potencialmente corrompidos ou inválidos.
try {
const deserialized = deserializeLayout(fillLayout, data);
} catch (error) {
console.error('Falha na desserialização:', error);
}
Aproveite Layouts Reutilizáveis
Criar layouts reutilizáveis para estruturas comumente repetidas melhora a manutenção do código e reduz a duplicação. Esses layouts podem representar campos ou combinações de campos frequentemente encontrados em comunicação cross-chain, como IDs de cadeia, endereços e assinaturas.
Por exemplo, defina um layout reutilizável para IDs de cadeia e endereços:
const commonLayout = [
{ name: 'chainId', binary: 'uint', size: 2 },
{ name: 'address', binary: 'bytes', size: 32 },
] as const satisfies Layout;
// Reutilize o layout comum em diferentes contextos
const exampleLayout = [
...commonLayout,
{ name: 'sequence', binary: 'uint', size: 8 },
];
Ao abstrair elementos comuns em um único layout, você garante consistência em diferentes partes da aplicação e simplifica futuras atualizações.
Considerações de Performance
A serialização e desserialização eficientes são cruciais ao lidar com grandes quantidades de dados cross-chain. Abaixo estão algumas estratégias e melhores práticas para garantir o desempenho ideal ao usar layouts do SDK Wormhole.
Instanciação Preguiçosa
A criação de um discriminador pode ser intensiva em recursos para conjuntos de dados complexos ou grandes. As estruturas de layout não acarretam custos significativos de forma antecipada, mas adiar a criação de discriminadores até que sejam necessários pode melhorar a eficiência.
const lazyDiscriminator = lazyInstantiate(() => layoutDiscriminator(layouts));
Essa abordagem garante que os discriminadores sejam criados apenas quando necessário, ajudando a otimizar o desempenho, especialmente para layouts complexos ou condicionais.
Recursos
Para mais aprendizado e experiência prática, explore os seguintes recursos:
Wormhole TypeScript SDK - O repositório do SDK Wormhole contém a implementação central dos layouts, incluindo itens de layout predefinidos e utilitários como
serializeLayout
edeserializeLayout
.Repositório de testes de layouts - Para experimentação prática, confira o repositório do pacote de layouts, que fornece exemplos e testes unitários para ajudar a entender melhor a serialização, desserialização e o mecanismo de tipagem forte. Executar esses testes localmente é uma ótima maneira de aprofundar seu entendimento de como os layouts funcionam em cenários do mundo real.
Last updated