Περίληψη
Μια συναλλαγή περιέχει υπογραφές + ένα μήνυμα. Το μήνυμα περιέχει μια κεφαλίδα, διευθύνσεις λογαριασμών, πρόσφατο blockhash και μεταγλωττισμένες εντολές. Μέγιστο σειριοποιημένο μέγεθος: 1.232 bytes.
Μια
Transaction
έχει δύο πεδία ανώτατου επιπέδου:
signatures: Ένας πίνακας υπογραφώνmessage: Πληροφορίες συναλλαγής, συμπεριλαμβανομένης της λίστας εντολών που θα επεξεργαστούν
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
Διάγραμμα που δείχνει τα δύο μέρη μιας συναλλαγής
Το συνολικό σειριοποιημένο μέγεθος μιας συναλλαγής δεν πρέπει να υπερβαίνει τα
PACKET_DATA_SIZE
(1.232 bytes). Αυτό το όριο ισούται με 1.280 bytes (το ελάχιστο MTU του IPv6)
μείον 48 bytes για κεφαλίδες δικτύου (40 bytes IPv6 + 8 bytes κεφαλίδα
τμήματος). Τα 1.232 bytes περιλαμβάνουν τόσο τον πίνακα
signatures όσο και τη δομή message.
Διάγραμμα που δείχνει τη μορφή συναλλαγής και τα όρια μεγέθους
Υπογραφές
Το πεδίο signatures είναι ένας πίνακας με συμπαγή κωδικοποίηση από
Signature
τιμές. Κάθε Signature είναι μια υπογραφή Ed25519 64 bytes του σειριοποιημένου
Message, υπογεγραμμένη με το ιδιωτικό κλειδί του λογαριασμού υπογράφοντος.
Απαιτείται μία υπογραφή για κάθε λογαριασμό υπογράφοντος
στον οποίο αναφέρονται οι εντολές της συναλλαγής.
Κάθε υπογραφή παράγεται από ένα ιδιωτικό κλειδί. Το πού βρίσκεται αυτό το κλειδί — ένα τοπικό keypair, ένα HSM ή KMS στο cloud, ή μια διαχειριζόμενη υπηρεσία πορτοφολιού — είναι μια απόφαση σχεδιασμού παραγωγής. Δείτε Υπογραφή σε Παραγωγή.
Η πρώτη υπογραφή στον πίνακα ανήκει στον πληρωτή τελών, τον λογαριασμό που πληρώνει το βασικό τέλος και το τέλος προτεραιότητας της συναλλαγής. Αυτή η πρώτη υπογραφή χρησιμεύει επίσης ως το αναγνωριστικό συναλλαγής, που χρησιμοποιείται για την αναζήτηση της συναλλαγής στο δίκτυο. Το αναγνωριστικό συναλλαγής αναφέρεται συνήθως ως η υπογραφή συναλλαγής.
Απαιτήσεις πληρωτή τελών:
- Πρέπει να είναι ο πρώτος λογαριασμός στο μήνυμα (δείκτης 0) και υπογράφων.
- Πρέπει να είναι λογαριασμός που ανήκει στο System Program ή λογαριασμός nonce
(επαληθεύεται από
validate_fee_payer). - Πρέπει να διαθέτει αρκετά lamports για να καλύψει το
rent_exempt_minimum + total_fee· διαφορετικά η συναλλαγή αποτυγχάνει μεInsufficientFundsForFee.
Μήνυμα
Το πεδίο message είναι μια
Message
δομή που περιέχει το περιεχόμενο της συναλλαγής:
header: Η κεφαλίδα του μηνύματοςaccount_keys: Ένας πίνακας από διευθύνσεις λογαριασμών που απαιτούνται από τις εντολές της συναλλαγήςrecent_blockhash: Ένα blockhash που λειτουργεί ως χρονική σήμανση για τη συναλλαγήinstructions: Ένας πίνακας από εντολές
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 είναι μια
MessageHeader
δομή με τρία πεδία u8 που κατανέμουν τον πίνακα account_keys σε ομάδες
δικαιωμάτων:
num_required_signatures: Συνολικός αριθμός υπογραφών που απαιτούνται από τη συναλλαγή.num_readonly_signed_accounts: Αριθμός υπογεγραμμένων λογαριασμών που είναι μόνο για ανάγνωση.num_readonly_unsigned_accounts: Αριθμός ανυπόγραφων λογαριασμών που είναι μόνο για ανάγνωση.
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,}
Παλαιά και εκδοχοποιημένα προθέματα μηνυμάτων
Σε ένα παλαιό μήνυμα συναλλαγής, το πρώτο byte του μηνύματος είναι
num_required_signatures, ακολουθούμενο από τα άλλα δύο bytes
MessageHeader. Σε ένα εκδοχοποιημένο μήνυμα συναλλαγής, το πρώτο byte είναι
αντ' αυτού ένα πρόθεμα έκδοσης· τα τρία bytes MessageHeader ξεκινούν αμέσως
μετά από αυτό το πρόθεμα. Δείτε εκδοχοποιημένες
συναλλαγές για την πλήρη
διάταξη μηνύματος v0.
Διάγραμμα που δείχνει τα τρία μέρη της κεφαλίδας μηνύματος
Διευθύνσεις λογαριασμών
Το πεδίο
account_keys
είναι ένας πίνακας δημόσιων κλειδιών κωδικοποιημένος με συμπαγή μορφή. Κάθε
καταχώρηση προσδιορίζει έναν λογαριασμό που χρησιμοποιείται από τουλάχιστον μία
από τις οδηγίες της συναλλαγής. Ο πίνακας πρέπει να περιλαμβάνει κάθε λογαριασμό
και πρέπει να ακολουθεί αυτή την αυστηρή σειρά:
- Υπογράφων + Εγγράψιμος
- Υπογράφων + Μόνο ανάγνωση
- Μη υπογράφων + Εγγράψιμος
- Μη υπογράφων + Μόνο ανάγνωση
Αυτή η αυστηρή σειρά επιτρέπει στον πίνακα account_keys να συνδυαστεί με τα
τρία πλήθη στην header κεφαλίδα του μηνύματος για τον
προσδιορισμό των δικαιωμάτων για κάθε λογαριασμό χωρίς αποθήκευση σημαιών
μεταδεδομένων ανά λογαριασμό. Τα πλήθη κεφαλίδας διαχωρίζουν τον πίνακα στις
τέσσερις ομάδες δικαιωμάτων που αναφέρονται παραπάνω.
Διάγραμμα που δείχνει τη σειρά του πίνακα διευθύνσεων λογαριασμών
Πρόσφατο blockhash
Το πεδίο recent_blockhash είναι ένα hash 32 bytes που εξυπηρετεί δύο σκοπούς:
- Χρονική σήμανση: αποδεικνύει ότι η συναλλαγή δημιουργήθηκε πρόσφατα.
- Αποκλεισμός διπλοτύπων: αποτρέπει την επεξεργασία της ίδιας συναλλαγής δύο φορές.
Ένα blockhash λήγει μετά από 150 slot. Εάν το blockhash δεν είναι πλέον έγκυρο
όταν φτάσει η συναλλαγή, απορρίπτεται με BlockhashNotFound, εκτός εάν
είναι μια έγκυρη
ανθεκτική συναλλαγή nonce.
Η μέθοδος RPC getLatestBlockhash σάς
επιτρέπει να λάβετε το τρέχον blockhash και το τελευταίο ύψος μπλοκ στο οποίο
το blockhash θα είναι έγκυρο.
Οδηγίες
Το πεδίο
instructions
είναι ένας compact-κωδικοποιημένος πίνακας από δομές
CompiledInstruction.
Κάθε CompiledInstruction αναφέρεται σε λογαριασμούς μέσω ευρετηρίου στον
πίνακα account_keys αντί για πλήρες δημόσιο κλειδί. Περιέχει:
program_id_index: Ευρετήριο στον πίνακαaccount_keysπου προσδιορίζει το πρόγραμμα προς κλήση.accounts: Πίνακας ευρετηρίων στονaccount_keysπου καθορίζει τους λογαριασμούς που θα περαστούν στο πρόγραμμα.data: Πίνακας byte που περιέχει τον διακριτή οδηγίας και τα σειριοποιημένα ορίσματα.
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>,}
Compact πίνακας οδηγιών
Δυαδική μορφή συναλλαγής
Οι συναλλαγές σειριοποιούνται χρησιμοποιώντας ένα compact σχήμα κωδικοποίησης. Όλοι οι πίνακες μεταβλητού μήκους (υπογραφές, κλειδιά λογαριασμών, οδηγίες) έχουν πρόθεμα compact-u16 κωδικοποίηση μήκους. Αυτή η μορφή χρησιμοποιεί 1 byte για τιμές 0-127 και 2-3 bytes για μεγαλύτερες τιμές.
Διάταξη legacy συναλλαγής (στο δίκτυο):
| Πεδίο | Μέγεθος | Περιγραφή |
|---|---|---|
num_signatures | 1-3 bytes (compact-u16) | Αριθμός υπογραφών |
signatures | num_signatures x 64 bytes | Υπογραφές Ed25519 |
num_required_signatures | 1 byte | Πεδίο 1 MessageHeader |
num_readonly_signed | 1 byte | Πεδίο 2 MessageHeader |
num_readonly_unsigned | 1 byte | Πεδίο 3 MessageHeader |
num_account_keys | 1-3 bytes (compact-u16) | Αριθμός στατικών κλειδιών λογαριασμών |
account_keys | num_account_keys x 32 bytes | Δημόσια κλειδιά |
recent_blockhash | 32 bytes | Blockhash |
num_instructions | 1-3 bytes (compact-u16) | Αριθμός οδηγιών |
instructions | μεταβλητό | Πίνακας compiled οδηγιών |
Κάθε compiled οδηγία σειριοποιείται ως εξής:
| Πεδίο | Μέγεθος | Περιγραφή |
|---|---|---|
program_id_index | 1 byte | Ευρετήριο στα κλειδιά λογαριασμών |
num_accounts | 1-3 bytes (compact-u16) | Αριθμός ευρετηρίων λογαριασμών |
account_indices | num_accounts x 1 byte | Ευρετήρια κλειδιών λογαριασμών |
data_len | 1-3 bytes (compact-u16) | Μήκος instruction data |
data | data_len bytes | Αδιαφανή instruction data |
Υπολογισμός μεγέθους
Δεδομένου PACKET_DATA_SIZE = 1.232 bytes, ο διαθέσιμος χώρος μπορεί να
υπολογιστεί:
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
Παράδειγμα: Συναλλαγή μεταφοράς SOL
Το παρακάτω διάγραμμα δείχνει πώς οι συναλλαγές και οι εντολές συνεργάζονται για να επιτρέπουν στους χρήστες να αλληλεπιδρούν με το δίκτυο. Σε αυτό το παράδειγμα, SOL μεταφέρεται από έναν λογαριασμό σε έναν άλλο.
Τα μεταδεδομένα του λογαριασμού αποστολέα υποδεικνύουν ότι πρέπει να υπογράψει για τη συναλλαγή. Αυτό επιτρέπει στο System Program να αφαιρέσει lamports. Τόσο ο λογαριασμός αποστολέα όσο και ο λογαριασμός παραλήπτη πρέπει να είναι εγγράψιμοι, ώστε το υπόλοιπό τους σε lamport να μπορεί να αλλάξει. Για την εκτέλεση αυτής της εντολής, το πορτοφόλι του αποστολέα στέλνει τη συναλλαγή που περιέχει την υπογραφή του και το μήνυμα που περιέχει την εντολή μεταφοράς SOL.
Διάγραμμα μεταφοράς SOL
Αφού σταλεί η συναλλαγή, το System Program επεξεργάζεται την εντολή μεταφοράς και ενημερώνει το υπόλοιπο σε lamport και των δύο λογαριασμών.
Διάγραμμα διαδικασίας μεταφοράς SOL
Επαληθεύστε τον παραλήπτη πριν στείλετε SOL
Μια μεταφορά μέσω System Program προσθέτει lamports σε οποιονδήποτε λογαριασμό. Δεν υπάρχει έλεγχος σε επίπεδο πρωτοκόλλου που να επαληθεύει ότι ο παραλήπτης μπορεί να μετακινήσει ξανά το SOL. Τα lamports μπορούν να μετακινηθούν μόνο από το πρόγραμμα ιδιοκτησίας του λογαριασμού, επομένως η αποστολή SOL σε ένα token mint, σε ένα πρόγραμμα ή σε ένα PDA που δεν ελέγχετε ενέχει κίνδυνο μόνιμης απώλειας κεφαλαίων — μόνο μια αρχή που ορίζεται από το πρόγραμμα ιδιοκτησίας μπορεί να τα επιστρέψει. Το SOL που αποστέλλεται σε ένα token account είναι ανακτήσιμο μόνο από τον κάτοχο εκείνου του λογαριασμού, ποτέ από τον αποστολέα.
Οι μεταφορές SPL token είναι εν μέρει αυτοπροστατευόμενες: το Token Program απορρίπτει μια μεταφορά της οποίας οι λογαριασμοί δεν ταιριάζουν με το αναμενόμενο mint. Οι μεταφορές Native SOL δεν διαθέτουν τέτοια προστασία, οπότε ο αποστολέας πρέπει να επαληθεύσει τον παραλήπτη πριν υπογράψει. Δείτε Επαλήθευση Διεύθυνσης για την πλήρη λογική ταξινόμησης.
Το παρακάτω παράδειγμα δείχνει τον κώδικα που σχετίζεται με τα παραπάνω
διαγράμματα. Δείτε τη
συνάρτηση 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);
Το παρακάτω παράδειγμα δείχνει τη δομή μιας συναλλαγής που περιέχει μία μόνο εντολή μεταφοράς 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));
Ο παρακάτω κώδικας δείχνει την έξοδο από τα προηγούμενα αποσπάσματα κώδικα. Η μορφή διαφέρει μεταξύ των SDK, ωστόσο παρατηρήστε ότι κάθε εντολή περιέχει τις ίδιες απαιτούμενες πληροφορίες.
{"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}}]}
Επαλήθευση του παραλήπτη πριν από τη μεταφορά
Επειδή η μεταφορά SOL επιτυγχάνει σε οποιονδήποτε λογαριασμό, ελέγξτε τον παραλήπτη πριν υπογράψετε. Ανακτήστε τον λογαριασμό και στείλτε μόνο σε ένα πορτοφόλι System Program (ή σε μια μη χρηματοδοτημένη διεύθυνση on-curve ); απορρίψτε mints, token accounts, προγράμματα και PDA που δεν ελέγχετε.
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);
Αυτό το απόσπασμα ελέγχει τους παραλήπτες εγγενούς SOL. Για την πλήρη ταξινόμηση που καλύπτει επίσης αποστολές token SPL (token accounts, ATA, Token-2022), δείτε Verify Address.
Ανάκτηση λεπτομερειών συναλλαγής
Μετά την υποβολή, ανακτήστε τις λεπτομέρειες της συναλλαγής χρησιμοποιώντας την υπογραφή συναλλαγής και τη μέθοδο RPC getTransaction.
Μπορείτε επίσης να βρείτε τη συναλλαγή χρησιμοποιώντας το 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"}
Η ακατέργαστη απόκριση αναγνωρίζει λογαριασμούς με βάση το ευρετήριο και αποθηκεύει τις εσωτερικές οδηγίες (CPI) ως κωδικοποιημένα blobs. Για να τις αναλύσετε σε διευθύνσεις και να πλοηγηθείτε στο πλήρες δέντρο οδηγιών, δείτε Transaction Introspection.
Is this page helpful?