Struktura transakcji

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ów
  • message: Informacje o transakcji, w tym lista instrukcji do przetworzenia
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

Diagram przedstawiający dwie części transakcjiDiagram 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 rozmiaruDiagram 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łędem InsufficientFundsForFee.

Komunikat

Pole message to Message struktura zawierająca ładunek transakcji:

  • header: Nagłówek komunikatu
  • account_keys: Tablica adresów kont wymaganych przez instrukcje transakcji
  • recent_blockhash: Blockhash pełniący rolę znacznika czasu transakcji
  • instructions: Tablica instrukcji
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>,
}

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.
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,
}

Prefiksy starszych i wersjonowanych wiadomości

W starszej wiadomości transakcyjnej pierwszy bajt wiadomości to num_required_signatures, po którym następują pozostałe dwa bajty MessageHeader. W wersjonowanej wiadomości transakcyjnej pierwszy bajt jest prefiksem wersji; trzybajtowy MessageHeader zaczyna się bezpośrednio po tym prefiksie. Zobacz wersjonowane transakcje, aby zapoznać się z pełnym układem wiadomości v0.

Diagram przedstawiający trzy części nagłówka wiadomościDiagram przedstawiający trzy części nagłówka wiadomości

Adresy kont

Pole account_keys to kompaktowo zakodowana 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 przestrzegać następującej ścisłej kolejności:

  1. Podpisujący + Zapisywalny
  2. Podpisujący + Tylko do odczytu
  3. Niepodpisujący + Zapisywalny
  4. Niepodpisujący + Tylko do odczytu

Ta ścisła kolejność pozwala na połączenie tablicy account_keys z trzema licznikami w header nagłówka wiadomości w celu określenia uprawnień 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 kontDiagram przedstawiający kolejność tablicy adresów kont

Ostatni blockhash

Pole recent_blockhash to 32-bajtowy hash, który służy dwóm celom:

  1. Znacznik czasu: potwierdza, że transakcja została niedawno utworzona.
  2. Deduplikacja: zapobiega dwukrotnemu przetworzeniu tej samej transakcji.

Blockhash wygasa po 150 slot-ach. Jeśli blockhash nie jest już ważny w momencie dotarcia transakcji, zostaje ona odrzucona z błędem BlockhashNotFound, chyba że jest to ważna transakcja z trwałym nonce.

Metoda RPC getLatestBlockhash pozwala uzyskać aktualny blockhash oraz ostatnią wysokość bloku, przy której blockhash będzie ważny.

Instrukcje

Pole instructions to kompaktowo zakodowana tablica struktur CompiledInstruction. Każdy CompiledInstruction odwołuje się do kont za pomocą indeksu w tablicy account_keys, a nie pełnego klucza publicznego. Zawiera:

  1. program_id_index: Indeks w tablicy account_keys identyfikujący program do wywołania.
  2. accounts: Tablica indeksów w tablicy account_keys określająca konta przekazywane do programu.
  3. data: Tablica bajtów zawierająca dyskryminator instrukcji i zserializowane argumenty.
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>,
}

Kompaktowa tablica instrukcjiKompaktowa 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 kodowaniem długości w formacie compact-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):

PoleRozmiarOpis
num_signatures1–3 bajty (compact-u16)Liczba sygnatur
signaturesnum_signatures x 64 bajtySygnatury Ed25519
num_required_signatures1 bajtPole MessageHeader 1
num_readonly_signed1 bajtPole MessageHeader 2
num_readonly_unsigned1 bajtPole MessageHeader 3
num_account_keys1–3 bajty (compact-u16)Liczba statycznych kluczy kont
account_keysnum_account_keys x 32 bajtyKlucze publiczne
recent_blockhash32 bajtyBlockhash
num_instructions1–3 bajty (compact-u16)Liczba instrukcji
instructionszmiennyTablica skompilowanych instrukcji

Każda skompilowana instrukcja jest serializowana następująco:

PoleRozmiarOpis
program_id_index1 bajtIndeks w tablicy kluczy kont
num_accounts1–3 bajty (compact-u16)Liczba indeksów kont
account_indicesnum_accounts x 1 bajtIndeksy kluczy kont
data_len1–3 bajty (compact-u16)Długość instruction data
datadata_len bajtówOpaque instruction data

Obliczanie rozmiaru

Przy założeniu PACKET_DATA_SIZE = 1 232 bajtów, dostępna przestrzeń może być obliczona:

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 przedstawia, jak transakcje i instrukcje współpracują ze sobą, umożliwiając użytkownikom interakcję z siecią. W tym przykładzie SOL jest transferowane 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 ichs saldo w 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 SOLDiagram transferu SOL

Po wysłaniu transakcji System Program przetwarza instrukcję transferu i aktualizuje saldo w lamport obu kont.

Diagram procesu transferu SOLDiagram procesu transferu SOL

Zweryfikuj odbiorcę przed wysłaniem SOL

Transfer za pomocą System Program dodaje lamport do dowolnego konta. Nie istnieje żadna weryfikacja na poziomie protokołu, że odbiorca może wyprowadzić SOL z powrotem. Lamport mogą być wyprowadzone tylko przez program będący właścicielem konta, dlatego wysłanie SOL na mint tokena, program lub PDA, nad którym nie masz kontroli, grozi trwałą utratą środków — tylko upoważnienie określone przez program właściciela może je zwrócić. SOL wysłane na token account może odzyskać jedynie właściciel tego konta, nigdy nadawca.

Transfery tokenów SPL są częściowo samochronne: Token Program odrzuca transfer, którego konta nie odpowiadają oczekiwanemu mintowi. Natywne transfery SOL nie posiadają takiego zabezpieczenia, dlatego nadawca musi zweryfikować odbiorcę przed podpisaniem. Zobacz Weryfikacja adresu, aby zapoznać się z pełną logiką klasyfikacji.

Poniższy przykład pokazuje kod związany z powyższymi diagramami. 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 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.

Poniższy przykład przedstawia strukturę transakcji zawierającej pojedynczą 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 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.

Poniższy kod pokazuje dane wyjściowe z poprzednich fragmentów kodu. Format różni się między 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
}
}
]
}

Zweryfikuj odbiorcę przed transferem

Ponieważ transfer SOL powiedzie się na dowolne konto, sprawdź odbiorcę przed podpisaniem. Pobierz konto i wysyłaj wyłącznie do portfela System Program (lub niefinansowanego adresu on-curve); odrzucaj minty, token accounts, programy i PDA, których nie kontrolujesz.

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.

Ten fragment kodu sprawdza natywnych odbiorców SOL. Aby zapoznać się z pełną klasyfikacją uwzględniającą również wysyłanie tokenów SPL (token accounts, ATA, Token-2022), zobacz Verify Address.

Pobieranie szczegółów transakcji

Po wysłaniu pobierz szczegóły transakcji, używając sygnatury transakcji i metody RPC getTransaction.

Transakcję możesz również znaleźć za pomocą Solana Explorer.

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"
}

Surowa odpowiedź identyfikuje konta według indeksu i przechowuje wewnętrzne instrukcje (CPI) jako zakodowane dane binarne. Aby rozwiązać je na adresy i prześledzić pełne drzewo instrukcji, zobacz Transaction Introspection.

Is this page helpful?

Spis treści

Edytuj stronę