From 1cc955cb29cdc1b621da08c480bcaa9ed953c58f Mon Sep 17 00:00:00 2001 From: John O'Sullivan Date: Tue, 7 Jan 2020 16:28:11 -0500 Subject: [PATCH 1/2] Upgraded README w/ explanation of EVM storage; added comments to all script fxns --- .gitignore | 3 +- README.md | 65 ++++++++++++++++++++++++++--- index.js | 115 ++++++++++++++++++++++++++++++++++++++++----------- package.json | 3 +- 4 files changed, 154 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 6bc8f53..9a8aef5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store node_modules/ -quorum-config.json \ No newline at end of file +quorum-config.json +quorum-genesis.json \ No newline at end of file diff --git a/README.md b/README.md index d58d4bd..8aaa97e 100644 --- a/README.md +++ b/README.md @@ -22,18 +22,71 @@ The `quorum-config.json` should be in the following format: ```json { - "threshold": 3, + "chainID": 1, + "makers": ["0xca843569e3427144cead5e4d5999a3d0ccf92b8e"], "voters": [ "0x0fbdc686b912d7722dc86510934589e0aaf3b55a", "0x9186eb3d20cbd1f5f992a950d808c4495153abd5", "0x0638e1574728b6d862dd5d3a3e0942c3be47d996" - ], - "makers": ["0xca843569e3427144cead5e4d5999a3d0ccf92b8e"] + ] } ``` Where: +* `chainID` is mandatory & will be the block's `chainID` +* `makers` is mandatory & will be used to set the blockmakers on the BlockVoting contract. The number of makers is also used to calculate the initial difficulty (`EXPECTED_HASHRATE * numMakers * DESIRED_SECONDS_PER_BLOCK`). +* `voters` is optional. They will be set as voters on the BlockMaking contract, but in the POW model, voters are unnecessary. If specified, they will be included among the owners of the WeylGov contract. +* `gasLimit` is optional, and if specified, will be included as the block's `gasLimit`. -* `threshold` is the number of required voters -* `voters` is a list of voter addresses -* `makers` is a list of blockmaker addresses +How Does EVM Storage Work +--- + +Fundamentally, this script uses a template and the above config to lay out initial storage values for our two governance smart contracts. Its functions make more sense if you understand how Ethereum represents contract storage. The genesis block has an `alloc` key which allows us to give any address some `code`, `storage`, or a `balance`. `code` is for contract bytecode, `balance` is initial gas. `storage` allows you to create initial values for the state variables declared in the smart contract's source. + +The genesis block represents state in a flat object. Each state variable is referenced in the order it was declared in the source code; take a look at the sample contract below: + +```sol +contract WeylExample { + + uint public numOwners; + mapping(address => bool) private owners; + + // Rest of contract... +} +``` + +The first state variable is a uint, or more importantly, a simple [Value Type](https://solidity.readthedocs.io/en/v0.4.24/types.html#value-types). Its key in the storage object will be `0`, but converted into a padded hex string: `0x0...0`. Its uint value is stored as an un-padded, `0x`-prefixed hex string, so with one owner, the Weyl contract's storage shape starts out like this: + +``` +{ + "alloc": { + "0x0...02A" : { + "code": "0x...", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1" + } + } + } +} +``` + +The second state variable is a [Mapping](https://solidity.readthedocs.io/en/v0.4.24/types.html#mappings). The storage object is flat, so each value stored in the map gets its own key. This key is calculated with the following steps: + +1. Pad the address which will be used to key into the map and the index of the variable (for our `owners` variable, `1`). +2. Make a hex Buffer which is the address followed by the index. +3. Return the `sha3` hash of this Buffer as a `0x`-prefixed hex string. + +The ValueTypes within the map are stored the same way. Put this all together, and in the one-owner case, you end up with the following `storage` object: + +``` +{ + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1", + "0xe9b6e9843417f0ea3a8ed4ff9903fd0b05391abe8cff9eae43d392f2221aefe9": "0x1" + } +} +``` + +Similar procedures are used for initializing arrays, except with array indices instead of map keys. + +Make sure to do extensive tests with any new genesis block, as misformatted memory will make functions return garbage values. This script is only responsible for setting the variables shown above. If we wanted to import governance history into a new genesis block, we would also need to set initial values for the other assorted state variables. \ No newline at end of file diff --git a/index.js b/index.js index 07d2b62..c1df796 100755 --- a/index.js +++ b/index.js @@ -19,17 +19,35 @@ const REMAINDER_ADDR = '0x9153A2a04cc57B486AB82bC0bE341DCa367B7934'; // Used to calculate the initial difficulty const EXPECTED_MAKER_HASHRATE = 50000; -function padIndex(number, prefix) { +/** + * Given an index (non-negative integer), return it as a + * hex string left-padded to 32 chars. Pass prefix boolean + * as `true` to get an '0x' prefix. + * @param {number} index + * @param {boolean} prefix + */ +function padIndex(index, prefix) { if(prefix) { - return utils.addHexPrefix(utils.setLengthLeft([number], 32, false).toString('hex')); + return utils.addHexPrefix(utils.setLengthLeft([index], 32, false).toString('hex')); } - return utils.setLengthLeft([number], 32, false).toString('hex'); + return utils.setLengthLeft([index], 32, false).toString('hex'); } +/** + * Given an Ethereum address (40 characters after removing '0x'), + * left-pad with '0's to get a 64-char un-prefixed hex string. + * @param {string} address + */ function padAddress(address) { return "000000000000000000000000" + utils.stripHexPrefix(address); } +/** + * Given a Mapping variable's index and a key to store, return + * an '0x'-prefixed hex string for use in the "storage" object + * @param {number} index + * @param {string} address + */ function storageKey(index, address) { let paddedAddress = padAddress(address); let paddedIndex = padIndex(index); @@ -37,6 +55,16 @@ function storageKey(index, address) { return utils.addHexPrefix(result) } +/** + * Given a contract address, a variable index, and an array of + * addresses, add all of the key-value pairs so each address + * has a `true` value in that map. Globally modifies the + * `template` object. + * + * @param {*} alloc + * @param {*} index + * @param {*} addresses + */ function mapAddressesAt(alloc, index, addresses) { let value = '0x01'; for(let i=0; i Date: Mon, 20 Jan 2020 18:36:13 -0500 Subject: [PATCH 2/2] Added a pseudocode line & a section on how to hard fork --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8aaa97e..fbe5cc0 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ contract WeylExample { } ``` -The first state variable is a uint, or more importantly, a simple [Value Type](https://solidity.readthedocs.io/en/v0.4.24/types.html#value-types). Its key in the storage object will be `0`, but converted into a padded hex string: `0x0...0`. Its uint value is stored as an un-padded, `0x`-prefixed hex string, so with one owner, the Weyl contract's storage shape starts out like this: +The first state variable is a uint, or more importantly, a simple [Value Type](https://solidity.readthedocs.io/en/v0.4.24/types.html#value-types). Its key in the storage object will be `0` -- state variables are zero-indexed -- but converted into a padded hex string: `0x0...0`. Its uint value is stored as an un-padded, `0x`-prefixed hex string, so with one owner, the Weyl contract's storage shape starts out like this: ``` { @@ -76,7 +76,11 @@ The second state variable is a [Mapping](https://solidity.readthedocs.io/en/v0.4 2. Make a hex Buffer which is the address followed by the index. 3. Return the `sha3` hash of this Buffer as a `0x`-prefixed hex string. -The ValueTypes within the map are stored the same way. Put this all together, and in the one-owner case, you end up with the following `storage` object: +Written in pseudocode, setting one address key in a map to `true` looks something like: +``` +storage[ '0x' + sha3( pad(addressKey) + pad(mapIndex) )] = '0x1'; +``` +Where `addressKey` is the key on the map and `mapIndex` is that map's index in the list of state variables as declared in the sourcecode (in the sample case, `mapIndex = 1`). Put this all together, and in the one-owner case, you end up with the following `storage` object: ``` { @@ -86,7 +90,13 @@ The ValueTypes within the map are stored the same way. Put this all together, a } } ``` - Similar procedures are used for initializing arrays, except with array indices instead of map keys. -Make sure to do extensive tests with any new genesis block, as misformatted memory will make functions return garbage values. This script is only responsible for setting the variables shown above. If we wanted to import governance history into a new genesis block, we would also need to set initial values for the other assorted state variables. \ No newline at end of file +Make sure to do extensive tests with any new genesis block, as misformatted memory will make functions return garbage values. This script is only responsible for setting the variables shown above. If we wanted to import governance history into a new genesis block, we would also need to set initial values for the other assorted state variables. + +So We Want to Hardfork... +--- + +The syntax described above was all we needed to know when we were initializing the BlockVoting & EximGov contracts. The mappings we needed to set only stored booleans, but we have some mappings which store structs. If we wanted to do a hard fork where we imported the full current state of the governance contract, we'd need to do research on how those are represented. + +One strategy to rebuild `storage` would be manually querying all of the locations where there are values. `web3` provides a function to fetch the value at any specific index. In practice, this means we would need to calculate all `map` and `array` indices ourselves, potentially using the transaction history, and then fetch the value stored in each one. It's technically possible, but a significant amount of legwork on our end. Definitely worth thinking through whether we really need to import *all* of this contract's historical state. \ No newline at end of file