Skip to content

Commit

Permalink
fix: check only the first blessed inscription in next block on gap de…
Browse files Browse the repository at this point in the history
…tection (#325)
  • Loading branch information
rafaelcr committed Mar 7, 2024
1 parent 1a89df7 commit 9cad6c1
Show file tree
Hide file tree
Showing 3 changed files with 18 additions and 236 deletions.
26 changes: 13 additions & 13 deletions src/pg/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`
);
}

/**
Expand Down
13 changes: 5 additions & 8 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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);
Expand Down
215 changes: 0 additions & 215 deletions tests/ordhook/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 9cad6c1

Please sign in to comment.