惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
T
Troy Hunt's Blog
The Last Watchdog
The Last Watchdog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Secure Thoughts
Hacker News - Newest:
Hacker News - Newest: "LLM"
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
TaoSecurity Blog
TaoSecurity Blog
博客园 - Franky
有赞技术团队
有赞技术团队
Google Online Security Blog
Google Online Security Blog
罗磊的独立博客
L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
NISL@THU
NISL@THU
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Commits to openclaw:main
Recent Commits to openclaw:main
K
Kaspersky official blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
W
WeLiveSecurity
SecWiki News
SecWiki News
Google DeepMind News
Google DeepMind News
O
OpenAI News
V
V2EX
AI
AI
博客园_首页
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
美团技术团队
C
Cyber Attacks, Cyber Crime and Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 三生石上(FineUI控件)
G
GRAHAM CLULEY
月光博客
月光博客
T
Threatpost
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Vulnerabilities – Threatpost
Last Week in AI
Last Week in AI
爱范儿
爱范儿
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cloudbric
Cloudbric
酷 壳 – CoolShell
酷 壳 – CoolShell
The Hacker News
The Hacker News
N
News | PayPal Newsroom
Know Your Adversary
Know Your Adversary

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
What I Learned About PDAs in a Week of Building on Solana
Lymah · 2026-06-28 · via DEV Community

I spent a week building a counter program in Anchor. By the end, every
user had their own account, a global config singleton controlled whether
new counters could be created, and a close instruction returned the rent
deposit back to the wallet. None of that would have been possible without
Program Derived Addresses.

This post is what I wish someone had handed me on day one.


The Problem PDAs Solve

On Solana, programs are stateless. If your program needs to remember
something, a user's score, a vault balance, a config flag, it needs
a dedicated account to store it in. And that account needs an address
your program can find again later, without the client having to pass
around a randomly generated keypair.

PDAs are that address. Deterministic. Derivable from inputs your
program already knows. No private key required.


The Mental Model

Here is the Web2 analogy that helped me most:

A PDA is like a database primary key you can compute from the
row's logical identity.

In Postgres, if you want one row per user, you use user_id as the
primary key. You never store the key separately — you derive it from
who is asking. PDAs work the same way. You feed in a list of seeds
(bytes that describe the identity of the thing you want to store) plus
your program's ID, and you get back a deterministic address.

Every time you run the derivation with the same inputs, you get the
same address. That is the whole point.

Where the analogy breaks: the address may or may not have an account
at it yet. Deriving a PDA does not create anything on the chain. It just
computes a public key. The account gets created when your program runs
an instruction with the init constraint, and that creation costs
rent, paid by whoever is signing the transaction.

One more thing the analogy misses: the program ID is baked into the
derivation. The same seeds run through a different program produce a
completely different address. This means only your program can
predictably derive and sign for its own PDAs. No other program can
guess them or collide with them accidentally.


Anatomy of a Derivation

Here is the accounts struct from my counter program, exactly as it
appears in lib.rs:

#[derive(Accounts)]
pub struct InitCounter {
    #[account(
        init,
        payer = user,
        space = 8 + Counter::INIT_SPACE,
        seeds = [b"counter", user.key().as_ref()],
        bump
    )]
    pub counter: Account,
    #[account(mut)]
    pub user: Signer,
    pub system_program: Program,
}

Let me walk through every piece.

seeds = [b"counter", user.key().as_ref()]

The seed array is the input to the hash. It has two parts here:

  • b"counter" — a static byte string that acts as a namespace. It ensures this address is clearly "a counter account," not something else.
  • user.key().as_ref() — the signer's public key as bytes. This is what makes the address unique per user.

Under the hood, Anchor runs sha256(seeds + program_id + bump)
and checks whether the result lands on the ed25519 elliptic curve.
If it does, it decrements the bump and tries again. The first result
that lands off the curve is returned as the canonical PDA. Off-curve
means no private key can produce it, which means only your program
can sign for it.

bump

The bump is a single byte (0–255) appended to the hash input.
PublicKey.findProgramAddressSync starts at 255 and counts down
until it finds an off-curve result. The first bump that works is
the canonical bump. Anchor computes it for you and stores it in
ctx.bumps.counter.

init

This constraint tells Anchor to create the account at the derived
address if it doesn't already exist. It makes a CPI (cross-program
invocation) to the System Program, which allocates the bytes and
transfers the rent-exempt deposit from the payer.

space = 8 + Counter::INIT_SPACE

The 8 bytes are Anchor's discriminator — a prefix stamped on every
account so the program can later verify "yes, this is a Counter
account, not something else." Counter::INIT_SPACE is computed
automatically by the #[derive(InitSpace)] macro.


Why the Seeds Matter

The seeds are your access control policy.

My counter program uses [b"counter", user.key().as_ref()]. That
means every signer gets their own PDA. Alice's counter and Bob's
counter live at completely different addresses, derived independently.

// Alice's counter
const [alicePda] = PublicKey.findProgramAddressSync(
  [Buffer.from("counter"), alice.toBuffer()],
  programId
);

// Bob's counter
const [bobPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("counter"), bob.toBuffer()],
  programId
);

console.log(alicePda.equals(bobPda)); // false — always

Compare that to the config singleton in my program, which uses
[b"config"] — no wallet in the seeds:

// Same address for every caller
const [configFromAlice] = PublicKey.findProgramAddressSync(
  [Buffer.from("config")], programId
);
const [configFromBob] = PublicKey.findProgramAddressSync(
  [Buffer.from("config")], programId
);

console.log(configFromAlice.equals(configFromBob)); // true — always

One address. Every caller computes the same thing. That's exactly what
you want for a global config — one row at the top of the database that
everyone reads from.

Use the wrong pattern and you get disasters. If I had written my
counter as [b"counter"], the first user to call init_counter
would own the only counter that can ever exist. Everyone else would
get "account already in use."

One byte changes everything:

["counter",  walletA] → E4C9RStxbiweqVv7rHM88HgzWNhcCibTHWtGn7HrowjM

["counters", walletA] → AoaX8ntGs9tj7b96pbMcPHD822X8tEqRAg7p48VgdcmZ

["counter\0",walletA] → Es37q2wXjeYPkGhfcn4qnB3Uu167VmbtKUVaQ8i87LTm

["Counter",  walletA] → 2WSX2ScZwFgDDYZWkspqrCHTWnSsGoXquH425a2zgsWy

Same program, same wallet, four completely different addresses.
There is no "close enough" in PDA derivation.


What the Bump Buys You

The bump is not magic. It is just the first byte value (starting from
255, counting down) that pushed the hash result off the ed25519 curve.
Off-curve means no private key exists at that point. No private key
means no human can sign for it. Only your program can — by providing
the same seeds plus bump to the runtime when it needs to authorize
something.

Always store the bump on the account:

#[account]
#[derive(InitSpace)]
pub struct Counter {
    pub user: Pubkey,
    pub count: u64,
    pub bump: u8,   // ← store this
}

pub fn init_counter(ctx: Context<InitCounter>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.bump = ctx.bumps.counter;  // ← Anchor gives it to you here
    Ok(())
}

Then on subsequent instructions, pass it back instead of re-deriving:

#[account(
    mut,
    seeds = [b"counter", user.key().as_ref()],
    bump = counter.bump,   // ← use the stored bump, not bump alone
)]
pub counter: Account<'info, Counter>,

Re-deriving the bump on every call is unnecessary compute. Storing
it once and reusing it is free. Anchor's bump constraint (without
= counter.bump) re-derives every time. bump = counter.bump
uses the stored value. Use the stored value.


The Full Lifecycle

My week in four steps:

1. Derive the address (off-chain)

const [counterPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("counter"), wallet.toBuffer()],
  programId
);
// counterPda is an address. Nothing on the chain yet.

2. Initialize the account (init instruction)

The init constraint on the Anchor accounts struct creates the
account at the derived address, allocates the right number of bytes,
and funds the rent-exempt deposit from the payer. After this
transaction, the account exists on the chain with your program as its
owner.

3. Mutate data (subsequent instructions)

Every later instruction re-derives the PDA from the seeds and
validates that it matches the account passed in. The seeds and bump
Constraints do this automatically. Your handler just reads and writes
the fields.

4. Close the account (close instruction)

Closing a PDA is not like dropping a table row. It is:

  1. Transfer all lamports to the destination wallet
  2. Zero out the account data
  3. The runtime removes the account from state at the end of the transaction (a zero-lamport account cannot persist)

In Anchor, one constraint does all of this:

#[account(
    mut,
    close = user,    // ← drains lamports to user, zeros data
    seeds = [b"counter", user.key().as_ref()],
    bump = counter.bump,
    has_one = user,
)]
pub counter: Account<'info, Counter>,

The rent deposit comes back to the wallet. The account disappears.
getAccountInfo returns null on the next call.


What I Would Tell Past Me

The program ID is part of the derivation. The same seeds in a
different program produce a completely different address. This is a
feature — your PDAs belong to your program and no other.

PDAs cannot sign transactions themselves. Only programs can sign
on a PDA's behalf, by providing the seeds and bump to the runtime as
"signer seeds." This is how your program authorizes CPIs from a PDA
it owns.

init_if_needed is a footgun. It initializes the account if it
doesn't exist, and silently does nothing if it does. That sounds
convenient until a caller reuses it to overwrite an account they
don't own. Use init when you mean "create exactly once."

The seeds enforce access control, not just naming. When Anchor
validates a PDA constraint, it re-derives the expected address from
the seeds and compares it to what you passed in. If someone passes
the wrong account, the derivation produces a different address, and
the transaction is rejected before your handler runs. You do not need
an explicit ownership check — the seeds are the ownership check.

Design your seed scheme before you write any code. You cannot
change the seeds after deployment. A counter keyed by
[b"counter", user.key()] and one keyed by
[b"user_counter", user.key()] are in completely different address
spaces. Pick once, keep forever.


Resources

The full counter program from this arc is available in my
100 Days of Solana repo. If anything in this post is wrong,
leave a comment — I'm still learning.


This post is part of *#100DaysOfSolana*. Building on Solana
every day.