diff --git a/src/edict.ts b/src/edict.ts index 48fd397..5d51099 100644 --- a/src/edict.ts +++ b/src/edict.ts @@ -1,31 +1,33 @@ import * as bitcoin from 'bitcoinjs-lib'; import { Option, Some, None } from '@sniptt/monads'; import { RuneId } from './runeid'; -import { u128 } from './u128'; +import { U32_MAX, u128 } from './u128'; export type Edict = { id: RuneId; amount: u128; - output: u128; + output: number; }; export namespace Edict { export function fromIntegers( tx: bitcoin.Transaction, - id: u128, + id: RuneId, amount: u128, output: u128 ): Option { - const runeId = RuneId.fromU128(id); + if (id.block === 0 && id.tx > 0) { + return None; + } - if (runeId.block === 0 && runeId.tx > 0) { + if (output > u128(U32_MAX)) { return None; } - if (output > u128(tx.outs.length)) { + if (output > tx.outs.length) { return None; } - return Some({ id: runeId, amount, output }); + return Some({ id, amount, output: Number(output) }); } } diff --git a/src/rune.ts b/src/rune.ts index 54d8095..1fb7d88 100644 --- a/src/rune.ts +++ b/src/rune.ts @@ -81,7 +81,7 @@ export class Rune { } static getReserved(n: u128): Rune { - return new Rune(u128.checkedAdd(RESERVED, n)); + return new Rune(u128.checkedAdd(RESERVED, n).unwrap()); } toString() { @@ -109,9 +109,11 @@ export class Rune { if (i > 0) { x = u128(x + 1n); } - x = u128.checkedMultiply(x, u128(26)); + x = u128.checkedMultiply(x, u128(26)).unwrap(); if ('A' <= c && c <= 'Z') { - x = u128.checkedAdd(x, u128(c.charCodeAt(0) - 'A'.charCodeAt(0))); + x = u128 + .checkedAdd(x, u128(c.charCodeAt(0) - 'A'.charCodeAt(0))) + .unwrap(); } else { throw new Error(`invalid character in rune name: ${c}`); } diff --git a/src/runeid.ts b/src/runeid.ts index 6fb07d5..5830ac4 100644 --- a/src/runeid.ts +++ b/src/runeid.ts @@ -1,18 +1,66 @@ -import { u128 } from './u128'; +import { None, Option, Some } from '@sniptt/monads'; +import _ from 'lodash'; +import { U32_MAX, u128 } from './u128'; export class RuneId { constructor(readonly block: number, readonly tx: number) {} - toU128() { - return u128((BigInt(this.block) << 16n) | BigInt(this.tx)); + static new(block: number, tx: number): Option { + const id = new RuneId(block, tx); + + if (id.block === 0 && id.tx > 0) { + return None; + } + + return Some(id); } - toString() { - return `${this.block}:${this.tx}`; + static sort(runeIds: RuneId[]): RuneId[] { + return _.sortBy(runeIds, (runeId) => [runeId.block, runeId.tx]); + } + + delta(next: RuneId): Option<[u128, u128]> { + const block = next.block - this.block; + if (block < 0) { + return None; + } + + let tx: number; + if (block === 0) { + tx = next.tx - this.tx; + if (tx < 0) { + return None; + } + } else { + tx = next.tx; + } + + return Some([u128(block), u128(tx)]); } - static fromU128(n: u128) { - return new RuneId(Number(n >> 16n), Number(n & 0xffffn)); + next(block: u128, tx: u128): Option { + if (block > BigInt(U32_MAX) || tx > BigInt(U32_MAX)) { + return None; + } + + const blockNumber = Number(block); + const txNumber = Number(tx); + + const nextBlock = this.block + blockNumber; + if (nextBlock > U32_MAX) { + return None; + } + + const nextTx = blockNumber === 0 ? this.tx + txNumber : txNumber; + if (nextTx > U32_MAX) { + return None; + } + + return RuneId.new(nextBlock, nextTx); + } + + toString() { + return `${this.block}:${this.tx}`; } static fromString(s: string) { diff --git a/src/runestone.ts b/src/runestone.ts index e096929..868dd2d 100644 --- a/src/runestone.ts +++ b/src/runestone.ts @@ -8,7 +8,7 @@ import { Edict } from './edict'; import { Etching } from './etching'; import { SeekBuffer } from './seekbuffer'; import { Tag } from './tag'; -import { u128 } from './u128'; +import { U32_MAX, u128 } from './u128'; import * as bitcoin from 'bitcoinjs-lib'; import _ from 'lodash'; import { Option, Some, None } from '@sniptt/monads'; @@ -19,6 +19,19 @@ import { RuneId } from './runeid'; export const MAX_SPACERS = 0b00000111_11111111_11111111_11111111; +type ValidPayload = Buffer; + +class InvalidPayload { + static readonly INSTANCE = new InvalidPayload(); + private constructor() {} +} + +type Payload = ValidPayload | InvalidPayload; + +export function isValidPayload(payload: Payload): payload is ValidPayload { + return payload !== InvalidPayload.INSTANCE; +} + export class Runestone { constructor( readonly cenotaph: boolean, @@ -37,61 +50,100 @@ export class Runestone { } static decipher(transaction: bitcoin.Transaction): Option { - const payload = Runestone.payload(transaction); - if (payload.isNone()) { + const optionPayload = Runestone.payload(transaction); + if (optionPayload.isNone()) { return None; } + const payload = optionPayload.unwrap(); + if (!isValidPayload(payload)) { + return Some(new Runestone(true, None, None, [], None)); + } - const integers = Runestone.integers(payload.unwrap()); + const optionIntegers = Runestone.integers(payload); + if (optionIntegers.isNone()) { + return Some(new Runestone(true, None, None, [], None)); + } const { cenotaph, edicts, fields } = Message.fromIntegers( transaction, - integers + optionIntegers.unwrap() ); - const claim = Tag.take(fields, Tag.CLAIM); - - const deadline = Tag.take(fields, Tag.DEADLINE).andThen((value) => - value <= 0xffff_ffffn ? Some(Number(value)) : None + const claim = Tag.take( + Tag.CLAIM, + fields, + 2, + ([block, tx]): Option => + block <= u128(U32_MAX) && tx <= u128(U32_MAX) + ? RuneId.new(Number(block), Number(tx)) + : None ); - const defaultOutput = Tag.take(fields, Tag.DEFAULT_OUTPUT).andThen( - (value) => (value <= 0xffff_ffffn ? Some(Number(value)) : None) + const deadline = Tag.take( + Tag.DEADLINE, + fields, + 1, + ([value]): Option => + value <= u128(U32_MAX) ? Some(Number(value)) : None ); - const divisibility = Tag.take(fields, Tag.DIVISIBILITY) - .andThen((value) => (value < 0xffn ? Some(Number(value)) : None)) - .andThen((value) => (value <= MAX_DIVISIBILITY ? Some(value) : None)) - .unwrapOr(0); + const defaultOutput = Tag.take( + Tag.DEFAULT_OUTPUT, + fields, + 1, + ([value]): Option => + value <= u128(U32_MAX) && Number(value) < transaction.outs.length + ? Some(Number(value)) + : None + ); - const limit = Tag.take(fields, Tag.LIMIT).map((value) => - value >= MAX_LIMIT ? MAX_LIMIT : value + const divisibility = Tag.take( + Tag.DIVISIBILITY, + fields, + 1, + ([value]): Option => + value <= 0xffn && Number(value) <= MAX_DIVISIBILITY + ? Some(Number(value)) + : None + ).unwrapOr(0); + + const limit = Tag.take(Tag.LIMIT, fields, 1, ([value]) => + value <= MAX_LIMIT ? Some(value) : None ); - const rune = Tag.take(fields, Tag.RUNE).map((value) => new Rune(value)); + const rune = Tag.take(Tag.RUNE, fields, 1, ([value]) => + Some(new Rune(value)) + ); - const spacers = Tag.take(fields, Tag.SPACERS) - .andThen((value) => (value < 0xffn ? Some(Number(value)) : None)) - .andThen((value) => (value <= MAX_SPACERS ? Some(value) : None)) - .unwrapOr(0); + const spacers = Tag.take( + Tag.SPACERS, + fields, + 1, + ([value]): Option => + value <= u128(U32_MAX) && Number(value) <= MAX_SPACERS + ? Some(Number(value)) + : None + ).unwrapOr(0); + + const symbol = Tag.take(Tag.SYMBOL, fields, 1, ([value]) => { + if (value > u128(U32_MAX)) { + return None; + } - const symbol = Tag.take(fields, Tag.SYMBOL) - .andThen((value) => - value <= 0xffff_ffffn ? Some(Number(value)) : None - ) - .andThen((value) => { - try { - return Some(String.fromCodePoint(value)); - } catch (e) { - return None; - } - }); + try { + return Some(String.fromCodePoint(Number(value))); + } catch (e) { + return None; + } + }); - const term = Tag.take(fields, Tag.TERM).andThen((value) => + const term = Tag.take(Tag.TERM, fields, 1, ([value]) => value <= 0xffff_ffffn ? Some(Number(value)) : None ); - let flags = Tag.take(fields, Tag.FLAGS).unwrapOr(u128(0)); + let flags = Tag.take(Tag.FLAGS, fields, 1, ([value]) => + Some(value) + ).unwrapOr(u128(0)); const etchResult = Flag.take(flags, Flag.ETCH); const etch = etchResult.set; @@ -124,7 +176,7 @@ export class Runestone { cenotaph || flags !== 0n || [...fields.keys()].find((tag) => tag % 2n === 0n) !== undefined, - claim.map((value) => RuneId.fromU128(value)), + claim, defaultOutput, edicts, etching @@ -144,24 +196,26 @@ export class Runestone { flags = Flag.set(flags, Flag.MINT); } - payloads.push(Tag.encode(Tag.FLAGS, flags)); + payloads.push(Tag.encode(Tag.FLAGS, [flags])); if (etching.rune.isSome()) { const rune = etching.rune.unwrap(); - payloads.push(Tag.encode(Tag.RUNE, rune.value)); + payloads.push(Tag.encode(Tag.RUNE, [rune.value])); } if (etching.divisibility !== 0) { - payloads.push(Tag.encode(Tag.DIVISIBILITY, u128(etching.divisibility))); + payloads.push( + Tag.encode(Tag.DIVISIBILITY, [u128(etching.divisibility)]) + ); } if (etching.spacers !== 0) { - payloads.push(Tag.encode(Tag.SPACERS, u128(etching.spacers))); + payloads.push(Tag.encode(Tag.SPACERS, [u128(etching.spacers)])); } if (etching.symbol.isSome()) { const symbol = etching.symbol.unwrap(); - payloads.push(Tag.encode(Tag.SYMBOL, u128(symbol.codePointAt(0)!))); + payloads.push(Tag.encode(Tag.SYMBOL, [u128(symbol.codePointAt(0)!)])); } if (etching.mint.isSome()) { @@ -169,33 +223,33 @@ export class Runestone { if (mint.deadline.isSome()) { const deadline = mint.deadline.unwrap(); - payloads.push(Tag.encode(Tag.DEADLINE, u128(deadline))); + payloads.push(Tag.encode(Tag.DEADLINE, [u128(deadline)])); } if (mint.limit.isSome()) { const limit = mint.limit.unwrap(); - payloads.push(Tag.encode(Tag.LIMIT, limit)); + payloads.push(Tag.encode(Tag.LIMIT, [limit])); } if (mint.term.isSome()) { const term = mint.term.unwrap(); - payloads.push(Tag.encode(Tag.TERM, u128(term))); + payloads.push(Tag.encode(Tag.TERM, [u128(term)])); } } } if (this.claim.isSome()) { const claim = this.claim.unwrap(); - payloads.push(Tag.encode(Tag.CLAIM, claim.toU128())); + payloads.push(Tag.encode(Tag.CLAIM, [claim.block, claim.tx].map(u128))); } if (this.defaultOutput.isSome()) { const defaultOutput = this.defaultOutput.unwrap(); - payloads.push(Tag.encode(Tag.DEFAULT_OUTPUT, u128(defaultOutput))); + payloads.push(Tag.encode(Tag.DEFAULT_OUTPUT, [u128(defaultOutput)])); } if (this.cenotaph) { - payloads.push(Tag.encode(Tag.CENOTAPH, u128(0))); + payloads.push(Tag.encode(Tag.CENOTAPH, [u128(0)])); } if (this.edicts.length) { @@ -203,13 +257,15 @@ export class Runestone { const edicts = _.sortBy(this.edicts, (edict) => edict.id); - let id = u128(0); + let previous = new RuneId(0, 0); for (const edict of edicts) { - const next = edict.id.toU128(); - payloads.push(u128.encodeVarInt(u128(next - id))); + const [block, tx] = previous.delta(edict.id).unwrap(); + + payloads.push(u128.encodeVarInt(block)); + payloads.push(u128.encodeVarInt(tx)); payloads.push(u128.encodeVarInt(edict.amount)); - payloads.push(u128.encodeVarInt(edict.output)); - id = next; + payloads.push(u128.encodeVarInt(u128(edict.output))); + previous = edict.id; } } @@ -226,20 +282,21 @@ export class Runestone { return bitcoin.script.compile(stack); } - static payload(transaction: bitcoin.Transaction): Option { + static payload(transaction: bitcoin.Transaction): Option { + // search transaction outputs for payload for (const output of transaction.outs) { const instructions = bitcoin.script.decompile(output.script); if (instructions === null) { throw new Error('unable to decompile'); } - let nextInstruction: Instruction | undefined; - - nextInstruction = instructions.shift(); + // payload starts with OP_RETURN + let nextInstruction: Instruction | undefined = instructions.shift(); if (nextInstruction !== bitcoin.opcodes.OP_RETURN) { continue; } + // followed by the protocol identifier nextInstruction = instructions.shift(); if ( !nextInstruction || @@ -249,12 +306,15 @@ export class Runestone { continue; } + // construct the payload by concatinating remaining data pushes let payloads: Buffer[] = []; for (const instruction of instructions) { const result = tryConvertInstructionToBuffer(instruction); if (Instruction.isBuffer(result)) { payloads.push(result); + } else { + return Some(InvalidPayload.INSTANCE); } } @@ -264,15 +324,19 @@ export class Runestone { return None; } - static integers(payload: Buffer): u128[] { + static integers(payload: Buffer): Option { const integers: u128[] = []; const seekBuffer = new SeekBuffer(payload); while (!seekBuffer.isFinished()) { - integers.push(u128.readVarInt(seekBuffer)); + const optionInt = u128.decodeVarInt(seekBuffer); + if (optionInt.isNone()) { + return None; + } + integers.push(optionInt.unwrap()); } - return integers; + return Some(integers); } } @@ -280,44 +344,54 @@ export class Message { constructor( readonly cenotaph: boolean, readonly edicts: Edict[], - readonly fields: Map + readonly fields: Map ) {} static fromIntegers(tx: bitcoin.Transaction, payload: u128[]): Message { const edicts: Edict[] = []; - const fields = new Map(); + const fields = new Map(); let cenotaph = false; for (const i of _.range(0, payload.length, 2)) { const tag = payload[i]; if (u128(Tag.BODY) === tag) { - let id = u128(0); - for (const chunk of _.chunk(payload.slice(i + 1), 3)) { - if (chunk.length !== 3) { + let id = new RuneId(0, 0); + for (const chunk of _.chunk(payload.slice(i + 1), 4)) { + if (chunk.length !== 4) { + cenotaph = true; break; } - id = u128.saturatingAdd(id, chunk[0]); + const optionNext = id.next(chunk[0], chunk[1]); + if (optionNext.isNone()) { + cenotaph = true; + break; + } + const next = optionNext.unwrap(); - const optionEdict = Edict.fromIntegers(tx, id, chunk[1], chunk[2]); - if (optionEdict.isSome()) { - edicts.push(optionEdict.unwrap()); - } else { + const optionEdict = Edict.fromIntegers(tx, next, chunk[2], chunk[3]); + if (optionEdict.isNone()) { cenotaph = true; + break; } + const edict = optionEdict.unwrap(); + + id = next; + edicts.push(edict); } break; } const value = payload[i + 1]; if (value === undefined) { + cenotaph = true; break; } - if (!fields.has(tag)) { - fields.set(tag, value); - } + const values = fields.get(tag) ?? []; + values.push(value); + fields.set(tag, values); } return new Message(cenotaph, edicts, fields); diff --git a/src/tag.ts b/src/tag.ts index d6a0182..9cecd14 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -1,5 +1,7 @@ import { None, Option, Some } from '@sniptt/monads'; +import _ from 'lodash'; import { u128 } from './u128'; +import { FixedArray } from './utils'; export enum Tag { BODY = 0, @@ -19,17 +21,47 @@ export enum Tag { } export namespace Tag { - export function take(fields: Map, tag: Tag): Option { - const key = u128(tag); - const value = fields.get(key); - fields.delete(key); - return value ? Some(value) : None; + export function take( + tag: Tag, + fields: Map, + n: N, + withFn: (values: FixedArray) => Option + ): Option { + const field = fields.get(u128(tag)); + if (field === undefined) { + return None; + } + + const values: u128[] = []; + for (const i of _.range(n)) { + if (field[i] === undefined) { + return None; + } + values[i] = field[i]; + } + + const optionValue = withFn(values as FixedArray); + if (optionValue.isNone()) { + return None; + } + + field.splice(0, n); + + if (field.length === 0) { + fields.delete(u128(tag)); + } + + return Some(optionValue.unwrap()); } - export function encode(tag: Tag, value: u128): Buffer { - return Buffer.concat([ - u128.encodeVarInt(u128(tag)), - u128.encodeVarInt(value), - ]); + export function encode(tag: Tag, values: u128[]): Buffer { + return Buffer.concat( + _.flatten( + values.map((value) => [ + u128.encodeVarInt(u128(tag)), + u128.encodeVarInt(value), + ]) + ) + ); } } diff --git a/src/u128.ts b/src/u128.ts index 1cd1bab..6614cfc 100644 --- a/src/u128.ts +++ b/src/u128.ts @@ -1,3 +1,4 @@ +import { None, Option, Some } from '@sniptt/monads'; import { SeekBuffer } from './seekbuffer'; /** @@ -29,6 +30,8 @@ export type u128 = BigTypedNumber<'u128'>; export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; +export const U32_MAX = 0xffffffff; + /** * Convert Number or BigInt to 128-bit unsigned integer. * @param num - The Number or BigInt to convert. @@ -40,26 +43,24 @@ export function u128(num: number | bigint): u128 { } export namespace u128 { - export class OverflowError extends Error {} - export const MAX = u128(U128_MAX_BIGINT); - export function checkedAdd(x: u128, y: u128): u128 { + export function checkedAdd(x: u128, y: u128): Option { const result = x + y; if (result > u128.MAX) { - throw new OverflowError(); + return None; } - return u128(result); + return Some(u128(result)); } - export function checkedMultiply(x: u128, y: u128): u128 { + export function checkedMultiply(x: u128, y: u128): Option { const result = x * y; if (result > u128.MAX) { - throw new OverflowError(); + return None; } - return u128(result); + return Some(u128(result)); } export function saturatingAdd(x: u128, y: u128): u128 { @@ -76,43 +77,54 @@ export namespace u128 { return u128(x < y ? 0 : x - y); } - export function readVarInt(seekBuffer: SeekBuffer): u128 { + export function decodeVarInt(seekBuffer: SeekBuffer): Option { + try { + return Some(tryDecodeVarInt(seekBuffer)); + } catch (e) { + return None; + } + } + + export function tryDecodeVarInt(seekBuffer: SeekBuffer): u128 { let result = u128(0); - do { + for (let i = 0; i <= 18; i++) { const byte = seekBuffer.readUInt8(); if (byte === undefined) { - return result; + throw new Error('Unterminated'); } - result = u128.saturatingMultiply(result, u128(128)); + const value = u128(byte) & 0b0111_1111n; - if (byte < 128) { - return u128.saturatingAdd(result, u128(byte)); + if (i === 18 && (value & 0b0111_1100n) !== 0n) { + throw new Error('Overflow'); } - result = u128.saturatingAdd(result, u128(byte - 127)); - } while (true); + result = u128(result | (value << u128(7 * i))); + + if ((byte & 0b1000_0000) === 0) { + return result; + } + } + + throw new Error('Overlong'); } export function encodeVarInt(value: u128): Buffer { - const buffer = Buffer.alloc(19); - let bufindex = 18; - - buffer.writeUInt8(Number(value & 0xffn) & 0b0111_1111, bufindex); - while (value > 0b0111_1111) { - value = u128(value / 128n - 1n); - bufindex--; - buffer.writeUInt8(Number(value & 0xffn) | 0b1000_0000, bufindex); + const v: number[] = []; + while (value >> 7n > 0n) { + v.push(Number(value & 0xffn) | 0b1000_0000); + value = u128(value >> 7n); } + v.push(Number(value & 0xffn)); - return buffer.subarray(bufindex); + return Buffer.from(v); } } export function* getAllU128(buffer: Buffer): Generator { const seekBuffer = new SeekBuffer(buffer); while (!seekBuffer.isFinished()) { - const nextValue = u128.readVarInt(seekBuffer); + const nextValue = u128.tryDecodeVarInt(seekBuffer); if (nextValue === undefined) { return; } diff --git a/src/utils.ts b/src/utils.ts index aa1eb7e..6b01bdd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -43,3 +43,9 @@ export function tryConvertInstructionToBuffer(instruction: Instruction) { return instruction; } } + +type GrowToSize = A['length'] extends N + ? A + : GrowToSize; + +export type FixedArray = GrowToSize; diff --git a/test/runeid.test.ts b/test/runeid.test.ts index 153eb02..6479a85 100644 --- a/test/runeid.test.ts +++ b/test/runeid.test.ts @@ -2,12 +2,54 @@ import { RuneId } from '../src/runeid'; import { u128 } from '../src/u128'; describe('runeid', () => { - test('rune id to 128', () => { - expect(new RuneId(3, 1).toU128()).toBe(0b11_0000_0000_0000_0001n); + test('delta', () => { + let expected = [ + new RuneId(4, 2), + new RuneId(1, 2), + new RuneId(1, 1), + new RuneId(3, 1), + new RuneId(2, 0), + ]; + + expected = RuneId.sort(expected); + + expect(expected).toEqual([ + new RuneId(1, 1), + new RuneId(1, 2), + new RuneId(2, 0), + new RuneId(3, 1), + new RuneId(4, 2), + ]); + + let previous = new RuneId(0, 0); + const deltas: [u128, u128][] = []; + for (const id of expected) { + const delta = previous.delta(id).unwrap(); + deltas.push(delta); + previous = id; + } + + expect(deltas).toEqual([ + [1n, 1n], + [0n, 1n], + [1n, 0n], + [1n, 1n], + [1n, 2n], + ]); + + previous = new RuneId(0, 0); + const actual: RuneId[] = []; + for (const [block, tx] of deltas) { + const next = previous.next(block, tx).unwrap(); + actual.push(next); + previous = next; + } + + expect(actual).toEqual(expected); }); test('display', () => { - expect(new RuneId(1, 2).toString()).toBe('1:2'); + expect(RuneId.new(1, 2).unwrap().toString()).toBe('1:2'); }); test('from string', () => { @@ -18,10 +60,4 @@ describe('runeid', () => { expect(() => RuneId.fromString('1:a')).toThrow(); expect(RuneId.fromString('1:2')).toEqual(new RuneId(1, 2)); }); - - test('from u128', () => { - expect(RuneId.fromU128(u128(0x060504030201n))).toEqual( - new RuneId(0x06050403, 0x0201) - ); - }); }); diff --git a/test/runestone.test.ts b/test/runestone.test.ts index 49093ae..f142f12 100644 --- a/test/runestone.test.ts +++ b/test/runestone.test.ts @@ -1,7 +1,7 @@ import * as bitcoin from 'bitcoinjs-lib'; import _ from 'lodash'; -import { MAX_SPACERS, Runestone } from '../src/runestone'; -import { u128 } from '../src/u128'; +import { MAX_SPACERS, Runestone, isValidPayload } from '../src/runestone'; +import { U32_MAX, u128 } from '../src/u128'; import { None, Option, Some } from '@sniptt/monads'; import { Tag } from '../src/tag'; import { Flag } from '../src/flag'; @@ -111,28 +111,34 @@ describe('runestone', () => { ).toBe(true); }); - test('non_push_opcodes_in_runestone_are_ignored', () => { - expect( - Runestone.decipher( - getSimpleTransaction([ - bitcoin.opcodes.OP_RETURN, - MAGIC_NUMBER, - Buffer.concat([ - Buffer.from([0]), - u128.encodeVarInt(createRuneId(1).toU128()), - ]), - bitcoin.opcodes.OP_VERIFY, - Buffer.from([2, 0]), - ]) - ).unwrap() - ).toMatchObject({ - edicts: [ - { - id: createRuneId(1), - amount: u128(2), - output: u128(0), - }, - ], + test('outputs_with_non_pushdata_opcodes_are_cenotaph', () => { + const transaction = new bitcoin.Transaction(); + transaction.addOutput( + bitcoin.script.compile([ + bitcoin.opcodes.OP_RETURN, + MAGIC_NUMBER, + bitcoin.opcodes.OP_VERIFY, + Buffer.from([0]), + u128.encodeVarInt(u128(1)), + u128.encodeVarInt(u128(1)), + Buffer.from([2, 0]), + ]), + 0 + ); + transaction.addOutput( + bitcoin.script.compile([ + bitcoin.opcodes.OP_RETURN, + MAGIC_NUMBER, + Buffer.from([0]), + u128.encodeVarInt(u128(1)), + u128.encodeVarInt(u128(1)), + Buffer.from([3, 0]), + ]), + 0 + ); + + expect(Runestone.decipher(transaction).unwrap()).toMatchObject({ + cenotaph: true, }); }); @@ -168,27 +174,18 @@ describe('runestone', () => { }); test('deciphering_non_empty_runestone_is_successful', () => { - expect( - decipher([Tag.BODY, createRuneId(1).toU128(), 2, 0].map(u128)) - ).toMatchObject({ - edicts: [{ id: createRuneId(1), amount: 2n, output: 0n }], + expect(decipher([Tag.BODY, 1, 1, 2, 0].map(u128))).toMatchObject({ + edicts: [{ id: createRuneId(1), amount: 2n, output: 0 }], }); }); test('decipher_etching', () => { const runestone = decipher( - [ - Tag.FLAGS, - Flag.mask(Flag.ETCH), - Tag.BODY, - createRuneId(1).toU128(), - 2, - 0, - ].map(u128) + [Tag.FLAGS, Flag.mask(Flag.ETCH), Tag.BODY, 1, 1, 2, 0].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -201,20 +198,13 @@ describe('runestone', () => { test('decipher_etching_with_rune', () => { const runestone = decipher( - [ - Tag.FLAGS, - Flag.mask(Flag.ETCH), - Tag.RUNE, - 4, - Tag.BODY, - createRuneId(1).toU128(), - 2, - 0, - ].map(u128) + [Tag.FLAGS, Flag.mask(Flag.ETCH), Tag.RUNE, 4, Tag.BODY, 1, 1, 2, 0].map( + u128 + ) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -227,20 +217,13 @@ describe('runestone', () => { test('etch_flag_is_required_to_etch_rune_even_if_mint_is_set', () => { const runestone = decipher( - [ - Tag.FLAGS, - Flag.mask(Flag.MINT), - Tag.TERM, - 4, - Tag.BODY, - createRuneId(1).toU128(), - 2, - 0, - ].map(u128) + [Tag.FLAGS, Flag.mask(Flag.MINT), Tag.TERM, 4, Tag.BODY, 1, 1, 2, 0].map( + u128 + ) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); expect(runestone.etching.isNone()).toBe(true); }); @@ -253,14 +236,15 @@ describe('runestone', () => { Tag.TERM, 4, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -283,14 +267,15 @@ describe('runestone', () => { Tag.LIMIT, 4, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -305,7 +290,19 @@ describe('runestone', () => { expect(mint.limit.unwrap()).toBe(4n); }); - test('duplicate_tags_are_ignored', () => { + test('invalid_varint_produces_cenotaph', () => { + expect( + Runestone.decipher( + getSimpleTransaction([ + bitcoin.opcodes.OP_RETURN, + MAGIC_NUMBER, + Buffer.from([128]), + ]) + ).unwrap() + ).toMatchObject({ cenotaph: true }); + }); + + test('duplicate_even_tags_produce_cenotaph', () => { const runestone = decipher( [ Tag.FLAGS, @@ -315,15 +312,17 @@ describe('runestone', () => { Tag.RUNE, 5, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); + expect(runestone.cenotaph).toBe(true); const etching = runestone.etching.unwrap(); expect(etching.divisibility).toBe(0); @@ -333,58 +332,62 @@ describe('runestone', () => { expect(etching.mint.isNone()).toBe(true); }); - test('unrecognized_odd_tag_is_ignored', () => { + test('duplicate_odd_tags_are_ignored', () => { const runestone = decipher( - [Tag.NOP, 100, Tag.BODY, createRuneId(1).toU128(), 2, 0].map(u128) + [ + Tag.FLAGS, + Flag.mask(Flag.ETCH), + Tag.DIVISIBILITY, + 4, + Tag.DIVISIBILITY, + 5, + Tag.BODY, + 1, + 1, + 2, + 0, + ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); + expect(runestone.etching.unwrap()).toMatchObject({ + divisibility: 4, + }); }); test('runestone_with_unrecognized_even_tag_is_cenotaph', () => { const runestone = decipher( - [Tag.CENOTAPH, 0, Tag.BODY, createRuneId(1).toU128(), 2, 0].map(u128) + [Tag.CENOTAPH, 0, Tag.BODY, 1, 1, 2, 0].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); expect(runestone.cenotaph).toBe(true); }); test('runestone_with_unrecognized_flag_is_cenotaph', () => { const runestone = decipher( - [ - Tag.FLAGS, - Flag.mask(Flag.CENOTAPH), - Tag.BODY, - createRuneId(1).toU128(), - 2, - 0, - ].map(u128) + [Tag.FLAGS, Flag.mask(Flag.CENOTAPH), Tag.BODY, 1, 1, 2, 0].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); expect(runestone.cenotaph).toBe(true); }); test('runestone_with_edict_id_with_zero_block_and_nonzero_tx_is_cenotaph', () => { - const runestone = decipher( - [Tag.BODY, new RuneId(0, 1).toU128(), 2, 0].map(u128) - ); + const runestone = decipher([Tag.BODY, 0, 1, 2, 0].map(u128)); expect(runestone.edicts).toEqual([]); expect(runestone.cenotaph).toBe(true); }); test('runestone_with_output_over_max_is_cenotaph', () => { - const runestone = decipher( - [Tag.BODY, createRuneId(1).toU128(), 2, 2].map(u128) - ); + const runestone = decipher([Tag.BODY, 1, 1, 2, 2].map(u128)); expect(runestone.edicts).toEqual([]); expect(runestone.cenotaph).toBe(true); @@ -396,32 +399,22 @@ describe('runestone', () => { expect(runestone.etching.isSome()).toBe(true); }); - test('additional_integers_in_body_are_ignored', () => { - const runestone = decipher( - [ - Tag.FLAGS, - Flag.mask(Flag.ETCH), - Tag.RUNE, - 4, - Tag.BODY, - createRuneId(1).toU128(), - 2, - 0, - 4, - 5, - ].map(u128) - ); - - expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, - ]); + test('trailing_integers_in_body_is_cenotaph', () => { + const integers = [Tag.BODY, 1, 1, 2, 0]; + + for (const i of _.range(4)) { + const runestone = decipher(integers.map(u128)); + if (i === 0) { + expect(runestone.edicts).toEqual([ + { id: createRuneId(1), amount: 2n, output: 0 }, + ]); + expect(runestone.cenotaph).toBe(false); + } else { + expect(runestone.cenotaph).toBe(true); + } - const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); - expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); - expect(etching.symbol.isNone()).toBe(true); - expect(etching.mint.isNone()).toBe(true); + integers.push(0); + } }); test('decipher_etching_with_divisibility', () => { @@ -434,14 +427,15 @@ describe('runestone', () => { Tag.DIVISIBILITY, 5, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -462,14 +456,15 @@ describe('runestone', () => { Tag.DIVISIBILITY, MAX_DIVISIBILITY + 1, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -490,14 +485,15 @@ describe('runestone', () => { Tag.SYMBOL, 0x110000, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -518,14 +514,15 @@ describe('runestone', () => { Tag.SYMBOL, 97, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -556,14 +553,15 @@ describe('runestone', () => { Tag.LIMIT, 3, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -592,7 +590,8 @@ describe('runestone', () => { Tag.LIMIT, 3, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, 4, @@ -601,7 +600,7 @@ describe('runestone', () => { ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); expect(runestone.etching.isNone()).toBe(true); }); @@ -618,14 +617,15 @@ describe('runestone', () => { Tag.SYMBOL, 97, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -643,38 +643,44 @@ describe('runestone', () => { Tag.DIVISIBILITY, Tag.BODY, Tag.BODY, - createRuneId(1).toU128(), + 1, + 1, 2, 0, ].map(u128) ); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); expect(runestone.etching.isSome()).toBe(true); }); test('runestone_may_contain_multiple_edicts', () => { - const runestone = decipher( - [Tag.BODY, createRuneId(1).toU128(), 2, 0, 3, 5, 0].map(u128) - ); + const runestone = decipher([Tag.BODY, 1, 1, 2, 0, 0, 3, 5, 0].map(u128)); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, - { id: createRuneId(4), amount: 5n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, + { id: createRuneId(4), amount: 5n, output: 0 }, ]); }); - test('runestones_with_invalid_rune_ids_is_cenotaph', () => { - const runestone = decipher( - [Tag.BODY, createRuneId(1).toU128(), 2, 0, u128.MAX, 5, 6].map(u128) - ); + test('runestones_with_invalid_rune_id_blocks_are_cenotaph', () => { + expect( + decipher([Tag.BODY, 1, 1, 2, 0, u128.MAX, 1, 0, 0].map(u128)) + ).toMatchObject({ + edicts: [{ id: createRuneId(1), amount: 2n, output: 0 }], + cenotaph: true, + }); + }); - expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, - ]); - expect(runestone.cenotaph).toBe(true); + test('runestones_with_invalid_rune_id_txs_are_cenotaph', () => { + expect( + decipher([Tag.BODY, 1, 1, 2, 0, 1, u128.MAX, 0, 0].map(u128)) + ).toMatchObject({ + edicts: [{ id: createRuneId(1), amount: 2n, output: 0 }], + cenotaph: true, + }); }); test('payload_pushes_are_concatenated', () => { @@ -687,14 +693,15 @@ describe('runestone', () => { u128.encodeVarInt(u128(Tag.DIVISIBILITY)), u128.encodeVarInt(u128(5)), u128.encodeVarInt(u128(Tag.BODY)), - u128.encodeVarInt(createRuneId(1).toU128()), + u128.encodeVarInt(u128(1)), + u128.encodeVarInt(u128(1)), u128.encodeVarInt(u128(2)), u128.encodeVarInt(u128(0)), ]) ).unwrap(); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); const etching = runestone.etching.unwrap(); @@ -705,7 +712,7 @@ describe('runestone', () => { }); test('runestone_may_be_in_second_output', () => { - const payload = getPayload([0, createRuneId(1).toU128(), 2, 0].map(u128)); + const payload = getPayload([0, 1, 1, 2, 0].map(u128)); const transaction = new bitcoin.Transaction(); @@ -722,12 +729,12 @@ describe('runestone', () => { const runestone = Runestone.decipher(transaction).unwrap(); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); }); test('runestone_may_be_after_non_matching_op_return', () => { - const payload = getPayload([0, createRuneId(1).toU128(), 2, 0].map(u128)); + const payload = getPayload([0, 1, 1, 2, 0].map(u128)); const transaction = new bitcoin.Transaction(); @@ -747,7 +754,7 @@ describe('runestone', () => { const runestone = Runestone.decipher(transaction).unwrap(); expect(runestone.edicts).toEqual([ - { id: createRuneId(1), amount: 2n, output: 0n }, + { id: createRuneId(1), amount: 2n, output: 0 }, ]); }); @@ -803,13 +810,13 @@ describe('runestone', () => { { amount: u128(0), id: new RuneId(0, 0), - output: u128(0), + output: 0, }, ], Some( new Etching(MAX_DIVISIBILITY, Some(new Rune(u128.MAX)), 0, None, None) ), - 31 + 32 ); testcase( @@ -817,134 +824,132 @@ describe('runestone', () => { { amount: u128.MAX, id: new RuneId(0, 0), - output: u128(0), + output: 0, }, ], Some( new Etching(MAX_DIVISIBILITY, Some(new Rune(u128.MAX)), 0, None, None) ), - 49 + 50 ); testcase( [ { amount: u128(0), - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, }, ], None, - 12 + 14 ); testcase( [ { amount: u128.MAX, - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, }, ], None, - 30 + 32 ); testcase( [ { amount: u128.MAX, - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, }, { amount: u128.MAX, - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, }, ], None, - 51 + 54 ); testcase( [ { amount: u128.MAX, - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, }, { amount: u128.MAX, - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, }, { amount: u128.MAX, - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, }, ], None, - 72 + 76 ); testcase( _.range(4).map(() => ({ amount: u128(0xffff_ffff_ffff_ffffn), - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, })), None, - 57 + 62 ); testcase( _.range(5).map(() => ({ amount: u128(0xffff_ffff_ffff_ffffn), - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, })), None, - 69 + 75 ); testcase( _.range(5).map(() => ({ amount: u128(0xffff_ffff_ffff_ffffn), - id: new RuneId(0, 0xffff), - output: u128(0), + id: new RuneId(0, U32_MAX), + output: 0, })), None, - 66 + 73 ); testcase( _.range(5).map(() => ({ amount: u128(1_000_000_000_000_000_000n), - id: new RuneId(1_000_000, 0xffff), - output: u128(0), + id: new RuneId(1_000_000, U32_MAX), + output: 0, })), None, - 64 + 70 ); }); - // TODO: update unit test in ord - test('etching_with_term_greater_than_maximum_is_ignored', () => { + test('etching_with_term_greater_than_maximum_is_still_an_etching', () => { { const runestone = decipher( [ Tag.FLAGS, - Flag.mask(Flag.ETCH) | Flag.mask(Flag.MINT), + Flag.mask(Flag.ETCH), Tag.TERM, - 0xffff_ffffn, + 0xffff_ffff_ffff_ffffn + 1n, ].map(u128) ); - const etching = runestone.etching.unwrap(); - const mint = etching.mint.unwrap(); - expect(mint.term.unwrap()).toBe(0xffff_ffff); + expect(runestone.cenotaph).toBe(true); + expect(runestone.etching.isSome()).toBe(true); } { @@ -971,8 +976,11 @@ describe('runestone', () => { transaction.addOutput(scriptPubKey, 0); const payload = Runestone.payload(transaction).unwrap(); + expect(isValidPayload(payload)).toBe(true); - expect(Runestone.integers(payload)).toEqual(expected.map(u128)); + expect(Runestone.integers(payload as Buffer).unwrap()).toEqual( + expected.map(u128) + ); const txnRunestone = Runestone.fromTransaction(transaction).unwrap(); @@ -1037,18 +1045,18 @@ describe('runestone', () => { testcase( new Runestone( true, - Some(RuneId.fromU128(u128(12))), - Some(11), + Some(createRuneId(12)), + Some(0), [ { amount: u128(8), id: createRuneId(9), - output: u128(0), + output: 0, }, { amount: u128(5), id: createRuneId(6), - output: u128(1), + output: 1, }, ], Some( @@ -1083,15 +1091,19 @@ describe('runestone', () => { Tag.TERM, 5, Tag.CLAIM, + 1, + Tag.CLAIM, 12, Tag.DEFAULT_OUTPUT, - 11, + 0, Tag.CENOTAPH, 0, Tag.BODY, - createRuneId(6).toU128(), + 1, + 6, 5, 1, + 0, 3, 8, 0, @@ -1129,10 +1141,10 @@ describe('runestone', () => { false, None, None, - _.range(173).map((i) => ({ + _.range(129).map((i) => ({ id: new RuneId(0, 0), amount: u128(0), - output: u128(0), + output: 0, })), None ).encipher(); @@ -1146,10 +1158,10 @@ describe('runestone', () => { false, None, None, - _.range(174).map((i) => ({ + _.range(130).map((i) => ({ id: createRuneId(0), amount: u128(0), - output: u128(0), + output: 0, })), None ).encipher(); @@ -1173,4 +1185,61 @@ describe('runestone', () => { expect(SpacedRune.fromString(rune).spacers).toBe(MAX_SPACERS); }); + + test('edict_output_greater_than_32_max_produces_cenotaph', () => { + expect(decipher([Tag.BODY, 1, 1, 1, U32_MAX + 1].map(u128)).cenotaph).toBe( + true + ); + }); + + test('partial_claim_produces_cenotaph', () => { + expect(decipher([Tag.CLAIM, 1].map(u128)).cenotaph).toBe(true); + }); + + test('invalid_claim_produces_cenotaph', () => { + expect(decipher([Tag.CLAIM, 0, Tag.CLAIM, 1].map(u128)).cenotaph).toBe( + true + ); + }); + + test('invalid_deadline_produces_cenotaph', () => { + expect(decipher([Tag.DEADLINE, u128.MAX].map(u128)).cenotaph).toBe(true); + }); + + test('invalid_deadline_produces_cenotaph', () => { + expect(decipher([Tag.DEFAULT_OUTPUT, 1].map(u128)).cenotaph).toBe(true); + expect(decipher([Tag.DEFAULT_OUTPUT, u128.MAX].map(u128)).cenotaph).toBe( + true + ); + }); + + test('invalid_divisibility_does_not_produce_cenotaph', () => { + expect(decipher([Tag.DIVISIBILITY, u128.MAX].map(u128)).cenotaph).toBe( + false + ); + }); + + test('invalid_limit_produces_cenotaph', () => { + expect(decipher([Tag.LIMIT, u128.MAX].map(u128)).cenotaph).toBe(true); + expect( + decipher([Tag.LIMIT, 0xffffffff_ffffffffn + 1n].map(u128)).cenotaph + ).toBe(true); + }); + + test('min_and_max_runes_are_not_cenotaphs', () => { + expect(decipher([Tag.RUNE, 0].map(u128)).cenotaph).toBe(false); + expect(decipher([Tag.RUNE, u128.MAX].map(u128)).cenotaph).toBe(false); + }); + + test('invalid_spacers_does_not_produce_cenotaph', () => { + expect(decipher([Tag.SPACERS, u128.MAX].map(u128)).cenotaph).toBe(false); + }); + + test('invalid_symbol_does_not_produce_cenotaph', () => { + expect(decipher([Tag.SYMBOL, u128.MAX].map(u128)).cenotaph).toBe(false); + }); + + test('invalid_term_produces_cenotaph', () => { + expect(decipher([Tag.TERM, u128.MAX].map(u128)).cenotaph).toBe(true); + }); }); diff --git a/test/tag.test.ts b/test/tag.test.ts index 4493b47..fe57d79 100644 --- a/test/tag.test.ts +++ b/test/tag.test.ts @@ -1,3 +1,4 @@ +import { Some, None } from '@sniptt/monads'; import { Tag } from '../src/tag'; import { u128 } from '../src/u128'; @@ -8,20 +9,51 @@ describe('tag', () => { }); test('take', () => { - const fields = new Map(); - fields.set(u128(2), u128(3)); + const fields = new Map(); + fields.set(u128(2), [u128(3)]); - expect(Tag.take(fields, Tag.FLAGS).unwrap()).toBe(3n); + expect(Tag.take(Tag.FLAGS, fields, 1, () => None).isNone()).toBe(true); + expect(fields.size).not.toBe(0); + expect( + Tag.take(Tag.FLAGS, fields, 1, ([flags]) => Some(flags)).unwrap() + ).toBe(3n); expect(fields.size).toBe(0); - expect(Tag.take(fields, Tag.FLAGS).isNone()).toBe(true); + expect( + Tag.take(Tag.FLAGS, fields, 1, ([flags]) => Some(flags)).isNone() + ).toBe(true); + }); + + test('take leaves unconsumed values', () => { + const fields = new Map(); + fields.set(u128(2), [1, 2, 3].map(u128)); + + expect(fields.get(u128(2))?.length).toBe(3); + + expect(Tag.take(Tag.FLAGS, fields, 1, () => None).isNone()).toBe(true); + + expect(fields.get(u128(2))?.length).toBe(3); + + expect( + Tag.take(Tag.FLAGS, fields, 2, ([a, b]) => Some([a, b])).unwrap() + ).toEqual([1n, 2n]); + + expect(fields.get(u128(2))?.length).toBe(1); + + expect(Tag.take(Tag.FLAGS, fields, 1, ([a]) => Some([a])).unwrap()).toEqual( + [3n] + ); + + expect(fields.get(u128(2))).toBeUndefined(); }); test('encode', () => { - expect([...Tag.encode(Tag.FLAGS, u128(3))]).toEqual([2, 3]); + expect([...Tag.encode(Tag.FLAGS, [3].map(u128))]).toEqual([2, 3]); + expect([...Tag.encode(Tag.RUNE, [5].map(u128))]).toEqual([4, 5]); + expect([...Tag.encode(Tag.RUNE, [5, 6].map(u128))]).toEqual([4, 5, 4, 6]); }); test('burn and nop are one byte', () => { - expect(Tag.encode(Tag.CENOTAPH, u128(0)).length).toBe(2); - expect(Tag.encode(Tag.NOP, u128(0)).length).toBe(2); + expect(Tag.encode(Tag.CENOTAPH, [u128(0)]).length).toBe(2); + expect(Tag.encode(Tag.NOP, [u128(0)]).length).toBe(2); }); }); diff --git a/test/u128.test.ts b/test/u128.test.ts index 213bff5..4579f21 100644 --- a/test/u128.test.ts +++ b/test/u128.test.ts @@ -15,12 +15,14 @@ describe('u128 functions', () => { }); test('u128 checked operations errors on overflow', () => { - expect(u128.checkedAdd(u128(45n), u128(25n))).toBe(70n); - expect(u128.checkedMultiply(u128(45n), u128(25n))).toBe(1125n); + expect(u128.checkedAdd(u128(45n), u128(25n)).unwrap()).toBe(70n); + expect(u128.checkedMultiply(u128(45n), u128(25n)).unwrap()).toBe(1125n); - expect(() => u128.checkedAdd(u128(2n ** 127n), u128(2n ** 127n))).toThrow(); expect(() => - u128.checkedMultiply(u128(2n ** 127n), u128(2n ** 127n)) + u128.checkedAdd(u128(2n ** 127n), u128(2n ** 127n)).unwrap() + ).toThrow(); + expect(() => + u128.checkedMultiply(u128(2n ** 127n), u128(2n ** 127n)).unwrap() ).toThrow(); }); @@ -40,12 +42,23 @@ describe('u128 functions', () => { }); describe('u128 varint encoding', () => { + test('zero round trips successfully', () => { + const n = u128(0); + const encoded = u128.encodeVarInt(n); + + const seekBuffer = new SeekBuffer(encoded); + const decoded = u128.tryDecodeVarInt(seekBuffer); + + expect(decoded).toBe(n); + expect(seekBuffer.isFinished()).toBe(true); + }); + test('encode/decode varints roundtrips correctly', () => { const n = u128.MAX; const encoded = u128.encodeVarInt(n); const seekBuffer = new SeekBuffer(encoded); - const decoded = u128.readVarInt(seekBuffer); + const decoded = u128.tryDecodeVarInt(seekBuffer); expect(decoded).toBe(n); expect(seekBuffer.isFinished()).toBe(true); @@ -58,7 +71,7 @@ describe('u128 varint encoding', () => { const encoded = u128.encodeVarInt(n); const seekBuffer = new SeekBuffer(encoded); - const decoded = u128.readVarInt(seekBuffer); + const decoded = u128.tryDecodeVarInt(seekBuffer); expect(decoded).toBe(n); expect(seekBuffer.isFinished()).toBe(true); @@ -75,76 +88,97 @@ describe('u128 varint encoding', () => { const encoded = u128.encodeVarInt(n); const seekBuffer = new SeekBuffer(encoded); - const decoded = u128.readVarInt(seekBuffer); + const decoded = u128.tryDecodeVarInt(seekBuffer); expect(decoded).toBe(n); expect(seekBuffer.isFinished()).toBe(true); } }); - test('large varints saturate to maximum', () => { - const seekBuffer = new SeekBuffer( + test('varints may not be longer than 19 bytes', () => { + const VALID = new SeekBuffer( Buffer.from([ - 130, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, - 254, 254, 254, 255, 0, + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 0, ]) ); - const decoded = u128.readVarInt(seekBuffer); - expect(decoded).toBe(u128.MAX); - }); - - test('truncated large varints with large final byte saturate to maximum', () => { - const seekBuffer = new SeekBuffer( + const INVALID = new SeekBuffer( Buffer.from([ - 130, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, - 254, 254, 254, 255, 255, + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 0, ]) ); - const decoded = u128.readVarInt(seekBuffer); - expect(decoded).toBe(u128.MAX); - }); - test('varints with large final byte saturate to maximum', () => { - const seekBuffer = new SeekBuffer( - Buffer.from([ - 130, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, - 254, 254, 254, 255, 127, - ]) - ); - const decoded = u128.readVarInt(seekBuffer); - expect(decoded).toBe(u128.MAX); + expect(u128.tryDecodeVarInt(VALID)).toBe(u128(0)); + expect(() => u128.tryDecodeVarInt(INVALID)).toThrow('Overlong'); }); - it.each([ - [0n, [0x00]], - [1n, [0x01]], - [127n, [0x7f]], - [128n, [0x80, 0x00]], - [255n, [0x80, 0x7f]], - [256n, [0x81, 0x00]], - [16383n, [0xfe, 0x7f]], - [16384n, [0xff, 0x00]], - [16511n, [0xff, 0x7f]], - [65535n, [0x82, 0xfe, 0x7f]], - [1n << 32n, [0x8e, 0xfe, 0xfe, 0xff, 0x00]], - ])( - 'taproot annex format bip test vectors round trip successfully', - (n, encoding) => { - const actualEncoding = u128.encodeVarInt(u128(n)); - expect([...actualEncoding]).toEqual(encoding); - - const seekBuffer = new SeekBuffer(Buffer.from(encoding)); - const actualu128 = u128.readVarInt(seekBuffer); - - expect(actualu128).toBe(n); - expect(seekBuffer.isFinished()).toBe(true); - } - ); - - test('varints may be truncated', () => { - const seekBuffer = new SeekBuffer(Buffer.from([128])); - const decoded = u128.readVarInt(seekBuffer); + test('varints may not overflow u128', () => { + expect(() => + u128.tryDecodeVarInt( + new SeekBuffer( + Buffer.from([ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 64, + ]) + ) + ) + ).toThrow('Overflow'); + expect(() => + u128.tryDecodeVarInt( + new SeekBuffer( + Buffer.from([ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 32, + ]) + ) + ) + ).toThrow('Overflow'); + expect(() => + u128.tryDecodeVarInt( + new SeekBuffer( + Buffer.from([ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 16, + ]) + ) + ) + ).toThrow('Overflow'); + expect(() => + u128.tryDecodeVarInt( + new SeekBuffer( + Buffer.from([ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 8, + ]) + ) + ) + ).toThrow('Overflow'); + expect(() => + u128.tryDecodeVarInt( + new SeekBuffer( + Buffer.from([ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 4, + ]) + ) + ) + ).toThrow('Overflow'); + expect( + u128.tryDecodeVarInt( + new SeekBuffer( + Buffer.from([ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 128, 128, 128, 128, 2, + ]) + ) + ) + ).toBe(u128(2n ** 127n)); + }); - expect(decoded).toBe(1n); + test('varints must be terminated', () => { + expect(() => + u128.tryDecodeVarInt(new SeekBuffer(Buffer.from([128]))) + ).toThrow('Unterminated'); }); });