Skip to content
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

feature gate: add activate instruction #5541

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions feature-gate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

This program serves to manage new features on Solana.

It serves one purpose: revoking features that are pending activation.
It serves two main purposes: queuing new features for activation and revoking
features that are already queued and 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)!
[here](https://github.com/solana-foundation/solana-improvement-documents/pull/72)!
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,
}
35 changes: 35 additions & 0 deletions feature-gate/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,26 @@ use {
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
system_program,
},
};

/// Feature Gate program instructions
#[derive(Clone, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum FeatureGateInstruction {
/// Queue a feature for activation by allocating and assigning a feature
/// account.
///
/// Note: This instruction expects the account to be owned by the system
/// program.
///
/// Accounts expected by this instruction:
///
/// 0. `[w+s]` Feature account (must be a system account)
/// 1. `[w+s]` Payer (for rent lamports)
/// 2. `[]` 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 +57,23 @@ impl FeatureGateInstruction {
}
}

/// Creates an 'ActivateFeature' instruction.
pub fn activate_feature(feature_id: &Pubkey, payer: &Pubkey) -> Instruction {
let accounts = vec![
AccountMeta::new(*feature_id, true),
AccountMeta::new(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
];

let data = FeatureGateInstruction::ActivateFeature.pack();

Instruction {
program_id: crate::id(),
accounts,
data,
}
}

/// Creates a 'RevokePendingActivation' instruction.
pub fn revoke_pending_activation(feature_id: &Pubkey, destination: &Pubkey) -> Instruction {
let accounts = vec![
Expand All @@ -70,6 +100,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
55 changes: 54 additions & 1 deletion feature-gate/program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,61 @@ use {
entrypoint::ProgramResult,
feature::Feature,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_program,
rent::Rent,
system_instruction, system_program,
sysvar::Sysvar,
},
};

/// 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 payer_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());
}

buffalojoec marked this conversation as resolved.
Show resolved Hide resolved
let rent = Rent::get()?;
let space = Feature::size_of() as u64;

// Just in case the account already has some lamports
let required_lamports = rent
.minimum_balance(space as usize)
.max(1)
.saturating_sub(feature_info.lamports());

if required_lamports > 0 {
invoke(
&system_instruction::transfer(payer_info.key, feature_info.key, required_lamports),
&[payer_info.clone(), feature_info.clone()],
)?;
}

invoke(
&system_instruction::allocate(feature_info.key, space),
&[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 +103,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
170 changes: 159 additions & 11 deletions feature-gate/program/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@ 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_program,
transaction::{Transaction, TransactionError},
},
spl_feature_gate::{error::FeatureGateError, instruction::revoke_pending_activation},
spl_feature_gate::{
error::FeatureGateError,
instruction::{activate_feature, 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(
&[activate_feature(
&feature_keypair.pubkey(),
&context.payer.pubkey(),
rent_lamports,
),
)],
Some(&context.payer.pubkey()),
&[&context.payer, feature_keypair],
context.last_blockhash,
Expand All @@ -36,6 +35,155 @@ 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;

// Fail: feature not signer
let mut activate_ix = activate_feature(&feature_keypair.pubkey(), &context.payer.pubkey());
activate_ix.accounts[0].is_signer = false;
let transaction = Transaction::new_signed_with_payer(
&[activate_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: payer not signer
let mut activate_ix = activate_feature(&feature_keypair.pubkey(), &context.payer.pubkey());
activate_ix.accounts[1].is_signer = false;
let transaction = Transaction::new_signed_with_payer(
&[activate_ix],
Some(&mock_invalid_signer.pubkey()),
&[&mock_invalid_signer, &feature_keypair],
context.last_blockhash,
);
let error = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(0, InstructionError::PrivilegeEscalation)
);

// Fail: feature not owned by system program
let transaction = Transaction::new_signed_with_payer(
&[activate_feature(
&mock_invalid_feature.pubkey(),
&context.payer.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(
&[activate_feature(
&feature_keypair.pubkey(),
&context.payer.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 new_latest_blockhash = context.get_new_latest_blockhash().await.unwrap();
let transaction = Transaction::new_signed_with_payer(
&[activate_feature(
&feature_keypair.pubkey(),
&context.payer.pubkey(),
)],
Some(&context.payer.pubkey()),
&[&context.payer, &feature_keypair],
new_latest_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 +214,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
Loading