Extract transactions from entries
Emit transaction identities, messages, instructions, and complete account keys without losing version context.
Before you start
- Ordered decoded Entry values
- An account-state source for address lookup tables when full keys are required
Preserve the versioned transaction
Each decoded Entry contains an ordered Vec<VersionedTransaction>. Emit the transaction together with slot, entry ordinal, transaction ordinal, data-range boundary, and arrival timing. Do not convert immediately to a legacy transaction shape because versioned messages have address lookup references that legacy messages do not.
The first transaction signature is commonly used as the transaction ID. Validate that the signature vector has the count required by the message header before indexing it. Transaction signatures authenticate the serialized message, but raw shred processing does not prove that execution succeeded or the slot survived.
rust
use solana_entry::entry::Entry;
use solana_transaction::versioned::VersionedTransaction;
struct Observed<'a> {
slot: u64,
entry_index: usize,
transaction_index: usize,
transaction: &'a VersionedTransaction,
}
fn extract(slot: u64, entries: &[Entry]) -> Vec<Observed<'_>> {
entries.iter().enumerate().flat_map(|(entry_index, entry)| {
entry.transactions.iter().enumerate().map(move |(transaction_index, transaction)| {
Observed { slot, entry_index, transaction_index, transaction }
})
}).collect()
}This preserves order and ownership. Clone or serialize only at a downstream boundary that needs it.
Distinguish legacy and v0 messages
A legacy message contains every account key in its static key list. A v0 message contains static keys plus address_table_lookups. Each lookup identifies an address lookup table account and two index vectors: writable and readonly.
Compiled instruction account indices in a v0 message refer to the combined account-key list:
- Static account keys in message order.
- All dynamically loaded writable addresses, following lookup order and each writable index order.
- All dynamically loaded readonly addresses, following lookup order and each readonly index order.
Do not append readonly addresses immediately after each table's writable addresses. Collect all writable results first, then all readonly results.
This pure resolver demonstrates the required order:
rust
use std::collections::HashMap;
#[derive(Clone)]
struct Lookup { table: [u8; 32], writable: Vec<u8>, readonly: Vec<u8> }
fn resolve(
static_keys: &[[u8; 32]],
lookups: &[Lookup],
tables: &HashMap<[u8; 32], Vec<[u8; 32]>>,
) -> Result<Vec<[u8; 32]>, String> {
let mut writable = Vec::new();
let mut readonly = Vec::new();
for lookup in lookups {
let addresses = tables.get(&lookup.table).ok_or("lookup table missing")?;
for &i in &lookup.writable {
writable.push(*addresses.get(usize::from(i)).ok_or("writable lookup index out of range")?);
}
for &i in &lookup.readonly {
readonly.push(*addresses.get(usize::from(i)).ok_or("readonly lookup index out of range")?);
}
}
let mut keys = Vec::with_capacity(static_keys.len() + writable.len() + readonly.len());
keys.extend_from_slice(static_keys);
keys.extend(writable);
keys.extend(readonly);
Ok(keys)
}
fn main() {
if resolve(&[], &[], &HashMap::new()).unwrap().is_empty() == false {
std::process::exit(1);
}
}Load tables at the correct state
The transaction alone does not contain lookup table addresses. Obtain each table account from a state source and deserialize it with the address lookup table program's state implementation. Validate account owner, activation and deactivation rules, slot-hash context where required, and index bounds.
For speculative raw shreds, a normal RPC node may not yet expose the proposed slot's account state. Resolving against the latest confirmed table can be wrong if the leader's fork has extended, deactivated, or otherwise changed the table. Choose an explicit mode:
- Emit unresolved versioned transactions immediately, with static keys and lookup references.
- Resolve against a labelled confirmed-state snapshot, accepting that it is contextual rather than execution-exact.
- Maintain a fork-aware local account-state pipeline and apply earlier transactions in order.
The third mode approaches validator replay complexity. Do not describe confirmed-state resolution as proof of the account set the proposed transaction will use.
Parse instructions after key resolution
Each compiled instruction supplies a program ID index, account index vector, and opaque data bytes. Resolve indices against the combined keys. Reject out-of-range indices. Program-specific decoding then uses the program ID and the instruction's versioned binary schema.
Address positions also determine signer and writable status. Static signer and readonly counts come from the message header. Dynamically loaded writable addresses are writable but not signers; loaded readonly addresses are readonly and not signers.
Keep the raw instruction data beside any parsed representation. Programs upgrade and custom programs have no universal schema. An unknown program is not a malformed transaction.
Verify and reconcile
Transaction signature verification is separate from shred leader verification. Use the signers identified by the message header and verify each signature over the serialized versioned message. Batch where measured, but preserve individual failures.
Do not claim execution success from extraction. A transaction can fail, the block can be abandoned, or the slot can disappear from the confirmed fork. Emit an observation status such as proposed, then reconcile by signature and slot against processed, confirmed, or finalized data.
Test legacy and v0 messages, multiple lookup tables, mixed writable and readonly indices, missing accounts, wrong owners, deactivated tables, index overflow, empty entries, multiple signatures, unknown programs, failed transactions, and a transaction observed again on another fork.
Cache lookup table accounts by table key and an explicit state version, never by key alone across an unlimited slot range. Invalidate or version the entry when account state changes. Bound cache bytes and expose resolution source, state slot, hit rate, missing table, and stale-context outcomes without using account keys as metric labels.
Preserve the serialized versioned message for signature verification and replay. A reconstructed list of account keys is auxiliary context and must not replace message bytes. Re-encoding a parsed structure can differ across library versions, so use the format library's canonical message-data accessor.
Parameters
When it goes wrong
lookup table missing
Cause. A v0 message references an account absent from the selected state source.
Fix. Emit the transaction unresolved or retry against an explicitly labelled state context.
writable lookup index out of range
Cause. The table state does not contain the requested address position.
Fix. Reject that resolution result and inspect fork, slot, table owner, and activation state.
compiled instruction account index out of range
Cause. Combined account keys were ordered incorrectly or the message is malformed.
Fix. Build static plus all writable plus all readonly keys, then validate every index.
Questions
- Can a v0 transaction be fully decoded from shreds alone?
- Its signatures, message, lookup references, and instruction bytes can be decoded from shreds. Full account keys require the referenced address lookup table account states. Emit unresolved references immediately or use a clearly labelled confirmed or fork-aware state source for resolution.
- In what order are lookup-table addresses appended?
- Start with static message keys. Append every loaded writable address across lookups in lookup order, then append every loaded readonly address across lookups in lookup order. Compiled instruction indices reference this combined list. Interleaving each table's readonly keys after its writable keys is incorrect.
- Does extracting a transaction mean it succeeded?
- No. Extraction shows that a leader proposed the versioned transaction in authenticated shred data. Execution can fail, and the slot can be skipped or abandoned. Preserve proposal context and reconcile the signature and slot against later processed, confirmed, or finalized state.