-
Notifications
You must be signed in to change notification settings - Fork 34
Add native Rust (non-Anchor) VRF example #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| [package] | ||
| name = "native-rust-vrf" | ||
| version = "0.1.0" | ||
| edition = "2024" | ||
|
|
||
| [lib] | ||
| crate-type = ["cdylib", "lib"] | ||
|
|
||
| [dependencies] | ||
| borsh = "1.5.7" | ||
| ephemeral-vrf-sdk = "0.2.3" | ||
| solana-program = "2.2.1" | ||
| solana-sdk-ids = "2.2.1" | ||
| solana-system-interface = { version = "1.0.0", features = ["bincode"] } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # rust-native-vrf-example | ||
|
|
||
| Native Solana program (no Anchor) that uses **MagicBlock Ephemeral VRF** via `ephemeral-vrf-sdk`: the user **requests** randomness; the **VRF program** **callbacks** with 32 bytes; you **derive** a value (e.g. 1–6) and store it in `PlayerState`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent dice range between intro and callback section. Line 3 says randomness is mapped to Also applies to: 26-26 🤖 Prompt for AI Agents |
||
|
|
||
| **Program id:** `5hExoUW5SvPxTHTcz3ok117BoLa1TzzG6KZZfWD23DfD` (see `src/lib.rs`). | ||
|
|
||
| --- | ||
|
|
||
| ## Instructions (what each one does) | ||
|
|
||
| ### `InitializePlayer` (Borsh: wallet → your program) | ||
|
|
||
| - **Caller:** user (signs as **authority**). | ||
| - **Effect:** creates the **player** PDA for seeds `["player", authority]`, writes `PlayerState` (discriminator, `random_value` initially `0`, bump). | ||
| - **File:** `src/instructions/initialize_player.rs` | ||
|
|
||
| ### `RequestRandomness { client_seed: u8 }` (Borsh: wallet → your program) | ||
|
|
||
| - **Caller:** user (payer signs). | ||
| - **Effect:** checks queue / accounts, builds `RequestRandomnessParams` and CPIs the **ephemeral VRF** program with `create_request_randomness_ix`. Your program signs the CPI using the **identity** PDA (`["identity"]` under this program). The request encodes *which* callback to run and *which* accounts the VRF will pass when it invokes you (e.g. the player PDA as writable). **This instruction does not** set the final roll; it only records the request on-chain and triggers the VRF. | ||
| - **File:** `src/instructions/request_randomness.rs` | ||
|
|
||
| ### VRF callback → `CallbackConsumeRandomness` (not plain Borsh on the same enum) | ||
|
|
||
| - **Caller:** the **VRF** program, not the user. Instruction data = fixed **8-byte** prefix (see `vrf_lite::CALLBACK_CONSUME_RANDOMNESS`) **+ 32** random bytes (40 bytes total). `src/processor.rs` routes this **before** `VrfInstruction::try_from_slice`, because it is not the same layout as your wallet Borsh instructions. | ||
| - **Effect:** verifies `VRF_PROGRAM_IDENTITY` is the signer, parses the 32-byte seed, maps it (e.g. `rnd::random_u8_with_range` → 1–10), updates `PlayerState.random_value` on the player PDA. | ||
| - **Files:** `src/vrf_lite.rs`, `src/instructions/callback_consume_randomness.rs` | ||
|
|
||
| --- | ||
|
|
||
| ## Build and deploy | ||
|
|
||
| ```bash | ||
| cargo build-sbf | ||
| solana program deploy target/deploy/<your_program>.so --program-id reflex_program-keypair.json | ||
| ``` | ||
|
Comment on lines
+33
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stray The deploy command references 📝 Proposed fix cargo build-sbf
-solana program deploy target/deploy/<your_program>.so --program-id reflex_program-keypair.json
+solana program deploy target/deploy/native_rust_vrf.so --program-id native-rust-vrf-keypair.json🤖 Prompt for AI Agents |
||
|
|
||
| Upgrade the same program id when you change the `.so` (redeploy with the same program keypair). | ||
|
|
||
| --- | ||
|
|
||
| ## Client tests (`test/`) | ||
|
|
||
| ```bash | ||
| cd test | ||
| npm install | ||
| # Off-chain Borsh checks only | ||
| npm test | ||
| # On-chain: devnet (or set SOLANA_RPC_URL / SOLANA_WS_URL); needs payer keypair | ||
| RUN_INTEGRATION=1 npm test | ||
| ``` | ||
|
|
||
| - **`RUN_INIT_INTEGRATION=1`:** also runs the `initialize_player` chain test (default off so you can focus on VRF if the player PDA already exists). | ||
| - **`AUTO_INIT_PLAYER=1`:** with `RUN_INTEGRATION=1`, creates the player PDA if missing before the VRF test. | ||
|
|
||
| `PROGRAM_ID` in the client matches `getTestProgramId()` in `test/utils.ts` (override with env). | ||
|
|
||
| --- | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // import crates / libraries | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::processor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use solana_program::{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // declare and export the program's entrypoint | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entrypoint!(process_instruction); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // program entrypoint's implementation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub fn process_instruction( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| program_id: &Pubkey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accounts: &[AccountInfo], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _instruction_data: &[u8], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> ProgramResult { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Log a message indicating the program ID, number of accounts, and instruction data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "process_instruction: Program {} is executed with {} account(s) and the following data={:?}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| program_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accounts.len(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _instruction_data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processor::process_instruction(program_id, accounts, _instruction_data)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Drop the leading underscore on the used parameter, and simplify the return.
♻️ Proposed fix pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
- _instruction_data: &[u8],
+ instruction_data: &[u8],
) -> ProgramResult {
- // Log a message indicating the program ID, number of accounts, and instruction data
- msg!(
- "process_instruction: Program {} is executed with {} account(s) and the following data={:?}",
- program_id,
- accounts.len(),
- _instruction_data
- );
- processor::process_instruction(program_id, accounts, _instruction_data)?;
- Ok(())
+ msg!(
+ "process_instruction: program {} accounts={} data_len={}",
+ program_id,
+ accounts.len(),
+ instruction_data.len()
+ );
+ processor::process_instruction(program_id, accounts, instruction_data)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| use solana_program::program_error::ProgramError; | ||
|
|
||
| /// Program-specific errors. Custom codes start at 0x1770 to avoid colliding with common SPL ranges. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| #[repr(u32)] | ||
| pub enum VrfError { | ||
| AlreadyInitialized = 0x1770, | ||
| InvalidPda = 0x1771, | ||
| InvalidInstructionData = 0x1772, | ||
| AccountOrder = 0x1773, | ||
| MissingSignature = 0x1774, | ||
| InvalidSystemProgram = 0x1775, | ||
| ExpectedUnallocatedPda = 0x1776, | ||
| /// `callback_consume` must be invoked by the VRF (prefix + 32B); wallet cannot trigger it this way. | ||
| CallbackUnexpectedUserInvoke = 0x1777, | ||
| /// First account must be `ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY` and signer. | ||
| InvalidVrfProgramIdentity = 0x1778, | ||
| /// VRF callback `instruction_data` is not 8+32 with the expected prefix. | ||
| InvalidCallbackData = 0x1779, | ||
| /// `oracle_queue` must match the queue used with this cluster (we pin `DEFAULT_QUEUE` from the SDK). | ||
| InvalidOracleQueue = 0x177a, | ||
| /// `program identity` PDA (seeds `[identity]`) is wrong. | ||
| InvalidProgramIdentityPda = 0x177b, | ||
| /// `request_randomness` requires an initialized `Player` account. | ||
| PlayerNotInitialized = 0x177c, | ||
| /// PDA is not owned by this program or bad discriminator. | ||
| InvalidPlayerState = 0x177d, | ||
| } | ||
|
|
||
| impl From<VrfError> for ProgramError { | ||
| fn from(e: VrfError) -> Self { | ||
| ProgramError::Custom(e as u32) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| use crate::{error::VrfError, state::PlayerState, vrf_lite}; | ||||||||||||||||||||||||||||||||||||||||||||
| use borsh::BorshDeserialize; | ||||||||||||||||||||||||||||||||||||||||||||
| use ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY; | ||||||||||||||||||||||||||||||||||||||||||||
| use ephemeral_vrf_sdk::rnd; | ||||||||||||||||||||||||||||||||||||||||||||
| use solana_program::{ | ||||||||||||||||||||||||||||||||||||||||||||
| account_info::AccountInfo, | ||||||||||||||||||||||||||||||||||||||||||||
| entrypoint::ProgramResult, | ||||||||||||||||||||||||||||||||||||||||||||
| program_error::ProgramError, | ||||||||||||||||||||||||||||||||||||||||||||
| pubkey::Pubkey, | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| /// `[0] vrf_program_identity` — `ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY`, **signer** (VRF) | ||||||||||||||||||||||||||||||||||||||||||||
| /// `[1] player` (mut) — the same PDA you passed in the request’s `accounts_metas` | ||||||||||||||||||||||||||||||||||||||||||||
| pub fn process( | ||||||||||||||||||||||||||||||||||||||||||||
| program_id: &Pubkey, | ||||||||||||||||||||||||||||||||||||||||||||
| accounts: &[AccountInfo], | ||||||||||||||||||||||||||||||||||||||||||||
| instruction_data: &[u8], | ||||||||||||||||||||||||||||||||||||||||||||
| ) -> ProgramResult { | ||||||||||||||||||||||||||||||||||||||||||||
| if accounts.len() < 2 { | ||||||||||||||||||||||||||||||||||||||||||||
| return Err(VrfError::AccountOrder.into()); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| let vrf_id = &accounts[0]; | ||||||||||||||||||||||||||||||||||||||||||||
| let player = &accounts[1]; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if vrf_id.key != &VRF_PROGRAM_IDENTITY { | ||||||||||||||||||||||||||||||||||||||||||||
| return Err(VrfError::InvalidVrfProgramIdentity.into()); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| if !vrf_id.is_signer { | ||||||||||||||||||||||||||||||||||||||||||||
| return Err(VrfError::InvalidVrfProgramIdentity.into()); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let randomness: &[u8; 32] = vrf_lite::parse_vrf_callback_randomness(instruction_data) | ||||||||||||||||||||||||||||||||||||||||||||
| .map_err(|_| VrfError::InvalidCallbackData)?; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if !player.is_writable { | ||||||||||||||||||||||||||||||||||||||||||||
| return Err(VrfError::AccountOrder.into()); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| if player.owner != program_id { | ||||||||||||||||||||||||||||||||||||||||||||
| return Err(VrfError::InvalidPlayerState.into()); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let mut p = PlayerState::try_from_slice(&player.try_borrow_data()?).map_err(|_| { | ||||||||||||||||||||||||||||||||||||||||||||
| if player.data_is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||
| VrfError::PlayerNotInitialized | ||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||
| VrfError::InvalidPlayerState | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| })?; | ||||||||||||||||||||||||||||||||||||||||||||
| if p.discriminator != crate::state::DISCRIMINATOR_PLAYER { | ||||||||||||||||||||||||||||||||||||||||||||
| return Err(VrfError::InvalidPlayerState.into()); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
♻️ Suggested change- let mut p = PlayerState::try_from_slice(&player.try_borrow_data()?).map_err(|_| {
+ let data_ref = player.try_borrow_data()?;
+ let mut p = PlayerState::try_from_slice(&data_ref[..PlayerState::LEN]).map_err(|_| {
if player.data_is_empty() {
VrfError::PlayerNotInitialized
} else {
VrfError::InvalidPlayerState
}
})?;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // p.random_value = rnd::random_u64(randomness); | ||||||||||||||||||||||||||||||||||||||||||||
| let roll_1_to_6 = rnd::random_u8_with_range(randomness, 1, 6) as u64; | ||||||||||||||||||||||||||||||||||||||||||||
| p.random_value = roll_1_to_6; | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+53
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop the stale commented-out alternative and reconsider clamping randomness to 1..=6 here. Two things:
Related: see the test-side comment on 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| let out = borsh::to_vec(&p).map_err(|_| ProgramError::InvalidAccountData)?; | ||||||||||||||||||||||||||||||||||||||||||||
| let mut data = player.try_borrow_mut_data()?; | ||||||||||||||||||||||||||||||||||||||||||||
| if out.len() > data.len() { | ||||||||||||||||||||||||||||||||||||||||||||
| return Err(ProgramError::AccountDataTooSmall); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| data[..out.len()].copy_from_slice(&out); | ||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| use crate::{ | ||
| error::VrfError, | ||
| state::{self, PlayerState}, | ||
| }; | ||
| use solana_program::{ | ||
| account_info::AccountInfo, | ||
| entrypoint::ProgramResult, | ||
| msg, | ||
| program::invoke_signed, | ||
| program_error::ProgramError, | ||
| pubkey::Pubkey, | ||
| rent::Rent, | ||
| sysvar::Sysvar, | ||
| }; | ||
| use solana_sdk_ids::system_program; | ||
| use solana_system_interface::instruction as system_instruction; | ||
|
|
||
|
|
||
| /// Accounts: `[0] player authority (signer, mut)`, `[1] player PDA (mut)`, | ||
| /// `[2] system program`. | ||
| pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { | ||
| if accounts.len() < 3 { | ||
| return Err(VrfError::AccountOrder.into()); | ||
| } | ||
| let authority = &accounts[0]; | ||
| let player_pda = &accounts[1]; | ||
| let system_info = &accounts[2]; | ||
|
|
||
| if !authority.is_signer { | ||
| return Err(VrfError::MissingSignature.into()); | ||
| } | ||
| if *system_info.key != system_program::ID { | ||
| return Err(VrfError::InvalidSystemProgram.into()); | ||
| } | ||
| if !player_pda.is_writable { | ||
| return Err(VrfError::AccountOrder.into()); | ||
| } | ||
|
|
||
| let (expected_pda, bump) = state::find_player_pda(authority.key, program_id); | ||
| if player_pda.key != &expected_pda { | ||
| return Err(VrfError::InvalidPda.into()); | ||
| } | ||
|
|
||
| if player_pda.owner == program_id | ||
| && player_pda.data_len() >= 1 | ||
| && player_pda.try_borrow_data()?[0] == state::DISCRIMINATOR_PLAYER | ||
| { | ||
| return Err(VrfError::AlreadyInitialized.into()); | ||
| } | ||
|
|
||
| if player_pda.lamports() > 0 | ||
| && *player_pda.owner != system_program::ID | ||
| && *player_pda.owner != *program_id | ||
| { | ||
| return Err(VrfError::ExpectedUnallocatedPda.into()); | ||
| } | ||
|
|
||
| let space = PlayerState::LEN; | ||
| let rent = Rent::get()?; | ||
| let lamports = rent.minimum_balance(space); | ||
| let player = PlayerState::new(bump); | ||
|
|
||
| let bump_seed = [bump]; | ||
| let signer: &[&[u8]] = &[state::PLAYER_SEED, authority.key.as_ref(), &bump_seed]; | ||
|
|
||
| invoke_signed( | ||
| &system_instruction::create_account( | ||
| authority.key, | ||
| player_pda.key, | ||
| lamports, | ||
| space as u64, | ||
| program_id, | ||
| ), | ||
| &[authority.clone(), player_pda.clone(), system_info.clone()], | ||
| &[signer], | ||
| )?; | ||
|
Comment on lines
+51
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pre-funded PDA prevents initialization (minor griefing vector). If the PDA already holds lamports (owner = system program, no data), the early checks pass, but
Not blocking. 🤖 Prompt for AI Agents |
||
|
|
||
| let data = borsh::to_vec(&player).map_err(|_| ProgramError::InvalidAccountData)?; | ||
| let mut dst = player_pda.try_borrow_mut_data()?; | ||
| if data.len() > dst.len() { | ||
| return Err(ProgramError::AccountDataTooSmall); | ||
| } | ||
| dst[..data.len()].copy_from_slice(&data); | ||
| msg!( | ||
| "initialize_player: ok authority={} pda={}", | ||
| authority.key, | ||
| player_pda.key | ||
| ); | ||
| Ok(()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| //! Per-instruction handlers. `processor` only decodes the enum and dispatches here. | ||
|
|
||
| pub mod callback_consume_randomness; | ||
| pub mod initialize_player; | ||
| pub mod request_randomness; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
What is the current Rust version bundled with the latest Solana / Agave cargo-build-sbf platform-tools, and does it support edition = "2024"?💡 Result:
The latest Agave / Solana cargo-build-sbf platform-tools uses platform-tools v1.54 (as of March 2026), which bundles a Rust version at least 1.84.1 or newer (v1.47 used 1.84.1; later versions like v1.54 likely use 1.94+ given Agave discussions). It does not support edition = "2024", as Cargo 1.84.0 explicitly lacks edition2024 support (requires Cargo 1.85+ or nightly), and issues persist into late 2025 without evidence of upgrade by April 2026.
Citations:
Downgrade
edition = "2024"toedition = "2021".Edition 2024 is not supported by the latest Solana/Agave
cargo-build-sbfplatform-tools (v1.54 as of March 2026). The bundled Cargo version lacks edition 2024 support and requires Cargo 1.85+. Contributors will encounter a build failure when runningcargo build-sbf. Align with other examples in the repo by usingedition = "2021".🤖 Prompt for AI Agents