From 9cad6c16d34fdd11c1d9f473b2f3802a8da464d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Thu, 7 Mar 2024 17:45:05 -0600 Subject: [PATCH] fix: check only the first blessed inscription in next block on gap detection (#325) --- src/pg/helpers.ts | 26 ++--- src/pg/pg-store.ts | 13 +-- tests/ordhook/server.test.ts | 215 ----------------------------------- 3 files changed, 18 insertions(+), 236 deletions(-) diff --git a/src/pg/helpers.ts b/src/pg/helpers.ts index 25d5d504..e992a695 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -16,26 +16,26 @@ import { import { OrdinalSatoshi } from '../api/util/ordinal-satoshi'; /** - * Check if writing a block would create an inscription number gap + * Check if writing the next block would create an inscription number gap * @param currentNumber - Current max blessed number - * @param newNumbers - New blessed numbers to be inserted + * @param writes - Incoming inscription event data + * @param currentBlockHeight - Current height + * @param nextBlockHeight - Height to be inserted */ export function assertNoBlockInscriptionGap(args: { currentNumber: number; - newNumbers: number[]; + writes: InscriptionEventData[]; currentBlockHeight: number; - newBlockHeight: number; + nextBlockHeight: number; }) { if (!ENV.INSCRIPTION_GAP_DETECTION_ENABLED) return; - args.newNumbers.sort((a, b) => a - b); - for (let n = 0; n < args.newNumbers.length; n++) { - const curr = args.currentNumber + n; - const next = args.newNumbers[n]; - if (next !== curr + 1) - throw new BadPayloadRequestError( - `Block inscription gap detected: Attempting to insert #${next} (${args.newBlockHeight}) but current max is #${curr}. Chain tip is at ${args.currentBlockHeight}.` - ); - } + const nextReveal = args.writes.find(w => 'inscription' in w && w.inscription.number >= 0); + if (!nextReveal) return; + const next = (nextReveal as InscriptionRevealData).inscription.number; + if (next !== args.currentNumber + 1) + throw new BadPayloadRequestError( + `Block inscription gap detected: Attempting to insert #${next} (${args.nextBlockHeight}) but current max is #${args.currentNumber}. Chain tip is at ${args.currentBlockHeight}.` + ); } /** diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 5f643294..20b33abc 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -109,7 +109,7 @@ export class PgStore extends BasePgStore { // Check where we're at in terms of ingestion, e.g. block height and max blessed inscription // number. This will let us determine if we should skip ingesting this block or throw an // error if a gap is detected. - const currentBlessedNumber = (await this.getMaxInscriptionNumber()) ?? -1; + const currentNumber = (await this.getMaxInscriptionNumber()) ?? -1; const currentBlockHeight = await this.getChainTipBlockHeight(); const event = applyEvent as BitcoinEvent; if ( @@ -125,14 +125,11 @@ export class PgStore extends BasePgStore { logger.info(`PgStore ingesting block ${event.block_identifier.index}`); const time = stopwatch(); const writes = revealInsertsFromOrdhookEvent(event); - const newBlessedNumbers = writes - .filter(w => 'inscription' in w && w.inscription.number >= 0) - .map(w => (w as InscriptionRevealData).inscription.number ?? 0); assertNoBlockInscriptionGap({ - currentNumber: currentBlessedNumber, - newNumbers: newBlessedNumbers, - currentBlockHeight: currentBlockHeight, - newBlockHeight: event.block_identifier.index, + currentNumber, + writes, + currentBlockHeight, + nextBlockHeight: event.block_identifier.index, }); for (const writeChunk of batchIterate(writes, INSERT_BATCH_SIZE)) await this.insertInscriptions(writeChunk, payload.chainhook.is_streaming_blocks); diff --git a/tests/ordhook/server.test.ts b/tests/ordhook/server.test.ts index 09c96a88..05c7a3cc 100644 --- a/tests/ordhook/server.test.ts +++ b/tests/ordhook/server.test.ts @@ -521,221 +521,6 @@ describe('EventServer', () => { expect(response.statusCode).toBe(400); }); - test('server rejects payload with intermediate inscription gap', async () => { - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778575, - hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 0, jubilee: 0 }, - inscription_fee: 705, - inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', - inscription_output_value: 10000, - inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', - ordinal_number: 257418248345364, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .build() - ); - const errorPayload = new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778576, - hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 1, jubilee: 1 }, - inscription_fee: 705, - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .transaction({ - hash: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 4, jubilee: 4 }, // Gap - inscription_fee: 705, - inscription_id: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5o0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .build(); - await expect(db.updateInscriptions(errorPayload)).rejects.toThrow(BadPayloadRequestError); - const response = await server['fastify'].inject({ - method: 'POST', - url: `/payload`, - headers: { authorization: `Bearer ${ENV.ORDHOOK_NODE_AUTH_TOKEN}` }, - payload: errorPayload, - }); - expect(response.statusCode).toBe(400); - }); - - test('server accepts payload with unordered unbound inscriptions', async () => { - await db.updateInscriptions( - new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778575, - hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 0, jubilee: 0 }, - inscription_fee: 705, - inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', - inscription_output_value: 10000, - inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', - ordinal_number: 257418248345364, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .build() - ); - const unboundPayload = new TestChainhookPayloadBuilder() - .apply() - .block({ - height: 778576, - hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, - }) - .transaction({ - hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 2, jubilee: 2 }, - inscription_fee: 705, - inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .transaction({ - hash: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5', - }) - .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', - content_length: 5, - inscription_number: { classic: 1, jubilee: 1 }, - inscription_fee: 705, - inscription_id: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5o0', - inscription_output_value: 10000, - inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 0, // Unbounded - ordinal_block_height: 650000, - ordinal_offset: 0, - satpoint_post_inscription: - '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5:0:0', - inscription_input_index: 0, - transfers_pre_inscription: 0, - tx_index: 0, - curse_type: null, - inscription_pointer: null, - delegate: null, - metaprotocol: null, - metadata: null, - parent: null, - }) - .build(); - await expect(db.updateInscriptions(unboundPayload)).resolves.not.toThrow( - BadPayloadRequestError - ); - }); - test('server ignores past blocks', async () => { const payload = new TestChainhookPayloadBuilder() .apply()