Prechádzať zdrojové kódy

lang: Add ProgramData account (#1095)

Paul 3 rokov pred
rodič
commit
3321a3f9c9

+ 2 - 0
.github/actions/setup-solana/action.yaml

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

+ 42 - 0
.github/workflows/tests.yaml

@@ -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 }}

+ 1 - 0
CHANGELOG.md

@@ -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

+ 4 - 3
Cargo.lock

@@ -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",

+ 1 - 0
lang/Cargo.toml

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

+ 82 - 0
lang/src/bpf_upgradeable_state.rs

@@ -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)
+    }
+}

+ 2 - 0
lang/src/error.rs

@@ -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")]

+ 6 - 3
lang/src/lib.rs

@@ -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,
     };
 

+ 3 - 5
lang/syn/src/codegen/program/handlers.rs

@@ -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();

+ 8 - 0
lang/syn/src/lib.rs

@@ -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)]

+ 2 - 0
lang/syn/src/parser/accounts/mod.rs

@@ -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")),
     };
 

+ 1 - 0
tests/bpf-upgradeable-state/.gitignore

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

+ 12 - 0
tests/bpf-upgradeable-state/Anchor.toml

@@ -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"

+ 4 - 0
tests/bpf-upgradeable-state/Cargo.toml

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

+ 1 - 0
tests/bpf-upgradeable-state/bpf_upgradeable_state-keypair.json

@@ -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]

+ 12 - 0
tests/bpf-upgradeable-state/migrations/deploy.ts

@@ -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.
+}

+ 12 - 0
tests/bpf-upgradeable-state/package.json

@@ -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"
+    }
+}

+ 1 - 0
tests/bpf-upgradeable-state/program_with_different_programdata.json

@@ -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]

+ 18 - 0
tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/Cargo.toml

@@ -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" }

+ 2 - 0
tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/Xargo.toml

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

+ 53 - 0
tests/bpf-upgradeable-state/programs/bpf-upgradeable-state/src/lib.rs

@@ -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>,
+}

+ 125 - 0
tests/bpf-upgradeable-state/tests/bpf-upgradable-state.ts

@@ -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);
+    }
+  });
+});

+ 10 - 0
tests/bpf-upgradeable-state/tsconfig.json

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

+ 4 - 4
tests/yarn.lock

@@ -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"

+ 5 - 0
ts/src/error.ts

@@ -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.
   [