Skip to content

Commit 68fa7ea

Browse files
authored
Merge pull request #12 from mikemaccana-edwardbot/fix-code-inconsistencies-2026-04-22
Fix code inconsistencies across various programs
2 parents 1a8ca59 + 26e37b0 commit 68fa7ea

24 files changed

Lines changed: 767 additions & 161 deletions

File tree

basics/counter/native/program/src/lib.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,34 @@ use solana_program::entrypoint;
2020
entrypoint!(process_instruction);
2121

2222
pub fn process_instruction(
23-
_program_id: &Pubkey,
23+
program_id: &Pubkey,
2424
accounts: &[AccountInfo],
2525
instruction_data: &[u8],
2626
) -> ProgramResult {
27+
// Reject empty instruction data explicitly rather than panicking via split_at.
28+
if instruction_data.is_empty() {
29+
msg!("Error: instruction data is empty");
30+
return Err(ProgramError::InvalidInstructionData);
31+
}
2732
let (instruction_discriminant, instruction_data_inner) = instruction_data.split_at(1);
2833
match instruction_discriminant[0] {
2934
0 => {
3035
msg!("Instruction: Increment");
31-
process_increment_counter(accounts, instruction_data_inner)?;
36+
process_increment_counter(program_id, accounts, instruction_data_inner)?;
3237
}
3338
_ => {
34-
msg!("Error: unknown instruction")
39+
// Previously this branch logged a message and returned Ok(()), which let
40+
// unknown discriminants succeed silently. Return InvalidInstructionData
41+
// so callers get a real failure.
42+
msg!("Error: unknown instruction");
43+
return Err(ProgramError::InvalidInstructionData);
3544
}
3645
}
3746
Ok(())
3847
}
3948

4049
pub fn process_increment_counter(
50+
program_id: &Pubkey,
4151
accounts: &[AccountInfo],
4252
_instruction_data: &[u8],
4353
) -> Result<(), ProgramError> {
@@ -49,6 +59,13 @@ pub fn process_increment_counter(
4959
"Counter account must be writable"
5060
);
5161

62+
// Owner check: without this the program would happily decode any 8 bytes of
63+
// data as a Counter, even when the account belongs to a different program.
64+
if counter_account.owner != program_id {
65+
msg!("Error: counter account is not owned by this program");
66+
return Err(ProgramError::IncorrectProgramId);
67+
}
68+
5269
let mut counter = Counter::try_from_slice(&counter_account.try_borrow_mut_data()?)?;
5370
counter.count += 1;
5471
counter.serialize(&mut *counter_account.data.borrow_mut())?;

basics/counter/native/program/tests/test.rs

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,47 @@ use solana_rent::Rent;
99
use solana_system_interface::instruction::create_account;
1010
use solana_transaction::Transaction;
1111

12+
// The .so is built into ../../tests/fixtures by `pnpm build-and-test` (which runs
13+
// `cargo build-sbf --sbf-out-dir=./tests/fixtures` from the package root). Run
14+
// that script (or `cargo build-sbf` with --sbf-out-dir set accordingly) before
15+
// `cargo test`.
16+
const PROGRAM_SO: &[u8] = include_bytes!("../../tests/fixtures/counter_solana_native.so");
17+
18+
fn setup_with_counter() -> (LiteSVM, Pubkey, Keypair, Keypair) {
19+
let program_id = Pubkey::new_unique();
20+
21+
let mut svm = LiteSVM::new();
22+
svm.add_program(program_id, PROGRAM_SO).unwrap();
23+
24+
let payer = Keypair::new();
25+
let counter_account = Keypair::new();
26+
27+
svm.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL * 10).unwrap();
28+
29+
let counter_account_size = std::mem::size_of::<Counter>();
30+
let create_ix = create_account(
31+
&payer.pubkey(),
32+
&counter_account.pubkey(),
33+
Rent::default().minimum_balance(counter_account_size),
34+
counter_account_size as u64,
35+
&program_id,
36+
);
37+
let tx = Transaction::new_signed_with_payer(
38+
&[create_ix],
39+
Some(&payer.pubkey()),
40+
&[&payer, &counter_account],
41+
svm.latest_blockhash(),
42+
);
43+
svm.send_transaction(tx).unwrap();
44+
(svm, program_id, payer, counter_account)
45+
}
46+
1247
#[test]
1348
fn test_counter() {
1449
let program_id = Pubkey::new_unique();
15-
let program_bytes = include_bytes!("../../tests/fixtures/counter_solana_native.so");
1650

1751
let mut svm = LiteSVM::new();
18-
svm.add_program(program_id, program_bytes).unwrap();
52+
svm.add_program(program_id, PROGRAM_SO).unwrap();
1953

2054
let payer = Keypair::new();
2155
let counter_account = Keypair::new();
@@ -38,7 +72,7 @@ fn test_counter() {
3872
&[&payer, &counter_account],
3973
svm.latest_blockhash(),
4074
);
41-
assert!(svm.send_transaction(tx).is_ok());
75+
svm.send_transaction(tx).unwrap();
4276

4377
let ix = Instruction {
4478
program_id,
@@ -53,9 +87,79 @@ fn test_counter() {
5387
svm.latest_blockhash(),
5488
);
5589

56-
let _ = svm.send_transaction(tx).is_ok();
90+
// Actually assert the transaction succeeded - the previous shape used
91+
// `let _ = ...is_ok();` which discarded the result.
92+
svm.send_transaction(tx).unwrap();
5793

5894
let counter_account_data = svm.get_account(&counter_account.pubkey()).unwrap().data;
5995
let counter = Counter::try_from_slice(&counter_account_data).unwrap();
6096
assert_eq!(counter.count, 1);
6197
}
98+
99+
#[test]
100+
fn test_unknown_instruction_fails() {
101+
let (mut svm, program_id, payer, counter_account) = setup_with_counter();
102+
103+
// Discriminant 9 is not handled and must now return an error rather than Ok(()).
104+
let ix = Instruction {
105+
program_id,
106+
accounts: vec![AccountMeta::new(counter_account.pubkey(), false)],
107+
data: vec![9],
108+
};
109+
let tx = Transaction::new_signed_with_payer(
110+
&[ix],
111+
Some(&payer.pubkey()),
112+
&[&payer],
113+
svm.latest_blockhash(),
114+
);
115+
assert!(
116+
svm.send_transaction(tx).is_err(),
117+
"unknown instruction discriminant must fail"
118+
);
119+
}
120+
121+
#[test]
122+
fn test_wrong_owner_fails() {
123+
let program_id = Pubkey::new_unique();
124+
125+
let mut svm = LiteSVM::new();
126+
svm.add_program(program_id, PROGRAM_SO).unwrap();
127+
128+
let payer = Keypair::new();
129+
let counter_account = Keypair::new();
130+
svm.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL * 10).unwrap();
131+
132+
// Create the counter account but owned by a different (random) program.
133+
let counter_account_size = std::mem::size_of::<Counter>();
134+
let wrong_owner = Pubkey::new_unique();
135+
let create_ix = create_account(
136+
&payer.pubkey(),
137+
&counter_account.pubkey(),
138+
Rent::default().minimum_balance(counter_account_size),
139+
counter_account_size as u64,
140+
&wrong_owner,
141+
);
142+
let tx = Transaction::new_signed_with_payer(
143+
&[create_ix],
144+
Some(&payer.pubkey()),
145+
&[&payer, &counter_account],
146+
svm.latest_blockhash(),
147+
);
148+
svm.send_transaction(tx).unwrap();
149+
150+
let ix = Instruction {
151+
program_id,
152+
accounts: vec![AccountMeta::new(counter_account.pubkey(), false)],
153+
data: vec![0],
154+
};
155+
let tx = Transaction::new_signed_with_payer(
156+
&[ix],
157+
Some(&payer.pubkey()),
158+
&[&payer],
159+
svm.latest_blockhash(),
160+
);
161+
assert!(
162+
svm.send_transaction(tx).is_err(),
163+
"counter owned by a different program must be rejected"
164+
);
165+
}

basics/counter/pinocchio/program/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,22 @@ pub fn process_instruction(
2121
accounts: &[AccountView],
2222
instruction_data: &[u8],
2323
) -> ProgramResult {
24+
if instruction_data.is_empty() {
25+
log!("Error: instruction data is empty");
26+
return Err(ProgramError::InvalidInstructionData);
27+
}
2428
let (instruction_discriminant, instruction_data_inner) = instruction_data.split_at(1);
2529
match instruction_discriminant[0] {
2630
0 => {
2731
log!("Instruction: Increment");
2832
process_increment_counter(accounts, instruction_data_inner)?;
2933
}
3034
_ => {
35+
// Previously this branch logged a message and returned Ok(()), which let
36+
// unknown discriminants succeed silently. Return InvalidInstructionData
37+
// so callers get a real failure.
3138
log!("Error: unknown instruction");
39+
return Err(ProgramError::InvalidInstructionData);
3240
}
3341
}
3442
Ok(())

basics/counter/pinocchio/program/tests/test.rs

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ use solana_rent::Rent;
88
use solana_system_interface::instruction::create_account;
99
use solana_transaction::Transaction;
1010

11+
// The .so is built into ../../tests/fixtures by `pnpm build-and-test` (which runs
12+
// `cargo build-sbf --sbf-out-dir=./tests/fixtures` from the package root). Run
13+
// that script (or `cargo build-sbf` with --sbf-out-dir set accordingly) before
14+
// `cargo test`.
15+
const PROGRAM_SO: &[u8] = include_bytes!("../../tests/fixtures/counter_solana_pinocchio.so");
16+
1117
#[test]
1218
fn test_counter() {
1319
let program_id = Pubkey::new_unique();
14-
let program_bytes = include_bytes!("../../tests/fixtures/counter_solana_pinocchio.so");
1520

1621
let mut svm = LiteSVM::new();
17-
svm.add_program(program_id, program_bytes).unwrap();
22+
svm.add_program(program_id, PROGRAM_SO).unwrap();
1823

1924
let payer = Keypair::new();
2025
let counter_account = Keypair::new();
@@ -38,8 +43,7 @@ fn test_counter() {
3843
svm.latest_blockhash(),
3944
);
4045

41-
let res = svm.send_transaction(tx);
42-
assert!(res.is_ok());
46+
svm.send_transaction(tx).unwrap();
4347

4448
let ix = Instruction {
4549
program_id,
@@ -54,11 +58,55 @@ fn test_counter() {
5458
svm.latest_blockhash(),
5559
);
5660

57-
let res = svm.send_transaction(tx);
58-
assert!(res.is_ok());
61+
svm.send_transaction(tx).unwrap();
5962

6063
let counter_account_data = svm.get_account(&counter_account.pubkey()).unwrap().data;
6164
let counter_bytes: [u8; 8] = counter_account_data[0..8].try_into().unwrap();
6265
let count = u64::from_le_bytes(counter_bytes);
6366
assert_eq!(count, 1);
6467
}
68+
69+
#[test]
70+
fn test_unknown_instruction_fails() {
71+
let program_id = Pubkey::new_unique();
72+
73+
let mut svm = LiteSVM::new();
74+
svm.add_program(program_id, PROGRAM_SO).unwrap();
75+
76+
let payer = Keypair::new();
77+
let counter_account = Keypair::new();
78+
svm.airdrop(&payer.pubkey(), LAMPORTS_PER_SOL * 10).unwrap();
79+
80+
let counter_account_size = std::mem::size_of::<Counter>();
81+
let create_ix = create_account(
82+
&payer.pubkey(),
83+
&counter_account.pubkey(),
84+
Rent::default().minimum_balance(counter_account_size),
85+
counter_account_size as u64,
86+
&program_id,
87+
);
88+
let tx = Transaction::new_signed_with_payer(
89+
&[create_ix],
90+
Some(&payer.pubkey()),
91+
&[&payer, &counter_account],
92+
svm.latest_blockhash(),
93+
);
94+
svm.send_transaction(tx).unwrap();
95+
96+
// Discriminant 9 is not handled and must now return an error rather than Ok(()).
97+
let ix = Instruction {
98+
program_id,
99+
accounts: vec![AccountMeta::new(counter_account.pubkey(), false)],
100+
data: vec![9],
101+
};
102+
let tx = Transaction::new_signed_with_payer(
103+
&[ix],
104+
Some(&payer.pubkey()),
105+
&[&payer],
106+
svm.latest_blockhash(),
107+
);
108+
assert!(
109+
svm.send_transaction(tx).is_err(),
110+
"unknown instruction discriminant must fail"
111+
);
112+
}
Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
1-
// For any custom errors
1+
use anchor_lang::prelude::*;
2+
3+
// Real failure modes for the carnival examples. Previously this file was empty
4+
// and the rejection branches in the instruction handlers just logged a message
5+
// and returned Ok(()), which made it impossible for callers (or tests) to
6+
// distinguish "the ride happened" from "the ride refused service".
7+
#[error_code]
8+
pub enum CarnivalError {
9+
// The rider/gamer/eater did not bring enough tickets for the requested
10+
// attraction.
11+
#[msg("Not enough tickets for the requested attraction")]
12+
NotEnoughTickets,
13+
14+
// The rider is below the minimum height required for the ride.
15+
#[msg("Rider is below the minimum height for this ride")]
16+
RiderTooShort,
17+
}

basics/repository-layout/anchor/programs/carnival/src/instructions/eat_food.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anchor_lang::prelude::*;
22

3-
use crate::state::food;
3+
use crate::{error::CarnivalError, state::food};
44

55
// Instruction Data
66

@@ -24,10 +24,12 @@ pub fn eat_food(ix: EatFoodInstructionData) -> Result<()> {
2424
food_stand.food_type,
2525
food_stand.tickets
2626
);
27-
} else {
28-
msg!(" Enjoy your {}!", food_stand.food_type);
29-
};
27+
// Refuse service rather than logging and returning Ok(()), so
28+
// callers can distinguish a sale from a refusal.
29+
return Err(CarnivalError::NotEnoughTickets.into());
30+
}
3031

32+
msg!(" Enjoy your {}!", food_stand.food_type);
3133
return Ok(());
3234
}
3335
}

basics/repository-layout/anchor/programs/carnival/src/instructions/get_on_ride.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anchor_lang::prelude::*;
22

3-
use crate::state::ride;
3+
use crate::{error::CarnivalError, state::ride};
44

55
// Instruction Data
66

@@ -18,14 +18,17 @@ pub fn get_on_ride(ix: GetOnRideInstructionData) -> Result<()> {
1818
if ix.ride.eq(&ride.name) {
1919
msg!("You're about to ride the {}!", ride.name);
2020

21+
// Refuse service: failures used to log + return Ok(()), which made
22+
// them indistinguishable from a successful ride for callers and
23+
// tests. Return a real error instead.
2124
if ix.rider_ticket_count < ride.tickets {
2225
msg!(
2326
" Sorry {}, you need {} tickets to ride the {}!",
2427
ix.rider_name,
2528
ride.tickets,
2629
ride.name
2730
);
28-
return Ok(());
31+
return Err(CarnivalError::NotEnoughTickets.into());
2932
};
3033

3134
if ix.rider_height < ride.min_height {
@@ -35,7 +38,7 @@ pub fn get_on_ride(ix: GetOnRideInstructionData) -> Result<()> {
3538
ride.min_height,
3639
ride.name
3740
);
38-
return Ok(());
41+
return Err(CarnivalError::RiderTooShort.into());
3942
};
4043

4144
msg!(" Welcome aboard the {}!", ride.name);

0 commit comments

Comments
 (0)