Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/utils/src/bitauth-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
27 changes: 23 additions & 4 deletions packages/utils/src/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SourceTagEntry[]>((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 {
Expand Down
15 changes: 15 additions & 0 deletions packages/utils/test/bitauth-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
38 changes: 38 additions & 0 deletions packages/utils/test/fixtures/bitauth-script.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+$/, ''),
},
];
Expand Down
Loading