← Solana Architect

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.

In this lesson7 field notes · practice · sources · checkpoint
A signed action is checked for account permissions and freshness, then a claim receipt prevents replay before execution or rejection.
A signed action is checked for account permissions and freshness, then a claim receipt prevents replay before execution or rejection.Open full-size visual ↗
Read a text version of this diagram

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.

  1. SIGNED ACTION + ACCOUNT METAS
  2. MESSAGE AGE + STATUS CACHE
  3. CLAIM + RECEIPT CHECK
  4. ATOMIC ACTION OR REJECT
Two replay defenses: Solana checks transaction-message freshness and recent duplicates; the application separately validates and atomically consumes a business claim using durable receipt state.
A fresh blockhash changes the transaction message. Only durable application state can prove that a business claim was already consumed.Open full-size visual ↗
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.

Illustrative Anchor account-validation excerpt. The handler must also verify expiry and claim authenticity, perform the protected CPI, and populate the receipt in this same instruction. Account types and error definitions are omitted.
#[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. Most exercises work on paper or with a local test; only a lesson that clearly says “read-only Devnet” makes a public network request.

  1. Write the release invariant and draw its trust boundary: voucher issuer, beneficiary, escrow program, Token Program, and every account the caller can substitute.
  2. 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.
  3. 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.
  4. 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.
  5. 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?

  1. Expiry alone; once the first message expires, the benefit is permanently consumed.
  2. A durable application record that atomically marks the business claim as used, in addition to signature and expiry checks.
  3. 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.

Never enter a recovery phrase or private key into a learning site or lesson exercise.

DOWNLOADABLE LOCAL PROGRAM CASE STUDY

Trace replay resistance in the escrow lab.

The tested Intermediate Anchor lab keeps a maker-owned monotonic counter after escrow cleanup and checks replay after close. Inspect its SBF tests, then compare address-reuse protection with a receipt that consumes a signed business claim. These solve related but different uniqueness problems.

Download Token Escrow Lab 01

Run locally in WSL Ubuntu with the toolchain described in the archive. The teaching lab does not connect to a cluster, use a wallet file, or spend real SOL; it is not an audited production protocol.

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 as you use this material.

  1. Transactions ↗
  2. Partial Signing ↗
  3. Transaction Pipeline ↗
  4. Program Derived Addresses (PDAs) ↗
  5. Cross Program Invocation ↗
  6. Program Derived Address with Anchor ↗
  7. Account Constraints ↗
  8. Sealevel Attacks ↗
  9. Durable Nonces ↗

Report an issue with this lesson

OPEN LEARNING

Keep learning at your own pace.

This lesson, its guided practice, and all six course lessons are open to everyone. Account-based checkpoints, exams, and saved progress will open when free accounts are ready.

Continue to the next lesson ↗