fix: fix blockhash cache reuse

This commit is contained in:
Justin Starry
2020-06-11 15:00:59 +08:00
committed by Michael Vines
parent 24bb060292
commit 22a63fe93c
2 changed files with 152 additions and 91 deletions

View File

@ -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;

View File

@ -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},