Skip to main content

Command Palette

Search for a command to run...

Decoding Solana: Transaction Structure and Execution

Updated
Decoding Solana: Transaction Structure and Execution

You click “Send” in your Solana wallet, and in seconds, your SOL is gone, but how did it get there so fast? Solana isn’t just fast — it’s engineered for speed. But what’s actually happening behind the scenes when you send SOL?

Let's break down how transactions work on Solana, from clicking "send" to what happens under the hood.

Introduction (Transaction Process)

Have you ever wondered what happens when you send Sol to a friend? From your perspective, it seems straightforward: you enter their public key, the amount, click "send," sign the transaction with your wallet (like Phantom), and boom—it's done.

But what really goes on behind the scenes?

  1. First, the decentralized application (dApp) you're using creates the transaction.

  2. This transaction is then sent to your wallet for signing. Once signed with your private key, your wallet sends it back to the dApp.

  3. The dApp then uses an HTTP API call, sendTransaction , to send the signed transaction to its current RPC (Remote Procedure Call) provider.

  4. The RPC server then packages your transaction as a UDP packet and sends it directly to the current and next validators on Solana's leader schedule. Unlike other blockchains that randomly gossip transactions, Solana has a pre-determined leader schedule, which helps with its speed.

  5. When the validator's Transaction Processing Unit (TPU) receives the transaction, it verifies your signature, executes the transaction, and then shares it with other validators across the network.

You can use the dApp from here - https://solana-swerve.vercel.app

Transaction Processing Unit (TPU)

So, you might be thinking, "What exactly is this TPU ?" Simply put, the Transaction Processing Unit (TPU) is what handles all the transaction processing on a Solana validator. It's designed like a software assembly line, using message queues to move transactions through various stages. The output from one stage becomes the input for the next.

Solana validators are pretty unique because they communicate using UDP packets. UDP is a fast, connectionless protocol, which means it doesn't guarantee the order or delivery of messages. While this might sound risky, it actually helps Solana avoid slowdowns and simplifies validator setup. However, this also means transaction and gossip message sizes are limited, and there's a higher chance of transactions getting dropped or validators being targeted by denial-of-service (DoS) attacks.

The TPU starts with the FetchStage. This stage has dedicated "sockets" for different packet types: tpu for regular transactions, tpu_vote for votes, and tpu_forward for when the current leader needs to pass unprocessed transactions to the next leader. Packets are collected in batches of 128 and then sent to the SigVerifyStage. These sockets are created and stored in ContactInfo, which contains node information shared across the network.

Next up is the SigVerifyStage. Here, the validator checks the signatures on these packets, marking them for discard if they fail. If a GPU is installed, it's used for signature verification. This stage also helps prevent overload by dropping excessive packets based on IP addresses. Votes and regular packets are processed separately to prevent attacks on the voting system.

Finally, we get to the BankingStage, which is the core of the validator. This stage receives three types of verified packets: gossip vote packets, TPU vote packets, and normal TPU transaction packets. Each type has its own processing thread, and regular transactions even get two threads.

When a node becomes the leader, transactions are first converted into a "SanitizedTransaction." Then, they run through a Quality of Service (QoS) model to select which ones to execute. A group of transactions is then chosen for parallel execution, enabled by Solana's Sealevel programming model. This parallel processing is possible because clients explicitly mark accounts as "writable" and a "read-write lock" is used to prevent data conflicts. Once executed, the results are sent to the PohService and then broadcast to the network. They are also saved to the bank and accounts database. Unprocessed transactions are buffered and retried, or forwarded to the next leader if time runs out.

One of Solana's key features is this parallel transaction processing. Because transactions explicitly mark which accounts they read from and write to, the BankingStage can execute batches of transactions simultaneously without concurrency issues. This requires read-write locking within and between batches.

After the BankingStage, the PohService comes into play. PoH stands for Proof of History, Solana's way of proving that time has passed. Every batch of executed transactions is mixed into a running hash chain called Proof of History. This constantly ticking hash acts like a cryptographic clock: it shows when each batch was sealed without needing external timestamps. The final stage is the BroacastStage. The processed entries are sliced into 64 KB shreds and fanned out across the network using Turbine, a tree‑shaped gossip protocol. Each node forwards only a portion of the data to its “children,” letting blocks propagate fast even on poor connections.

Put together, the TPU pipeline lets a leader node validate signatures, run programs, order transactions, and share results— all in the few hundred milliseconds allotted to its slot. That tight loop is why a Solana transfer often feels instantaneous to end‑users.

Transaction Structure (Deep Dive)

What does Solana transaction look like?

Let's take a deeper look into the structure of a Solana transaction. There are two types:

  1. Legacy Transactions

  2. Versioned Transactions (TransactionV0).

Solana network has a maximum transactional unit (MTU) size of 1280 bytes. This limit adheres to IPv6 MTU size constraints, which helps ensure speed and reliability. Out of this after headers, 1232 bytes are available for packet data, such as serialized transactions. The total size of a transaction, including its signatures and message, must stay within this limit.

A transaction is composed of:

  • Signatures

  • Legacy Message

Now before deep diving into transaction structure, let’s see how the code for sending someone SOL looks like -

const { Keypair, Connection, LAMPORTS_PER_SOL, SystemProgram, Transaction, sendAndConfirmTransaction } = require("@solana/web3.js");


async function sendSol() {

    const sender = Keypair.generate();  // Creating keypair ( public + private) key for sender
    const receiver = Keypair.generate();    // Creating keypair ( public + private) key for receiver

    const connection  = new Connection("https://api.devnet.solana.com");    // Connecting to the devnet of solana blockchain


    const signature = await connection.requestAirdrop(sender.publicKey, 1 * LAMPORTS_PER_SOL);    // Airdropping some sol in sender address
    await connection.confirmTransaction(signature,"confirmed");
    // Creating the instruction to send the sol
    const ins2 = SystemProgram.transfer({   // calling the transfer function to send sol
        fromPubkey : sender.publicKey,      // telling from which address to send sol
        toPubkey : receiver.publicKey,      // telling to which address to send sol
        lamports : 0.1 * LAMPORTS_PER_SOL   // specifiying how much sol to send
    });

    const txn = new Transaction().add(ins2);    // Creating the transaction *IMPORTANT*

    const {blockhash} = await connection.getLatestBlockhash();      // Getting the latest blockhash

    txn.recentBlockhash = blockhash;    // Assigning the latest blockhash to transaction

    txn.sign(sender);       // Signing the transaction
    const transactionSignature = await sendAndConfirmTransaction(connection,txn,sender);   // sending the transaction to the blockchain
    console.log(transactionSignature);
}

sendSol();

You can see we have created a transaction for sending SOL. Now lets see what all is inside the transaction.

Signature :

This is a compact array of signatures, where each signature is a 64-byte ed25519. A "compact array " is a serialized array that includes its length in a multi-byte encoding called "Compact-u16," followed by each item in the array.

[
  {
    "signature": {
      "type": "Buffer",
      "data": [
        182, 85, 205, 81, 116, 210, 93, 72, 121, 185, 228, 198, 67, 86, 95, 229,
        135, 115, 196, 201, 36, 241, 27, 101, 168, 164, 33, 153, 242, 141, 39,
        122, 108, 182, 15, 92, 39, 10, 199, 231, 85, 146, 29, 20, 20, 122, 127,
        100, 39, 246, 211, 250, 78, 125, 1, 160, 136, 56, 151, 220, 174, 7, 156,
        10
      ]
    },
    "publicKey": "5iM8oMCa1zHxvxM5kAEvBmWr6LzTQYLbDWfVEh8dKSDE"
  }
]

The Legacy Message :

This contains a list of instructions that are processed as a single, atomic unit. Message contains - header (3 bytes), account keys (32 bytes each), recent blockhash (32 bytes), plus instructions.

  • Message Header: This 3-byte header specifies the number of signer accounts and read-only accounts. It contains three 8-bit unsigned integers (u8):

    • num_required_signatures: The number of required signatures: the Solana runtime verifies this number with the length of the compact array of signatures in the transaction.

    • num_readonly_signed_accounts: The number of read-only account addresses that require signatures.

    • num_readonly_unsigned_accounts: The number of read-only account addresses that do not require signatures.

    "header": {
        "numRequiredSignatures": 1,
        "numReadonlySignedAccounts": 0,
        "numReadonlyUnsignedAccounts": 1
      }
  • Account Addresses: This is an array of account addresses that are required by the instructions within the transaction. It's a compact array where each account address takes up 32 bytes. Within this array -

    1. Account addresses that require signatures are listed first, with read and write access accounts preceding read-only access accounts.

    2. Similarly, for account addresses that do not require signatures, read and write access accounts are listed before read-only access accounts.

    "accountKeys": [
      "J2mN4r5rhUoX4UAToRBZ2xeTAJRPbrewZ7nTxMwcecjG", // [0] Sender's public key. Appears first because it is a signer and writable. Signer accounts must come before non-signers.
      "4rZFbZhxfAKhXQuSCPuqSVnH5tE68Uj6W3HNm5LWUj52", // [1] Receiver's public key. Writable but not a signer, so it follows the signer in the order.
      "11111111111111111111111111111111"             // [2] System Program's public key. Read-only and non-signer. Programs are usually listed last unless they are signers or writable (rare).
    ]
  • Recent Blockhash: This acts as a timestamp for the transaction. It's a 32-byte SHA-256 hash used to indicate when the ledger was last observed. If a blockhash is too old, validators will reject the transaction. Every transaction needs a recent blockhash for two main reasons: to serve as a timestamp and to prevent duplicate transactions.

    A blockhash expires after 150 blocks (approximately 1 minute, assuming 400ms block times), after which the transaction cannot be processed.

  •     "recentBlockhash": "E347y9Nah1dxApJjhwtNZqBvp77gUrKH7NM7FPL4JKa",
    
  • Instructions: This is an array of instructions that need to be executed. Similar to the array of account addresses, this is a compact array that starts with a Compact-u16 encoding of the number of instructions, followed by an array of individual instructions. Each instruction in the array has these components:

    • Program ID Index: An 8-bit unsigned integer (u8) index that points to the program's address in the account addresses array. This indicates which program will process the instruction.

    • Account Indexes: An array of 8-bit unsigned integer (u8) indexes that point to the account addresses required for this specific instruction.

    • Instruction Data: A byte array that specifies which instruction to invoke on the program, along with any additional data the instruction needs (like function arguments).

    "instructions": [
        { 
            "programIdIndex": 2,
             "accounts": [0, 1],
             "data": "3Bxs411Dtc7pkFQj"
        }
      ]

Code for getting the structure of transaction -

const {Keypair, Connection, LAMPORTS_PER_SOL, SystemProgram, Transaction} = require("@solana/web3.js");

async function getTransactionStructure() {

  const connection = new Connection("https://api.devnet.solana.com");

  // Create sender and receiver keypairs
  const sender = Keypair.generate();
  const receiver = Keypair.generate();

  console.log("Sender public key:", sender.publicKey.toBase58());
  console.log("Receiver public key:", receiver.publicKey.toBase58());

  const transferAmount = 0.1;

  // Create a transfer instruction
  const instruction = SystemProgram.transfer({
    fromPubkey: sender.publicKey,
    toPubkey: receiver.publicKey,
    lamports: LAMPORTS_PER_SOL * transferAmount,
  });

  // Create a transaction and add the instruction
  const transaction = new Transaction().add(instruction);

  const { blockhash } = await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;

  // Sign the transaction
  transaction.sign(sender);

  console.log("\nTransaction struture  =  \n" + JSON.stringify(transaction));

  console.log("\nDetailed Transaction Structure = ");
  console.log("Signature:");
  console.log(JSON.stringify(transaction.signatures));

  console.log("\nMessage:");
  const message = transaction.compileMessage();
  console.log(JSON.stringify(message));
}

getTransactionStructure();

Solana Transaction Architecture in One Visual

Ares

Part 5 of 9

The second chapter of Tech Lab-AP’s mission to push beyond the known. Ares delivers dense, high-impact, and deeply researched content on the forces shaping tomorrow’s technology. From quantum systems to synthetic intelligence. Go beyond.

Up next

How to Make Chrome Extensions Unremovable on Windows and macOS Using Enterprise Policy

In controlled environments — like schools, businesses, parental supervision, or kiosk systems — administrators may want to enforce a Chrome extension so that it cannot be removed by the user. Google Chrome provides built-in enterprise features to sup...