From da873d1954f58d13adbeaa8492deaf58f68e89fe Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 29 Sep 2026 11:00:11 +0200 Subject: [PATCH] fix: fix failing debug() when scope cleanup tags overlapped due to optimisations --- packages/utils/src/bitauth-script.ts | 4 +- packages/utils/src/script.ts | 27 +++++++++++-- packages/utils/test/bitauth-script.test.ts | 15 ++++++++ .../test/fixtures/bitauth-script.fixture.ts | 38 +++++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/utils/src/bitauth-script.ts b/packages/utils/src/bitauth-script.ts index 7cd2f7b48..03dd67d32 100644 --- a/packages/utils/src/bitauth-script.ts +++ b/packages/utils/src/bitauth-script.ts @@ -82,7 +82,9 @@ function segmentScript(params: WalkParams): Segment[] { const tag = findTagAt(tags, index); if (tag) { - segments.push(annotationSegment(script, tag, tags, locationData, sourceLines)); + // The rendered output is executed, so an overlapping tag must never re-emit opcodes that were already rendered + const remainingTag = { ...tag, startIndex: index }; + segments.push(annotationSegment(script, remainingTag, tags, locationData, sourceLines)); index = tag.endIndex + 1; continue; } diff --git a/packages/utils/src/script.ts b/packages/utils/src/script.ts index 7a62fd74d..115baeb8c 100644 --- a/packages/utils/src/script.ts +++ b/packages/utils/src/script.ts @@ -193,17 +193,36 @@ const SCOPE_CLEANUP_OPCODES = [Op.OP_DROP, Op.OP_NIP, Op.OP_2DROP]; function reconcileScopeCleanupTags(script: Script, sourceTags: SourceTagEntry[]): SourceTagEntry[] { const otherTags = sourceTags.filter((tag) => tag.kind !== SourceTagKind.SCOPE_CLEANUP); - return sourceTags.filter((tag) => { - if (tag.kind !== SourceTagKind.SCOPE_CLEANUP) return true; + const cleanupTags = sourceTags.filter((tag) => { + if (tag.kind !== SourceTagKind.SCOPE_CLEANUP) return false; const isOnlyCleanupOpcodes = range(tag.startIndex, tag.endIndex) .every((index) => SCOPE_CLEANUP_OPCODES.includes(script[index] as Op)); if (!isOnlyCleanupOpcodes) return false; - const overlapsOtherTag = otherTags - .some((other) => tag.startIndex <= other.endIndex && other.startIndex <= tag.endIndex); + const overlapsOtherTag = otherTags.some((other) => tagsOverlap(tag, other)); return !overlapsOtherTag; }); + + const mergedCleanupTags = mergeOverlappingTags(cleanupTags); + const allTags = [...otherTags, ...mergedCleanupTags]; + return allTags.sort((a, b) => a.startIndex - b.startIndex); +} + +function mergeOverlappingTags(tags: SourceTagEntry[]): SourceTagEntry[] { + const sortedTags = [...tags].sort((a, b) => a.startIndex - b.startIndex); + + return sortedTags.reduce((mergedTags, tag) => { + const previousTag = mergedTags.at(-1); + if (!previousTag || !tagsOverlap(previousTag, tag)) return [...mergedTags, tag]; + + const mergedTag = { ...previousTag, endIndex: Math.max(previousTag.endIndex, tag.endIndex) }; + return [...mergedTags.slice(0, -1), mergedTag]; + }, []); +} + +function tagsOverlap(a: SourceTagEntry, b: SourceTagEntry): boolean { + return a.startIndex <= b.endIndex && b.startIndex <= a.endIndex; } interface ReplaceOpsResult extends OptimiseBytecodeResult { diff --git a/packages/utils/test/bitauth-script.test.ts b/packages/utils/test/bitauth-script.test.ts index a4be46d02..dcd8c2574 100644 --- a/packages/utils/test/bitauth-script.test.ts +++ b/packages/utils/test/bitauth-script.test.ts @@ -50,6 +50,21 @@ describe('Libauth Script formatting', () => { const formattedBitAuthScript = formatBitAuthScript(debugInformation, sourceCode); expectBitAuthScriptToCompileTo(formattedBitAuthScript, debugInformation.bytecode); }); + + it('should emit every opcode once when source tags overlap', () => { + // Two scope cleanups sharing the OP_2DROP (30:30 and 30:31) must not render the shared opcode twice + const fixture = fixtures.find((f) => f.name.startsWith('OverlappingScopeCleanup'))!; + const debugInformation = { + bytecode: binToHex(scriptToBytecode(asmToScript(fixture.asmBytecode))), + sourceMap: fixture.sourceMap, + sourceTags: '25:25:fu;26:29:lc;30:30:sc;30:31:sc;36:36:sc', + logs: [], + requires: [], + }; + + const formattedBitAuthScript = formatBitAuthScript(debugInformation, fixture.sourceCode); + expectBitAuthScriptToCompileTo(formattedBitAuthScript, debugInformation.bytecode); + }); }); describe('User-defined function formatting', () => { diff --git a/packages/utils/test/fixtures/bitauth-script.fixture.ts b/packages/utils/test/fixtures/bitauth-script.fixture.ts index 5dcce2670..285a232ab 100644 --- a/packages/utils/test/fixtures/bitauth-script.fixture.ts +++ b/packages/utils/test/fixtures/bitauth-script.fixture.ts @@ -401,6 +401,44 @@ OP_ENDIF /* } */ OP_0 OP_GREATERTHAN /* require(x > 0); */ /* } */ /* } */ +`.replace(/^\n+/, '').replace(/\n+$/, ''), + }, + { + // The optimiser merges the loop variable drop with the if-block drops (OP_DROP OP_DROP OP_DROP -> OP_2DROP OP_DROP), + // so both scope cleanups share the OP_2DROP and are merged into a single tag + name: 'OverlappingScopeCleanup (merged cleanup of nested scopes)', + sourceCode: `contract OverlappingScopeCleanup() { + function spend(int a, int b) { + if (a > 0) { + int x = a + 1; + int y = a + 2; + for (int i = 0; i < 2; i = i + 1) { + require(x + y > i); + } + } + require(b == 7); + } +}`, + asmBytecode: 'OP_DUP OP_0 OP_GREATERTHAN OP_IF OP_DUP OP_1ADD OP_OVER OP_2 OP_ADD OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_2 OP_PICK OP_2 OP_PICK OP_ADD OP_OVER OP_GREATERTHAN OP_VERIFY OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_2DROP OP_DROP OP_ENDIF OP_SWAP OP_7 OP_NUMEQUAL OP_NIP', + sourceMap: '3:12:3:13;:16::17;:12:::1;:19:9:9:0;4:20:4:21;:::25:1;5::5:21:0;:24::25;:20:::1;6:25:6:26:0;:12:8:13;:28:6:29;:32::33;:28:::1;;;:46:8:13:0;7:24:7:25;;:28::29;;:24:::1;:32::33:0;:24:::1;:16::35;6:35:6:44;:46:8:13;;:12;;3:19:9:9;;;10:16:10:17:0;:21::22;:8::24:1;2:33:11:5', + sourceTags: '25:25:fu;26:29:lc;30:31:sc;36:36:sc', + expectedBitAuthScript: ` + /* contract OverlappingScopeCleanup() { */ + /* function spend(int a, int b) { */ +OP_DUP OP_0 OP_GREATERTHAN OP_IF /* if (a > 0) { */ +OP_DUP OP_1ADD /* int x = a + 1; */ +OP_OVER OP_2 OP_ADD /* int y = a + 2; */ +OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF /* for (int i = 0; i < 2; i = i + 1) { */ +OP_2 OP_PICK OP_2 OP_PICK OP_ADD OP_OVER OP_GREATERTHAN OP_VERIFY /* require(x + y > i); */ +OP_1ADD /* >>> for-loop update (i = i + 1) */ +OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL /* >>> loop condition check */ + /* } */ +OP_2DROP OP_DROP /* >>> scope cleanup */ +OP_ENDIF /* } */ +OP_SWAP OP_7 OP_NUMEQUAL /* require(b == 7); */ +OP_NIP /* >>> scope cleanup */ + /* } */ + /* } */ `.replace(/^\n+/, '').replace(/\n+$/, ''), }, ];