Skip to content

Commit e12e8d9

Browse files
authored
Support any program loader in getProgramAuthority (#106)
This fixes non-canonical metadata writes for programs owned by loaders other than v1, v2 or v3 — e.g. native programs such as ComputeBudget — by mirroring the on-chain authority check instead of rejecting unknown loaders. Removes the unused LOADER_V1_PROGRAM_ADDRESS, LOADER_V2_PROGRAM_ADDRESS and LOADER_V4_PROGRAM_ADDRESS exports.
1 parent 202bcbe commit e12e8d9

3 files changed

Lines changed: 79 additions & 16 deletions

File tree

clients/js/src/utils.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,8 @@ import {
3535

3636
export const ACCOUNT_HEADER_LENGTH = 96;
3737

38-
export const LOADER_V1_PROGRAM_ADDRESS =
39-
'BPFLoader1111111111111111111111111111111111' as Address<'BPFLoader1111111111111111111111111111111111'>;
40-
export const LOADER_V2_PROGRAM_ADDRESS =
41-
'BPFLoader2111111111111111111111111111111111' as Address<'BPFLoader2111111111111111111111111111111111'>;
4238
export const LOADER_V3_PROGRAM_ADDRESS =
4339
'BPFLoaderUpgradeab1e11111111111111111111111' as Address<'BPFLoaderUpgradeab1e11111111111111111111111'>;
44-
export const LOADER_V4_PROGRAM_ADDRESS =
45-
'CoreBPFLoaderV41111111111111111111111111111' as Address<'CoreBPFLoaderV41111111111111111111111111111'>;
4640

4741
export type MetadataInput = {
4842
payer: TransactionSigner;
@@ -127,17 +121,16 @@ export async function getProgramAuthority(
127121
throw Error('Program account must be executable');
128122
}
129123

130-
// Check all the loader programs.
131-
switch (programAccount.programAddress) {
132-
case LOADER_V1_PROGRAM_ADDRESS:
133-
case LOADER_V2_PROGRAM_ADDRESS:
134-
return { authority: program };
135-
case LOADER_V3_PROGRAM_ADDRESS:
136-
return await getProgramAuthorityForLoaderV3(rpc, programAccount);
137-
case LOADER_V4_PROGRAM_ADDRESS:
138-
default:
139-
throw new Error('Unsupported loader program: ' + programAccount.programAddress);
124+
// Loader v3 programs store their upgrade authority in a separate program
125+
// data account.
126+
if (programAccount.programAddress === LOADER_V3_PROGRAM_ADDRESS) {
127+
return await getProgramAuthorityForLoaderV3(rpc, programAccount);
140128
}
129+
130+
// For any other owner (loader v1, v2, v4, the native loader, etc.), the
131+
// on-chain program considers the program's own address to be its authority
132+
// — i.e. the program keypair itself must sign to prove canonicity.
133+
return { authority: program };
141134
}
142135

143136
async function getProgramAuthorityForLoaderV3(rpc: Rpc<GetAccountInfoApi>, programAccount: EncodedAccount) {

clients/js/test/_setup.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import path from 'node:path';
33
import { systemProgram } from '@solana-program/system';
44
import {
55
Address,
6+
address,
67
createClient,
78
generateKeyPairSigner,
89
getAddressEncoder,
910
getOptionEncoder,
1011
getStructEncoder,
1112
getU32Encoder,
1213
getU64Encoder,
14+
getUtf8Encoder,
1315
KeyPairSigner,
1416
lamports,
1517
Lamports,
@@ -133,3 +135,28 @@ export const createDeployedProgram = async (
133135

134136
return [program.address, programData];
135137
};
138+
139+
export const NATIVE_LOADER_PROGRAM_ADDRESS = address('NativeLoader1111111111111111111111111111111');
140+
141+
/**
142+
* Fabricates a native (built-in) program inside the LiteSVM instance — i.e. an
143+
* executable account owned by the native loader, like `ComputeBudget111...`.
144+
* The program is never actually invoked — only its account is read by the
145+
* client for the canonicity check.
146+
*/
147+
export const createNativeProgram = async (client: TestClient): Promise<Address> => {
148+
const program = await generateKeyPairSigner();
149+
const data = getUtf8Encoder().encode('native_program');
150+
const space = BigInt(data.length);
151+
152+
client.svm.setAccount({
153+
address: program.address,
154+
data,
155+
executable: true,
156+
lamports: lamports(client.svm.minimumBalanceForRentExemption(space)),
157+
programAddress: NATIVE_LOADER_PROGRAM_ADDRESS,
158+
space,
159+
});
160+
161+
return program.address;
162+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { generateKeyPairSigner } from '@solana/kit';
2+
import { expect, it } from 'vitest';
3+
4+
import { getProgramAuthority } from '../src';
5+
import { createDeployedProgram, createNativeProgram, createTestClient, generateKeyPairSignerWithSol } from './_setup';
6+
7+
it('resolves the upgrade authority of a loader-v3 program', async () => {
8+
// Given a program deployed with loader v3 and the following upgrade authority.
9+
const client = await createTestClient();
10+
const authority = await generateKeyPairSigner();
11+
const [program, programData] = await createDeployedProgram(client, authority);
12+
13+
// When we get the program authority.
14+
const result = await getProgramAuthority(client.rpc, program);
15+
16+
// Then we expect the upgrade authority and the program data account to be returned.
17+
expect(result).toStrictEqual({ authority: authority.address, programData });
18+
});
19+
20+
it('resolves the program itself as the authority of a native program', async () => {
21+
// Given a native program — i.e. an executable account owned by the native loader.
22+
const client = await createTestClient();
23+
const program = await createNativeProgram(client);
24+
25+
// When we get the program authority.
26+
const result = await getProgramAuthority(client.rpc, program);
27+
28+
// Then we expect the program's own address to be returned as the authority,
29+
// mirroring the on-chain rule for programs not owned by loader v3.
30+
expect(result).toStrictEqual({ authority: program });
31+
});
32+
33+
it('fails for non-executable accounts', async () => {
34+
// Given a plain, non-executable wallet account.
35+
const client = await createTestClient();
36+
const wallet = await generateKeyPairSignerWithSol(client);
37+
38+
// When we try to get its program authority.
39+
const promise = getProgramAuthority(client.rpc, wallet.address);
40+
41+
// Then we expect an error to be thrown.
42+
await expect(promise).rejects.toThrow('Program account must be executable');
43+
});

0 commit comments

Comments
 (0)