DEV Community

Siddhant Chavan
Siddhant Chavan

Posted on

The Bug I Never Wrote: What Testing Failure Taught Me About Solana

100 Days of Solana, Day 100

Where I started

I'd built REST APIs for years but had never touched a blockchain, or written a line of Rust. The curiosity how blockchain works, started my curiosity.

What I expected

I came in with a Web2 instinct: tests exist to prove your code does what it's supposed to do. Write the function, write a test that calls it, watch it pass, move on. A "failing test" was something you fixed, not something you shipped on purpose.

What changed my understanding

The moment this cracked open was building the capstone: a small Anchor program called proof-of-ship that lets a wallet permanently record, on chain, that it shipped something. The rule is simple — one ship record per wallet, forever. The rule lives entirely in the account's seeds:

seeds = [b"ship", builder.key().as_ref()],
bump
Enter fullscreen mode Exit fullscreen mode

Each wallet's record lives at one deterministic address. Try to create a second one, and init refuses, because an account already exists there.

I wrote two tests. The first proved the happy path: call ship(), fetch the record, confirm the name and builder match.

The second test is the one that changed how I think about testing:

it("only lets each wallet ship once", async () => {
  let rejected = false;
  try {
    await program.methods.ship("Second try", "This should never land").rpc();
  } catch (_err) {
    rejected = true;
  }
  assert.isTrue(rejected, "second ship should have been rejected");
});
Enter fullscreen mode Exit fullscreen mode

This test isn't checking for a bug. It's checking that a rule holds. There's no function in my program called preventDuplicateShip(). There's no if statement rejecting the second attempt. The rule "one ship per wallet" isn't enforced by logic I wrote — it's enforced by the Solana runtime itself, because the PDA's address already has data in it. My job wasn't to write the rejection. My job was to prove the rejection actually happens.

What I understand now

On Web2 systems I controlled the whole stack, so "does it work" mostly meant "does the happy path return the right JSON." On Solana, the program shares an environment with every other program and every other wallet, and the account model itself becomes part of your security boundary. A constraint you wrote, a require! check, an init on a PDA with specific seeds — none of these mean anything until you've watched them reject bad input in a test, on purpose, and watched that rejection come from the runtime rather than from your own code's if statement.

The happy-path test proves your program can do the right thing. The failure-path test proves it can't do the wrong thing — and on a public, permissionless network, that second guarantee is the one that actually matters when a stranger's transaction lands on your program at 3am.

How it appeared in my capstone

proof-of-ship is intentionally small: one instruction, one PDA account, one constraint on input length, one happy-path test, one failure-path test, deployed to devnet. [Add your program ID / Explorer link here.] [Add your GitHub repo link here.]

I connected it to [the Arc 13 frontend / your wallet-adapter React app], so any wallet can connect, ship once, and see its on-chain message rendered back — which meant testing the "only once" rule wasn't just an assertion in a test file, it was something I could watch fail in the browser too, with a real wallet, a real rejected transaction, and a real error toast.

What I would tell another developer

Write the test for what your program refuses to do before you're confident you need it. If you can't write a failing-path test for a rule, you probably haven't actually built the rule — you've just described it in a comment. [Add a specific "I wish I'd known this on Day X" note here.]

*Tags: #solana, #webdev, #100daysofsolana *

Top comments (0)