feat: support restarting failed bpf loader deploys (#12163)
* feat: support restarting failed bpf loader deploys * chore: add error message if program already exists
This commit is contained in:
parent
bb72cbe7ae
commit
54775ffedf
|
@ -962,7 +962,7 @@ declare module '@solana/web3.js' {
|
||||||
program: Account,
|
program: Account,
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
data: Buffer | Uint8Array | Array<number>,
|
data: Buffer | Uint8Array | Array<number>,
|
||||||
): Promise<PublicKey>;
|
): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === src/bpf-loader.js ===
|
// === src/bpf-loader.js ===
|
||||||
|
@ -975,7 +975,7 @@ declare module '@solana/web3.js' {
|
||||||
program: Account,
|
program: Account,
|
||||||
elfBytes: Buffer | Uint8Array | Array<number>,
|
elfBytes: Buffer | Uint8Array | Array<number>,
|
||||||
loaderProgramId: PublicKey,
|
loaderProgramId: PublicKey,
|
||||||
): Promise<PublicKey>;
|
): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === src/bpf-loader-deprecated.js ===
|
// === src/bpf-loader-deprecated.js ===
|
||||||
|
|
|
@ -969,7 +969,7 @@ declare module '@solana/web3.js' {
|
||||||
program: Account,
|
program: Account,
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
data: Buffer | Uint8Array | Array<number>,
|
data: Buffer | Uint8Array | Array<number>,
|
||||||
): Promise<PublicKey>;
|
): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === src/bpf-loader.js ===
|
// === src/bpf-loader.js ===
|
||||||
|
@ -982,7 +982,7 @@ declare module '@solana/web3.js' {
|
||||||
program: Account,
|
program: Account,
|
||||||
elfBytes: Buffer | Uint8Array | Array<number>,
|
elfBytes: Buffer | Uint8Array | Array<number>,
|
||||||
loaderProgramId: PublicKey,
|
loaderProgramId: PublicKey,
|
||||||
): Promise<PublicKey>;
|
): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === src/bpf-loader-deprecated.js ===
|
// === src/bpf-loader-deprecated.js ===
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class BpfLoader {
|
||||||
* @param program Account to load the program into
|
* @param program Account to load the program into
|
||||||
* @param elf The entire ELF containing the BPF program
|
* @param elf The entire ELF containing the BPF program
|
||||||
* @param loaderProgramId The program id of the BPF loader to use
|
* @param loaderProgramId The program id of the BPF loader to use
|
||||||
|
* @return true if program was loaded successfully, false if program was already loaded
|
||||||
*/
|
*/
|
||||||
static load(
|
static load(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
@ -38,7 +39,7 @@ export class BpfLoader {
|
||||||
program: Account,
|
program: Account,
|
||||||
elf: Buffer | Uint8Array | Array<number>,
|
elf: Buffer | Uint8Array | Array<number>,
|
||||||
loaderProgramId: PublicKey,
|
loaderProgramId: PublicKey,
|
||||||
): Promise<void> {
|
): Promise<boolean> {
|
||||||
return Loader.load(connection, payer, program, loaderProgramId, elf);
|
return Loader.load(connection, payer, program, loaderProgramId, elf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ export class Loader {
|
||||||
* @param program Account to load the program into
|
* @param program Account to load the program into
|
||||||
* @param programId Public key that identifies the loader
|
* @param programId Public key that identifies the loader
|
||||||
* @param data Program octets
|
* @param data Program octets
|
||||||
|
* @return true if program was loaded successfully, false if program was already loaded
|
||||||
*/
|
*/
|
||||||
static async load(
|
static async load(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
@ -52,12 +53,57 @@ export class Loader {
|
||||||
program: Account,
|
program: Account,
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
data: Buffer | Uint8Array | Array<number>,
|
data: Buffer | Uint8Array | Array<number>,
|
||||||
): Promise<void> {
|
): Promise<boolean> {
|
||||||
{
|
{
|
||||||
const balanceNeeded = await connection.getMinimumBalanceForRentExemption(
|
const balanceNeeded = await connection.getMinimumBalanceForRentExemption(
|
||||||
data.length,
|
data.length,
|
||||||
);
|
);
|
||||||
const transaction = new Transaction().add(
|
|
||||||
|
// Fetch program account info to check if it has already been created
|
||||||
|
const programInfo = await connection.getAccountInfo(
|
||||||
|
program.publicKey,
|
||||||
|
'single',
|
||||||
|
);
|
||||||
|
|
||||||
|
let transaction: Transaction | null = null;
|
||||||
|
if (programInfo !== null) {
|
||||||
|
if (programInfo.executable) {
|
||||||
|
console.error('Program load failed, account is already executable');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (programInfo.data.length !== data.length) {
|
||||||
|
transaction = transaction || new Transaction();
|
||||||
|
transaction.add(
|
||||||
|
SystemProgram.allocate({
|
||||||
|
accountPubkey: program.publicKey,
|
||||||
|
space: data.length,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!programInfo.owner.equals(programId)) {
|
||||||
|
transaction = transaction || new Transaction();
|
||||||
|
transaction.add(
|
||||||
|
SystemProgram.assign({
|
||||||
|
accountPubkey: program.publicKey,
|
||||||
|
programId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (programInfo.lamports < balanceNeeded) {
|
||||||
|
transaction = transaction || new Transaction();
|
||||||
|
transaction.add(
|
||||||
|
SystemProgram.transfer({
|
||||||
|
fromPubkey: payer.publicKey,
|
||||||
|
toPubkey: program.publicKey,
|
||||||
|
lamports: balanceNeeded - programInfo.lamports,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transaction = new Transaction().add(
|
||||||
SystemProgram.createAccount({
|
SystemProgram.createAccount({
|
||||||
fromPubkey: payer.publicKey,
|
fromPubkey: payer.publicKey,
|
||||||
newAccountPubkey: program.publicKey,
|
newAccountPubkey: program.publicKey,
|
||||||
|
@ -66,6 +112,11 @@ export class Loader {
|
||||||
programId,
|
programId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the account is already created correctly, skip this step
|
||||||
|
// and proceed directly to loading instructions
|
||||||
|
if (transaction !== null) {
|
||||||
await sendAndConfirmTransaction(
|
await sendAndConfirmTransaction(
|
||||||
connection,
|
connection,
|
||||||
transaction,
|
transaction,
|
||||||
|
@ -76,6 +127,7 @@ export class Loader {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dataLayout = BufferLayout.struct([
|
const dataLayout = BufferLayout.struct([
|
||||||
BufferLayout.u32('instruction'),
|
BufferLayout.u32('instruction'),
|
||||||
|
@ -165,5 +217,8 @@ export class Loader {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// success
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,18 +62,19 @@ describe('load BPF Rust program', () => {
|
||||||
let program: Account;
|
let program: Account;
|
||||||
let signature: string;
|
let signature: string;
|
||||||
let payerAccount: Account;
|
let payerAccount: Account;
|
||||||
|
let programData: Buffer;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const data = await fs.readFile(
|
programData = await fs.readFile(
|
||||||
'test/fixtures/noop-rust/solana_bpf_rust_noop.so',
|
'test/fixtures/noop-rust/solana_bpf_rust_noop.so',
|
||||||
);
|
);
|
||||||
|
|
||||||
const {feeCalculator} = await connection.getRecentBlockhash();
|
const {feeCalculator} = await connection.getRecentBlockhash();
|
||||||
const fees =
|
const fees =
|
||||||
feeCalculator.lamportsPerSignature *
|
feeCalculator.lamportsPerSignature *
|
||||||
(BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES);
|
(BpfLoader.getMinNumSignatures(programData.length) + NUM_RETRIES);
|
||||||
const balanceNeeded = await connection.getMinimumBalanceForRentExemption(
|
const balanceNeeded = await connection.getMinimumBalanceForRentExemption(
|
||||||
data.length,
|
programData.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
payerAccount = await newAccountWithLamports(
|
payerAccount = await newAccountWithLamports(
|
||||||
|
@ -81,12 +82,30 @@ describe('load BPF Rust program', () => {
|
||||||
fees + balanceNeeded,
|
fees + balanceNeeded,
|
||||||
);
|
);
|
||||||
|
|
||||||
program = new Account();
|
// Create program account with low balance
|
||||||
|
program = await newAccountWithLamports(connection, balanceNeeded - 1);
|
||||||
|
|
||||||
|
// First load will fail part way due to lack of funds
|
||||||
|
const insufficientPayerAccount = await newAccountWithLamports(
|
||||||
|
connection,
|
||||||
|
2 * feeCalculator.lamportsPerSignature * 8,
|
||||||
|
);
|
||||||
|
|
||||||
|
const failedLoad = BpfLoader.load(
|
||||||
|
connection,
|
||||||
|
insufficientPayerAccount,
|
||||||
|
program,
|
||||||
|
programData,
|
||||||
|
BPF_LOADER_PROGRAM_ID,
|
||||||
|
);
|
||||||
|
await expect(failedLoad).rejects.toThrow('Transaction was not confirmed');
|
||||||
|
|
||||||
|
// Second load will succeed
|
||||||
await BpfLoader.load(
|
await BpfLoader.load(
|
||||||
connection,
|
connection,
|
||||||
payerAccount,
|
payerAccount,
|
||||||
program,
|
program,
|
||||||
data,
|
programData,
|
||||||
BPF_LOADER_PROGRAM_ID,
|
BPF_LOADER_PROGRAM_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -196,4 +215,16 @@ describe('load BPF Rust program', () => {
|
||||||
|
|
||||||
expect(logs.length).toEqual(0);
|
expect(logs.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('reload program', async () => {
|
||||||
|
expect(
|
||||||
|
await BpfLoader.load(
|
||||||
|
connection,
|
||||||
|
payerAccount,
|
||||||
|
program,
|
||||||
|
programData,
|
||||||
|
BPF_LOADER_PROGRAM_ID,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue