How do Solana entries become transactions?
A receiver orders data shreds, concatenates each complete serialized range, and decodes a vector of Solana entries. Each entry contains zero or more versioned transactions. The receiver parses signatures and messages, resolves static and lookup-table account indexes, and maps compiled instructions. Execution status, logs, balances, and finality require later replay or RPC reconciliation.
protocol desk · updated 2026-08-31
5 layers separate packet from transaction
The decoding path has five distinct layers: UDP datagram, validated shred, ordered entry-byte range, decoded entry, and versioned transaction. Instruction interpretation and execution results sit after those.
Each layer has its own boundary. UDP length bounds safe reads. The shred header defines meaningful payload. Data index defines byte order. DATA_COMPLETE defines a serialized vector boundary. Entry and transaction encodings define object lengths.
Skipping a layer creates fragile code. Searching each packet for transaction signatures ignores serialization framing. Treating one entry as one transaction discards vectors. Treating parsed instructions as successful execution invents outcomes that raw shreds do not carry.
Start with authenticated data shreds
Only data shreds contribute source bytes to the entry stream. Coding shreds first pass through FEC recovery and can recreate missing data packets, but their parity payload is never handed directly to the transaction decoder.
The receiver checks variant, version, slot, index, FEC metadata, size, leader signature or Merkle proof, and set consistency. Recovered data returns through equivalent validation.
This ordering keeps hostile UDP away from complex deserializers. A transaction parser accepts a bounded byte vector built from one authenticated slot view, not arbitrary public datagrams.
Assemble one completed byte range
Data shreds are placed by increasing data index. The receiver extracts each packet's meaningful data region and appends it to the current batch buffer only when all preceding indexes are present.
A DATA_COMPLETE flag closes the serialized entry-vector range. The final LAST_IN_SLOT shred also closes a range. Headers, zero padding, Merkle proof nodes, chaining material, and optional signatures remain outside the buffer.
If indexes 70 through 74 form a batch and index 72 is absent, later payloads cannot be concatenated around the hole. Length prefixes or object fields may cross that position. FEC or repair must restore the missing bytes first.
Deserialize a vector of entries
The completed byte range encodes a vector, not one entry. The decoder reads the vector framing and then each entry's hash count, hash, and transaction vector according to the supported ledger serialization.
Successful deserialization must consume the expected range. Trailing non-padding bytes can indicate a wrong boundary or unsupported encoding. An allocation length inside the serialized data must be bounded before reserving memory.
Carry the batch's start and end data indexes into the decoded result. Those coordinates make parser faults and recovery-induced delays traceable without pretending that an individual transaction maps to exactly one shred.
Flatten entries without losing order
Assign a slot-wide entry ordinal in decoded order. For each entry, assign transaction ordinals from its vector order. Tick entries with zero transactions still increment the entry ordinal.
The natural event coordinate is cluster context, slot, block view, entry ordinal, and transaction ordinal. The transaction signature is an important lookup key but not sufficient event context because a signed transaction can appear in several proposed forks or slots.
Do not reset entry ordinal at every DATA_COMPLETE batch. Batch boundaries are serialization and streaming boundaries inside one slot. The PoH sequence continues across them.
Versioned transaction is the outer object
Solana ledger entries carry versioned transactions. A versioned transaction pairs a list of signatures with a versioned message.
The message can use the legacy format or a supported version such as version zero. Version determines how the message is decoded and whether address table lookups appear. Unknown message versions must be rejected or surfaced as unsupported without guessing a nearby layout.
The transaction object supplies proposed signed intent. It does not include the RPC execution metadata structure. Logs, return data, fee outcome, and post balances arrive from replay or another executed-state source.
The first signature is the usual transaction ID
Solana APIs commonly identify a transaction by its first signature. The message header states how many account keys must sign, and the signature list must be consistent with that requirement under protocol validation.
A parser can extract the first 64-byte signature early once the complete transaction object is framed. It should still validate the transaction structure before emitting it as a usable event.
Use a binary signature representation in the hot path and encode base58 only at API or logging boundaries. Text encoding adds allocation and CPU at high packet rates. Preserve the complete signature list for multisigner transactions.
The message defines accounts and instructions
A message contains a header, account-key information, a recent blockhash, and compiled instructions. Each compiled instruction refers to a program and accounts by numeric index rather than repeat full public keys.
The decoder builds the effective account-key list, then maps program_id_index and each account index into that list. Instruction data remains an opaque byte string until a program-specific decoder handles it.
Bounds checks are mandatory. An instruction that references account index 19 with only 12 effective keys is malformed. Never index arrays based on unvalidated ledger bytes, even after outer shred authentication.
Legacy messages use static keys
In a legacy message, account keys needed by compiled instructions are carried in the message's static list. The header partitions signer and writable roles through counts and ordering rules.
The decoder can resolve program and account indexes from the transaction bytes alone. It still needs program-specific schemas to interpret instruction data and account-state context to predict effects.
Legacy refers to message format, not to legacy shred authentication. A Merkle data shred can carry an entry containing a legacy transaction message. Keep the two version domains separate in types and diagnostics.
Version-zero messages can use lookup tables
Version-zero messages add address table lookup references. Those references name lookup table accounts and indexes for writable and read-only addresses. The complete effective key list requires table contents from the relevant state.
A shred-only consumer can parse static keys, table account identifiers, and lookup indexes immediately. Full instruction resolution waits for a fork-aware lookup-table cache or a state service.
Using current finalized table contents for an unconfirmed proposed slot can be wrong if the table changed between contexts. Cache entries need slot and fork awareness, plus explicit behavior when state is absent. unresolved_lookup is more honest than an invented public key.
Program decoding is another boundary
Once program IDs and account keys are resolved, a program decoder can interpret instruction data. System Program transfers, token instructions, order-book operations, and custom protocols all use different binary schemas.
Dispatch by exact program ID and supported program version. A discriminator match without the expected program ID is not enough. Programs can upgrade, and account layouts can change.
Generic infrastructure should always retain raw instruction bytes and compiled indexes. A failed application decoder should not invalidate an otherwise valid transaction. Mark program_decode_status and continue emitting the base message.
Inner instructions are not in the message
Cross-program invocations occur during execution. RPC metadata can report inner instructions generated when one program invokes another. Raw transaction messages contain only top-level compiled instructions.
A shred parser cannot know the complete CPI tree, logs, return data, compute-unit consumption, or runtime error before replay. Static simulation can estimate some behavior against a chosen state, but that is a separate operation with state and timing assumptions.
Strategies should distinguish observed top_level_instruction from predicted_inner_action and executed_inner_instruction. Combining them creates false certainty around swaps, transfers, and liquidations.
Transaction order is not universal execution independence
Entries and transaction vectors provide a recorded order. Solana's runtime can schedule non-conflicting transactions in parallel while respecting account locks and ledger semantics.
A raw decoder should preserve leader order but avoid claiming that every adjacent transaction executed in strictly separated wall-clock intervals. What matters for state prediction is the runtime's bank semantics, account conflicts, and actual replay result.
For low-latency observation, entry order still gives a stable coordinate. It lets systems compare their decoded stream with later block data and reason about nearby transactions that may touch the same accounts.
Deduplication needs fork context
The same transaction signature can be forwarded repeatedly and can appear in competing proposed blocks. Deduplicating globally forever by signature can hide a fork transition or replay observation.
Use a bounded key containing transaction signature plus slot and block view for raw events. Maintain a separate signature-centric index for later executed outcome reconciliation.
When the same signature appears twice inside one coherent block view, preserve enough evidence to determine protocol validity rather than silently dropping one position. Event deduplication and ledger validation are related but different.
Reconciliation adds outcomes
Later, a validator or RPC source exposes executed transaction metadata. Match by cluster, slot or block identity, transaction signature, and ordinal where available.
Attach success or error, fee, log messages, compute units, inner instructions, loaded addresses, and pre/post balances from the authoritative executed source. Record commitment level and update it over time.
A raw transaction not found in the eventual canonical block should transition to dropped_orphaned or another explicit state. It should not disappear from audit history if a strategy acted on it.
Latency measurement needs stage timestamps
Capture datagram receive time, completed-batch time, entry-deserialized time, transaction-emitted time, account-resolution time, and executed-reconciliation time.
The difference between first relevant shred and batch completion reflects network order and boundary placement. Batch completion to transaction emission reflects decoder work. Lookup resolution can dominate if it hits remote state. Execution reconciliation belongs to a much later clock.
Use a monotonic time source for local stage durations. Slot and PoH position supply ledger order but are not substitutes for microsecond latency measurements.
The output contract should preserve partial knowledge. A base event can contain transaction signatures, message version, recent blockhash, static keys, lookup references, compiled instructions, slot coordinates, and source shred range. resolved_accounts can arrive as a later enrichment when lookup state exists. executed_outcome arrives later again.
This staged event model prevents a slow state lookup from holding transactions whose static fields are already useful. It also prevents consumers from assuming an absent field means an empty value. unknown, not_applicable, and resolved_empty are different states.
Backpressure between stages needs bounds. A lookup resolver that falls behind should not retain unlimited raw entry batches. Emit unresolved base events, shed optional enrichment according to policy, and preserve a reconciliation key. The UDP receive and shred decoder must keep capacity for current packets.
Test the mapping with transactions that use no lookup tables, one table, several tables, maximum supported account indexes, multiple signatures, unknown message versions, and malformed compiled instruction indexes. Compare decoded messages with validator or RPC representations for the same captured slot while remembering that RPC adds executed metadata.
For strategy systems, program decoding can run in a prioritized branch. Known market program IDs receive low-latency schemas. Unknown programs remain opaque without blocking the base transaction stream. Later decoder upgrades can replay stored bounded samples, but the hot path should not guess a schema from instruction bytes alone.
Every enrichment should preserve the original transaction bytes or a collision-resistant hash for later comparison.
In practice
Slot 365,772,410 yields a completed entry batch across data indexes 128 through 134. The six earlier packets and one recovered packet contribute 6,842 meaningful bytes.
Deserialization returns three entries. Entry ordinal 22 is a tick. Entry 23 contains 14 transactions. Entry 24 contains 9, for 23 transactions total. Transaction 23:6 uses a version-zero message with 12 static keys and two lookup references that add 7 writable and 5 read-only keys.
The parser emits the transaction signature, static keys, lookup table IDs, and compiled instructions at 84 microseconds after batch completion. Full account resolution arrives 310 microseconds later from a slot-aware cache. Execution metadata later reports that 21 of the 23 transactions succeeded. The raw entry stream never claimed those outcomes.
What this does not cover
This page covers the extraction path from complete data-shred ranges to versioned transaction messages. It does not define every legacy or version-zero message byte, program-specific instruction schema, address lookup table lifecycle, runtime scheduler, or execution metadata field.
Transaction parsing exposes proposed signed messages. It cannot produce inner instructions, logs, balances, fees, compute use, success, fork selection, or finality without replay or a later executed-state source.
Related questions
- Can a transaction be decoded from one shred?
- Only when the surrounding serialized framing happens to be complete, which receivers should not assume. Transactions live inside entry vectors whose bytes can cross several data shreds. The safe path orders meaningful payloads through a DATA_COMPLETE boundary, deserializes entries, then extracts each versioned transaction.
- What identifies a Solana transaction?
- Solana APIs commonly use the first transaction signature as the transaction identifier. Raw event identity still needs cluster, slot, block view, entry ordinal, and transaction ordinal because one signed transaction can appear in several proposals or forks. Preserve the complete signature list for transactions requiring multiple signers.
- Can version-zero account keys be resolved from shreds alone?
- Not always. The message includes static keys and address lookup table references, but dynamic addresses require the relevant table account contents. Correct resolution needs state for the proposed slot and fork. A shred-only parser should emit unresolved table identifiers and indexes when matching state is unavailable.
- Do raw transactions include inner instructions and logs?
- No. Inner instructions, logs, return data, compute consumption, and runtime errors arise during execution. Raw entries carry top-level signed transaction messages. Those execution details require local replay, simulation against specified state, or later RPC metadata and should be attached as a separate reconciliation stage.
- Should instruction data be decoded before emitting a transaction?
- Base transaction emission should not depend on every program-specific decoder. Resolve bounded message structure first, retain raw instruction bytes, and dispatch known program IDs to supported schemas. Unknown or upgraded programs can remain opaque while the valid transaction event still carries signatures, accounts, indexes, and data.
Read next
Ready to build against this? The documentation covers the implementation.