Keypairs & Addresses
Solana identity is built on Ed25519 keypairs. The public key is the account address; the secret key authorizes transactions. Understanding signing roles prevents authorization bugs and key leaks.
Recipe
Quick-reference recipe card - copy-paste ready.
solana-keygen new --outfile ./keypair.json --no-bip39-passphrase
solana-keygen pubkey ./keypair.jsonimport { createKeyPairSignerFromBytes, address } from "@solana/kit";
import { readFileSync } from "node:fs";
const secret = Uint8Array.from(JSON.parse(readFileSync("./keypair.json", "utf8")));
const signer = await createKeyPairSignerFromBytes(secret);
console.log(signer.address);When to reach for this:
- Creating wallets for development and testing
- Loading signers in server-side transaction builders
- Verifying that an account meta is marked
isSigner: true - Deriving PDAs (addresses without secret keys) for program-owned accounts
- Validating base58 address strings before RPC calls
Working Example
import {
createSolanaRpc,
createKeyPairSignerFromBytes,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
getSignatureFromTransaction,
pipe,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
import { readFileSync } from "node:fs";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
// Load signer from keypair file
const secret = Uint8Array.from(
JSON.parse(readFileSync(process.env.HOME + "/.config/solana/id.json", "utf8")),
);
const feePayer = await createKeyPairSignerFromBytes(secret);
const recipient = await createKeyPairSignerFromBytes(
crypto.getRandomValues(new Uint8Array(64)),
);
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayer(feePayer.address, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) =>
appendTransactionMessageInstruction(
getTransferSolInstruction({
source: feePayer.address,
destination: recipient.address,
amount: 1_000_000n,
}),
m,
),
);
const signed = await signTransactionMessageWithSigners(message, [feePayer]);
const signature = getSignatureFromTransaction(signed);
console.log("Signature:", signature);What this demonstrates:
- Keypair files store a 64-byte array: 32-byte seed + 32-byte public key
- The fee payer must sign because it authorizes lamport deduction
createKeyPairSignerFromByteswraps the keypair as a Kit signer- Recipient does not sign - only the source account's authority signs transfers
Deep Dive
How It Works
- Ed25519 produces a 32-byte public key from a 32-byte seed
- Solana stores the 64-byte secret key (seed + pubkey) in keypair JSON files
- Transactions list accounts with
isSignerandisWritableflags - The runtime verifies Ed25519 signatures against declared signers before execution
Signer Roles
| Role | Signs? | Typical Account |
|---|---|---|
| Fee payer | Yes | First signer; pays transaction fee |
| Authority | Yes | Owner of lamports or token account |
| Program | No | Executable account invoked by instruction |
| PDA | No (program signs via seeds) | Derived address without private key |
Rust Notes
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct UpdateData<'info> {
#[account(mut, signer)]
pub authority: Signer<'info>,
#[account(mut, has_one = authority)]
pub data_account: Account<'info, MyData>,
}Signer<'info>constraint verifies the account signed the transaction- PDAs use
invoke_signedwith seeds instead of a private key
Gotchas
- Committing keypair.json to git - instant fund drain on mainnet. Fix: use
.gitignore, env vars, or hardware wallets for production. - Missing
isSignerflag - transaction rejected at runtime. Fix: mark every signing account as signer in instruction accounts. - Assuming pubkey equals PDA - PDAs are off-curve and have no secret key. Fix: derive PDAs with
find_program_addressand let the program sign. - Using wrong keypair for cluster - keypair works everywhere but accounts are cluster-specific. Fix: separate dev and prod key files.
- 64-byte vs 32-byte confusion - Solana keypair files are 64 bytes, seeds are 32. Fix: use
solana-keygenor Kit helpers, not raw byte slicing. - Fee payer not first signer - some clients require fee payer at index 0. Fix: set fee payer first in the account list.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
solana-keygen CLI | Quick dev wallets | Production key management |
| Hardware wallet (Ledger) | Mainnet signing | Automated server signing at high throughput |
@solana/kit signers | TypeScript transaction building | On-chain program logic |
| PDAs with seeds | Program-controlled accounts | User-owned wallets |
FAQs
What algorithm does Solana use for signatures?
Ed25519. Public keys are 32 bytes, base58-encoded for display.
What is in a Solana keypair JSON file?
A JSON array of 64 bytes: the 32-byte secret seed concatenated with the 32-byte public key.
Is a public key the same as an address?
Yes. The terms are interchangeable - the base58-encoded 32-byte Ed25519 public key.
Can two different keypairs share an address?
No. Each valid keypair maps to exactly one public key/address.
What is a Program Derived Address (PDA)?
An address derived from seeds and a program ID. It has no private key - the program signs for it via invoke_signed.
How many signers can a transaction have?
Up to 16 signatures per transaction in the current format. The fee payer is always one of them.
Does the recipient sign a SOL transfer?
No. Only the source account's authority (and fee payer) must sign.
How do I validate a base58 address string?
import { address } from "@solana/kit";
try {
address("invalid!");
} catch {
console.log("Invalid address");
}Can I recover a keypair from just the public key?
No. The secret key cannot be derived from the public key (one-way function).
What is the System Program address?
11111111111111111111111111111111 - handles account creation and SOL transfers.
Should server-side apps use the same keypair as the CLI default?
No. Use dedicated service keypairs with minimal funded balances and strict access controls.
How does a hardware wallet sign?
The private key never leaves the device. The app sends the transaction message; the device returns the signature.
Related
- Solana Basics - keypair generation example
- How a Transaction Flows - where signing fits
- Wallets & Explorers - wallet tools
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.