Transaction

Transaction Body

Connection.sendTransaction

sendTransaction handles message signing and sending tx to solana network.
  • If it’s a versioned transaction, it will just serialize the tx and send it. This means versioned transaction should be signed before calling sendTransaction.
  • If it’s a legacy transaction, it first let signers sign the tx to get signatures, and update recentBlockhash, then serializes the tx and send it.
/// --- node_modules/@solana/web3.js/src/connection.ts --- export class Connection { /// ... /** * Sign and send a transaction */ // eslint-disable-next-line no-dupe-class-members async sendTransaction( transaction: VersionedTransaction | Transaction, signersOrOptions?: Array<Signer> | SendOptions, options?: SendOptions, ): Promise<TransactionSignature> { if ('version' in transaction) { if (signersOrOptions && Array.isArray(signersOrOptions)) { throw new Error('Invalid arguments'); } const wireTransaction = transaction.serialize(); return await this.sendRawTransaction(wireTransaction, signersOrOptions); } if (signersOrOptions === undefined || !Array.isArray(signersOrOptions)) { throw new Error('Invalid arguments'); } const signers = signersOrOptions; if (transaction.nonceInfo) { transaction.sign(...signers); } else { let disableCache = this._disableBlockhashCaching; for (;;) { const latestBlockhash = await this._blockhashWithExpiryBlockHeight(disableCache); transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight; transaction.recentBlockhash = latestBlockhash.blockhash; transaction.sign(...signers); if (!transaction.signature) { throw new Error('!signature'); // should never happen } const signature = transaction.signature.toString('base64'); if (!this._blockhashInfo.transactionSignatures.includes(signature)) { // The signature of this transaction has not been seen before with the // current recentBlockhash, all done. Let's break this._blockhashInfo.transactionSignatures.push(signature); break; } else { // This transaction would be treated as duplicate (its derived signature // matched to one of already recorded signatures). // So, we must fetch a new blockhash for a different signature by disabling // our cache not to wait for the cache expiration (BLOCKHASH_CACHE_TIMEOUT_MS). disableCache = true; } } } const wireTransaction = transaction.serialize(); return await this.sendRawTransaction(wireTransaction, options); } /// ... }
 

Transaction.sign

The Transaction.sign method is responsible for applying digital signatures to a transaction, ensuring its authenticity and authorization by the specified signers.
Steps:
  1. Signers Validation
    1. Ensures that at least one signer is provided. A transaction must have at least one signature to be valid.
  1. Deduplication of Signers
    1. Removes duplicate signers to prevent multiple signatures from the same account.
  1. Resetting Signatures Array
    1. Initializes the signatures array with the provided signers. Existing signatures are overwritten. This behavior ensures that the transaction only contains signatures from the current signers.
  1. Compiling the Transaction Message
    1. Compiles the transaction into a Message object, which is a structured representation of the transaction's contents.
      • _compile handles message compilation, including deduplicating and ordering accounts, handling fee payer positioning, and preparing instruction data.
      • It ensures that the message is ready for signing, incorporating all instructions and account metadata.
  1. Partial Signing
    1. Applies signatures to the transaction using the provided signers.
/// --- node_modules/@solana/web3.js/src/transaction/legacy.ts --- export class Transaction { /// ... sign(...signers: Array<Signer>) { /// Signers Validation if (signers.length === 0) { throw new Error('No signers'); } /// Deduplication of Signers const seen = new Set(); const uniqueSigners = []; for (const signer of signers) { const key = signer.publicKey.toString(); if (seen.has(key)) { continue; } else { seen.add(key); uniqueSigners.push(signer); } } /// Resetting Signatures Array this.signatures = uniqueSigners.map(signer => ({ signature: null, publicKey: signer.publicKey, })); /// Compiling the Transaction Message const message = this._compile(); /// Partial Signing this._partialSign(message, ...uniqueSigners); } /// ... }

Compile Message

_compile() is typically invoked before signing or serializing the transaction, ensuring that the transaction structure and signatures are consistent and ready for signing or verification.
 
Steps:
  1. Compiling the Message
    1. Calls compileMessage() to produce a Message object.
      compileMessage():
      • This is a crucial step where the transaction’s instructions, fee payer, and account lists are transformed into a canonical Message.
      • The Message includes:
        • A header specifying counts of required signatures and read-only accounts.
        • An ordered list of account keys.
        • The recent blockhash or nonce.
        • Compiled instructions referencing these accounts by index.
  1. Extracting Signed Keys
    1. Identify the subset of message.accountKeys that must sign the transaction.
      • message.header.numRequiredSignatures indicates how many accounts at the start of message.accountKeys are expected to produce signatures.
  1. Validating Existing Signatures
    1. If the current number of this.signatures matches the number of required signers (signedKeys.length), the code checks if each signature’s public key matches the corresponding signer key in signedKeys.
      • The transaction can have pre-existing signatures from previous signing operations.
      • If these signatures already align perfectly with the required signer keys from the message (meaning we do not need to rearrange or reset them), then the transaction is consistent as-is, and there's no need to alter the signatures array.
  1. Reconstructing Signatures Array
    1. If the existing signatures array either doesn't match the order and keys required by message or if it’s not the correct length, _compile() resets it.
/// --- node_modules/@solana/web3.js/src/transaction/legacy.ts --- export class Transaction { /// ... _compile(): Message { /// Compiling the Message const message = this.compileMessage(); /// Extracting Signed Keys const signedKeys = message.accountKeys.slice( 0, message.header.numRequiredSignatures, ); /// Validating Existing Signatures if (this.signatures.length === signedKeys.length) { const valid = this.signatures.every((pair, index) => { return signedKeys[index].equals(pair.publicKey); }); if (valid) return message; } /// Reconstructing Signatures Array this.signatures = signedKeys.map(publicKey => ({ signature: null, publicKey, })); return message; } /// ... } export class Message { header: MessageHeader; accountKeys: PublicKey[]; recentBlockhash: Blockhash; instructions: CompiledInstruction[]; /// ... } /// --- node_modules/@solana/web3.js/src/message/index.ts --- /** * The message header, identifying signed and read-only account */ export type MessageHeader = { /** * The number of signatures required for this message to be considered valid. The * signatures must match the first `numRequiredSignatures` of `accountKeys`. */ numRequiredSignatures: number; /** The last `numReadonlySignedAccounts` of the signed keys are read-only accounts */ numReadonlySignedAccounts: number; /** The last `numReadonlySignedAccounts` of the unsigned keys are read-only accounts */ numReadonlyUnsignedAccounts: number; }; /// --- node_modules/@solana/web3.js/src/blockhash.ts --- /** * Blockhash as Base58 string. */ export type Blockhash = string; /// --- node_modules/@solana/web3.js/src/message/legacy.ts --- /** * An instruction to execute by a program * * @property {number} programIdIndex * @property {number[]} accounts * @property {string} data */ export type CompiledInstruction = { /** Index into the transaction keys array indicating the program account that executes this instruction */ programIdIndex: number; /** Ordered indices into the transaction keys array indicating which accounts to pass to the program */ accounts: number[]; /** The program input data encoded as base 58 */ data: string; };

compileMessage

compileMessage() is called internally to finalize a transaction before signing, verifying signatures, or serialization. The resulting Message is a lower-level representation that the Solana blockchain understands. It includes:
  • A structured list of accounts used by the transaction.
  • A header that specifies how many accounts must sign, and how many of them are read-only.
  • The recent blockhash (or nonce) used to prevent replay attacks.
  • Compiled instructions that reference accounts by index rather than by public key to save space and increase efficiency.
 
Steps:
  1. Memorization Check
    1. If the transaction’s current JSON representation (this.toJSON()) matches the previously cached version (this._json), return the already compiled _message without recompiling.
      This saves computation if the transaction hasn't changed since last compilation.
  1. Determining Blockhash and Instructions
      • If the transaction uses a durable nonce, set recentBlockhash to the nonce value and prepend the nonceInstruction if it's not already present.
      • Otherwise, use the recentBlockhash provided for a normal transaction.
      • Checks: Throws if recentBlockhash is missing, since every transaction must reference a valid blockhash or nonce.
      • Outcome: instructions now contains a final list of instructions, possibly including a nonce instruction at the start.
      • Note that for a tx using durable nonce, the blockhash is the nonce, and the first instruction should be the System’s AdvanceNonceAccount instruction.
  1. Determining Fee Payer
    1. Determines which account pays the transaction fee. If not explicitly set, the first signer is used as fee payer.
  1. Validate Instructions
    1. Ensures each instruction has a defined programId.
  1. Collecting Account Metas and Program IDs
    1. Extracts account metadata from each instruction and records program IDs (the on-chain programs to execute).
  1. Append Program Accounts
    1. Adds each program ID as a read-only, non-signing account. Even though these are executables, they need to appear in the account list for the runtime to load and execute them.
  1. Deduplicate and Merge Account Metas
    1. Removes duplicate entries for the same account and merges their permissions. If any usage of that account is as a signer or writable, it remains so.
  1. Sorting Accounts
      • Priority:
          1. Signers first.
          1. Among signers, writable accounts first.
          1. Among equals, lex order by pubkey.
      • Outcome: Ensures a deterministic ordering that meets Solana's runtime expectations.
  1. Fee Payer to Front
    1. The fee payer must appear as the first account. If not present, it's inserted at the front.
  1. Validate Signers and Check for Unknown Signers
    1. Ensures every signature corresponds to a known account in uniqueMetas. If a signature references an account not present in uniqueMetas, it fails. If a signer wasn't marked as signer, mark it now (deprecated behavior).
      Note: solana allows to append signature of an account into a tx which is not specified as signer. It only shows warning.
  1. Counting Signers and Read-Only Accounts
    1. Counts how many signers, how many read-only signers, and how many read-only non-signers exist to populate the Message.header.
      Note accountKeys has same account order with uniqueMetas because uniqueMetas has been ordered based on account properties already (signer first, then writable first).
  1. Constructing the Message
    1. Build the Message object fields.
      • accountKeys: Final ordered list of account public keys.
      • compiledInstructions: Each instruction references accounts by their index in accountKeys, and data is base58-encoded.
/// --- node_modules/@solana/web3.js/src/transaction/legacy.ts --- export class Transaction { /// ... /** * Compile transaction data */ compileMessage(): Message { /// Memorization Check if ( this._message && JSON.stringify(this.toJSON()) === JSON.stringify(this._json) ) { return this._message; } /// Determining Blockhash and Instructions let recentBlockhash; let instructions: TransactionInstruction[]; if (this.nonceInfo) { recentBlockhash = this.nonceInfo.nonce; if (this.instructions[0] != this.nonceInfo.nonceInstruction) { instructions = [this.nonceInfo.nonceInstruction, ...this.instructions]; } else { instructions = this.instructions; } } else { recentBlockhash = this.recentBlockhash; instructions = this.instructions; } if (!recentBlockhash) { throw new Error('Transaction recentBlockhash required'); } if (instructions.length < 1) { console.warn('No instructions provided'); } /// Determining Fee Payer let feePayer: PublicKey; if (this.feePayer) { feePayer = this.feePayer; } else if (this.signatures.length > 0 && this.signatures[0].publicKey) { // Use implicit fee payer feePayer = this.signatures[0].publicKey; } else { throw new Error('Transaction fee payer required'); } /// Validate Instructions for (let i = 0; i < instructions.length; i++) { if (instructions[i].programId === undefined) { throw new Error( `Transaction instruction index ${i} has undefined program id`, ); } } /// Collecting Account Metas and Program IDs const programIds: string[] = []; const accountMetas: AccountMeta[] = []; instructions.forEach(instruction => { instruction.keys.forEach(accountMeta => { accountMetas.push({...accountMeta}); }); const programId = instruction.programId.toString(); if (!programIds.includes(programId)) { programIds.push(programId); } }); /// Append Program Accounts' Metas programIds.forEach(programId => { accountMetas.push({ pubkey: new PublicKey(programId), isSigner: false, isWritable: false, }); }); /// Deduplicate and Merge Account Metas const uniqueMetas: AccountMeta[] = []; accountMetas.forEach(accountMeta => { const pubkeyString = accountMeta.pubkey.toString(); const uniqueIndex = uniqueMetas.findIndex(x => { return x.pubkey.toString() === pubkeyString; }); if (uniqueIndex > -1) { uniqueMetas[uniqueIndex].isWritable = uniqueMetas[uniqueIndex].isWritable || accountMeta.isWritable; uniqueMetas[uniqueIndex].isSigner = uniqueMetas[uniqueIndex].isSigner || accountMeta.isSigner; } else { uniqueMetas.push(accountMeta); } }); /// Sort. Prioritizing first by signer, then by writable uniqueMetas.sort(function (x, y) { if (x.isSigner !== y.isSigner) { // Signers always come before non-signers return x.isSigner ? -1 : 1; } if (x.isWritable !== y.isWritable) { // Writable accounts always come before read-only accounts return x.isWritable ? -1 : 1; } // Otherwise, sort by pubkey, stringwise. const options = { localeMatcher: 'best fit', usage: 'sort', sensitivity: 'variant', ignorePunctuation: false, numeric: false, caseFirst: 'lower', } as Intl.CollatorOptions; return x.pubkey .toBase58() .localeCompare(y.pubkey.toBase58(), 'en', options); }); /// Move fee payer to the front const feePayerIndex = uniqueMetas.findIndex(x => { return x.pubkey.equals(feePayer); }); if (feePayerIndex > -1) { const [payerMeta] = uniqueMetas.splice(feePayerIndex, 1); payerMeta.isSigner = true; payerMeta.isWritable = true; uniqueMetas.unshift(payerMeta); } else { uniqueMetas.unshift({ pubkey: feePayer, isSigner: true, isWritable: true, }); } /// Validate that Signers Exist in Account Metas and Check for Unknown Signers for (const signature of this.signatures) { const uniqueIndex = uniqueMetas.findIndex(x => { return x.pubkey.equals(signature.publicKey); }); if (uniqueIndex > -1) { if (!uniqueMetas[uniqueIndex].isSigner) { uniqueMetas[uniqueIndex].isSigner = true; console.warn( 'Transaction references a signature that is unnecessary, ' + 'only the fee payer and instruction signer accounts should sign a transaction. ' + 'This behavior is deprecated and will throw an error in the next major version release.', ); } } else { throw new Error(`unknown signer: ${signature.publicKey.toString()}`); } } /// Counting Signers and Read-Only Accounts let numRequiredSignatures = 0; let numReadonlySignedAccounts = 0; let numReadonlyUnsignedAccounts = 0; // Split out signing from non-signing keys and count header values const signedKeys: string[] = []; const unsignedKeys: string[] = []; uniqueMetas.forEach(({pubkey, isSigner, isWritable}) => { if (isSigner) { signedKeys.push(pubkey.toString()); numRequiredSignatures += 1; if (!isWritable) { numReadonlySignedAccounts += 1; } } else { unsignedKeys.push(pubkey.toString()); if (!isWritable) { numReadonlyUnsignedAccounts += 1; } } }); /// Constructing the Message const accountKeys = signedKeys.concat(unsignedKeys); const compiledInstructions: CompiledInstruction[] = instructions.map( instruction => { const {data, programId} = instruction; return { programIdIndex: accountKeys.indexOf(programId.toString()), accounts: instruction.keys.map(meta => accountKeys.indexOf(meta.pubkey.toString()), ), data: bs58.encode(data), }; }, ); compiledInstructions.forEach(instruction => { invariant(instruction.programIdIndex >= 0); instruction.accounts.forEach(keyIndex => invariant(keyIndex >= 0)); }); /// Return the Constructed Message return new Message({ header: { numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts, }, accountKeys, recentBlockhash, instructions: compiledInstructions, }); } toJSON(): TransactionJSON { return { recentBlockhash: this.recentBlockhash || null, feePayer: this.feePayer ? this.feePayer.toJSON() : null, nonceInfo: this.nonceInfo ? { nonce: this.nonceInfo.nonce, nonceInstruction: this.nonceInfo.nonceInstruction.toJSON(), } : null, instructions: this.instructions.map(instruction => instruction.toJSON()), signers: this.signatures.map(({publicKey}) => { return publicKey.toJSON(); }), }; } /// ... }

PartialSign

While the sign method completely sets the transaction’s signers and clears any previous signatures, partialSign is designed for a scenario where you already have a partially signed transaction and wish to add more signatures without disturbing the existing ones.
 
Steps:
  1. Input Validation
    1. Ensures that the method is called with at least one signer. A call without any signers doesn't make sense, as no new signatures can be added.
  1. Deduplicating Signers
    1. Removes any duplicate signers to ensure each public key is only counted once.
  1. Compiling the Message
    1. Ensures the transaction is in a canonical and finalized state before adding signatures.
  1. Invoking _partialSign
    1. Actually applies the signatures from the provided signers to the transaction.
/// --- node_modules/@solana/web3.js/src/transaction/legacy.ts --- export class Transaction { /// ... /** * Partially sign a transaction with the specified accounts. All accounts must * correspond to either the fee payer or a signer account in the transaction * instructions. * * All the caveats from the `sign` method apply to `partialSign` * * @param {Array<Signer>} signers Array of signers that will sign the transaction */ partialSign(...signers: Array<Signer>) { /// Input Validation if (signers.length === 0) { throw new Error('No signers'); } /// Deduplicating Signers const seen = new Set(); const uniqueSigners = []; for (const signer of signers) { const key = signer.publicKey.toString(); if (seen.has(key)) { continue; } else { seen.add(key); uniqueSigners.push(signer); } } /// Compiling the Message const message = this._compile(); /// Invoking _partialSign this._partialSign(message, ...uniqueSigners); } /// ... }

_partialSign

_partialSign serializes transaction message to get serialized buffer, and let signers to sign for the data to get signature, finally it inserts signature into Transaction.signatures.
/// --- node_modules/@solana/web3.js/src/transaction/legacy.ts --- export class Transaction { /// ... _partialSign(message: Message, ...signers: Array<Signer>) { const signData = message.serialize(); signers.forEach(signer => { const signature = sign(signData, signer.secretKey); this._addSignature(signer.publicKey, toBuffer(signature)); }); } /** * @internal */ _addSignature(pubkey: PublicKey, signature: Buffer) { invariant(signature.length === 64); const index = this.signatures.findIndex(sigpair => pubkey.equals(sigpair.publicKey), ); if (index < 0) { throw new Error(`unknown signer: ${pubkey.toString()}`); } this.signatures[index].signature = Buffer.from(signature); } /// ... }

serialize

The Message class represents a compiled transaction message, which includes all the accounts and instructions that will be executed. The serialize() method:
  1. Encodes the Message into a binary format ready for inclusion in a transaction.
  1. Uses a well-defined layout: first the message header and accounts, then the recent blockhash, and finally the instructions.
  1. Prepares data that can be signed and combined with transaction signatures later by the Transaction class or when creating a fully signed transaction.
 
Steps:
  1. Encoding the Number of Keys
    1. Uses a shortvec encoding to represent numKeys (the number of account keys).
  1. Preparing Instructions for Serialization
      • Decodes each instruction’s data from base58 into a raw byte array.
      • Encodes the length of the instruction’s account indices (keyIndicesCount) and the length of the instruction’s data (dataCount) using shortvec.
      • This produces a normalized instruction structure suitable for passing into a BufferLayout encoder.
  1. Encoding the Number of Instructions
      • Encode how many instructions are in the message using shortvec.
      • Prepares a buffer that will hold all instructions data.
  1. Encoding Each Instruction
      • Purpose:
        • Uses BufferLayout to define how each instruction is serialized.
        • For each instruction:
          • programIdIndex: 1 byte.
          • keyIndicesCount: Variable length (from shortvec).
          • keyIndices: A sequence of account index bytes.
          • dataLength: Variable length (shortvec).
          • data: The instruction data bytes.
      • Outcome: instructionBuffer now contains a binary representation of all instructions appended together.
  1. Defining the Message Layout (Header, Keys, Recent Blockhash)
    1. Purpose:
      Defines the binary layout of the message portion:
      • Header:
        • numRequiredSignatures (1 byte)
        • numReadonlySignedAccounts (1 byte)
        • numReadonlyUnsignedAccounts (1 byte)
      • keyCount (shortvec-encoded count)
      • keys: Each account public key is 32 bytes, all listed in order.
      • recentBlockhash: 32 bytes.
  1. Creating the Transaction Object for Encoding
    1. Organize the message header and data into a structured object that matches the signDataLayout.
  1. Encoding the Message Header and Accounts
      • Purpose: Encode the transaction header and accounts into signData using signDataLayout.
      • 2048: A large enough buffer size to fit the entire message.
      • length: The number of bytes written.
  1. Appending Instructions to signData
      • Purpose: After encoding the header and accounts, append the instruction buffer.
      • Result:
        • The final signData = [header + account keys + recentBlockhash] + [instructions].
  1. Return the Final Result
    1. The returned Buffer is the fully serialized message in wire format.
/// --- node_modules/@solana/web3.js/src/transaction/legacy.ts --- export class Transaction { /// ... serialize(): Buffer { /// Encoding the Number of Keys const numKeys = this.accountKeys.length; let keyCount: number[] = []; shortvec.encodeLength(keyCount, numKeys); /// Preparing Instructions for Serialization const instructions = this.instructions.map(instruction => { const {accounts, programIdIndex} = instruction; const data = Array.from(bs58.decode(instruction.data)); let keyIndicesCount: number[] = []; shortvec.encodeLength(keyIndicesCount, accounts.length); let dataCount: number[] = []; shortvec.encodeLength(dataCount, data.length); return { programIdIndex, keyIndicesCount: Buffer.from(keyIndicesCount), keyIndices: accounts, dataLength: Buffer.from(dataCount), data, }; }); /// Encoding the Number of Instructions let instructionCount: number[] = []; shortvec.encodeLength(instructionCount, instructions.length); let instructionBuffer = Buffer.alloc(PACKET_DATA_SIZE); Buffer.from(instructionCount).copy(instructionBuffer); let instructionBufferLength = instructionCount.length; /// Encoding Each Instruction instructions.forEach(instruction => { const instructionLayout = BufferLayout.struct< Readonly<{ data: number[]; dataLength: Uint8Array; keyIndices: number[]; keyIndicesCount: Uint8Array; programIdIndex: number; }> >([ BufferLayout.u8('programIdIndex'), BufferLayout.blob( instruction.keyIndicesCount.length, 'keyIndicesCount', ), BufferLayout.seq( BufferLayout.u8('keyIndex'), instruction.keyIndices.length, 'keyIndices', ), BufferLayout.blob(instruction.dataLength.length, 'dataLength'), BufferLayout.seq( BufferLayout.u8('userdatum'), instruction.data.length, 'data', ), ]); const length = instructionLayout.encode( instruction, instructionBuffer, instructionBufferLength, ); instructionBufferLength += length; }); instructionBuffer = instructionBuffer.slice(0, instructionBufferLength); /// Defining the Message Layout (Header, Keys, Recent Blockhash) const signDataLayout = BufferLayout.struct< Readonly<{ keyCount: Uint8Array; keys: Uint8Array[]; numReadonlySignedAccounts: Uint8Array; numReadonlyUnsignedAccounts: Uint8Array; numRequiredSignatures: Uint8Array; recentBlockhash: Uint8Array; }> >([ BufferLayout.blob(1, 'numRequiredSignatures'), BufferLayout.blob(1, 'numReadonlySignedAccounts'), BufferLayout.blob(1, 'numReadonlyUnsignedAccounts'), BufferLayout.blob(keyCount.length, 'keyCount'), BufferLayout.seq(Layout.publicKey('key'), numKeys, 'keys'), Layout.publicKey('recentBlockhash'), ]); /// Creating the Transaction Object for Encoding const transaction = { numRequiredSignatures: Buffer.from([this.header.numRequiredSignatures]), numReadonlySignedAccounts: Buffer.from([ this.header.numReadonlySignedAccounts, ]), numReadonlyUnsignedAccounts: Buffer.from([ this.header.numReadonlyUnsignedAccounts, ]), keyCount: Buffer.from(keyCount), keys: this.accountKeys.map(key => toBuffer(key.toBytes())), recentBlockhash: bs58.decode(this.recentBlockhash), }; /// Encoding the Message Header and Accounts let signData = Buffer.alloc(2048); const length = signDataLayout.encode(transaction, signData); instructionBuffer.copy(signData, length); /// Appending Instructions to signData return signData.slice(0, length + instructionBuffer.length); } /// ... } /// --- node_modules/@solana/web3.js/src/utils/shortvec-encoding.ts --- export function encodeLength(bytes: Array<number>, len: number) { let rem_len = len; for (;;) { let elem = rem_len & 0x7f; rem_len >>= 7; if (rem_len == 0) { bytes.push(elem); break; } else { elem |= 0x80; bytes.push(elem); } } }

Authority in Each Instruction

When sending a transaction includes multiple intrusctions. If multiple instructions refer to same accounts, those accouts have same signer and writer authority.

CPI Call Authority

When use anchor CPI call, the priviledge is decided by callee’s Accounts structure. For exmaple, if account is marked as #[account(mut)], then account will be passed as writable(decieded in the account meta).

Ownership Transfer

TODO: What’s the restriction, and how does solana runtime check?