Skip to content

Commit

Permalink
feature gate: add activate instruction
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec committed Oct 16, 2023
1 parent cb72e2a commit af10219
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 47 deletions.
3 changes: 2 additions & 1 deletion feature-gate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion feature-gate/program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
6 changes: 6 additions & 0 deletions feature-gate/program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
37 changes: 37 additions & 0 deletions feature-gate/program/src/feature_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! Module for managing feature IDs
// use {
// crate::error::FeatureGateError,
// solana_program::{program_error::ProgramError, pubkey::Pubkey},
// solana_sdk::feature_set::FEATURE_NAMES,
// };

// /// Returns `true` if the feature ID exists in the current feature set.
// fn feature_exists(feature_id: &Pubkey) -> bool {
// FEATURE_NAMES.iter().any(|(id, _)| id == feature_id)
// }

// /// Derives the feature ID from a authority's address.
// pub fn derive_feature_id(authority: &Pubkey) -> Result<Pubkey, ProgramError>
// { let nonce = 0u16; // u16::MAX = 65_535 features!
// let mut feature_id = Pubkey::find_program_address(&[b"feature"],
// &crate::id()).0; while feature_exists(&feature_id) {
// nonce.checked_add(1).ok_or(FeatureGateError::Overflow)?;
// feature_id = Pubkey::find_program_address(
// &[b"feature", &nonce.to_le_bytes(), &authority.to_bytes()],
// &crate::id(),
// )
// .0;
// }
// Ok(feature_id)
// }

use solana_program::{program_error::ProgramError, pubkey::Pubkey};

/// Derives the feature ID from a authority's address.
pub fn derive_feature_id(authority: &Pubkey) -> Result<(Pubkey, u8), ProgramError> {
Ok(Pubkey::find_program_address(
&[b"feature", authority.as_ref()],
&crate::id(),
))
}
63 changes: 58 additions & 5 deletions feature-gate/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,33 @@
use {
num_enum::{IntoPrimitive, TryFromPrimitive},
solana_program::{
feature::Feature,
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction, system_program,
},
};

/// Feature Gate program instructions
#[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]` Feature account (must be a system account)
/// 1. `[s]` Feature activation authority (can be multisig)
/// 2. `[]` System program
ActivateFeature,
/// Revoke a pending feature activation.
///
/// A "pending" feature activation is a feature account that has been
Expand All @@ -23,8 +40,8 @@ pub enum FeatureGateInstruction {
///
/// Accounts expected by this instruction:
///
/// 0. `[w+s]` Feature account
/// 1. `[w]` Destination (for rent lamports)
/// 0. `[w]` Feature account
/// 1. `[w+s]` Feature activation authority (can be multisig)
RevokePendingActivation,
}
impl FeatureGateInstruction {
Expand All @@ -44,11 +61,42 @@ impl FeatureGateInstruction {
}
}

/// Creates an 'ActivateFeature' instruction.
pub fn activate_feature(feature_id: &Pubkey, authority: &Pubkey) -> Instruction {
let accounts = vec![
AccountMeta::new(*feature_id, false),
AccountMeta::new_readonly(*authority, 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,
authority: &Pubkey,
) -> [Instruction; 2] {
let lamports = Rent::default().minimum_balance(Feature::size_of());
[
system_instruction::transfer(authority, feature_id, lamports),
activate_feature(feature_id, authority),
]
}

/// Creates a 'RevokePendingActivation' instruction.
pub fn revoke_pending_activation(feature: &Pubkey, destination: &Pubkey) -> Instruction {
pub fn revoke_pending_activation(feature_id: &Pubkey, authority: &Pubkey) -> Instruction {
let accounts = vec![
AccountMeta::new(*feature, true),
AccountMeta::new(*destination, false),
AccountMeta::new(*feature_id, false),
AccountMeta::new(*authority, true),
];

let data = FeatureGateInstruction::RevokePendingActivation.pack();
Expand All @@ -70,6 +118,11 @@ mod test {
assert_eq!(instruction, &unpacked);
}

#[test]
fn test_pack_unpack_activate() {
test_pack_unpack(&FeatureGateInstruction::ActivateFeature);
}

#[test]
fn test_pack_unpack_revoke() {
test_pack_unpack(&FeatureGateInstruction::RevokePendingActivation);
Expand Down
1 change: 1 addition & 0 deletions feature-gate/program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#[cfg(not(feature = "no-entrypoint"))]
mod entrypoint;
pub mod error;
pub mod feature_id;
pub mod instruction;
pub mod processor;

Expand Down
58 changes: 51 additions & 7 deletions feature-gate/program/src/processor.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,58 @@
//! Program state processor
use {
crate::{error::FeatureGateError, instruction::FeatureGateInstruction},
crate::{
error::FeatureGateError, feature_id::derive_feature_id, instruction::FeatureGateInstruction,
},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
feature::Feature,
msg,
program::invoke_signed,
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 authority_info = next_account_info(account_info_iter)?;
let _system_program_info = next_account_info(account_info_iter)?;

if !authority_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}

if feature_info.owner != &system_program::id() {
return Err(FeatureGateError::InvalidFeatureAccount.into());
}

let (feature_id, feature_id_bump) = derive_feature_id(authority_info.key)?;

if feature_info.key != &feature_id {
return Err(FeatureGateError::IncorrectFeatureId.into());
}

invoke_signed(
&system_instruction::allocate(feature_info.key, Feature::size_of() as u64),
&[feature_info.clone()],
&[&[b"feature", authority_info.key.as_ref(), &[feature_id_bump]]],
)?;
invoke_signed(
&system_instruction::assign(feature_info.key, program_id),
&[feature_info.clone()],
&[&[b"feature", authority_info.key.as_ref(), &[feature_id_bump]]],
)?;

Ok(())
}

/// Processes an [RevokePendingActivation](enum.FeatureGateInstruction.html)
/// instruction.
pub fn process_revoke_pending_activation(
Expand All @@ -22,9 +62,9 @@ pub fn process_revoke_pending_activation(
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)?;
let authority_info = next_account_info(account_info_iter)?;

if !feature_info.is_signer {
if !authority_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}

Expand All @@ -39,13 +79,13 @@ pub fn process_revoke_pending_activation(
return Err(FeatureGateError::FeatureAlreadyActivated.into());
}

let new_destination_lamports = feature_info
let new_authority_lamports = feature_info
.lamports()
.checked_add(destination_info.lamports())
.checked_add(authority_info.lamports())
.ok_or::<ProgramError>(FeatureGateError::Overflow.into())?;

**feature_info.try_borrow_mut_lamports()? = 0;
**destination_info.try_borrow_mut_lamports()? = new_destination_lamports;
**authority_info.try_borrow_mut_lamports()? = new_authority_lamports;

feature_info.realloc(0, true)?;
feature_info.assign(&system_program::id());
Expand All @@ -57,6 +97,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)
Expand Down
Loading

0 comments on commit af10219

Please sign in to comment.