Skip to content

Commit

Permalink
feat: add block etag
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr committed Oct 1, 2024
1 parent 2370c21 commit 73fbbef
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 6 deletions.
14 changes: 14 additions & 0 deletions src/api/controllers/cache-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
sha256,
} from '@hirosystems/api-toolkit';
import { FastifyReply, FastifyRequest } from 'fastify';
import { BlockParams } from '../routes/v2/schemas';

/**
* Describes a key-value to be saved into a request's locals, representing the current
Expand All @@ -21,6 +22,8 @@ enum ETagType {
mempool = 'mempool',
/** ETag based on the status of a single transaction across the mempool or canonical chain. */
transaction = 'transaction',
/** Etag based on the status of a single block */
block = 'block',
/** Etag based on the confirmed balance of a single principal (STX address or contract id) */
principal = 'principal',
/** Etag based on `principal` but also including its mempool transactions */
Expand Down Expand Up @@ -122,6 +125,13 @@ async function calculateETag(
];
return sha256(elements.join(':'));

case ETagType.block: {
const params = req.params as BlockParams;
const status = await db.getBlockCanonicalStatus(params.height_or_hash);
if (!status) return ETAG_EMPTY;
return `${status.index_block_hash}:${status.canonical}`;
}

case ETagType.principal:
case ETagType.principalMempool:
const params = req.params as { address?: string; principal?: string };
Expand Down Expand Up @@ -180,6 +190,10 @@ export async function handleTransactionCache(request: FastifyRequest, reply: Fas
return handleCache(ETagType.transaction, request, reply);
}

export async function handleBlockCache(request: FastifyRequest, reply: FastifyReply) {
return handleCache(ETagType.block, request, reply);
}

export async function handlePrincipalCache(request: FastifyRequest, reply: FastifyReply) {
return handleCache(ETagType.principal, request, reply);
}
Expand Down
6 changes: 3 additions & 3 deletions src/api/routes/v2/blocks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { handleChainTipCache } from '../../../api/controllers/cache-controller';
import { handleBlockCache, handleChainTipCache } from '../../../api/controllers/cache-controller';
import { BlockParamsSchema, cleanBlockHeightOrHashParam, parseBlockParam } from './schemas';
import { parseDbNakamotoBlock } from './helpers';
import { InvalidRequestError, NotFoundError } from '../../../errors';
Expand Down Expand Up @@ -100,7 +100,7 @@ export const BlockRoutesV2: FastifyPluginAsync<
fastify.get(
'/:height_or_hash',
{
preHandler: handleChainTipCache,
preHandler: handleBlockCache,
preValidation: (req, _reply, done) => {
cleanBlockHeightOrHashParam(req.params);
done();
Expand Down Expand Up @@ -129,7 +129,7 @@ export const BlockRoutesV2: FastifyPluginAsync<
fastify.get(
'/:height_or_hash/transactions',
{
preHandler: handleChainTipCache,
preHandler: handleBlockCache,
preValidation: (req, _reply, done) => {
cleanBlockHeightOrHashParam(req.params);
done();
Expand Down
22 changes: 22 additions & 0 deletions src/datastore/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
import * as path from 'path';
import { PgStoreV2 } from './pg-store-v2';
import { Fragment } from 'postgres';
import { parseBlockParam } from '../api/routes/v2/schemas';

export const MIGRATIONS_DIR = path.join(REPO_DIR, 'migrations');

Expand Down Expand Up @@ -4459,4 +4460,25 @@ export class PgStore extends BasePgStore {
`;
return result.map(r => r.tx_id);
}

/** Returns the `index_block_hash` and canonical status of a single block */
async getBlockCanonicalStatus(
height_or_hash: string | number
): Promise<{ index_block_hash: string; canonical: boolean } | undefined> {
const param = parseBlockParam(height_or_hash);
const result = await this.sql<{ index_block_hash: string; canonical: boolean }[]>`
SELECT index_block_hash, canonical
FROM blocks
WHERE
${
param.type == 'latest'
? this.sql`index_block_hash = (SELECT index_block_hash FROM chain_tip)`
: param.type == 'hash'
? this.sql`index_block_hash = ${param.hash}`
: this.sql`block_height = ${param.height} AND canonical = true`
}
LIMIT 1
`;
if (result.count) return result[0];
}
}
104 changes: 101 additions & 3 deletions tests/api/cache-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ import { startApiServer, ApiServer } from '../../src/api/init';
import { I32_MAX } from '../../src/helpers';
import { TestBlockBuilder, testMempoolTx } from '../utils/test-builders';
import { PgWriteStore } from '../../src/datastore/pg-write-store';
import { PgSqlClient, bufferToHex } from '@hirosystems/api-toolkit';
import { bufferToHex } from '@hirosystems/api-toolkit';
import { migrate } from '../utils/test-helpers';

describe('cache-control tests', () => {
let db: PgWriteStore;
let client: PgSqlClient;
let api: ApiServer;

beforeEach(async () => {
Expand All @@ -27,7 +26,6 @@ describe('cache-control tests', () => {
withNotifier: false,
skipMigrations: true,
});
client = db.sql;
api = await startApiServer({ datastore: db, chainId: ChainID.Testnet });
});

Expand Down Expand Up @@ -818,4 +816,104 @@ describe('cache-control tests', () => {
expect(request8.status).toBe(304);
expect(request8.text).toBe('');
});

test('block cache control', async () => {
await db.update(
new TestBlockBuilder({
block_height: 1,
index_block_hash: '0x01',
parent_index_block_hash: '0x00',
}).build()
);
await db.update(
new TestBlockBuilder({
block_height: 2,
index_block_hash: '0x8f652ee1f26bfbffe3cf111994ade25286687b76e6a2f64c33b4632a1f4545ac',
parent_index_block_hash: '0x01',
}).build()
);

// Valid latest Etag.
const request1 = await supertest(api.server).get(`/extended/v2/blocks/latest`);
expect(request1.status).toBe(200);
expect(request1.type).toBe('application/json');
const etag0 = request1.headers['etag'];

// Same block hash Etag.
const request2 = await supertest(api.server).get(
`/extended/v2/blocks/0x8f652ee1f26bfbffe3cf111994ade25286687b76e6a2f64c33b4632a1f4545ac`
);
expect(request2.status).toBe(200);
expect(request2.type).toBe('application/json');
expect(request2.headers['etag']).toEqual(etag0);

// Same block height Etag.
const request3 = await supertest(api.server).get(`/extended/v2/blocks/2`);
expect(request3.status).toBe(200);
expect(request3.type).toBe('application/json');
expect(request3.headers['etag']).toEqual(etag0);

// Cache works with valid ETag.
const request4 = await supertest(api.server)
.get(`/extended/v2/blocks/2`)
.set('If-None-Match', etag0);
expect(request4.status).toBe(304);
expect(request4.text).toBe('');

// Add new block.
await db.update(
new TestBlockBuilder({
block_height: 3,
index_block_hash: '0x03',
parent_index_block_hash:
'0x8f652ee1f26bfbffe3cf111994ade25286687b76e6a2f64c33b4632a1f4545ac',
}).build()
);

// Cache still works with same ETag.
const request5 = await supertest(api.server)
.get(`/extended/v2/blocks/2`)
.set('If-None-Match', etag0);
expect(request5.status).toBe(304);
expect(request5.text).toBe('');

// Re-org block 2
await db.update(
new TestBlockBuilder({
block_height: 2,
index_block_hash: '0x02bb',
parent_index_block_hash: '0x01',
}).build()
);
await db.update(
new TestBlockBuilder({
block_height: 3,
index_block_hash: '0x03bb',
parent_index_block_hash: '0x02bb',
}).build()
);
await db.update(
new TestBlockBuilder({
block_height: 4,
index_block_hash: '0x04bb',
parent_index_block_hash: '0x03bb',
}).build()
);

// Cache is now a miss.
const request6 = await supertest(api.server)
.get(`/extended/v2/blocks/2`)
.set('If-None-Match', etag0);
expect(request6.status).toBe(200);
expect(request6.type).toBe('application/json');
expect(request6.headers['etag']).not.toEqual(etag0);
const etag1 = request6.headers['etag'];

// Cache works with new ETag.
const request7 = await supertest(api.server)
.get(`/extended/v2/blocks/2`)
.set('If-None-Match', etag1);
expect(request7.status).toBe(304);
expect(request7.text).toBe('');
});
});

0 comments on commit 73fbbef

Please sign in to comment.