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 Signaturenmessage: Transaktionsinformationen, einschließlich der Liste der zu verarbeitenden Anweisungen
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
Diagramm, 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 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_feezu decken; andernfalls schlägt die Transaktion mitInsufficientFundsForFeefehl.
Nachricht
Das Feld message ist ein
Message-Struct,
das die Nutzlast der Transaktion enthält:
header: Der Nachrichten-Headeraccount_keys: Ein Array von Kontenadressen, die von den Anweisungen der Transaktion benötigt werdenrecent_blockhash: Ein Blockhash, der als Zeitstempel für die Transaktion dientinstructions: Ein Array von Anweisungen
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>,}
Header
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.
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 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:
- Signer + Schreibbar
- Signer + Nur-Lesen
- Kein Signer + Schreibbar
- 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 zeigt
Aktueller Blockhash
Das Feld recent_blockhash ist ein 32-Byte-Hash, der zwei Zwecken dient:
- Zeitstempel: beweist, dass die Transaktion kürzlich erstellt wurde.
- 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:
program_id_index: Index inaccount_keyszur Identifikation des aufzurufenden Programms.accounts: Array von Indizes inaccount_keyszur Angabe der Konten, die an das Programm übergeben werden.data: Byte-Array mit dem instruction Diskriminator und serialisierten Argumenten.
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 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):
| Feld | Größe | Beschreibung |
|---|---|---|
num_signatures | 1-3 Bytes (compact-u16) | Anzahl der Signaturen |
signatures | num_signatures x 64 Bytes | Ed25519-Signaturen |
num_required_signatures | 1 Byte | MessageHeader Feld 1 |
num_readonly_signed | 1 Byte | MessageHeader Feld 2 |
num_readonly_unsigned | 1 Byte | MessageHeader Feld 3 |
num_account_keys | 1-3 Bytes (compact-u16) | Anzahl der statischen Kontoschlüssel |
account_keys | num_account_keys x 32 Bytes | Öffentliche Schlüssel |
recent_blockhash | 32 Bytes | Blockhash |
num_instructions | 1-3 Bytes (compact-u16) | Anzahl der Anweisungen |
instructions | variabel | Array von kompilierten Anweisungen |
Jede kompilierte Anweisung wird wie folgt serialisiert:
| Feld | Größe | Beschreibung |
|---|---|---|
program_id_index | 1 Byte | Index in die Kontoschlüssel |
num_accounts | 1-3 Bytes (compact-u16) | Anzahl der Kontoindizes |
account_indices | num_accounts x 1 Byte | Kontoschlüssel-Indizes |
data_len | 1-3 Bytes (compact-u16) | Länge der instruction data |
data | data_len Bytes | Opake 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-Transferdiagramm
Nachdem die Transaktion gesendet wurde, verarbeitet das System Program die Transfer-Anweisung und aktualisiert das lamport-Guthaben beider Konten.
SOL-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 transferconst { 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 recipientconst 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 transferconst { 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);
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 transferconst 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 recipientconst transferInstruction = client.system.instructions.transferSol({source: sender,destination: recipient.address,amount: transferAmount});// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Decode the messageBytesconst compiledTransactionMessage =getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);console.log(JSON.stringify(compiledTransactionMessage, null, 2));
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.
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);
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.
{"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?