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