Skip to content

Commit

Permalink
feature gate: init program
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec committed Oct 15, 2023
1 parent 4a8fead commit 3411e13
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions feature-gate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "spl-feature-gate"
version = "0.1.0"
description = "Solana Program Library Feature Gate Program"
authors = ["Solana Labs Maintainers <[email protected]>"]
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"]
10 changes: 10 additions & 0 deletions feature-gate/README.md
Original file line number Diff line number Diff line change
@@ -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)!
17 changes: 17 additions & 0 deletions feature-gate/src/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 14 additions & 0 deletions feature-gate/src/error.rs
Original file line number Diff line number Diff line change
@@ -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,
}
77 changes: 77 additions & 0 deletions feature-gate/src/instruction.rs
Original file line number Diff line number Diff line change
@@ -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<Self, ProgramError> {
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<u8> {
vec![self.to_owned().into()]
}
}

/// Creates a 'RevokePendingActivation' instruction.
pub fn revoke(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);
}
}
16 changes: 16 additions & 0 deletions feature-gate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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");
61 changes: 61 additions & 0 deletions feature-gate/src/processor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! 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 [revoke](enum.FeatureGateInstruction.html) instruction.
pub fn process_revoke(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::<ProgramError>(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(program_id, accounts)
}
}
}
140 changes: 140 additions & 0 deletions feature-gate/tests/functional.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// #![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},
};

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() {
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(&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(&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(&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);
}

0 comments on commit 3411e13

Please sign in to comment.