Skip to content

Commit

Permalink
feat: thread new blockheight expiry strategy through `sendAndConfirmT…
Browse files Browse the repository at this point in the history
…ransaction` (#25227)

* chore: extract expirable blockhash record into its own type

* fix: the local latest blockhash cache now fetches and stores lastValidBlockHeight

* fix: allow people to supply a confirmation strategy to sendAndConfirmRawTransaction

* test: upgrade RPC helpers to use blockheight confirmation method

* test: patch up tests to use blockheight based confirmation strategy

* fix: eliminate deprecated construction of Transaction inside simulateTransaction

* test: eliminate deprecated constructions of Transaction in tests
  • Loading branch information
steveluscher authored May 15, 2022
1 parent d07e47c commit ad38806
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 72 deletions.
67 changes: 40 additions & 27 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,15 +284,18 @@ export type RpcResponseAndContext<T> = {
value: T;
};

export type BlockhashWithExpiryBlockHeight = Readonly<{
blockhash: Blockhash;
lastValidBlockHeight: number;
}>;

/**
* A strategy for confirming transactions that uses the last valid
* block height for a given blockhash to check for transaction expiration.
*/
export type BlockheightBasedTransactionConfimationStrategy = {
signature: TransactionSignature;
blockhash: Blockhash;
lastValidBlockHeight: number;
};
} & BlockhashWithExpiryBlockHeight;

/**
* @internal
Expand Down Expand Up @@ -2218,12 +2221,12 @@ export class Connection {
/** @internal */ _disableBlockhashCaching: boolean = false;
/** @internal */ _pollingBlockhash: boolean = false;
/** @internal */ _blockhashInfo: {
recentBlockhash: Blockhash | null;
latestBlockhash: BlockhashWithExpiryBlockHeight | null;
lastFetch: number;
simulatedSignatures: Array<string>;
transactionSignatures: Array<string>;
} = {
recentBlockhash: null,
latestBlockhash: null,
lastFetch: 0,
transactionSignatures: [],
simulatedSignatures: [],
Expand Down Expand Up @@ -3322,11 +3325,11 @@ export class Connection {

/**
* Fetch the latest blockhash from the cluster
* @return {Promise<{blockhash: Blockhash, lastValidBlockHeight: number}>}
* @return {Promise<BlockhashWithExpiryBlockHeight>}
*/
async getLatestBlockhash(
commitment?: Commitment,
): Promise<{blockhash: Blockhash; lastValidBlockHeight: number}> {
): Promise<BlockhashWithExpiryBlockHeight> {
try {
const res = await this.getLatestBlockhashAndContext(commitment);
return res.value;
Expand All @@ -3337,13 +3340,11 @@ export class Connection {

/**
* Fetch the latest blockhash from the cluster
* @return {Promise<{blockhash: Blockhash, lastValidBlockHeight: number}>}
* @return {Promise<BlockhashWithExpiryBlockHeight>}
*/
async getLatestBlockhashAndContext(
commitment?: Commitment,
): Promise<
RpcResponseAndContext<{blockhash: Blockhash; lastValidBlockHeight: number}>
> {
): Promise<RpcResponseAndContext<BlockhashWithExpiryBlockHeight>> {
const args = this._buildArgs([], commitment);
const unsafeRes = await this._rpcRequest('getLatestBlockhash', args);
const res = create(unsafeRes, GetLatestBlockhashRpcResult);
Expand Down Expand Up @@ -3989,16 +3990,18 @@ export class Connection {
/**
* @internal
*/
async _recentBlockhash(disableCache: boolean): Promise<Blockhash> {
async _blockhashWithExpiryBlockHeight(
disableCache: boolean,
): Promise<BlockhashWithExpiryBlockHeight> {
if (!disableCache) {
// Wait for polling to finish
while (this._pollingBlockhash) {
await sleep(100);
}
const timeSinceFetch = Date.now() - this._blockhashInfo.lastFetch;
const expired = timeSinceFetch >= BLOCKHASH_CACHE_TIMEOUT_MS;
if (this._blockhashInfo.recentBlockhash !== null && !expired) {
return this._blockhashInfo.recentBlockhash;
if (this._blockhashInfo.latestBlockhash !== null && !expired) {
return this._blockhashInfo.latestBlockhash;
}
}

Expand All @@ -4008,21 +4011,25 @@ export class Connection {
/**
* @internal
*/
async _pollNewBlockhash(): Promise<Blockhash> {
async _pollNewBlockhash(): Promise<BlockhashWithExpiryBlockHeight> {
this._pollingBlockhash = true;
try {
const startTime = Date.now();
const cachedLatestBlockhash = this._blockhashInfo.latestBlockhash;
const cachedBlockhash = cachedLatestBlockhash
? cachedLatestBlockhash.blockhash
: null;
for (let i = 0; i < 50; i++) {
const {blockhash} = await this.getRecentBlockhash('finalized');
const latestBlockhash = await this.getLatestBlockhash('finalized');

if (this._blockhashInfo.recentBlockhash != blockhash) {
if (cachedBlockhash !== latestBlockhash.blockhash) {
this._blockhashInfo = {
recentBlockhash: blockhash,
latestBlockhash,
lastFetch: Date.now(),
transactionSignatures: [],
simulatedSignatures: [],
};
return blockhash;
return latestBlockhash;
}

// Sleep for approximately half a slot
Expand All @@ -4048,13 +4055,11 @@ export class Connection {
let transaction;
if (transactionOrMessage instanceof Transaction) {
let originalTx: Transaction = transactionOrMessage;
transaction = new Transaction({
recentBlockhash: originalTx.recentBlockhash,
nonceInfo: originalTx.nonceInfo,
feePayer: originalTx.feePayer,
signatures: [...originalTx.signatures],
});
transaction = new Transaction();
transaction.feePayer = originalTx.feePayer;
transaction.instructions = transactionOrMessage.instructions;
transaction.nonceInfo = originalTx.nonceInfo;
transaction.signatures = originalTx.signatures;
} else {
transaction = Transaction.populate(transactionOrMessage);
// HACK: this function relies on mutating the populated transaction
Expand All @@ -4066,7 +4071,11 @@ export class Connection {
} else {
let disableCache = this._disableBlockhashCaching;
for (;;) {
transaction.recentBlockhash = await this._recentBlockhash(disableCache);
const latestBlockhash = await this._blockhashWithExpiryBlockHeight(
disableCache,
);
transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
transaction.recentBlockhash = latestBlockhash.blockhash;

if (!signers) break;

Expand Down Expand Up @@ -4154,7 +4163,11 @@ export class Connection {
} else {
let disableCache = this._disableBlockhashCaching;
for (;;) {
transaction.recentBlockhash = await this._recentBlockhash(disableCache);
const latestBlockhash = await this._blockhashWithExpiryBlockHeight(
disableCache,
);
transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
transaction.recentBlockhash = latestBlockhash.blockhash;
transaction.sign(...signers);
if (!transaction.signature) {
throw new Error('!signature'); // should never happen
Expand Down
59 changes: 52 additions & 7 deletions src/util/send-and-confirm-raw-transaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type {Buffer} from 'buffer';

import {Connection} from '../connection';
import {
BlockheightBasedTransactionConfimationStrategy,
Connection,
} from '../connection';
import type {TransactionSignature} from '../transaction';
import type {ConfirmOptions} from '../connection';

Expand All @@ -11,14 +14,57 @@ import type {ConfirmOptions} from '../connection';
*
* @param {Connection} connection
* @param {Buffer} rawTransaction
* @param {BlockheightBasedTransactionConfimationStrategy} confirmationStrategy
* @param {ConfirmOptions} [options]
* @returns {Promise<TransactionSignature>}
*/
export async function sendAndConfirmRawTransaction(
connection: Connection,
rawTransaction: Buffer,
confirmationStrategy: BlockheightBasedTransactionConfimationStrategy,
options?: ConfirmOptions,
): Promise<TransactionSignature>;

/**
* @deprecated Calling `sendAndConfirmRawTransaction()` without a `confirmationStrategy`
* is no longer supported and will be removed in a future version.
*/
// eslint-disable-next-line no-redeclare
export async function sendAndConfirmRawTransaction(
connection: Connection,
rawTransaction: Buffer,
options?: ConfirmOptions,
): Promise<TransactionSignature>;

// eslint-disable-next-line no-redeclare
export async function sendAndConfirmRawTransaction(
connection: Connection,
rawTransaction: Buffer,
confirmationStrategyOrConfirmOptions:
| BlockheightBasedTransactionConfimationStrategy
| ConfirmOptions
| undefined,
maybeConfirmOptions?: ConfirmOptions,
): Promise<TransactionSignature> {
let confirmationStrategy:
| BlockheightBasedTransactionConfimationStrategy
| undefined;
let options: ConfirmOptions | undefined;
if (
confirmationStrategyOrConfirmOptions &&
Object.prototype.hasOwnProperty.call(
confirmationStrategyOrConfirmOptions,
'lastValidBlockHeight',
)
) {
confirmationStrategy =
confirmationStrategyOrConfirmOptions as BlockheightBasedTransactionConfimationStrategy;
options = maybeConfirmOptions;
} else {
options = confirmationStrategyOrConfirmOptions as
| ConfirmOptions
| undefined;
}
const sendOptions = options && {
skipPreflight: options.skipPreflight,
preflightCommitment: options.preflightCommitment || options.commitment,
Expand All @@ -29,12 +75,11 @@ export async function sendAndConfirmRawTransaction(
sendOptions,
);

const status = (
await connection.confirmTransaction(
signature,
options && options.commitment,
)
).value;
const commitment = options && options.commitment;
const confirmationPromise = confirmationStrategy
? connection.confirmTransaction(confirmationStrategy, commitment)
: connection.confirmTransaction(signature, commitment);
const status = (await confirmationPromise).value;

if (status.err) {
throw new Error(
Expand Down
6 changes: 3 additions & 3 deletions test/bpf-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ if (process.env.TEST_LIVE) {
});

it('simulate transaction without signature verification', async () => {
const simulatedTransaction = new Transaction({
feePayer: payerAccount.publicKey,
}).add({
const simulatedTransaction = new Transaction();
simulatedTransaction.feePayer = payerAccount.publicKey;
simulatedTransaction.add({
keys: [
{pubkey: payerAccount.publicKey, isSigner: true, isWritable: true},
],
Expand Down
16 changes: 12 additions & 4 deletions test/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,11 @@ describe('Connection', function () {
const badTransactionSignature = 'bad transaction signature';

await expect(
connection.confirmTransaction(badTransactionSignature),
connection.confirmTransaction({
blockhash: 'sampleBlockhash',
lastValidBlockHeight: 9999,
signature: badTransactionSignature,
}),
).to.be.rejectedWith('signature must be base58 encoded');

await mockRpcResponse({
Expand Down Expand Up @@ -2899,11 +2903,11 @@ describe('Connection', function () {
const accountFrom = Keypair.generate();
const accountTo = Keypair.generate();

const {blockhash} = await helpers.latestBlockhash({connection});
const latestBlockhash = await helpers.latestBlockhash({connection});

const transaction = new Transaction({
feePayer: accountFrom.publicKey,
recentBlockhash: blockhash,
...latestBlockhash,
}).add(
SystemProgram.transfer({
fromPubkey: accountFrom.publicKey,
Expand Down Expand Up @@ -3825,7 +3829,11 @@ describe('Connection', function () {
{skipPreflight: true},
);

await connection.confirmTransaction(signature);
await connection.confirmTransaction({
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
signature,
});

const response = (await connection.getSignatureStatus(signature)).value;
if (response !== null) {
Expand Down
8 changes: 6 additions & 2 deletions test/mocks/rpc-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ const processTransaction = async ({
commitment: Commitment;
err?: any;
}) => {
const blockhash = (await latestBlockhash({connection})).blockhash;
const {blockhash, lastValidBlockHeight} = await latestBlockhash({connection});
transaction.lastValidBlockHeight = lastValidBlockHeight;
transaction.recentBlockhash = blockhash;
transaction.sign(...signers);

Expand Down Expand Up @@ -222,7 +223,10 @@ const processTransaction = async ({
result: true,
});

return await connection.confirmTransaction(signature, commitment);
return await connection.confirmTransaction(
{blockhash, lastValidBlockHeight, signature},
commitment,
);
};

const airdrop = async ({
Expand Down
14 changes: 8 additions & 6 deletions test/stake-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,10 @@ describe('StakeProgram', () => {
lockup: new Lockup(0, 0, from.publicKey),
lamports: amount,
});
const createWithSeedTransaction = new Transaction({recentBlockhash}).add(
createWithSeed,
);
const createWithSeedTransaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add(createWithSeed);

expect(createWithSeedTransaction.instructions).to.have.length(2);
const systemInstructionType = SystemInstruction.decodeInstructionType(
Expand All @@ -368,9 +369,10 @@ describe('StakeProgram', () => {
votePubkey: vote.publicKey,
});

const delegateTransaction = new Transaction({recentBlockhash}).add(
delegate,
);
const delegateTransaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add(delegate);
const anotherStakeInstructionType = StakeInstruction.decodeInstructionType(
delegateTransaction.instructions[0],
);
Expand Down
Loading

0 comments on commit ad38806

Please sign in to comment.