Tapahtuman rakenne

Yhteenveto

Tapahtuma sisältää allekirjoitukset + viestin. Viesti sisältää otsikon, tiliosoitteet, viimeaikaisen lohkohajautusarvon ja käännetyt ohjeet. Sarjallistetun koon maksimi: 1 232 tavua.

Transaction-rakenteella on kaksi ylätason kenttää:

  • signatures: Allekirjoitusten taulukko
  • message: Tapahtumatiedot, mukaan lukien luettelo käsiteltävistä ohjeista
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

Kaavio, joka näyttää tapahtuman kaksi osaaKaavio, joka näyttää tapahtuman kaksi osaa

Tapahtuman sarjallistetun kokonaiskoon ei saa ylittää PACKET_DATA_SIZE-arvoa (1 232 tavua). Tämä rajoitus vastaa 1 280 tavua (IPv6:n vähimmäis-MTU) miinus 48 tavua verkon otsikoille (40 tavua IPv6 + 8 tavua fragmenttiotsikko). 1 232 tavua sisältää sekä signatures-taulukon että message-rakenteen.

Kaavio, joka näyttää tapahtumamuodon ja kokorajoituksetKaavio, joka näyttää tapahtumamuodon ja kokorajoitukset

Allekirjoitukset

signatures-kenttä on kompaktisti koodattu taulukko Signature-arvoja. Jokainen Signature on 64-tavuinen Ed25519-allekirjoitus sarjallistetusta Message-rakenteesta, allekirjoitettu allekirjoittajatilin yksityisellä avaimella. Yksi allekirjoitus vaaditaan jokaiselta allekirjoittajatililtä, johon tapahtuman ohjeet viittaavat.

Jokaisen allekirjoituksen tuottaa yksityinen avain. Se, missä kyseinen avain sijaitsee — paikallinen keypair, pilvi-HSM tai KMS, tai hallittu lompakkopalvelu — on tuotantoympäristön suunnittelupäätös. Katso Allekirjoittaminen tuotannossa.

Taulukon ensimmäinen allekirjoitus kuuluu maksun maksajalle, tilille joka maksaa transaktion perusmaksun ja priorisointimaksun. Tämä ensimmäinen allekirjoitus toimii myös transaktion tunnuksena, jota käytetään transaktion hakemiseen verkosta. Transaktion tunnukseen viitataan yleisesti nimellä transaktioallekirjoitus.

Maksun maksajan vaatimukset:

  • Täytyy olla ensimmäinen tili viestissä (indeksi 0) ja allekirjoittaja.
  • Täytyy olla System Program -omistuksessa oleva tili tai nonce-tili (vahvistetaan validate_fee_payer toimesta).
  • Täytyy pitää hallussaan riittävästi lamport-yksiköitä kattaakseen rent_exempt_minimum + total_fee; muuten transaktio epäonnistuu virheellä InsufficientFundsForFee.

Viesti

message-kenttä on Message -rakenne, joka sisältää transaktion hyötykuorman:

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

Otsikko

header-kenttä on MessageHeader -rakenne, jossa on kolme u8-kenttää, jotka jakavat account_keys-taulukon oikeusryhmiin:

  • num_required_signatures: Transaktion vaatimien allekirjoitusten kokonaismäärä.
  • num_readonly_signed_accounts: Vain luku -tilassa olevien allekirjoitettujen tilien lukumäärä.
  • num_readonly_unsigned_accounts: Vain luku -tilassa olevien allekirjoittamattomien tilien lukumäärä.
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,
}

Kaavio, joka näyttää viestin otsikon kolme osaaKaavio, joka näyttää viestin otsikon kolme osaa

Tilin osoitteet

account_keys -kenttä on kompaktisti koodattu taulukko julkisia avaimia. Jokainen merkintä yksilöi tilin, jota vähintään yksi transaktion käskyistä käyttää. Taulukon täytyy sisältää jokainen tili ja sen täytyy noudattaa seuraavaa tarkkaa järjestystä:

  1. Allekirjoittaja + Kirjoitettava
  2. Allekirjoittaja + Vain luku
  3. Ei-allekirjoittaja + Kirjoitettava
  4. Ei-allekirjoittaja + Vain luku

Tämä tarkka järjestys mahdollistaa sen, että account_keys-taulukkoa voidaan yhdistää viestin header-osion kolmeen laskuriin, jolloin kunkin tilin käyttöoikeudet voidaan määrittää ilman tilikohtaisia metatietolippuja. Otsikon laskurit jakavat taulukon neljään yllä mainittuun käyttöoikeusryhmään.

Kaavio, joka näyttää tilitaulukossa olevien osoitteiden järjestyksenKaavio, joka näyttää tilitaulukossa olevien osoitteiden järjestyksen

Viimeisin lohkohajautusarvo

recent_blockhash-kenttä on 32-tavuinen hajautusarvo, joka palvelee kahta tarkoitusta:

  1. Aikaleima: todistaa, että transaktio on luotu äskettäin.
  2. Deduplikointi: estää saman transaktion käsittelyn kahdesti.

Lohkohajautusarvo vanhenee 150 slot jälkeen. Jos lohkohajautusarvo ei ole enää voimassa transaktion saapuessa, se hylätään virheellä BlockhashNotFound, ellei kyseessä ole voimassa oleva kestävä nonce-transaktio.

getLatestBlockhash RPC-metodi mahdollistaa nykyisen lohkohajautusarvon ja viimeisen lohkokorkeuden hakemisen, johon asti lohkohajautusarvo on voimassa.

Käskyt

instructions-kenttä on kompaktikoodattu taulukko CompiledInstruction-rakenteista. Kukin CompiledInstruction viittaa tileihin indeksillä account_keys-taulukossa täydellisen julkisen avaimen sijaan. Se sisältää:

  1. program_id_index: Indeksi account_keys-taulukossa kutsuttavan ohjelman tunnistamiseksi.
  2. accounts: Taulukko indekseistä account_keys-taulukossa ohjelmalle välitettävien tilien määrittämiseksi.
  3. data: Taulukko tavuja, joka sisältää instruction data -erottelijan ja serialisoidut argumentit.
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>,
}

Kompakti taulukko käskyistäKompakti taulukko käskyistä

Transaktion binäärimuoto

Transaktiot serialisoidaan kompaktia koodausmenetelmää käyttäen. Kaikki muuttuvanpituiset taulukot (allekirjoitukset, tiliavaimet, käskyt) on etuliitetty compact-u16-pituuskoodauksella. Tämä muoto käyttää 1 tavua arvoille 0–127 ja 2–3 tavua suuremmille arvoille.

Vanha transaktiorakenne (siirtomuodossa):

KenttäKokoKuvaus
num_signatures1–3 tavua (compact-u16)Allekirjoitusten määrä
signaturesnum_signatures x 64 tavuaEd25519-allekirjoitukset
num_required_signatures1 tavuMessageHeader kenttä 1
num_readonly_signed1 tavuMessageHeader kenttä 2
num_readonly_unsigned1 tavuMessageHeader kenttä 3
num_account_keys1–3 tavua (compact-u16)Staattisten tiliavainten määrä
account_keysnum_account_keys x 32 tavuaJulkiset avaimet
recent_blockhash32 tavuaLohkohajautusarvo
num_instructions1–3 tavua (compact-u16)Ohjeiden määrä
instructionsvaihtelevaTaulukko käännetyistä ohjeista

Jokainen käännetty ohje sarjallistetaan seuraavasti:

KenttäKokoKuvaus
program_id_index1 tavuIndeksi tiliavaimiin
num_accounts1–3 tavua (compact-u16)Tili-indeksien määrä
account_indicesnum_accounts x 1 tavuTiliavainten indeksit
data_len1–3 tavua (compact-u16)instruction data -kentän pituus
datadata_len tavuaLäpinäkymätön instruction data

Kokoluokka

Kun PACKET_DATA_SIZE = 1 232 tavua, käytettävissä oleva tila voidaan laskea:

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

Esimerkki: SOL-siirtotransaktio

Alla oleva kaavio näyttää, miten transaktiot ja ohjeet toimivat yhdessä mahdollistaen käyttäjien vuorovaikutuksen verkon kanssa. Tässä esimerkissä SOL siirretään yhdeltä tililtä toiselle.

Lähettäjätilin metatiedot osoittavat, että sen on allekirjoitettava transaktio. Tämä sallii System Program -ohjelman vähentää lamport-yksiköitä. Sekä lähettäjän että vastaanottajan tilien on oltava kirjoitettavissa, jotta niiden lamport-saldo voi muuttua. Tämän käskyn suorittamiseksi lähettäjän lompakko lähettää transaktion, joka sisältää allekirjoituksen ja viestin, joka sisältää SOL-siirtokäskyn.

SOL-siirtokaavioSOL-siirtokaavio

Kun transaktio on lähetetty, System Program käsittelee siirtokäskyn ja päivittää molempien tilien lamport-saldon.

SOL-siirtoprosessin kaavioSOL-siirtoprosessin kaavio

Tarkista vastaanottaja ennen SOL:n lähettämistä

System Program -siirto lisää lamport-yksiköitä mihin tahansa tilille. Protokollatasolla ei ole tarkistusta siitä, pystyykö vastaanottaja siirtämään SOL:n takaisin ulos. Lamport-yksiköitä voi siirtää ulos vain tilin omistavan ohjelman kautta, joten SOL:n lähettäminen token mint -osoitteeseen, ohjelmaan tai PDA:han, jota et hallitse, vaarantaa varojen pysyvän menetyksen — vain omistavan ohjelman määrittämä auktoriteetti voi palauttaa ne. SOL, joka lähetetään token account -tilille, on palautettavissa vain kyseisen tilin omistajan toimesta, ei koskaan lähettäjän toimesta.

SPL token -siirrot ovat osittain itsensä suojaavia: Token Program hylkää siirron, jonka tilit eivät vastaa odotettua minttiä. Natiiville SOL-siirroille ei ole tällaista suojaa, joten lähettäjän on varmistettava vastaanottaja ennen allekirjoittamista. Katso täydellinen luokittelulogiikka kohdasta Verify Address.

Alla oleva esimerkki näyttää yllä oleviin kaavioihin liittyvän koodin. Katso System Program:n transfer-funktio.

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.

Seuraava esimerkki näyttää transaktion rakenteen, joka sisältää yhden SOL-siirtokäskyn.

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.

Alla oleva koodi näyttää edellisten koodinäytteiden tulosteen. Muoto vaihtelee SDK:iden välillä, mutta huomaa, että jokainen ohje sisältää samat vaaditut tiedot.

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

Tarkista vastaanottaja ennen siirtoa

Koska SOL-siirto onnistuu mihin tahansa tilille, tarkista vastaanottaja ennen allekirjoittamista. Hae tili ja lähetä vain System Program -lompakkoon (tai rahoittamattomaan on-curve osoitteeseen); hylkää mintit, token account -tilit, ohjelmat ja PDA:t, joita et hallitse.

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.

Tämä katkelma tarkistaa natiivin SOL:n vastaanottajat. Täydellinen luokittelu, joka käsittelee myös SPL-tokenien lähetykset (token account -tilit, ATA:t, Token-2022), löytyy täältä: Verify Address.

Tapahtuman tietojen hakeminen

Lähetyksen jälkeen hae tapahtuman tiedot käyttämällä tapahtuman allekirjoitusta ja getTransaction RPC-metodia.

Voit myös etsiä tapahtuman Solana Explorer -palvelun avulla.

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

Raakavastaus tunnistaa tilit indeksin perusteella ja tallentaa sisäiset (CPI) käskyt koodattuina blobeina. Näiden muuntamiseksi osoitteiksi ja koko käskypuun läpikäymiseksi, katso Tapahtuman tarkastelu.

Is this page helpful?

Sisällysluettelo

Muokkaa sivua
© 2026 Solana Foundation. Kaikki oikeudet pidätetään.