diff --git a/src/script.js b/src/script.js index 7c4aad1..cc20182 100644 --- a/src/script.js +++ b/src/script.js @@ -199,23 +199,32 @@ function toBuffer (input) { return bsv.Script.fromASM(text.replace(/\s+/g, ' ')).toBuffer() } -// Whether a push anywhere in buf, including after OP_RETURN, runs past its end. -function truncated (buf) { +// What in buf, including the data after OP_RETURN, ASM cannot hold: a push +// that runs past the end, a push not written with the shortest opcode for its +// length (ASM records only the data), or an opcode with no name. +function asmObstacles (buf) { + const found = { truncated: false, nonMinimal: false, unnamed: false } let i = 0 while (i < buf.length) { const code = buf[i++] - if (code === 0 || code > OP.OP_PUSHDATA4) continue + if (code > OP.OP_PUSHDATA4) { + if (!NAMES[code]) found.unnamed = true + continue + } + if (code === 0) continue let len = code if (code >= OP.OP_PUSHDATA1) { const w = code === OP.OP_PUSHDATA1 ? 1 : code === OP.OP_PUSHDATA2 ? 2 : 4 - if (i + w > buf.length) return true + if (i + w > buf.length) { found.truncated = true; break } len = w === 1 ? buf[i] : w === 2 ? buf.readUInt16LE(i) : buf.readUInt32LE(i) i += w + const shortest = len === 0 ? 0 : len < OP.OP_PUSHDATA1 ? len : len < 0x100 ? OP.OP_PUSHDATA1 : len < 0x10000 ? OP.OP_PUSHDATA2 : OP.OP_PUSHDATA4 + if (shortest !== code) found.nonMinimal = true } - if (i + len > buf.length) return true + if (i + len > buf.length) { found.truncated = true; break } i += len } - return false + return found } // ASM that reads back as exactly `buf`, or an error. ASM cannot say how data @@ -227,12 +236,12 @@ function exactAsm (buf) { let back = null try { back = toBuffer(asm) } catch (e) {} if (!back || !back.equals(buf)) { - const ops = parse(buf, { deadBlocks: false }) - const why = truncated(buf) + const found = asmObstacles(buf) + const why = found.truncated ? 'it contains a truncated push' - : ops.some(o => isPush(o) && o.code !== TAIL && opSize(pushOp(pushValue(o))) !== opSize(o)) + : found.nonMinimal ? 'a push is not minimally encoded, and ASM cannot say how data was pushed' - : ops.some(o => o.code >= 0 && !isPush(o) && !NAMES[o.code]) + : found.unnamed ? 'it uses an opcode that has no name, which @smartledger/bsv reads back from ASM only from 9.11.1' : !/\bOP_/.test(asm) ? 'it is only data pushes, and ASM with no opcodes reads back as hex' diff --git a/test/unit.test.js b/test/unit.test.js index 2a8585e..ea7e8ed 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -204,6 +204,7 @@ test('ASM output reads back byte for byte, or is refused', () => { // An empty push in OP_RETURN data, as buildDataOut('') writes it (read back from bsv 9.11.2). assert.strictEqual(exactAsm(Buffer.from('516a00020aff', 'hex')), 'OP_1 OP_RETURN 0 0aff') assert.throws(() => exactAsm(Buffer.from('516a05aabb', 'hex')), /truncated push/) + assert.throws(() => exactAsm(Buffer.from('516a4c00', 'hex')), /not minimally encoded/) assert.throws(() => exactAsm(Buffer.from('4c50' + 'ab'.repeat(80), 'hex')), /only data pushes/) const bin = path.join(__dirname, '..', 'bin', 'scriptmin.js') @@ -215,7 +216,7 @@ test('ASM output reads back byte for byte, or is refused', () => { const written = require('fs').readFileSync(path.join(dir, 'out.asm'), 'utf8') assert.ok(written.trim().endsWith('OP_RETURN ' + 'ab'.repeat(40)), written) require('fs').writeFileSync(path.join(dir, 'odd.hex'), '76756a4c03aabbcc') - assert.throws(() => execFileSync('node', [bin, '--asm', '--tests', '0', '-o', path.join(dir, 'odd.asm'), path.join(dir, 'odd.hex')], { stdio: 'pipe' }), e => e.status === 2 && /write it as hex/.test(e.stderr)) + assert.throws(() => execFileSync('node', [bin, '--asm', '--tests', '0', '-o', path.join(dir, 'odd.asm'), path.join(dir, 'odd.hex')], { stdio: 'pipe' }), e => e.status === 2 && /not minimally encoded/.test(e.stderr)) } finally { require('fs').rmSync(dir, { recursive: true, force: true }) }