Podsumowanie
Transakcja składa się z podpisów oraz wiadomości. Wiadomość zawiera nagłówek, adresy kont, ostatni blockhash i skompilowane instrukcje. Maksymalny rozmiar po serializacji: 1 232 bajty.
A
Transaction
posiada dwa główne pola:
signatures: Tablica podpisówmessage: Informacje o transakcji, w tym lista instrukcji do przetworzenia
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
Diagram przedstawiający dwie części transakcji
Całkowity rozmiar transakcji po serializacji nie może przekroczyć
PACKET_DATA_SIZE
(1 232 bajtów). Ten limit to 1 280 bajtów (minimalny MTU IPv6) minus 48 bajtów
na nagłówki sieciowe (40 bajtów IPv6 + 8 bajtów nagłówka fragmentacji). Te 1 232
bajty obejmują zarówno tablicę signatures, jak i strukturę
message.
Diagram przedstawiający format transakcji i limity rozmiaru
Podpisy
Pole signatures to tablica zakodowana w formacie compact, zawierająca wartości
Signature.
Każdy Signature to 64-bajtowy podpis Ed25519 serializowanego Message,
podpisany prywatnym kluczem konta podpisującego. Jeden podpis jest wymagany dla
każdego konta podpisującego wskazanego w instrukcjach
transakcji.
Każdy podpis jest generowany przez klucz prywatny. Miejsce przechowywania tego klucza — lokalny keypair, sprzętowy moduł HSM lub KMS w chmurze, bądź zarządzana usługa portfela — jest decyzją projektową na etapie produkcji. Zobacz Podpisywanie w środowisku produkcyjnym.
Pierwszy podpis w tablicy należy do płatnika opłat — konta, które opłaca opłatę bazową i opłatę priorytetową transakcji. Ten pierwszy podpis pełni również rolę identyfikatora transakcji, używanego do wyszukiwania transakcji w sieci. Identyfikator transakcji jest powszechnie określany jako sygnatura transakcji.
Wymagania dotyczące płatnika opłat:
- Musi być pierwszym kontem w komunikacie (indeks 0) i posiadaczem podpisu.
- Musi być kontem należącym do System Program lub kontem nonce (weryfikowanym
przez
validate_fee_payer). - Musi posiadać wystarczającą ilość lamport, aby pokryć
rent_exempt_minimum + total_fee; w przeciwnym razie transakcja zakończy się błędemInsufficientFundsForFee.
Komunikat
Pole message to
Message
struktura zawierająca ładunek transakcji:
header: Nagłówek komunikatuaccount_keys: Tablica adresów kont wymaganych przez instrukcje transakcjirecent_blockhash: Blockhash pełniący rolę znacznika czasu transakcjiinstructions: Tablica instrukcji
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>,}
Nagłówek
Pole header to
MessageHeader
struktura z trzema polami u8, które dzielą tablicę account_keys na grupy
uprawnień:
num_required_signatures: Łączna liczba podpisów wymaganych przez transakcję.num_readonly_signed_accounts: Liczba podpisanych kont przeznaczonych tylko do odczytu.num_readonly_unsigned_accounts: Liczba niepodpisanych kont przeznaczonych tylko do odczytu.
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,}
Diagram przedstawiający trzy części nagłówka komunikatu
Adresy kont
Pole
account_keys
to zakodowana kompaktowo tablica kluczy publicznych. Każdy wpis identyfikuje
konto używane przez co najmniej jedną z instrukcji transakcji. Tablica musi
zawierać każde konto i musi zachowywać następującą ścisłą kolejność:
- Podpisujący + Zapisywalny
- Podpisujący + Tylko do odczytu
- Niepodpisujący + Zapisywalny
- Niepodpisujący + Tylko do odczytu
To ścisłe porządkowanie pozwala tablicy account_keys w połączeniu z trzema
licznikami w header nagłówka wiadomości określić uprawnienia dla
każdego konta bez przechowywania flag metadanych dla poszczególnych kont.
Liczniki nagłówka dzielą tablicę na cztery grupy uprawnień wymienione powyżej.
Diagram przedstawiający kolejność tablicy adresów kont
Ostatni blockhash
Pole recent_blockhash to 32-bajtowy skrót, który pełni dwie funkcje:
- Znacznik czasu: potwierdza, że transakcja została niedawno utworzona.
- Deduplikacja: zapobiega dwukrotnemu przetworzeniu tej samej transakcji.
Blockhash wygasa po 150 slot. Jeśli blockhash nie jest już ważny w momencie
dotarcia transakcji, zostaje ona odrzucona z błędem BlockhashNotFound,
chyba że jest to prawidłowa
trwała transakcja nonce.
Metoda RPC getLatestBlockhash pozwala
uzyskać aktualny blockhash oraz ostatnią wysokość bloku, przy której blockhash
będzie jeszcze ważny.
Instrukcje
Pole
instructions
to kompaktowo zakodowana tablica struktur
CompiledInstruction.
Każda struktura CompiledInstruction odwołuje się do kont poprzez indeks w
tablicy account_keys, a nie poprzez pełny klucz publiczny. Zawiera:
program_id_index: Indeks w tablicyaccount_keysidentyfikujący program do wywołania.accounts: Tablica indeksów waccount_keyswskazująca konta przekazywane do programu.data: Tablica bajtów zawierająca instruction discriminator i zserializowane argumenty.
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>,}
Kompaktowa tablica instrukcji
Binarny format transakcji
Transakcje są serializowane przy użyciu kompaktowego schematu kodowania. Wszystkie tablice o zmiennej długości (sygnatury, klucze kont, instrukcje) są poprzedzone kompaktowym kodowaniem długości w formacie u16. Format ten używa 1 bajtu dla wartości 0–127 oraz 2–3 bajtów dla większych wartości.
Struktura transakcji Legacy (w formacie sieciowym):
| Pole | Rozmiar | Opis |
|---|---|---|
num_signatures | 1-3 bajty (compact-u16) | Liczba podpisów |
signatures | num_signatures x 64 bajty | Podpisy Ed25519 |
num_required_signatures | 1 bajt | Pole 1 MessageHeader |
num_readonly_signed | 1 bajt | Pole 2 MessageHeader |
num_readonly_unsigned | 1 bajt | Pole 3 MessageHeader |
num_account_keys | 1-3 bajty (compact-u16) | Liczba statycznych kluczy konta |
account_keys | num_account_keys x 32 bajty | Klucze publiczne |
recent_blockhash | 32 bajty | Blockhash |
num_instructions | 1-3 bajty (compact-u16) | Liczba instrukcji |
instructions | zmienny | Tablica skompilowanych instrukcji |
Każda skompilowana instrukcja jest serializowana następująco:
| Pole | Rozmiar | Opis |
|---|---|---|
program_id_index | 1 bajt | Indeks w tablicy kluczy konta |
num_accounts | 1-3 bajty (compact-u16) | Liczba indeksów kont |
account_indices | num_accounts x 1 bajt | Indeksy kluczy kont |
data_len | 1-3 bajty (compact-u16) | Długość instruction data |
data | data_len bajtów | Nieprzezroczyste instruction data |
Obliczanie rozmiaru
Przy PACKET_DATA_SIZE = 1 232 bajty, dostępną przestrzeń można wyliczyć:
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
Przykład: transakcja transferu SOL
Poniższy diagram pokazuje, jak transakcje i instrukcje współdziałają ze sobą, umożliwiając użytkownikom interakcję z siecią. W tym przykładzie SOL jest przesyłany z jednego konta na drugie.
Metadane konta nadawcy wskazują, że musi ono podpisać transakcję. Pozwala to System Program odliczyć lamport. Zarówno konto nadawcy, jak i odbiorcy muszą być zapisywalne, aby ich saldo lamport mogło ulec zmianie. Aby wykonać tę instrukcję, portfel nadawcy wysyła transakcję zawierającą jego podpis oraz wiadomość zawierającą instrukcję transferu SOL.
Diagram transferu SOL
Po wysłaniu transakcji System Program przetwarza instrukcję transferu i aktualizuje saldo lamport obu kont.
Diagram procesu transferu SOL
Zweryfikuj odbiorcę przed wysłaniem SOL
Transfer przez System Program dodaje lamport do dowolnego konta. Nie istnieje żadna weryfikacja na poziomie protokołu, że odbiorca będzie mógł wyprowadzić SOL z powrotem. Lamport mogą być wyprowadzone tylko przez program będący właścicielem konta, dlatego wysłanie SOL do mint tokena, programu lub PDA, którego nie kontrolujesz, grozi trwałą utratą środków — tylko uprawnienie określone przez program właściciela może je zwrócić. SOL wysłany do token account może być odzyskany wyłącznie przez właściciela tego konta, nigdy przez nadawcę.
Transfery SPL tokenów są częściowo samochronne: Token Program odrzuca transfer, którego konta nie pasują do oczekiwanego mint. Natywne transfery SOL nie mają takiego zabezpieczenia, dlatego nadawca musi zweryfikować odbiorcę przed podpisaniem. Zobacz Weryfikacja adresu, aby zapoznać się z pełną logiką klasyfikacji.
Poniższy przykład przedstawia kod odpowiadający powyższym diagramom. Zobacz
funkcję transfer
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);
Poniższy przykład przedstawia strukturę transakcji zawierającej jedną instrukcję transferu SOL.
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));
Poniższy kod przedstawia dane wyjściowe z poprzednich fragmentów kodu. Format różni się w zależności od SDK, ale zauważ, że każda instrukcja zawiera te same wymagane informacje.
{"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}}]}
Weryfikacja odbiorcy przed transferem
Ponieważ transfer SOL powiedzie się na dowolne konto, sprawdź odbiorcę przed podpisaniem. Pobierz konto i wysyłaj środki tylko do portfela System Program (lub nieaktywnego adresu na krzywej); odrzucaj minty, token account, programy i PDA, nad którymi nie masz kontroli.
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);
Ten fragment kodu weryfikuje natywnych odbiorców SOL. Pełną klasyfikację obejmującą również wysyłanie tokenów SPL (token account, ATA, Token-2022) znajdziesz w sekcji Weryfikacja adresu.
Pobieranie szczegółów transakcji
Po wysłaniu transakcji pobierz jej szczegóły, używając sygnatury transakcji oraz metody RPC getTransaction.
Transakcję możesz również znaleźć za pomocą Solana Explorer.
{"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"}
Surowa odpowiedź identyfikuje konta według indeksu i przechowuje wewnętrzne instrukcje (CPI) jako zakodowane bloki. Aby rozwiązać je na adresy i przejść przez pełne drzewo instrukcji, zobacz Introspekcja transakcji.
Is this page helpful?