learn.sol
Week 4 • Transaction Message Building And Signers

Transaction Message Building & Signers

Build a transaction the right way: message -> fee payer -> lifetime -> instructions -> signatures.

Step 1: 0-to-1 Theory

Primitives

  • transaction message: immutable plan before signing
  • fee payer: signer that pays network fee
  • blockhash lifetime: validity window
  • instructions: ordered program operations

Mental Model

A transaction is not “a function call”. It is a signed packet that says:

  1. which accounts are involved
  2. which programs to invoke
  3. in what order

Signatures cover the message bytes. If you change the message after signing, the signature no longer matches.

Invariants

  1. Lifetime must be set before signing.
  2. Signatures are invalid if message changes after signing.
  3. Instruction order changes semantics.

Quick Checks

  1. Why can’t you modify a message after signing?
  2. Why is blockhash freshness a runtime invariant?

Step 2: Real-World Implementation (Code Solution)

Install system program client:

pnpm add @solana-program/system

Create scripts/build-message.ts:

import {
  appendTransactionMessageInstructions,
  createSolanaRpc,
  createTransactionMessage,
  generateKeyPairSigner,
  lamports,
  pipe,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";

// Local validator RPC (fastest way to learn without devnet latency).
const rpc = createSolanaRpc("http://127.0.0.1:8899");

async function main() {
  // In real apps, the wallet is usually the sender signer.
  // Here we generate keypairs so the script is self-contained.
  const sender = await generateKeyPairSigner();
  const recipient = await generateKeyPairSigner();
  // The blockhash defines the transaction lifetime window.
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  // Build one instruction: transfer SOL from sender -> recipient.
  const transferIx = getTransferSolInstruction({
    source: sender,
    destination: recipient.address,
    amount: lamports(1_000_000n),
  });

  // Build the message in the correct order:
  // message -> fee payer -> lifetime -> instructions.
  const message = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(sender, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstructions([transferIx], tx),
  );

  // Signatures cover the message bytes; after signing, do not mutate the message.
  const signed = await signTransactionMessageWithSigners(message);
  console.log("Built and signed transaction message", !!signed);
}

main().catch(console.error);

Run:

pnpm tsx scripts/build-message.ts

Expected result:

  • script logs successful signed-message build.

Step 3: Mastery Test

  • Easy: what happens if fee payer signer is omitted?
  • Medium: what if blockhash expires before send?
  • Hard: why can instruction reordering create security bugs?

Sources

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Transaction Message Building & Signers | learn.sol