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

lang: programdata_address field for Program account #1125

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ incremented for features.

## [Unreleased]

### Features

* lang: Add `programdata_address: Option<Pubkey>` field to `Program` account. Will be populated if account is a program owned by the upgradable bpf loader ([#1125](https://github.com/project-serum/anchor/pull/1125))

## [0.19.0] - 2021-12-08

### Fixes
Expand Down
54 changes: 51 additions & 3 deletions lang/src/program.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
use crate::error::ErrorCode;
use crate::*;
use solana_program::account_info::AccountInfo;
use solana_program::bpf_loader_upgradeable::{self, UpgradeableLoaderState};
use solana_program::instruction::AccountMeta;
use solana_program::program_error::ProgramError;
use solana_program::pubkey::Pubkey;
use std::fmt;
use std::ops::Deref;

// TODO: can we remove _account?
armaniferrante marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Contributor Author

@paul-schaaf paul-schaaf Dec 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@armaniferrante ^

is there any use to this field, considering that it's going to be empty struct all of the time

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the type is needed for the id() function. Though a better way of doing might be with PhantomData. Can consider that for a separate PR though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that sounds better to me. #1140

armaniferrante marked this conversation as resolved.
Show resolved Hide resolved
/// Account container that checks ownership on deserialization.
#[derive(Clone)]
pub struct Program<'info, T: Id + AccountDeserialize + Clone> {
_account: T,
info: AccountInfo<'info>,
programdata_address: Option<Pubkey>,
}

impl<'info, T: Id + AccountDeserialize + Clone + fmt::Debug> fmt::Debug for Program<'info, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Program")
.field("account", &self._account)
.field("info", &self.info)
.field("programdata_address", &self.programdata_address)
.finish()
}
}

impl<'a, T: Id + AccountDeserialize + Clone> Program<'a, T> {
fn new(info: AccountInfo<'a>, _account: T) -> Program<'a, T> {
Self { info, _account }
fn new(
info: AccountInfo<'a>,
_account: T,
programdata_address: Option<Pubkey>,
) -> Program<'a, T> {
Self {
info,
_account,
programdata_address,
}
}

/// Deserializes the given `info` into a `Program`.
Expand All @@ -37,9 +50,44 @@ impl<'a, T: Id + AccountDeserialize + Clone> Program<'a, T> {
if !info.executable {
return Err(ErrorCode::InvalidProgramExecutable.into());
}
let programdata_address = if *info.owner == bpf_loader_upgradeable::ID {
let mut data: &[u8] = &info.try_borrow_data()?;
let upgradable_loader_state =
UpgradeableLoaderState::try_deserialize_unchecked(&mut data)?;

match upgradable_loader_state {
UpgradeableLoaderState::Uninitialized
| UpgradeableLoaderState::Buffer {
authority_address: _,
}
| UpgradeableLoaderState::ProgramData {
slot: _,
upgrade_authority_address: _,
} => {
// unreachable because check above already
armaniferrante marked this conversation as resolved.
Show resolved Hide resolved
// ensures that program is executable
// and therefore a program account
armaniferrante marked this conversation as resolved.
Show resolved Hide resolved
unreachable!()
}
UpgradeableLoaderState::Program {
programdata_address,
} => Some(programdata_address),
}
} else {
None
};

// Programs have no data so use an empty slice.
let mut empty = &[][..];
Ok(Program::new(info.clone(), T::try_deserialize(&mut empty)?))
Ok(Program::new(
info.clone(),
T::try_deserialize(&mut empty)?,
programdata_address,
))
}

pub fn programdata_address(&self) -> Option<Pubkey> {
self.programdata_address
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ pub mod bpf_upgradeable_state {
ctx.accounts.settings.admin_data = admin_data;
Ok(())
}

pub fn set_admin_settings_use_program_state(
ctx: Context<SetAdminSettingsUseProgramState>,
admin_data: u64,
) -> ProgramResult {
ctx.accounts.settings.admin_data = admin_data;
Ok(())
}
}

#[account]
Expand All @@ -36,6 +44,7 @@ pub struct Settings {
pub enum CustomError {
InvalidProgramDataAddress,
AccountNotProgram,
AccountNotBpfUpgradableProgram,
}

#[derive(Accounts)]
Expand All @@ -51,3 +60,17 @@ pub struct SetAdminSettings<'info> {
pub program_data: Account<'info, ProgramData>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
#[instruction(admin_data: u64)]
pub struct SetAdminSettingsUseProgramState<'info> {
#[account(init, payer = authority)]
pub settings: Account<'info, Settings>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(constraint = program.programdata_address() == Some(program_data.key()))]
pub program: Program<'info, crate::program::BpfUpgradeableState>,
#[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
pub program_data: Account<'info, ProgramData>,
pub system_program: Program<'info, System>,
}
40 changes: 39 additions & 1 deletion tests/bpf-upgradeable-state/tests/bpf-upgradable-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,21 @@ describe('bpf_upgradeable_state', () => {
signers: [settings]
});
assert.equal((await program.account.settings.fetch(settings.publicKey)).adminData, 500);
});

console.log("Your transaction signature", tx);
it('Reads ProgramData and sets field, uses program state', async () => {
const settings = anchor.web3.Keypair.generate();
const tx = await program.rpc.setAdminSettingsUseProgramState(new anchor.BN(500), {
accounts: {
authority: program.provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
programData: programDataAddress,
program: program.programId,
settings: settings.publicKey
},
signers: [settings]
});
assert.equal((await program.account.settings.fetch(settings.publicKey)).adminData, 500);
});

it('Validates constraint on ProgramData', async () => {
Expand Down Expand Up @@ -122,4 +135,29 @@ describe('bpf_upgradeable_state', () => {
assert.equal(err.code, 6000);
}
});

it('Deserializes Program and validates that programData is the expected account', async () => {
const secondProgramAddress = new PublicKey("Fkv67TwmbakfZw2PoW57wYPbqNexAH6vuxpyT8vmrc3B");
const secondProgramProgramDataAddress = findProgramAddressSync(
[secondProgramAddress.toBytes()],
new anchor.web3.PublicKey("BPFLoaderUpgradeab1e11111111111111111111111")
)[0];

const settings = anchor.web3.Keypair.generate();
try {
await program.rpc.setAdminSettingsUseProgramState(new anchor.BN(500), {
accounts: {
authority: program.provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
programData: secondProgramProgramDataAddress,
settings: settings.publicKey,
program: program.programId,
},
signers: [settings]
});
assert.ok(false);
} catch (err) {
assert.equal(err.code, 2003);
}
});
});