diff --git a/src/chains/ethereum/src/api.ts b/src/chains/ethereum/src/api.ts index 0bc3f12cf6..db436acc51 100644 --- a/src/chains/ethereum/src/api.ts +++ b/src/chains/ethereum/src/api.ts @@ -9,7 +9,7 @@ import { import { TypedData as NotTypedData, signTypedData_v4 } from "eth-sig-util"; import { EthereumInternalOptions } from "./options"; import { types, Data, Quantity } from "@ganache/utils"; -import Blockchain from "./blockchain"; +import Blockchain, { TransactionTraceOptions } from "./blockchain"; import Tag from "./things/tags"; import { VM_EXCEPTION, VM_EXCEPTIONS } from "./errors/errors"; import Address from "./things/address"; @@ -1777,6 +1777,34 @@ export default class EthereumApi implements types.Api { } //#endregion + //#region debug + + /** + * Attempt to run the transaction in the exact same manner as it was executed + * on the network. It will replay any transaction that may have been executed + * prior to this one before it will finally attempt to execute the transaction + * that corresponds to the given hash. + * + * In addition to the hash of the transaction you may give it a secondary + * optional argument, which specifies the options for this specific call. + * The possible options are: + * + * * `disableStorage`: {boolean} Setting this to `true` will disable storage capture (default = `false`). + * * `disableMemory`: {boolean} Setting this to `true` will disable memory capture (default = `false`). + * * `disableStack`: {boolean} Setting this to `true` will disable stack capture (default = `false`). + * + * @param transactionHash + * @param options + */ + async debug_traceTransaction( + transactionHash: string, + options?: TransactionTraceOptions + ) { + return this.#blockchain.traceTransaction(transactionHash, options); + } + + //#endregion + //#region personal /** * Returns all the Ethereum account addresses of all keys that have been diff --git a/src/chains/ethereum/src/blockchain.ts b/src/chains/ethereum/src/blockchain.ts index 57aded2c45..c7bfce25d3 100644 --- a/src/chains/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/src/blockchain.ts @@ -27,6 +27,9 @@ import { VmError, ERROR } from "ethereumjs-vm/dist/exceptions"; import { EthereumInternalOptions } from "./options"; import { Snapshots } from "./types/snapshots"; import { RuntimeBlock, Block } from "./things/runtime-block"; +import { ITraceData, TraceDataFactory } from "./things/trace-data"; +import TraceStorageMap from "./things/trace-storage-map"; + const { BUFFER_EMPTY, RPCQUANTITY_EMPTY, @@ -81,6 +84,45 @@ type BlockchainTypedEvents = { }; type BlockchainEvents = "start" | "stop"; +export type TransactionTraceOptions = { + disableStorage?: boolean; + disableMemory?: boolean; + disableStack?: boolean; +}; + +export type StructLog = { + depth: number; + error: string; + gas: number; + gasCost: number; + memory: Array; + op: string; + pc: number; + stack: Array; + storage: TraceStorageMap; +}; + +interface Logger { + log(message?: any, ...optionalParams: any[]): void; +} + +export type BlockchainOptions = { + db?: string | object; + db_path?: string; + initialAccounts?: Account[]; + hardfork?: string; + allowUnlimitedContractSize?: boolean; + gasLimit?: Quantity; + time?: Date; + blockTime?: number; + coinbase: Account; + chainId: number; + common: Common; + legacyInstamine: boolean; + vmErrorsOnRPCResponse: boolean; + logger: Logger; +}; + /** * Sets the provided VM state manager's state root *without* first * checking for checkpoints or flushing the existing cache. @@ -795,6 +837,291 @@ export default class Blockchain extends Emittery.Typed< } } + /** + * traceTransaction + * + * Run a previously-run transaction in the same state in which it occurred at the time it was run. + * This will return the vm-level trace output for debugging purposes. + * + * Strategy: + * + * 1. Find block where transaction occurred + * 2. Set state root of that block + * 3. Rerun every transaction in that block prior to and including the requested transaction + * 4. Send trace results back. + * + * @param transactionHash + * @param params + */ + public async traceTransaction( + transactionHash: string, + params: TransactionTraceOptions + ) { + let currentDepth = -1; + const storageStack: TraceStorageMap[] = []; + + // TODO: gas could go theoretically go over Number.MAX_SAFE_INTEGER. + // (Ganache v2 didn't handle this possibility either, so it hasn't been + // updated yet) + let gas = 0; + // TODO: returnValue isn't used... it wasn't used in v2 either. What's this + // supposed to be? + let returnValue = ""; + const structLogs: Array = []; + + const transactionHashBuffer = Data.from(transactionHash).toBuffer(); + // #1 - get block via transaction object + const transaction = await this.transactions.get(transactionHashBuffer); + + if (!transaction) { + throw new Error("Unknown transaction " + transactionHash); + } + + const targetBlock = await this.blocks.get(transaction._blockNum); + const parentBlock = await this.blocks.getByHash( + targetBlock.header.parentHash.toBuffer() + ); + + // #2 - Set state root of original block + // + // TODO: Forking needs the forked block number passed during this step: + // https://github.com/trufflesuite/ganache-core/blob/develop/lib/blockchain_double.js#L917 + const trie = new CheckpointTrie( + this.#database.trie, + parentBlock.header.stateRoot.toBuffer() + ); + + // Prepare the "next" block with necessary transactions + const newBlock = new RuntimeBlock( + Quantity.from(parentBlock.header.number.toBigInt() + 1n), + parentBlock.hash(), + parentBlock.header.miner, + parentBlock.header.gasLimit.toBuffer(), + // make sure we use the same timestamp as the target block + targetBlock.header.timestamp + ) as RuntimeBlock & { uncleHeaders: []; transactions: Transaction[] }; + newBlock.transactions = []; + newBlock.uncleHeaders = []; + + const transactions = targetBlock.getTransactions(); + for (const tx of transactions) { + newBlock.transactions.push(tx); + + // After including the target transaction, that's all we need to do. + if (tx.hash().equals(transactionHashBuffer)) { + break; + } + } + + type StepEvent = { + gasLeft: BN; + memory: Array; // Not officially sure the type. Not a buffer or uint8array + stack: Array; + depth: number; + opcode: { + name: string; + }; + pc: number; + address: Buffer; + }; + + const TraceData = TraceDataFactory(); + + const stepListener = ( + event: StepEvent, + next: (error?: any, cb?: any) => void + ) => { + // See these docs: + // https://github.com/ethereum/go-ethereum/wiki/Management-APIs + + const gasLeft = event.gasLeft.toNumber(); + const totalGasUsedAfterThisStep = + Quantity.from(transaction.gasLimit).toNumber() - gasLeft; + const gasUsedPreviousStep = totalGasUsedAfterThisStep - gas; + gas += gasUsedPreviousStep; + + const memory: ITraceData[] = []; + if (params.disableMemory !== true) { + // We get the memory as one large array. + // Let's cut it up into 32 byte chunks as required by the spec. + let index = 0; + while (index < event.memory.length) { + const slice = event.memory.slice(index, index + 32); + memory.push(TraceData.from(Buffer.from(slice))); + index += 32; + } + } + + const stack: ITraceData[] = []; + if (params.disableStack !== true) { + for (const stackItem of event.stack) { + stack.push(TraceData.from(stackItem.toBuffer())); + } + } + + const structLog: StructLog = { + depth: event.depth, + error: "", + gas: gasLeft, + gasCost: 0, + memory, + op: event.opcode.name, + pc: event.pc, + stack, + storage: null + }; + + // The gas difference calculated for each step is indicative of gas consumed in + // the previous step. Gas consumption in the final step will always be zero. + if (structLogs.length) { + structLogs[structLogs.length - 1].gasCost = gasUsedPreviousStep; + } + + if (params.disableStorage === true) { + // Add the struct log as is - nothing more to do. + structLogs.push(structLog); + next(); + } else { + const { depth: eventDepth } = event; + if (currentDepth > eventDepth) { + storageStack.pop(); + } else if (currentDepth < eventDepth) { + storageStack.push(new TraceStorageMap()); + } + + currentDepth = eventDepth; + + switch (event.opcode.name) { + case "SSTORE": { + const key = stack[stack.length - 1]; + const value = stack[stack.length - 2]; + + // new TraceStorageMap() here creates a shallow clone, to prevent other steps from overwriting + structLog.storage = new TraceStorageMap(storageStack[eventDepth]); + + // Tell vm to move on to the next instruction. See below. + structLogs.push(structLog); + next(); + + // assign after callback because this storage change actually takes + // effect _after_ this opcode executes + storageStack[eventDepth].set(key, value); + break; + } + case "SLOAD": { + const key = stack[stack.length - 1]; + vm.stateManager.getContractStorage( + event.address, + key.toBuffer(), + (err: Error, result: Buffer) => { + if (err) { + return next(err); + } + + const value = TraceData.from(result); + storageStack[eventDepth].set(key, value); + + // new TraceStorageMap() here creates a shallow clone, to prevent other steps from overwriting + structLog.storage = new TraceStorageMap( + storageStack[eventDepth] + ); + structLogs.push(structLog); + next(); + } + ); + break; + } + default: + // new TraceStorageMap() here creates a shallow clone, to prevent other steps from overwriting + structLog.storage = new TraceStorageMap(storageStack[eventDepth]); + structLogs.push(structLog); + next(); + } + } + }; + + let txHashCurrentlyProcessing: string = null; + + const beforeTxListener = (tx: Transaction) => { + txHashCurrentlyProcessing = Data.from(tx.hash()).toString(); + if (txHashCurrentlyProcessing == transactionHash) { + vm.on("step", stepListener); + } + }; + + const afterTxListener = () => { + if (txHashCurrentlyProcessing == transactionHash) { + removeListeners(); + } + }; + + const removeListeners = () => { + vm.removeListener("step", stepListener); + vm.removeListener("beforeTx", beforeTxListener); + vm.removeListener("afterTx", afterTxListener); + }; + + const blocks = this.blocks; + + // ethereumjs vm doesn't use the callback style anymore + const getBlock = class T { + static async [promisify.custom](number: BN) { + const block = await blocks.get(number.toBuffer()).catch(_ => null); + return block ? block.value : null; + } + }; + + const vm = new VM({ + state: trie, + activatePrecompiles: true, + common: this.#common, + allowUnlimitedContractSize: this.#options.chain + .allowUnlimitedContractSize, + blockchain: { + getBlock + } as any + }); + + // Listen to beforeTx and afterTx so we know when our target transaction + // is processing. These events will add the event listener for getting the trace data. + vm.on("beforeTx", beforeTxListener); + vm.on("afterTx", afterTxListener); + + // Don't even let the vm try to flush the block's _cache to the stateTrie. + // When forking some of the data that the traced function may request will + // exist only on the main chain. Because we pretty much lie to the VM by + // telling it we DO have data in our Trie, when we really don't, it gets + // lost during the commit phase when it traverses the "borrowed" data's + // trie (as it may not have a valid root). Because this is a trace, and we + // don't need to commit the data, duck punching the `flush` method (the + // simplest method I could find) is fine. + // Remove this and you may see the infamous + // `Uncaught TypeError: Cannot read property 'pop' of undefined` error! + vm.stateManager._cache.flush = cb => cb(); + + // #3 - Process the block without committing the data. + + // The vmerr key on the result appears to be removed. + // The previous implementation had specific error handling. + // It's possible we've removed handling specific cases in this implementation. + // e.g., the previous incatation of RuntimeError + await vm.runBlock({ + block: newBlock, // .value is the object the vm expects + generate: true, + skipBlockValidation: true + }); + + // Just to be safe + removeListeners(); + + // #4 - send state results back + return { + gas, + structLogs, + returnValue + }; + } + /** * Gracefully shuts down the blockchain service and all of its dependencies. */ diff --git a/src/chains/ethereum/src/things/trace-data.ts b/src/chains/ethereum/src/things/trace-data.ts new file mode 100644 index 0000000000..d52ebc439b --- /dev/null +++ b/src/chains/ethereum/src/things/trace-data.ts @@ -0,0 +1,106 @@ +import { utils } from "@ganache/utils"; +const { bufferToMinHexKey } = utils; + +export interface ITraceData { + toBuffer(): Buffer; + toString(): string; + toJSON(): string; +} + +const BYTE_LENGTH = 32; + +/** + * Precomputed 32-byte prefixes to make stringification a faster + */ +const PREFIXES = [ + "", + "00", + "0000", + "000000", + "00000000", + "0000000000", + "000000000000", + "00000000000000", + "0000000000000000", + "000000000000000000", + "00000000000000000000", + "0000000000000000000000", + "000000000000000000000000", + "00000000000000000000000000", + "0000000000000000000000000000", + "000000000000000000000000000000", + "00000000000000000000000000000000", + "0000000000000000000000000000000000", + "000000000000000000000000000000000000", + "00000000000000000000000000000000000000", + "0000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000" +]; + +export const TraceDataFactory = () => { + const traceDataLookup: Map = new Map(); + + const TraceData = { + from: (value: Buffer) => { + // Remove all leading zeroes from keys. + const key = bufferToMinHexKey(value); + const existing = traceDataLookup.get(key); + + if (existing) { + return existing; + } + + let buffer: Buffer; + let str: string; + + const data = { + /** + * Returns a 32-byte 0-padded Buffer + */ + toBuffer: () => { + if (buffer) { + return buffer; + } + const length = value.byteLength; + if (length === BYTE_LENGTH) { + buffer = value; + } else { + // convert the buffer into the appropriately sized buffer. + const lengthDiff = BYTE_LENGTH - length; + buffer = Buffer.allocUnsafe(BYTE_LENGTH).fill(0, 0, lengthDiff); + value.copy(buffer, lengthDiff, 0, length); + } + return buffer; + }, + /** + * Returns a 32-byte hex-string representation + */ + toJSON: () => { + if (str) { + return str; + } + // convert a hex key like "ab01" into "00...00ab01" + return (str = `${PREFIXES[BYTE_LENGTH - key.length / 2]}${key}`); + } + }; + + traceDataLookup.set(key, data); + return data; + } + }; + + return TraceData; +}; + +export default TraceDataFactory; diff --git a/src/chains/ethereum/src/things/trace-storage-map.ts b/src/chains/ethereum/src/things/trace-storage-map.ts new file mode 100644 index 0000000000..a41a2ce720 --- /dev/null +++ b/src/chains/ethereum/src/things/trace-storage-map.ts @@ -0,0 +1,15 @@ +import { ITraceData } from "./trace-data"; + +class TraceStorageMap extends Map { + toJSON(): Record { + const obj: Record = {}; + + for (const [key, value] of this) { + obj[key.toJSON()] = value; + } + + return obj; + } +} + +export default TraceStorageMap; diff --git a/src/chains/ethereum/tests/api/debug/debug.test.ts b/src/chains/ethereum/tests/api/debug/debug.test.ts new file mode 100644 index 0000000000..ed7d639cc3 --- /dev/null +++ b/src/chains/ethereum/tests/api/debug/debug.test.ts @@ -0,0 +1,314 @@ +import getProvider from "../../helpers/getProvider"; +import compile, { CompileOutput } from "../../helpers/compile"; +import assert from "assert"; +import EthereumProvider from "../../../src/provider"; +import path from "path"; +import { Quantity, Data } from "@ganache/utils"; +import Blockchain from "../../../src/blockchain"; +import Account from "../../../src/things/account"; +import Address from "../../../src/things/address"; +import Common from "ethereumjs-common"; +import Transaction from "../../../src/things/transaction"; +import { EthereumOptionsConfig } from "../../../src/options/index"; +import TraceStorageMap from "../../../src/things/trace-storage-map"; + +describe("api", () => { + describe("debug", () => { + let contract: CompileOutput; + let provider: EthereumProvider; + let accounts: Array; + let from: string; + let contractAddress: string; + let transactionHash: string; + let initialValue: string; + let methods: Record; + + before(async () => { + contract = compile( + path.join(__dirname, "..", "..", "contracts", "Debug.sol") + ); + + provider = await getProvider({ + miner: { + defaultTransactionGasLimit: 6721975 + } + }); + + [from] = await provider.send("eth_accounts"); + + await provider.send("eth_subscribe", ["newHeads"]); + + // Deploy the contract + const deploymentTransactionHash = await provider.send( + "eth_sendTransaction", + [ + { + from, + data: contract.code + } + ] + ); + + await provider.once("message"); + + const receipt = await provider.send("eth_getTransactionReceipt", [ + deploymentTransactionHash + ]); + contractAddress = receipt.contractAddress; + + methods = contract.contract.evm.methodIdentifiers; + + // Send a transaction that will be the one we trace + initialValue = + "0000000000000000000000000000000000000000000000000000000000000019"; + transactionHash = await provider.send("eth_sendTransaction", [ + { + from, + to: contractAddress, + data: "0x" + methods["setValue(uint256)"] + initialValue + } + ]); + + await provider.once("message"); + + // Send another transaction thate changes the state, to ensure traces don't change state + const newValue = + "0000000000000000000000000000000000000000000000000000000000001337"; + await provider.send("eth_sendTransaction", [ + { + from, + to: contractAddress, + data: "0x" + methods["setValue(uint256)"] + newValue + } + ]); + + await provider.once("message"); + + await provider.send("eth_sendTransaction", [ + { from, to: contractAddress, data: "0x" + methods["doARevert()"] } + ]); + }); + + it("should trace a successful transaction without changing state", async () => { + const { structLogs } = await provider.send("debug_traceTransaction", [ + transactionHash, + {} + ]); + + // "So basic" test - did we at least get some structlogs? + assert(structLogs.length > 0); + + // Check formatting of stack + for (const [, { stack }] of structLogs.entries()) { + if (stack.length > 0) { + // check formatting of stack - it was broken when updating to ethereumjs-vm v2.3.3 + assert.strictEqual(stack[0].length, 64); + assert.notStrictEqual(stack[0].substr(0, 2), "0x"); + break; + } + } + + // Check formatting of memory + for (const [, { memory }] of structLogs.entries()) { + if (memory.length > 0) { + // check formatting of memory + assert.strictEqual(memory[0].length, 64); + assert.notStrictEqual(memory[0].substr(0, 2), "0x"); + break; + } + } + + const lastop = structLogs[structLogs.length - 1]; + + assert.strictEqual(lastop.op, "STOP"); + assert.strictEqual(lastop.gasCost, 0); + assert.strictEqual(lastop.pc, 202); // This will change if you edit Debug.sol + + // This makes sure we get the initial value back (the first transaction to setValue()) + // and not the value of the second setValue() transaction + assert.strictEqual( + lastop.storage[ + "0000000000000000000000000000000000000000000000000000000000000000" + ], + initialValue + ); + + // Finally, lets assert that performing the trace didn't change the data on chain + const value = await provider.send("eth_call", [ + { from, to: contractAddress, data: "0x" + methods["value()"] } + ]); + + // State of the blockchain should still be the same as the second transaction + assert.strictEqual( + value, + "0x0000000000000000000000000000000000000000000000000000000000001337" + ); + }); + + it("should still trace reverted transactions", async () => { + const { structLogs } = await provider.send("debug_traceTransaction", [ + transactionHash, + {} + ]); + + // This test mostly ensures we didn't get some type of error message + // from the virtual machine on a reverted transaction. + // If we haven't errored at this state, we're doing pretty good. + + // Let's make sure the last operation is a STOP instruction. + const { op } = structLogs.pop(); + + assert.strictEqual(op, "STOP"); + }); + + it("should have a low memory footprint", async () => { + // This test is more of a change signal than it is looking + // for correct output. By including this test, we assert that + // the number of objects referenced in the trace is exactly + // what we expect. We can use the total amount of counted objects + // as a proxy for memory consumed. Knowing when this amount + // changes can help signal significant changes to memory consumption. + + // Expectations and test input. The expected number of objects + // in the final trace is found through execution. Again, + // this test is meant as a change detector, not necessarily a + // failure detector. + const expectedObjectsInFinalTrace = 30899; + const timesToRunLoop = 100; + const from = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; + const privateKey = Data.from( + "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" + ); + + // The next line is gross, but it makes testing new values easy. + const timesToRunLoopArgument = Data.from( + Quantity.from(timesToRunLoop).toBuffer(), + 32 + ) + .toString() + .replace("0x", ""); + + const address = new Address(from); + const initialAccounts = [new Account(address)]; + + // The following will set up a vm, deploy the debugging contract, + // then run the transaction against that contract that we want to trace. + const common = new Common("mainnet", "muirGlacier"); + + const blockchain = new Blockchain( + EthereumOptionsConfig.normalize({}), + common, + initialAccounts, + address + ); + + await blockchain.once("start"); + + // Deployment transaction + const deploymentTransaction = new Transaction( + { + data: contract.code, + from, + gasLimit: 6721975, + nonce: "0x0" + }, + common + ); + deploymentTransaction._from = address.toBuffer(); + + const deploymentTransactionHash = await blockchain.queueTransaction( + deploymentTransaction, + privateKey + ); + + await blockchain.once("block"); + + const { contractAddress } = await blockchain.transactionReceipts.get( + deploymentTransactionHash.toBuffer() + ); + + // Transaction to call the loop function + const loopTransaction = new Transaction( + { + data: Buffer.from( + methods["loop(uint256)"] + timesToRunLoopArgument, + "hex" + ), + to: contractAddress, + from, + gasLimit: 6721975, + nonce: "0x1" + }, + common + ); + loopTransaction._from = address.toBuffer(); + const loopTransactionHash = await blockchain.queueTransaction( + loopTransaction, + privateKey + ); + + await blockchain.once("block"); + + // Get the trace so we can count all of the items in the result + const trace = await blockchain.traceTransaction( + loopTransactionHash.toString(), + {} + ); + + // Now lets count the number of items within the trace. We intend to count + // all individual literals as separate items, and object references as the + // same object (e.g., only counted once). There might be some gotcha's here; + // quality of this test is dependent on the correctness of the counter. + + const countMap = new Set(); + const stack: Array = [trace]; + + while (stack.length > 0) { + // pop is faster than shift, outcome is the same + let obj = stack.pop(); + + // Create new objects for literals as they take up + // their own memory slots + if (typeof obj === "string") { + obj = new String(obj); + } else if (typeof obj === "number") { + obj = new Number(obj); + } + + // if counted, don't recount. + if (countMap.has(obj)) { + continue; + } + + // Not counted? Set it. + countMap.add(obj); + + // Let's not do anything with Strings, Numbers, & TraceData; we have them counted. + if ( + !(obj instanceof String) && + !(obj instanceof Number) && + !(obj.toBuffer && obj.toJSON) + ) { + // key/value pairs that can be iterated over via for...of + let entries: IterableIterator<[any, any]> | Array<[any, any]>; + + // Honestly I'm not entirely sure I need this special case + // for this map, but I don't want to leave it out. + if (obj instanceof TraceStorageMap) { + entries = obj.entries(); + } else { + entries = Object.entries(obj); + } + + for (const [, value] of entries) { + if (value != null) { + stack.push(value); + } + } + } + } + + assert.strictEqual(countMap.size, expectedObjectsInFinalTrace); + }); + }); +}); diff --git a/src/chains/ethereum/tests/contracts/Debug.sol b/src/chains/ethereum/tests/contracts/Debug.sol new file mode 100644 index 0000000000..1c0b4271ac --- /dev/null +++ b/src/chains/ethereum/tests/contracts/Debug.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.4; + +contract Debug { + uint public value; + + event ValueSet(uint); + + constructor() public payable { + value = 5; + } + + function setValue(uint val) public { + value = val; + emit ValueSet(val); + } + + function loop(uint times) public { + for (uint i = 0; i < times; i++) { + value += i; + } + } + + function doARevert() public { + revert("all your base"); + } +} diff --git a/src/chains/ethereum/tests/helpers/compile.ts b/src/chains/ethereum/tests/helpers/compile.ts index 4b64cbbe21..907dc84de1 100644 --- a/src/chains/ethereum/tests/helpers/compile.ts +++ b/src/chains/ethereum/tests/helpers/compile.ts @@ -17,7 +17,15 @@ import solc from "solc"; import { readFileSync } from "fs-extra"; import { parse } from "path"; -export default function compile(contractPath: string, contractName?: string) { +export type CompileOutput = { + code: string; + contract: solc.CompilerOutputContracts[string][string]; +}; + +export default function compile( + contractPath: string, + contractName?: string +): CompileOutput { const parsedPath = parse(contractPath); const content = readFileSync(contractPath, { encoding: "utf8" }); const globalName = parsedPath.base; diff --git a/src/packages/utils/src/utils/buffer-to-key.ts b/src/packages/utils/src/utils/buffer-to-key.ts new file mode 100644 index 0000000000..e6823724a7 --- /dev/null +++ b/src/packages/utils/src/utils/buffer-to-key.ts @@ -0,0 +1,295 @@ +let stringify: (buffer: Buffer, start: number, end: number) => string; + +if (typeof (Buffer.prototype as any).latin1Slice === "function") { + stringify = (buffer: Buffer, start: number, end: number) => { + // this is just `buffer.toString("hex")`, but it skips a bunch of checks + // that don't apply because our `start` and `end` just can't be out of + // bounds. + return (buffer as any).hexSlice(start, end); + }; +} else { + stringify = (buffer: Buffer, start: number, end: number) => { + return buffer.slice(start, end).toString("hex"); + }; +} + +/** + * Trims leading 0s from a buffer and returns a key representing the buffer's + * trimmed value (`Buffer.from([0, 0, 12, 0])` => `1200`). + * + * @param buffer + */ +export function bufferToMinHexKey(buffer: Buffer) { + for (let i = 0, length = buffer.byteLength; i < length; i++) { + const value = buffer[i]; + // once we find a non-zero value take the rest of the buffer as the key + if (value !== 0) { + if (i + 1 === length) { + // use a lookup table for single character lookups + return HEX_MAP[value]; + } else { + return stringify(buffer, i, length); + } + } + } + return ""; +} + +const HEX_MAP = [ + "00", + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "0a", + "0b", + "0c", + "0d", + "0e", + "0f", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "1a", + "1b", + "1c", + "1d", + "1e", + "1f", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "2a", + "2b", + "2c", + "2d", + "2e", + "2f", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "3a", + "3b", + "3c", + "3d", + "3e", + "3f", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "4a", + "4b", + "4c", + "4d", + "4e", + "4f", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "5a", + "5b", + "5c", + "5d", + "5e", + "5f", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "6a", + "6b", + "6c", + "6d", + "6e", + "6f", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "7a", + "7b", + "7c", + "7d", + "7e", + "7f", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "89", + "8a", + "8b", + "8c", + "8d", + "8e", + "8f", + "90", + "91", + "92", + "93", + "94", + "95", + "96", + "97", + "98", + "99", + "9a", + "9b", + "9c", + "9d", + "9e", + "9f", + "a0", + "a1", + "a2", + "a3", + "a4", + "a5", + "a6", + "a7", + "a8", + "a9", + "aa", + "ab", + "ac", + "ad", + "ae", + "af", + "b0", + "b1", + "b2", + "b3", + "b4", + "b5", + "b6", + "b7", + "b8", + "b9", + "ba", + "bb", + "bc", + "bd", + "be", + "bf", + "c0", + "c1", + "c2", + "c3", + "c4", + "c5", + "c6", + "c7", + "c8", + "c9", + "ca", + "cb", + "cc", + "cd", + "ce", + "cf", + "d0", + "d1", + "d2", + "d3", + "d4", + "d5", + "d6", + "d7", + "d8", + "d9", + "da", + "db", + "dc", + "dd", + "de", + "df", + "e0", + "e1", + "e2", + "e3", + "e4", + "e5", + "e6", + "e7", + "e8", + "e9", + "ea", + "eb", + "ec", + "ed", + "ee", + "ef", + "f0", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "fa", + "fb", + "fc", + "fd", + "fe", + "ff" +]; diff --git a/src/packages/utils/src/utils/index.ts b/src/packages/utils/src/utils/index.ts index e2ab466a84..285ebfef0e 100644 --- a/src/packages/utils/src/utils/index.ts +++ b/src/packages/utils/src/utils/index.ts @@ -6,3 +6,4 @@ export * from "./unref"; export * from "./has-own"; export * from "./uint-to-buffer"; export * from "./constants"; +export * from "./buffer-to-key";