Add native metaprotocol field

This commit is contained in:
recovery committed 2026-10-05 04:03:16 +00:00
1 parent 5d1a1ae866
commit bcc9f0f1cd
7 files changed
+52 -19

No files matched your search

+2 -2
View File
@@ -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
+2 -2
View File
@@ -23,7 +23,7 @@
<div class="wallet-footer"><label class="file-button" for="load-wallet">↑ Load test wallet or lab backup</label><input id="load-wallet" type="file" accept=".json,application/json"><p class="muted">Saved on this device. A wallet backup contains its test key; a lab backup also preserves the chain.</p></div>
</article>
<article class="panel"><div class="panel-title"><span class="step">02</span><h2>Make some blocks</h2></div><p>Skip the faucet queue. Every block gives you <strong>50 fake BTC</strong>, ready to use immediately.</p><div class="chain-stats"><div><span class="label">BLOCK HEIGHT</span><strong id="height">0</strong></div><div><span class="label">PENDING TXS</span><strong id="pending">0</strong></div></div><button id="mine" class="primary" disabled>Mine now <span>+</span></button><label for="miner-interval">Auto miner</label><select id="miner-interval"><option value="0">OFF</option><option value="10">10s</option><option value="30">30s</option><option value="60">60s</option></select><p id="miner-status" aria-live="polite">Auto mining OFF</p><p class="muted">Keep a lab tab open; browser throttling may delay blocks. Mining also confirms your pending inscriptions.</p></article>
<article class="panel inscribe"><div class="panel-title"><span class="step">03</span><h2>Leave a little something</h2><span class="tag">INSCRIPTION LAB</span></div><label for="mime-type">Content type</label><select id="mime-type"></select><label for="body-file">Or upload a local file (16 KiB maximum)</label><input id="body-file" type="file"><label for="parent">Parent inscription (optional, immutable)</label><select id="parent"><option value="">No parent</option></select><label for="delegate">Delegate inscription ID (optional, future IDs allowed)</label><input id="delegate" placeholder="64 hex characters + i0"><label for="reinscribe">Reinscribe an owned sat (optional)</label><select id="reinscribe"><option value="">New sat</option></select><label for="pointer">Advanced pointer (zero-based output sat position)</label><input id="pointer" inputmode="numeric" placeholder="Default first sat"><button id="recursive-demo" class="quiet">Use recursive demo</button><p class="muted">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.</p><label for="payload">What will you put on your chain?</label><textarea id="payload" placeholder="Hello, little Bitcoin universe…" rows="3" disabled></textarea><label for="metadata">JSON metadata (optional, independent of body)</label><textarea id="metadata" rows="3" placeholder='{"protocol":"my-experiment","note":"hello"}'></textarea><p id="metadata-count" class="muted">0 / 2048 CBOR bytes · ord tag 5 · JSON is authoring format only</p><div class="compose-footer"><span id="byte-count" class="muted">0 / 16384 bytes</span><button id="review" disabled>Review inscription ↗</button></div>
<article class="panel inscribe"><div class="panel-title"><span class="step">03</span><h2>Leave a little something</h2><span class="tag">INSCRIPTION LAB</span></div><label for="mime-type">Content type</label><select id="mime-type"></select><label for="body-file">Or upload a local file (16 KiB maximum)</label><input id="body-file" type="file"><label for="parent">Parent inscription (optional, immutable)</label><select id="parent"><option value="">No parent</option></select><label for="delegate">Delegate inscription ID (optional, future IDs allowed)</label><input id="delegate" placeholder="64 hex characters + i0"><label for="reinscribe">Reinscribe an owned sat (optional)</label><select id="reinscribe"><option value="">New sat</option></select><label for="pointer">Advanced pointer (zero-based output sat position)</label><input id="pointer" inputmode="numeric" placeholder="Default first sat"><button id="recursive-demo" class="quiet">Use recursive demo</button><p class="muted">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.</p><label for="payload">What will you put on your chain?</label><textarea id="payload" placeholder="Hello, little Bitcoin universe…" rows="3" disabled></textarea><label for="metadata">JSON metadata (optional, independent of body)</label><textarea id="metadata" rows="3" placeholder='{"protocol":"my-experiment","note":"hello"}'></textarea><p id="metadata-count" class="muted">0 / 2048 CBOR bytes · ord tag 5 · JSON is authoring format only</p><label for="metaprotocol">Metaprotocol (optional, independent field)</label><input id="metaprotocol" placeholder="e.g. tea-v1 · ord tag 7"><p class="muted">Native ord metaprotocol identifier. Metadata and inscription data remain independent.</p><div class="compose-footer"><span id="byte-count" class="muted">0 / 16384 bytes</span><button id="review" disabled>Review inscription ↗</button></div>
<div id="review-panel" hidden><h3>Review before signing</h3><div id="review-preview"></div><pre id="review-text"></pre><p id="review-parent"></p><pre id="review-metadata"></pre><p>Two transactions: commit + reveal. <strong>1,000 fake sats</strong> in fees; <strong>1,000 fake sats</strong> in a new wallet output. Any selected parent or reinscription output is also returned in full; reinscription appends to its existing sat stack.</p><p class="muted">The next button signs both transactions locally with this test wallet and submits them to your private chain.</p><button id="sign" class="primary">Sign & inscribe</button> <button id="cancel-review" class="quiet">Cancel</button></div>
</article>
</section>
@@ -40,7 +40,7 @@
<article id="inspector" class="panel inspector" hidden tabindex="-1"></article>
</section>
<section class="panel" id="indexes"><h2>Indexes</h2>
<p>Metadata is data; indexers are interpretation. Discovery uses the top-level metadata.protocol string. Raw events remain uninterpreted.</p>
<p>Metadata is data; indexers are interpretation. Native metaprotocol tag 7 is authoritative; metadata.protocol remains a compatibility fallback. Raw events remain uninterpreted.</p>
<p id="index-stats"></p><label for="protocol">Discovered protocol names</label><select id="protocol"></select>
<p id="protocol-stats"></p><h3>Ordered raw events</h3><p class="muted">Confirmed blocks in chain order, then mempool order.</p><div id="protocol-events"></div>
<label for="reducer">Optional trusted local indexer</label><select id="reducer"><option value="">None</option><option value="event-counter">Example: event-counter</option></select>
+1 -1
View File
@@ -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;
+15 -5
View File
@@ -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;
+4 -4
View File
@@ -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);
+6 -5
View File
@@ -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.');
+22
View File
@@ -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");
});