@cashscript/utils 0.11.0-next.2 → 0.11.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- import { PathLike } from 'fs';
2
1
  export interface AbiInput {
3
2
  name: string;
4
3
  type: string;
@@ -22,6 +21,7 @@ export interface StackItem {
22
21
  type: string;
23
22
  stackIndex: number;
24
23
  ip: number;
24
+ transformations?: string;
25
25
  }
26
26
  export type LogData = StackItem | string;
27
27
  export interface RequireStatement {
@@ -42,6 +42,4 @@ export interface Artifact {
42
42
  };
43
43
  updatedAt: string;
44
44
  }
45
- export declare function importArtifact(artifactFile: PathLike): Artifact;
46
- export declare function exportArtifact(artifact: Artifact, targetFile: string, format: 'json' | 'ts'): void;
47
45
  export declare function formatArtifact(artifact: Artifact, format: 'json' | 'ts'): string;
package/dist/artifact.js CHANGED
@@ -1,11 +1,3 @@
1
- import fs from 'fs';
2
- export function importArtifact(artifactFile) {
3
- return JSON.parse(fs.readFileSync(artifactFile, { encoding: 'utf-8' }));
4
- }
5
- export function exportArtifact(artifact, targetFile, format) {
6
- const jsonString = formatArtifact(artifact, format);
7
- fs.writeFileSync(targetFile, jsonString);
8
- }
9
1
  export function formatArtifact(artifact, format) {
10
2
  if (format === 'ts') {
11
3
  // We remove any undefined values to make the artifact serializable using stringifyAsTs
@@ -0,0 +1 @@
1
+ export declare const optimisationReplacements: [string, string][];
@@ -0,0 +1,124 @@
1
+ const provableOptimisations = [
2
+ // Hardcoded arithmetic
3
+ ['OP_1 OP_ADD', 'OP_1ADD'],
4
+ ['OP_1 OP_SUB', 'OP_1SUB'],
5
+ ['OP_1 OP_NEGATE', 'OP_1NEGATE'],
6
+ ['OP_0 OP_NUMEQUAL OP_NOT', 'OP_0NOTEQUAL'],
7
+ ['OP_NUMEQUAL OP_NOT', 'OP_NUMNOTEQUAL'],
8
+ ['OP_SHA256 OP_SHA256', 'OP_HASH256'],
9
+ ['OP_SHA256 OP_RIPEMD160', 'OP_HASH160'],
10
+ // Hardcoded stack ops
11
+ ['OP_2 OP_PICK OP_1 OP_PICK OP_3 OP_PICK', 'OP_3DUP OP_SWAP'],
12
+ ['OP_2 OP_PICK OP_2 OP_PICK OP_2 OP_PICK', 'OP_3DUP'],
13
+ ['OP_0 OP_PICK OP_2 OP_PICK', 'OP_2DUP OP_SWAP'],
14
+ ['OP_2 OP_PICK OP_4 OP_PICK', 'OP_2OVER OP_SWAP'],
15
+ ['OP_3 OP_PICK OP_3 OP_PICK', 'OP_2OVER'],
16
+ ['OP_2 OP_ROLL OP_3 OP_ROLL', 'OP_2SWAP OP_SWAP'],
17
+ ['OP_3 OP_ROLL OP_3 OP_ROLL', 'OP_2SWAP'],
18
+ ['OP_4 OP_ROLL OP_5 OP_ROLL', 'OP_2ROT OP_SWAP'],
19
+ ['OP_5 OP_ROLL OP_5 OP_ROLL', 'OP_2ROT'],
20
+ ['OP_0 OP_PICK', 'OP_DUP'],
21
+ ['OP_1 OP_PICK', 'OP_OVER'],
22
+ ['OP_0 OP_ROLL', ''],
23
+ ['OP_1 OP_ROLL', 'OP_SWAP'],
24
+ ['OP_2 OP_ROLL', 'OP_ROT'],
25
+ ['OP_DROP OP_DROP', 'OP_2DROP'],
26
+ // Secondary effects
27
+ ['OP_DUP OP_SWAP', 'OP_DUP'],
28
+ ['OP_SWAP OP_SWAP', ''],
29
+ ['OP_2SWAP OP_2SWAP', ''],
30
+ ['OP_ROT OP_ROT OP_ROT', ''],
31
+ ['OP_2ROT OP_2ROT OP_2ROT', ''],
32
+ ['OP_OVER OP_OVER', 'OP_2DUP'],
33
+ ['OP_DUP OP_DROP', ''],
34
+ ['OP_DUP OP_NIP', ''],
35
+ // Enabling secondary effects
36
+ ['OP_DUP OP_OVER', 'OP_DUP OP_DUP'],
37
+ // Merge OP_VERIFY
38
+ ['OP_EQUAL OP_VERIFY', 'OP_EQUALVERIFY'],
39
+ ['OP_NUMEQUAL OP_VERIFY', 'OP_NUMEQUALVERIFY'],
40
+ ['OP_CHECKSIG OP_VERIFY', 'OP_CHECKSIGVERIFY'],
41
+ ['OP_CHECKDATASIG OP_VERIFY', 'OP_CHECKDATASIGVERIFY'],
42
+ // Remove/replace extraneous OP_SWAP
43
+ ['OP_SWAP OP_ADD', 'OP_ADD'],
44
+ // This was added to keep the old behaviour while explicitly disallowing partial matches in the optimisation regex
45
+ ['OP_SWAP OP_EQUALVERIFY', 'OP_EQUALVERIFY'],
46
+ ['OP_SWAP OP_EQUAL', 'OP_EQUAL'],
47
+ // This was added to keep the old behaviour while explicitly disallowing partial matches in the optimisation regex
48
+ ['OP_SWAP OP_NUMEQUALVERIFY', 'OP_NUMEQUALVERIFY'],
49
+ ['OP_SWAP OP_NUMEQUAL', 'OP_NUMEQUAL'],
50
+ ['OP_SWAP OP_NUMNOTEQUAL', 'OP_NUMNOTEQUAL'],
51
+ ['OP_SWAP OP_GREATERTHANOREQUAL', 'OP_LESSTHANOREQUAL'],
52
+ ['OP_SWAP OP_LESSTHANOREQUAL', 'OP_GREATERTHANOREQUAL'],
53
+ ['OP_SWAP OP_GREATERTHAN', 'OP_LESSTHAN'],
54
+ ['OP_SWAP OP_LESSTHAN', 'OP_GREATERTHAN'],
55
+ ['OP_SWAP OP_DROP', 'OP_NIP'],
56
+ ['OP_SWAP OP_NIP', 'OP_DROP'],
57
+ // Remove/replace extraneous OP_DUP
58
+ ['OP_DUP OP_DROP', ''],
59
+ ['OP_DUP OP_NIP', ''],
60
+ // Random optimisations (don't know what I'm targeting with this)
61
+ ['OP_2DUP OP_DROP', 'OP_OVER'],
62
+ ['OP_2DUP OP_NIP', 'OP_DUP'],
63
+ ['OP_CAT OP_DROP', 'OP_2DROP'],
64
+ ['OP_NIP OP_DROP', 'OP_2DROP'],
65
+ // Far-fetched stuff
66
+ ['OP_DUP OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
67
+ ['OP_OVER OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
68
+ ['OP_2 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
69
+ ['OP_3 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
70
+ ['OP_4 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
71
+ ['OP_5 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
72
+ ['OP_6 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
73
+ ['OP_7 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
74
+ ['OP_8 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
75
+ ['OP_9 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
76
+ ['OP_10 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
77
+ ['OP_11 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
78
+ ['OP_12 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
79
+ ['OP_13 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
80
+ ['OP_14 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
81
+ ['OP_16 OP_PICK OP_ROT OP_SWAP OP_DROP', 'OP_SWAP'],
82
+ ['OP_DUP OP_ROT OP_DROP', 'OP_NIP OP_DUP'],
83
+ ['OP_OVER OP_ROT OP_DROP', 'OP_SWAP'],
84
+ ['OP_2 OP_PICK OP_ROT OP_DROP', 'OP_NIP OP_OVER'],
85
+ ['OP_0 OP_NIP', 'OP_DROP OP_0'],
86
+ ['OP_1 OP_NIP', 'OP_DROP OP_1'],
87
+ ['OP_2 OP_NIP', 'OP_DROP OP_2'],
88
+ ['OP_3 OP_NIP', 'OP_DROP OP_3'],
89
+ ['OP_4 OP_NIP', 'OP_DROP OP_4'],
90
+ ['OP_5 OP_NIP', 'OP_DROP OP_5'],
91
+ ['OP_6 OP_NIP', 'OP_DROP OP_6'],
92
+ ['OP_7 OP_NIP', 'OP_DROP OP_7'],
93
+ ['OP_8 OP_NIP', 'OP_DROP OP_8'],
94
+ ['OP_9 OP_NIP', 'OP_DROP OP_9'],
95
+ ['OP_10 OP_NIP', 'OP_DROP OP_10'],
96
+ ['OP_11 OP_NIP', 'OP_DROP OP_11'],
97
+ ['OP_12 OP_NIP', 'OP_DROP OP_12'],
98
+ ['OP_13 OP_NIP', 'OP_DROP OP_13'],
99
+ ['OP_14 OP_NIP', 'OP_DROP OP_14'],
100
+ ['OP_15 OP_NIP', 'OP_DROP OP_15'],
101
+ ['OP_16 OP_NIP', 'OP_DROP OP_16'],
102
+ ['OP_2 OP_PICK OP_SWAP OP_2 OP_PICK OP_NIP', 'OP_DROP OP_2DUP'],
103
+ ];
104
+ const unprovableOptimisations = [
105
+ // Hardcoded arithmetic
106
+ // CashProof can't prove OP_IF without parameters
107
+ ['OP_NOT OP_IF', 'OP_NOTIF'],
108
+ // Merge OP_VERIFY
109
+ // CashProof can't prove OP_CHECKMULTISIG without specifying N
110
+ ['OP_CHECKMULTISIG OP_VERIFY', 'OP_CHECKMULTISIGVERIFY'],
111
+ // Remove/replace extraneous OP_SWAP
112
+ // CashProof can't prove bitwise operators
113
+ ['OP_SWAP OP_AND', 'OP_AND'],
114
+ ['OP_SWAP OP_OR', 'OP_OR'],
115
+ ['OP_SWAP OP_XOR', 'OP_XOR'],
116
+ // Remove/replace extraneous OP_DUP
117
+ // CashProof can't prove bitwise operators
118
+ ['OP_DUP OP_AND', ''],
119
+ ['OP_DUP OP_OR', ''],
120
+ ];
121
+ // Note: we moved these optimisations into a single file, but kept the exact same order as before,
122
+ // because the order in which oprimisations are applied can impact the output.
123
+ export const optimisationReplacements = [...provableOptimisations, ...unprovableOptimisations];
124
+ //# sourceMappingURL=optimisations.js.map
package/dist/script.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { OpcodesBch2023 } from '@bitauth/libauth';
2
+ import { FullLocationData } from './types.js';
3
+ import { LogEntry, RequireStatement } from './artifact.js';
2
4
  export declare const Op: typeof OpcodesBch2023;
3
5
  export type Op = number;
4
6
  export type OpOrData = Op | Uint8Array;
@@ -16,4 +18,12 @@ export declare function countOpcodes(script: Script): number;
16
18
  export declare function calculateBytesize(script: Script): number;
17
19
  export declare function encodeNullDataScript(chunks: OpOrData[]): Uint8Array;
18
20
  export declare function generateRedeemScript(baseScript: Script, encodedConstructorArgs: Script): Script;
19
- export declare function optimiseBytecode(script: Script, runs?: number): Script;
21
+ interface OptimiseBytecodeResult {
22
+ script: Script;
23
+ locationData: FullLocationData;
24
+ logs: LogEntry[];
25
+ requires: RequireStatement[];
26
+ }
27
+ export declare function optimiseBytecode(script: Script, locationData: FullLocationData, logs: LogEntry[], requires: RequireStatement[], constructorParamLength: number, runs?: number): OptimiseBytecodeResult;
28
+ export declare function optimiseBytecodeOld(script: Script, runs?: number): Script;
29
+ export {};
package/dist/script.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { OpcodesBch2023, encodeDataPush, hexToBin, disassembleBytecodeBch, flattenBinArray, encodeAuthenticationInstructions, decodeAuthenticationInstructions, } from '@bitauth/libauth';
2
2
  import OptimisationsEquivFile from './cashproof-optimisations.js';
3
+ import { optimisationReplacements } from './optimisations.js';
4
+ import { PositionHint } from './types.js';
3
5
  export const Op = OpcodesBch2023;
4
6
  export function scriptToAsm(script) {
5
7
  return bytecodeToAsm(scriptToBytecode(script));
@@ -101,7 +103,21 @@ function getPushDataOpcode(data) {
101
103
  export function generateRedeemScript(baseScript, encodedConstructorArgs) {
102
104
  return [...encodedConstructorArgs.slice().reverse(), ...baseScript];
103
105
  }
104
- export function optimiseBytecode(script, runs = 1000) {
106
+ export function optimiseBytecode(script, locationData, logs, requires, constructorParamLength, runs = 1000) {
107
+ for (let i = 0; i < runs; i += 1) {
108
+ const oldScript = script;
109
+ const { script: newScript, locationData: newLocationData, logs: newLogs, requires: newRequires, } = replaceOps(script, locationData, logs, requires, constructorParamLength, optimisationReplacements);
110
+ // Break on fixed point
111
+ if (scriptToAsm(oldScript) === scriptToAsm(newScript))
112
+ break;
113
+ script = newScript;
114
+ locationData = newLocationData;
115
+ logs = newLogs;
116
+ requires = newRequires;
117
+ }
118
+ return { script, locationData, logs, requires };
119
+ }
120
+ export function optimiseBytecodeOld(script, runs = 1000) {
105
121
  const optimisations = OptimisationsEquivFile
106
122
  // Split by line and filter all line comments (#)
107
123
  .split('\n')
@@ -116,14 +132,14 @@ export function optimiseBytecode(script, runs = 1000) {
116
132
  .filter((equiv) => equiv.length === 2);
117
133
  for (let i = 0; i < runs; i += 1) {
118
134
  const oldScript = script;
119
- script = replaceOps(script, optimisations);
135
+ script = replaceOpsOld(script, optimisations);
120
136
  // Break on fixed point
121
137
  if (scriptToAsm(oldScript) === scriptToAsm(script))
122
138
  break;
123
139
  }
124
140
  return script;
125
141
  }
126
- function replaceOps(script, optimisations) {
142
+ function replaceOpsOld(script, optimisations) {
127
143
  let asm = scriptToAsm(script);
128
144
  // Apply all optimisations in the cashproof file
129
145
  optimisations.forEach(([pattern, replacement]) => {
@@ -144,4 +160,139 @@ function replaceOps(script, optimisations) {
144
160
  asm = asm.replace(/\s+/g, ' ').trim();
145
161
  return asmToScript(asm);
146
162
  }
163
+ function replaceOps(script, locationData, logs, requires, constructorParamLength, optimisations) {
164
+ let asm = scriptToAsm(script);
165
+ let newLocationData = [...locationData];
166
+ let newLogs = [...logs];
167
+ let newRequires = [...requires];
168
+ optimisations.forEach(([pattern, replacement]) => {
169
+ let processedAsm = '';
170
+ let asmToSearch = asm;
171
+ // We add a space or end of string to the end of the pattern to ensure that we match the whole pattern
172
+ // (no partial matches)
173
+ const regex = new RegExp(`${pattern}(\\s|$)`, 'g');
174
+ let matchIndex = asmToSearch.search(regex);
175
+ while (matchIndex !== -1) {
176
+ // We add the part before the match to the processed asm
177
+ processedAsm = mergeAsm(processedAsm, asmToSearch.slice(0, matchIndex));
178
+ // We count the number of spaces in the processed asm + 1, which is equal to the script index
179
+ // We do the same thing to calculate the number of opcodes in the pattern and replacement
180
+ const scriptIndex = processedAsm === '' ? 0 : [...processedAsm.matchAll(/\s+/g)].length + 1;
181
+ const patternLength = [...pattern.matchAll(/\s+/g)].length + 1;
182
+ const replacementLength = replacement === '' ? 0 : [...replacement.matchAll(/\s+/g)].length + 1;
183
+ // We get the locationdata entries for every opcode in the pattern
184
+ const patternLocations = newLocationData.slice(scriptIndex, scriptIndex + patternLength);
185
+ // We get the lowest start location and highest end location of the pattern
186
+ const lowestStart = getLowestStartLocation(patternLocations);
187
+ const highestEnd = getHighestEndLocation(patternLocations);
188
+ // If any of the pattern locations have a position hint of END, we use that as the position hint
189
+ const positionHint = patternLocations.some((location) => location.positionHint === PositionHint.END)
190
+ ? PositionHint.END
191
+ : PositionHint.START;
192
+ // We merge the lowest start and highest end locations into a single location data entry
193
+ const mergedLocation = {
194
+ location: {
195
+ start: lowestStart.location.start,
196
+ end: highestEnd.location.end,
197
+ },
198
+ positionHint,
199
+ };
200
+ // We replace the pattern locations with the merged location
201
+ // (note that every opcode in the replacement has the same location)
202
+ const replacementLocations = new Array(replacementLength).fill(mergedLocation);
203
+ newLocationData.splice(scriptIndex, patternLength, ...replacementLocations);
204
+ const lengthDiff = patternLength - replacementLength; // 2 or 1
205
+ // The IP of an opcode in the script is its index within the script + the constructor parameters, because
206
+ // the constructor parameters still have to get added to the front of the script when a new Contract is created.
207
+ const scriptIp = scriptIndex + constructorParamLength;
208
+ newRequires = newRequires.map((require) => {
209
+ // We calculate the new ip of the require by subtracting the length diff between the matched pattern and replacement
210
+ const newCalculatedRequireIp = require.ip - lengthDiff;
211
+ return {
212
+ ...require,
213
+ // If the require is within the pattern, we want to make sure that the new ip is at least the scriptIp
214
+ // Note that this is impossible for the current set of optimisations, but future proofs the code
215
+ ip: require.ip >= scriptIp ? Math.max(scriptIp, newCalculatedRequireIp) : require.ip,
216
+ };
217
+ });
218
+ newLogs = newLogs.map((log) => {
219
+ // We calculate the new ip of the log by subtracting the length diff between the matched pattern and replacement
220
+ const newCalculatedLogIp = log.ip - lengthDiff;
221
+ return {
222
+ // If the log is within the pattern, we want to make sure that the new ip is at least the scriptIp
223
+ ip: log.ip >= scriptIp ? Math.max(scriptIp, newCalculatedLogIp) : log.ip,
224
+ line: log.line,
225
+ data: log.data.map((data) => {
226
+ if (typeof data === 'string')
227
+ return data;
228
+ // If the log is completely before the pattern, we don't need to change anything
229
+ if (data.ip <= scriptIp)
230
+ return data;
231
+ // If the log is completely after the pattern, we just need to offset the ip by the length diff
232
+ if (data.ip >= scriptIp + patternLength) {
233
+ const newCalculatedDataIp = data.ip - lengthDiff;
234
+ return { ...data, ip: newCalculatedDataIp };
235
+ }
236
+ const addedTransformationsCount = data.ip - scriptIp;
237
+ const addedTransformations = [...pattern.split(/\s+/g)].slice(0, addedTransformationsCount).join(' ');
238
+ const newTransformations = data.transformations ? `${addedTransformations} ${data.transformations}` : addedTransformations;
239
+ return {
240
+ ...data,
241
+ ip: scriptIp,
242
+ transformations: newTransformations,
243
+ };
244
+ }),
245
+ };
246
+ });
247
+ // We add the replacement to the processed asm
248
+ processedAsm = mergeAsm(processedAsm, replacement);
249
+ // We do not add the matched pattern anywhere since it gets replaced
250
+ // We set the asmToSearch to the part after the match
251
+ asmToSearch = asmToSearch.slice(matchIndex + pattern.length).trim();
252
+ // Find the next match
253
+ matchIndex = asmToSearch.search(regex);
254
+ }
255
+ // We add the remaining asm to the processed asm
256
+ processedAsm = mergeAsm(processedAsm, asmToSearch);
257
+ // We replace the original asm with the processed asm so that the next optimisation can use the updated asm
258
+ asm = processedAsm;
259
+ });
260
+ return {
261
+ script: asmToScript(asm),
262
+ locationData: newLocationData,
263
+ logs: newLogs,
264
+ requires: newRequires,
265
+ };
266
+ }
267
+ const getHighestEndLocation = (locations) => {
268
+ return locations.reduce((highest, current) => {
269
+ if (current.location.end.line > highest.location.end.line) {
270
+ return current;
271
+ }
272
+ if (highest.location.end.line === current.location.end.line) {
273
+ if (current.location.end.column > highest.location.end.column) {
274
+ return current;
275
+ }
276
+ }
277
+ return highest;
278
+ }, locations[0]);
279
+ };
280
+ const getLowestStartLocation = (locations) => {
281
+ return locations.reduce((lowest, current) => {
282
+ if (current.location.start.line < lowest.location.start.line) {
283
+ return current;
284
+ }
285
+ if (lowest.location.start.line === current.location.start.line) {
286
+ if (current.location.start.column < lowest.location.start.column) {
287
+ return current;
288
+ }
289
+ }
290
+ return lowest;
291
+ }, locations[0]);
292
+ };
293
+ const mergeAsm = (asm1, asm2) => {
294
+ // We merge two ASM strings by adding a space between them, and removing any duplicate spaces
295
+ // or trailing/leading spaces, which might have been introduced due to regex matching / replacements / empty asm strings
296
+ return `${asm1} ${asm2}`.replace(/\s+/g, ' ').trim();
297
+ };
147
298
  //# sourceMappingURL=script.js.map
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@cashscript/utils",
3
- "version": "0.11.0-next.2",
3
+ "version": "0.11.0-next.4",
4
4
  "description": "CashScript utilities and types",
5
5
  "keywords": [
6
6
  "bitcoin cash",
7
7
  "cashscript",
8
8
  "sdk",
9
- "smart contracts"
9
+ "smart contracts",
10
+ "cashtokens"
10
11
  ],
11
12
  "homepage": "https://cashscript.org",
12
13
  "bugs": {
@@ -18,6 +19,9 @@
18
19
  },
19
20
  "license": "MIT",
20
21
  "author": "Rosco Kalis <roscokalis@gmail.com>",
22
+ "contributors": [
23
+ "Mathieu Geukens <mr-zwets@protonmail.com>"
24
+ ],
21
25
  "main": "dist/index.js",
22
26
  "types": "dist/index.d.ts",
23
27
  "type": "module",
@@ -48,5 +52,5 @@
48
52
  "jest": "^29.7.0",
49
53
  "typescript": "^5.7.3"
50
54
  },
51
- "gitHead": "47d29372460bc397abc174e7404b7620858b6ecc"
55
+ "gitHead": "68cdf30db455e3eef448a4feacfff0f5f26a36a7"
52
56
  }