diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/complex_loop.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/complex_loop.ts index e85dc0355..3ef965264 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/complex_loop.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/complex_loop.ts @@ -6,20 +6,20 @@ export const fixtures: Fixture[] = [ contractName: 'Loopy', constructorInputs: [], abi: [{ name: 'doLoop', inputs: [] }], - bytecode: 'OP_0 OP_0 OP_0 OP_0 OP_BEGIN OP_DUP OP_UTXOVALUE OP_4 OP_ROLL OP_DROP OP_SWAP OP_2SWAP OP_ROT OP_1ADD OP_TXOUTPUTCOUNT OP_2DUP OP_LESSTHAN OP_DUP OP_IF OP_2 OP_PICK OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NOT OP_NIP OP_DUP OP_IF OP_4 OP_PICK OP_3 OP_PICK OP_OUTPUTVALUE OP_ADD OP_5 OP_ROLL OP_DROP OP_SWAP OP_TOALTSTACK OP_SWAP OP_2SWAP OP_ROT OP_FROMALTSTACK OP_ELSE OP_3 OP_ROLL OP_1ADD OP_SWAP OP_2SWAP OP_ROT OP_ENDIF OP_ENDIF OP_2DROP OP_DUP OP_TXINPUTCOUNT OP_GREATERTHANOREQUAL OP_UNTIL OP_2SWAP OP_GREATERTHAN OP_VERIFY OP_SWAP OP_0 OP_GREATERTHAN OP_NIP', + bytecode: 'OP_0 OP_0 OP_0 OP_0 OP_BEGIN OP_DUP OP_UTXOVALUE OP_4 OP_ROLL OP_DROP OP_SWAP OP_2SWAP OP_ROT OP_1ADD OP_TXOUTPUTCOUNT OP_2DUP OP_LESSTHAN OP_DUP OP_IF OP_2 OP_PICK OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NOT OP_NIP OP_DUP OP_IF OP_4 OP_ROLL OP_3 OP_PICK OP_OUTPUTVALUE OP_ADD OP_SWAP OP_TOALTSTACK OP_SWAP OP_2SWAP OP_ROT OP_FROMALTSTACK OP_ELSE OP_3 OP_ROLL OP_1ADD OP_SWAP OP_2SWAP OP_ROT OP_ENDIF OP_ENDIF OP_2DROP OP_DUP OP_TXINPUTCOUNT OP_GREATERTHANOREQUAL OP_UNTIL OP_2SWAP OP_GREATERTHAN OP_VERIFY OP_SWAP OP_0 OP_GREATERTHAN OP_NIP', debug: { - bytecode: '000000006576c6547a757c727b8bc46e9f76635279d100879177766354795379cc93557a757c6b7c727b6c67537a8b7c727b68686d76c3a26672a0697c00a077', - sourceMap: '3:23:3:24;4:24:4:25;5:25:5:26;6:16:6:17;8:8:26:39;9:33:9:34;:23::41:1;:12::42;;;;;;10::10:22;12:20:12:37:0;13:21:13:26;::::1;15:16:15:17:0;:19:25:13;16:31:16:32;;:20::47:1;:51::53:0;:20:::1;;:16::54;18:20:18:21:0;:23:20:17;19:32:19:41;;:55::56;;:44::63:1;:32;:20::64;;;;;;;;;20:23:22:17:0;21:20:21:48:1;;;;;;20:23:22:17;15:19:25:13;8:11:26:9;26:17::18:0;:21::37;8:8::39:1;;28:16:28:36:0;::::1;:8::38;29:16:29:26:0;:29::30;:8::32:1;2:22:30:5', + bytecode: '000000006576c6547a757c727b8bc46e9f76635279d1008791777663547a5379cc937c6b7c727b6c67537a8b7c727b68686d76c3a26672a0697c00a077', + sourceMap: '3:23:3:24;4:24:4:25;5:25:5:26;6:16:6:17;8:8:26:39;9:33:9:34;:23::41:1;:12::42;;;;;;10:16:10:21;12:20:12:37:0;13:21:13:26;::::1;15:16:15:17:0;:19:25:13;16:31:16:32;;:20::47:1;:51::53:0;:20:::1;;:16::54;18:20:18:21:0;:23:20:17;19:32:19:41;;:55::56;;:44::63:1;:32;:20::64;;;;;;20:23:22:17:0;21:33:21:43;;:::47:1;:20::48;;;20:23:22:17;15:19:25:13;8:11:26:9;26:17::18:0;:21::37;8:8::39:1;;28:16:28:36:0;::::1;:8::38;29:16:29:26:0;:29::30;:8::32:1;2:22:30:5', logs: [ - { ip: 51, line: 24, data: [{ stackIndex: 3, type: 'int', ip: 51 }] }, + { ip: 48, line: 24, data: [{ stackIndex: 3, type: 'int', ip: 48 }] }, ], requires: [ - { ip: 59, line: 28 }, - { ip: 63, line: 29 }, + { ip: 56, line: 28 }, + { ip: 60, line: 29 }, ], - sourceTags: '52:52:sc;63:63:sc', + sourceTags: '49:49:sc;60:60:sc', }, - fingerprint: '0dfd8b49c1d5e3af83759f9921d61901e5dbfa99e2b2afbe490e9bb3fba813f5', + fingerprint: 'f20852ce7ad768a7fdd7596dc66e619dea42aaeb2c2a8f5af7622efc41999a65', }, }, ]; diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/compound_assign.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/compound_assign.ts index d48952c3b..16cdb26de 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/compound_assign.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/compound_assign.ts @@ -14,18 +14,18 @@ export const fixtures: Fixture[] = [ // require(x == 15); + 'OP_DUP OP_15 OP_NUMEQUALVERIFY ' // x -= 3; - + 'OP_DUP OP_3 OP_SUB ' + + 'OP_3 OP_SUB ' // require(x == 12); + 'OP_12 OP_NUMEQUAL ' // Cleanup - + 'OP_NIP OP_NIP', - fingerprint: '3e67ebf63dbbf278589d0fffe8dd94663758334a8bf7858ea9c3613cd472b190', + + 'OP_NIP', + fingerprint: 'c67fdea7bf9a5f18f19e1940adefc1f4b9add4f1957c1c0f2d45ae7e2e8eb1e6', debug: { - bytecode: '5a765593765f9d7653945c9c7777', - sourceMap: '3:16:3:18;4:8:4:9;:13::14;:8:::1;5:16:5:17:0;:21::23;:8::25:1;7::7:9:0;:13::14;:8:::1;8:21:8:23:0;:8::25:1;2:21:9:5;', + bytecode: '5a765593765f9d53945c9c77', + sourceMap: '3:16:3:18;4:8:4:9;:13::14;:8:::1;5:16:5:17:0;:21::23;:8::25:1;7:13:7:14:0;:8:::1;8:21:8:23:0;:8::25:1;2:21:9:5', logs: [], - requires: [{ ip: 6, line: 5 }, { ip: 12, line: 8 }], - sourceTags: '12:13:sc', + requires: [{ ip: 6, line: 5 }, { ip: 11, line: 8 }], + sourceTags: '11:11:sc', }, }, }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/deep_increment_decrement.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/deep_increment_decrement.ts index 24143b19b..ebda47ce1 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/deep_increment_decrement.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/deep_increment_decrement.ts @@ -63,7 +63,7 @@ export const fixtures: Fixture[] = [ + 'OP_ENDIF', debug: { bytecode: '76009c637800515253006576539f766b63557a8c7c6b7c6b7c727b6c6c547a8b7c6b7c727b6c8b686c9166757b7b9393569d937b9c7767519d6e9f638c7c8c7c7b8c7b7b68939300a068', - sourceMap: '2:4:16:5;;;;3:24:3:30;4:20:4:21;5:16:5:17;6::6;7::7;9:21:9:22;:8:12:9;:24:9:25;:28::29;:24:::1;;;:36:12:9:0;10:12:10:38:1;;;;;;;;;;;;11::11:30;;;;;;;;;9:31:9:34;:36:12:9;;:8;;;14:16:14:17:0;:20::21;:16:::1;:::25;:29::30:0;:8::32:1;15:16:15:33;:37::43:0;:8::45:1;2:30:16:5;:4;18::26::0;;19:12:19:17;::::1;:19:23:9:0;20:12:20:22:1;21::21;;;22::22;;;;19:19:23:9;25:16:25:21;:::25;:28::29:0;:8::31:1;1:0:27:1', + sourceMap: '2:4:16:5;;;;3:24:3:30;4:20:4:21;5:16:5:17;6::6;7::7;9:21:9:22;:8:12:9;:24:9:25;:28::29;:24:::1;;;:36:12:9:0;10:24:10:33;;:::37:1;:12::38;;;;;;;;;11:20:11:25:0;;:::29:1;:12::30;;;;;;9:31:9:34;:36:12:9;;:8;;;14:16:14:17:0;:20::21;:16:::1;:::25;:29::30:0;:8::32:1;15:16:15:33;:37::43:0;:8::45:1;2:30:16:5;:4;18::26::0;;19:12:19:17;::::1;:19:23:9:0;20:16:20:21:1;21::21:17:0;:::21:1;:12::22;22:16:22:17:0;:::21:1;:12::22;;19:19:23:9;25:16:25:21;:::25;:28::29:0;:8::31:1;1:0:27:1', logs: [], requires: [ { ip: 49, line: 14 }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop.ts index c0df0c751..e6d1926f8 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop.ts @@ -20,7 +20,7 @@ export const fixtures: Fixture[] = [ fingerprint: 'a1926f6964606192bb765db69639d46975507c021b233c1456d344398fa5b8f5', debug: { bytecode: '00658b76c3a26652a0', - sourceMap: '3:16:3:17;5:8:7:39;6:12:6:22:1;7:17:7:18:0;:21::37;5:8::39:1;;10:20:10:21:0;:8::23:1', + sourceMap: '3:16:3:17;5:8:7:39;6:16:6:21:1;7:17:7:18:0;:21::37;5:8::39:1;;10:20:10:21:0;:8::23:1', logs: [{ ip: 7, line: 9, data: [{ stackIndex: 0, type: 'int', ip: 7 }] }], requires: [{ ip: 9, line: 10 }], }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_no_introspection.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_no_introspection.ts index 000fff1c6..ec733fd29 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_no_introspection.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_no_introspection.ts @@ -9,7 +9,7 @@ export const fixtures: Fixture[] = [ bytecode: 'OP_0 OP_2 OP_BEGIN OP_SWAP OP_1ADD OP_SWAP OP_2DUP OP_ADD OP_10 OP_LESSTHAN OP_VERIFY OP_OVER OP_10 OP_GREATERTHANOREQUAL OP_UNTIL OP_2DROP OP_1', debug: { bytecode: '0052657c8b7c6e935a9f69785aa2666d51', - sourceMap: '3:16:3:17;4::4;6:8:10:25;7:12:7:22:1;;;9:20:9:25:0;::::1;:28::30:0;:20:::1;:12::32;10:17:10:18:0;:21::23;6:8::25:1;;2:22:11:5;', + sourceMap: '3:16:3:17;4::4;6:8:10:25;7:16:7:17;:::21:1;:12::22;9:20:9:25:0;::::1;:28::30:0;:20:::1;:12::32;10:17:10:18:0;:21::23;6:8::25:1;;2:22:11:5;', logs: [ { ip: 6, line: 8, data: [{ stackIndex: 1, type: 'int', ip: 6 }] }, ], diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_require_inside_loop.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_require_inside_loop.ts index 25965c127..e6bab0df7 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_require_inside_loop.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/do_while_loop_require_inside_loop.ts @@ -22,7 +22,7 @@ export const fixtures: Fixture[] = [ fingerprint: '8ba3e5e6fcc79a8e3fb57ac388692ae3b49fbece9f528e547380b24338a8d8b9', debug: { bytecode: '00658b765a9f6976c3a2667551', - sourceMap: '3:16:3:17;5:8:8:39;6:12:6:22:1;7:20:7:21:0;:24::26;:20:::1;:12::28;8:17:8:18:0;:21::37;5:8::39:1;;2:22:9:5;', + sourceMap: '3:16:3:17;5:8:8:39;6:16:6:21:1;7:20:7::0;:24::26;:20:::1;:12::28;8:17:8:18:0;:21::37;5:8::39:1;;2:22:9:5;', logs: [], requires: [{ ip: 6, line: 7 }], }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/double_negation.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/double_negation.ts index abb1f9794..909ada3aa 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/double_negation.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/double_negation.ts @@ -27,7 +27,7 @@ export const fixtures: Fixture[] = [ + 'OP_NIP OP_NIP', debug: { bytecode: '76690000657c8b7d5479a27776667b637853799d687c00a07777', - sourceMap: '4:18:4:22;:8::24:1;6:16:6:17:0;7:20:7:25;9:8:13:24;10:12:10:22:1;;::11:20:0;11:24::30;;:19:::1;:12::31;13:18:13:22:0;9:8::24:1;16:14:16:18:0;:12:18:9;17:20:17:21;:25::31;;:12::33:1;16:20:18:9;20:16:20:17:0;:20::21;:8::23:1;2:31:21:5;', + sourceMap: '4:18:4:22;:8::24:1;6:16:6:17:0;7:20:7:25;9:8:13:24;10:16:10:17;:::21:1;:12:11:20:0;11:24::30;;:19:::1;:12::31;13:18:13:22:0;9:8::24:1;16:14:16:18:0;:12:18:9;17:20:17:21;:25::31;;:12::33:1;16:20:18:9;20:16:20:17:0;:20::21;:8::23:1;2:31:21:5;', logs: [], requires: [ { ip: 2, line: 4 }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/for_loop_stack_items.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/for_loop_stack_items.ts index 5daa5ef8c..ad9355d14 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/for_loop_stack_items.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/for_loop_stack_items.ts @@ -6,19 +6,19 @@ export const fixtures: Fixture[] = [ contractName: 'ForLoopBasic', constructorInputs: [], abi: [{ name: 'spend', inputs: [] }], - bytecode: 'OP_0 OP_1 OP_1 OP_0 OP_BEGIN OP_DUP OP_3 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_3 OP_PICK OP_OVER OP_ADD OP_4 OP_ROLL OP_DROP OP_SWAP OP_2SWAP OP_ROT OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_ROT OP_3 OP_NUMEQUALVERIFY OP_SWAP OP_1 OP_NUMEQUALVERIFY OP_1 OP_NUMEQUAL', + bytecode: 'OP_0 OP_1 OP_1 OP_0 OP_BEGIN OP_DUP OP_3 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_3 OP_ROLL OP_OVER OP_ADD OP_SWAP OP_2SWAP OP_ROT OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_ROT OP_3 OP_NUMEQUALVERIFY OP_SWAP OP_1 OP_NUMEQUALVERIFY OP_1 OP_NUMEQUAL', debug: { - bytecode: '005151006576539f766b6353797893547a757c727b8b686c9166757b539d7c519d519c', - sourceMap: '3:18:3:19;4:16:4:17;5::5;7:21:7:22;:8:9:9;:24:7:25;:28::29;:24:::1;;;:39:9:9:0;8:18:8:21;;:24::25;:18:::1;:12::26;;;;;;7:31:7:37;:39:9:9;;:8;;;11:16:11:19:0;:23::24;:8::26:1;12:16:12:17:0;:21::22;:8::24:1;13:21:13:22:0;:8::24:1', + bytecode: '005151006576539f766b63537a78937c727b8b686c9166757b539d7c519d519c', + sourceMap: '3:18:3:19;4:16:4:17;5::5;7:21:7:22;:8:9:9;:24:7:25;:28::29;:24:::1;;;:39:9:9:0;8:18:8:21;;:24::25;:18:::1;:12::26;;;7:31:7:37;:39:9:9;;:8;;;11:16:11:19:0;:23::24;:8::26:1;12:16:12:17:0;:21::22;:8::24:1;13:21:13:22:0;:8::24:1', logs: [], requires: [ - { ip: 29, line: 11 }, - { ip: 32, line: 12 }, - { ip: 35, line: 13 }, + { ip: 26, line: 11 }, + { ip: 29, line: 12 }, + { ip: 32, line: 13 }, ], - sourceTags: '21:21:fu;22:25:lc;26:26:sc', + sourceTags: '18:18:fu;19:22:lc;23:23:sc', }, - fingerprint: 'daa7f96c846a6af3366dd7a2c5597d29524665dce0cc9a5fc5e2a12c97babe2f', + fingerprint: '97fab6cffeffd849fe147d49c58f2bc421022c38a11ec92143cb791c0fbb7900', }, }, ]; diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/for_while_nested.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/for_while_nested.ts index f2ee5e92d..6e475c9f8 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/for_while_nested.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/for_while_nested.ts @@ -9,7 +9,7 @@ export const fixtures: Fixture[] = [ bytecode: 'OP_0 OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_3DUP OP_DROP OP_ADD OP_OVER OP_ADD OP_2SWAP OP_NIP OP_ROT OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_SWAP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_4 OP_NUMEQUAL', debug: { bytecode: '00006576529f766b63006576529f766b636f7593789372777b8b686c91667c8b77686c916675549c', - sourceMap: '3:18:3:19;5:21:5:22;:8:13:9;:24:5:25;:28::29;:24:::1;;;:42:13:9:0;6:20:6:21;8:12:12:13;:19:8:20;:23::24;:19:::1;;;:26:12:13:0;9:22:9:29;;::::1;:32::33:0;:22:::1;:16::34;;;10::10:26;8:26:12:13;;:12;;5:31:5:40;;::13:9;:42;;:8;;;15:23:15:24:0;:8::26:1', + sourceMap: '3:18:3:19;5:21:5:22;:8:13:9;:24:5:25;:28::29;:24:::1;;;:42:13:9:0;6:20:6:21;8:12:12:13;:19:8:20;:23::24;:19:::1;;;:26:12:13:0;9:22:9:29;;::::1;:32::33:0;:22:::1;:16::34;;;10:20:10:25;8:26:12:13;;:12;;5:35:5:36:0;:::40:1;:31:13:9;:42;;:8;;;15:23:15:24:0;:8::26:1', logs: [ { ip: 26, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/global_function_in_control_flow.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/global_function_in_control_flow.ts index c602161a6..59ff700f9 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/global_function_in_control_flow.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/global_function_in_control_flow.ts @@ -16,7 +16,7 @@ export const fixtures: Fixture[] = [ // for (int i = 0; i < 3; i = i + 1) { + 'OP_0 OP_BEGIN OP_DUP OP_3 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF ' // total = total + triple(x); - + 'OP_OVER OP_3 OP_PICK OP_3 OP_MUL OP_ADD OP_ROT OP_DROP OP_SWAP ' + + 'OP_SWAP OP_2 OP_PICK OP_3 OP_MUL OP_ADD OP_SWAP ' // For loop update + 'OP_1ADD ' // Loop condition @@ -33,13 +33,13 @@ export const fixtures: Fixture[] = [ + 'OP_9 OP_NUMEQUAL ' // Cleanup + 'OP_NIP', - fingerprint: 'c14759968b45b80fb6bdf19f487d3dbad06e493976633b61eeb4d1522085b5cc', + fingerprint: 'ea5c2c2a8339e34338cf04379f01f1dd41cfc861e3034ac93966e00598a34e57', debug: { - bytecode: '7c92007c63006576539f766b637853795395937b757c8b686c916675677853957768599c77', - sourceMap: '6:26:6:38;;7:20:7:21;8:12:8:19;:21:12:9;9:25:9:26;:12:11:13;:28:9:29;:32::33;:28:::1;;;:46:11:13:0;10:24:10:29;:39::40;;:32::41:1;;:24;:16::42;;;9:35:9:44;:46:11:13;;:12;;;12:15:14:9:0;13:27:13:28;:20::29:1;;:12::30;12:15:14:9;15:25:15:26:0;:8::28:1;6:40:16:5', + bytecode: '7c92007c63006576539f766b637c52795395937c8b686c916675677853957768599c77', + sourceMap: '6:26:6:38;;7:20:7:21;8:12:8:19;:21:12:9;9:25:9:26;:12:11:13;:28:9:29;:32::33;:28:::1;;;:46:11:13:0;10:24:10:29;:39::40;;:32::41:1;;:24;:16::42;9:39:9:44;:46:11:13;;:12;;;12:15:14:9:0;13:27:13:28;:20::29:1;;:12::30;12:15:14:9;15:25:15:26:0;:8::28:1;6:40:16:5', logs: [], - requires: [{ ip: 36, line: 15 }], - sourceTags: '0:1:pv;22:22:fu;23:26:lc;27:27:sc;36:36:sc', + requires: [{ ip: 34, line: 15 }], + sourceTags: '0:1:pv;20:20:fu;21:24:lc;25:25:sc;34:34:sc', functions: [ { name: 'triple', @@ -50,7 +50,7 @@ export const fixtures: Fixture[] = [ requires: [], }, ], - inlineRanges: '16:17:triple;30:31:triple', + inlineRanges: '16:17:triple;28:29:triple', }, }, }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/increment_decrement.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/increment_decrement.ts index 59d1b4593..cf45fbf05 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/increment_decrement.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/increment_decrement.ts @@ -14,18 +14,18 @@ export const fixtures: Fixture[] = [ // require(x == 6); + 'OP_DUP OP_6 OP_NUMEQUALVERIFY ' // x--; - + 'OP_DUP OP_1SUB ' + + 'OP_1SUB ' // require(x == 5); + 'OP_5 OP_NUMEQUAL ' // Cleanup - + 'OP_NIP OP_NIP', - fingerprint: '5ccf48b6901709f59aaa5fab621021cf8598fb6fdd591f7f714902070c84bb85', + + 'OP_NIP', + fingerprint: '142fea5495e16cd7fd687d05c5ab810d99db4dff3c1c200e3a16e457e1c135aa', debug: { - bytecode: '55768b76569d768c559c7777', - sourceMap: '3:16:3:17;4:8:4:9;:::11:1;5:16:5:17:0;:21::22;:8::24:1;7::7:9:0;:::11:1;8:21:8:22:0;:8::24:1;2:21:9:5;', + bytecode: '55768b76569d8c559c77', + sourceMap: '3:16:3:17;4:8:4:9;:::11:1;5:16:5:17:0;:21::22;:8::24:1;7::7:11;8:21:8:22:0;:8::24:1;2:21:9:5', logs: [], - requires: [{ ip: 5, line: 5 }, { ip: 10, line: 8 }], - sourceTags: '10:11:sc', + requires: [{ ip: 5, line: 5 }, { ip: 9, line: 8 }], + sourceTags: '9:9:sc', }, }, }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/sum_input_amount.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/sum_input_amount.ts index 62cea672e..c9cfdb9be 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/sum_input_amount.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/sum_input_amount.ts @@ -26,7 +26,7 @@ export const fixtures: Fixture[] = [ fingerprint: '46abccb7ef0a81c35e6f20ae62afd904d9e72e7bca703798a6726d7434ab7ad1', debug: { bytecode: '00006576c67b757c8b76c3a2667c02d007a077', - sourceMap: '3:18:3:19;4:16:4:17;6:8:9:39;7:28:7:29;:18::36:1;:12::37;;;8::8:22;9:17:9:18:0;:21::37;6:8::39:1;;12:16:12:19:0;:22::26;:8::28:1;2:22:13:5', + sourceMap: '3:18:3:19;4:16:4:17;6:8:9:39;7:28:7:29;:18::36:1;:12::37;;;8:16:8:21;9:17:9:18:0;:21::37;6:8::39:1;;12:16:12:19:0;:22::26;:8::28:1;2:22:13:5', logs: [{ ip: 13, line: 11, data: [{ stackIndex: 1, type: 'int', ip: 13 }] }], requires: [{ ip: 16, line: 12 }], sourceTags: '16:16:sc', diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_modifiers.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_modifiers.ts index bbeb92c9b..dbd359900 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_modifiers.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_modifiers.ts @@ -18,21 +18,20 @@ export const fixtures: Fixture[] = [ // if (acc > 0) { + 'OP_DUP OP_0 OP_GREATERTHAN OP_IF ' // (acc, int unused scratch) = pair(acc); - // the unused declaration is dropped instead of parked, then the reassignment - // value folds into acc's slot - + 'OP_DUP OP_DUP OP_1ADD OP_SWAP OP_2 OP_MUL OP_DROP OP_NIP OP_ENDIF ' + // the unused declaration is dropped instead of parked, and acc is updated in place + + 'OP_DUP OP_1ADD OP_SWAP OP_2 OP_MUL OP_DROP OP_ENDIF ' // require(acc + doubled >= 0); + 'OP_ROT OP_ADD OP_0 OP_GREATERTHANOREQUAL OP_VERIFY ' // require(payload == 0x12); + '12 OP_EQUAL', - fingerprint: '77f06b3b6cfe6d5e97768ab4c1649870173bfaedd960771ceec13a89356867cb', + fingerprint: '934d6300e6c249103dcb66220304f897a237d14f5276448eaf41f6fe937d685b', debug: { - bytecode: '76768b7c529577021234517f757b7600a06376768b7c52957577687b9300a269011287', + bytecode: '76768b7c529577021234517f757b7600a063768b7c529575687b9300a269011287', sourceMap: '12:60:12:61;:55::62:1;;;;;:8::63;15:46:15:52:0;:59::60;:46::61:1;:8::62;19:18:19:19:0;' - + '20:12:20:15;:18::19;:12:::1;:21:22:9:0;21:45:21:48;:40::49:1;;;;;:12::50;;20:21:22:9;' + + '20:12:20:15;:18::19;:12:::1;:21:22:9:0;21:40:21:49:1;;;;;:12::50;20:21:22:9;' + '24:22:24:29:0;:16:::1;:33::34:0;:16:::1;:8::36;25:27:25:31:0;:8::33:1', logs: [], - requires: [{ ip: 29, line: 24 }, { ip: 32, line: 25 }], + requires: [{ ip: 27, line: 24 }, { ip: 30, line: 25 }], functions: [ { name: 'pair', @@ -43,7 +42,7 @@ export const fixtures: Fixture[] = [ requires: [], }, ], - inlineRanges: '1:5:pair;17:21:pair', + inlineRanges: '1:5:pair;16:20:pair', }, }, }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment.ts index 0c2123714..284fbfe39 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment.ts @@ -13,7 +13,7 @@ export const fixtures: Fixture[] = [ bytecode: 'OP_DUP OP_1ADD OP_2DUP OP_SWAP OP_2DUP OP_SWAP OP_0 OP_BEGIN OP_DUP OP_4 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_OVER OP_4 OP_PICK OP_SWAP OP_5 OP_ROLL OP_DROP OP_SWAP OP_TOALTSTACK OP_SWAP OP_2SWAP OP_ROT OP_FROMALTSTACK OP_ROT OP_DROP OP_SWAP OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_0 OP_BEGIN OP_DUP OP_4 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_OVER OP_4 OP_PICK OP_OVER OP_2 OP_PICK OP_3 OP_ROLL OP_MUL OP_1ADD OP_TOALTSTACK OP_5 OP_ROLL OP_DROP OP_SWAP OP_TOALTSTACK OP_SWAP OP_2SWAP OP_ROT OP_FROMALTSTACK OP_ROT OP_DROP OP_SWAP OP_FROMALTSTACK OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY OP_SWAP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_OVER OP_4 OP_PICK OP_OVER OP_2 OP_PICK OP_3 OP_ROLL OP_MUL OP_1ADD OP_6 OP_ROLL OP_DROP OP_SWAP OP_TOALTSTACK OP_SWAP OP_TOALTSTACK OP_SWAP OP_2SWAP OP_ROT OP_FROMALTSTACK OP_FROMALTSTACK OP_TOALTSTACK OP_ROT OP_DROP OP_SWAP OP_FROMALTSTACK OP_DUP OP_0 OP_GREATERTHANOREQUAL OP_VERIFY OP_SWAP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_2DUP OP_ROT OP_ROT OP_ADD OP_ROT OP_ADD OP_ADD OP_0 OP_GREATERTHANOREQUAL OP_VERIFY OP_2DROP OP_2DROP OP_1', debug: { bytecode: '768b6e7c6e7c006576549f766b637854797c557a757c6b7c727b6c7b757c8b686c916675006576549f766b63785479785279537a958b6b557a757c6b7c727b6c7b757c6c7600a2697c8b77686c916675006576529f766b63785479785279537a958b567a757c6b7c6b7c727b6c6c6b7b757c6c7600a2697c8b77686c9166756e7b7b937b939300a2696d6d51', - sourceMap: '17:16:18:20;18:::24:1;21:22:21:26:0;:17::27:1;24:26:24:30:0;:21::31:1;27::27:22:0;:8:29:9;:24:27:25;:28::29;:24:::1;;;:42:29:9:0;28:26:28:27;:29::30;;:21::31:1;:12::32;;;;;;;;;;;;27:31:27:40;:42:29:9;;:8;;;33:21:33:22:0;:8:36:9;:24:33:25;:28::29;:24:::1;;;:42:36:9:0;34:34:34:35;:37::38;;:29::39:1;;;;;;;:12::40;;;;;;;;;;;;;;35:20:35:22:0;:26::27;:20:::1;:12::29;33:31:33:40;;::36:9;:42;;:8;;;40:21:40:22:0;:8:43:9;:24:40:25;:28::29;:24:::1;;;:42:43:9:0;41:34:41:35;:37::38;;:29::39:1;;;;;;;:12::40;;;;;;;;;;;;;;;;;42:20:42:22:0;:26::27;:20:::1;:12::29;40:31:40:40;;::43:9;:42;;:8;;;46:24:46:28:0;48:16:48:17;:20::21;:16:::1;:24::25:0;:16:::1;:::29;:33::34:0;:16:::1;:8::36;16:29:49:5;;', + sourceMap: '17:16:18:20;18:::24:1;21:22:21:26:0;:17::27:1;24:26:24:30:0;:21::31:1;27::27:22:0;:8:29:9;:24:27:25;:28::29;:24:::1;;;:42:29:9:0;28:26:28:27;:29::30;;:21::31:1;:12::32;;;;;;;;;;;;27:35:27:40;:42:29:9;;:8;;;33:21:33:22:0;:8:36:9;:24:33:25;:28::29;:24:::1;;;:42:36:9:0;34:34:34:35;:37::38;;:29::39:1;;;;;;;:12::40;;;;;;;;;;;;;;35:20:35:22:0;:26::27;:20:::1;:12::29;33:35:33:36:0;:::40:1;:31:36:9;:42;;:8;;;40:21:40:22:0;:8:43:9;:24:40:25;:28::29;:24:::1;;;:42:43:9:0;41:34:41:35;:37::38;;:29::39:1;;;;;;;:12::40;;;;;;;;;;;;;;;;;42:20:42:22:0;:26::27;:20:::1;:12::29;40:35:40:36:0;:::40:1;:31:43:9;:42;;:8;;;46:24:46:28:0;48:16:48:17;:20::21;:16:::1;:24::25:0;:16:::1;:::29;:33::34:0;:16:::1;:8::36;16:29:49:5;;', logs: [], requires: [ { ip: 71, line: 35 }, @@ -39,7 +39,7 @@ export const fixtures: Fixture[] = [ requires: [], }, ], - inlineRanges: '3:3:swap;5:5:swap;17:17:swap;47:53:step;91:97:step;128:128:swap', + inlineRanges: '3:3:swap;5:5:swap;17:17:swap;47:53:step;91:97:step', }, fingerprint: '16a15dba45f221df3828786838f63c5928154d9339311d6467f79a8f25cb3093', }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment_after_final_read.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment_after_final_read.ts index db521c8d3..dccdd180a 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment_after_final_read.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/tuple_reassignment_after_final_read.ts @@ -55,7 +55,7 @@ export const fixtures: Fixture[] = [ fingerprint: '753932ad4ea2741423cb8c655ecd391aef7aeb228f53b0ef67ba3ef32dd5e52c', debug: { bytecode: '76009c635a00785a9d5379519c6353797652957c8b7b757c7b757c6876009c7c529c9b77777767519d5a00785a9d655279519c766b6352797652957c8b7b757c7b757c7b8b7b7b686c916676009c7c529c9b777768', - sourceMap: '11:4:19:5;;;;12:16:12:18;13::13:17;14::14;:21::23;:8::25:1;15:12:15:13:0;;:17::18;:12:::1;:20:17:9:0;16:26:16:27;;:21::28:1;;;;;:12::29;;;;;;15:20:17:9;18:16:18:17:0;:21::22;:16:::1;:26::27:0;:31::32;:26:::1;:8::34;11:29:19:5;;;:4;21::30::0;;22:16:22:18;23::23:17;24::24;:21::23;:8::25:1;25::28:9:0;:15:25:16;;:20::21;:15:::1;;;:23:28:9:0;26:26:26:27;;:21::28:1;;;;;:12::29;;;;;;27::27:22;;;;25:23:28:9;;:8;;29:16:29:17:0;:21::22;:16:::1;:26::27:0;:31::32;:26:::1;:8::34;21:30:30:5;;10:0:31:1', + sourceMap: '11:4:19:5;;;;12:16:12:18;13::13:17;14::14;:21::23;:8::25:1;15:12:15:13:0;;:17::18;:12:::1;:20:17:9:0;16:26:16:27;;:21::28:1;;;;;:12::29;;;;;;15:20:17:9;18:16:18:17:0;:21::22;:16:::1;:26::27:0;:31::32;:26:::1;:8::34;11:29:19:5;;;:4;21::30::0;;22:16:22:18;23::23:17;24::24;:21::23;:8::25:1;25::28:9:0;:15:25:16;;:20::21;:15:::1;;;:23:28:9:0;26:26:26:27;;:21::28:1;;;;;:12::29;;;;;;27:16:27:17:0;:::21:1;:12::22;;25:23:28:9;;:8;;29:16:29:17:0;:21::22;:16:::1;:26::27:0;:31::32;:26:::1;:8::34;21:30:30:5;;10:0:31:1', logs: [], requires: [{ ip: 8, line: 14 }, { ip: 35, line: 18 }, { ip: 45, line: 24 }, { ip: 82, line: 29 }], sourceTags: '35:37:sc;71:74:lc;82:83:sc', diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop.ts index a456e02eb..009390ea4 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop.ts @@ -9,7 +9,7 @@ export const fixtures: Fixture[] = [ bytecode: 'OP_0 OP_BEGIN OP_DUP OP_TXINPUTCOUNT OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_2 OP_GREATERTHAN', debug: { bytecode: '006576c39f766b638b686c916652a0', - sourceMap: '3:16:3:17;5:8:7:9;:15:5:16;:19::35;:15:::1;;;:37:7:9:0;6:12:6:22:1;5:37:7:9;;:8;;10:20:10:21:0;:8::23:1', + sourceMap: '3:16:3:17;5:8:7:9;:15:5:16;:19::35;:15:::1;;;:37:7:9:0;6:16:6:21:1;5:37:7:9;;:8;;10:20:10:21:0;:8::23:1', logs: [ { ip: 13, line: 9, data: [{ stackIndex: 0, type: 'int', ip: 13 }] }, ], diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_basic.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_basic.ts index f5e430986..9244994fe 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_basic.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_basic.ts @@ -9,7 +9,7 @@ export const fixtures: Fixture[] = [ bytecode: 'OP_0 OP_BEGIN OP_DUP OP_3 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_3 OP_NUMEQUAL', debug: { bytecode: '006576539f766b638b686c9166539c', - sourceMap: '3:16:3:17;5:8:7:9;:15:5:16;:19::20;:15:::1;;;:22:7:9:0;6:12:6:22:1;5:22:7:9;;:8;;9:21:9:22:0;:8::24:1', + sourceMap: '3:16:3:17;5:8:7:9;:15:5:16;:19::20;:15:::1;;;:22:7:9:0;6:16:6:21:1;5:22:7:9;;:8;;9:21:9:22:0;:8::24:1', logs: [], requires: [ { ip: 15, line: 9 }, diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_nested.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_nested.ts index ffc68bed6..292c66449 100644 --- a/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_nested.ts +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/while_loop_nested.ts @@ -36,7 +36,7 @@ export const fixtures: Fixture[] = [ fingerprint: 'd50bb71e353bc7c4b2acd4d9c858760aaf37f88fa10e263dc85002bfed2bbfc2', debug: { bytecode: '00006578529f766b63006576529f766b637c8b7c8b686c91667b8b777c686c91667c529d549c', - sourceMap: '3:16:3:17;4:20:4:21;6:8:15:9;:15:6:16;:19::20;:15:::1;;;:22:15:9:0;7:20:7:21;9:12:12:13;:19:9:20;:23::24;:19:::1;;;:26:12:13:0;10:16:10:34:1;;;11::11:26;9:26:12:13;;:12;;14::14:22;;6:22:15:9;;;;:8;;17:16:17:17:0;:21::22;:8::24:1;18:25:18:26:0;:8::28:1', + sourceMap: '3:16:3:17;4:20:4:21;6:8:15:9;:15:6:16;:19::20;:15:::1;;;:22:15:9:0;7:20:7:21;9:12:12:13;:19:9:20;:23::24;:19:::1;;;:26:12:13:0;10:24:10:29;:::33:1;:16::34;11:20:11:25;9:26:12:13;;:12;;14:16:14:17:0;:::21:1;6:22:15:9;;;;:8;;17:16:17:17:0;:21::22;:8::24:1;18:25:18:26:0;:8::28:1', logs: [], requires: [{ ip: 35, line: 17 }, { ip: 38, line: 18 }], sourceTags: '21:24:lc;29:32:lc', diff --git a/packages/utils/src/optimisations.ts b/packages/utils/src/optimisations.ts index 729d52d32..c658c00b2 100644 --- a/packages/utils/src/optimisations.ts +++ b/packages/utils/src/optimisations.ts @@ -1,6 +1,23 @@ +import { binToHex, hexToBin, isVmNumberError, vmNumberToBigInt } from '@bitauth/libauth'; +import { encodeInt } from './data.js'; + +// A match replaces consecutive parts of the script, each with its own replacement. Every replacement opcode gets the +// merged source location of its part, so a match should keep the opcodes that it does not change in their own parts. +export type OptimisationMatch = OptimisationPart[]; + +export interface OptimisationPart { + // The number of ASM tokens that the part replaces + length: number; + replacement: string[]; +} + +// Matches a family of patterns at the index of the script's ASM tokens, for patterns that a single +// [pattern, replacement] pair cannot express, e.g. because they depend on a stack depth +export type OptimisationMatcher = (tokens: string[], index: number) => OptimisationMatch | undefined; + // Note: the order in which these optimisations are applied can impact the output, so entries should // not be reordered without carefully verifying the compiled bytecode of existing contracts. -export const optimisationReplacements = [ +const optimisationReplacements: Array<[string, string] | OptimisationMatcher> = [ // Hardcoded arithmetic ['OP_1 OP_ADD', 'OP_1ADD'], ['OP_1 OP_SUB', 'OP_1SUB'], @@ -78,44 +95,15 @@ export const optimisationReplacements = [ ['OP_NIP OP_DROP', 'OP_2DROP'], // Far-fetched stuff - ['OP_DUP OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_OVER OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_2 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_3 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_4 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_5 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_6 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_7 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_8 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_9 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_10 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_11 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_12 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_13 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_14 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], - ['OP_16 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'], + // OP_ROT OP_SWAP OP_DROP => OP_SWAP, for any depth n + dropPickedCopy, ['OP_DUP OP_ROT OP_DROP', 'OP_NIP OP_DUP'], ['OP_OVER OP_ROT OP_DROP', 'OP_SWAP'], ['OP_2 OP_PICK OP_ROT OP_DROP', 'OP_NIP OP_OVER'], - ['OP_0 OP_NIP', 'OP_DROP OP_0'], - ['OP_1 OP_NIP', 'OP_DROP OP_1'], - ['OP_2 OP_NIP', 'OP_DROP OP_2'], - ['OP_3 OP_NIP', 'OP_DROP OP_3'], - ['OP_4 OP_NIP', 'OP_DROP OP_4'], - ['OP_5 OP_NIP', 'OP_DROP OP_5'], - ['OP_6 OP_NIP', 'OP_DROP OP_6'], - ['OP_7 OP_NIP', 'OP_DROP OP_7'], - ['OP_8 OP_NIP', 'OP_DROP OP_8'], - ['OP_9 OP_NIP', 'OP_DROP OP_9'], - ['OP_10 OP_NIP', 'OP_DROP OP_10'], - ['OP_11 OP_NIP', 'OP_DROP OP_11'], - ['OP_12 OP_NIP', 'OP_DROP OP_12'], - ['OP_13 OP_NIP', 'OP_DROP OP_13'], - ['OP_14 OP_NIP', 'OP_DROP OP_14'], - ['OP_15 OP_NIP', 'OP_DROP OP_15'], - ['OP_16 OP_NIP', 'OP_DROP OP_16'], + // OP_NIP => OP_DROP , for any push + moveDropBeforePush, ['OP_2 OP_PICK OP_SWAP OP_2 OP_PICK OP_NIP', 'OP_DROP OP_2DUP'], @@ -158,10 +146,6 @@ export const optimisationReplacements = [ ['OP_NOT OP_NOTIF', 'OP_IF'], ['OP_NOT OP_NOT OP_VERIFY', 'OP_VERIFY'], - // Remove extraneous OP_DUP/OP_NIP around in-place increments (e.g. for-loop updates) - ['OP_DUP OP_1ADD OP_NIP', 'OP_1ADD'], - ['OP_DUP OP_1SUB OP_NIP', 'OP_1SUB'], - // Replace alt stack round trips (e.g. from reassignments inside loops) with regular stack ops ['OP_SWAP OP_TOALTSTACK OP_SWAP OP_FROMALTSTACK', 'OP_ROT OP_ROT'], ['OP_TOALTSTACK OP_NIP OP_FROMALTSTACK', 'OP_ROT OP_DROP'], @@ -184,50 +168,225 @@ export const optimisationReplacements = [ ['OP_2 OP_PICK OP_OVER', 'OP_3DUP OP_NIP'], ['OP_2 OP_PICK OP_NIP', 'OP_DROP OP_OVER'], ['OP_TOALTSTACK OP_ROT OP_ROT OP_FROMALTSTACK', 'OP_2SWAP OP_ROT'], - - // NOTE: these optimisations (as well as some others above) are currently hardcoded, but should become dynamic. - // This is something we want to properly implement in v0.15 - // OP_TUCK leaves the two operands in the opposite order, so this only holds for order-independent comparisons ['OP_DUP OP_ROT OP_NUMEQUALVERIFY', 'OP_TUCK OP_NUMEQUALVERIFY'], - // Replace copy-and-drop around in-place increments and decrements of a deeper variable (e.g. x = x + 1 inside a branch) - // These come before the deeper drop rules below, which would otherwise match the end of these patterns first - ['OP_OVER OP_1ADD OP_ROT OP_DROP', 'OP_SWAP OP_1ADD'], - ['OP_2 OP_PICK OP_1ADD OP_3 OP_ROLL OP_DROP', 'OP_ROT OP_1ADD'], - ['OP_3 OP_PICK OP_1ADD OP_4 OP_ROLL OP_DROP', 'OP_3 OP_ROLL OP_1ADD'], - ['OP_4 OP_PICK OP_1ADD OP_5 OP_ROLL OP_DROP', 'OP_4 OP_ROLL OP_1ADD'], - ['OP_5 OP_PICK OP_1ADD OP_6 OP_ROLL OP_DROP', 'OP_5 OP_ROLL OP_1ADD'], - ['OP_6 OP_PICK OP_1ADD OP_7 OP_ROLL OP_DROP', 'OP_6 OP_ROLL OP_1ADD'], - ['OP_7 OP_PICK OP_1ADD OP_8 OP_ROLL OP_DROP', 'OP_7 OP_ROLL OP_1ADD'], - ['OP_8 OP_PICK OP_1ADD OP_9 OP_ROLL OP_DROP', 'OP_8 OP_ROLL OP_1ADD'], - ['OP_9 OP_PICK OP_1ADD OP_10 OP_ROLL OP_DROP', 'OP_9 OP_ROLL OP_1ADD'], - ['OP_10 OP_PICK OP_1ADD OP_11 OP_ROLL OP_DROP', 'OP_10 OP_ROLL OP_1ADD'], - ['OP_11 OP_PICK OP_1ADD OP_12 OP_ROLL OP_DROP', 'OP_11 OP_ROLL OP_1ADD'], - ['OP_12 OP_PICK OP_1ADD OP_13 OP_ROLL OP_DROP', 'OP_12 OP_ROLL OP_1ADD'], - ['OP_13 OP_PICK OP_1ADD OP_14 OP_ROLL OP_DROP', 'OP_13 OP_ROLL OP_1ADD'], - ['OP_14 OP_PICK OP_1ADD OP_15 OP_ROLL OP_DROP', 'OP_14 OP_ROLL OP_1ADD'], - ['OP_15 OP_PICK OP_1ADD OP_16 OP_ROLL OP_DROP', 'OP_15 OP_ROLL OP_1ADD'], - ['OP_OVER OP_1SUB OP_ROT OP_DROP', 'OP_SWAP OP_1SUB'], - ['OP_2 OP_PICK OP_1SUB OP_3 OP_ROLL OP_DROP', 'OP_ROT OP_1SUB'], - ['OP_3 OP_PICK OP_1SUB OP_4 OP_ROLL OP_DROP', 'OP_3 OP_ROLL OP_1SUB'], - ['OP_4 OP_PICK OP_1SUB OP_5 OP_ROLL OP_DROP', 'OP_4 OP_ROLL OP_1SUB'], - ['OP_5 OP_PICK OP_1SUB OP_6 OP_ROLL OP_DROP', 'OP_5 OP_ROLL OP_1SUB'], - ['OP_6 OP_PICK OP_1SUB OP_7 OP_ROLL OP_DROP', 'OP_6 OP_ROLL OP_1SUB'], - ['OP_7 OP_PICK OP_1SUB OP_8 OP_ROLL OP_DROP', 'OP_7 OP_ROLL OP_1SUB'], - ['OP_8 OP_PICK OP_1SUB OP_9 OP_ROLL OP_DROP', 'OP_8 OP_ROLL OP_1SUB'], - ['OP_9 OP_PICK OP_1SUB OP_10 OP_ROLL OP_DROP', 'OP_9 OP_ROLL OP_1SUB'], - ['OP_10 OP_PICK OP_1SUB OP_11 OP_ROLL OP_DROP', 'OP_10 OP_ROLL OP_1SUB'], - ['OP_11 OP_PICK OP_1SUB OP_12 OP_ROLL OP_DROP', 'OP_11 OP_ROLL OP_1SUB'], - ['OP_12 OP_PICK OP_1SUB OP_13 OP_ROLL OP_DROP', 'OP_12 OP_ROLL OP_1SUB'], - ['OP_13 OP_PICK OP_1SUB OP_14 OP_ROLL OP_DROP', 'OP_13 OP_ROLL OP_1SUB'], - ['OP_14 OP_PICK OP_1SUB OP_15 OP_ROLL OP_DROP', 'OP_14 OP_ROLL OP_1SUB'], - ['OP_15 OP_PICK OP_1SUB OP_16 OP_ROLL OP_DROP', 'OP_15 OP_ROLL OP_1SUB'], - ///////////////////////////////////////////////////////////////////////// + // => , for any depth n and any expression, which updates a + // variable in place instead of copying it and dropping the original (e.g. x = x + 1 inside a branch) + // This comes before the deeper drop rules below, which would otherwise match the end of its patterns first + updateInPlace, // Replace drops of items deeper in the stack (e.g. from scope cleanup) with shorter equivalents ['OP_ROT OP_DROP OP_NIP', 'OP_NIP OP_NIP'], ['OP_3 OP_ROLL OP_DROP OP_ROT', 'OP_2SWAP OP_NIP'], ['OP_ROT OP_ROT OP_DROP', 'OP_NIP OP_SWAP'], ['OP_3 OP_ROLL OP_DROP OP_NIP OP_NIP', 'OP_NIP OP_NIP OP_NIP'], -] as [string, string][]; +]; + +// Static [pattern, replacement] pairs become matchers of their exact tokens, so every optimisation applies the same way +export const optimisationMatchers = optimisationReplacements.map((optimisation) => ( + typeof optimisation === 'function' ? optimisation : matchStaticReplacement(optimisation) +)); + +function matchStaticReplacement([pattern, replacement]: [string, string]): OptimisationMatcher { + const patternTokens = tokeniseAsm(pattern); + const replacementTokens = tokeniseAsm(replacement); + + return (tokens, index) => ( + matchesTokens(tokens, index, patternTokens) + ? [{ length: patternTokens.length, replacement: replacementTokens }] + : undefined + ); +} + +// OP_ROT OP_SWAP OP_DROP drops the copy that the pick pushed, and swaps the two items below it +function dropPickedCopy(tokens: string[], index: number): OptimisationMatch | undefined { + const pick = matchPick(tokens, index); + if (!pick || !matchesTokens(tokens, pick.end, ['OP_ROT', 'OP_SWAP', 'OP_DROP'])) return undefined; + + return [{ length: pick.end + 3 - index, replacement: ['OP_SWAP'] }]; +} + +// Moving the OP_DROP before the push lets it combine with the ops that produced the dropped item +function moveDropBeforePush(tokens: string[], index: number): OptimisationMatch | undefined { + if (!isPush(tokens[index]) || tokens[index + 1] !== 'OP_NIP') return undefined; + + return [{ length: 2, replacement: ['OP_DROP', tokens[index]] }]; +} + +// The expression can be anything that turns the copied value into a single new value, including copies of other +// variables (see matchExpressionStep) +function updateInPlace(tokens: string[], index: number): OptimisationMatch | undefined { + const pick = matchPick(tokens, index); + if (!pick) return undefined; + + const drop = dropTokens(pick.depth + 1); + for (const expression of updatedExpressions(tokens, pick.end, pick.depth)) { + if (matchesTokens(tokens, expression.end, drop)) { + return [ + { length: pick.end - index, replacement: rollTokens(pick.depth) }, + ...expression.parts, + { length: drop.length, replacement: [] }, + ]; + } + } + + return undefined; +} + +interface UpdatedExpression { + end: number; + parts: OptimisationPart[]; +} + +const MAX_EXPRESSION_STEPS = 100; + +// Yields every expression from `start` that turns the top stack item into exactly one value, with one part per step +function* updatedExpressions(tokens: string[], start: number, rolledDepth: number): Generator { + const parts: OptimisationPart[] = []; + let height = 1; // the number of stack items that the expression has produced so far + let position = start; + + // The step limit keeps the search linear in long runs of opcodes that could all be part of an expression + while (parts.length <= MAX_EXPRESSION_STEPS) { + if (height === 1) yield { end: position, parts: [...parts] }; + + const step = matchExpressionStep(tokens, position, height, rolledDepth); + if (!step) return; + + parts.push(step.part); + height += step.heightChange; + position += step.part.length; + } +} + +interface ExpressionStep { + part: OptimisationPart; + heightChange: number; +} + +// A step can only consume the items that the expression produced itself, but it can copy any item from below them. +// Those copies are rewritten for when the original variable was rolled from `rolledDepth` instead of copied from it, +// which moves the items below it up by one. A copy of the original variable itself ends the expression, since the +// expression might already have consumed it. +function matchExpressionStep( + tokens: string[], + position: number, + height: number, + rolledDepth: number, +): ExpressionStep | undefined { + const pick = matchPick(tokens, position); + if (pick) { + const depthBelowExpression = pick.depth - height; + if (depthBelowExpression === rolledDepth) return undefined; + + const depth = depthBelowExpression > rolledDepth ? pick.depth - 1 : pick.depth; + return { part: { length: pick.end - position, replacement: pickTokens(depth) }, heightChange: 1 }; + } + + const stackEffect = getStackEffect(tokens[position]); + if (!stackEffect || stackEffect.pops > height) return undefined; + + return { part: { length: 1, replacement: [tokens[position]] }, heightChange: stackEffect.pushes - stackEffect.pops }; +} + +interface Pick { + depth: number; + end: number; +} + +function matchPick(tokens: string[], index: number): Pick | undefined { + if (tokens[index] === 'OP_DUP') return { depth: 0, end: index + 1 }; + if (tokens[index] === 'OP_OVER') return { depth: 1, end: index + 1 }; + + const depth = decodeNumber(tokens[index]); + if (depth === undefined || depth < 0 || tokens[index + 1] !== 'OP_PICK') return undefined; + + return { depth, end: index + 2 }; +} + +// , and are the forms of OP_PICK, OP_ROLL and OP_ROLL OP_DROP that are left after +// the hardcoded stack op rules above, e.g. OP_OVER instead of OP_1 OP_PICK +const pickTokens = (depth: number): string[] => [['OP_DUP'], ['OP_OVER']][depth] ?? [encodeNumber(depth), 'OP_PICK']; +const rollTokens = (depth: number): string[] => [[], ['OP_SWAP'], ['OP_ROT']][depth] ?? [encodeNumber(depth), 'OP_ROLL']; +const dropTokens = (depth: number): string[] => [['OP_DROP'], ['OP_NIP']][depth] ?? [...rollTokens(depth), 'OP_DROP']; + +interface StackEffect { + pops: number; + pushes: number; +} + +function getStackEffect(token: string | undefined): StackEffect | undefined { + if (token === undefined) return undefined; + if (isPush(token)) return { pops: 0, pushes: 1 }; + return PURE_OPCODE_STACK_EFFECTS.find(({ opcodes }) => opcodes.includes(token)); +} + +// Opcodes that replace their `pops` top stack items with `pushes` items that only depend on the popped items and the +// transaction. Any other opcode (e.g. OP_VERIFY, OP_ROLL, OP_TOALTSTACK or OP_IF) ends an expression. +// OP_DUP, OP_OVER and OP_PICK are handled separately, because they can read items below the expression. +const PURE_OPCODE_STACK_EFFECTS: Array = [ + { + pops: 0, + pushes: 1, + opcodes: ['OP_INPUTINDEX', 'OP_ACTIVEBYTECODE', 'OP_TXVERSION', 'OP_TXINPUTCOUNT', 'OP_TXOUTPUTCOUNT', 'OP_TXLOCKTIME'], + }, + { pops: 1, pushes: 0, opcodes: ['OP_DROP'] }, + { + pops: 1, + pushes: 1, + opcodes: [ + 'OP_1ADD', 'OP_1SUB', 'OP_NEGATE', 'OP_ABS', 'OP_NOT', 'OP_0NOTEQUAL', 'OP_INVERT', 'OP_BIN2NUM', 'OP_REVERSEBYTES', + 'OP_RIPEMD160', 'OP_SHA1', 'OP_SHA256', 'OP_HASH160', 'OP_HASH256', 'OP_UTXOVALUE', 'OP_UTXOBYTECODE', + 'OP_OUTPOINTTXHASH', 'OP_OUTPOINTINDEX', 'OP_INPUTBYTECODE', 'OP_INPUTSEQUENCENUMBER', 'OP_OUTPUTVALUE', + 'OP_OUTPUTBYTECODE', 'OP_UTXOTOKENCATEGORY', 'OP_UTXOTOKENCOMMITMENT', 'OP_UTXOTOKENAMOUNT', + 'OP_OUTPUTTOKENCATEGORY', 'OP_OUTPUTTOKENCOMMITMENT', 'OP_OUTPUTTOKENAMOUNT', + ], + }, + { pops: 1, pushes: 2, opcodes: ['OP_SIZE'] }, + { pops: 2, pushes: 0, opcodes: ['OP_2DROP'] }, + { + pops: 2, + pushes: 1, + opcodes: [ + 'OP_NIP', 'OP_CAT', 'OP_NUM2BIN', 'OP_AND', 'OP_OR', 'OP_XOR', 'OP_EQUAL', 'OP_ADD', 'OP_SUB', 'OP_MUL', 'OP_DIV', + 'OP_MOD', 'OP_LSHIFTNUM', 'OP_RSHIFTNUM', 'OP_LSHIFTBIN', 'OP_RSHIFTBIN', 'OP_BOOLAND', 'OP_BOOLOR', 'OP_NUMEQUAL', + 'OP_NUMNOTEQUAL', 'OP_LESSTHAN', 'OP_GREATERTHAN', 'OP_LESSTHANOREQUAL', 'OP_GREATERTHANOREQUAL', 'OP_MIN', + 'OP_MAX', 'OP_CHECKSIG', + ], + }, + { pops: 2, pushes: 2, opcodes: ['OP_SWAP', 'OP_SPLIT'] }, + { pops: 2, pushes: 3, opcodes: ['OP_TUCK'] }, + { pops: 2, pushes: 4, opcodes: ['OP_2DUP'] }, + { pops: 3, pushes: 1, opcodes: ['OP_WITHIN', 'OP_CHECKDATASIG'] }, + { pops: 3, pushes: 3, opcodes: ['OP_ROT'] }, + { pops: 3, pushes: 6, opcodes: ['OP_3DUP'] }, + { pops: 4, pushes: 4, opcodes: ['OP_2SWAP'] }, + { pops: 4, pushes: 6, opcodes: ['OP_2OVER'] }, + { pops: 6, pushes: 6, opcodes: ['OP_2ROT'] }, +]; + +// Data pushes are hex tokens in the ASM, and number pushes are OP_0, OP_1NEGATE and OP_1 to OP_16 +function isPush(token: string | undefined): boolean { + return token !== undefined && (!token.startsWith('OP_') || /^OP_(\d+|1NEGATE)$/.test(token)); +} + +function decodeNumber(token: string | undefined): number | undefined { + if (token === undefined || !isPush(token)) return undefined; + if (token.startsWith('OP_')) return token === 'OP_1NEGATE' ? -1 : Number(token.slice(3)); + + const number = vmNumberToBigInt(hexToBin(token)); + return isVmNumberError(number) ? undefined : Number(number); +} + +function encodeNumber(number: number): string { + return number <= 16 ? `OP_${number}` : binToHex(encodeInt(BigInt(number))); +} + +function matchesTokens(tokens: string[], index: number, expectedTokens: string[]): boolean { + return expectedTokens.every((token, offset) => tokens[index + offset] === token); +} + +function tokeniseAsm(asm: string): string[] { + return asm.match(/\S+/g) ?? []; +} diff --git a/packages/utils/src/script.ts b/packages/utils/src/script.ts index 115baeb8c..2afe94fba 100644 --- a/packages/utils/src/script.ts +++ b/packages/utils/src/script.ts @@ -8,7 +8,7 @@ import { OpcodesBch, AuthenticationInstruction, } from '@bitauth/libauth'; -import { optimisationReplacements } from './optimisations.js'; +import { OptimisationMatch, OptimisationMatcher, OptimisationPart, optimisationMatchers } from './optimisations.js'; import { range } from './data.js'; import { FullLocationData, PositionHint, SingleLocationData, SourceTagEntry, SourceTagKind } from './types.js'; import { InlineRange, LogEntry, RequireStatement } from './artifact.js'; @@ -172,7 +172,7 @@ export function optimiseBytecode( ): OptimiseBytecodeResult { for (let i = 0; i < runs; i += 1) { const result = replaceOps( - script, locationData, logs, requires, sourceTags, inlineRanges, constructorParamLength, optimisationReplacements, + script, locationData, logs, requires, sourceTags, inlineRanges, constructorParamLength, optimisationMatchers, ); // Break on fixed point @@ -181,8 +181,11 @@ export function optimiseBytecode( ({ script, locationData, logs, requires, sourceTags, inlineRanges } = result); } + // A tag whose opcodes were all optimised away leaves no range + const remainingSourceTags = sourceTags.filter((tag) => tag.endIndex >= tag.startIndex); + return { - script, locationData, logs, requires, sourceTags: reconcileScopeCleanupTags(script, sourceTags), inlineRanges, + script, locationData, logs, requires, sourceTags: reconcileScopeCleanupTags(script, remainingSourceTags), inlineRanges, }; } @@ -237,146 +240,190 @@ function replaceOps( sourceTags: SourceTagEntry[], inlineRanges: InlineRange[], constructorParamLength: number, - optimisations: [string, string][], + optimisations: OptimisationMatcher[], ): ReplaceOpsResult { const originalAsm = scriptToAsm(script); - let asm = originalAsm; - const newLocationData = [...locationData]; - let newLogs = [...logs]; - let newRequires = [...requires]; - let newSourceTags = [...sourceTags]; - let newInlineRanges = [...inlineRanges]; - - optimisations.forEach(([pattern, replacement]) => { - const patternTokens = pattern.split(/\s+/); - const patternLength = patternTokens.length; - const replacementLength = replacement === '' ? 0 : replacement.split(/\s+/).length; - const lengthDiff = patternLength - replacementLength; - - // (?(); - asm.split(' ').reduce((characterOffset, token, scriptIndex) => { - scriptIndexAtCharacterOffset.set(characterOffset, scriptIndex); - return characterOffset + token.length + 1; - }, 0); + const state: ReplaceOpsState = { + tokens: originalAsm === '' ? [] : originalAsm.split(' '), + locationData: [...locationData], + logs, + requires, + sourceTags, + inlineRanges, + constructorParamLength, + }; + optimisations.forEach((optimisation) => { // Process the matches right to left: replacing a pattern only shifts the metadata positions // that come after it, so the indices of the remaining (earlier) matches stay valid as-is. - for (const match of matches.reverse()) { - const scriptIndex = scriptIndexAtCharacterOffset.get(match.index)!; - - // We get the locationData entries for every opcode in the pattern - const patternLocations = newLocationData.slice(scriptIndex, scriptIndex + patternLength); - - // We get the lowest start location and highest end location of the pattern - const lowestStart = getLowestStartLocation(patternLocations); - const highestEnd = getHighestEndLocation(patternLocations); - - // Initially we set the position hint to END if any of the pattern locations have a position hint of END - // It turned out that this was not the correct approach in the case of OP_NOT OP_IF => OP_NOTIF, - // because OP_IF and OP_NOTIF are START opcodes, and OP_NOT is an END opcode. - // After reviewing the entire list of optimisations, we set the position hint to the last location's position hint - // which we believe to be the correct approach, but it is hard to reason about. - // We've also consulted with AI (o3-max) to help us reason about this, and it seems to be the correct approach. - const positionHint = patternLocations.at(-1)?.positionHint ?? PositionHint.START; - - // We merge the lowest start and highest end locations into a single location data entry - const mergedLocation = { - location: { - start: lowestStart.location.start, - end: highestEnd.location.end, - }, - positionHint, - }; + findMatches(state.tokens, optimisation).reverse().forEach((match) => replaceMatch(state, match)); + }); - // We replace the pattern locations with the merged location - // (note that every opcode in the replacement has the same location) - const replacementLocations = new Array(replacementLength).fill(mergedLocation); - newLocationData.splice(scriptIndex, patternLength, ...replacementLocations); - - // The IP of an opcode in the script is its index within the script + the constructor parameters, because - // the constructor parameters still have to get added to the front of the script when a new Contract is created. - const scriptIp = scriptIndex + constructorParamLength; - - // Positions after the replaced pattern shift back by the length difference; positions inside - // the replaced pattern clamp to the pattern's start. (Positions inside a pattern are impossible - // for the current set of optimisations, but the clamp future-proofs the code.) - const adjustPosition = (position: number, patternStart: number): number => ( - position >= patternStart ? Math.max(patternStart, position - lengthDiff) : position - ); - - newRequires = newRequires.map((require) => ({ - ...require, - ip: adjustPosition(require.ip, scriptIp), - })); - - newLogs = newLogs.map((log) => { - return { - ip: adjustPosition(log.ip, scriptIp), - line: log.line, - data: log.data.map((data) => { - if (typeof data === 'string') return data; - - // If the log is completely before the pattern, we don't need to change anything - if (data.ip <= scriptIp) return data; - - // If the log is completely after the pattern, we just need to offset the ip by the length diff - if (data.ip >= scriptIp + patternLength) { - const newCalculatedDataIp = data.ip - lengthDiff; - return { ...data, ip: newCalculatedDataIp }; - } - - const addedTransformationsCount = data.ip - scriptIp; - const addedTransformations = patternTokens.slice(0, addedTransformationsCount).join(' '); - const newTransformations = data.transformations ? `${addedTransformations} ${data.transformations}` : addedTransformations; - - return { - ...data, - ip: scriptIp, - transformations: newTransformations, - }; - }), - }; - }); - - // Source tags use raw script indices (no constructor offset), so they adjust against scriptIndex - newSourceTags = newSourceTags.map((tag) => ({ - ...tag, - startIndex: adjustPosition(tag.startIndex, scriptIndex), - endIndex: adjustPosition(tag.endIndex, scriptIndex), - })); - - // Inline ranges use ip coordinates (like requires), so both bounds adjust against scriptIp - newInlineRanges = newInlineRanges.map((inlineRange) => ({ - ...inlineRange, - startIp: adjustPosition(inlineRange.startIp, scriptIp), - endIp: adjustPosition(inlineRange.endIp, scriptIp), - })); + const asm = state.tokens.join(' '); + return { + script: asmToScript(asm), + changed: asm !== originalAsm, + locationData: state.locationData, + logs: state.logs, + requires: state.requires, + sourceTags: state.sourceTags, + inlineRanges: state.inlineRanges, + }; +} + +// The script (as ASM tokens) and metadata that replaceOps updates with every replaced match +interface ReplaceOpsState extends Omit { + tokens: string[]; + constructorParamLength: number; +} + +interface IndexedOptimisationMatch { + index: number; + parts: OptimisationMatch; +} + +// Scan left to right, and continue after each match so that the matches of one optimisation never overlap +function findMatches(tokens: string[], optimisation: OptimisationMatcher): IndexedOptimisationMatch[] { + const matches = []; + + for (let index = 0; index < tokens.length;) { + const parts = optimisation(tokens, index); + + if (parts) { + matches.push({ index, parts }); + index += sumLengths(parts.map((part) => part.length)); + } else { + index += 1; } + } + + return matches; +} + +function replaceMatch(state: ReplaceOpsState, { index, parts }: IndexedOptimisationMatch): void { + const patternLength = sumLengths(parts.map((part) => part.length)); + const replacementLength = sumLengths(parts.map((part) => part.replacement.length)); + const patternTokens = state.tokens.slice(index, index + patternLength); + + // The IP of an opcode in the script is its index within the script + the constructor parameters, because + // the constructor parameters still have to get added to the front of the script when a new Contract is created. + const scriptIp = index + state.constructorParamLength; - asm = asm.replace(regex, replacement).replace(/\s+/g, ' ').trim(); + // Log data reads the stack, which every part of the match can rearrange, so it adjusts against the whole match + state.logs = state.logs.map((log) => ({ + ...log, + data: log.data.map((data) => { + if (typeof data === 'string') return data; + + // If the log is completely before the pattern, we don't need to change anything + if (data.ip <= scriptIp) return data; + + // If the log is completely after the pattern, we just need to offset the ip by the length diff + if (data.ip >= scriptIp + patternLength) { + const newCalculatedDataIp = data.ip - (patternLength - replacementLength); + return { ...data, ip: newCalculatedDataIp }; + } + + const addedTransformationsCount = data.ip - scriptIp; + const addedTransformations = patternTokens.slice(0, addedTransformationsCount).join(' '); + const newTransformations = data.transformations ? `${addedTransformations} ${data.transformations}` : addedTransformations; + + return { + ...data, + ip: scriptIp, + transformations: newTransformations, + }; + }), + })); + + // Replace the parts right to left as well, so that each part only shifts the positions of the parts after it + let partIndex = index + patternLength; + [...parts].reverse().forEach((part) => { + partIndex -= part.length; + replacePart(state, partIndex, part); }); +} - return { - script: asmToScript(asm), - changed: asm !== originalAsm, - locationData: newLocationData, - logs: newLogs, - requires: newRequires, - sourceTags: newSourceTags, - inlineRanges: newInlineRanges, +function replacePart( + state: ReplaceOpsState, + scriptIndex: number, + { length: patternLength, replacement }: OptimisationPart, +): void { + const replacementLength = replacement.length; + const lengthDiff = patternLength - replacementLength; + + state.tokens.splice(scriptIndex, patternLength, ...replacement); + + // We get the locationData entries for every opcode in the pattern + const patternLocations = state.locationData.slice(scriptIndex, scriptIndex + patternLength); + + // We get the lowest start location and highest end location of the pattern + const lowestStart = getLowestStartLocation(patternLocations); + const highestEnd = getHighestEndLocation(patternLocations); + + // Initially we set the position hint to END if any of the pattern locations have a position hint of END + // It turned out that this was not the correct approach in the case of OP_NOT OP_IF => OP_NOTIF, + // because OP_IF and OP_NOTIF are START opcodes, and OP_NOT is an END opcode. + // After reviewing the entire list of optimisations, we set the position hint to the last location's position hint + // which we believe to be the correct approach, but it is hard to reason about. + // We've also consulted with AI (o3-max) to help us reason about this, and it seems to be the correct approach. + const positionHint = patternLocations.at(-1)?.positionHint ?? PositionHint.START; + + // We merge the lowest start and highest end locations into a single location data entry + const mergedLocation = { + location: { + start: lowestStart.location.start, + end: highestEnd.location.end, + }, + positionHint, }; + + // We replace the pattern locations with the merged location + // (note that every opcode in the part's replacement has the same location) + const replacementLocations = new Array(replacementLength).fill(mergedLocation); + state.locationData.splice(scriptIndex, patternLength, ...replacementLocations); + + const scriptIp = scriptIndex + state.constructorParamLength; + + // Positions after the replaced pattern shift back by the length difference; positions inside + // the replaced pattern clamp to the pattern's start. + const adjustPosition = (position: number, patternStart: number): number => ( + position >= patternStart ? Math.max(patternStart, position - lengthDiff) : position + ); + + // The (inclusive) end of a range inside a pattern that is removed entirely moves to the opcode before the pattern + const adjustEndPosition = (position: number, patternStart: number): number => ( + replacementLength === 0 ? adjustPosition(position + 1, patternStart) - 1 : adjustPosition(position, patternStart) + ); + + state.requires = state.requires.map((require) => ({ + ...require, + ip: adjustPosition(require.ip, scriptIp), + })); + + state.logs = state.logs.map((log) => ({ + ...log, + ip: adjustPosition(log.ip, scriptIp), + })); + + // Source tags use raw script indices (no constructor offset), so they adjust against scriptIndex + state.sourceTags = state.sourceTags.map((tag) => ({ + ...tag, + startIndex: adjustPosition(tag.startIndex, scriptIndex), + endIndex: adjustEndPosition(tag.endIndex, scriptIndex), + })); + + // Inline ranges use ip coordinates (like requires), so both bounds adjust against scriptIp + state.inlineRanges = state.inlineRanges.map((inlineRange) => ({ + ...inlineRange, + startIp: adjustPosition(inlineRange.startIp, scriptIp), + endIp: adjustEndPosition(inlineRange.endIp, scriptIp), + })); } +const sumLengths = (lengths: number[]): number => lengths.reduce((total, length) => total + length, 0); + const getHighestEndLocation = (locations: SingleLocationData[]): SingleLocationData => { return locations.reduce((highest, current) => { if (current.location.end.line > highest.location.end.line) { diff --git a/packages/utils/test/bitauth-script.test.ts b/packages/utils/test/bitauth-script.test.ts index dcd8c2574..c1e4dcddb 100644 --- a/packages/utils/test/bitauth-script.test.ts +++ b/packages/utils/test/bitauth-script.test.ts @@ -52,12 +52,12 @@ describe('Libauth Script formatting', () => { }); 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 + // Two scope cleanups sharing the OP_2DROP (28:28 and 28:29) 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', + sourceTags: '23:23:fu;24:27:lc;28:28:sc;28:29:sc;34:34:sc', logs: [], requires: [], }; diff --git a/packages/utils/test/fixtures/bitauth-script.fixture.ts b/packages/utils/test/fixtures/bitauth-script.fixture.ts index 5b0bf3d2b..c2eeb4cb2 100644 --- a/packages/utils/test/fixtures/bitauth-script.fixture.ts +++ b/packages/utils/test/fixtures/bitauth-script.fixture.ts @@ -232,7 +232,7 @@ contract ForWhileNested() { } `.replace(/^\n+/, '').replace(/\n+$/, ''), asmBytecode: 'OP_0 OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_3DUP OP_DROP OP_ADD OP_OVER OP_ADD OP_2SWAP OP_NIP OP_ROT OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_SWAP OP_1ADD OP_NIP OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_4 OP_NUMEQUAL', - sourceMap: '3:18:3:19;5:21:5:22;:8:13:9;:24:5:25;:28::29;:24:::1;;;:42:13:9:0;6:20:6:21;8:12:12:13;:19:8:20;:23::24;:19:::1;;;:26:12:13:0;9:22:9:29;;::::1;:32::33:0;:22:::1;:16::34;;;10::10:26;8:26:12:13;;:12;;5:31:5:40;;::13:9;:42;;:8;;;15:23:15:24:0;:8::26:1', + sourceMap: '3:18:3:19;5:21:5:22;:8:13:9;:24:5:25;:28::29;:24:::1;;;:42:13:9:0;6:20:6:21;8:12:12:13;:19:8:20;:23::24;:19:::1;;;:26:12:13:0;9:22:9:29;;::::1;:32::33:0;:22:::1;:16::34;;;10:20:10:25;8:26:12:13;;:12;;5:35:5:36:0;:::40:1;:31:13:9;:42;;:8;;;15:23:15:24:0;:8::26:1', sourceTags: '26:29:lc;30:32:fu;33:36:lc;37:37:sc', expectedBitAuthScript: ` /* contract ForWhileNested() { */ @@ -248,7 +248,7 @@ OP_1ADD /* /* console.log("sum:", sum, "i:", i, "j:", j); */ OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL /* >>> loop condition check */ /* } */ -OP_SWAP OP_1ADD OP_NIP /* >>> for-loop update (i = i + 1) */ +OP_SWAP OP_1ADD OP_NIP /* >>> for-loop update (i + 1) */ OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL /* >>> loop condition check */ OP_DROP /* >>> scope cleanup */ /* } */ @@ -280,7 +280,7 @@ OP_4 OP_NUMEQUAL /* requ } }`, asmBytecode: 'OP_0 OP_0 OP_BEGIN OP_DUP OP_3 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF OP_3DUP OP_DROP OP_ADD OP_OVER OP_ADD OP_2SWAP OP_NIP OP_ROT OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_1ADD OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL OP_DROP OP_0 OP_BEGIN OP_1ADD OP_DUP OP_3 OP_GREATERTHANOREQUAL OP_UNTIL OP_ADD OP_12 OP_NUMEQUAL', - sourceMap: '3:20:3:21;5:21:5:22;:8:10:9;:24:5:25;:28::29;:24:::1;;;6:19:10:9:0;7:25:7:26;:12:9:13;:28:7:29;:32::33;:28:::1;;;:46:9:13:0;8:24:8:33;;::::1;:36::37:0;:24:::1;:16::38;;;7:35:7:44;:46:9:13;;:12;;;5:31:6:17;6:19:10:9;;5:8;;;12:20:12:21:0;13:8:15:28;14:12:14:30:1;15:17:15:22:0;:25::26;13:8::28:1;;17:16:17:29;:33::35:0;:8::37:1', + sourceMap: '3:20:3:21;5:21:5:22;:8:10:9;:24:5:25;:28::29;:24:::1;;;6:19:10:9:0;7:25:7:26;:12:9:13;:28:7:29;:32::33;:28:::1;;;:46:9:13:0;8:24:8:33;;::::1;:36::37:0;:24:::1;:16::38;;;7:39:7:44;:46:9:13;;:12;;;6::6:17;:19:10:9;;5:8;;;12:20:12:21:0;13:8:15:28;14:20:14:29:1;15:17:15:22:0;:25::26;13:8::28:1;;17:16:17:29;:33::35:0;:8::37:1', sourceTags: '25:25:fu;26:29:lc;30:30:sc;31:31:fu;32:35:lc;36:36:sc', expectedBitAuthScript: ` /* contract NestedForWithDoWhile() { */ @@ -291,11 +291,11 @@ OP_0 OP_BEGIN OP_DUP OP_3 OP_LESSTHAN OP_DUP OP_TOALTSTACK /* for OP_IF /* i + 1) { */ OP_0 OP_BEGIN OP_DUP OP_2 OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF /* for (int j = 0; j < 2; j = j + 1) { */ OP_3DUP OP_DROP OP_ADD OP_OVER OP_ADD OP_2SWAP OP_NIP OP_ROT /* total = total + i + j; */ -OP_1ADD /* >>> for-loop update (j = j + 1) */ +OP_1ADD /* >>> for-loop update (j + 1) */ OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL /* >>> loop condition check */ OP_DROP /* >>> scope cleanup */ /* } */ -OP_1ADD /* >>> for-loop update () */ +OP_1ADD /* >>> for-loop update (i + 1) */ OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL /* >>> loop condition check */ OP_DROP /* >>> scope cleanup */ /* } */ @@ -419,26 +419,26 @@ OP_0 OP_GREATERTHAN /* require(x > 0); */ 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', + 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_3DUP OP_DROP 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:29;;::::1;:32::33:0;:24:::1;:16::35;6:39: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: '23:23:fu;24:27:lc;28:29:sc;34:34: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 */ - /* } */ - /* } */ + /* 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_3DUP OP_DROP OP_ADD OP_OVER OP_GREATERTHAN OP_VERIFY /* require(x + y > i); */ +OP_1ADD /* >>> for-loop update (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+$/, ''), }, ]; @@ -497,7 +497,7 @@ contract LocalFunctions() { OP_0 /* int sum = 0; */ OP_0 OP_BEGIN OP_DUP OP_3 OP_PICK OP_LESSTHAN OP_DUP OP_TOALTSTACK OP_IF /* for (int i = 0; i < n; i = i + 1) { */ OP_2DUP OP_ADD OP_ROT OP_DROP OP_SWAP /* sum = sum + i; */ - OP_1ADD /* >>> for-loop update (i = i + 1) */ + OP_1ADD /* >>> for-loop update (i + 1) */ OP_ENDIF OP_FROMALTSTACK OP_NOT OP_UNTIL /* >>> loop condition check */ OP_DROP /* >>> scope cleanup */ /* } */ diff --git a/packages/utils/test/optimisations.test.ts b/packages/utils/test/optimisations.test.ts new file mode 100644 index 000000000..52c585428 --- /dev/null +++ b/packages/utils/test/optimisations.test.ts @@ -0,0 +1,126 @@ +import { + bigIntToVmNumber, + binToHex, + createTestAuthenticationProgramBch, + createVirtualMachineBch2026, + encodeDataPush, + flattenBinArray, +} from '@bitauth/libauth'; +import { + asmToBytecode, + asmToScript, + optimiseBytecode, + OptimiseBytecodeResult, + PositionHint, + range, + RequireStatement, + scriptToAsm, + SingleLocationData, + SourceTagEntry, + SourceTagKind, +} from '../src/index.js'; + +describe('optimiseBytecode()', () => { + describe('optimisations for any stack depth or expression', () => { + const fixtures = [ + { name: 'updates the top item in place', asm: 'OP_DUP OP_2 OP_ADD OP_NIP', expected: 'OP_2 OP_ADD' }, + { name: 'updates a deeper item in place', asm: 'OP_OVER OP_1ADD OP_ROT OP_DROP', expected: 'OP_SWAP OP_1ADD' }, + { + name: 'updates an item deeper than OP_16 in place', + asm: '11 OP_PICK OP_1SUB 12 OP_ROLL OP_DROP', + expected: '11 OP_ROLL OP_1SUB', + }, + { + name: 'updates an item in place with a multi-opcode expression', + asm: 'OP_3 OP_PICK OP_SHA256 OP_SIZE OP_NIP OP_4 OP_ROLL OP_DROP', + expected: 'OP_3 OP_ROLL OP_SHA256 OP_SIZE OP_NIP', + }, + { + name: 'keeps copies of items above the updated item as they are', + asm: 'OP_3 OP_PICK OP_2 OP_PICK OP_ADD OP_4 OP_ROLL OP_DROP', + expected: 'OP_3 OP_ROLL OP_2 OP_PICK OP_ADD', + }, + { + name: 'rewrites copies of items below the updated item', + asm: 'OP_3 OP_PICK OP_6 OP_PICK OP_ADD OP_4 OP_ROLL OP_DROP', + expected: 'OP_3 OP_ROLL OP_5 OP_PICK OP_ADD', + }, + { + name: 'does not update an item in place when the expression copies the original item', + asm: 'OP_3 OP_PICK OP_4 OP_PICK OP_MUL OP_4 OP_ROLL OP_DROP', + expected: 'OP_3 OP_PICK OP_4 OP_PICK OP_MUL OP_4 OP_ROLL OP_DROP', + }, + { + name: 'does not update an item in place when the expression reads more than its own items', + asm: 'OP_3 OP_PICK OP_DEPTH OP_ADD OP_4 OP_ROLL OP_DROP', + expected: 'OP_3 OP_PICK OP_DEPTH OP_ADD OP_4 OP_ROLL OP_DROP', + }, + { name: 'moves an OP_NIP after a data push', asm: 'abcd OP_NIP', expected: 'OP_DROP abcd' }, + { name: 'moves an OP_NIP after a number push', asm: 'OP_CAT OP_1NEGATE OP_NIP', expected: 'OP_2DROP OP_1NEGATE' }, + ]; + + fixtures.forEach(({ name, asm, expected }) => { + it(`${name} (${asm})`, () => { + const optimised = scriptToAsm(optimiseAsm(asm).script); + expect(optimised).toEqual(expected); + expect(evaluate(optimised)).toEqual(evaluate(asm)); + }); + }); + }); + + describe('debug information of an in-place update', () => { + // x -= 3; require(x == 12); followed by the cleanup of the original x + const asm = 'OP_DUP OP_3 OP_SUB OP_12 OP_NUMEQUAL OP_NIP'; + const optimise = (sourceTags: SourceTagEntry[] = []): OptimiseBytecodeResult => ( + optimiseAsm(asm, [{ ip: 5, line: 1 }], sourceTags) + ); + + it('keeps the source locations of the opcodes it keeps', () => { + const result = optimise(); + expect(scriptToAsm(result.script)).toEqual('OP_3 OP_SUB OP_12 OP_NUMEQUAL'); + expect(result.locationData).toEqual(locations(asm).slice(1, 5)); + }); + + it('keeps a final require directly after its condition', () => { + expect(optimise().requires).toEqual([{ ip: 4, line: 1 }]); + }); + + it('ends a source tag at the last opcode it keeps', () => { + const tag = { startIndex: 3, endIndex: 5, kind: SourceTagKind.LOOP_CONDITION }; + expect(optimise([tag]).sourceTags).toEqual([{ ...tag, startIndex: 2, endIndex: 3 }]); + }); + + it('removes a source tag whose opcodes were all removed', () => { + const tag = { startIndex: 5, endIndex: 5, kind: SourceTagKind.LOOP_CONDITION }; + expect(optimise([tag]).sourceTags).toEqual([]); + }); + }); +}); + +// Give every opcode its own source location, so that merged locations are distinguishable +function locations(asm: string): SingleLocationData[] { + return asmToScript(asm).map((_, index) => ({ + location: { start: { line: 1, column: index }, end: { line: 1, column: index + 1 } }, + positionHint: PositionHint.START, + })); +} + +function optimiseAsm( + asm: string, + requires: RequireStatement[] = [], + sourceTags: SourceTagEntry[] = [], +): OptimiseBytecodeResult { + return optimiseBytecode(asmToScript(asm), locations(asm), [], requires, sourceTags, [], 0); +} + +const vm = createVirtualMachineBch2026(false); + +// Evaluate the script on a stack of distinct numbers, so reading the wrong stack item changes the result +function evaluate(asm: string): object { + const unlockingBytecode = flattenBinArray(range(1, 20).map((n) => encodeDataPush(bigIntToVmNumber(BigInt(n))))); + const program = createTestAuthenticationProgramBch({ + lockingBytecode: asmToBytecode(asm), unlockingBytecode, valueSatoshis: 0n, + }); + const { stack, alternateStack, error } = vm.evaluate(program); + return { stack: stack.map(binToHex), alternateStack: alternateStack.map(binToHex), error }; +}