fix: fix blockhash cache reuse
This commit is contained in:
committed by
Michael Vines
parent
24bb060292
commit
22a63fe93c
@ -19,6 +19,8 @@ import type {FeeCalculator} from './fee-calculator';
|
||||
import type {Account} from './account';
|
||||
import type {TransactionSignature} from './transaction';
|
||||
|
||||
export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
|
||||
|
||||
type RpcRequest = (methodName: string, args: Array<any>) => any;
|
||||
|
||||
/**
|
||||
@ -975,7 +977,7 @@ export class Connection {
|
||||
_commitment: ?Commitment;
|
||||
_blockhashInfo: {
|
||||
recentBlockhash: Blockhash | null,
|
||||
seconds: number,
|
||||
lastFetch: Date,
|
||||
transactionSignatures: Array<string>,
|
||||
};
|
||||
_disableBlockhashCaching: boolean = false;
|
||||
@ -1011,7 +1013,7 @@ export class Connection {
|
||||
this._commitment = commitment;
|
||||
this._blockhashInfo = {
|
||||
recentBlockhash: null,
|
||||
seconds: -1,
|
||||
lastFetch: new Date(0),
|
||||
transactionSignatures: [],
|
||||
};
|
||||
|
||||
@ -1731,10 +1733,10 @@ export class Connection {
|
||||
} else {
|
||||
for (;;) {
|
||||
// Attempt to use a recent blockhash for up to 30 seconds
|
||||
const seconds = new Date().getSeconds();
|
||||
if (
|
||||
this._blockhashInfo.recentBlockhash != null &&
|
||||
this._blockhashInfo.seconds < seconds + 30
|
||||
Date.now() - this._blockhashInfo.lastFetch <
|
||||
BLOCKHASH_CACHE_TIMEOUT_MS
|
||||
) {
|
||||
transaction.recentBlockhash = this._blockhashInfo.recentBlockhash;
|
||||
transaction.sign(...signers);
|
||||
@ -1748,7 +1750,7 @@ export class Connection {
|
||||
if (!this._blockhashInfo.transactionSignatures.includes(signature)) {
|
||||
this._blockhashInfo.transactionSignatures.push(signature);
|
||||
if (this._disableBlockhashCaching) {
|
||||
this._blockhashInfo.seconds = -1;
|
||||
this._blockhashInfo.lastFetch = new Date(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -1763,7 +1765,7 @@ export class Connection {
|
||||
if (this._blockhashInfo.recentBlockhash != blockhash) {
|
||||
this._blockhashInfo = {
|
||||
recentBlockhash: blockhash,
|
||||
seconds: new Date().getSeconds(),
|
||||
lastFetch: new Date(),
|
||||
transactionSignatures: [],
|
||||
};
|
||||
break;
|
||||
|
@ -14,12 +14,11 @@ import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch';
|
||||
import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash';
|
||||
import {url} from './url';
|
||||
import {sleep} from '../src/util/sleep';
|
||||
import {BLOCKHASH_CACHE_TIMEOUT_MS} from '../src/connection';
|
||||
import type {SignatureStatus, TransactionError} from '../src/connection';
|
||||
|
||||
if (!mockRpcEnabled) {
|
||||
// Testing max commitment level takes around 20s to complete
|
||||
jest.setTimeout(30000);
|
||||
}
|
||||
// Testing blockhash cache takes around 30s to complete
|
||||
jest.setTimeout(40000);
|
||||
|
||||
const errorMessage = 'Invalid';
|
||||
const errorResponse = {
|
||||
@ -46,7 +45,7 @@ const verifySignatureStatus = (
|
||||
|
||||
const confirmations = status.confirmations;
|
||||
if (typeof confirmations === 'number') {
|
||||
expect(confirmations).toBeGreaterThan(0);
|
||||
expect(confirmations).toBeGreaterThanOrEqual(0);
|
||||
} else {
|
||||
expect(confirmations).toBeNull();
|
||||
}
|
||||
@ -1630,6 +1629,33 @@ test('transaction', async () => {
|
||||
'0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
},
|
||||
]);
|
||||
mockRpc.push([
|
||||
url,
|
||||
{
|
||||
method: 'getSignatureStatuses',
|
||||
params: [
|
||||
[
|
||||
'0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
error: null,
|
||||
result: {
|
||||
context: {
|
||||
slot: 11,
|
||||
},
|
||||
value: [
|
||||
{
|
||||
slot: 0,
|
||||
confirmations: 0,
|
||||
status: {Ok: null},
|
||||
err: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockRpc.push([
|
||||
url,
|
||||
{
|
||||
@ -1646,10 +1672,11 @@ test('transaction', async () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
await connection.requestAirdrop(
|
||||
const airdropFromSig = await connection.requestAirdrop(
|
||||
accountFrom.publicKey,
|
||||
minimumAmount + 100010,
|
||||
);
|
||||
await connection.confirmTransaction(airdropFromSig, 0);
|
||||
expect(await connection.getBalance(accountFrom.publicKey)).toBe(
|
||||
minimumAmount + 100010,
|
||||
);
|
||||
@ -1658,7 +1685,7 @@ test('transaction', async () => {
|
||||
url,
|
||||
{
|
||||
method: 'requestAirdrop',
|
||||
params: [accountTo.publicKey.toBase58(), minimumAmount + 21],
|
||||
params: [accountTo.publicKey.toBase58(), minimumAmount],
|
||||
},
|
||||
{
|
||||
error: null,
|
||||
@ -1666,6 +1693,33 @@ test('transaction', async () => {
|
||||
'8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
},
|
||||
]);
|
||||
mockRpc.push([
|
||||
url,
|
||||
{
|
||||
method: 'getSignatureStatuses',
|
||||
params: [
|
||||
[
|
||||
'8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
error: null,
|
||||
result: {
|
||||
context: {
|
||||
slot: 11,
|
||||
},
|
||||
value: [
|
||||
{
|
||||
slot: 0,
|
||||
confirmations: 0,
|
||||
status: {Ok: null},
|
||||
err: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockRpc.push([
|
||||
url,
|
||||
{
|
||||
@ -1678,14 +1732,16 @@ test('transaction', async () => {
|
||||
context: {
|
||||
slot: 11,
|
||||
},
|
||||
value: minimumAmount + 21,
|
||||
value: minimumAmount,
|
||||
},
|
||||
},
|
||||
]);
|
||||
await connection.requestAirdrop(accountTo.publicKey, minimumAmount + 21);
|
||||
expect(await connection.getBalance(accountTo.publicKey)).toBe(
|
||||
minimumAmount + 21,
|
||||
const airdropToSig = await connection.requestAirdrop(
|
||||
accountTo.publicKey,
|
||||
minimumAmount,
|
||||
);
|
||||
await connection.confirmTransaction(airdropToSig, 0);
|
||||
expect(await connection.getBalance(accountTo.publicKey)).toBe(minimumAmount);
|
||||
|
||||
mockGetRecentBlockhash('max');
|
||||
mockRpc.push([
|
||||
@ -1696,7 +1752,7 @@ test('transaction', async () => {
|
||||
{
|
||||
error: null,
|
||||
result:
|
||||
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
'1WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
},
|
||||
]);
|
||||
|
||||
@ -1717,7 +1773,7 @@ test('transaction', async () => {
|
||||
method: 'getSignatureStatuses',
|
||||
params: [
|
||||
[
|
||||
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
'1WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
],
|
||||
],
|
||||
},
|
||||
@ -1730,7 +1786,7 @@ test('transaction', async () => {
|
||||
value: [
|
||||
{
|
||||
slot: 0,
|
||||
confirmations: 1,
|
||||
confirmations: 0,
|
||||
status: {Ok: null},
|
||||
err: null,
|
||||
},
|
||||
@ -1739,96 +1795,91 @@ test('transaction', async () => {
|
||||
},
|
||||
]);
|
||||
|
||||
// Wait for one confirmation
|
||||
const confirmResult = (await connection.confirmTransaction(signature, 1))
|
||||
.value;
|
||||
let confirmResult = (await connection.confirmTransaction(signature, 0)).value;
|
||||
verifySignatureStatus(confirmResult);
|
||||
|
||||
mockGetRecentBlockhash('max');
|
||||
mockRpc.push([
|
||||
url,
|
||||
{
|
||||
method: 'getSignatureStatuses',
|
||||
params: [
|
||||
[
|
||||
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
],
|
||||
],
|
||||
method: 'sendTransaction',
|
||||
},
|
||||
{
|
||||
error: null,
|
||||
result: {
|
||||
context: {
|
||||
slot: 11,
|
||||
},
|
||||
value: [
|
||||
{
|
||||
slot: 0,
|
||||
confirmations: 11,
|
||||
status: {Ok: null},
|
||||
err: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
result:
|
||||
'2WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
},
|
||||
]);
|
||||
|
||||
const response = verifySignatureStatus(
|
||||
(await connection.getSignatureStatus(signature)).value,
|
||||
// Send again and ensure that new blockhash is used
|
||||
const lastFetch = Date.now();
|
||||
const transaction2 = SystemProgram.transfer({
|
||||
fromPubkey: accountFrom.publicKey,
|
||||
toPubkey: accountTo.publicKey,
|
||||
lamports: 10,
|
||||
});
|
||||
const signature2 = await connection.sendTransaction(
|
||||
transaction2,
|
||||
[accountFrom],
|
||||
{skipPreflight: true},
|
||||
);
|
||||
expect(signature).not.toEqual(signature2);
|
||||
expect(transaction.recentBlockhash).not.toEqual(transaction2.recentBlockhash);
|
||||
|
||||
const unprocessedSignature =
|
||||
'8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk';
|
||||
mockRpc.push([
|
||||
url,
|
||||
{
|
||||
method: 'getSignatureStatuses',
|
||||
params: [
|
||||
[
|
||||
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
unprocessedSignature,
|
||||
],
|
||||
],
|
||||
method: 'sendTransaction',
|
||||
},
|
||||
{
|
||||
error: null,
|
||||
result: {
|
||||
context: {
|
||||
slot: 11,
|
||||
},
|
||||
value: [
|
||||
{
|
||||
slot: 0,
|
||||
confirmations: 11,
|
||||
status: {Ok: null},
|
||||
err: null,
|
||||
},
|
||||
null,
|
||||
],
|
||||
},
|
||||
result:
|
||||
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
},
|
||||
]);
|
||||
|
||||
const responses = (
|
||||
await connection.getSignatureStatuses([signature, unprocessedSignature])
|
||||
).value;
|
||||
expect(responses.length).toEqual(2);
|
||||
expect(responses[1]).toBeNull();
|
||||
// Send new transaction and ensure that same blockhash is used
|
||||
const transaction3 = SystemProgram.transfer({
|
||||
fromPubkey: accountFrom.publicKey,
|
||||
toPubkey: accountTo.publicKey,
|
||||
lamports: 9,
|
||||
});
|
||||
await connection.sendTransaction(transaction3, [accountFrom], {
|
||||
skipPreflight: true,
|
||||
});
|
||||
expect(transaction2.recentBlockhash).toEqual(transaction3.recentBlockhash);
|
||||
|
||||
const firstResponse = verifySignatureStatus(responses[0]);
|
||||
expect(firstResponse.slot).toBeGreaterThanOrEqual(response.slot);
|
||||
expect(firstResponse.err).toEqual(response.err);
|
||||
|
||||
const responseConfirmations = response.confirmations;
|
||||
if (
|
||||
typeof responseConfirmations === 'number' &&
|
||||
typeof firstResponse.confirmations === 'number'
|
||||
) {
|
||||
expect(firstResponse.confirmations).toBeGreaterThanOrEqual(
|
||||
responseConfirmations,
|
||||
// Sleep until blockhash cache times out
|
||||
await sleep(
|
||||
Math.max(0, 1000 + BLOCKHASH_CACHE_TIMEOUT_MS - (Date.now() - lastFetch)),
|
||||
);
|
||||
|
||||
mockGetRecentBlockhash('max');
|
||||
mockRpc.push([
|
||||
url,
|
||||
{
|
||||
method: 'sendTransaction',
|
||||
},
|
||||
{
|
||||
error: null,
|
||||
result:
|
||||
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
|
||||
},
|
||||
]);
|
||||
|
||||
const transaction4 = SystemProgram.transfer({
|
||||
fromPubkey: accountFrom.publicKey,
|
||||
toPubkey: accountTo.publicKey,
|
||||
lamports: 13,
|
||||
});
|
||||
|
||||
await connection.sendTransaction(transaction4, [accountFrom], {
|
||||
skipPreflight: true,
|
||||
});
|
||||
|
||||
expect(transaction4.recentBlockhash).not.toEqual(
|
||||
transaction3.recentBlockhash,
|
||||
);
|
||||
} else {
|
||||
expect(firstResponse.confirmations).toBeNull();
|
||||
}
|
||||
|
||||
mockRpc.push([
|
||||
url,
|
||||
@ -1864,12 +1915,12 @@ test('transaction', async () => {
|
||||
context: {
|
||||
slot: 11,
|
||||
},
|
||||
value: minimumAmount + 31,
|
||||
value: minimumAmount + 42,
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(await connection.getBalance(accountTo.publicKey)).toBe(
|
||||
minimumAmount + 31,
|
||||
minimumAmount + 42,
|
||||
);
|
||||
});
|
||||
|
||||
@ -1883,7 +1934,11 @@ test('multi-instruction transaction', async () => {
|
||||
const accountTo = new Account();
|
||||
const connection = new Connection(url, 'recent');
|
||||
|
||||
await connection.requestAirdrop(accountFrom.publicKey, LAMPORTS_PER_SOL);
|
||||
let signature = await connection.requestAirdrop(
|
||||
accountFrom.publicKey,
|
||||
LAMPORTS_PER_SOL,
|
||||
);
|
||||
await connection.confirmTransaction(signature, 0);
|
||||
expect(await connection.getBalance(accountFrom.publicKey)).toBe(
|
||||
LAMPORTS_PER_SOL,
|
||||
);
|
||||
@ -1893,7 +1948,11 @@ test('multi-instruction transaction', async () => {
|
||||
'recent',
|
||||
);
|
||||
|
||||
await connection.requestAirdrop(accountTo.publicKey, minimumAmount + 21);
|
||||
signature = await connection.requestAirdrop(
|
||||
accountTo.publicKey,
|
||||
minimumAmount + 21,
|
||||
);
|
||||
await connection.confirmTransaction(signature, 0);
|
||||
expect(await connection.getBalance(accountTo.publicKey)).toBe(
|
||||
minimumAmount + 21,
|
||||
);
|
||||
@ -1911,7 +1970,7 @@ test('multi-instruction transaction', async () => {
|
||||
lamports: 100,
|
||||
}),
|
||||
);
|
||||
const signature = await connection.sendTransaction(
|
||||
signature = await connection.sendTransaction(
|
||||
transaction,
|
||||
[accountFrom, accountTo],
|
||||
{skipPreflight: true},
|
||||
|
Reference in New Issue
Block a user