Skip to content
This repository has been archived by the owner on Sep 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #21 from huff-language/stage
Browse files Browse the repository at this point in the history
🚀 Stage
  • Loading branch information
Maddiaa0 authored Nov 28, 2022
2 parents bd0585d + afa2be6 commit e3ca8ae
Show file tree
Hide file tree
Showing 8 changed files with 1,001 additions and 779 deletions.
37 changes: 22 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,77 @@

**NOTE: Debugger requires path installations of [huffc](https://github.com/huff-language/huff-rs) and [hevm](https://github.com/dapphub/dapptools/tree/master/src/hevm) from [dapptools](https://github.com/dapphub/dapptools) before use.**

Features at a glances:
> A mirror version of this repository can be found on [open-vsx](https://open-vsx.org/extension/huff-language/huff-language)
Features at a glances:

- Advanced language support
- Syntax Highlighting
- Hover cards detailing gas usage
- Code Generation

## Hevm Powered Debugging
Step through execution of your contracts from external calls, or even individual macros. Manipulate the stack inputs when your macro starts and even override storage slots. Star power your productivity.

Step through execution of your contracts from external calls, or even individual macros. Manipulate the stack inputs when your macro starts and even override storage slots. Star power your productivity.
This is particularly useful for debugging your macros individually. For more e2e debugging, the foundry debugger is an excellent tool.

**How it works:**
Open up any .huff file and press `Load Interface` to prepare external functions, enter in your calldata parameters and hit debug to get started.
Open up any .huff file and press `Load Interface` to prepare external functions, enter in your calldata parameters and hit debug to get started.

![function debugging window](./resources/Function_selector.png)

The example above is for an erc721 mint function, entering from the function debugger will allow you to step through execution from the MAIN entrypoint. To select another function, click the drop down and select another function! The drop down will be populated with functions that are defined at the top of the file that you are currently debugging.

**Notice - *Relevant to WSL users***
To get the extension to run in wsl there is a workaround that you will have to make. Unfortunately, vscode extensions do not run in the same environment as your wsl shell so do not have access to the same path (even when mounted with wsl-remote). If the extension detects that you are working in a remote code space it will attempt to execute huffc and hevm within the wsl shell. The wsl api does not run the ~/.bashrc script (the file which huffup adds to the path), therefore to use this extension in wsl you will have to copy the following into your ~/.profile.
**Notice - _Relevant to WSL users_**
To get the extension to run in wsl there is a workaround that you will have to make. Unfortunately, vscode extensions do not run in the same environment as your wsl shell so do not have access to the same path (even when mounted with wsl-remote). If the extension detects that you are working in a remote code space it will attempt to execute huffc and hevm within the wsl shell. The wsl api does not run the ~/.bashrc script (the file which huffup adds to the path), therefore to use this extension in wsl you will have to copy the following into your ~/.profile.

```
if [ -d "$HOME/.huff/bin" ]; then
PATH="$HOME/.huff/bin:$PATH"
fi
```
We will look to address this limitation in further releases of huffup.

We will look to address this limitation in further releases of huffup.

### Macro Debugging

Macro debugging is the tool's most useful feature. Clicking `Load Macros` will load the macros of the currently open file.
If the macro has no `takes` arguments then the window will look like this:


![function debugging window](./resources/macro_window.png)


You can choose to add arbitrary hex `calldata` to the debugger instance by selecting the `With Calldata` checkbox.
You can choose to add arbitrary hex `calldata` to the debugger instance by selecting the `With Calldata` checkbox.

#### Defining `takes` parameters:
![function debugging window](./resources/macro_with_takes_params.png)

![function debugging window](./resources/macro_with_takes_params.png)

If the selected macro has `takes(n)` then n windows will appear to allow setting of the stack before execution.

#### Overriding storage:
![function debugging window](./resources/storage_overrides.png)

![function debugging window](./resources/storage_overrides.png)

`Storage Overrides` allows you to test your macro with arbitrary overrides for specified storage slots! To use, select the `Storage Overrides` checkbox, then add as many slots as you require. Upon execution, the slots will be set to the values that you have defined. You can quickly toggle this feature off by unselecting the `Storage Overrides` checkbox.

## Code Generation

**Switch table generation**
Generate the MAIN macro switch table with jumps from just the interface definition. No more visiting keccak online or using `cast sig xxx` to copy function selectors into your code. Just write the interface and let us handle the rest.

Usage:
`commandPallete -> Huff: Generate MAIN() Switch Table`
`commandPallete -> Huff: Generate MAIN() Switch Table`

**Event signature generation**
Similarly to switch table generation above. Forget about calculating the keccak of you event topics yourself.
Similarly to switch table generation above. Forget about calculating the keccak of you event topics yourself.

Usage:
`commandPallete -> Huff: Generate interface signature from interface`
`commandPallete -> Huff: Generate interface signature from interface`

## Hover Cards
Hovering the cursor over an opcode will explain what operation it performs, the minimum amount of gas it uses, as well as a link to evm.codes to read more about it.

Hovering the cursor over an opcode will explain what operation it performs, the minimum amount of gas it uses, as well as a link to evm.codes to read more about it.

## Currently not supported

[] Constructor Arguments.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "huff-language",
"version": "0.0.30",
"version": "0.0.32",
"displayName": "Huff",
"preview": true,
"icon": "resources/huff.png",
Expand Down
135 changes: 83 additions & 52 deletions src/features/debugger/function/debugger.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
const fs = require("fs");
const createKeccakHash = require('keccak');
const createKeccakHash = require("keccak");
const path = require("path");
const vscode = require("vscode");

// TODO: use a slimmer abicoder
const { AbiCoder } = require("@ethersproject/abi");
const { hevmConfig } = require("../../../options");
const { deployContract, runInUserTerminal, writeHevmCommand, resetStateRepo, registerError, compileFromFile, checkInstallations, purgeCache, craftTerminalCommand } = require("../debuggerUtils");
const {
deployContract,
runInUserTerminal,
writeHevmCommand,
resetStateRepo,
registerError,
compileFromFile,
checkInstallations,
purgeCache,
craftTerminalCommand,
} = require("../debuggerUtils");
const { isWsl, wslMountedDriveRegex } = require("../../../settings");


/**Start function debugger
*
/**Start function debugger
*
* @param {String} sourceDirectory The current working directory of selected files workspace
* @param {String} currentFile The path to the currently selected file
* @param {String} functionSelector The 4byte function selector of the transaction being debugged
* @param {Array<Array<String>>} argsArray Each arg is provided in the format [type, value] so that they can easily be parsed with abi encoder
* @param {Object} options Options - not explicitly defined
* @param {Object} options Options - not explicitly defined
*/
async function startDebugger(sourceDirectory, currentFile, imports, functionSelector, argsArray, options = { state: true }) {
async function startDebugger(
sourceDirectory,
currentFile,
imports,
functionSelector,
argsArray,
options = {}
) {
try {
if (!(await checkInstallations())) return;

Expand All @@ -33,14 +49,15 @@ async function startDebugger(sourceDirectory, currentFile, imports, functionSele
// Create deterministic deployment address for each contract for the deployed contract
const config = {
...hevmConfig,
...options,
hevmContractAddress: createKeccakHash("keccak256")
.update(Buffer.from(currentFile))
.digest("hex")
.toString("hex")
.slice(0, 42),
stateChecked: true,
mountedDrive,
}
};

// Get calldata
const calldata = await encodeCalldata(functionSelector, argsArray);
Expand All @@ -49,112 +66,126 @@ async function startDebugger(sourceDirectory, currentFile, imports, functionSele
const compilableFile = flattenFile(cwd, currentFile, imports);

// Compile binary using locally installed compiler - in the future this will be replaced with a wasm compiler
const bytecode = compileFromFile(compilableFile, config.tempMacroFilename, cwd);
const bytecode = compileFromFile(
compilableFile,
config.tempMacroFilename,
cwd
);

// Get runtime bytecode and run constructor logic
const runtimeBytecode = deployContract(bytecode, config, cwd);

runDebugger(runtimeBytecode, calldata, options, config, cwd)
}
catch (e) {
registerError(e, "Compilation failed, please contact the team in the huff discord");
return null
runDebugger(runtimeBytecode, calldata, options, config, cwd);
} catch (e) {
registerError(
e,
"Compilation failed, please contact the team in the huff discord"
);
return null;
}
}

/**Flatten File
*
* @param {String} cwd
* @param {String} currentFile
* @param {Array<String>} imports declared file imports at the top of the current file
* @returns
*
* @param {String} cwd
* @param {String} currentFile
* @param {Array<String>} imports declared file imports at the top of the current file
* @returns
*/
function flattenFile(cwd, currentFile, imports) {
// Get relative path of files
const dirPath = currentFile.split("/").slice(0, -1).join("/")
const dirPath = currentFile.split("/").slice(0, -1).join("/");

// Get absolute paths
const paths = imports.map(importPath => path.join(`${cwd}/${dirPath}`, importPath.replace(/#include\s?"/, "").replace('"', "")));
const paths = imports.map((importPath) =>
path.join(
`${cwd}/${dirPath}`,
importPath.replace(/#include\s?"/, "").replace('"', "")
)
);

// Read file contents and remove other instances of main
// main regex
const mainRegex = /#define\s+macro\s+MAIN\s?\((?<args>[^\)]*)\)\s?=\s?takes\s?\((?<takes>[\d])\)\s?returns\s?\((?<returns>[\d])\)\s?{(?<body>[\s\S]*?(?=}))}/gsm
// main regex
const mainRegex =
/#define\s+macro\s+MAIN\s?\((?<args>[^\)]*)\)\s?=\s?takes\s?\((?<takes>[\d])\)\s?returns\s?\((?<returns>[\d])\)\s?{(?<body>[\s\S]*?(?=}))}/gms;
const files = [
fs.readFileSync(cwd + "/" + currentFile).toString(),
...paths.map(path => {
return fs.readFileSync(path)
.toString().replace(mainRegex, "")
})
...paths.map((path) => {
return fs.readFileSync(path).toString().replace(mainRegex, "");
}),
];

// Flatten and remove imports
return `${files.join("\n")}`.replace(/#include\s".*"/gm, "");
}


/**Run debugger
*
*
* Craft hevm command and run it in the user terminal
*
* @param {String} bytecode
* @param {String} calldata
* @param {Object} flags
* @param {Object} config
* @param {String} cwd
*
* @param {String} bytecode
* @param {String} calldata
* @param {Object} flags
* @param {Object} config
* @param {String} cwd
*/
function runDebugger(bytecode, calldata, flags, config, cwd) {
console.log("Entering debugger...")
console.log("Entering debugger...");

// Hevm Command
const hevmCommand = `hevm exec \
--code ${bytecode} \
--address ${config.hevmContractAddress} \
--caller ${config.hevmCaller} \
--gas 0xffffffff \
--state ${((config.mountedDrive) ? ("/mnt/" + config.mountedDrive) : "") + cwd + "/" + config.statePath} \
--state ${
(config.mountedDrive ? "/mnt/" + config.mountedDrive : "") +
cwd +
"/" +
config.statePath
} \
--debug \
${calldata ? "--calldata " + calldata : ""}`
${config.callValueChecked ? "--value " + config.callValue : ""} \
${calldata ? "--calldata " + calldata : ""}`;

// command is cached into a file as execSync has a limit on the command size that it can execute
writeHevmCommand(hevmCommand, config.tempHevmCommandFilename, cwd);
const terminalCommand = craftTerminalCommand(cwd, config)
const terminalCommand = craftTerminalCommand(cwd, config);
runInUserTerminal(terminalCommand);
}


/**Prepare Debug Transaction
*
*
* Use abi encoder to encode transaction data
*
* @param {String} functionSelector
* @param {Array<Array<String>} argsObject
* @returns
*
* @param {String} functionSelector
* @param {Array<Array<String>} argsObject
* @returns
*/
async function encodeCalldata(functionSelector, argsObject) {
console.log("Preparing debugger calldata...")
console.log("Preparing debugger calldata...");
try {
if (argsObject.length == 0) return `0x${functionSelector[0]}`;

// TODO: error handle with user prompts
const abiEncoder = new AbiCoder()
const abiEncoder = new AbiCoder();

// create interface readable by the abi encoder
let type = [];
let value = [];
argsObject.forEach(arg => {
argsObject.forEach((arg) => {
type.push(arg[0]);
value.push(arg[1]);
});

const encoded = abiEncoder.encode(type, value);

return `0x${functionSelector[0]}${encoded.slice(2, encoded.length)}`

return `0x${functionSelector[0]}${encoded.slice(2, encoded.length)}`;
} catch (e) {
registerError(e, `Compilation failed\nSee\n${e}`);
}
}

module.exports = {
startDebugger
}
startDebugger,
};
Loading

0 comments on commit e3ca8ae

Please sign in to comment.