spherenet-program-whitelist-interface 0.3.0

Interface of SphereNet's Program Whitelist Program
Documentation
//! Onchain functionality for validating deploy authorities (solana-program side).
//!
//! This is the gate the client's upgradeable loader calls at deploy/upgrade time
//! instead of hand-reading the whitelist entry inline. It mirrors vw's
//! `validate_vote_account_solana_program`: re-derive the entry PDA, verify the
//! passed account, and check the entry is valid for the deployer.

use crate::{
    error::ProgramWhitelistError,
    ids::{
        account_solana::ID as WHITELIST_ACCOUNT_ID,
        program_solana::ID as WHITELIST_PROGRAM_ID,
    },
    state::{load, whitelist_entry::ProgramWhitelistEntry},
};

use {
    solana_instruction::error::InstructionError as SolanaInstructionError,
    solana_pubkey::Pubkey as SolanaPubkey,
};

/// Information about a program-whitelist entry account.
pub struct WhitelistEntryInformation<'a> {
    /// The key of the whitelist entry account.
    pub key: &'a SolanaPubkey,
    /// The owner of the whitelist entry account.
    pub owner: &'a SolanaPubkey,
    /// The data of the whitelist entry account.
    pub data: &'a [u8],
}

/// Gate a program deploy/upgrade on the deployer's program-whitelist entry.
///
/// Re-derives the entry PDA from the constant whitelist account id + the deployer
/// authority (so no whitelist-account argument is needed — the derivation uses
/// `account::id()`), verifies the passed entry account matches and is owned by
/// the program-whitelist program, and that it is a valid entry for that authority.
///
/// Gates on **approval**, not mere existence: `load` succeeds for any non-
/// `Uninitialized` state, so a `Pending` request (initialized but awaiting
/// authority approval) loads fine — the explicit `is_approved()` check below is
/// what withholds deploy access until the entry reaches `Approved`.
pub fn validate_deploy_authority_solana_program(
    entry: WhitelistEntryInformation,
    deploy_authority: &SolanaPubkey,
) -> Result<(), SolanaInstructionError> {
    let (pda, _) = SolanaPubkey::find_program_address(
        &[WHITELIST_ACCOUNT_ID.as_ref(), deploy_authority.as_ref()],
        &WHITELIST_PROGRAM_ID,
    );

    // Same conditions and codes as the on-chain entry door: identity then owner,
    // each with its descriptive custom code (not framework InvalidSeeds/
    // InvalidAccountOwner), so a given failure maps to one code across both surfaces.
    if &pda != entry.key {
        return Err(to_solana_instruction_error(
            ProgramWhitelistError::InvalidProgramWhitelistEntryAccount,
        ));
    }

    if entry.owner != &WHITELIST_PROGRAM_ID {
        return Err(to_solana_instruction_error(
            ProgramWhitelistError::InvalidProgramWhitelistEntryAccountOwner,
        ));
    }

    let entry_data = load::<ProgramWhitelistEntry>(entry.data)
        .map_err(|_| SolanaInstructionError::InvalidAccountData)?;

    // Redundant with the PDA derivation (the entry is seeded by the authority),
    // but kept as an explicit belt-and-suspenders check, matching the former
    // inline loader gate.
    if entry_data.entry_address != deploy_authority.to_bytes() {
        return Err(SolanaInstructionError::InvalidAccountData);
    }

    // A `Pending` entry loads fine (it's initialized) but is not yet approved —
    // gate on approval, not just existence.
    if !entry_data.is_approved() {
        return Err(to_solana_instruction_error(
            ProgramWhitelistError::DeployerNotApproved,
        ));
    }

    Ok(())
}

/// Map a `ProgramWhitelistError` into a Solana `InstructionError` for the
/// loader-side gate, mirroring vw's `to_solana_instruction_error`.
pub fn to_solana_instruction_error(
    error: ProgramWhitelistError,
) -> SolanaInstructionError {
    SolanaInstructionError::Custom(error as u32)
}

#[cfg(test)]
mod tests {
    use {
        super::*,
        crate::state::{
            whitelist_entry::ProgramWhitelistEntry, EntryState, SizeOf,
        },
    };

    fn entry_pda(deploy_authority: &SolanaPubkey) -> SolanaPubkey {
        SolanaPubkey::find_program_address(
            &[WHITELIST_ACCOUNT_ID.as_ref(), deploy_authority.as_ref()],
            &WHITELIST_PROGRAM_ID,
        )
        .0
    }

    fn entry_bytes(
        deploy_authority: &SolanaPubkey,
        state: EntryState,
    ) -> [u8; ProgramWhitelistEntry::SIZE_OF] {
        let mut data = [0u8; ProgramWhitelistEntry::SIZE_OF];
        data[..32].copy_from_slice(deploy_authority.as_ref());
        data[32] = state as u8;
        data
    }

    /// The deploy gate the loader calls must BLOCK a `Pending` entry
    /// (`DeployerNotApproved`) and pass only once the entry is `Approved`. This
    /// is the actual gate function — not just the `is_approved()` predicate.
    #[test]
    fn deploy_gate_blocks_pending_allows_approved() {
        let deploy_authority = SolanaPubkey::new_from_array([7u8; 32]);
        let key = entry_pda(&deploy_authority);

        // Pending -> deploy blocked.
        let pending = entry_bytes(&deploy_authority, EntryState::Pending);
        let err = validate_deploy_authority_solana_program(
            WhitelistEntryInformation {
                key: &key,
                owner: &WHITELIST_PROGRAM_ID,
                data: &pending,
            },
            &deploy_authority,
        )
        .unwrap_err();
        assert_eq!(
            err,
            SolanaInstructionError::Custom(
                ProgramWhitelistError::DeployerNotApproved as u32
            ),
        );

        // Approved -> deploy allowed.
        let approved = entry_bytes(&deploy_authority, EntryState::Approved);
        validate_deploy_authority_solana_program(
            WhitelistEntryInformation {
                key: &key,
                owner: &WHITELIST_PROGRAM_ID,
                data: &approved,
            },
            &deploy_authority,
        )
        .expect("approved entry must pass the deploy gate");
    }

    #[test]
    fn deploy_gate_rejects_wrong_entry_pda() {
        let deploy_authority = SolanaPubkey::new_from_array([7u8; 32]);
        let wrong_key = SolanaPubkey::new_from_array([9u8; 32]);
        let approved = entry_bytes(&deploy_authority, EntryState::Approved);

        let err = validate_deploy_authority_solana_program(
            WhitelistEntryInformation {
                key: &wrong_key,
                owner: &WHITELIST_PROGRAM_ID,
                data: &approved,
            },
            &deploy_authority,
        )
        .unwrap_err();

        assert_eq!(
            err,
            SolanaInstructionError::Custom(
                ProgramWhitelistError::InvalidProgramWhitelistEntryAccount as u32
            ),
        );
    }

    #[test]
    fn deploy_gate_rejects_foreign_owner() {
        let deploy_authority = SolanaPubkey::new_from_array([7u8; 32]);
        let key = entry_pda(&deploy_authority);
        let approved = entry_bytes(&deploy_authority, EntryState::Approved);
        let foreign_owner = SolanaPubkey::new_from_array([2u8; 32]);

        let err = validate_deploy_authority_solana_program(
            WhitelistEntryInformation {
                key: &key,
                owner: &foreign_owner,
                data: &approved,
            },
            &deploy_authority,
        )
        .unwrap_err();

        assert_eq!(
            err,
            SolanaInstructionError::Custom(
                ProgramWhitelistError::InvalidProgramWhitelistEntryAccountOwner as u32
            ),
        );
    }

    #[test]
    fn deploy_gate_rejects_malformed_data() {
        let deploy_authority = SolanaPubkey::new_from_array([7u8; 32]);
        let key = entry_pda(&deploy_authority);
        // Wrong length — `load` fails its size check.
        let too_short = [0u8; 8];

        let err = validate_deploy_authority_solana_program(
            WhitelistEntryInformation {
                key: &key,
                owner: &WHITELIST_PROGRAM_ID,
                data: &too_short,
            },
            &deploy_authority,
        )
        .unwrap_err();

        assert_eq!(err, SolanaInstructionError::InvalidAccountData);
    }

    #[test]
    fn deploy_gate_rejects_entry_address_mismatch() {
        let deploy_authority = SolanaPubkey::new_from_array([7u8; 32]);
        let key = entry_pda(&deploy_authority);
        let other_authority = SolanaPubkey::new_from_array([8u8; 32]);
        // Correct PDA + owner + approved, but the stored entry_address is a
        // different authority — the belt-and-suspenders check rejects it.
        let mismatched = entry_bytes(&other_authority, EntryState::Approved);

        let err = validate_deploy_authority_solana_program(
            WhitelistEntryInformation {
                key: &key,
                owner: &WHITELIST_PROGRAM_ID,
                data: &mismatched,
            },
            &deploy_authority,
        )
        .unwrap_err();

        assert_eq!(err, SolanaInstructionError::InvalidAccountData);
    }
}