diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..cac8f6d2 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/node_modules +.huff \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..762e6578 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,51 @@ +module.exports = { + "extends": "airbnb-base", + "env": { + "mocha": true, + "node": true, + "browser": true + }, + "rules": { + "strict": 0, + "arrow-body-style": 0, + "comma-dangle": [ + "error", + { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" + } + ], + "import/no-extraneous-dependencies": 0, + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "linebreak-style": 0, + "no-console": 0, + "no-underscore-dangle": [ + "error", + { + "allow": [ + "_id" + ] + } + ], + "prefer-template": 0, + "max-len": [ + "warn", + 130, + { + "ignoreComments": true + }, + { + "ignoreTrailingComments": true + } + ] + } +}; diff --git a/.gitignore b/.gitignore index 1d409657..1abd12b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -.nyc_output \ No newline at end of file +.nyc_output +yarn.lock diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..7273ef81 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +node_modules +example +testData +*.spec.js +.eslintignore +.eslintrc.js diff --git a/README.md b/README.md index 11693c65..6fa37e5c 100644 --- a/README.md +++ b/README.md @@ -182,3 +182,8 @@ console.log('macro return data = ', returnValue); console.log('output stack state = ', stack); console.log('output memory state = ', memory); ``` + +### **Testing** + +Tests can be run with `yarn test`. Example contracts, such as the ERC20 implementation, can be tested with `yarn exampletest`. + diff --git a/example/erc20/README.md b/example/erc20/README.md new file mode 100644 index 00000000..2d4be97a --- /dev/null +++ b/example/erc20/README.md @@ -0,0 +1,17 @@ +## Mintable ERC20 implementation in Huff + +This is an ERC20 contract implemented in Huff to demonstrate the language's (very basic) syntax. It consists of three files: + +| filename | description | +| -------------------- | --------------------------------------------------------------------------------------------------------------- | +| `erc20.huff` | Huff source file | +| `erc20_interface.js` | JavaScript methods allowing the user to easily run Huff macros corresponding to ERC20 methods (e.g. `transfer`) | +| `erc20.spec.js` | Unit tests -- the Huff code really works! | + +(Example unit tests can be run from `/huff/` with `yarn exampletest`.) + +The process by which the Huff code was created is documented in a series of blog posts on Medium: +1. [About Huff, constructor, and function selector](https://medium.com/aztec-protocol/from-zero-to-nowhere-smart-contract-programming-in-huff-1-2-ba2b6de7fa83) +2. [Events, mappings, and `transfer`](https://medium.com/aztec-protocol/from-zero-to-nowhere-smart-contract-programming-in-huff-2-3-5438ef7e5beb) +3. [`totalSupply`, `balanceOf`, and `mint`](https://medium.com/aztec-protocol/from-zero-to-nowhere-smart-contract-programming-in-huff-3-4-6b347e23d66e) +4. [Nested mappings, `allowance`, `approve`, and `transferFrom`](https://medium.com/aztec-protocol/from-zero-to-nowhere-smart-contract-programming-in-huff-4-4-9e6c34648992) diff --git a/example/erc20/erc20.huff b/example/erc20/erc20.huff new file mode 100644 index 00000000..e7a74033 --- /dev/null +++ b/example/erc20/erc20.huff @@ -0,0 +1,270 @@ +#define macro BALANCE_LOCATION = takes(0) returns(1) { + 0x00 // do not change! +} + +#define macro OWNER_LOCATION = takes(0) returns(1) { + 0x01 +} + +#define macro TOTAL_SUPPLY_LOCATION = takes(0) returns(1) { + 0x02 +} + +#define macro ALLOWANCE_LOCATION = takes(0) returns(1) { + 0x03 +} + +#define macro ADDRESS_MASK = takes(1) returns(1) { + 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff + and +} + +#define macro TRANSFER_EVENT_SIGNATURE = takes(0) returns(1) { + 0xDDF252AD1BE2C89B69C2B068FC378DAA952BA7F163C4A11628F55A4DF523B3EF +} + +#define macro APPROVAL_EVENT_SIGNATURE = takes(0) returns(1) { + 0x8C5BE1E5EBEC7D5BD14F71427D1E84F3DD0314C0F7B2291E5B200AC8C7C3B925 +} + +template +#define macro UTILS__NOT_PAYABLE = takes(0) returns(0) { + callvalue jumpi +} + +#define macro UTILS__ONLY_OWNER = takes(0) returns(0) { + OWNER_LOCATION() sload caller eq is_owner jumpi + 0x00 0x00 revert + is_owner: +} + +#define macro ERC20 = takes(0) returns(0) { + caller OWNER_LOCATION() sstore +} + +template +#define macro ERC20__FUNCTION_SIGNATURE = takes(0) returns(0) { + 0x00 calldataload 224 shr // function signature + dup1 0x095ea7b3 eq jumpi + dup1 0x18160ddd eq jumpi + dup1 0x23b872dd eq jumpi + dup1 0x40c10f19 eq jumpi + dup1 0x70a08231 eq jumpi + dup1 0xa9059cbb eq jumpi + dup1 0xdd62ed3e eq jumpi + UTILS__NOT_PAYABLE() + 0x00 0x00 return +} + +#define macro ERC20__TRANSFER_INIT = takes(0) returns(6) { + 0x04 calldataload ADDRESS_MASK() + caller + TRANSFER_EVENT_SIGNATURE() + 0x20 + 0x00 + 0x24 calldataload + // value 0x00 0x20 signature from to +} + +#define macro ERC20__TRANSFER_GIVE_TO = takes(6) returns(7) { + // value 0x00 0x20 signature from to + dup6 0x00 mstore + 0x40 0x00 sha3 + // key(balances[to]) value 0x00 0x20 signature from to + dup1 sload + // balances[to] key value 0x00 0x20 signature from to + dup3 // v b k v 0x00 0x20 sig f t + add // v+b k v 0x00 0x20 sig f t + dup1 // v+b v+b k v 0x00 0x20 sig f t + dup4 // v v+b v+b k v 0x00 0x20 sig f t + gt // v>v+b v+b k v 0x00 0x20 sig f t + swap2 // k v+b v>v+b v 0x00 0x20 sig f t + sstore // v>v+b v 0x00 0x20 sig f t +} + +template +#define macro ERC20__TRANSFER_TAKE_FROM = takes(7) returns(8) { + // error_code value 0x00 0x20 signature from to + 0x00 mstore + 0x40 0x00 sha3 + // key(balances[from]) error_code value 0x00 0x20 signature from to + dup1 sload // balances[from] key error value 0x00 0x20 sig f t + // b k e1 v 0 2 s f t + dup4 dup2 // b v b k e1 v 0 2 s f t + sub dup5 // v (b-v) b k e1 v 0 2 s f t + swap3 // k (b-v) b v e1 v 0 2 s f t + sstore // b v e1 v 0 2 s f t + lt // error2 error1 value 0x00 0x20 signature from to +} + +template +#define macro ERC20__TRANSFER = takes(0) returns(0) { + ERC20__TRANSFER_INIT() + ERC20__TRANSFER_GIVE_TO() + ERC20__TRANSFER_TAKE_FROM() + // error2 error1 value 0x00 0x20 signature from to + callvalue or or jumpi + // value 0x00 0x20 signature from to + 0x00 mstore // value must be stored at 0x00 + log3 + 0x01 0x00 mstore + 0x20 0x00 return +} + +#define macro ERC20__TRANSFER_FROM_INIT = takes(0) returns(6) { + 0x24 calldataload ADDRESS_MASK() // stack: to + 0x04 calldataload ADDRESS_MASK() // stack: from to + TRANSFER_EVENT_SIGNATURE() + 0x20 + 0x00 + 0x44 calldataload + // stacK: value 0x00 0x20 signature from to +} + +#define macro ERC20__TRANSFER_SUB_ALLOWANCE = takes(8) returns (9) { + // stack: error2 error1 value 0x00 0x20 signature from to + dup7 0x00 mstore + ALLOWANCE_LOCATION() 0x20 mstore + 0x40 0x00 sha3 + 0x20 mstore + caller 0x00 mstore + 0x40 0x00 sha3 + // stack: key(allowances[from][msg.sender]) error2 error1 value 0x00 0x20 signature from to + dup1 sload // allowance key e2 e1 v 0x00 0x20 s f t + dup5 dup2 // a v a k e2 e1 v 0 2 s f t + sub dup6 // v a-v a k e2 e1 v 0 2 s f t + swap3 sstore // a v e2 e1 v 0 2 s f t + lt // stack: error3 error2 error1 value 0x00 0x20 signature from to +} + +template +#define macro ERC20__TRANSFER_FROM = takes(0) returns(0) { + ERC20__TRANSFER_FROM_INIT() + ERC20__TRANSFER_GIVE_TO() + ERC20__TRANSFER_TAKE_FROM() + ERC20__TRANSFER_SUB_ALLOWANCE() + // error3 error2 error1 value 0x00 0x20 signature from to + callvalue or or or jumpi + // value 0x00 0x20 signature from to + 0x00 mstore + log3 + 0x01 0x00 mstore + 0x20 0x00 return +} + +template +#define macro ERC20__BALANCE_OF = takes(0) returns(0) { + UTILS__NOT_PAYABLE() + 0x04 calldataload ADDRESS_MASK() + 0x00 mstore + 0x40 0x00 sha3 // stacK: key(balances[owner]) + sload // stack: balances[owner] + 0x00 mstore + 0x20 0x00 return +} + +template +#define macro ERC20__ALLOWANCE = takes(0) returns(0) { + UTILS__NOT_PAYABLE() + 0x04 calldataload ADDRESS_MASK() + 0x00 mstore + ALLOWANCE_LOCATION() 0x20 mstore + 0x40 0x00 sha3 + // stack: key(allowances[owner]) + 0x20 mstore + 0x24 calldataload ADDRESS_MASK() + 0x00 mstore + 0x40 0x00 sha3 + // stack: key(allowances[owner][spender]) + sload + 0x00 mstore + 0x20 0x00 return +} + +template +#define macro ERC20__APPROVE = takes(0) returns(0) { + UTILS__NOT_PAYABLE() + 0x04 calldataload ADDRESS_MASK() + caller + APPROVAL_EVENT_SIGNATURE() + 0x20 + 0x00 + // stack: 0x00 0x20 signature msg.sender spender + 0x24 calldataload // get value + dup1 + // stack: value value 0x00 0x20 signature msg.sender spender + caller 0x00 mstore + ALLOWANCE_LOCATION() 0x20 mstore + 0x40 0x00 sha3 + 0x20 mstore + dup7 0x00 mstore + 0x40 0x00 sha3 + // stack: key(allowances[msg.sender][spender]) value value 0x00 0x20 signature msg.sender spender + sstore + 0x00 mstore + // stack: 0x00 0x20 signature msg.sender spender + log3 + 0x01 0x00 mstore + 0x20 0x00 return +} + +template +#define macro ERC20__TOTAL_SUPPLY = takes(0) returns(0) { + UTILS__NOT_PAYABLE() + TOTAL_SUPPLY_LOCATION() sload + 0x00 mstore + 0x20 0x00 return +} + +template +#define macro ERC20__MINT = takes(0) returns(0) { + UTILS__ONLY_OWNER() + 0x04 calldataload ADDRESS_MASK() + 0 TRANSFER_EVENT_SIGNATURE() 0x20 0x00 + 0x24 calldataload + // stack: value 0x00 0x20 signature from to + ERC20__TRANSFER_GIVE_TO() + // stack: error1 value 0x00 0x20 signature from to + dup2 dup1 + // stack: value value 0x00 0x20 signature from to + TOTAL_SUPPLY_LOCATION() sload add dup1 TOTAL_SUPPLY_LOCATION() sstore + lt + // stack: error2 error1 0x00 0x20 signature from to + callvalue or or jumpi + log3 + 0x01 0x00 mstore + 0x20 0x00 return +} + +#define macro ERC20__MAIN = takes(0) returns(0) { + + ERC20__FUNCTION_SIGNATURE< + transfer, + transfer_from, + balance_of, + allowance, + approve, + total_supply, + mint, + throw_error + >() + + transfer: + ERC20__TRANSFER() + transfer_from: + ERC20__TRANSFER_FROM() + balance_of: + ERC20__BALANCE_OF() + allowance: + ERC20__ALLOWANCE() + approve: + ERC20__APPROVE() + total_supply: + ERC20__TOTAL_SUPPLY() + mint: + ERC20__MINT() + + throw_error: + 0x00 0x00 revert + +} diff --git a/example/erc20/erc20.spec.js b/example/erc20/erc20.spec.js new file mode 100644 index 00000000..28817e9d --- /dev/null +++ b/example/erc20/erc20.spec.js @@ -0,0 +1,98 @@ +const chai = require('chai'); + +const { expect } = chai; +const BN = require('bn.js'); +const crypto = require('crypto'); +const erc20 = require('./erc20_interface.js'); + +describe('ERC20 Huff contract', () => { + const owner = new BN(crypto.randomBytes(20), 16); + const user1 = new BN(crypto.randomBytes(20), 16); + const user2 = new BN(crypto.randomBytes(20), 16); + const user3 = new BN(crypto.randomBytes(20), 16); + const user4 = new BN(crypto.randomBytes(20), 16); + const value1 = new BN(12000); + const value2 = new BN(7300); + const value3 = new BN(6500); + const value4 = new BN(5003); + const value5 = new BN(4720); + const value6 = new BN(4302); + + before(async () => { + await erc20.init(owner); + }); + + it('balances and totalSupply initialised to 0', async () => { + const balance1 = await erc20.getBalanceOf(owner); + expect(balance1.eq(new BN(0))).to.equal(true); + const balance2 = await erc20.getBalanceOf(user1); + expect(balance2.eq(new BN(0))).to.equal(true); + const totalSupply = await erc20.getTotalSupply(); + expect(totalSupply.eq(new BN(0))).to.equal(true); + }); + + it('mint tokens to owner', async () => { + await erc20.mint(owner, owner, value1); + const ownerBalance = await erc20.getBalanceOf(owner); + expect(ownerBalance.eq(value1)).to.equal(true); + const totalSupply = await erc20.getTotalSupply(); + expect(totalSupply.eq(value1)).to.equal(true); + }); + + it('mint tokens to non-owner', async () => { + await erc20.mint(owner, user1, value2); + const balance = await erc20.getBalanceOf(user1); + expect(balance.eq(value2)).to.equal(true); + const totalSupply = await erc20.getTotalSupply(); + expect(totalSupply.eq(value1.add(value2))).to.equal(true); + }); + + it('transfer tokens', async () => { + await erc20.transfer(user1, user2, value3); + const balance1 = await erc20.getBalanceOf(user1); + const balance2 = await erc20.getBalanceOf(user2); + expect(balance1.eq(value2.sub(value3))).to.equal(true); + expect(balance2.eq(value3)).to.equal(true); + }); + + it('allowances initialised to 0', async () => { + const allowance1 = await erc20.getAllowance(owner, user2); + expect(allowance1.eq(new BN(0))).to.equal(true); + const allowance2 = await erc20.getAllowance(user1, user3); + expect(allowance2.eq(new BN(0))).to.equal(true); + }); + + it('approve tokens', async () => { + await erc20.approve(user2, user3, value4); + const allowance = await erc20.getAllowance(user2, user3); + expect(allowance.eq(value4)).to.equal(true); + }); + + it('transferFrom', async () => { + await erc20.transferFrom(user3, user2, user4, value5); + const allowance = await erc20.getAllowance(user2, user3); + expect(allowance.eq(value4.sub(value5))).to.equal(true); + const balance2 = await (erc20.getBalanceOf(user2)); + const balance3 = await (erc20.getBalanceOf(user3)); + const balance4 = await (erc20.getBalanceOf(user4)); + expect(balance2.eq(value3.sub(value5))).to.equal(true); + expect(balance3.eq(new BN(0))).to.equal(true); + expect(balance4.eq(value5)).to.equal(true); + }); + + it('transfer again', async () => { + await erc20.transfer(user4, user1, value6); + const balance1 = await (erc20.getBalanceOf(user1)); + const balance4 = await (erc20.getBalanceOf(user4)); + expect(balance1.eq(value2.sub(value3).add(value6))).to.equal(true); + expect(balance4.eq(value5.sub(value6))).to.equal(true); + }); + + it('mint again', async () => { + await erc20.mint(owner, user2, value4); + const balance2 = await (erc20.getBalanceOf(user2)); + expect(balance2.eq(value3.sub(value5).add(value4))); + const totalSupply = await erc20.getTotalSupply(); + expect(totalSupply.eq(value1.add(value2).add(value4))).to.equal(true); + }); +}); diff --git a/example/erc20/erc20_interface.js b/example/erc20/erc20_interface.js new file mode 100644 index 00000000..c8f5b7d3 --- /dev/null +++ b/example/erc20/erc20_interface.js @@ -0,0 +1,102 @@ +const BN = require('bn.js'); +const { Runtime, getNewVM } = require('../../src/runtime.js'); + +const main = new Runtime('erc20.huff', __dirname); +const vm = getNewVM(); + +const logSteps = false; +const logGas = false; + +if (logSteps) { + vm.on('step', (data) => { + console.log(`Opcode: ${data.opcode.name}\tStack: ${data.stack}`); + }); +} + +async function init(caller) { + const initialMemory = []; const inputStack = []; const calldata = []; const + callvalue = 0; + const callerAddr = caller; + await main(vm, 'ERC20', inputStack, initialMemory, calldata, callvalue, callerAddr); +} + +async function getTotalSupply() { + const calldata = [{ index: 0, value: 0x18160ddd, len: 4 }]; + const initialMemory = []; const inputStack = []; const + callvalue = 0; + const callerAddr = 0; // callerAddr doesn't matter + const data = await main(vm, 'ERC20__MAIN', inputStack, initialMemory, calldata, callvalue, callerAddr); + if (logGas) { console.log(`Gas used by totalSupply(): ${data.gas}`); } + return new BN(data.returnValue.toString('hex'), 16); +} + +async function getBalanceOf(owner) { + const calldata = [{ index: 0, value: 0x70a08231, len: 4 }, { index: 4, value: owner, len: 32 }]; + const initialMemory = []; const inputStack = []; const + callvalue = 0; + const callerAddr = 0; // callerAddr doesn't matter + const data = await main(vm, 'ERC20__MAIN', inputStack, initialMemory, calldata, callvalue, callerAddr); + if (logGas) { console.log(`Gas used by balanceOf(...): ${data.gas}`); } + return new BN(data.returnValue.toString('hex'), 16); +} + +async function transfer(caller, to, value) { + const calldata = [{ index: 0, value: 0xa9059cbb, len: 4 }, + { index: 4, value: to, len: 32 }, + { index: 36, value, len: 32 }]; + const initialMemory = []; const inputStack = []; const + callvalue = 0; + const callerAddr = caller; + const data = await main(vm, 'ERC20__MAIN', inputStack, initialMemory, calldata, callvalue, callerAddr); + if (logGas) { console.log(`Gas used by transfer(...): ${data.gas}`); } +} + +async function mint(caller, to, value) { + const calldata = [{ index: 0, value: 0x40c10f19, len: 4 }, + { index: 4, value: to, len: 32 }, + { index: 36, value, len: 32 }]; + const initialMemory = []; const inputStack = []; const + callvalue = 0; + const callerAddr = caller; + const data = await main(vm, 'ERC20__MAIN', inputStack, initialMemory, calldata, callvalue, callerAddr); + if (logGas) { console.log(`Gas used by mint(...): ${data.gas}`); } +} + +async function getAllowance(owner, spender) { + const calldata = [{ index: 0, value: 0xdd62ed3e, len: 4 }, + { index: 4, value: owner, len: 32 }, + { index: 36, value: spender, len: 32 }]; + const initialMemory = []; const inputStack = []; const + callvalue = 0; + const callerAddr = 0; // callerAddr doesn't matter + const data = await main(vm, 'ERC20__MAIN', inputStack, initialMemory, calldata, callvalue, callerAddr); + if (logGas) { console.log(`Gas used by allowance(...): ${data.gas}`); } + return new BN(data.returnValue.toString('hex'), 16); +} + +async function approve(caller, spender, amount) { + const calldata = [{ index: 0, value: 0x095ea7b3, len: 4 }, + { index: 4, value: spender, len: 32 }, + { index: 36, value: amount, len: 32 }]; + const initialMemory = []; const inputStack = []; const + callvalue = 0; + const callerAddr = caller; + const data = await main(vm, 'ERC20__MAIN', inputStack, initialMemory, calldata, callvalue, callerAddr); + if (logGas) { console.log(`Gas used by approve(...): ${data.gas}`); } +} + +async function transferFrom(caller, owner, recipient, amount) { + const calldata = [{ index: 0, value: 0x23b872dd, len: 4 }, + { index: 4, value: owner, len: 32 }, + { index: 36, value: recipient, len: 32 }, + { index: 68, value: amount, len: 32 }]; + const initialMemory = []; const inputStack = []; const + callvalue = 0; + const callerAddr = caller; + const data = await main(vm, 'ERC20__MAIN', inputStack, initialMemory, calldata, callvalue, callerAddr); + if (logGas) { console.log(`Gas used by transferFrom(...): ${data.gas}`); } +} + +module.exports = { + init, getTotalSupply, getBalanceOf, transfer, mint, approve, getAllowance, transferFrom, +}; diff --git a/package.json b/package.json index b06a5104..02e53f16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aztec/huff", - "version": "0.1.0", + "version": "0.0.4", "author": "AZTEC", "description": "Compiler for Huff, a DSL for low-level Ethereum smart contract programming", "license": "LGPL-3.0", @@ -9,19 +9,8 @@ "/lib" ], "homepage": "https://github.com/AztecProtocol/AZTEC#readme", - "keywords": [ - "huff", - "ethereum", - "evm", - "smart", - "contracts" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/AztecProtocol/AZTEC.git" - }, "bugs": { - "url": "https://github.com/AztecProtocol/AZTEC/issues" + "url": "https://github.com/AztecProtocol/huff/issues" }, "dependencies": { "bn.js": "^4.11.8", @@ -38,12 +27,24 @@ "mocha": "^5.2.0", "sinon": "^7.2.3" }, - "bin": { - "huff": "./src/cli/index.js" + "engines": { + "node": ">=8.3" + }, + "keywords": [ + "blockchain", + "ethereum", + "evm", + "huff", + "programming-language", + "smart-contracts" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AztecProtocol/huff.git" }, "scripts": { - "build": "./node_modules/.bin/rimraf ./lib && mkdir lib && cp -r ./src/* ./lib", - "lint": "./node_modules/.bin/eslint --ignore-path ../../.eslintignore .", - "test": "./node_modules/.bin/mocha ./src/ --trace-warnings --exit --colors --recursive --reporter spec" + "lint": "eslint --ignore-path ./.eslintignore .", + "test": "mocha ./src/ --bail --colors --exit --recursive --reporter spec --trace-warnings", + "exampletest": "mocha ./example/ --bail --colors --exit --recursive --reporter spec --trace-warnings" } } diff --git a/src/compiler.js b/src/compiler.js index d8ba7540..ff67eee4 100644 --- a/src/compiler.js +++ b/src/compiler.js @@ -16,7 +16,9 @@ function Compiler(projectName, projectPath) { } const settings = JSON.parse(settingsString); - const { abi, name, file: entryFilename, entryMacro, constructor } = settings; + const { + abi, name, file: entryFilename, entryMacro, constructor, + } = settings; if (!entryMacro || !constructor || !entryFilename) { throw new Error(`could not find ${entryMacro}, ${constructor}, ${entryFilename}`); } diff --git a/src/inputMap/inputMap.spec.js b/src/inputMap/inputMap.spec.js index 94f83b53..bd667f79 100644 --- a/src/inputMap/inputMap.spec.js +++ b/src/inputMap/inputMap.spec.js @@ -52,6 +52,7 @@ describe('inputMap tests', () => { lineIndex, } = inputMap.getFileLine(39, map); expect(lineNumber).to.equal(5); + expect(filename).to.equal('foo'); expect(line).to.equal(' the la'); expect(lineIndex).to.equal(5); @@ -63,6 +64,7 @@ describe('inputMap tests', () => { lineIndex, } = inputMap.getFileLine(200, map)); expect(lineNumber).to.equal(5); + expect(filename).to.equal('bar'); expect(line).to.equal('from their zenith in the high Middle Ages.'); expect(lineIndex).to.equal(5); diff --git a/src/opcodes/opcodes.js b/src/opcodes/opcodes.js index 665b955d..3c053fe5 100644 --- a/src/opcodes/opcodes.js +++ b/src/opcodes/opcodes.js @@ -29,8 +29,6 @@ opcodes.reverseOpcodes = { '1b': 'SHL', '1c': 'SHR', '1d': 'SAR', - '1e': 'ROL', - '1f': 'ROR', '20': 'SHA3', '30': 'ADDRESS', '31': 'BALANCE', @@ -47,12 +45,15 @@ opcodes.reverseOpcodes = { '3c': 'EXTCODECOPY', '3d': 'RETURNDATASIZE', '3e': 'RETURNDATACOPY', + '3f': 'EXTCODEHASH', '40': 'BLOCKHASH', '41': 'COINBASE', '42': 'TIMESTAMP', '43': 'NUMBER', '44': 'DIFFICULTY', '45': 'GASLIMIT', + '46': 'CHAINID', + '47': 'SELFBALANCE', '50': 'POP', '51': 'MLOAD', '52': 'MSTORE', @@ -187,12 +188,15 @@ opcodes.opcodes = { extcodecopy: '3c', returndatasize: '3d', returndatacopy: '3e', + extcodehash: '3f', blockhash: '40', coinbase: '41', timestamp: '42', number: '43', difficulty: '44', gaslimit: '45', + chainid: '46', + selfbalance: '47', pop: '50', mload: '51', mstore: '52', @@ -286,9 +290,7 @@ opcodes.opcodes = { swap16: '9f', shl: '1b', shr: '1c', - sar: '1d', - rol: '1e', - ror: '1f', + sar: '1d' }; module.exports = opcodes; diff --git a/src/parser.js b/src/parser.js index 3b5587fa..2263630d 100644 --- a/src/parser.js +++ b/src/parser.js @@ -301,17 +301,20 @@ parser.processMacro = ( if (jumptable.table.jumps) { tableOffsets[jumptable.name] = tableOffset; tableOffset += jumptable.table.size; - tablecode = jumptable.table.jumps.map((jumplabel) => { - if (!result.jumpindices[jumplabel]) { - return ''; - } - const offset = result.jumpindices[jumplabel]; - const hex = formatEvenBytes(toHex(offset)); - if (!jumptable.table.compressed) { - return padNBytes(hex, 0x20); - } - return hex; - }).join(''); + tablecode = jumptable.table.jumps + .map((jumplabel) => { + if (!result.jumpindices[jumplabel]) { + return ''; + } + const offset = result.jumpindices[jumplabel]; + const hex = formatEvenBytes(toHex(offset)); + if (!jumptable.table.compressed) { + return padNBytes(hex, 0x20); + } else { + return padNBytes(hex, 0x02); + } + }) + .join(''); } else { tablecode = jumptable.table.table; tableOffsets[jumptable.name] = tableOffset; @@ -329,7 +332,7 @@ parser.processMacro = ( } const pre = bytecode.slice(0, (offset * 2) + 2); const post = bytecode.slice((offset * 2) + 6); - bytecode = `${pre}${formatEvenBytes(toHex(tableOffsets[tableInstance.label]))}${post}`; + bytecode = `${pre}${padNBytes(formatEvenBytes(toHex(tableOffsets[tableInstance.label])), 2)}${post}`; }); return { ...result, @@ -524,6 +527,7 @@ parser.processMacroInternal = ( sourcemap: [], }); + return { data, unmatchedJumps, diff --git a/src/parser.spec.js b/src/parser.spec.js index 1e8970a3..5134c384 100644 --- a/src/parser.spec.js +++ b/src/parser.spec.js @@ -217,6 +217,33 @@ describe('parser tests', () => { }); }); + describe('process packed jump table', () => { + const source = `#define jumptable__packed JUMP_TABLE { + lsb_0 + } + + #define macro PACKED_TABLE_TEST = takes(0) returns(0) { + __tablesize(JUMP_TABLE) __tablestart(JUMP_TABLE) + lsb_0: + }` + it(`processMacro will ensure packed jump table labels are 2 bytes`, () => { + const map = inputMap.createInputMap([ + { + filename: 'test', + data: source, + }, + ]); + const { macros, jumptables } = parser.parseTopLevel(source, 0, map); + const { JUMP_TABLE: { table: { jumps, size, compressed } } } = jumptables; + expect(compressed).to.be.true; + expect(jumps).to.deep.equal(['lsb_0']) + const output = parser.processMacro('PACKED_TABLE_TEST', 0, [], macros, map, jumptables); + const { jumpindices, bytecode } = output.data; + expect(jumpindices.lsb_0).to.equal(5); + expect(bytecode).to.equal('60026100065b000005'); + }) + }); + describe('parse top level', () => { let parseMacro; beforeEach(() => { diff --git a/src/runtime.js b/src/runtime.js index 2524c8ac..39287c17 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -6,14 +6,20 @@ const newParser = require('./parser'); const utils = require('./utils'); const { opcodes } = require('./opcodes/opcodes'); -// eslint-disable-next-line no-unused-vars -function toBytes32(input, padding = 'left') { // assumes hex format +function getNewVM() { + return new VM({ hardfork: 'constantinople' }); +} + +function toBytesN(input, len, padding = 'left') { + // assumes hex format let s = input; - if (s.length > 64) { - throw new Error(`string ${input} is more than 32 bytes long!`); + if (s.length > len * 2) { + throw new Error(`string ${input} is too long!`); } - while (s.length < 64) { - if (padding === 'left') { // left pad to hash a number. Right pad to hash a string + + while (s.length < len * 2) { + if (padding === 'left') { + // left pad to hash a number. Right pad to hash a string s = `0${s}`; } else { s = `${s}0`; @@ -22,6 +28,23 @@ function toBytes32(input, padding = 'left') { // assumes hex format return s; } +function processMemory(bnArray) { + let calldatalength = 0; + for (const { index, len } of bnArray) { + if (index + len > calldatalength) { + calldatalength = index + len; + } + } + const buffer = new Array(calldatalength).fill(0); + for (const { index, value, len } of bnArray) { + const hex = toBytesN(value.toString(16), len); + for (let i = 0; i < hex.length; i += 2) { + buffer[i / 2 + index] = parseInt(`${hex[i]}${hex[i + 1]}`, 16); + } + } + return buffer; +} + function getPushOp(hex) { const data = utils.formatEvenBytes(hex); const opcode = utils.toHex(95 + (data.length / 2)); @@ -43,53 +66,66 @@ function encodeStack(stack) { }, ''); } -function runCode(vm, bytecode, calldata = null, sourcemapOffset = 0, sourcemap = [], callvalue = 0, debug = false) { +function runCode(vm, bytecode, calldata, sourcemapOffset = 0, sourcemap = [], callvalue = 0, callerAddr = 0) { + if (calldata) { + for (const x of calldata) { + if (x.len === undefined) { + x.len = 32; // set len to 32 if undefined (for sake of backward compatibility) + } + } + } return new Promise((resolve, reject) => { - vm.runCode({ - code: Buffer.from(bytecode, 'hex'), - gasLimit: Buffer.from('ffffffff', 'hex'), - data: calldata, // ? processMemory(calldata) : null, - value: new BN(callvalue), - }, (err, results) => { - if (err) { - if (debug) { + vm.runCode( + { + code: Buffer.from(bytecode, 'hex'), + gasLimit: Buffer.from('ffffffff', 'hex'), + data: calldata ? processMemory(calldata) : null, + value: new BN(callvalue), + caller: callerAddr, + }, + (err, results) => { + if (err) { console.log(results.runState.programCounter); console.log(sourcemap[results.runState.programCounter - sourcemapOffset]); + return reject(err); } - return reject(err); + return resolve(results); } - return resolve(results); + ); }); - }); -} - -function Runtime(filename, path, debug = false) { - const { inputMap, macros, jumptables } = newParser.parseFile(filename, path); - return async function runMacro(macroName, stack = [], memory = [], calldata = null, callvalue = 0) { - const memoryCode = encodeMemory(memory); - const stackCode = encodeStack(stack); - const initCode = `${memoryCode}${stackCode}`; - const initGasEstimate = (memory.length * 9) + (stack.length * 3); - const offset = initCode.length / 2; - const { - data: { bytecode: macroCode, sourcemap }, - } = newParser.processMacro(macroName, offset, [], macros, inputMap, jumptables); - const bytecode = `${initCode}${macroCode}`; - const vm = new VM({ hardfork: 'constantinople' }); - const results = await runCode(vm, bytecode, calldata, offset, sourcemap, callvalue, debug); - const gasSpent = results.runState.gasLimit.sub(results.runState.gasLeft).sub(new BN(initGasEstimate)).toString(10); - if (debug) { - console.log('code size = ', macroCode.length / 2); - console.log('gas consumed = ', gasSpent); - } - return { - gas: gasSpent, - stack: results.runState.stack, - memory: results.runState.memory, - returnValue: results.runState.returnValue, - bytecode: macroCode, + } + + function Runtime(filename, path, debug = false) { + const { inputMap, macros, jumptables } = newParser.parseFile(filename, path); + return async function runMacro(vm, macroName, stack = [], memory = [], calldata = null, callvalue = 0, callerAddr = 0) { + + const memoryCode = encodeMemory(memory); + const stackCode = encodeStack(stack); + const initCode = `${memoryCode}${stackCode}`; + const initGasEstimate = (memory.length * 9) + (stack.length * 3); + const offset = initCode.length / 2; + const { + data: { bytecode: macroCode, sourcemap }, + } = newParser.processMacro(macroName, offset, [], macros, inputMap, jumptables); + + const bytecode = `${initCode}${macroCode}`; + const results = await runCode(vm, bytecode, calldata, offset, sourcemap, callvalue, debug); + const gasSpent = results.runState.gasLimit.sub(results.runState.gasLeft).sub(new BN(initGasEstimate)).toString(10); + if (debug) { + console.log('code size = ', macroCode.length / 2); + console.log('gas consumed = ', gasSpent); + } + + return { + gas: gasSpent, + stack: results.runState.stack, + memory: results.runState.memory, + returnValue: results.runState.returnValue, + bytecode: macroCode, + }; }; - }; -} - -module.exports = Runtime; + } + + module.exports.Runtime = Runtime; + module.exports.getNewVM = getNewVM; + \ No newline at end of file diff --git a/src/runtime.spec.js b/src/runtime.spec.js index 3263f028..6d6c99d7 100644 --- a/src/runtime.spec.js +++ b/src/runtime.spec.js @@ -1,7 +1,9 @@ const chai = require('chai'); const BN = require('bn.js'); const path = require('path'); -const Runtime = require('./runtime'); +const { Runtime, getNewVM } = require('./runtime.js'); + +const vm = getNewVM(); const { expect } = chai; @@ -37,7 +39,7 @@ describe('runtime tests using double algorithm', () => { ]; }); it('macro DOUBLE correctly calculates point doubling', async () => { - const { stack } = await double('DOUBLE', inputStack); + const { stack } = await double(vm, 'DOUBLE', inputStack); expected = [ new BN('30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47', 16), new BN('55f8cabad8ae94c14c1482e3e20f7ce889e3143949181f404b04da8df02029ba', 16), @@ -51,7 +53,7 @@ describe('runtime tests using double algorithm', () => { }); it('macro DOUBLE__MAIN correctly calculates point doubling (inverted y)', async () => { - const { stack } = await double('DOUBLE__MAIN_IMPL', inputStack.slice(1)); + const { stack } = await double(vm, 'DOUBLE__MAIN_IMPL', inputStack.slice(1)); expected = [ new BN('55f8cabad8ae94c14c1482e3e20f7ce889e3143949181f404b04da8df02029ba', 16), new BN('4dc6bb6ed2a4e4b4a6eb59d2e90fb4745f2c0f99ea678a14ce43360f0a27b16e', 16),