I spent Days 57–61 building the same counter program over and over.
Not because I kept breaking it (well, I did), but because each day
revealed something about Anchor that the previous day's green tests
had been quietly hiding.
This is that story.
The Accounts Struct — Where Anchor Earns Its Name
Every Anchor instruction starts with an accounts struct. Here's mine
for Initialize:
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = authority, space = 8 + 40)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
Three fields, three jobs:
-
counter— the new on-chain account being created. Anchor'sinitconstraint handles thecreate_accountCPI to the System Program automatically.space = 8 + 40is the discriminator (8 bytes) plus the actual struct data (40 bytes). -
authority— the wallet paying for the account and signing the transaction.mutis required because its lamport balance will decrease. -
system_program— Solana requires you to pass the System Program explicitly whenever you're creating accounts. Nothing happens if it's missing — the transaction just fails.
Coming from backend development, this was the first thing that
genuinely surprised me. In Web2 you pass data to a function. In
Anchor you first declare every account your instruction will touch,
with its permissions, and Anchor verifies all of it before your
handler code runs even one line.
The Handlers — Short by Design
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
counter.authority = ctx.accounts.authority.key();
Ok(())
}
ctx.accounts gives you typed, validated access to every account
declared in the struct above. The handler is three lines because
Anchor already did the hard work — account creation, lamport
deduction, ownership checks — before this function was called.
Now increment:
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count.checked_add(1)
.ok_or(ErrorCode::Overflow)?;
Ok(())
}
And its accounts struct:
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
has_one = authority is the constraint that does the real work.
Before increment runs, Anchor checks that counter.authority ==. If a different wallet signs, the transaction
authority.key()
reverts with ConstraintHasOne (Error 2001) before a single line
of my handler executes. The constraint is a pre-condition, not a
runtime check I have to write myself.
The Tests — Two Paths That Matter
Happy path:
#[test]
fn initialize_then_increment() {
// ... setup ...
let res = svm.send_transaction(increment_tx);
assert!(res.is_ok());
let counter: Counter = svm.get_account_data(&counter_kp.pubkey());
assert_eq!(counter.count, 1);
}
What would have to go wrong for this to fail? The increment
handler would have to not add 1 — either wrong arithmetic, a
missing account write-back, or a serialization error.
Failure path — wrong authority:
#[test]
fn increment_fails_when_wrong_authority_signs() {
// wrong_wallet is a fresh keypair, not the counter's authority
let res = svm.send_transaction(bad_tx);
assert!(res.is_err(), "increment should fail when signed by wrong authority");
}
What would have to go wrong for this to fail? The has_one
constraint would have to be missing or wrong. Without it, any
wallet could increment anyone's counter — which is exactly the
exploit this test exists to prevent.
Day 61 — Mutation Testing: Proving the Tests Are Real
Green tests don't prove your code is correct. They prove your code
satisfies your tests. Those are different things if your tests are
wrong.
So on Day 61 I intentionally broke the program three ways and
watched the test suite respond.
Bug 2: I changed checked_add(1) to checked_add(2)
// Before
counter.count = counter.count.checked_add(1).ok_or(ErrorCode::Overflow)?;
// After (intentional bug)
counter.count = counter.count.checked_add(2).ok_or(ErrorCode::Overflow)?;
Test output:
thread 'initialize_then_increment' panicked at programs/counter/tests/counter.rs:97:5:
assertion left == right failed
left: 2
right: 1
The test caught it immediately. The assertion assert_eq!(counter.count, 1) exists precisely because of off-by-one errors like this. Before this experiment I had pattern-matched that assertion from a tutorial. After the experiment I understood why it has to be exactly 1 and not "nonzero" or "greater than 0."
I ran git restore and all three tests went green again.
What Writing This Revealed
The has_one constraint took me three drafts to explain cleanly.
I had been thinking of it as "check the authority" — vague enough
that I couldn't have told you exactly when it runs or what it
compares. Writing forced precision: it compares counter.authority
(stored on-chain at init time) against the authority account
passed in at increment time, and it runs before the handler, not
inside it.
That distinction — constraint vs. runtime check — is the thing
I would have gotten wrong in a code review before this week.
What's Next
Week 10 will push past single-account programs. I want to build
something with multiple accounts interacting — a vault, an escrow,
or a simple DEX instruction — where the account constraints do
more than guard a single field. That's where I expect the next
gap to show up.
Day 62 of 100. Building daily.
Top comments (0)