diff --git a/feature-gate/README.md b/feature-gate/README.md index c0dbaf5787c..e83c14df86a 100644 --- a/feature-gate/README.md +++ b/feature-gate/README.md @@ -2,7 +2,8 @@ This program serves to manage new features on Solana. -It serves one purpose: revoking features that are pending activation. +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 diff --git a/feature-gate/program/Cargo.toml b/feature-gate/program/Cargo.toml index 5cf6233d029..a2dbcddf026 100644 --- a/feature-gate/program/Cargo.toml +++ b/feature-gate/program/Cargo.toml @@ -17,8 +17,8 @@ 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" +solana-program-test = "1.16.16" [lib] crate-type = ["cdylib", "lib"] diff --git a/feature-gate/program/src/error.rs b/feature-gate/program/src/error.rs index 731c61213a4..942f5ae134f 100644 --- a/feature-gate/program/src/error.rs +++ b/feature-gate/program/src/error.rs @@ -11,4 +11,10 @@ pub enum FeatureGateError { /// Feature already activated #[error("Feature already activated")] FeatureAlreadyActivated, + /// Incorrect feature ID + #[error("Incorrect feature ID")] + IncorrectFeatureId, + /// Invalid feature account + #[error("Invalid feature account")] + InvalidFeatureAccount, } diff --git a/feature-gate/program/src/instruction.rs b/feature-gate/program/src/instruction.rs index 335f2996aea..37ee72b35fd 100644 --- a/feature-gate/program/src/instruction.rs +++ b/feature-gate/program/src/instruction.rs @@ -3,9 +3,12 @@ use { num_enum::{IntoPrimitive, TryFromPrimitive}, solana_program::{ + feature::Feature, instruction::{AccountMeta, Instruction}, program_error::ProgramError, pubkey::Pubkey, + rent::Rent, + system_instruction, system_program, }, }; @@ -13,6 +16,19 @@ use { #[derive(Clone, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] #[repr(u8)] pub enum FeatureGateInstruction { + /// Submit a feature for activation. + /// + /// Note: This instruction expects the account to exist and be owned by the + /// system program. The account should also have enough rent-exempt lamports + /// to cover the cost of the account creation for a + /// `solana_program::feature::Feature` state prior to invoking this + /// instruction. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w+s]` Feature account (must be a system account) + /// 1. `[]` System program + ActivateFeature, /// Revoke a pending feature activation. /// /// A "pending" feature activation is a feature account that has been @@ -44,10 +60,40 @@ impl FeatureGateInstruction { } } +/// Creates an 'ActivateFeature' instruction. +pub fn activate_feature(feature_id: &Pubkey) -> Instruction { + let accounts = vec![ + AccountMeta::new(*feature_id, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let data = FeatureGateInstruction::ActivateFeature.pack(); + + Instruction { + program_id: crate::id(), + accounts, + data, + } +} + +/// Creates a set of two instructions: +/// * One to fund the feature account with rent-exempt lamports +/// * Another is the Feature Gate Program's 'ActivateFeature' instruction +pub fn activate_feature_with_rent_transfer( + feature_id: &Pubkey, + payer: &Pubkey, +) -> [Instruction; 2] { + let lamports = Rent::default().minimum_balance(Feature::size_of()); + [ + system_instruction::transfer(payer, feature_id, lamports), + activate_feature(feature_id), + ] +} + /// 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), ]; @@ -70,6 +116,11 @@ mod test { assert_eq!(instruction, &unpacked); } + #[test] + fn test_pack_unpack_activate_feature() { + test_pack_unpack(&FeatureGateInstruction::ActivateFeature); + } + #[test] fn test_pack_unpack_revoke_pending_activation() { test_pack_unpack(&FeatureGateInstruction::RevokePendingActivation); diff --git a/feature-gate/program/src/processor.rs b/feature-gate/program/src/processor.rs index c1ade8311fc..b6b6b46965b 100644 --- a/feature-gate/program/src/processor.rs +++ b/feature-gate/program/src/processor.rs @@ -7,12 +7,41 @@ use { entrypoint::ProgramResult, feature::Feature, msg, + program::invoke, program_error::ProgramError, pubkey::Pubkey, - system_program, + system_instruction, system_program, }, }; +/// Processes an [ActivateFeature](enum.FeatureGateInstruction.html) +/// instruction. +pub fn process_activate_feature(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let feature_info = next_account_info(account_info_iter)?; + let _system_program_info = next_account_info(account_info_iter)?; + + if !feature_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + if feature_info.owner != &system_program::id() { + return Err(FeatureGateError::InvalidFeatureAccount.into()); + } + + invoke( + &system_instruction::allocate(feature_info.key, Feature::size_of() as u64), + &[feature_info.clone()], + )?; + invoke( + &system_instruction::assign(feature_info.key, program_id), + &[feature_info.clone()], + )?; + + Ok(()) +} + /// Processes an [RevokePendingActivation](enum.FeatureGateInstruction.html) /// instruction. pub fn process_revoke_pending_activation( @@ -57,6 +86,10 @@ pub fn process_revoke_pending_activation( pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = FeatureGateInstruction::unpack(input)?; match instruction { + FeatureGateInstruction::ActivateFeature => { + msg!("Instruction: ActivateFeature"); + process_activate_feature(program_id, accounts) + } FeatureGateInstruction::RevokePendingActivation => { msg!("Instruction: RevokePendingActivation"); process_revoke_pending_activation(program_id, accounts) diff --git a/feature-gate/program/tests/functional.rs b/feature-gate/program/tests/functional.rs index e586e7bc459..271376e622e 100644 --- a/feature-gate/program/tests/functional.rs +++ b/feature-gate/program/tests/functional.rs @@ -5,25 +5,23 @@ use { solana_program_test::{processor, tokio, ProgramTest, ProgramTestContext}, solana_sdk::{ account::Account as SolanaAccount, - feature::{activate_with_lamports, Feature}, + feature::Feature, pubkey::Pubkey, signature::{Keypair, Signer}, + system_instruction, system_program, transaction::{Transaction, TransactionError}, }, - spl_feature_gate::{error::FeatureGateError, instruction::revoke_pending_activation}, + spl_feature_gate::{ + error::FeatureGateError, + instruction::{ + activate_feature, activate_feature_with_rent_transfer, revoke_pending_activation, + }, + }, }; -async fn setup_pending_feature( - context: &mut ProgramTestContext, - feature_keypair: &Keypair, - rent_lamports: u64, -) { +async fn setup_pending_feature(context: &mut ProgramTestContext, feature_keypair: &Keypair) { let transaction = Transaction::new_signed_with_payer( - &activate_with_lamports( - &feature_keypair.pubkey(), - &context.payer.pubkey(), - rent_lamports, - ), + &activate_feature_with_rent_transfer(&feature_keypair.pubkey(), &context.payer.pubkey()), Some(&context.payer.pubkey()), &[&context.payer, feature_keypair], context.last_blockhash, @@ -36,6 +34,141 @@ async fn setup_pending_feature( .unwrap(); } +#[tokio::test] +async fn test_activate_feature() { + let feature_keypair = Keypair::new(); + let mock_invalid_feature = Keypair::new(); + let mock_invalid_signer = Keypair::new(); + + let mut program_test = ProgramTest::new( + "spl_feature_gate", + spl_feature_gate::id(), + processor!(spl_feature_gate::processor::process), + ); + + // Need to fund this account for a test transfer later + program_test.add_account( + mock_invalid_signer.pubkey(), + SolanaAccount { + lamports: 500_000_000, + owner: system_program::id(), + ..SolanaAccount::default() + }, + ); + // Add a mock account that's NOT a valid feature account for testing later + program_test.add_account( + mock_invalid_feature.pubkey(), + SolanaAccount { + lamports: 500_000_000, + owner: spl_feature_gate::id(), + ..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()); + + // Fail: feature not signer + let mut activate_ix = activate_feature(&feature_keypair.pubkey()); + activate_ix.accounts[0].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &mock_invalid_signer.pubkey(), + &feature_keypair.pubkey(), + rent_lamports, + ), + activate_ix, + ], + Some(&mock_invalid_signer.pubkey()), + &[&mock_invalid_signer], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature) + ); + + // Fail: feature not owned by system program + let transaction = Transaction::new_signed_with_payer( + &[activate_feature(&mock_invalid_feature.pubkey())], + Some(&context.payer.pubkey()), + &[&context.payer, &mock_invalid_feature], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(FeatureGateError::InvalidFeatureAccount as u32), + ) + ); + + // Success: Submit a feature for activation + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &feature_keypair.pubkey(), + rent_lamports, + ), + activate_feature(&feature_keypair.pubkey()), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &feature_keypair], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // Confirm feature account exists with proper configurations + let feature_account = context + .banks_client + .get_account(feature_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + assert_eq!(feature_account.owner, spl_feature_gate::id()); + + // Cannot activate the same feature again + let transaction = Transaction::new_signed_with_payer( + &[activate_feature(&feature_keypair.pubkey())], + Some(&context.payer.pubkey()), + &[&context.payer, &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::InvalidFeatureAccount as u32) + ) + ); +} + #[tokio::test] async fn test_revoke_pending_activation() { let feature_keypair = Keypair::new(); @@ -66,7 +199,7 @@ async fn test_revoke_pending_activation() { 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; + setup_pending_feature(&mut context, &feature_keypair).await; // Fail: feature not signer let mut revoke_ix = revoke_pending_activation(&feature_keypair.pubkey(), &destination);