Expert · LESSON 02 OF 06
2. Authority, signatures, and replay resistance
By the end, you can design a solana instruction authorization that binds the intended action and cannot be replayed.
Read the workflow
A signed action is checked against the supplied account permissions and freshness rules before business logic runs. A status cache can reject stale or repeated message identifiers, while a claim receipt prevents the same eligibility from being consumed twice. The protected action and receipt update should commit atomically or both roll back on failure.
- SIGNED ACTION + ACCOUNT METAS
- MESSAGE AGE + STATUS CACHE
- CLAIM + RECEIPT CHECK
- ATOMIC ACTION OR REJECT
FIELD NOTE 01
Separate identity from permission
A public key identifies an account; it does not grant every program permission to act for that account. An instruction should name the signer whose approval is required and the exact state transition that signer authorizes. A connected wallet, a copied address, or a signature from a different message is not equivalent to transaction approval. Start by listing the actor, protected asset, operation, and authority for each protocol action. If the expected signer is a PDA, explain which program derives and signs for it.
FIELD NOTE 02
Validate the complete instruction context
A program receives an instruction plus account references supplied by the caller. Verify required signatures and writable permissions, then check each account’s owner, address, data type, and relationship to the other accounts. For example, a token withdrawal should validate the vault, mint, destination, and authority together. A correct signature over the wrong destination is still the wrong action. Derive canonical PDAs from a documented seed schema and reject caller-provided alternatives that do not match.
FIELD NOTE 03
Bind signed messages to one purpose
When a protocol accepts an off-chain authorization, include an unambiguous domain, intended program or application, cluster, action, source state, recipient, amount or level, expiry, and unique identifier. Use a canonical byte representation and a versioned schema so fields cannot be reordered or interpreted differently. Domain separation helps prevent a message created for one application or environment from being accepted by another. Never sign a vague statement such as “approved” without defining what is approved and by whom.
FIELD NOTE 04
Make one-time use durable
An expiry limits how long a message remains valid but does not make it single-use. A caller can replay an unexpired message unless the system records consumption. Store a nonce, sequence number, or deterministic receipt account and change it atomically with the authorized action. A PDA can provide a canonical address for a claim or receipt, but address derivation alone does not enforce policy: the program must check the expected seeds, signer, stored state, and transition. Define whether a nonce is per wallet, per action, or global.
FIELD NOTE 05
Understand transaction freshness and application replay
The validator checks transaction age and a recent status cache that rejects a message hash it has already processed. A recent blockhash limits how long that exact message remains valid; a durable nonce provides a different lifetime mechanism. Current Solana docs warn durable nonces may be deprecated, so recheck their support before relying on them. This transaction layer is not a business receipt. Rebuilding the same voucher action with a new blockhash changes the signed transaction message, so transaction-level duplicate detection cannot establish that the voucher was already consumed. The program needs durable one-time-use state and must update that state atomically with the protected action. If a wallet prompt or RPC response times out, inspect the original signature and account state before preparing a replacement.
FIELD NOTE 06
Handle delegated and composed authority
Signer and writable privileges passed to a CPI extend to the callee, but the callee cannot escalate beyond the privileges it received. With `invoke_signed`, the caller adds only a PDA derived under its own program ID to the signer set. Verify the callee program address and constrain every account forwarded to it; an arbitrary executable account must not decide where protected assets go. If a protocol accepts an Ed25519 signature-verification instruction, bind the exact verified public key and message bytes to the action and accounts being executed. The mere presence of a successful verification instruction does not authorize arbitrary state changes.
FIELD NOTE 07
Test mutations at the enforcement layer
Build positive and negative tests from the authorization schema. Change the signer, program domain, cluster, amount, recipient, nonce, expiry, and account order one at a time. Attempt duplicate use, a wrong PDA bump, and an unintended CPI target. For every rejection, assert the protected state remains unchanged and the nonce is not consumed incorrectly. A server-side unit test can validate service policy, but only an execution test at the program boundary shows that an untrusted caller cannot bypass the on-chain check.
MAKE THE CONNECTION
A detail worth keeping
Treat replay as two separate questions. First, has this exact transaction message already been processed or expired? Second, has this economic authorization already been consumed by any transaction? A status cache answers only the first question for its recent window. The program must answer the second with a receipt PDA, monotonic sequence, or another durable uniqueness rule scoped to the issuer, action, and protected object.
Work through an example
In the excerpt, `init` gives a claim one canonical receipt address, while the escrow constraint binds the claim ID to stored state. The handler still verifies the signed claim and expiry, transfers only to the stored beneficiary, and writes the receipt in the same atomic instruction. If the transfer fails, receipt creation rolls back too. Do not close or reset a receipt in a way that makes the same claim ID usable again; test this across retries and after any account cleanup.
#[derive(Accounts)]
#[instruction(authorization_id: [u8; 32])]
pub struct Release<'info> {
#[account(
mut,
has_one = beneficiary,
constraint = escrow.authorization_id == authorization_id
)]
pub escrow: Account<'info, Escrow>,
#[account(
init,
payer = beneficiary,
space = 8 + ReleaseReceipt::INIT_SPACE,
seeds = [b"release", escrow.key().as_ref(), authorization_id.as_ref()],
bump
)]
pub receipt: Account<'info, ReleaseReceipt>,
#[account(mut)]
pub beneficiary: Signer<'info>,
pub system_program: Program<'info, System>,
}Design a signed release authorization for a fictional token escrow. Specify its canonical schema and byte encoding, trusted signer, domain and program, escrow, mint, recipient, amount, expiry, and unique claim ID. Show how one program instruction validates and consumes the claim while moving the tokens. Then distinguish replaying the identical transaction from resubmitting the same claim in a transaction with a new blockhash.
GUIDED PRACTICE · EXPERT
Do the work, one step at a time
Use fictional addresses and disposable test data. Do not enter a recovery phrase or private key. Unless a lesson explicitly says “read-only Devnet,” use paper or a local test and do not connect to a public network.
- Write the release invariant and draw its trust boundary: voucher issuer, beneficiary, escrow program, Token Program, and every account the caller can substitute.
- Specify an unambiguous, versioned authorization schema. Include domain, program, network, action, escrow, mint, recipient, base-unit amount, expiry, and a unique claim ID; define the exact signed bytes.
- Use the receipt-PDA excerpt to define one-time consumption. State how the program validates issuer signature, escrow terms, canonical PDA, beneficiary, and expiry in the same instruction as the protected token CPI.
- Test wrong issuer, domain, program, recipient, mint, amount, expired voucher, wrong receipt PDA, and duplicate claim. For each rejection, assert the escrow, token balances, and receipt state are unchanged.
- Inspect the downloadable escrow lab’s persistent maker counter and replay-after-close tests. Explain what unique escrow IDs protect against and why they do not replace a receipt for a separate signed voucher.
Before you move on, check your reasoning
- Define one sensitive action and its authority.
- Specify canonical fields and the exact signed or on-chain context.
- Choose durable one-time-use state and explain its uniqueness rule.
OPEN SELF-CHECK · UNGRADED
Pause and test your reasoning
A signed eligibility message expires, but a client later signs a fresh message for the same benefit. What prevents a second claim?
- Expiry alone; once the first message expires, the benefit is permanently consumed.
- A durable application record that atomically marks the business claim as used, in addition to signature and expiry checks.
- The transaction signature cache, even when the new transaction has a different blockhash and signature.
Reveal one best answer and its explanation
One best answer: A durable application record that atomically marks the business claim as used, in addition to signature and expiry checks.
Signature verification proves control over exact message bytes and expiry bounds freshness, but neither enforces single use. Persist and atomically consume a unique claim identifier so a newly signed transaction cannot repeat the same business action.
Sources: Durable Nonces ↗ · Transaction Pipeline ↗
This practice is not scored, saved, or part of a certificate. Account-based lesson checkpoints and level exams remain separate.
Scored checkpoints and level exams are online and require a free account. They are not included in this pack.
Optional lab files
These files are included with the pack. Follow the safety and toolchain notes in the lesson before running a lab; a Devnet read requires an internet connection.
READ THE SOURCE
Go deeper in the official docs
Reviewed on 2026-09-29. Protocol documentation can change; check the linked page’s current version when you reconnect.