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 18, 2023
1 parent f202fdb commit e1481be
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 15 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
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,
}
51 changes: 51 additions & 0 deletions feature-gate/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,32 @@
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+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
Expand Down Expand Up @@ -44,6 +60,36 @@ 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_id: &Pubkey, destination: &Pubkey) -> Instruction {
let accounts = vec![
Expand All @@ -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);
Expand Down
35 changes: 34 additions & 1 deletion feature-gate/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 a [RevokePendingActivation](enum.FeatureGateInstruction.html)
/// instruction.
pub fn process_revoke_pending_activation(
Expand Down Expand Up @@ -54,6 +83,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
159 changes: 146 additions & 13 deletions feature-gate/program/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit e1481be

Please sign in to comment.