Skip to content

Commit 0bdc767

Browse files
committed
Port the ContractsIdentifier code
1 parent dd34257 commit 0bdc767

File tree

3 files changed

+4
-268
lines changed

3 files changed

+4
-268
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,218 +1,2 @@
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 };
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,3 @@
1-
import {
2-
getLibraryAddressPositions,
3-
linkHexStringBytecode,
4-
} from "@nomicfoundation/edr";
5-
import { Opcode } from "./opcodes";
1+
import { linkHexStringBytecode } from "@nomicfoundation/edr";
62

7-
export { getLibraryAddressPositions, linkHexStringBytecode };
8-
9-
export function zeroOutAddresses(
10-
code: Uint8Array,
11-
addressesPositions: number[]
12-
): Uint8Array {
13-
const addressesSlices = addressesPositions.map((start) => ({
14-
start,
15-
length: 20,
16-
}));
17-
18-
return zeroOutSlices(code, addressesSlices);
19-
}
20-
21-
export function zeroOutSlices(
22-
code: Uint8Array,
23-
slices: Array<{ start: number; length: number }>
24-
): Uint8Array {
25-
for (const { start, length } of slices) {
26-
code = Buffer.concat([
27-
code.slice(0, start),
28-
Buffer.alloc(length, 0),
29-
code.slice(start + length),
30-
]);
31-
}
32-
33-
return code;
34-
}
35-
36-
export function normalizeLibraryRuntimeBytecodeIfNecessary(
37-
code: Uint8Array
38-
): Uint8Array {
39-
// Libraries' protection normalization:
40-
// Solidity 0.4.20 introduced a protection to prevent libraries from being called directly.
41-
// This is done by modifying the code on deployment, and hard-coding the contract address.
42-
// The first instruction is a PUSH20 of the address, which we zero-out as a way of normalizing
43-
// it. Note that it's also zeroed-out in the compiler output.
44-
if (code[0] === Opcode.PUSH20) {
45-
return zeroOutAddresses(code, [1]);
46-
}
47-
48-
return code;
49-
}
3+
export { linkHexStringBytecode };

Diff for: packages/hardhat-core/src/internal/hardhat-network/stack-traces/opcodes.ts

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ export {
22
Opcode,
33
isPush,
44
isJump,
5-
getPushLength,
6-
getOpcodeLength,
75
isCall,
86
isCreate,
97
opcodeToString,

0 commit comments

Comments
 (0)