Skip to content

Commit

Permalink
feat(LLC): add solana compute units optimisations
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhd committed Jun 24, 2024
1 parent 8df5b29 commit 165d255
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 30 deletions.
18 changes: 18 additions & 0 deletions libs/coin-modules/coin-solana/src/api/cached.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { makeLRUCache, minutes, seconds } from "@ledgerhq/live-network/cache";
import { PublicKey, TransactionInstruction, TransactionMessage } from "@solana/web3.js";
import hash from "object-hash";
import { ChainAPI } from "./chain";

Expand All @@ -8,6 +9,17 @@ const cacheKeyAssocTokenAccAddress = (owner: string, mint: string) => `${owner}:
const cacheKeyMinimumBalanceForRentExemption = (dataLengt: number) => dataLengt.toString();

const cacheKeyTransactions = (signatures: string[]) => hash([...signatures].sort());
const cacheKeyInstructions = (ixs: TransactionInstruction[], payer: PublicKey) => {
return hash(
new TransactionMessage({
instructions: ixs,
payerKey: payer,
recentBlockhash: payer.toString(),
})
.compileToLegacyMessage()
.serialize(),
);
};

const cacheKeyByArgs = (...args: any[]) => hash(args);

Expand Down Expand Up @@ -80,6 +92,12 @@ export function cached(api: ChainAPI): ChainAPI {
seconds(30),
),

getSimulationComputeUnits: makeLRUCache(
api.getSimulationComputeUnits,
cacheKeyInstructions,
seconds(30),
),

config: api.config,
};
}
44 changes: 41 additions & 3 deletions libs/coin-modules/coin-solana/src/api/chain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
SignaturesForAddressOptions,
StakeProgram,
GetRecentPrioritizationFeesConfig,
TransactionInstruction,
ComputeBudgetProgram,
VersionedTransaction,
TransactionMessage,
} from "@solana/web3.js";
import { makeLRUCache, minutes } from "@ledgerhq/live-network/cache";
import { getEnv } from "@ledgerhq/live-env";
Expand Down Expand Up @@ -73,9 +77,14 @@ export type ChainAPI = Readonly<{
getEpochInfo: () => ReturnType<Connection["getEpochInfo"]>;

getRecentPrioritizationFees: (
config?: GetRecentPrioritizationFeesConfig,
accounts: string[],
) => ReturnType<Connection["getRecentPrioritizationFees"]>;

getSimulationComputeUnits: (
instructions: Array<TransactionInstruction>,
payer: PublicKey,
) => Promise<number | null>;

config: Config;
}>;

Expand Down Expand Up @@ -212,8 +221,37 @@ export function getChainAPI(

getEpochInfo: () => connection().getEpochInfo().catch(remapErrors),

getRecentPrioritizationFees: (config?: GetRecentPrioritizationFeesConfig) => {
return connection().getRecentPrioritizationFees(config).catch(remapErrors);
getRecentPrioritizationFees: (accounts: string[]) => {
return connection()
.getRecentPrioritizationFees({
lockedWritableAccounts: accounts.map(acc => new PublicKey(acc)),
})
.catch(remapErrors);
},

getSimulationComputeUnits: async (instructions, payer) => {
// https://solana.com/developers/guides/advanced/how-to-request-optimal-compute
const testInstructions = [
// Set an arbitrarily high number in simulation
// so we can be sure the transaction will succeed
// and get the real compute units used
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
...instructions,
];
const testTransaction = new VersionedTransaction(
new TransactionMessage({
instructions: testInstructions,
payerKey: payer,
// RecentBlockhash can by any public key during simulation
// since 'replaceRecentBlockhash' is set to 'true' below
recentBlockhash: PublicKey.default.toString(),
}).compileToV0Message(),
);
const rpcResponse = await connection().simulateTransaction(testTransaction, {
replaceRecentBlockhash: true,
sigVerify: false,
});
return rpcResponse.value.err ? null : rpcResponse.value.unitsConsumed || null;
},

config,
Expand Down
55 changes: 35 additions & 20 deletions libs/coin-modules/coin-solana/src/api/chain/web3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ComputeBudgetProgram,
} from "@solana/web3.js";
import chunk from "lodash/chunk";
import uniqBy from "lodash/uniqBy";
import uniq from "lodash/uniq";
import { ChainAPI } from ".";
import { Awaited } from "../../logic";
import {
Expand Down Expand Up @@ -153,7 +153,7 @@ export const buildTransferInstructions = async (
instructions.push(memoIx);
}

return appendMaybePriorityFeeInstruction(api, [fromPublicKey, toPublicKey], instructions);
return appendMaybePriorityFeeInstructions(api, instructions, fromPublicKey);
};

export const buildTokenTransferInstructions = async (
Expand Down Expand Up @@ -272,18 +272,31 @@ export async function getStakeAccountAddressWithSeed({
return pubkey.toBase58();
}

export async function getPriorityFee(api: ChainAPI, accounts: PublicKey[]): Promise<number> {
const uniqAccs = uniqBy(accounts, acc => acc.toBase58());
const recentFees = await api.getRecentPrioritizationFees({
lockedWritableAccounts: uniqAccs,
});

export async function getPriorityFee(api: ChainAPI, accounts: string[]): Promise<number> {
const recentFees = await api.getRecentPrioritizationFees(uniq(accounts));
return median(recentFees.map(item => item.prioritizationFee));
}

export async function appendMaybePriorityFeeInstructions(
api: ChainAPI,
ixs: TransactionInstruction[],
payer: PublicKey,
): Promise<TransactionInstruction[]> {
const instructions = [...ixs];
const writableAccs = instructions
.map(ix => ix.keys.filter(acc => acc.isWritable).map(acc => acc.pubkey.toBase58()))
.flat();
const priorityFeeIx = await buildMaybePriorityFeeInstruction(api, writableAccs);
if (priorityFeeIx) instructions.unshift(priorityFeeIx);
const computeUnitsIx = await buildComputeUnitInstruction(api, instructions, payer);

if (computeUnitsIx) instructions.unshift(computeUnitsIx);
return instructions;
}

export async function buildMaybePriorityFeeInstruction(
api: ChainAPI,
accounts: PublicKey[],
accounts: string[],
): Promise<TransactionInstruction | null> {
const priorityFee = await getPriorityFee(api, accounts);
if (priorityFee === 0) return null;
Expand All @@ -292,14 +305,16 @@ export async function buildMaybePriorityFeeInstruction(
microLamports: priorityFee,
});
}

export async function appendMaybePriorityFeeInstruction(
export async function buildComputeUnitInstruction(
api: ChainAPI,
accounts: PublicKey[],
ixs: TransactionInstruction[],
): Promise<TransactionInstruction[]> {
const priorityFeeIx = await buildMaybePriorityFeeInstruction(api, accounts);
return priorityFeeIx ? [priorityFeeIx, ...ixs] : ixs;
payer: PublicKey,
): Promise<TransactionInstruction | null> {
const computeUnits = await api.getSimulationComputeUnits(ixs, payer);
// adding 10% more CPU to make sure it will work
return computeUnits
? ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits * 0.1 + computeUnits })
: null;
}

export function buildCreateAssociatedTokenAccountInstruction({
Expand Down Expand Up @@ -336,7 +351,7 @@ export async function buildStakeDelegateInstructions(
votePubkey: voteAcc,
});

return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions);
return appendMaybePriorityFeeInstructions(api, tx.instructions, withdrawAuthority);
}

export async function buildStakeUndelegateInstructions(
Expand All @@ -350,7 +365,7 @@ export async function buildStakeUndelegateInstructions(
stakePubkey: stakeAcc,
});

return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions);
return appendMaybePriorityFeeInstructions(api, tx.instructions, withdrawAuthority);
}

export async function buildStakeWithdrawInstructions(
Expand All @@ -367,7 +382,7 @@ export async function buildStakeWithdrawInstructions(
toPubkey: recipient,
});

return appendMaybePriorityFeeInstruction(api, [withdrawAuthority, stakeAcc], tx.instructions);
return appendMaybePriorityFeeInstructions(api, tx.instructions, withdrawAuthority);
}

export async function buildStakeSplitInstructions(
Expand All @@ -385,7 +400,7 @@ export async function buildStakeSplitInstructions(
basePubkey: basePk,
seed,
});
return appendMaybePriorityFeeInstruction(api, [basePk, stakePk], splitIx.instructions);
return appendMaybePriorityFeeInstructions(api, splitIx.instructions, basePk);
}

export async function buildStakeCreateAccountInstructions(
Expand Down Expand Up @@ -421,5 +436,5 @@ export async function buildStakeCreateAccountInstructions(
votePubkey: new PublicKey(delegate.voteAccAddress),
}),
);
return appendMaybePriorityFeeInstruction(api, [fromPubkey, stakePubkey], tx.instructions);
return appendMaybePriorityFeeInstructions(api, tx.instructions, fromPubkey);
}
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,7 @@ const baseAPI = {
},
]);
},
getSimulationComputeUnits: (_ixs: any[], _payer: any) => Promise.resolve(1000),
} as ChainAPI;

type StakeTestSpec = {
Expand Down
15 changes: 8 additions & 7 deletions libs/coin-modules/coin-solana/src/tx-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { ChainAPI } from "./api";
import { buildTransactionWithAPI } from "./buildTransaction";
import createTransaction from "./createTransaction";
import { Transaction, TransactionModel } from "./types";
import { assertUnreachable } from "./utils";
import { LEDGER_VALIDATOR, assertUnreachable } from "./utils";
import { VersionedTransaction as OnChainTransaction } from "@solana/web3.js";
import { log } from "@ledgerhq/logs";
import { getStakeAccountAddressWithSeed } from "./api/chain/web3";

const DEFAULT_TX_FEE = 5000;

Expand All @@ -13,7 +14,7 @@ export async function estimateTxFee(
address: string,
kind: TransactionModel["kind"],
) {
const tx = createDummyTx(address, kind);
const tx = await createDummyTx(address, kind);
const [onChainTx] = await buildTransactionWithAPI(address, tx, api);

let fee = await api.getFeeForMessage(onChainTx.message);
Expand Down Expand Up @@ -76,7 +77,7 @@ const createDummyTransferTx = (address: string): Transaction => {
};
};

const createDummyStakeCreateAccountTx = (address: string): Transaction => {
const createDummyStakeCreateAccountTx = async (address: string): Promise<Transaction> => {
return {
...createTransaction({} as any),
model: {
Expand All @@ -87,12 +88,12 @@ const createDummyStakeCreateAccountTx = (address: string): Transaction => {
kind: "stake.createAccount",
amount: 0,
delegate: {
voteAccAddress: randomAddresses[0],
voteAccAddress: LEDGER_VALIDATOR.voteAccount,
},
fromAccAddress: address,
seed: "",
stakeAccAddress: randomAddresses[1],
stakeAccRentExemptAmount: 0,
stakeAccAddress: await getStakeAccountAddressWithSeed({ fromAddress: address, seed: "" }),
stakeAccRentExemptAmount: 2282880,
},
...commandDescriptorCommons,
},
Expand Down Expand Up @@ -149,7 +150,7 @@ const createDummyStakeWithdrawTx = (address: string): Transaction => {
amount: 0,
authorizedAccAddr: address,
stakeAccAddr: randomAddresses[0],
toAccAddr: randomAddresses[1],
toAccAddr: address,
},
...commandDescriptorCommons,
},
Expand Down

0 comments on commit 165d255

Please sign in to comment.