From da5bd23fe21189b1b404714b792a4b9fb78e4150 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 13 Oct 2023 16:24:31 +0200 Subject: [PATCH 1/3] feature gate: init program --- Cargo.lock | 11 +++ Cargo.toml | 1 + feature-gate/Cargo.toml | 27 ++++++ feature-gate/README.md | 10 +++ feature-gate/src/entrypoint.rs | 17 ++++ feature-gate/src/error.rs | 14 +++ feature-gate/src/instruction.rs | 77 ++++++++++++++++ feature-gate/src/lib.rs | 16 ++++ feature-gate/src/processor.rs | 65 ++++++++++++++ feature-gate/tests/functional.rs | 146 +++++++++++++++++++++++++++++++ 10 files changed, 384 insertions(+) create mode 100644 feature-gate/Cargo.toml create mode 100644 feature-gate/README.md create mode 100644 feature-gate/src/entrypoint.rs create mode 100644 feature-gate/src/error.rs create mode 100644 feature-gate/src/instruction.rs create mode 100644 feature-gate/src/lib.rs create mode 100644 feature-gate/src/processor.rs create mode 100644 feature-gate/tests/functional.rs diff --git a/Cargo.lock b/Cargo.lock index ad4f418c42a..0e7490099ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6553,6 +6553,17 @@ dependencies = [ "spl-token 4.0.0", ] +[[package]] +name = "spl-feature-gate" +version = "0.1.0" +dependencies = [ + "num_enum 0.7.0", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-program-error 0.3.0", +] + [[package]] name = "spl-feature-proposal" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 45fd58854fd..9e0696ded77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "examples/rust/sysvar", "examples/rust/transfer-lamports", "examples/rust/transfer-tokens", + "feature-gate", "feature-proposal/program", "feature-proposal/cli", "governance/addin-mock/program", diff --git a/feature-gate/Cargo.toml b/feature-gate/Cargo.toml new file mode 100644 index 00000000000..eb3c93dae6c --- /dev/null +++ b/feature-gate/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "spl-feature-gate" +version = "0.1.0" +description = "Solana Program Library Feature Gate Program" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +num_enum = "0.7.0" +solana-program = "1.16.16" +spl-program-error = { version = "0.3.0", path = "../libraries/program-error" } + +[dev-dependencies] +solana-program-test = "1.16.16" +solana-sdk = "1.16.16" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/feature-gate/README.md b/feature-gate/README.md new file mode 100644 index 00000000000..e83c14df86a --- /dev/null +++ b/feature-gate/README.md @@ -0,0 +1,10 @@ +# Feature Gate Program + +This program serves to manage new features on Solana. + +It serves two main purposes: activating new features and revoking features that +are pending activation. + +More information & documentation will follow as this program matures, but you +can follow the discussions +[here](https://github.com/solana-labs/solana/issues/32780)! diff --git a/feature-gate/src/entrypoint.rs b/feature-gate/src/entrypoint.rs new file mode 100644 index 00000000000..c261a918a42 --- /dev/null +++ b/feature-gate/src/entrypoint.rs @@ -0,0 +1,17 @@ +//! Program entrypoint + +use { + crate::processor, + solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, + }, +}; + +entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + processor::process(program_id, accounts, input) +} diff --git a/feature-gate/src/error.rs b/feature-gate/src/error.rs new file mode 100644 index 00000000000..6be835f2ad2 --- /dev/null +++ b/feature-gate/src/error.rs @@ -0,0 +1,14 @@ +//! Program error types + +use spl_program_error::*; + +/// Program specific errors +#[spl_program_error] +pub enum FeatureGateError { + /// Operation overflowed + #[error("Operation overflowed")] + Overflow, + /// Feature not inactive + #[error("Feature not inactive")] + FeatureAlreadyActivated, +} diff --git a/feature-gate/src/instruction.rs b/feature-gate/src/instruction.rs new file mode 100644 index 00000000000..2e787ffb48b --- /dev/null +++ b/feature-gate/src/instruction.rs @@ -0,0 +1,77 @@ +//! Program instructions + +use { + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + }, +}; + +/// Feature Gate program instructions +#[derive(Clone, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum FeatureGateInstruction { + /// Revoke a pending feature activation. + /// + /// A "pending" feature activation is a feature account that has been + /// allocated and assigned, but hasn't been processed by the network yet. + /// + /// Features that _have_ been processed by the network are activated, and + /// cannot be revoked. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w+s]` Feature account + /// 1. `[w]` Destination (for rent lamports) + RevokePendingActivation, +} +impl FeatureGateInstruction { + /// Unpacks a byte buffer into a + /// [FeatureGateInstruction](enum.FeatureGateInstruction.html). + pub fn unpack(input: &[u8]) -> Result { + if input.len() != 1 { + return Err(ProgramError::InvalidInstructionData); + } + Self::try_from(input[0]).map_err(|_| ProgramError::InvalidInstructionData) + } + + /// Packs a [FeatureGateInstruction](enum.FeatureGateInstruction.html) into + /// a byte buffer. + pub fn pack(&self) -> Vec { + vec![self.to_owned().into()] + } +} + +/// Creates a 'RevokePendingActivation' instruction. +pub fn revoke_pending_activation(feature: &Pubkey, destination: &Pubkey) -> Instruction { + let accounts = vec![ + AccountMeta::new(*feature, true), + AccountMeta::new(*destination, false), + ]; + + let data = FeatureGateInstruction::RevokePendingActivation.pack(); + + Instruction { + program_id: crate::id(), + accounts, + data, + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn test_pack_unpack(instruction: &FeatureGateInstruction) { + let packed = instruction.pack(); + let unpacked = FeatureGateInstruction::unpack(&packed).unwrap(); + assert_eq!(instruction, &unpacked); + } + + #[test] + fn test_pack_unpack_revoke() { + test_pack_unpack(&FeatureGateInstruction::RevokePendingActivation); + } +} diff --git a/feature-gate/src/lib.rs b/feature-gate/src/lib.rs new file mode 100644 index 00000000000..e6be6428a15 --- /dev/null +++ b/feature-gate/src/lib.rs @@ -0,0 +1,16 @@ +//! Feature Gate program + +#![deny(missing_docs)] +#![cfg_attr(not(test), forbid(unsafe_code))] + +#[cfg(not(feature = "no-entrypoint"))] +mod entrypoint; +pub mod error; +pub mod instruction; +pub mod processor; + +// Export current SDK types for downstream users building with a different SDK +// version +pub use solana_program; + +solana_program::declare_id!("Feature111111111111111111111111111111111111"); diff --git a/feature-gate/src/processor.rs b/feature-gate/src/processor.rs new file mode 100644 index 00000000000..c1ade8311fc --- /dev/null +++ b/feature-gate/src/processor.rs @@ -0,0 +1,65 @@ +//! Program state processor + +use { + crate::{error::FeatureGateError, instruction::FeatureGateInstruction}, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + feature::Feature, + msg, + program_error::ProgramError, + pubkey::Pubkey, + system_program, + }, +}; + +/// Processes an [RevokePendingActivation](enum.FeatureGateInstruction.html) +/// instruction. +pub fn process_revoke_pending_activation( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let feature_info = next_account_info(account_info_iter)?; + let destination_info = next_account_info(account_info_iter)?; + + if !feature_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + if feature_info.owner != program_id { + return Err(ProgramError::IllegalOwner); + } + + if Feature::from_account_info(feature_info)? + .activated_at + .is_some() + { + return Err(FeatureGateError::FeatureAlreadyActivated.into()); + } + + let new_destination_lamports = feature_info + .lamports() + .checked_add(destination_info.lamports()) + .ok_or::(FeatureGateError::Overflow.into())?; + + **feature_info.try_borrow_mut_lamports()? = 0; + **destination_info.try_borrow_mut_lamports()? = new_destination_lamports; + + feature_info.realloc(0, true)?; + feature_info.assign(&system_program::id()); + + Ok(()) +} + +/// Processes an [Instruction](enum.Instruction.html). +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { + let instruction = FeatureGateInstruction::unpack(input)?; + match instruction { + FeatureGateInstruction::RevokePendingActivation => { + msg!("Instruction: RevokePendingActivation"); + process_revoke_pending_activation(program_id, accounts) + } + } +} diff --git a/feature-gate/tests/functional.rs b/feature-gate/tests/functional.rs new file mode 100644 index 00000000000..a1266c24a26 --- /dev/null +++ b/feature-gate/tests/functional.rs @@ -0,0 +1,146 @@ +// #![cfg(feature = "test-sbf")] + +use { + solana_program::instruction::InstructionError, + solana_program_test::{processor, tokio, ProgramTest, ProgramTestContext}, + solana_sdk::{ + account::Account as SolanaAccount, + feature::{activate_with_lamports, Feature}, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, + }, + spl_feature_gate::{error::FeatureGateError, instruction::revoke_pending_activation}, +}; + +async fn setup_pending_feature( + context: &mut ProgramTestContext, + feature_keypair: &Keypair, + rent_lamports: u64, +) { + let transaction = Transaction::new_signed_with_payer( + &activate_with_lamports( + &feature_keypair.pubkey(), + &context.payer.pubkey(), + rent_lamports, + ), + Some(&context.payer.pubkey()), + &[&context.payer, feature_keypair], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); +} + +#[tokio::test] +async fn test_revoke_pending_activation() { + let feature_keypair = Keypair::new(); + let destination = Pubkey::new_unique(); + let mock_active_feature_keypair = Keypair::new(); + + let mut program_test = ProgramTest::new( + "spl_feature_gate", + spl_feature_gate::id(), + processor!(spl_feature_gate::processor::process), + ); + + // Add a mock _active_ feature for testing later + program_test.add_account( + mock_active_feature_keypair.pubkey(), + SolanaAccount { + lamports: 500_000_000, + owner: spl_feature_gate::id(), + data: vec![ + 1, // `Some()` + 45, 0, 0, 0, 0, 0, 0, 0, // Random slot `u64` + ], + ..SolanaAccount::default() + }, + ); + + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent.minimum_balance(Feature::size_of()); // For checking account balance later + + setup_pending_feature(&mut context, &feature_keypair, rent_lamports).await; + + // Fail: feature not signer + let mut revoke_ix = revoke_pending_activation(&feature_keypair.pubkey(), &destination); + revoke_ix.accounts[0].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[revoke_ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); + + // Fail: feature is already active + let transaction = Transaction::new_signed_with_payer( + &[revoke_pending_activation( + &mock_active_feature_keypair.pubkey(), + &destination, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mock_active_feature_keypair], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(FeatureGateError::FeatureAlreadyActivated as u32) + ) + ); + + // Success: Revoke a feature activation + let transaction = Transaction::new_signed_with_payer( + &[revoke_pending_activation( + &feature_keypair.pubkey(), + &destination, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &feature_keypair], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Confirm feature account was closed and destination account received lamports + let feature_account = context + .banks_client + .get_account(feature_keypair.pubkey()) + .await + .unwrap(); + assert!(feature_account.is_none()); + let destination_account = context + .banks_client + .get_account(destination) + .await + .unwrap() + .unwrap(); + assert_eq!(destination_account.lamports, rent_lamports); +} From 514bc34f0a13337a1920f74a904159f1a1540055 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 15 Oct 2023 19:28:47 +0200 Subject: [PATCH 2/3] move to program folder --- Cargo.toml | 2 +- feature-gate/README.md | 3 +-- feature-gate/{ => program}/Cargo.toml | 2 +- feature-gate/{ => program}/src/entrypoint.rs | 0 feature-gate/{ => program}/src/error.rs | 4 ++-- feature-gate/{ => program}/src/instruction.rs | 10 +++++----- feature-gate/{ => program}/src/lib.rs | 0 feature-gate/{ => program}/src/processor.rs | 0 feature-gate/{ => program}/tests/functional.rs | 2 +- 9 files changed, 11 insertions(+), 12 deletions(-) rename feature-gate/{ => program}/Cargo.toml (87%) rename feature-gate/{ => program}/src/entrypoint.rs (100%) rename feature-gate/{ => program}/src/error.rs (75%) rename feature-gate/{ => program}/src/instruction.rs (87%) rename feature-gate/{ => program}/src/lib.rs (100%) rename feature-gate/{ => program}/src/processor.rs (100%) rename feature-gate/{ => program}/tests/functional.rs (99%) diff --git a/Cargo.toml b/Cargo.toml index 9e0696ded77..b7e44e1768a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ "examples/rust/sysvar", "examples/rust/transfer-lamports", "examples/rust/transfer-tokens", - "feature-gate", + "feature-gate/program", "feature-proposal/program", "feature-proposal/cli", "governance/addin-mock/program", diff --git a/feature-gate/README.md b/feature-gate/README.md index e83c14df86a..c0dbaf5787c 100644 --- a/feature-gate/README.md +++ b/feature-gate/README.md @@ -2,8 +2,7 @@ This program serves to manage new features on Solana. -It serves two main purposes: activating new features and revoking features that -are pending activation. +It serves one purpose: revoking features that are pending activation. More information & documentation will follow as this program matures, but you can follow the discussions diff --git a/feature-gate/Cargo.toml b/feature-gate/program/Cargo.toml similarity index 87% rename from feature-gate/Cargo.toml rename to feature-gate/program/Cargo.toml index eb3c93dae6c..5cf6233d029 100644 --- a/feature-gate/Cargo.toml +++ b/feature-gate/program/Cargo.toml @@ -14,7 +14,7 @@ test-sbf = [] [dependencies] num_enum = "0.7.0" solana-program = "1.16.16" -spl-program-error = { version = "0.3.0", path = "../libraries/program-error" } +spl-program-error = { version = "0.3.0", path = "../../libraries/program-error" } [dev-dependencies] solana-program-test = "1.16.16" diff --git a/feature-gate/src/entrypoint.rs b/feature-gate/program/src/entrypoint.rs similarity index 100% rename from feature-gate/src/entrypoint.rs rename to feature-gate/program/src/entrypoint.rs diff --git a/feature-gate/src/error.rs b/feature-gate/program/src/error.rs similarity index 75% rename from feature-gate/src/error.rs rename to feature-gate/program/src/error.rs index 6be835f2ad2..731c61213a4 100644 --- a/feature-gate/src/error.rs +++ b/feature-gate/program/src/error.rs @@ -8,7 +8,7 @@ pub enum FeatureGateError { /// Operation overflowed #[error("Operation overflowed")] Overflow, - /// Feature not inactive - #[error("Feature not inactive")] + /// Feature already activated + #[error("Feature already activated")] FeatureAlreadyActivated, } diff --git a/feature-gate/src/instruction.rs b/feature-gate/program/src/instruction.rs similarity index 87% rename from feature-gate/src/instruction.rs rename to feature-gate/program/src/instruction.rs index 2e787ffb48b..6607249bcdd 100644 --- a/feature-gate/src/instruction.rs +++ b/feature-gate/program/src/instruction.rs @@ -23,8 +23,8 @@ pub enum FeatureGateInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[w+s]` Feature account - /// 1. `[w]` Destination (for rent lamports) + /// 0. `[w+s]` Feature account + /// 1. `[w]` Destination (for rent lamports) RevokePendingActivation, } impl FeatureGateInstruction { @@ -45,9 +45,9 @@ impl FeatureGateInstruction { } /// Creates a 'RevokePendingActivation' instruction. -pub fn revoke_pending_activation(feature: &Pubkey, destination: &Pubkey) -> Instruction { +pub fn revoke_pending_activation(feature_id: &Pubkey, destination: &Pubkey) -> Instruction { let accounts = vec![ - AccountMeta::new(*feature, true), + AccountMeta::new(*feature_id, true), AccountMeta::new(*destination, false), ]; @@ -71,7 +71,7 @@ mod test { } #[test] - fn test_pack_unpack_revoke() { + fn test_pack_unpack_revoke_pending_activation() { test_pack_unpack(&FeatureGateInstruction::RevokePendingActivation); } } diff --git a/feature-gate/src/lib.rs b/feature-gate/program/src/lib.rs similarity index 100% rename from feature-gate/src/lib.rs rename to feature-gate/program/src/lib.rs diff --git a/feature-gate/src/processor.rs b/feature-gate/program/src/processor.rs similarity index 100% rename from feature-gate/src/processor.rs rename to feature-gate/program/src/processor.rs diff --git a/feature-gate/tests/functional.rs b/feature-gate/program/tests/functional.rs similarity index 99% rename from feature-gate/tests/functional.rs rename to feature-gate/program/tests/functional.rs index a1266c24a26..e586e7bc459 100644 --- a/feature-gate/tests/functional.rs +++ b/feature-gate/program/tests/functional.rs @@ -1,4 +1,4 @@ -// #![cfg(feature = "test-sbf")] +#![cfg(feature = "test-sbf")] use { solana_program::instruction::InstructionError, From f202fdbec88243cd0fa32cd925f1791c5d54ced0 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 18 Oct 2023 10:08:02 +0200 Subject: [PATCH 3/3] address latest feedback --- feature-gate/program/src/instruction.rs | 6 +++--- feature-gate/program/src/processor.rs | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/feature-gate/program/src/instruction.rs b/feature-gate/program/src/instruction.rs index 6607249bcdd..4fa73a514e1 100644 --- a/feature-gate/program/src/instruction.rs +++ b/feature-gate/program/src/instruction.rs @@ -16,10 +16,10 @@ pub enum FeatureGateInstruction { /// Revoke a pending feature activation. /// /// A "pending" feature activation is a feature account that has been - /// allocated and assigned, but hasn't been processed by the network yet. + /// allocated and assigned, but hasn't yet been updated by the runtime + /// with an `activation_slot`. /// - /// Features that _have_ been processed by the network are activated, and - /// cannot be revoked. + /// Features that _have_ been activated by the runtime cannot be revoked. /// /// Accounts expected by this instruction: /// diff --git a/feature-gate/program/src/processor.rs b/feature-gate/program/src/processor.rs index c1ade8311fc..574ad143071 100644 --- a/feature-gate/program/src/processor.rs +++ b/feature-gate/program/src/processor.rs @@ -13,10 +13,10 @@ use { }, }; -/// Processes an [RevokePendingActivation](enum.FeatureGateInstruction.html) +/// Processes a [RevokePendingActivation](enum.FeatureGateInstruction.html) /// instruction. pub fn process_revoke_pending_activation( - program_id: &Pubkey, + _program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -28,10 +28,7 @@ pub fn process_revoke_pending_activation( return Err(ProgramError::MissingRequiredSignature); } - if feature_info.owner != program_id { - return Err(ProgramError::IllegalOwner); - } - + // This will also check the program ID if Feature::from_account_info(feature_info)? .activated_at .is_some()