Skip to content

Commit 6ec7de1

Browse files
committed
fix(cpi/hand): use u8 length prefix for String<50> in CPI wire format
The lever's switch_power instruction takes `String<50>`, which Quasar serialises with a single-byte length prefix (matching every other Quasar program: account-data, close-account, rent, realloc, repository-layout). The hand program's pull_lever CPI builder was manually constructing instruction data with a u32 (4-byte) length prefix, so every hand→lever CPI call sent a malformed payload. The breakage was masked because the lever's handler ignored the deserialised name (`_name`), but the value itself was corrupted — e.g. "\\0\\0\\0Al" instead of "Alice". The lever's own tests.rs had the same stale u32 prefix in its instruction-data builder, and hand's tests.rs had it for inbound pull_lever data too. Both now use `data.push(name.len() as u8)`, matching the canonical pattern used across the Quasar example suite. To keep this from regressing again, the lever now logs the deserialised name and both test suites assert that the round-tripped name appears in program logs verbatim ("Alice" / "Bob"). A stale length prefix on either leg of the CPI would surface immediately as a corrupted log line. Files changed: - hand/src/instructions/pull_lever.rs (CPI builder: u32 → u8) - hand/src/tests.rs (inbound builder: u32 → u8, name assert) - lever/src/instructions/switch_power.rs (log name) - lever/src/tests.rs (test builder: u32 → u8, name assert)
1 parent ddea756 commit 6ec7de1

4 files changed

Lines changed: 64 additions & 18 deletions

File tree

basics/cross-program-invocation/quasar/hand/src/instructions/pull_lever.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,27 @@ pub struct PullLever {
1515
pub fn handle_pull_lever(accounts: &PullLever, name: &str) -> Result<(), ProgramError> {
1616
log("Hand is pulling the lever!");
1717

18-
// Build the switch_power instruction data for the lever program:
19-
// [disc=1] [name: u32 len + bytes]
20-
// 128 bytes is enough for any reasonable name.
18+
// Build the switch_power instruction data for the lever program.
19+
//
20+
// Wire format: [discriminator = 1] [name: u8 length prefix + bytes].
21+
//
22+
// The lever's switch_power instruction takes `String<50>`, which Quasar
23+
// serialises with a single-byte length prefix (matching every other
24+
// Quasar program: account-data, close-account, rent, realloc,
25+
// repository-layout). An earlier version of this builder used a u32
26+
// length prefix, which sent a malformed payload on every CPI call.
27+
//
28+
// 128 bytes is enough for any reasonable name (max 50 + 1 + 1 = 52).
2129
let mut data = [0u8; 128];
2230
let name_bytes = name.as_bytes();
23-
let data_len = 1 + 4 + name_bytes.len();
31+
let data_len = 1 + 1 + name_bytes.len();
2432

2533
data[0] = 1;
26-
27-
let len_bytes = (name_bytes.len() as u32).to_le_bytes();
28-
data[1] = len_bytes[0];
29-
data[2] = len_bytes[1];
30-
data[3] = len_bytes[2];
31-
data[4] = len_bytes[3];
34+
data[1] = name_bytes.len() as u8;
3235

3336
let mut i = 0;
3437
while i < name_bytes.len() {
35-
data[5 + i] = name_bytes[i];
38+
data[2 + i] = name_bytes[i];
3639
i += 1;
3740
}
3841

basics/cross-program-invocation/quasar/hand/src/tests.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,17 @@ fn power_account(address: Pubkey, is_on: bool) -> Account {
3030
}
3131

3232
/// Build pull_lever instruction data (discriminator = 0).
33-
/// Wire format: [disc=0] [name: String]
33+
///
34+
/// Wire format: [discriminator = 0] [name: u8 length prefix + bytes].
35+
///
36+
/// The hand's pull_lever instruction takes `String<50>`, which Quasar
37+
/// serialises with a single-byte length prefix. The CPI builder in
38+
/// `pull_lever.rs` re-serialises the same name into the lever's
39+
/// instruction data using the same u8 prefix.
3440
fn build_pull_lever(name: &str) -> Vec<u8> {
35-
let mut data = vec![0u8]; // discriminator = 0
36-
data.extend_from_slice(&(name.len() as u32).to_le_bytes());
41+
let mut data = Vec::with_capacity(2 + name.len());
42+
data.push(0u8); // discriminator = 0
43+
data.push(name.len() as u8);
3744
data.extend_from_slice(name.as_bytes());
3845
data
3946
}
@@ -72,6 +79,14 @@ fn test_pull_lever_turns_on() {
7279
assert!(logs.contains("Hand is pulling"), "hand should log");
7380
assert!(logs.contains("pulling the power switch"), "lever should log");
7481
assert!(logs.contains("now on"), "power should turn on");
82+
// Verifies the CPI wire format: the lever logs the name it
83+
// deserialised. A stale u32 length prefix on either the inbound
84+
// `pull_lever` payload or the CPI to `switch_power` would corrupt
85+
// this (e.g. "\0\0\0Al" instead of "Alice").
86+
assert!(
87+
logs.contains("Alice"),
88+
"name should round-trip through hand → lever CPI; logs: {logs}"
89+
);
7590

7691
let account = result.account(&power_addr).unwrap();
7792
assert_eq!(account.data[1], 1, "power should be on");
@@ -107,6 +122,10 @@ fn test_pull_lever_turns_off() {
107122

108123
let logs = result.logs.join("\n");
109124
assert!(logs.contains("now off"), "power should turn off");
125+
assert!(
126+
logs.contains("Bob"),
127+
"name should round-trip through hand → lever CPI; logs: {logs}"
128+
);
110129

111130
let account = result.account(&power_addr).unwrap();
112131
assert_eq!(account.data[1], 0, "power should be off");

basics/cross-program-invocation/quasar/lever/src/instructions/switch_power.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ pub struct SwitchPower {
1111
}
1212

1313
#[inline(always)]
14-
pub fn handle_switch_power(accounts: &mut SwitchPower, _name: &str) -> Result<(), ProgramError> {
14+
pub fn handle_switch_power(accounts: &mut SwitchPower, name: &str) -> Result<(), ProgramError> {
1515
let current: bool = accounts.power.is_on.into();
1616
let new_state = !current;
1717
accounts.power.is_on = PodBool::from(new_state);
1818

1919
// Quasar's log() takes &str — no format! in no_std.
20+
// Logging the name verifies the wire format end-to-end: a stale u32
21+
// length prefix would surface here as a corrupted name (e.g. the
22+
// first three bytes parsed as zeros, leaving "\0\0\0Al" instead of
23+
// "Alice").
2024
log("Someone is pulling the power switch!");
25+
log(name);
2126

2227
if new_state {
2328
log("The power is now on.");

basics/cross-program-invocation/quasar/lever/src/tests.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,19 @@ fn build_initialize() -> Vec<u8> {
4747
}
4848

4949
/// Build switch_power instruction data (discriminator = 1).
50-
/// Wire format: [disc=1] [name: String]
50+
///
51+
/// Wire format: [discriminator = 1] [name: u8 length prefix + bytes].
52+
///
53+
/// The lever's switch_power instruction takes `String<50>`, which Quasar
54+
/// serialises with a single-byte length prefix (matching every other
55+
/// Quasar program: account-data, close-account, rent, realloc,
56+
/// repository-layout). An earlier version of this builder used a u32
57+
/// length prefix, which produced a malformed payload that happened to
58+
/// pass because the handler ignored the deserialised name.
5159
fn build_switch_power(name: &str) -> Vec<u8> {
52-
let mut data = vec![1u8]; // discriminator = 1
53-
data.extend_from_slice(&(name.len() as u32).to_le_bytes());
60+
let mut data = Vec::with_capacity(2 + name.len());
61+
data.push(1u8); // discriminator = 1
62+
data.push(name.len() as u8);
5463
data.extend_from_slice(name.as_bytes());
5564
data
5665
}
@@ -104,6 +113,12 @@ fn test_switch_power_on() {
104113
let logs = result.logs.join("\n");
105114
assert!(logs.contains("pulling the power switch"), "should log switch");
106115
assert!(logs.contains("now on"), "should say power is on");
116+
// Verifies wire format: a stale u32 length prefix would corrupt the
117+
// deserialised name (e.g. "\0\0\0Al" instead of "Alice").
118+
assert!(
119+
logs.contains("Alice"),
120+
"deserialised name should round-trip exactly; logs: {logs}"
121+
);
107122

108123
let account = result.account(&power_addr).unwrap();
109124
assert_eq!(account.data[1], 1, "power should now be on");
@@ -128,6 +143,10 @@ fn test_switch_power_off() {
128143

129144
let logs = result.logs.join("\n");
130145
assert!(logs.contains("now off"), "should say power is off");
146+
assert!(
147+
logs.contains("Bob"),
148+
"deserialised name should round-trip exactly; logs: {logs}"
149+
);
131150

132151
let account = result.account(&power_addr).unwrap();
133152
assert_eq!(account.data[1], 0, "power should now be off");

0 commit comments

Comments
 (0)