lang: Add ProgramData account (#1095)

This commit is contained in:
Paul 2021-12-05 20:14:16 +01:00 committed by GitHub
parent 517838e494
commit 3321a3f9c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 413 additions and 15 deletions

View File

@ -17,3 +17,5 @@ runs:
shell: bash
- run: solana-keygen new --no-bip39-passphrase
shell: bash
- run: solana config set --url localhost
shell: bash

View File

@ -179,6 +179,48 @@ jobs:
- uses: ./.github/actions/setup-solana/
- run: cd client/example && ./run-test.sh
test-bpf-upgradeable-state:
needs: setup-anchor-cli
name: Test tests/bpf-upgradeable-state
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/setup/
- uses: ./.github/actions/setup-ts/
- uses: ./.github/actions/setup-solana/
- uses: actions/cache@v2
name: Cache Cargo registry + index
id: cache-anchor
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
./target/
key: cargo-${{ runner.os }}-anchor-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/download-artifact@v2
with:
name: anchor-binary
path: ~/.cargo/bin/
- uses: actions/cache@v2
name: Cache tests/bpf-upgradeable-state target
id: cache-test-target
with:
path: tests/bpf-upgradeable-state/target
key: cargo-${{ runner.os }}-tests/bpf-upgradeable-state-${{ env.ANCHOR_VERSION }}
- run: solana-test-validator -r --quiet &
name: start validator
- run: cd tests/bpf-upgradeable-state && yarn
- run: cd tests/bpf-upgradeable-state && yarn link @project-serum/anchor
- run: cd tests/bpf-upgradeable-state && anchor build
- run: cd tests/bpf-upgradeable-state && solana program deploy --program-id program_with_different_programdata.json target/deploy/bpf_upgradeable_state.so
- run: cd tests/bpf-upgradeable-state && cp bpf_upgradeable_state-keypair.json target/deploy/bpf_upgradeable_state-keypair.json && anchor deploy && anchor test --skip-deploy --skip-build
test-programs:
needs: setup-anchor-cli
name: Test ${{ matrix.node.path }}

View File

@ -21,6 +21,7 @@ incremented for features.
* lang: Add `ErrorCode::AccountNotInitialized` error to separate the situation when the account has the wrong owner from when it does not exist (#[1024](https://github.com/project-serum/anchor/pull/1024))
* lang: Called instructions now log their name by default. This can be turned off with the `no-log-ix-name` flag ([#1057](https://github.com/project-serum/anchor/pull/1057))
* lang: `ProgramData` and `UpgradableLoaderState` can now be passed into `Account` as generics. see [UpgradeableLoaderState](https://docs.rs/solana-program/latest/solana_program/bpf_loader_upgradeable/enum.UpgradeableLoaderState.html). `UpgradableLoaderState` can also be matched on to get `ProgramData`, but when `ProgramData` is used instead, anchor does the serialization and checking that it is actually program data for you ([#1095](https://github.com/project-serum/anchor/pull/1095))
* ts: Add better error msgs in the ts client if something wrong (i.e. not a pubkey or a string) is passed in as an account in an instruction accounts object ([#1098](https://github.com/project-serum/anchor/pull/1098))
## [0.18.2] - 2021-11-14

7
Cargo.lock generated
View File

@ -81,11 +81,11 @@ dependencies = [
[[package]]
name = "anchor-attribute-constant"
version = "0.18.0"
version = "0.18.2"
dependencies = [
"anchor-syn",
"proc-macro2 1.0.29",
"syn 1.0.75",
"proc-macro2 1.0.32",
"syn 1.0.81",
]
[[package]]
@ -213,6 +213,7 @@ dependencies = [
"anchor-attribute-state",
"anchor-derive-accounts",
"base64 0.13.0",
"bincode",
"borsh",
"bytemuck",
"solana-program",

View File

@ -38,3 +38,4 @@ borsh = "0.9"
bytemuck = "1.4.0"
solana-program = "1.8.0"
thiserror = "1.0.20"
bincode = "1.3.3"

View File

@ -0,0 +1,82 @@
use crate::{AccountDeserialize, AccountSerialize, Owner};
use solana_program::{
bpf_loader_upgradeable::UpgradeableLoaderState, program_error::ProgramError, pubkey::Pubkey,
};
#[derive(Clone)]
pub struct ProgramData {
pub slot: u64,
pub upgrade_authority_address: Option<Pubkey>,
}
impl AccountDeserialize for ProgramData {
fn try_deserialize(
buf: &mut &[u8],
) -> Result<Self, solana_program::program_error::ProgramError> {
ProgramData::try_deserialize_unchecked(buf)
}
fn try_deserialize_unchecked(
buf: &mut &[u8],
) -> Result<Self, solana_program::program_error::ProgramError> {
let program_state = AccountDeserialize::try_deserialize_unchecked(buf)?;
match program_state {
UpgradeableLoaderState::Uninitialized => {
Err(anchor_lang::error::ErrorCode::AccountNotProgramData.into())
}
UpgradeableLoaderState::Buffer {
authority_address: _,
} => Err(anchor_lang::error::ErrorCode::AccountNotProgramData.into()),
UpgradeableLoaderState::Program {
programdata_address: _,
} => Err(anchor_lang::error::ErrorCode::AccountNotProgramData.into()),
UpgradeableLoaderState::ProgramData {
slot,
upgrade_authority_address,
} => Ok(ProgramData {
slot,
upgrade_authority_address,
}),
}
}
}
impl AccountSerialize for ProgramData {
fn try_serialize<W: std::io::Write>(
&self,
_writer: &mut W,
) -> Result<(), solana_program::program_error::ProgramError> {
// no-op
Ok(())
}
}
impl Owner for ProgramData {
fn owner() -> solana_program::pubkey::Pubkey {
anchor_lang::solana_program::bpf_loader_upgradeable::ID
}
}
impl Owner for UpgradeableLoaderState {
fn owner() -> Pubkey {
anchor_lang::solana_program::bpf_loader_upgradeable::ID
}
}
impl AccountSerialize for UpgradeableLoaderState {
fn try_serialize<W: std::io::Write>(&self, _writer: &mut W) -> Result<(), ProgramError> {
// no-op
Ok(())
}
}
impl AccountDeserialize for UpgradeableLoaderState {
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
UpgradeableLoaderState::try_deserialize_unchecked(buf)
}
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> {
bincode::deserialize(buf).map_err(|_| ProgramError::InvalidAccountData)
}
}

View File

@ -76,6 +76,8 @@ pub enum ErrorCode {
AccountNotSystemOwned,
#[msg("The program expected this account to be already initialized")]
AccountNotInitialized,
#[msg("The given account is not a program data account")]
AccountNotProgramData,
// State.
#[msg("The given state account does not have the correct address")]

View File

@ -35,6 +35,7 @@ mod account;
mod account_info;
mod account_meta;
mod boxed;
mod bpf_upgradeable_state;
mod common;
mod context;
mod cpi_account;
@ -56,6 +57,7 @@ mod unchecked_account;
mod vec;
pub use crate::account::Account;
pub use crate::bpf_upgradeable_state::*;
#[doc(hidden)]
#[allow(deprecated)]
pub use crate::context::CpiStateContext;
@ -252,9 +254,10 @@ impl Key for Pubkey {
pub mod prelude {
pub use super::{
access_control, account, constant, declare_id, emit, error, event, interface, program,
require, state, zero_copy, Account, AccountDeserialize, AccountLoader, AccountSerialize,
Accounts, AccountsExit, AnchorDeserialize, AnchorSerialize, Context, CpiContext, Id, Key,
Owner, Program, Signer, System, SystemAccount, Sysvar, ToAccountInfo, ToAccountInfos,
require, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, state, zero_copy,
Account, AccountDeserialize, AccountLoader, AccountSerialize, Accounts, AccountsExit,
AnchorDeserialize, AnchorSerialize, Context, CpiContext, Id, Key, Owner, Program,
ProgramData, Signer, System, SystemAccount, Sysvar, ToAccountInfo, ToAccountInfos,
ToAccountMetas, UncheckedAccount,
};

View File

@ -486,11 +486,9 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
.methods
.iter()
.map(|ix| {
if state.is_zero_copy {
// Easy to implement. Just need to write a test.
// Feel free to open a PR.
panic!("Trait implementations not yet implemented for zero copy state structs. Please file an issue.");
}
// Easy to implement. Just need to write a test.
// Feel free to open a PR.
assert!(!state.is_zero_copy, "Trait implementations not yet implemented for zero copy state structs. Please file an issue.");
let ix_arg_names: Vec<&syn::Ident> =
ix.args.iter().map(|arg| &arg.name).collect();

View File

@ -184,6 +184,9 @@ impl Field {
Ty::Signer => quote! {
Signer
},
Ty::ProgramData => quote! {
ProgramData
},
Ty::SystemAccount => quote! {
SystemAccount
},
@ -298,6 +301,7 @@ impl Field {
Ty::UncheckedAccount => quote! {},
Ty::Signer => quote! {},
Ty::SystemAccount => quote! {},
Ty::ProgramData => quote! {},
}
}
@ -316,6 +320,9 @@ impl Field {
Ty::SystemAccount => quote! {
SystemAccount
},
Ty::ProgramData => quote! {
ProgramData
},
Ty::ProgramAccount(ty) => {
let ident = &ty.account_type_path;
quote! {
@ -405,6 +412,7 @@ pub enum Ty {
Program(ProgramTy),
Signer,
SystemAccount,
ProgramData,
}
#[derive(Debug, PartialEq)]

View File

@ -79,6 +79,7 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult<bool> {
| "Program"
| "Signer"
| "SystemAccount"
| "ProgramData"
);
Ok(r)
}
@ -102,6 +103,7 @@ fn parse_ty(f: &syn::Field) -> ParseResult<Ty> {
"Program" => Ty::Program(parse_program_ty(&path)?),
"Signer" => Ty::Signer,
"SystemAccount" => Ty::SystemAccount,
"ProgramData" => Ty::ProgramData,
_ => return Err(ParseError::new(f.ty.span(), "invalid account type given")),
};

View File

@ -0,0 +1 @@
yarn.lock

View File

@ -0,0 +1,12 @@
[programs.localnet]
bpf_upgradeable_state = "Cum9tTyj5HwcEiAmhgaS7Bbj4UczCwsucrCkxRECzM4e"
[registry]
url = "https://anchor.projectserum.com"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

View File

@ -0,0 +1 @@
[114,99,192,17,48,208,90,184,231,46,220,91,47,115,132,253,218,163,228,101,8,121,220,138,41,140,176,127,254,91,51,28,176,244,174,182,223,57,57,125,117,201,31,213,9,39,207,212,100,173,88,252,61,235,89,156,53,86,4,90,16,251,191,219]

View File

@ -0,0 +1,12 @@
// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.
const anchor = require("@project-serum/anchor");
module.exports = async function (provider) {
// Configure client to use the provider.
anchor.setProvider(provider);
// Add your deploy script here.
}

View File

@ -0,0 +1,12 @@
{
"dependencies": {
"@project-serum/anchor": "^0.18.2"
},
"devDependencies": {
"chai": "^4.3.4",
"mocha": "^9.0.3",
"ts-mocha": "^8.0.0",
"@types/mocha": "^9.0.0",
"typescript": "^4.3.5"
}
}

View File

@ -0,0 +1 @@
[86,234,116,86,82,140,116,250,254,32,75,217,35,39,9,238,39,98,242,254,25,216,201,66,1,239,93,12,81,19,34,108,219,67,158,98,245,234,81,126,228,157,205,206,130,5,14,54,1,21,88,246,128,124,240,93,157,49,102,19,253,19,205,178]

View File

@ -0,0 +1,18 @@
[package]
name = "bpf-upgradeable-state"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "bpf_upgradeable_state"
[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = { path = "../../../../lang" }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,53 @@
use anchor_lang::prelude::*;
declare_id!("Cum9tTyj5HwcEiAmhgaS7Bbj4UczCwsucrCkxRECzM4e");
// TODO: Once anchor can deserialize data of programs (=programdata_address) automatically, add another test to this file.
// Instead of using UpgradeableLoaderState, it should use Program<'info, MY_PROGRAM>
#[program]
pub mod bpf_upgradeable_state {
use super::*;
pub fn set_admin_settings(ctx: Context<SetAdminSettings>, admin_data: u64) -> ProgramResult {
match *ctx.accounts.program {
UpgradeableLoaderState::Program {
programdata_address,
} => {
if programdata_address != ctx.accounts.program_data.key() {
return Err(CustomError::InvalidProgramDataAddress.into());
}
}
_ => {
return Err(CustomError::AccountNotProgram.into());
}
};
ctx.accounts.settings.admin_data = admin_data;
Ok(())
}
}
#[account]
#[derive(Default, Debug)]
pub struct Settings {
admin_data: u64,
}
#[error]
pub enum CustomError {
InvalidProgramDataAddress,
AccountNotProgram,
}
#[derive(Accounts)]
#[instruction(admin_data: u64)]
pub struct SetAdminSettings<'info> {
#[account(init, payer = authority)]
pub settings: Account<'info, Settings>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(address = crate::ID)]
pub program: Account<'info, UpgradeableLoaderState>,
#[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
pub program_data: Account<'info, ProgramData>,
pub system_program: Program<'info, System>,
}

View File

@ -0,0 +1,125 @@
import * as anchor from '@project-serum/anchor';
import { Program } from '@project-serum/anchor';
import { findProgramAddressSync } from '@project-serum/anchor/dist/cjs/utils/pubkey';
import { PublicKey } from '@solana/web3.js';
import assert from 'assert';
import { BpfUpgradeableState } from '../target/types/bpf_upgradeable_state';
describe('bpf_upgradeable_state', () => {
const provider = anchor.Provider.env();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
const program = anchor.workspace.BpfUpgradeableState as Program<BpfUpgradeableState>;
const programDataAddress = findProgramAddressSync(
[program.programId.toBytes()],
new anchor.web3.PublicKey("BPFLoaderUpgradeab1e11111111111111111111111")
)[0];
it('Reads ProgramData and sets field', async () => {
const settings = anchor.web3.Keypair.generate();
const tx = await program.rpc.setAdminSettings(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);
console.log("Your transaction signature", tx);
});
it('Validates constraint on ProgramData', async () => {
const settings = anchor.web3.Keypair.generate();
try {
const authority = anchor.web3.Keypair.generate();
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(authority.publicKey, 10000000000),
"confirmed"
);
await program.rpc.setAdminSettings(new anchor.BN(500), {
accounts: {
authority: authority.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
programData: programDataAddress,
settings: settings.publicKey,
program: program.programId,
},
signers: [settings, authority]
});
assert.ok(false);
} catch (err) {
assert.equal(err.code, 143);
assert.equal(err.msg, "A raw constraint was violated");
}
});
it('Validates that account is ProgramData', async () => {
const settings = anchor.web3.Keypair.generate();
try {
await program.rpc.setAdminSettings(new anchor.BN(500), {
accounts: {
authority: program.provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
programData: program.programId,
settings: settings.publicKey,
program: program.programId,
},
signers: [settings]
});
assert.ok(false);
} catch (err) {
assert.equal(err.code, 173);
assert.equal(err.msg, "The given account is not a program data account");
}
});
it('Validates that account is owned by the upgradeable bpf loader', async () => {
const settings = anchor.web3.Keypair.generate();
try {
await program.rpc.setAdminSettings(new anchor.BN(500), {
accounts: {
authority: program.provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
programData: program.provider.wallet.publicKey,
settings: settings.publicKey,
program: program.programId,
},
signers: [settings]
});
assert.ok(false);
} catch (err) {
assert.equal(err.code, 167);
assert.equal(err.msg, "The given account is not owned by the executing program");
}
});
it('Deserializes UpgradableLoaderState 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.setAdminSettings(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, 300);
}
});
});

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}

View File

@ -50,10 +50,10 @@
snake-case "^3.0.4"
toml "^3.0.0"
"@project-serum/anchor@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.18.0.tgz#867144282e59482230f797f73ee9f5634f846061"
integrity sha512-WTm+UB93MoxyCbjnHIibv/uUEoO/5gL4GEtE/aMioLF8Z4i0vCMPnvAN0xpk9VBu3t7ld2DcCE/L+6Z7dwU++w==
"@project-serum/anchor@^0.18.2":
version "0.18.2"
resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.18.2.tgz#0f13b5c2046446b7c24cf28763eec90febb28485"
integrity sha512-uyjiN/3Ipp+4hrZRm/hG18HzGLZyvP790LXrCsGO3IWxSl28YRhiGEpKnZycfMW94R7nxdUoE3wY67V+ZHSQBQ==
dependencies:
"@project-serum/borsh" "^0.2.2"
"@solana/web3.js" "^1.17.0"

View File

@ -90,6 +90,7 @@ const LangErrorCode = {
AccountNotSigner: 170,
AccountNotSystemOwned: 171,
AccountNotInitialized: 172,
AccountNotProgramData: 173,
// State.
StateInvalidAddress: 180,
@ -180,6 +181,10 @@ const LangErrorMessage = new Map([
LangErrorCode.AccountNotInitialized,
"The program expected this account to be already initialized",
],
[
LangErrorCode.AccountNotProgramData,
"The given account is not a program data account",
],
// State.
[