Skip to content

Commit 2565e15

Browse files
committed
inscription-parser: recognise BIP-110 compatible envelopes
BIP-110 disables OP_IF and OP_NOTIF in tapscript and limits data pushes to 256 bytes. Adds an alternative inscription envelope parser: a bare protocol push followed by a series of data pushes balanced by OP_DROP and OP_2DROP. Matches the envelope shape proposed in ordinals/ord#4545 so we parse the same inscriptions the ord indexer will. Both envelope shapes share the same "ord" push (four bytes); a classic envelope is any occurrence preceded by OP_FALSE OP_IF, anything else is BIP-110. One marker, one scanner, one extractor handles both -- the terminator switches to OP_DROP / OP_2DROP for BIP-110 and the envelope-size math is a single subtraction, no fudge factors. Testing: no BIP-110 shape inscription exists on chain yet, so the spec uses a hand-crafted synthetic transaction (txid = "deadbeef" x 8). The GOLDEN RULE (every test uses real blockchain data) is waived here on purpose: this test pins the wire-format parsing so it works on day one when a real BIP-110 envelope lands. Add a real-tx spec when one exists on mainnet.
1 parent f090777 commit 2565e15

7 files changed

Lines changed: 192 additions & 155 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { readTransaction } from '../../testdata/test.helper';
2+
import { InscriptionParserService } from './inscription-parser.service';
3+
4+
/**
5+
* BIP-110 compatible envelope (ordinals/ord#4545). Payload identical to
6+
* the classic ord envelope; only the outer wrapper differs.
7+
*
8+
* SYNTHETIC DATA: no BIP-110 shape inscription exists on chain yet.
9+
* The fixture uses a "deadbeef"-repeat txid. GOLDEN RULE waived so the
10+
* wire-format parsing is pinned ahead of the first real tx.
11+
*/
12+
describe('BIP-110 compatible inscription envelope', () => {
13+
14+
it('parses a "Hello, world!" inscription wrapped in the BIP-110 envelope', async () => {
15+
16+
const txid = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
17+
const inscription = InscriptionParserService.parse(readTransaction(txid))[0];
18+
19+
expect(inscription.inscriptionId).toBe(`${txid}i0`);
20+
expect(inscription.contentType).toBe('text/plain');
21+
expect(await inscription.getContent()).toBe('Hello, world!');
22+
// 4-byte "ord" marker + 5 pushes (32 bytes) + 3 drops.
23+
expect(inscription.envelopeSize).toBe(35);
24+
});
25+
});

src/inscription/inscription-parser.service.envelope.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,6 @@ describe('Inscription parser', () => {
128128
expect(txWitness).toEqual('0000000000000000000000000000000000000000000000000000000000000000');
129129

130130
const raw = hexToBytes(txWitness);
131-
const position = getNextInscriptionMark(raw, 0);
132-
133-
expect(position).toEqual(-1);
131+
expect(getNextInscriptionMark(raw, 0)).toBeNull();
134132
});
135133
});

src/inscription/inscription-parser.service.helper.spec.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,37 +46,33 @@ describe('getKnownFieldValues', () => {
4646

4747
describe('getNextInscriptionMark', () => {
4848

49-
it('should find the inscription mark (00 63 03 6f 72 64) and return the position after it', () => {
49+
it('finds the classic inscription mark and returns the position after it', () => {
5050
const raw = new Uint8Array([0, 1, 2, 0x00, 0x63, 0x03, 0x6f, 0x72, 0x64, 10, 20]);
51-
const startPosition = 0;
52-
const expectedPosition = 9;
53-
expect(getNextInscriptionMark(raw, startPosition)).toEqual(expectedPosition);
51+
expect(getNextInscriptionMark(raw, 0)).toEqual({ pointer: 9, isClassic: true });
5452
});
5553

56-
it('should return -1 if the inscription mark is not found', () => {
54+
it('returns null when the mark is not found', () => {
5755
const raw = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
58-
const startPosition = 0;
59-
expect(getNextInscriptionMark(raw, startPosition)).toEqual(-1);
56+
expect(getNextInscriptionMark(raw, 0)).toBeNull();
6057
});
6158

62-
it('should correctly handle an empty array', () => {
63-
const raw = new Uint8Array([]);
64-
const startPosition = 0;
65-
expect(getNextInscriptionMark(raw, startPosition)).toEqual(-1);
59+
it('handles an empty array', () => {
60+
expect(getNextInscriptionMark(new Uint8Array([]), 0)).toBeNull();
6661
});
6762

68-
it('should find the inscription mark even if it starts next to the end of the array', () => {
63+
it('finds the mark near the end of the array', () => {
6964
const raw = new Uint8Array([0, 1, 2, 0x00, 0x63, 0x03, 0x6f, 0x72, 0x64]);
70-
const startPosition = 3;
71-
const expectedPosition = 9;
72-
expect(getNextInscriptionMark(raw, startPosition)).toEqual(expectedPosition);
65+
expect(getNextInscriptionMark(raw, 3)).toEqual({ pointer: 9, isClassic: true });
7366
});
7467

75-
it('should find the inscription mark starting exactly at the startPosition', () => {
68+
it('finds the mark at startPosition', () => {
7669
const raw = new Uint8Array([0x00, 0x63, 0x03, 0x6f, 0x72, 0x64, 10, 20, 30]);
77-
const startPosition = 0;
78-
const expectedPosition = 6;
79-
expect(getNextInscriptionMark(raw, startPosition)).toEqual(expectedPosition);
70+
expect(getNextInscriptionMark(raw, 0)).toEqual({ pointer: 6, isClassic: true });
71+
});
72+
73+
it('classifies a bare "ord" push (no OP_FALSE OP_IF prefix) as BIP-110', () => {
74+
const raw = new Uint8Array([0x03, 0x6f, 0x72, 0x64, 10, 20]);
75+
expect(getNextInscriptionMark(raw, 0)).toEqual({ pointer: 4, isClassic: false });
8076
});
8177
});
8278

src/inscription/inscription-parser.service.helper.ts

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -105,54 +105,51 @@ export function getKnownFieldValues(fields: { tag: number; value: Uint8Array }[]
105105
}
106106

107107
/**
108-
* Searches for the next position of the ordinal inscription mark (0063036f7264)
109-
* within the raw transaction data, starting from a given position.
108+
* Envelope marker hex constants -- kept as two exact patterns so hex
109+
* pre-filters (hasInscription, parseInscriptionsWithinWitness) match
110+
* the byte sequences ord itself emits, not a shorter substring that
111+
* would widen the false-positive surface.
110112
*
111-
* This function looks for a specific sequence of 6 bytes that represents the start of an ordinal inscription.
112-
* If the sequence is found, the function returns the index immediately following the inscription mark.
113-
* If the sequence is not found, the function returns -1, indicating no inscription mark was found.
114-
*
115-
* Note: This function uses a simple hardcoded approach based on the fixed length of the inscription mark.
113+
* INSCRIPTION_MARK_HEX = OP_FALSE OP_IF OP_PUSHBYTES_3 'ord'
114+
* BIP110_INSCRIPTION_MARK_HEX = OP_PUSHBYTES_3 'ord' (ord#4545)
115+
*/
116+
export const INSCRIPTION_MARK_HEX = '0063036f7264';
117+
export const BIP110_INSCRIPTION_MARK_HEX = '036f7264';
118+
119+
/**
120+
* Find the next "ord" push in the raw witness. Returns the position
121+
* immediately after the marker plus which envelope shape carries it
122+
* (classic if the marker is preceded by OP_FALSE OP_IF, BIP-110
123+
* otherwise).
116124
*
117-
* @returns The position immediately after the inscription mark, or -1 if not found.
125+
* @returns { pointer, isClassic } or null when no more markers.
118126
*/
119-
export function getNextInscriptionMark(raw: Uint8Array, startPosition: number): number {
120-
121-
// OP_FALSE
122-
// OP_IF
123-
// OP_PUSHBYTES_3: This pushes the next 3 bytes onto the stack.
124-
// 0x6f, 0x72, 0x64: These bytes translate to the ASCII string "ord"
125-
const inscriptionMark = new Uint8Array([OP_FALSE, OP_IF, OP_PUSHBYTES_3, 0x6f, 0x72, 0x64]);
126-
127-
for (let index = startPosition; index <= raw.length - 6; index++) {
128-
if (raw[index] === inscriptionMark[0] &&
129-
raw[index + 1] === inscriptionMark[1] &&
130-
raw[index + 2] === inscriptionMark[2] &&
131-
raw[index + 3] === inscriptionMark[3] &&
132-
raw[index + 4] === inscriptionMark[4] &&
133-
raw[index + 5] === inscriptionMark[5]) {
134-
return index + 6;
127+
export function getNextInscriptionMark(raw: Uint8Array, startPosition: number): { pointer: number; isClassic: boolean } | null {
128+
129+
for (let i = startPosition; i <= raw.length - 4; i++) {
130+
if (raw[i] === OP_PUSHBYTES_3 &&
131+
raw[i + 1] === 0x6f &&
132+
raw[i + 2] === 0x72 &&
133+
raw[i + 3] === 0x64) {
134+
const isClassic = i >= 2 && raw[i - 2] === OP_FALSE && raw[i - 1] === OP_IF;
135+
return { pointer: i + 4, isClassic };
135136
}
136137
}
137138

138-
return -1;
139+
return null;
139140
}
140141

141142
/**
142143
* Checks if an inscription mark is found within a witness array.
143-
* The Inscription mark hex corresponds to OP_FALSE, OP_IF, OP_PUSHBYTES_3, 'o', 'r', 'd'.
144-
145-
* This code can potentially return false positive matches!
144+
* Matches EITHER envelope shape via its exact hex pattern:
145+
* - classic (OP_FALSE OP_IF OP_PUSHBYTES_3 'ord')
146+
* - BIP-110 (OP_PUSHBYTES_3 'ord', ord#4545)
146147
*
147-
* @param witness - Array of strings, each representing a hexadecimal encoded witness element.
148-
* @returns True if an inscription mark is found, false otherwise.
148+
* This code can potentially return false positive matches!
149149
*/
150150
export function hasInscription(witness: string[]): boolean {
151-
152-
// OP_FALSE (0x00), OP_IF (0x63), OP_PUSHBYTES_3 (0x03), 'o', 'r', 'd' (0x6f, 0x72, 0x64)
153-
const inscriptionMarkHex = '0063036f7264';
154-
155-
return isStringInArrayOfStrings(inscriptionMarkHex, witness);
151+
return isStringInArrayOfStrings(INSCRIPTION_MARK_HEX, witness)
152+
|| isStringInArrayOfStrings(BIP110_INSCRIPTION_MARK_HEX, witness);
156153
}
157154

158155
/**
@@ -354,21 +351,20 @@ export function measureInscriptionSize(witness: string[]): number | null {
354351
return null;
355352
}
356353

357-
// Find the witness element that contains the inscription (the tapscript)
358-
// OP_FALSE (0x00), OP_IF (0x63), OP_PUSHBYTES_3 (0x03), 'o', 'r', 'd' (0x6f, 0x72, 0x64)
359-
const inscriptionMarkHex = '0063036f7264';
360-
const element = witness.find(e => e.includes(inscriptionMarkHex));
354+
// Find the witness element that contains the inscription (the tapscript).
355+
// The 4-byte "ord" push is the shared marker across classic + BIP-110.
356+
const element = witness.find(e => e.includes(INSCRIPTION_MARK_HEX));
361357
if (!element) {
362358
return null;
363359
}
364360

365361
const raw = hexToBytes(element);
366362

367-
// Find the start of the inscription using the inscription mark
368-
const startPosition = getNextInscriptionMark(raw, 0);
369-
370-
if (startPosition === -1) {
371-
return null; // Inscription mark not found
363+
const mark = getNextInscriptionMark(raw, 0);
364+
if (!mark || !mark.isClassic) {
365+
// Test-only helper; measuring a BIP-110 envelope would need the
366+
// drop-balance walk. Not needed today (no on-chain BIP-110 tx).
367+
return null;
372368
}
373369

374370
// Find the position of last OP_ENDIF (0x68)
@@ -378,11 +374,10 @@ export function measureInscriptionSize(witness: string[]): number | null {
378374
return null; // OP_ENDIF not found
379375
}
380376

381-
// The size of the inscription is from the start position to the last OP_ENDIF
382-
const inscriptionSize = opEndIfIndex - startPosition;
383-
384-
// Add the size of the inscription mark (6 bytes) + OP_ENDIF (1 byte)
385-
return inscriptionSize + 7;
377+
// From the start of the classic wrapper (OP_FALSE OP_IF = 2 bytes
378+
// before the 4-byte "ord" push, so mark.pointer - 6) to one past
379+
// OP_ENDIF.
380+
return opEndIfIndex + 1 - (mark.pointer - 6);
386381
}
387382

388383
/**

0 commit comments

Comments
 (0)