Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: thread new blockheight expiry strategy through sendAndConfirmTransaction #25227

Merged
merged 7 commits into from
May 15, 2022
67 changes: 40 additions & 27 deletions web3.js/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 web3.js/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 web3.js/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 web3.js/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 web3.js/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 web3.js/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