Transaktionsstruktur

Zusammenfassung

Eine Transaktion besteht aus Signaturen + einer Nachricht. Die Nachricht enthält einen Header, Kontoadressen, einen aktuellen Blockhash und kompilierte Anweisungen. Maximale serialisierte Größe: 1.232 Bytes.

Eine Transaction hat zwei Felder auf oberster Ebene:

  • signatures: Ein Array von Signaturen
  • message: Transaktionsinformationen, einschließlich der Liste der zu verarbeitenden Anweisungen
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

Diagramm, das die zwei Teile einer Transaktion zeigtDiagramm, das die zwei Teile einer Transaktion zeigt

Die gesamte serialisierte Größe einer Transaktion darf PACKET_DATA_SIZE (1.232 Bytes) nicht überschreiten. Dieses Limit entspricht 1.280 Bytes (die IPv6-Mindest-MTU) minus 48 Bytes für Netzwerk-Header (40 Bytes IPv6 + 8 Bytes Fragment-Header). Die 1.232 Bytes umfassen sowohl das signatures-Array als auch die message-Struktur.

Diagramm, das das Transaktionsformat und die Größenlimits zeigtDiagramm, das das Transaktionsformat und die Größenlimits zeigt

Signaturen

Das signatures-Feld ist ein kompakt kodiertes Array von Signature-Werten. Jede Signature ist eine 64-Byte-Ed25519-Signatur der serialisierten Message, signiert mit dem privaten Schlüssel des Signer-Kontos. Eine Signatur ist für jedes Signer-Konto erforderlich, auf das die Anweisungen der Transaktion verweisen.

Jede Signatur wird von einem privaten Schlüssel erzeugt. Wo dieser Schlüssel gespeichert ist – in einem lokalen keypair, einem Cloud-HSM oder KMS oder einem verwalteten Wallet-Dienst – ist eine Designentscheidung für den Produktionsbetrieb. Siehe Signing in Production.

Die erste Signatur im Array gehört dem Fee-Payer, dem Konten, das die Grundgebühr und priority fee der Transaktion bezahlt. Diese erste Signatur dient auch als Transaktions-ID, mit der die Transaktion im Netzwerk nachgeschlagen werden kann. Die Transaktions-ID wird allgemein als Transaktionssignatur bezeichnet.

Anforderungen an den Fee-Payer:

  • Muss das erste Konten in der Nachricht (Index 0) und ein Signer sein.
  • Muss ein Konten im Besitz des System-Programms oder ein Nonce-Konten sein (validiert durch validate_fee_payer).
  • Muss genügend lamport halten, um rent_exempt_minimum + total_fee zu decken; andernfalls schlägt die Transaktion mit InsufficientFundsForFee fehl.

Nachricht

Das Feld message ist ein Message-Struct, das die Nutzlast der Transaktion enthält:

  • header: Der Nachrichten-Header
  • account_keys: Ein Array von Kontenadressen, die von den Anweisungen der Transaktion benötigt werden
  • recent_blockhash: Ein Blockhash, der als Zeitstempel für die Transaktion dient
  • instructions: Ein Array von Anweisungen
Message
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
pub header: MessageHeader,
/// All the account keys used by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
/// The id of a recent ledger entry.
pub recent_blockhash: Hash,
/// Programs that will be executed in sequence and committed in
/// one atomic transaction if all succeed.
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
}

Das Feld header ist ein MessageHeader-Struct mit drei u8-Feldern, die das Array account_keys in Berechtigungsgruppen unterteilen:

  • num_required_signatures: Gesamtanzahl der für die Transaktion erforderlichen Signaturen.
  • num_readonly_signed_accounts: Anzahl der signierten Konten, die schreibgeschützt sind.
  • num_readonly_unsigned_accounts: Anzahl der nicht signierten Konten, die schreibgeschützt sind.
MessageHeader
pub struct MessageHeader {
/// The number of signatures required for this message to be considered
/// valid. The signers of those signatures must match the first
/// `num_required_signatures` of [`Message::account_keys`].
pub num_required_signatures: u8,
/// The last `num_readonly_signed_accounts` of the signed keys are read-only
/// accounts.
pub num_readonly_signed_accounts: u8,
/// The last `num_readonly_unsigned_accounts` of the unsigned keys are
/// read-only accounts.
pub num_readonly_unsigned_accounts: u8,
}

Präfixe für Legacy- und versionierte Nachrichten

In einer Legacy-Transaktionsnachricht ist das erste Nachrichtenbyte num_required_signatures, gefolgt von den anderen zwei MessageHeader Bytes. In einer versionierten Transaktionsnachricht ist das erste Byte stattdessen ein Versionspräfix; die drei Bytes von MessageHeader beginnen unmittelbar nach diesem Präfix. Siehe versionierte Transaktionen für das vollständige v0-Nachrichten-Layout.

Diagramm, das die drei Teile des Nachrichten-Headers zeigtDiagramm, das die drei Teile des Nachrichten-Headers zeigt

Konten-Adressen

Das account_keys Feld ist ein kompakt kodiertes Array von öffentlichen Schlüsseln. Jeder Eintrag identifiziert ein Konten, das von mindestens einer der Anweisungen der Transaktion verwendet wird. Das Array muss jedes Konten enthalten und dieser strikten Reihenfolge folgen:

  1. Signer + Schreibbar
  2. Signer + Nur-Lesen
  3. Kein Signer + Schreibbar
  4. Kein Signer + Nur-Lesen

Diese strikte Reihenfolge ermöglicht es, das account_keys Array mit den drei Zählern im header des Nachrichten-Headers zu kombinieren, um die Berechtigungen für jedes Konten zu bestimmen, ohne Metadaten-Flags pro Konten zu speichern. Die Header-Zähler unterteilen das Array in die vier oben aufgeführten Berechtigungsgruppen.

Diagramm, das die Reihenfolge des Arrays der Konten-Adressen zeigtDiagramm, das die Reihenfolge des Arrays der Konten-Adressen zeigt

Aktueller Blockhash

Das Feld recent_blockhash ist ein 32-Byte-Hash, der zwei Zwecken dient:

  1. Zeitstempel: beweist, dass die Transaktion kürzlich erstellt wurde.
  2. Deduplizierung: verhindert, dass dieselbe Transaktion zweimal verarbeitet wird.

Ein Blockhash läuft nach 150 slot ab. Wenn der Blockhash nicht mehr gültig ist, wenn die Transaktion eintrifft, wird sie mit BlockhashNotFound abgelehnt, sofern es sich nicht um eine gültige dauerhafte Nonce-Transaktion handelt.

Die getLatestBlockhash RPC-Methode ermöglicht es Ihnen, den aktuellen Blockhash und die letzte Blockhöhe abzurufen, bis zu der der Blockhash gültig sein wird.

Anweisungen

Das instructions Feld ist ein kompakt-kodiertes Array von CompiledInstruction Structs. Jede CompiledInstruction referenziert Konten über einen Index im account_keys Array statt über den vollständigen öffentlichen Schlüssel. Es enthält:

  1. program_id_index: Index in account_keys zur Identifikation des aufzurufenden Programms.
  2. accounts: Array von Indizes in account_keys zur Angabe der Konten, die an das Programm übergeben werden.
  3. data: Byte-Array mit dem instruction Diskriminator und serialisierten Argumenten.
CompiledInstruction
pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

Kompaktes Array von AnweisungenKompaktes Array von Anweisungen

Binäres Transaktionsformat

Transaktionen werden mithilfe eines kompakten Kodierungsschemas serialisiert. Alle Arrays variabler Länge (Signaturen, Kontoschlüssel, Anweisungen) werden mit einer compact-u16-Längencodierung als Präfix versehen. Dieses Format verwendet 1 Byte für Werte von 0–127 und 2–3 Bytes für größere Werte.

Legacy-Transaktionslayout (auf dem Übertragungsweg):

FeldGrößeBeschreibung
num_signatures1-3 Bytes (compact-u16)Anzahl der Signaturen
signaturesnum_signatures x 64 BytesEd25519-Signaturen
num_required_signatures1 ByteMessageHeader Feld 1
num_readonly_signed1 ByteMessageHeader Feld 2
num_readonly_unsigned1 ByteMessageHeader Feld 3
num_account_keys1-3 Bytes (compact-u16)Anzahl der statischen Kontoschlüssel
account_keysnum_account_keys x 32 BytesÖffentliche Schlüssel
recent_blockhash32 BytesBlockhash
num_instructions1-3 Bytes (compact-u16)Anzahl der Anweisungen
instructionsvariabelArray von kompilierten Anweisungen

Jede kompilierte Anweisung wird wie folgt serialisiert:

FeldGrößeBeschreibung
program_id_index1 ByteIndex in die Kontoschlüssel
num_accounts1-3 Bytes (compact-u16)Anzahl der Kontoindizes
account_indicesnum_accounts x 1 ByteKontoschlüssel-Indizes
data_len1-3 Bytes (compact-u16)Länge der instruction data
datadata_len BytesOpake instruction data

Größenberechnung

Gegeben PACKET_DATA_SIZE = 1.232 Bytes, kann der verfügbare Speicherplatz berechnet werden:

Total = 1232 bytes
- compact-u16(num_sigs) # 1 byte
- num_sigs * 64 # signature bytes
- 3 # message header
- compact-u16(num_keys) # 1 byte
- num_keys * 32 # account key bytes
- 32 # recent blockhash
- compact-u16(num_ixs) # 1 byte
- sum(instruction_sizes) # per-instruction overhead + data

Beispiel: SOL-Transfertransaktion

Das folgende Diagramm zeigt, wie Transaktionen und Anweisungen zusammenwirken, damit Benutzer mit dem Netzwerk interagieren können. In diesem Beispiel wird SOL von einem Konten auf ein anderes übertragen.

Die Metadaten des Sender-Konten zeigen an, dass es die Transaktion unterzeichnen muss. Dies ermöglicht dem System Program, lamports abzuziehen. Sowohl das Sender- als auch das Empfänger-Konten müssen beschreibbar sein, damit sich ihr lamport-Guthaben ändern kann. Um diese Anweisung auszuführen, sendet das Wallet des Absenders die Transaktion mit seiner Signatur und der Nachricht, die die SOL-Transfer-Anweisung enthält.

SOL-TransferdiagrammSOL-Transferdiagramm

Nachdem die Transaktion gesendet wurde, verarbeitet das System Program die Transfer-Anweisung und aktualisiert das lamport-Guthaben beider Konten.

SOL-TransferprozessdiagrammSOL-Transferprozessdiagramm

Empfänger vor dem Senden von SOL überprüfen

Ein System Program-Transfer fügt lamports zu jedem Konten hinzu. Es gibt keine Protokoll-Überprüfung, ob der Empfänger die SOL wieder herausbewegen kann. Lamports können nur vom besitzenden Programm des Konten herausbewegt werden. Daher birgt das Senden von SOL an einen Token Mint, ein Programm oder eine PDA, die Sie nicht kontrollieren, das Risiko eines dauerhaften Fondsverlusts — nur eine vom besitzenden Programm festgelegte Autorität kann sie zurückgeben. SOL, das an ein token account gesendet wurde, kann nur vom Eigentümer dieses Konten zurückgefordert werden, niemals vom Absender.

SPL-Token-Transfers sind teilweise selbstschützend: Das Token Program lehnt einen Transfer ab, bei dem die Konten nicht mit dem erwarteten Mint übereinstimmen. Native SOL-Transfers haben keinen solchen Schutz, daher muss der Absender den Empfänger vor dem Unterzeichnen überprüfen. Siehe Adresse überprüfen für die vollständige Klassifikationslogik.

Das folgende Beispiel zeigt den Code, der zu den obigen Diagrammen gehört. Siehe die transfer Funktion des System Program.

import { createClient, generateKeyPairSigner, lamports } from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const sender = client.payer;
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Check balance before transfer
const { value: preBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: preBalance2 } = await client.rpc
.getBalance(recipient.address)
.send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
const transactionSignature = await client.sendTransaction([
transferInstruction
]);
// Check balance after transfer
const { value: postBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: postBalance2 } = await client.rpc
.getBalance(recipient.address)
.send();
console.log(
"Sender prebalance:",
Number(preBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient prebalance:",
Number(preBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Sender postbalance:",
Number(postBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient postbalance:",
Number(postBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log("Transaction Signature:", transactionSignature.context.signature);
Console
Click to execute the code.

Das folgende Beispiel zeigt die Struktur einer Transaktion, die eine einzelne SOL-Überweisungs Anweisungen enthält.

import {
createClient,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
const sender = client.payer;
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
source: sender,
destination: recipient.address,
amount: transferAmount
});
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Decode the messageBytes
const compiledTransactionMessage =
getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);
console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Console
Click to execute the code.

Der folgende Code zeigt die Ausgabe der vorherigen Code-Snippets. Das Format unterscheidet sich je nach SDK, beachten Sie jedoch, dass jede Anweisung dieselben erforderlichen Informationen enthält.

{
"version": 0,
"header": {
"numSignerAccounts": 1,
"numReadonlySignerAccounts": 0,
"numReadonlyNonSignerAccounts": 1
},
"staticAccounts": [
"HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa",
"5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL",
"11111111111111111111111111111111"
],
"lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1",
"instructions": [
{
"programAddressIndex": 2,
"accountIndices": [0, 1],
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}
]
}

Empfänger vor der Überweisung überprüfen

Da eine SOL-Überweisung auf jedes Konten erfolgreich ist, überprüfen Sie den Empfänger vor der Unterzeichnung. Rufen Sie das Konten ab und senden Sie nur an eine System Program-Wallet (oder eine nicht finanzierte On-Curve-Adresse); lehnen Sie Mints, token account, Programme und PDAs ab, die Sie nicht kontrollieren.

Kit
import {
type Address,
createSolanaRpc,
fetchJsonParsedAccount,
isOffCurveAddress
} from "@solana/kit";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const SYSTEM_PROGRAM = "11111111111111111111111111111111" as Address;
/**
* Throws if `recipient` cannot safely receive native SOL.
*
* Only System Program wallets (or unfunded on-curve addresses) are safe. Any
* other account locks the lamports because no authority can debit them.
*/
async function assertSafeSolRecipient(recipient: Address): Promise<void> {
const account = await fetchJsonParsedAccount(rpc, recipient);
if (!account.exists) {
// Off-curve = a PDA with no account; reject conservatively.
if (isOffCurveAddress(recipient)) {
throw new Error(
"Recipient is a PDA with no account; SOL would be locked"
);
}
// On-curve = an unfunded wallet, safe to fund.
return;
}
if (account.programAddress !== SYSTEM_PROGRAM) {
throw new Error(
`Recipient is owned by ${account.programAddress}, not a wallet; SOL would be locked`
);
}
}
// A wallet: safe.
await assertSafeSolRecipient(
"H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" as Address
);
// The USDC mint: rejected before any SOL leaves the sender.
await assertSafeSolRecipient(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as Address
);
Console
Click to execute the code.

Dieses Snippet überprüft native SOL-Empfänger. Die vollständige Klassifizierung, die auch SPL-Token-Sendungen (token account, ATAs, Token-2022) berücksichtigt, finden Sie unter Verify Address.

Transaktionsdetails abrufen

Nach der Übermittlung können die Transaktionsdetails mithilfe der Transaktionssignatur und der getTransaction RPC-Methode abgerufen werden.

Sie können die Transaktion auch über den Solana Explorer finden.

Transaction Data
{
"blockTime": 1745196488,
"meta": {
"computeUnitsConsumed": 150,
"err": null,
"fee": 5000,
"innerInstructions": [],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"postBalances": [989995000, 10000000, 1],
"postTokenBalances": [],
"preBalances": [1000000000, 0, 1],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 13049,
"transaction": {
"message": {
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"accountKeys": [
"8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ",
"7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma",
"11111111111111111111111111111111"
],
"recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf",
"instructions": [
{
"accounts": [0, 1],
"data": "3Bxs4NN8M2Yn4TLb",
"programIdIndex": 2,
"stackHeight": null
}
],
"indexToProgramIds": {}
},
"signatures": [
"3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"
]
},
"version": "legacy"
}

Die Rohantwort identifiziert Konten anhand des Index und speichert innere (CPI) Anweisungen als kodierte Blobs. Um diese in Adressen aufzulösen und den vollständigen Anweisungsbaum zu durchlaufen, siehe Transaktions-Introspektion.

Is this page helpful?

Inhaltsverzeichnis

Seite bearbeiten
© 2026 Solana Foundation. Alle Rechte vorbehalten.