Skip to content

Message Signing & Verification

The last three pages built the identities. Ed25519 Keypairs gave you a 32-byte private key and its matching 32-byte public key; Addresses ARE Public Keys showed that the Base58 string you paste into a wallet is that public key. But an identity you can name is not yet an identity you can act as. Owning an address proves nothing until you can prove — to a machine that has never met you and never will — that you, and only you, authorized a specific action.

That proof is a signature. This page is where the keypair earns its keep. We answer three questions from first principles: what a signature actually is, why anyone can check it without knowing your secret, and how the Solana runtime turns “this signature verifies” into “this transaction is allowed to run.” Get this and the whole permission model of the chain — who can move whose lamports, who can invoke whose account — stops being a rule you memorize and starts being a consequence of one 64-byte number.

A signature is a 64-byte value produced from two inputs: your private key, and the exact message you want to authorize. The operation is one-directional and deterministic in the way that matters:

sign(private_key, message) → signature (64 bytes)
verify(public_key, message, sig) → true / false

Notice the asymmetry, and stare at it, because it is the entire trick:

  • Signing needs the private key. Only the key-holder can do it.
  • Verifying needs only the public key — which is the address, already known to everyone.

So the person who authorizes and the people who check are using different halves of the same keypair. The signer proves possession of a secret without ever transmitting it. The verifier gains certainty without ever learning it. No shared password, no server that “logs you in,” no registry to consult. A signature is a portable, self-contained proof that travels with the message.

Alice (has private key) Anyone (has only the address)
────────────────────── ─────────────────────────────
msg = "pay Bob 5 SOL" msg = "pay Bob 5 SOL"
sig = sign(alice_priv, msg) ──────────► ok? = verify(alice_pub, msg, sig)
sig+msg → true only if BOTH the key and
the message are exactly right

A verifying signature gives you three properties at once. They are not marketing bullets; each one is a thing an attacker cannot do, and the whole security model rests on them.

1. Authenticity — only the key-holder could have made it. Producing a signature that verifies against alice_pub requires alice_priv. There is no shortcut, no forgery, no way to fake it from the public key alone. So a valid signature over a message is proof the message was authorized by whoever controls that private key. This is authentication with no login: the signature itself is the credential.

2. Integrity — it commits to this exact message. The signature is computed over the message bytes. Change one byte of the message — flip 5 SOL to 6 SOL, swap the recipient, reorder the instructions — and the signature no longer verifies. It does not “mostly” verify or “verify with a warning”; verification is a hard boolean, and a tampered message fails it. A signature is a seal on this message and no other.

3. Non-repudiation — anyone can verify, forever. Because verification needs only the public key, the proof is not between you and one counterparty. The signer cannot later say “that wasn’t me” to some parties and “it was me” to others — the same signature verifies identically for every observer who has the message and the address. This is exactly the property a public blockchain needs: thousands of validators must reach the same verdict on the same transaction, independently.

authenticity → who authorized it (needs the private key to produce)
integrity → what was authorized (bound to these exact bytes)
non-repudiation → provable to everyone (needs only the public key to check)

Under the hood — Ed25519’s deterministic nonce

Section titled “Under the hood — Ed25519’s deterministic nonce”

Every signature scheme in this family mixes in a per-signature random-looking value — call it the nonce. It is what makes two signatures over the same message look different and keeps the private key hidden. The danger is that if this nonce is ever predictable or reused across two different messages, the math collapses and the private key can be recovered from the two signatures. This is not theoretical; it is a well-trodden way to lose a key.

Ed25519 — the scheme Solana uses — closes that trap by construction. Instead of drawing the nonce from a random-number generator, it derives the nonce deterministically by hashing the private key together with the message:

ECDSA (Bitcoin, older Ethereum):
nonce = random() ← a bad RNG or a reused value LEAKS the key
Ed25519 (Solana):
nonce = hash( secret_part_of_key , message )
← no RNG at signing time; same (key,msg)
always yields the same, safe nonce

Two consequences follow, and both are load-bearing:

  • Signing needs no randomness at runtime. A validator, a hardware wallet, a resource-starved embedded signer — none of them need a good random number generator at the moment of signing. Whole categories of “the RNG was weak / broken / backdoored” failures simply do not apply.
  • The nonce-reuse key-leak is impossible. You cannot accidentally reuse a nonce across two different messages, because the nonce is a function of the message. Different message → different nonce, always; same message → same signature, harmlessly.

How Solana gates a transaction with signatures

Section titled “How Solana gates a transaction with signatures”

Now connect it to the machine. A Solana transaction is, at its core, a message (the list of instructions, the accounts they touch, a recent blockhash) plus a list of signatures over that message’s bytes. When a validator receives a transaction, before it will schedule or execute anything, it runs the check that this whole part has been building toward:

For every account the message marks as a required signer, there must be a signature that verifies against that account’s address over the message bytes. If any required signature is missing or fails, the entire transaction is rejected — it never runs.

This is where the three guarantees cash out on-chain:

  • Integrity means you cannot take a validly-signed transaction and edit it — bump the amount, redirect a transfer — because the signature is over the exact message bytes and would stop verifying.
  • Authenticity means only the holder of an account’s private key can produce a signature that passes for that account, so only they can authorize actions on its behalf.
  • Non-repudiation means every validator on Earth reaches the same accept/reject verdict independently.

That last point is the throughline of this whole book — how do you run one global state machine at hardware speed without a coordinator? Signature verification is a big part of the answer. Because a signature is checkable by anyone with only public inputs, untrusting validators do not need to consult each other, or a central authority, or any shared registry to agree that a transaction is authorized. Each one verifies the 64 bytes itself and arrives at the same answer. The permission check is local, stateless, and non-interactive — and that is precisely what lets thousands of machines process the same transaction stream without a bottleneck in the middle.

In the account model, a transaction hands a program a list of accounts, and each account carries flags. One of those flags is is_signer. Marking an account as a signer is a claim: the message includes a valid signature from this account’s key. The runtime enforces the claim before the program ever runs — it will not invoke the program unless every account tagged is_signer is backed by a verifying signature.

This is how a program knows an instruction was authorized by a particular account’s owner. The program does not verify signatures itself and does not see private keys; it simply trusts the runtime’s guarantee and reads the boolean:

// Inside a Solana program (Anchor-style): the runtime has ALREADY verified the
// signature by the time this runs. The program only needs to CHECK the flag.
if !ctx.accounts.authority.is_signer {
return err!(ErrorCode::MissingRequiredSignature);
}
// From here on, the program may treat `authority` as "the owner of this key
// approved this call" — because a signature over the whole message proved it.

The division of labor is clean and is worth internalizing. The runtime does the cryptography — it verifies each 64-byte signature against each signer’s 32-byte address over the message bytes. The program does the policy — it decides which account must be a signer for a given action (is this the account’s owner? the mint authority? the vault’s admin?) and rejects the instruction if the required signer flag is not set. Miss that policy check and you get the classic Solana bug class: a program that mutates an account without ever requiring the right signer, letting anyone act as anyone. (This is the same “validate every account you’re handed” discipline that the account model demands, and it is why Anchor turns Signer<'info> and #[account(mut, has_one = authority)] into declarative constraints — so the easy-to-forget check becomes hard to skip.)

  • Why does it exist? To let one party prove they authorized an exact message, to any number of untrusting parties, without ever revealing the secret that proves it. A blockchain has no login server; the signature is the authorization.
  • What problem does it solve? Agreement without coordination. Thousands of validators must independently reach the same verdict on who authorized what — signatures make that verdict a local, secret-free computation over public inputs (the address and the message).
  • What are the trade-offs? You must guard the private key absolutely — there is no “reset password,” and a leaked key is a total, irreversible loss of the identity. Ed25519 buys back much of the risk by removing the RNG from signing, but the key-custody problem is permanent.
  • When should I avoid it? Never, for transaction authorization on Solana — it is the gate. But do not reach for raw signature checks inside a program when the runtime’s is_signer flag and Anchor’s Signer constraint already give you the verified result; re-implementing verification is where bugs live.
  • What breaks if I remove it? Everything. With no signature check, any account could spend any other account’s lamports and invoke any authority, because nothing would tie an action to the key that owns it. The global state machine would have no notion of permission at all.
  1. A signature is produced from two inputs and checked with a different set of inputs. Name each set, and explain why the signer and the verifier use different halves of the keypair.
  2. State the three guarantees a verifying signature provides, and for each, name one concrete attack it prevents.
  3. Ed25519 derives its per-signature nonce deterministically from the key and message. What catastrophic failure does this design eliminate, and why can’t that failure occur when the nonce is a function of the message?
  4. Walk through what the Solana runtime does with a transaction’s signatures before any program runs, and explain what “this account is a signer” means to a program that reads the flag.
  5. Why does the fact that verification “needs no secret and no registry” matter for a chain of thousands of untrusting validators? Tie it back to the book’s throughline.
Show answers
  1. Signing takes the private key and the message; verifying takes the public key (the address), the message, and the signature. They use different halves because the whole point is asymmetry: only the private-key holder can produce a valid signature, but anyone with the already-public address can check it — so authorization stays secret while verification stays universal.
  2. Authenticity (only the key-holder could make it) prevents forgery/impersonation. Integrity (bound to the exact message bytes) prevents tampering — editing the amount or recipient invalidates the signature. Non-repudiation (verifiable by anyone with just the public key) prevents the signer from later denying it, or from showing different verdicts to different parties.
  3. It eliminates the nonce-reuse key-leak: in schemes with a random nonce, a predictable or reused nonce lets an attacker recover the private key from the signatures. It can’t happen in Ed25519 because the nonce is hash(secret, message) — a different message always yields a different nonce, so there is no way to accidentally reuse one across two distinct messages, and signing needs no runtime RNG at all.
  4. The runtime checks that for every account marked as a required signer, a signature in the transaction verifies against that account’s address over the message bytes; if any required signature is missing or fails, the whole transaction is rejected and never executes. To a program, “this account is a signer” (its is_signer flag) is the runtime’s already-verified guarantee that the owner of that account’s key approved this exact message — so the program can treat it as authorization without doing any cryptography itself.
  5. Because verification is a pure computation over public inputs (the address and the message), every validator can independently confirm a transaction is authorized without consulting each other or any central authority. That is what removes the coordinator from the permission check — the throughline of the book: a single global state machine can run at hardware speed precisely because the “is this allowed?” gate is local, stateless, and needs no shared state to evaluate.

→ Next: Program Derived Addresses · Prev: Addresses ARE Public Keys · Back to the Cryptography & Keys overview