π’ Update (March 2026): Sails v1.0.0-beta.1π₯ is now released! The Sails Header (v1) described in this post has shipped as part of this release, alongside IDL V2, new interface ID and ReflectHash specs. This article was written in December 2025 when the specification was still in development β it is now live. β Release notes on GitHub
If youβve ever tried to build an indexer or a block explorer on top of Vara, you know the problem: you see a message, you see a payload, and thatβs it. Without the programβs metadata β the Wasm binary, the source, some side-channel ABI file β that payload is just bytes. You canβt tell a Token::Transfer from a Game::Attack.
The usual answer is βbuild it per-dAppβ. Every frontend hardcodes its own decoding logic. Every indexer needs execution context. It works, but it doesnβt scale, and it means the ecosystem stays fragmented by default.
Sails Header v1 is our answer to that. Itβs a 16-byte prefix on every message β no runtime changes, no consensus involvement, purely userspace β that turns opaque payloads into self-describing service calls.
The Optimistic Identification Trick
Before getting into the wire format, itβs worth explaining the most useful thing this enables, because itβs not obvious from the spec alone.
The Interface ID β an 8-byte field in the header β is a structural fingerprint of a service, computed deterministically at compile time from its functions, events, and base services. The key property: every program that implements the same interface gets the same ID. Not βsimilarβ β identical.
This means an off-chain tool doesnβt need to verify bytecode or query a registry to know what kind of contract itβs talking to. If messages are coming in with the FungibleToken Interface ID, you can optimistically treat it as a token. A wallet can render a βTransferβ screen immediately. An indexer can filter for all NFT transfers across the whole network with a single field comparison.
Vara network message arrives
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Header (16 bytes) β Payload β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
Interface ID = 0xa3f29c11β¦ β known fingerprint
β
ββββββββββ΄βββββββββ
β β
βΌ βΌ
π³ Wallet π Indexer
renders "Transfer" filters "NFT transfers"
UI instantly network-wide, no ABI needed
Itβs duck typing, but for smart contracts β and it works without any on-chain coordination.
What the Header Actually Looks Like
Each Sails message starts with a fixed 16-byte preamble:
Byte 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
ββββββ¬βββββ¬βββββ¬βββββ¬βββββββββββββββββββββββββββββββββ¬βββββββββββ¬βββββββ¬βββββββ
β 47 β 4D β 01 β 10 β Interface ID (8 bytes) β Entry ID β Rt β 00 β
β'G' β'M' β v1 βlen β β (2 bytes)β Idx β rsv β
ββββββ΄βββββ΄βββββ΄βββββ΄βββββββββββββββββββββββββββββββββ΄βββββββββββ΄βββββββ΄βββββββ
ββββββββββββββΊ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ
Magic routing fields then payload β
- Magic
GMβ parsers skip non-Sails messages instantly - Interface ID β which service (e.g. FungibleToken, DaoVoting)
- Entry ID β which method within that service
- Route Index β which instance, if a program hosts multiple services
- Reserved byte β always
0x00in v1; reserved for future versions
Routing Gets Cheaper
Thereβs an underrated benefit on the contract side too. Hereβs what routing looked like before versus now:
Before: string matching
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
payload β read bytes β allocate string β strcmp()
β
"VFT.Transfer"? β handler A
"VFT.Approve"? β handler B
"DAO.Vote"? β handler C
Cost: heap allocations + byte-by-byte comparisons + typo risk
After: integer dispatch
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
header β read 8 bytes (interface_id)
β read 2 bytes (entry_id)
β jump table
0xa3f2β¦ + 0x0002 βββΊ handler B
Cost: two integer comparisons, O(1), zero allocations
No string vulnerabilities, no gas wasted on parsing, no typos in method names causing silent failures.
Route Inference and the Zero Sentinel
The route_id field has a subtle design worth knowing if youβre building tooling.
0x00 isnβt βthe first routeβ β itβs a sentinel that means figure it out if thereβs no ambiguity. If the target program has exactly one instance of the given interface_id, the receiver resolves the route automatically. If there are multiple instances, the message is rejected as ambiguous. This sidesteps a footgun where zero would silently route to the wrong service.
Non-zero values are explicit: they map to named route instances via a per-program manifest. The spec deliberately leaves that manifest format up to implementations β it can be embedded in the IDL, distributed alongside the binary, or hardcoded in the client.
One consequence for off-chain tools: when you see route_id = 0x00, you canβt assume thereβs only one service. Check first.
How the Interface ID Is Computed
The fingerprint is built from three layers, hashed together with Keccak256 at compile time:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FUNCTIONS (sorted lexicographically) β
β β
β transfer(to: ActorId, amount: u128) β Result<(), Error> β
β β type=Command, name, args via ReflectHash, T + E β
β β
β balance_of(account: ActorId) β u128 β
β β type=Query, name, args via ReflectHash, return type β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β EVENTS β
β β
β enum Event { Transfer { from, to, value }, Approval {}} β
β adding a new variant changes the ID β signals upgrade β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β BASE SERVICES β
β β
β extends BaseService β mix in its Interface ID β
β proves which exact version of the base is implemented β
ββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
Keccak256( F β E β B )
β
take [0..8]
β
βΌ
Interface ID = 0xa3f29c11e8d04f02
A few details worth noting:
Result<T, E>β bothTandEare hashed. Error handling is part of the interface contract, not an afterthought.- Protocol wrappers like
CommandReply<T>are stripped before hashing β the ID reflects business logic, not transport. - Base service IDs are mixed in recursively, creating a chain of trust in 8 bytes.
- The service name is deliberately excluded. You can rename a service without breaking the wire protocol β the ID only changes when the actual structure changes.
That last point is an intentional tradeoff: stability over nominality. Two services named differently but structurally identical will share an ID. Whether thatβs a feature or a footgun depends on your use case, but for tooling it means you canβt infer the name from the ID alone β you still need the IDL for that.
Commands/queries and events also have independent entry_id spaces, both starting at zero. Keep that in mind when decoding: entry_id = 1 means different things depending on whether youβre looking at a function call or an event.
Zero Runtime Cost
For Rust: all of this happens in build.rs. The compiler embeds the IDs as constants. At runtime the program writes a static byte array. Thereβs no hashing at message time, no gas spent on it.
The Path Forward
The base header is fixed at 16 bytes, but header_len (byte 3) can go higher. Extensions use a TLV format β type_id (u8), flags (u8), length (u16), data β appended immediately after the base fields. Unknown type_ids are skipped by their declared length, so future extensions are forward-compatible by design. type_id = 0 is reserved and must never appear on the wire. The obvious first candidate is correlation IDs for tracing async message chains.
The reserved byte at offset 15 is always 0x00 in v1. Future versions may repurpose it β receivers should reject v1 headers where itβs non-zero.
The versioning and extension design was deliberate: define something stable enough to build tooling on, flexible enough not to need replacing in two years.
Key Technical Specs
| Field | Size | Description |
|---|---|---|
| Magic | 2 bytes | 0x47 0x4D (βGMβ) |
| Version | 1 byte | 0x01 |
| Header Len | 1 byte | Offset to payload (default 0x10) |
| Interface ID | 8 bytes | Keccak256(Functions β Events β BaseServices)[0..8] |
| Entry ID | 2 bytes | Lexicographically sorted method index |
| Route Index | 1 byte | Instance identifier (0x00 = infer if unambiguous) |
| Reserved | 1 byte | Must be 0x00 in v1 |
Ready to build? Check out the Sails Repository to get started.