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

推荐订阅源

U
Unit 42
Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
I
InfoQ
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
量子位
博客园 - 叶小钗
月光博客
月光博客
IT之家
IT之家
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享

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
How I built a counter program in Anchor and learned to tr...
Prasiddh Naik · 2026-06-23 · via DEV Community

Prasiddh Naik

The counter program is small, but the useful part was not the counter. The useful part was learning where state lives, how Anchor turns account validation into Rust types, and how a test suite can prove that a permission check is actually doing work.

Here is the first piece that made Anchor feel different from a normal Web2 backend:

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = authority, space = 8 + Counter::INIT_SPACE)]
    pub counter: Account<'info, Counter>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

In a Web2 handler, I am used to receiving a request body and then deciding what database rows to read or write. In Anchor, the accounts are part of the request. This struct says the counter account should be created, the authority signer pays for that account, and the System Program is available because account creation needs it. The space value reserves enough bytes for Anchor's discriminator plus the fields in my Counter account.

The account itself is tiny: it stores the wallet allowed to update the counter and the current number.

The initialize handler is short because the account plumbing already happened before the body runs:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.authority = ctx.accounts.authority.key();
    counter.count = 0;
    Ok(())
}

ctx.accounts gives me typed access to the accounts from the Initialize struct. At this point, Anchor has already checked that authority signed, that the counter account can be initialized, and that the right programs are present. My code only has to write the starting state.

The second instruction increments the counter. This is the important part:

pub struct Increment<'info> {
    #[account(mut, has_one = authority)]
    pub counter: Account<'info, Counter>,
    pub authority: Signer<'info>,
}

The key line is has_one = authority. It tells Anchor to compare the authority field stored inside the Counter account with the signer passed into this instruction. If they do not match, the handler body never gets to run.

That was the permission check. The next question was whether my tests could prove it.

The Happy Path

The first test creates a fresh LiteSVM instance, initializes a counter, increments it, then reads the account back. The whole point is this final check:

assert_eq!(parsed.count, 1);
assert_eq!(parsed.authority, authority.pubkey());

This test would fail if initialize stored the wrong authority, if increment changed the wrong amount, or if the account could not be deserialized back into Counter state.

But happy paths are not enough.

If I only test that the right wallet can increment, I have not proved that the wrong wallet cannot. That is the Solana version of testing that an admin endpoint works but never testing that a non-admin gets rejected.

The Failure Path

So I added a test where authority_a creates the counter and authority_b tries to increment it. The important assertion is:

assert!(
    result.is_err(),
    "increment should fail when signed by the wrong authority"
);

This test would fail if I accidentally removed the has_one = authority constraint or otherwise allowed any signer to increment someone else's counter.

That sentence is the whole reason the test exists.

Breaking It On Purpose

The most useful thing I did was break the program intentionally.

For one experiment, I changed the increment logic from "add one" to "add two":

.checked_add(2)

The transaction still succeeded, but the test caught the wrong state:

thread 'initialize_then_increment' panicked at programs/counter/tests/counter.rs:86:5:
assertion `left == right` failed
  left: 2
 right: 1

That is the kind of failure I want. It is clear, specific, and tied directly to the behavior I care about.

I also removed has_one = authority once. Before the failure test existed, the suite stayed green. After the failure test existed, the suite failed exactly where it should:

increment should fail when signed by the wrong authority

That was the moment the permission check stopped being a line I trusted and became a line I could audit by running tests.

What I Learned

Anchor programs are small on the surface, but a lot happens before a handler body runs. The account structs are not just TypeScript-style types or documentation. They are validation rules. They decide which accounts are writable, which accounts must sign, which accounts get initialized, and which relationships must already be true.

I also learned that a green test suite only means something if it tries to prove the program wrong. The happy path proved the counter could work. The failure path proved the authority gate was real. The mutation experiments proved both assertions were load-bearing.

If I had another week, I would add one more failure test for double initialization, tighten the failure assertions to check the exact Anchor error, and then move the counter account to a PDA so the client does not have to create a random keypair for it.

Small program. Real lesson.