diff --git a/clients/js/src/utils.ts b/clients/js/src/utils.ts index 6fb0747..2b6f33c 100644 --- a/clients/js/src/utils.ts +++ b/clients/js/src/utils.ts @@ -35,14 +35,8 @@ import { export const ACCOUNT_HEADER_LENGTH = 96; -export const LOADER_V1_PROGRAM_ADDRESS = - 'BPFLoader1111111111111111111111111111111111' as Address<'BPFLoader1111111111111111111111111111111111'>; -export const LOADER_V2_PROGRAM_ADDRESS = - 'BPFLoader2111111111111111111111111111111111' as Address<'BPFLoader2111111111111111111111111111111111'>; export const LOADER_V3_PROGRAM_ADDRESS = 'BPFLoaderUpgradeab1e11111111111111111111111' as Address<'BPFLoaderUpgradeab1e11111111111111111111111'>; -export const LOADER_V4_PROGRAM_ADDRESS = - 'CoreBPFLoaderV41111111111111111111111111111' as Address<'CoreBPFLoaderV41111111111111111111111111111'>; export type MetadataInput = { payer: TransactionSigner; @@ -127,17 +121,16 @@ export async function getProgramAuthority( throw Error('Program account must be executable'); } - // Check all the loader programs. - switch (programAccount.programAddress) { - case LOADER_V1_PROGRAM_ADDRESS: - case LOADER_V2_PROGRAM_ADDRESS: - return { authority: program }; - case LOADER_V3_PROGRAM_ADDRESS: - return await getProgramAuthorityForLoaderV3(rpc, programAccount); - case LOADER_V4_PROGRAM_ADDRESS: - default: - throw new Error('Unsupported loader program: ' + programAccount.programAddress); + // Loader v3 programs store their upgrade authority in a separate program + // data account. + if (programAccount.programAddress === LOADER_V3_PROGRAM_ADDRESS) { + return await getProgramAuthorityForLoaderV3(rpc, programAccount); } + + // For any other owner (loader v1, v2, v4, the native loader, etc.), the + // on-chain program considers the program's own address to be its authority + // — i.e. the program keypair itself must sign to prove canonicity. + return { authority: program }; } async function getProgramAuthorityForLoaderV3(rpc: Rpc, programAccount: EncodedAccount) { diff --git a/clients/js/test/_setup.ts b/clients/js/test/_setup.ts index ba6866d..c5280e5 100644 --- a/clients/js/test/_setup.ts +++ b/clients/js/test/_setup.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { systemProgram } from '@solana-program/system'; import { Address, + address, createClient, generateKeyPairSigner, getAddressEncoder, @@ -10,6 +11,7 @@ import { getStructEncoder, getU32Encoder, getU64Encoder, + getUtf8Encoder, KeyPairSigner, lamports, Lamports, @@ -133,3 +135,28 @@ export const createDeployedProgram = async ( return [program.address, programData]; }; + +export const NATIVE_LOADER_PROGRAM_ADDRESS = address('NativeLoader1111111111111111111111111111111'); + +/** + * Fabricates a native (built-in) program inside the LiteSVM instance — i.e. an + * executable account owned by the native loader, like `ComputeBudget111...`. + * The program is never actually invoked — only its account is read by the + * client for the canonicity check. + */ +export const createNativeProgram = async (client: TestClient): Promise
=> { + const program = await generateKeyPairSigner(); + const data = getUtf8Encoder().encode('native_program'); + const space = BigInt(data.length); + + client.svm.setAccount({ + address: program.address, + data, + executable: true, + lamports: lamports(client.svm.minimumBalanceForRentExemption(space)), + programAddress: NATIVE_LOADER_PROGRAM_ADDRESS, + space, + }); + + return program.address; +}; diff --git a/clients/js/test/getProgramAuthority.test.ts b/clients/js/test/getProgramAuthority.test.ts new file mode 100644 index 0000000..052a44f --- /dev/null +++ b/clients/js/test/getProgramAuthority.test.ts @@ -0,0 +1,43 @@ +import { generateKeyPairSigner } from '@solana/kit'; +import { expect, it } from 'vitest'; + +import { getProgramAuthority } from '../src'; +import { createDeployedProgram, createNativeProgram, createTestClient, generateKeyPairSignerWithSol } from './_setup'; + +it('resolves the upgrade authority of a loader-v3 program', async () => { + // Given a program deployed with loader v3 and the following upgrade authority. + const client = await createTestClient(); + const authority = await generateKeyPairSigner(); + const [program, programData] = await createDeployedProgram(client, authority); + + // When we get the program authority. + const result = await getProgramAuthority(client.rpc, program); + + // Then we expect the upgrade authority and the program data account to be returned. + expect(result).toStrictEqual({ authority: authority.address, programData }); +}); + +it('resolves the program itself as the authority of a native program', async () => { + // Given a native program — i.e. an executable account owned by the native loader. + const client = await createTestClient(); + const program = await createNativeProgram(client); + + // When we get the program authority. + const result = await getProgramAuthority(client.rpc, program); + + // Then we expect the program's own address to be returned as the authority, + // mirroring the on-chain rule for programs not owned by loader v3. + expect(result).toStrictEqual({ authority: program }); +}); + +it('fails for non-executable accounts', async () => { + // Given a plain, non-executable wallet account. + const client = await createTestClient(); + const wallet = await generateKeyPairSignerWithSol(client); + + // When we try to get its program authority. + const promise = getProgramAuthority(client.rpc, wallet.address); + + // Then we expect an error to be thrown. + await expect(promise).rejects.toThrow('Program account must be executable'); +});