|
1 |
| -import { bytesToHex } from "@nomicfoundation/ethereumjs-util"; |
2 |
| - |
3 |
| -import { |
4 |
| - normalizeLibraryRuntimeBytecodeIfNecessary, |
5 |
| - zeroOutAddresses, |
6 |
| - zeroOutSlices, |
7 |
| -} from "./library-utils"; |
8 |
| -import { Bytecode } from "./model"; |
9 |
| -import { getOpcodeLength, Opcode } from "./opcodes"; |
10 |
| - |
11 |
| -/** |
12 |
| - * This class represent a somewhat special Trie of bytecodes. |
13 |
| - * |
14 |
| - * What makes it special is that every node has a set of all of its descendants and its depth. |
15 |
| - */ |
16 |
| -class BytecodeTrie { |
17 |
| - private readonly _childNodes: Map<number, BytecodeTrie> = new Map(); |
18 |
| - public readonly descendants: Bytecode[] = []; |
19 |
| - public match?: Bytecode; |
20 |
| - |
21 |
| - constructor(public readonly depth: number) {} |
22 |
| - |
23 |
| - public add(bytecode: Bytecode) { |
24 |
| - // eslint-disable-next-line @typescript-eslint/no-this-alias |
25 |
| - let trieNode: BytecodeTrie = this; |
26 |
| - |
27 |
| - for (const [index, byte] of bytecode.normalizedCode.entries()) { |
28 |
| - trieNode.descendants.push(bytecode); |
29 |
| - |
30 |
| - let childNode = trieNode._childNodes.get(byte); |
31 |
| - if (childNode === undefined) { |
32 |
| - childNode = new BytecodeTrie(index); |
33 |
| - trieNode._childNodes.set(byte, childNode); |
34 |
| - } |
35 |
| - |
36 |
| - trieNode = childNode; |
37 |
| - } |
38 |
| - |
39 |
| - // If multiple contracts with the exact same bytecode are added we keep the last of them. |
40 |
| - // Note that this includes the metadata hash, so the chances of happening are pretty remote, |
41 |
| - // except in super artificial cases that we have in our test suite. |
42 |
| - trieNode.match = bytecode; |
43 |
| - } |
44 |
| - |
45 |
| - /** |
46 |
| - * Searches for a bytecode. If it's an exact match, it is returned. If there's no match, but a |
47 |
| - * prefix of the code is found in the trie, the node of the longest prefix is returned. If the |
48 |
| - * entire code is covered by the trie, and there's no match, we return undefined. |
49 |
| - */ |
50 |
| - public search( |
51 |
| - code: Uint8Array, |
52 |
| - currentCodeByte: number = 0 |
53 |
| - ): Bytecode | BytecodeTrie | undefined { |
54 |
| - if (currentCodeByte > code.length) { |
55 |
| - return undefined; |
56 |
| - } |
57 |
| - |
58 |
| - // eslint-disable-next-line @typescript-eslint/no-this-alias |
59 |
| - let trieNode: BytecodeTrie = this; |
60 |
| - for (; currentCodeByte < code.length; currentCodeByte += 1) { |
61 |
| - const childNode = trieNode._childNodes.get(code[currentCodeByte]); |
62 |
| - |
63 |
| - if (childNode === undefined) { |
64 |
| - return trieNode; |
65 |
| - } |
66 |
| - |
67 |
| - trieNode = childNode; |
68 |
| - } |
69 |
| - |
70 |
| - return trieNode.match; |
71 |
| - } |
72 |
| -} |
73 |
| - |
74 |
| -export class ContractsIdentifier { |
75 |
| - private _trie = new BytecodeTrie(-1); |
76 |
| - private _cache: Map<string, Bytecode> = new Map(); |
77 |
| - |
78 |
| - constructor(private readonly _enableCache = true) {} |
79 |
| - |
80 |
| - public addBytecode(bytecode: Bytecode) { |
81 |
| - this._trie.add(bytecode); |
82 |
| - this._cache.clear(); |
83 |
| - } |
84 |
| - |
85 |
| - public getBytecodeForCall( |
86 |
| - code: Uint8Array, |
87 |
| - isCreate: boolean |
88 |
| - ): Bytecode | undefined { |
89 |
| - const normalizedCode = normalizeLibraryRuntimeBytecodeIfNecessary(code); |
90 |
| - |
91 |
| - let normalizedCodeHex: string | undefined; |
92 |
| - if (this._enableCache) { |
93 |
| - normalizedCodeHex = bytesToHex(normalizedCode); |
94 |
| - const cached = this._cache.get(normalizedCodeHex); |
95 |
| - |
96 |
| - if (cached !== undefined) { |
97 |
| - return cached; |
98 |
| - } |
99 |
| - } |
100 |
| - |
101 |
| - const result = this._searchBytecode(isCreate, normalizedCode); |
102 |
| - |
103 |
| - if (this._enableCache) { |
104 |
| - if (result !== undefined) { |
105 |
| - this._cache.set(normalizedCodeHex!, result); |
106 |
| - } |
107 |
| - } |
108 |
| - |
109 |
| - return result; |
110 |
| - } |
111 |
| - |
112 |
| - private _searchBytecode( |
113 |
| - isCreate: boolean, |
114 |
| - code: Uint8Array, |
115 |
| - normalizeLibraries = true, |
116 |
| - trie = this._trie, |
117 |
| - firstByteToSearch = 0 |
118 |
| - ): Bytecode | undefined { |
119 |
| - const searchResult = trie.search(code, firstByteToSearch); |
120 |
| - |
121 |
| - if (searchResult === undefined) { |
122 |
| - return undefined; |
123 |
| - } |
124 |
| - |
125 |
| - if (searchResult instanceof Bytecode) { |
126 |
| - return searchResult; |
127 |
| - } |
128 |
| - |
129 |
| - // Deployment messages have their abi-encoded arguments at the end of the bytecode. |
130 |
| - // |
131 |
| - // We don't know how long those arguments are, as we don't know which contract is being |
132 |
| - // deployed, hence we don't know the signature of its constructor. |
133 |
| - // |
134 |
| - // To make things even harder, we can't trust that the user actually passed the right |
135 |
| - // amount of arguments. |
136 |
| - // |
137 |
| - // Luckily, the chances of a complete deployment bytecode being the prefix of another one are |
138 |
| - // remote. For example, most of the time it ends with its metadata hash, which will differ. |
139 |
| - // |
140 |
| - // We take advantage of this last observation, and just return the bytecode that exactly |
141 |
| - // matched the searchResult (sub)trie that we got. |
142 |
| - if ( |
143 |
| - isCreate && |
144 |
| - searchResult.match !== undefined && |
145 |
| - searchResult.match.isDeployment |
146 |
| - ) { |
147 |
| - return searchResult.match; |
148 |
| - } |
149 |
| - |
150 |
| - if (normalizeLibraries) { |
151 |
| - for (const bytecodeWithLibraries of searchResult.descendants) { |
152 |
| - if ( |
153 |
| - bytecodeWithLibraries.libraryAddressPositions.length === 0 && |
154 |
| - bytecodeWithLibraries.immutableReferences.length === 0 |
155 |
| - ) { |
156 |
| - continue; |
157 |
| - } |
158 |
| - |
159 |
| - const normalizedLibrariesCode = zeroOutAddresses( |
160 |
| - code, |
161 |
| - bytecodeWithLibraries.libraryAddressPositions |
162 |
| - ); |
163 |
| - |
164 |
| - const normalizedCode = zeroOutSlices( |
165 |
| - normalizedLibrariesCode, |
166 |
| - bytecodeWithLibraries.immutableReferences |
167 |
| - ); |
168 |
| - |
169 |
| - const normalizedResult = this._searchBytecode( |
170 |
| - isCreate, |
171 |
| - normalizedCode, |
172 |
| - false, |
173 |
| - searchResult, |
174 |
| - searchResult.depth + 1 |
175 |
| - ); |
176 |
| - |
177 |
| - if (normalizedResult !== undefined) { |
178 |
| - return normalizedResult; |
179 |
| - } |
180 |
| - } |
181 |
| - } |
182 |
| - |
183 |
| - // If we got here we may still have the contract, but with a different metadata hash. |
184 |
| - // |
185 |
| - // We check if we got to match the entire executable bytecode, and are just stuck because |
186 |
| - // of the metadata. If that's the case, we can assume that any descendant will be a valid |
187 |
| - // Bytecode, so we just choose the most recently added one. |
188 |
| - // |
189 |
| - // The reason this works is because there's no chance that Solidity includes an entire |
190 |
| - // bytecode (i.e. with metadata), as a prefix of another one. |
191 |
| - if ( |
192 |
| - this._isMatchingMetadata(code, searchResult.depth) && |
193 |
| - searchResult.descendants.length > 0 |
194 |
| - ) { |
195 |
| - return searchResult.descendants[searchResult.descendants.length - 1]; |
196 |
| - } |
197 |
| - |
198 |
| - return undefined; |
199 |
| - } |
200 |
| - |
201 |
| - /** |
202 |
| - * Returns true if the lastByte is placed right when the metadata starts or after it. |
203 |
| - */ |
204 |
| - private _isMatchingMetadata(code: Uint8Array, lastByte: number): boolean { |
205 |
| - for (let byte = 0; byte < lastByte; ) { |
206 |
| - const opcode = code[byte]; |
207 |
| - |
208 |
| - // Solidity always emits REVERT INVALID right before the metadata |
209 |
| - if (opcode === Opcode.REVERT && code[byte + 1] === Opcode.INVALID) { |
210 |
| - return true; |
211 |
| - } |
212 |
| - |
213 |
| - byte += getOpcodeLength(opcode); |
214 |
| - } |
215 |
| - |
216 |
| - return false; |
217 |
| - } |
218 |
| -} |
| 1 | +import { ContractsIdentifier } from "@nomicfoundation/edr"; |
| 2 | +export { ContractsIdentifier }; |
0 commit comments