Skip to content

(Anchor V2) AnchorProgram Trait API Proposal #4196

@stegaBOB

Description

@stegaBOB

In order to make anchor v2 more modular and interoperable, we can introduce traits that can pull in a lot of the logic that is currently contained in macro generated code. Ideally our macros call into existing functions (trait methods in this case) instead of introducing their own complex logic. This makes the code much easier to audit as well: you can review type-checked and clean "anchor" logic in the rust source code directly instead of having to review the generated macro expansion.

This issue will focus on the flow from the entrypoint through to instruction dispatch. Additional issues will introduce traits for the rest of the anchor program lifecycle.

The AnchorProgram Trait

/// The main trait for anchor programs that gets automatically implemented by the `#[program]` macro.
/// This trait provides the program ID and error handling functionality.
///
/// This takes inspiration from `star_frame`'s `StarFrameProgram` and `InstructionSet` traits.
pub trait AnchorProgram {
    /// Having the ID as an associated const on the program firstly enables the ID check in
    /// [`Self::entrypoint`], but will also enable other traits that have `AnchorProgram` associated
    /// types to access that ID. For example, if we had an AnchorAccount trait that belongs to an
    /// AnchorProgram (through an associated type), we could use this ID to check if the account is
    /// owned by the program. Among other things, this can also be useful when generating an IDL.
    ///
    /// This changes the way users declare their program IDs. Instead of using the `declare_id!` macro,
    /// they could either implement the trait manually or our macro could either require an ID constant
    /// declared in the program module or take it in with a derive helper attribute
    const ID: Pubkey;

    /// If the user wants to implement their own error handling logic, they can just override this method.
    fn handle_error(
        err: anchor_lang::error::Error,
    ) -> anchor_lang::solana_program::program_error::ProgramError {
        err.log();
        err.into()
    }

    /// By having this entrypoint as a default implementation, the entrypoint call can use this directly!
    /// ```rs
    /// #[cfg(not(feature = "no-entrypoint"))]
    /// anchor_lang::solana_program::entrypoint!(<MyProgram as AnchorProgram>::entrypoint);
    /// ```
    ///
    /// We also no longer have to generate the ID check!
    fn entrypoint<'info>(
        program_id: &Pubkey,
        accounts: &[AccountInfo<'info>],
        data: &[u8],
    ) -> ProgramResult {
        if *program_id != Self::ID {
            return Err(anchor_lang::error::ErrorCode::DeclaredProgramIdMismatch.into())
                .map_err(Self::handle_error);
        }
        Self::dispatch(program_id, accounts, data).map_err(Self::handle_error)
    }

    /// This method will be generated by the #[program] macro to figure out which instruction to call.
    fn dispatch<'info>(
        program_id: &Pubkey,
        accounts: &[AccountInfo<'info>],
        data: &[u8],
    ) -> Result<()>;
}

Potential program module syntax and generated code

This would be how a basic program could be defined (the only change with current anchor is
that added ID const).

#[program]
mod basic_program {
    use super::*;
    const ID: Pubkey = pubkey!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

    pub fn initialize(_ctx: Context<Initialize>, _value: u8) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

This could then expand to something like the following:

// If we want to keep the program module structure we can make this just be a unit struct that implements AnchorProgram,
// but we could also accomplish all the macro shenanigans by making a user derive AnchorProgram on an enum of instructions.
// That is currently what star_frame does (with some added indirection through a separate InstructionSet trait which I no longer
// believe is necessary, hence the lack of one here)
pub struct BasicProgram;

impl AnchorProgram for BasicProgram {
    const ID: Pubkey = pubkey!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

    fn dispatch(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> Result<()> {
        if data.starts_with(instruction::Initialize::DISCRIMINATOR) {
            // This should be different with a trait for instructions themselves as well
            return __private::__global::initialize(
                program_id,
                accounts,
                &data[instruction::Initialize::DISCRIMINATOR.len()..],
            );
        }
        if data.starts_with(anchor_lang::event::EVENT_IX_TAG_LE) {
            return Err(anchor_lang::error::ErrorCode::EventInstructionStub.into());
        }
        Err(anchor_lang::error::ErrorCode::InstructionFallbackNotFound.into())
    }
}

pub mod instruction {
    use super::*;
    /// Instruction
    #[derive(AnchorSerialize, AnchorDeserialize)]
    pub struct Initialize {
        pub value: u8,
    }

    // An instruction trait that connects the instruction to it's Accounts would simplify a TON!

    // ---- currently auto-implemented anchor stuff below ----
    impl anchor_lang::Discriminator for Initialize {
        const DISCRIMINATOR: &'static [u8] = &[175, 175, 109, 31, 13, 152, 155, 237];
    }
    impl anchor_lang::InstructionData for Initialize {}

    impl anchor_lang::Owner for Initialize {
        fn owner() -> Pubkey {
            ID
        }
    }
}

// No more entrypoint boilerplate! A user can directly follow the flow of their programs by just clicking through the default 
#[cfg(not(feature = "no-entrypoint"))]
anchor_lang::solana_program::entrypoint!(<BasicProgram as AnchorProgram>::entrypoint);


// Plus the existing CPI stuff that already gets generated. A future issue will discuss some trait based alternatives!

Some questions:

  • From a macro perspective, forming that main dispatch method only requires knowing the instruction struct. Do we want to allow users to give their own structs to be included in the match (which would enable it to be included in the IDL as well if we generate that through a trait)?

  • Do we just replace the program macro with an enum of instructions altogether (or perhaps allow a user to do either or)?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions