-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4a8fead
commit 3411e13
Showing
9 changed files
with
363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |