From bcc9f0f1cdae644315b0a94819cf87dd82df2ff8 Mon Sep 17 00:00:00 2001 From: recovery Date: Mon, 5 Oct 2026 04:03:16 +0000 Subject: [PATCH] Add native metaprotocol field --- README.md | 4 ++-- index.html | 4 ++-- src/adapters/chain.js | 2 +- src/adapters/inscription.js | 20 +++++++++++++++----- src/adapters/metadata-index.js | 8 ++++---- src/main.js | 11 ++++++----- tests/adapters.test.js | 22 ++++++++++++++++++++++ 7 files changed, 52 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a205daa..ca51004 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Wallet ownership is derived from the unspent reveal output’s script, not a dis Collectible cards and details show metadata decoded from inscription witnesses. **Metadata is data; indexers are interpretation.** `MetadataIndex` reconstructs every inscription event from confirmed block raw transactions in chain order, followed by raw mempool transactions. Events include id, content type, status, height/block hash, metadata, presence flag and parent. No UI metadata sidecar is persisted. A top-level string `metadata.protocol` is the default discovery convention; other fields and values have no built-in semantics. The Indexes surface shows counts, discovered names and the ordered feed for a selected protocol. -The optional `event-counter` reducer derives counts and ordered ids from only the selected protocol's events, displayed separately. Reducers are trusted application code registered above the base index, never downloaded or executed from inscriptions. Adding a reducer requires a source-code change; this is a seam for future metaprotocol experiments, not a sidechain consensus engine. Native ord tag 7 is a separate future compatibility feature and is not conflated with `metadata.protocol`. +The optional `event-counter` reducer derives counts and ordered ids from only the selected protocol's events, displayed separately. Reducers are trusted application code registered above the base index, never downloaded or executed from inscriptions. Adding a reducer requires a source-code change; this is a seam for future metaprotocol experiments, not a sidechain consensus engine. Native ord metaprotocol tag 7 is supported as an independent inscription field. It is authoritative for protocol grouping; `metadata.protocol` remains a compatibility fallback when tag 7 is absent. The **LOCAL / IN-BROWSER** API explorer calls `LocalOrdAPI.request(path)` and displays/copies its `{status, contentType, body}` response. No HTTP request is made. Fly/the static server cannot read browser state. Available paths: @@ -66,7 +66,7 @@ Sat identity is **synthetic-local-fifo**, not a Bitcoin ordinal number. Height ` Select **Reinscribe an owned sat** to spend and return its current output while appending an inscription to that same sat. The original body and id remain, stack positions start at zero and ordering survives mining/replay/spends. A new 1,000-sat output is also created by the fixed commit/reveal template; reinscription targets the carried sat, not that new output. Parent and reinscription selection are mutually exclusive. The reinscription target determines its pointer. -Advanced pointer tag 2 is an unsigned 64-bit zero-based position across all reveal outputs. Encoding is little-endian with trailing zero bytes omitted (zero is empty); a pointer beyond total output value falls back to the first output sat. There is **one inscription per reveal**. Multiple inscriptions on one sat come from separate reveals. Actual Bitcoin subsidy schedules, miner fee recycling, multiple envelopes/reveal, arbitrary DOM recursion, native tag 7 and regtest/public backends are deferred. +Advanced pointer tag 2 is an unsigned 64-bit zero-based position across all reveal outputs. Encoding is little-endian with trailing zero bytes omitted (zero is empty); a pointer beyond total output value falls back to the first output sat. There is **one inscription per reveal**. Multiple inscriptions on one sat come from separate reveals. Actual Bitcoin subsidy schedules, miner fee recycling, multiple envelopes/reveal, arbitrary DOM recursion and regtest/public backends are deferred. ## Mining at your pace diff --git a/index.html b/index.html index acbd58e..c37a83c 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@
02

Make some blocks

Skip the faucet queue. Every block gives you 50 fake BTC, ready to use immediately.

BLOCK HEIGHT0
PENDING TXS0

Auto mining OFF

Keep a lab tab open; browser throttling may delay blocks. Mining also confirms your pending inscriptions.

-
03

Leave a little something

INSCRIPTION LAB

Recursive HTML/SVG scripts use local fetch and ord.render(text) in an isolated worker; ord.id is the requested inscription. No DOM or network access.

0 / 2048 CBOR bytes · ord tag 5 · JSON is authoring format only

+
03

Leave a little something

INSCRIPTION LAB

Recursive HTML/SVG scripts use local fetch and ord.render(text) in an isolated worker; ord.id is the requested inscription. No DOM or network access.

0 / 2048 CBOR bytes · ord tag 5 · JSON is authoring format only

Native ord metaprotocol identifier. Metadata and inscription data remain independent.

@@ -40,7 +40,7 @@

Indexes

-

Metadata is data; indexers are interpretation. Discovery uses the top-level metadata.protocol string. Raw events remain uninterpreted.

+

Metadata is data; indexers are interpretation. Native metaprotocol tag 7 is authoritative; metadata.protocol remains a compatibility fallback. Raw events remain uninterpreted.

Ordered raw events

Confirmed blocks in chain order, then mempool order.

diff --git a/src/adapters/chain.js b/src/adapters/chain.js index e3f367b..3bbf5fc 100644 --- a/src/adapters/chain.js +++ b/src/adapters/chain.js @@ -83,7 +83,7 @@ export class LocalChain { } config = { pubkey: parsed.pubkey }; inscription = { id: `${txid}i0`, txid, commitTxid: input.txid, text: parsed.text, - bytes: Array.from(parsed.bytes), contentType: parsed.contentType, parent: parsed.parent, delegate: parsed.delegate, pointer: parsed.pointer, metadata: parsed.metadata, height, value: POSTAGE }; + bytes: Array.from(parsed.bytes), contentType: parsed.contentType, parent: parsed.parent, delegate: parsed.delegate, pointer: parsed.pointer, metaprotocol: parsed.metaprotocol, metadata: parsed.metadata, height, value: POSTAGE }; } tx.vin.forEach((input, i) => { let valid = false; diff --git a/src/adapters/inscription.js b/src/adapters/inscription.js index 8a4a81d..2778edb 100644 --- a/src/adapters/inscription.js +++ b/src/adapters/inscription.js @@ -40,7 +40,9 @@ function parseParent(value) { // Local extension: MIME bytes, 520-byte pushes, optional ord parent tag 3. export function envelope(pubkey, value, parent = null, metadata = undefined, options = {}) { const { contentType, bytes } = body(value); - const { delegate = null, pointer } = options; + const { delegate = null, pointer, metaprotocol = null } = options; + const metaprotocolBytes = metaprotocol === null || metaprotocol === undefined || metaprotocol === "" ? null : new TextEncoder().encode(String(metaprotocol)); + if (metaprotocolBytes && metaprotocolBytes.length > 520) throw new Error("Metaprotocol identifier must be 520 bytes or less."); const metadataBytes = metadata === undefined ? undefined : encodeMetadata(metadata); const chunks = []; for (let i = 0; i < bytes.length; i += 520) chunks.push(hex(bytes.slice(i, i + 520))); @@ -57,7 +59,8 @@ export function envelope(pubkey, value, parent = null, metadata = undefined, opt (pointer === undefined ? '' : push('02') + push(encodePointer(pointer))) + (parent ? push('03') + push(serializeParent(parent)) : '') + (metadataBytes === undefined ? '' : Array.from({ length: Math.ceil(metadataBytes.length / 520) }, (_, i) => - push('05') + push(hex(metadataBytes.slice(i * 520, (i + 1) * 520)))).join('')) + '00' + chunks.map(push).join('') + '68'; + push('05') + push(hex(metadataBytes.slice(i * 520, (i + 1) * 520)))).join('')) + + (metaprotocolBytes ? push('07') + push(hex(metaprotocolBytes)) : '') + '00' + chunks.map(push).join('') + '68'; } export function readEnvelope(witness) { if (!Array.isArray(witness) || witness.length !== 3) throw new Error('Unsupported script-path witness.'); @@ -72,19 +75,25 @@ export function readEnvelope(witness) { if (typeof chunk !== 'string' || !/^(?:[0-9a-f]{2}){1,520}$/.test(chunk)) throw new Error('Invalid metadata push.'); metadataChunks.push(chunk); cursor += 2; } - if (parts[cursor] !== 'OP_0') throw new Error('Unsupported ord tag or missing body separator.'); const metadata = metadataChunks.length ? decodeMetadata(unhex(metadataChunks.join(''))) : undefined; + let metaprotocol = null; + if (parts[cursor] === '07') { + const raw = parts[cursor + 1]; + if (typeof raw !== 'string' || !/^(?:[0-9a-f]{2}){1,520}$/.test(raw)) throw new Error('Invalid metaprotocol field.'); + metaprotocol = new TextDecoder().decode(unhex(raw)); cursor += 2; + } + if (parts[cursor] !== 'OP_0') throw new Error('Unsupported ord tag or missing body separator.'); const payload = parts.slice(cursor + 1, -1); if (!payload.length || payload.some(p => typeof p !== 'string' || !/^(?:[0-9a-f]{2})+$/.test(p))) throw new Error('Invalid body envelope.'); const contentType = new TextDecoder().decode(unhex(parts[6])); const value = body({ contentType, bytes: unhex(payload.join('')) }); - const expected = envelope(parts[0], value, parent, metadata, { delegate, pointer }); + const expected = envelope(parts[0], value, parent, metadata, { delegate, pointer, metaprotocol }); // Read original v0 text snapshots whose content-type tag used OP_1. const legacy = metadata === undefined && !parent && contentType === MIME && value.bytes.length <= 280 && Script.encode([parts[0], 'OP_CHECKSIG', 'OP_0', 'OP_IF', textHex('ord'), '01', textHex(MIME), 'OP_0', hex(value.bytes), 'OP_ENDIF'], false).hex; if (expected !== witness[1] && legacy !== witness[1]) throw new Error('Unsupported ord envelope.'); if (!/^[0-9a-f]{64}$/.test(parts[0])) throw new Error('Invalid inscription signing key.'); - return { ...value, parent, delegate, pointer, metadata, metadataBytes: metadataChunks.length ? unhex(metadataChunks.join('')) : undefined, text: new TextDecoder().decode(value.bytes), pubkey: parts[0] }; + return { ...value, parent, delegate, pointer, metaprotocol, metadata, metadataBytes: metadataChunks.length ? unhex(metadataChunks.join('')) : undefined, text: new TextDecoder().decode(value.bytes), pubkey: parts[0] }; } export class InscriptionAdapter { constructor(chain, wallet) { this.chain = chain; this.wallet = wallet; } @@ -93,6 +102,7 @@ export class InscriptionAdapter { if (metadata !== undefined) encodeMetadata(metadata); if (options.delegate) serializeParent(options.delegate); if (options.pointer !== undefined) encodePointer(options.pointer); + if (options.metaprotocol !== undefined && options.metaprotocol !== null && options.metaprotocol !== '' && new TextEncoder().encode(String(options.metaprotocol)).length > 520) throw new Error('Metaprotocol identifier must be 520 bytes or less.'); if (parentId && options.reinscribe) throw new Error('Choose parent or reinscription, not both.'); const coins = this.chain.coins(this.wallet.script); const parent = parentId ? coins.find(c => c.inscriptions?.some(i => i.id === parentId)) : null; diff --git a/src/adapters/metadata-index.js b/src/adapters/metadata-index.js index 68dda34..ddea79f 100644 --- a/src/adapters/metadata-index.js +++ b/src/adapters/metadata-index.js @@ -3,7 +3,7 @@ import { Tx } from '@cmdcode/tapscript'; import { readEnvelope } from './inscription.js'; // Rebuild solely from serialized transactions, never collectible/UI sidecars. -// Discovery is a convention, not native ord metaprotocol tag 7 support. +// Native ord metaprotocol tag 7 is authoritative; metadata.protocol remains a compatibility fallback. export class MetadataIndex { constructor(chain) { this.events = []; @@ -21,12 +21,12 @@ export class MetadataIndex { const pointer = BigInt(item.pointer ?? 0), total = tx.vout.reduce((n, o) => n + Number(o.value), 0); const sat = satAt(ranges.flat(), pointer < BigInt(total) ? Number(pointer) : 0); const metadata = item.metadata; - this.events.push({ id: `${Tx.util.getTxid(tx)}i0`, contentType: item.contentType, delegate: item.delegate, pointer: item.pointer, sat, + this.events.push({ id: `${Tx.util.getTxid(tx)}i0`, contentType: item.contentType, delegate: item.delegate, pointer: item.pointer, metaprotocol: item.metaprotocol, sat, reinscriptionPosition: this.events.filter(e => e.sat === sat).length, status: height === null ? 'pending' : 'confirmed', height, block, metadata: metadata ?? null, hasMetadata: metadata !== undefined, parent: item.parent, - protocol: metadata && !Array.isArray(metadata) && typeof metadata === 'object' && - Object.hasOwn(metadata, 'protocol') && typeof metadata.protocol === 'string' ? metadata.protocol : null }); + protocol: item.metaprotocol ?? (metadata && !Array.isArray(metadata) && typeof metadata === 'object' && + Object.hasOwn(metadata, 'protocol') && typeof metadata.protocol === 'string' ? metadata.protocol : null) }); }; for (const block of chain.blocks) for (const raw of block.raws) visit(raw, block.height, block.hash); for (const raw of chain.mempool) visit(raw, null, null); diff --git a/src/main.js b/src/main.js index b7b801d..b84db1d 100644 --- a/src/main.js +++ b/src/main.js @@ -58,7 +58,7 @@ const composerBody = () => ({ contentType: $('mime-type').value, bytes: fileByte function updateCount() { $('byte-count').textContent = `${composerBody().bytes.length} / ${MAX_BYTES} bytes`; } function resetComposer() { fileRevision++; fileBytes = null; $('body-file').value = ''; $('payload').value = ''; - $('delegate').value = ''; $('pointer').value = ''; $('reinscribe').value = ''; $('metadata').value = ''; updateMetadataCount(); $('parent').value = ''; closeReview(); updateCount(); + $('delegate').value = ''; $('pointer').value = ''; $('reinscribe').value = ''; $('metaprotocol').value = ''; $('metadata').value = ''; updateMetadataCount(); $('parent').value = ''; closeReview(); updateCount(); } function lineage(item, chain) { const p = el('p', undefined, 'tx-links'); @@ -86,6 +86,7 @@ function updateMetadataCount() { function metadataView(item) { const node = el('div', undefined, 'metadata-document'); if (item.metadata !== undefined) node.append(el('strong', 'Decoded metadata'), el('pre', JSON.stringify(item.metadata, null, 2))); + if (item.metaprotocol) node.append(el('strong', `Metaprotocol: ${item.metaprotocol}`)); return node; } function renderIndexes(chain) { @@ -172,7 +173,7 @@ function render() { $('miner-interval').value = String(lab?.miner?.interval ?? 0); $('miner-status').textContent = minerStatus(lab); $('payload').disabled = disabled || !wallet || !!fileBytes || fileLoading || $('mime-type').value.startsWith('image/') && $('mime-type').value !== 'image/svg+xml'; - for (const id of ['mime-type', 'body-file', 'parent', 'metadata', 'delegate', 'pointer', 'reinscribe', 'recursive-demo']) $(id).disabled = disabled || !wallet || fileLoading; + for (const id of ['mime-type', 'body-file', 'parent', 'metadata', 'metaprotocol', 'delegate', 'pointer', 'reinscribe', 'recursive-demo']) $(id).disabled = disabled || !wallet || fileLoading; $('review').disabled = disabled || !wallet || !chain?.coins(wallet.script).some(c => !c.inscription) || fileLoading || !composerBody().bytes.length; $('sign').disabled = disabled; $('export-wallet').disabled = disabled; $('export-lab').disabled = busy; @@ -245,7 +246,7 @@ $('metadata').oninput = () => { closeReview(); updateMetadataCount(); }; MIME_TYPES.forEach(type => $('mime-type').append(new Option(type, type))); $('payload').oninput = () => { closeReview(); updateCount(); render(); }; $('parent').onchange = $('reinscribe').onchange = closeReview; -$('delegate').oninput = $('pointer').oninput = closeReview; +$('delegate').oninput = $('pointer').oninput = $('metaprotocol').oninput = closeReview; $('recursive-demo').onclick = () => { fileRevision++; fileBytes = null; $('body-file').value = ''; $('mime-type').value = 'text/html'; $('payload').value = RECURSIVE_DEMO; closeReview(); updateCount(); render(); @@ -271,14 +272,14 @@ $('body-file').onchange = async event => { $('review').onclick = () => { try { const payload = composerBody(), parent = $('parent').value || null, metadata = parseMetadataJSON($('metadata').value); - const options = { delegate: $('delegate').value.trim() || null, reinscribe: $('reinscribe').value || null, ...($('pointer').value.trim() ? { pointer: $('pointer').value.trim() } : {}) }; + const options = { delegate: $('delegate').value.trim() || null, metaprotocol: $('metaprotocol').value.trim() || null, reinscribe: $('reinscribe').value || null, ...($('pointer').value.trim() ? { pointer: $('pointer').value.trim() } : {}) }; new InscriptionAdapter(lab.chain, lab.wallet).prepare(payload, parent, metadata, options); closeReview(); reviewed = { payload, parent, metadata, options, address: lab.wallet.address }; $('review-text').textContent = ['text/plain', 'application/json'].includes(payload.contentType) ? new TextDecoder().decode(payload.bytes) : `${payload.contentType} · ${payload.bytes.length} bytes`; if (!['text/plain', 'application/json'].includes(payload.contentType)) $('review-preview').append(preview(payload)); $('review-parent').textContent = parent ? `Parent: ${parent}. Its output will be spent and its full value returned to this wallet. This relationship is immutable.` : 'No parent.'; - $('review-parent').textContent += ` Delegate: ${options.delegate ?? 'none'}. Reinscribe: ${options.reinscribe ?? 'none'}. Pointer: ${options.pointer ?? 'automatic'}.`; + $('review-parent').textContent += ` Delegate: ${options.delegate ?? 'none'}. Reinscribe: ${options.reinscribe ?? 'none'}. Pointer: ${options.pointer ?? 'automatic'}. Metaprotocol: ${options.metaprotocol ?? 'none'}.`; $('review-metadata').textContent = metadata === undefined ? 'No metadata.' : `Metadata (CBOR, tag 5): ${JSON.stringify(metadata, null, 2)}`; $('review-panel').hidden = false; $('sign').focus(); message('Review the payload and fake-sat costs. Nothing has been signed yet.'); diff --git a/tests/adapters.test.js b/tests/adapters.test.js index c31022e..686caa4 100644 --- a/tests/adapters.test.js +++ b/tests/adapters.test.js @@ -453,3 +453,25 @@ test('multiple input ranges flow FIFO across outputs and protect inscribed fee t assert.throws(()=>chain.submitPackage([Tx.encode(burn).hex]),/fee loss/); assert.deepEqual(chain.snapshot(),after); assert.ok(after.mempool.length>before.mempool.length); }); + +test("native metaprotocol tag 7 round-trips and overrides metadata.protocol for tracking", () => { + const {ins,chain} = setup(); + const id = ins.inscribe("hello", null, {protocol:"metadata-fallback", note:"kept"}, {metaprotocol:"tea-v1"}); + const record = chain.transactions.get(id.slice(0,-2)); + const witness = record.tx.vin.find(i => i.witness?.length === 3).witness; + const item = readEnvelope(witness); + assert.equal(item.metaprotocol, "tea-v1"); + assert.deepEqual(item.metadata, {protocol:"metadata-fallback", note:"kept"}); + const event = new MetadataIndex(chain).events.find(e => e.id === id); + assert.equal(event.metaprotocol, "tea-v1"); + assert.equal(event.protocol, "tea-v1"); + assert.deepEqual(new MetadataIndex(chain).forProtocol("tea-v1").map(e => e.id), [id]); +}); + +test("metadata.protocol remains a fallback when native metaprotocol is absent", () => { + const {ins,chain} = setup(); + const id = ins.inscribe("legacy", null, {protocol:"legacy-protocol"}); + const event = new MetadataIndex(chain).events.find(e => e.id === id); + assert.equal(event.metaprotocol, null); + assert.equal(event.protocol, "legacy-protocol"); +});