Skip to content

Commit

Permalink
fix: do not reject unbounded inscriptions that come in disorder (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr authored Nov 3, 2023
1 parent bdcd89a commit 56e2235
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 22 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@fastify/swagger": "^8.3.1",
"@fastify/type-provider-typebox": "^3.2.0",
"@hirosystems/api-toolkit": "^1.3.0",
"@hirosystems/chainhook-client": "^1.4.0",
"@hirosystems/chainhook-client": "^1.4.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^10.0.4",
"@semantic-release/git": "^10.0.1",
Expand Down
23 changes: 23 additions & 0 deletions src/pg/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import { PgBytea } from '@hirosystems/api-toolkit';
import { hexToBuffer } from '../api/util/helpers';
import { BadPayloadRequestError } from '@hirosystems/chainhook-client';

/**
* Check if writing a block would create an inscription number gap
* @param currentNumber - Current max blessed number
* @param newNumbers - New blessed numbers to be inserted
*/
export function assertNoBlockInscriptionGap(args: {
currentNumber: number;
newNumbers: number[];
currentBlockHeight: number;
newBlockHeight: number;
}) {
args.newNumbers.sort();
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}.`
);
}
}

/**
* Returns a list of referenced inscription ids from inscription content.
Expand Down
19 changes: 10 additions & 9 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ENV } from '../env';
import { Brc20PgStore } from './brc20/brc20-pg-store';
import { CountsPgStore } from './counts/counts-pg-store';
import { getIndexResultCountType } from './counts/helpers';
import { chunkArray, getInscriptionRecursion } from './helpers';
import { assertNoBlockInscriptionGap, chunkArray, getInscriptionRecursion } from './helpers';
import {
DbFullyLocatedInscriptionResult,
DbInscription,
Expand Down Expand Up @@ -90,8 +90,9 @@ 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.
let blessedNumber = (await this.getMaxInscriptionNumber()) ?? -1;
const currentBlessedNumber = (await this.getMaxInscriptionNumber()) ?? -1;
const currentBlockHeight = await this.getChainTipBlockHeight();
const newBlessedNumbers: number[] = [];

for (const rollbackEvent of payload.rollback) {
// TODO: Optimize rollbacks just as we optimized applys.
Expand Down Expand Up @@ -139,13 +140,7 @@ export class PgStore extends BasePgStore {
for (const operation of tx.metadata.ordinal_operations) {
if (operation.inscription_revealed) {
const reveal = operation.inscription_revealed;
if (reveal.inscription_number >= 0) {
if (blessedNumber + 1 !== reveal.inscription_number)
throw Error(
`PgStore inscription gap detected: Attempting to insert #${reveal.inscription_number} (${block_height}) but current max is #${blessedNumber}`
);
blessedNumber = reveal.inscription_number;
}
if (reveal.inscription_number >= 0) newBlessedNumbers.push(reveal.inscription_number);
const satoshi = new OrdinalSatoshi(reveal.ordinal_number);
const satpoint = parseSatPoint(reveal.satpoint_post_inscription);
const recursive_refs = getInscriptionRecursion(reveal.content_bytes);
Expand Down Expand Up @@ -251,6 +246,12 @@ export class PgStore extends BasePgStore {
}
}
}
assertNoBlockInscriptionGap({
currentNumber: currentBlessedNumber,
newNumbers: newBlessedNumbers,
currentBlockHeight: currentBlockHeight,
newBlockHeight: block_height,
});
// Divide insertion array into chunks of 4000 in order to avoid the postgres limit of 65534
// query params.
for (const writeChunk of chunkArray(writes, 4000))
Expand Down
108 changes: 103 additions & 5 deletions tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ENV } from '../src/env';
import { MIGRATIONS_DIR, PgStore } from '../src/pg/pg-store';
import { TestChainhookPayloadBuilder, TestFastifyServer } from './helpers';
import {
BadPayloadRequestError,
BitcoinInscriptionRevealed,
BitcoinInscriptionTransferred,
ChainhookEventObserver,
Expand Down Expand Up @@ -387,9 +388,14 @@ describe('EventServer', () => {
tx_index: 0,
})
.build();
await expect(db.updateInscriptions(errorPayload)).rejects.toThrowError(
/inscription gap detected/
);
await expect(db.updateInscriptions(errorPayload)).rejects.toThrow(BadPayloadRequestError);
const response = await server['fastify'].inject({
method: 'POST',
url: `/chainhook/${PREDICATE_UUID}`,
headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` },
payload: errorPayload,
});
expect(response.statusCode).toBe(400);
});

test('server rejects payload with intermediate inscription gap', async () => {
Expand Down Expand Up @@ -474,8 +480,100 @@ describe('EventServer', () => {
tx_index: 0,
})
.build();
await expect(db.updateInscriptions(errorPayload)).rejects.toThrowError(
/inscription gap detected/
await expect(db.updateInscriptions(errorPayload)).rejects.toThrow(BadPayloadRequestError);
const response = await server['fastify'].inject({
method: 'POST',
url: `/chainhook/${PREDICATE_UUID}`,
headers: { authorization: `Bearer ${ENV.CHAINHOOK_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: 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,
})
.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: 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,
})
.transaction({
hash: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5',
})
.inscriptionRevealed({
content_bytes: '0x48656C6C6F',
content_type: 'text/plain;charset=utf-8',
content_length: 5,
inscription_number: 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,
})
.build();
await expect(db.updateInscriptions(unboundPayload)).resolves.not.toThrow(
BadPayloadRequestError
);
});

Expand Down

0 comments on commit 56e2235

Please sign in to comment.