const TE = new TextEncoder(); export const ANS104_ED25519_SIGNATURE_TYPE = 2; function concatBytes(...chunks) { const len = chunks.reduce((n, c) => n + c.length, 0); const out = new Uint8Array(len); let p = 0; for (const c of chunks) { out.set(c, p); p += c.length; } return out; } function uint16LE(value) { const out = new Uint8Array(2); new DataView(out.buffer).setUint16(0, Number(value), true); return out; } function uint64LE(value) { let v = BigInt(value); if (v < 0n) throw new Error('uint64LE: negative value'); const out = new Uint8Array(8); for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; } return out; } function encodeAvroLong(value) { let n = BigInt(value); // Avro zig-zag signed long. let u = n >= 0n ? (n << 1n) : ((-n << 1n) - 1n); const bytes = []; do { let b = Number(u & 0x7fn); u >>= 7n; if (u !== 0n) b |= 0x80; bytes.push(b); } while (u !== 0n); return Uint8Array.from(bytes); } function encodeAvroString(value) { const bytes = TE.encode(String(value)); return concatBytes(encodeAvroLong(bytes.length), bytes); } /** Serialize ANS-104 tags as the Avro array used by arbundles/Turbo. */ export function serializeAns104Tags(tags = []) { const normalized = tags.map(({ name, value }) => ({ name: String(name ?? ''), value: String(value ?? ''), })); if (normalized.length > 128) throw new Error('ANS-104 supports at most 128 tags'); for (const tag of normalized) { if (!tag.name || !tag.value) throw new Error('ANS-104 tag name/value must be non-empty'); if (TE.encode(tag.name).length > 1024) throw new Error('ANS-104 tag name exceeds 1024 bytes'); if (TE.encode(tag.value).length > 3072) throw new Error('ANS-104 tag value exceeds 3072 bytes'); } if (normalized.length === 0) return Uint8Array.of(0); return concatBytes( encodeAvroLong(normalized.length), ...normalized.flatMap((tag) => [encodeAvroString(tag.name), encodeAvroString(tag.value)]), Uint8Array.of(0), ); } async function digest(name, bytes) { return new Uint8Array(await crypto.subtle.digest(name, bytes)); } async function deepHashBlob(bytes) { const tag = TE.encode(`blob${bytes.length}`); return digest('SHA-384', concatBytes(await digest('SHA-384', tag), await digest('SHA-384', bytes))); } async function deepHash(item) { if (item instanceof Uint8Array) return deepHashBlob(item); if (Array.isArray(item)) { let acc = await digest('SHA-384', TE.encode(`list${item.length}`)); for (const child of item) { acc = await digest('SHA-384', concatBytes(acc, await deepHash(child))); } return acc; } throw new Error('Unsupported ANS-104 deepHash item'); } /** * Build the exact signing message used by current arbundles/Turbo DataItems. * SHiNE intentionally uses no target and no anchor. */ export async function ans104SigningMessage({ owner32, tags = [], data }) { if (!(owner32 instanceof Uint8Array) || owner32.length !== 32) throw new Error('ANS-104 owner must be 32 bytes'); if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array'); const rawTags = serializeAns104Tags(tags); return deepHash([ TE.encode('dataitem'), TE.encode('1'), TE.encode(String(ANS104_ED25519_SIGNATURE_TYPE)), owner32, new Uint8Array(0), new Uint8Array(0), rawTags, data, ]); } /** Create one complete serialized ANS-104 DataItem signed with Ed25519. */ export async function createAns104DataItem({ owner32, privateKey, data, tags = [] }) { if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array'); const rawTags = serializeAns104Tags(tags); const signingMessage = await ans104SigningMessage({ owner32, tags, data }); const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, signingMessage)); if (signature.length !== 64) throw new Error(`Unexpected Ed25519 signature length: ${signature.length}`); return concatBytes( uint16LE(ANS104_ED25519_SIGNATURE_TYPE), signature, owner32, Uint8Array.of(0), // target absent Uint8Array.of(0), // anchor absent uint64LE(tags.length), uint64LE(rawTags.length), rawTags, data, ); } function readUint64LE(bytes, offset) { if (!(bytes instanceof Uint8Array) || offset < 0 || offset + 8 > bytes.length) throw new Error('ANS-104 truncated uint64'); let value = 0n; for (let i = 7; i >= 0; i -= 1) value = (value << 8n) | BigInt(bytes[offset + i]); if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('ANS-104 uint64 exceeds JS safe integer'); return Number(value); } /** Parse enough of an Ed25519 ANS-104 DataItem to re-sign its exact SHiNE payload. */ export function parseAns104DataItem(rawBytes) { const raw = rawBytes instanceof Uint8Array ? rawBytes : Uint8Array.from(rawBytes || []); let p = 0; if (raw.length < 2 + 64 + 32 + 1 + 1 + 8 + 8) throw new Error('ANS-104 DataItem is too short'); const sigType = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint16(p, true); p += 2; if (sigType !== ANS104_ED25519_SIGNATURE_TYPE) throw new Error(`Unsupported ANS-104 signature type: ${sigType}`); const signature = raw.slice(p, p + 64); p += 64; const owner32 = raw.slice(p, p + 32); p += 32; const targetPresent = raw[p++] !== 0; const target = targetPresent ? raw.slice(p, p + 32) : new Uint8Array(0); if (targetPresent) p += 32; const anchorPresent = raw[p++] !== 0; const anchor = anchorPresent ? raw.slice(p, p + 32) : new Uint8Array(0); if (anchorPresent) p += 32; const tagsCount = readUint64LE(raw, p); p += 8; const tagsBytesLength = readUint64LE(raw, p); p += 8; if (p + tagsBytesLength > raw.length) throw new Error('ANS-104 tags are truncated'); const rawTags = raw.slice(p, p + tagsBytesLength); p += tagsBytesLength; const data = raw.slice(p); return { sigType, signature, owner32, target, anchor, tagsCount, rawTags, data, raw }; } async function ans104SigningMessageRaw({ owner32, target = new Uint8Array(0), anchor = new Uint8Array(0), rawTags, data }) { if (!(owner32 instanceof Uint8Array) || owner32.length !== 32) throw new Error('ANS-104 owner must be 32 bytes'); if (!(target instanceof Uint8Array) || (target.length !== 0 && target.length !== 32)) throw new Error('ANS-104 target must be empty or 32 bytes'); if (!(anchor instanceof Uint8Array) || (anchor.length !== 0 && anchor.length !== 32)) throw new Error('ANS-104 anchor must be empty or 32 bytes'); if (!(rawTags instanceof Uint8Array)) throw new Error('ANS-104 rawTags must be Uint8Array'); if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array'); return deepHash([ TE.encode('dataitem'), TE.encode('1'), TE.encode(String(ANS104_ED25519_SIGNATURE_TYPE)), owner32, target, anchor, rawTags, data, ]); } /** * Re-sign a DataItem while preserving exact raw tags/target/anchor/data bytes. * Used by key rotation so SHA-256(SHiNE Frame) is unchanged across forks. */ export async function createAns104DataItemWithRawParts({ owner32, privateKey, data, rawTags, tagsCount, target = new Uint8Array(0), anchor = new Uint8Array(0), }) { const signingMessage = await ans104SigningMessageRaw({ owner32, target, anchor, rawTags, data }); const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, signingMessage)); if (signature.length !== 64) throw new Error(`Unexpected Ed25519 signature length: ${signature.length}`); return concatBytes( uint16LE(ANS104_ED25519_SIGNATURE_TYPE), signature, owner32, Uint8Array.of(target.length ? 1 : 0), target, Uint8Array.of(anchor.length ? 1 : 0), anchor, uint64LE(tagsCount), uint64LE(rawTags.length), rawTags, data, ); }