This tool was created to aggregate the EOS ERC20 token distribution in it's entirety, acknowledge various scenarios, run validation on data and provide files representing balances in various states.
This tool can be used to generate snapshots for any period in a deterministic fashion until the EOS ERC20 token has been frozen. Once frozen, the tool will only produce deterministic final snapshots that represent the final state of the EOS ERC20 token distribution.
- Installation
- Upgrade Instructions
- Troubleshooting
- Common Usage
- Configuration Options
- FAQ
- How it works
- Glossary
- MySQL (local or remote)
- Parity 1.7.8+
- [Node 8+](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions
- For a full sync, Parity can take 3-4 days.
- A fresh snapshot sync can take upwards of 15 hours to the current periods.
Plan accordingly
- Ram: 16GB+ Recommended
- Hard drive: SSD recommended, NVME to win the race. HDD read/write speeds are intolerably slow with parity.
- Around 128GB of free storage, required for full parity chain, database and large file exports.
- In the root directory of this project you'll find a
config.default.js
file. - Copy
config.default.js
and rename the copy toconfig.js
. The parameters are described in the file, but for your convenience they are also listed below in greater detail. DO NOT REMOVE OR RENAME config.default.js
MySQL stores all the intricate details of the distribution and the contract. These instructions assume you already have MySQL configured locally or have a remote MySQL instance.
- Add your mysql details to your newly copied config file.
- Import
schema.sql
located in the./bin
directory of this project.
The instructions below are for IPC because it's the recommended connection method. HTTP/WS has issues with multi-threading, and so you will be limited to a single thread for public key sync.
On most linux systems, the default IPC defined in config.default.js should work for most linux platforms.
parity --no-warp --mode active --tracing off --pruning fast --db-compaction ssd --jsonrpc-apis all --chain mainnet --cache-size 2048
If you're on Mac, it's suggested you pass a flag to parity to generate an IPC file, like below
parity --mode active --tracing off --pruning fast --db-compaction ssd --jsonrpc-apis all --chain mainnet --no-warp --cache-size 2048 --ipc-path /Users/youruser/jsonrpc.ipc
And then set that path in your config.js
it's imperitive that you start parity with --no-warp. If you have a pre-existing parity configuration file you've modified, you'll want to create a new configuration file for the snapshot.
Important: Since Parity v1.7.8 --warp
is enabled by default. If you fail to configure with no-warp
you will have issues.
Notes
- If you do not use IPC, you will have limited performance due to limitations with HTTP and WS transports.
- If you must use an HDD, be sure to change the
--db-compaction
parameter for parity tohdd
, like so:--db-compaction hdd
- You can adjust
--cache-size
as needed, this could provide some sync-speed improvements.
Please view Parity Documentation if you have issues with Parity.
If you're taking a "final" snapshot, much of the configuration is automatic.
- A final snapshot will be taken automatically if the crowdsale has ended and the tokens are frozen, and will override configured
period
. Block ranges for the snapshot will be determined by the opening block (first transaction from crowdsale contract) and the deterministic freeze block. This is to prevent user error.
If you're taking an "ongoing" snapshot, change "period" to the period for which you would like to generate a snapshot. Note If you put in a period that hasn't yet completed, the "last closed period" will override your choice.
From the root directory of this project directory, run the following:
npm update
npm install -g lerna
npm install
see faq for information on lerna
node snapshot.js --load_config
Note See other useful configuration options, such as --resume
and --poll
, below
- If MySQL and Parity are running, and Parity is synced, the script will start running.
- If either MySQL or Parity are not connecting, it will keep trying every 5 seconds until a connection is established. Double-check your config and that both MySQL and Parity are running.
- If Parity is not done syncing, it will tell you so, and let you know how far along it is. Once it's synced, it will continue.
- If you encounter an error, see "troubleshooting"
- When complete, the script will output several files into the
./data
directory. - Inside the
data
directory, will be a directory named after the period number you generated, for example:./data/123.
If it's a final snapshot, into the./data/final
directory. - Inside that directory, is directory named after a numerical index. This is so that if you run it multiple times, you don't overwrite another snapshot.
- This functionality helps with verification of determinism and for development purposes.
snapshot.csv
- This file contains the Et um Address, EOS Key and EOS Balance of every address that registered correctly with the contract up to the configured period, and has a balance greater than the value set bysnapshot_minimum_balance
(default:1)snapshot-unregistered.csv
- This file contains the Ethereum Address and EOS Balance of every address that either failed to register or registered incorrectly with the crowdsale contract up to the configured period.distribution.csv
- This file includes the Ethereum Address and EOS balance of the entire distribution up to the configured period, with no rules or validation imposed.snapshot.json
- This file contains information about the snapshot-session that created the above files. This file should not be used for verification, but can be used for debugging and to help identify indeterminism.
If overwrite_snapshot
is set to true, all the above files will be put into the root directory of the project. If you are doing any development on this project, it's suggested that you change this to false
(it's enabled by default in the config.default.js
file.)
- Import bin/schema.sql to database
npm update
npm install
There are three methods for configuration
- Config File (recommended)
- User Prompt, fast if your using default mysql, redis and web3 settings.
- CLI args, will override Prompt
period
(integer, default: last closed period) The period to sync to, it will sync to the last block of any given period.snapshot_minimum_balance
(integer, default: 1) Minimum balance required for inclusion in snapshots.overwrite_snapshot
(boolean, default: true) Overwrites snapshots in root directory.
poll
(boolean) Will finish the configured period, and then try the next one. If the next one isn't ready (or tokens aren't frozen) it will continue to try every 10 seconds until there's a new period to process.resume
(boolean) Will resume from last synced data. If you successfully run period 1, and the run period 3 with resume, it will only sync contract data between 1 and 3 (instead of starting fresh) and will only update wallets with changes between 1 and 3. Important This functionality isn't perfect, there are situations where this flag could produce a failed test and force a resync (run without resume)only_produce_final_snapshot
(boolean) Should be used in conjunction with poll. It will sync contract data, but will only calculate wallets after the tokens are frozen.
author
(string, default: "Anonymous") Optional meta to identify snapshotter
eth_node_type
(http, ipc , ws) [default: http] Based on performance testing,ipc
is recommended for local configurations andhttp
is recommended for cloud configurations.eth_node_path
(valid host/path) Relative to type as defined abovemysql_db
mysql_user
mysql_pass
mysql_host
mysql_port
All of the above options can be set either in your config.js or as startup parameters, simply prepend the option with --
so for example --poll
or --period=111
. Startup parameters will override config.js parameters.
Misuse of these parameters without understanding the implications can result in inaccurate snapshot and/or result in error
recalculate_wallets
(boolean, default: false) Recalculates wallet balances without syncing contracts. Useful for development when resyncing the contracts is unnecessary (for example, you've already synced the contracts for period 1, and simply need to recalculate wallet balances for period 1)skip_web3_sync
(boolean, default: false) Skips web3 sync requirement. Useful for development if you know that Parity is caught up to the period you want to sync to (for example, you want to run a snapshot on period 2, but ran period 300 last night, so you know Parity is caught up to Period 2.)
Because you need to sync the entire blockchain.
Because this distribution requires an ETL mechanism in order to determinstically aggregate the distribution.
Unclaimed tokens are attributed to the contributing address.
Tokens accidentally sent to one of the contracts are attributed to the sending address.
It is exposed to fallback registeration. Script will sync all ethereum public keys in block range. If it can locate a public key belonging to an unregsitered address, it will generate an EOS Public Key from the Ethereum Public Key. The Ethereum Private Key converted to EOS WIF should then match the EOS Public Key (NOTE: This is highly experimental, but has been tested to work with high confidence)
Addresses not exposed to fallback registrations that remain invalid are exported to snapshot-unregistered.csv
file, which could prove useful.
Determinstic index is the order of all wallets with respect to when they were seen by either of the contract's (EOSCrowdsale and EOSTokens)
Account names the deterministic index encoded to byte32, and then padded with "genesis11111" up to 12 chars.
Yes
Because of this unresolved issue
Registration Fallback will attempt to find a public key for the address, and fallback register it.
NO! That sounds like a recipe for indeterminism. The script will choose the deterministic end block for final snapshot, it's picks that block by detecting when the tokens were frozen. For ongoing snapshots the block ranges are determined by period, from the first block where the crowdsale had a transaction, to the last block of the defined period.
Data is corrupted, try running with --recalculate_wallets
, if that doesn't work, run without resume
npm update
npm install
You probably started your Parity node without --no-warp
(add --no-warp to parity startup), if using geth make sure syncmode
is set to "full" (For example: --syncmode "full"
)
First, run snapshot with --verbose_mt
, this will display the stdout of the child processes for public key sync. If there's an error, look into it. Most likely you just need to npm install
. You can also add BLUEBIRD_LONG_STACK_TRACES=1
to your snapshot startup like... BLUEBIRD_LONG_STACK_TRACES=1 node snapshot...
This is related to the web3 fork mentioned in FAQ. Web3.js team runs lerna before releasing a package, this package is not listed on NPM, so your system will have to run lerna. I'm not doing the package process to keep the forked repo as close to origin as possible.
npm update
npm install -g lerna
npm install
node snapshot --load_config --poll --resume --period=350
If period 350 isn't closed yet, the script will reset your period to the last_closed_period
, and then when finished try to sync again.
node snapshot --load_config --poll --resume --recalculate_wallets --period=350
This will recalculate the wallets, but resume on the contract data.
node snapshot --load_config --period=350 --recalculate_wallets
This is a nuke, it will truncate your contract tables and wallet table, recalculating wallets.
Note: Public Keys table is not deleted, if you need to delete it for whatever reason, it must be done manually
add --verbose_mt
to startup
Verbose MT will display stdout from child processes, it's noisy, so it's disabled by default.
There are some differences between "ongoing" and "final" snapshots that need to be mentioned.
- Ongoing snapshots will produce accurate output based on period by constraining all blockchain activity to that range.
- Block range for each period must be found (determinism)
- Block range is defined by period, first block of crowdsale to the last block of the defined period.
- Balances are calculated cumulatively, as opposed to
balanceOf()
method provided by EOSCrowdsale contract - EOS Key Registration is concluded by last registration within the block range.
- Final simplifies a few things.
- Block range is defined by the first block of crowdsale (when first transaction occured) to the freeze block
- Balances are not calculated but inferred from state returned by
balanceOf()
function provided by Token Contract. - EOS Key Registration utilizes
key()
public constant from crowdsale contract, registrations are not possible after 23 hours after distribution finishes.
The snapshot parameters this software proposes are as follows
- All data is constrained by block range determined by period (for ongoing snapshots)
- EOS Balance of an address must be greater-than or equal to
snapshot_minimum_balance
to be considered valid for snapshot export. - An EOS key must be associated to an account either by registration
- If an ethereum address sent EOS ERC20 tokens to the EOS Crowdsale or EOS Token contract within the block range of the snapshot, these tokens are allocated to the respective address.
- If an address has unclaimed EOS ERC20 tokens in the EOS Crowdsale contract within block range, these tokens are allocated to the respective address.
- If an address is unregistered and the script was able to find a public key belonging to the address, it will generate an EOS Public Key that matches the holding addresses' Ethereum private key.
- Registered addresses whose EOS key validates and whose EOS ERC20 balance is greater-than or equal to the
minimum_snapshot_balance
are exported tosnapshot.csv
file (ethereum address, EOS public key, balance) - Addresses that are not registered but are equal to or greater-than the
minimum_snapshot_balance
, will be exported tosnapshot_unregistered.csv
(ethereum address, balance) - Entire distribution is exported to
distribution.csv
file without any rules or validation applied (ethereum address, balance)
The script employs strict patterns to encourage predictable output, often at the expense of performance. The pattern is aggregate - calculate - validate and closely resembles an ETL or extract, transform and load pattern. This decision came after numerous iterations and determining that debugging from state was more efficient than debugging from logs.
Below is the script transposed to plain english.
-
User Configured Parameters are set through one of three methods.
-
Check Connections to MySQL, Redis and Web3
-
Truncate Databases
-
Generate Period Map
- Used to define block ranges of periods
- Determines the block range that the snapshot is based upon
-
Set Application State Variables (including user configurations)
-
Set Block Range
- If tokens are frozen, force the snapshot block range to
state.block_state
and the deterministic freeze block (block tokens were frozen) - Otherwise, set block range to user defined block range.
- If tokens are frozen, force the snapshot block range to
-
Sync Public keys
-
For every address between block range, sync ethereum public keys to table.
-
If IPC connection, use multi-threaded implementation.
-
-
Sync history of token and crowdsale contract.
-
EOS Transfers
-
Buys
-
Claims
-
Registrations
-
Reclaimable Transfers
-
Resuming:
-
If resume is set, it will only sync data from where it left off
-
Otherwise, contract tables will be truncated and it will re-sync everything from scratch.
-
-
-
Compile list of every address that has ever had an EOS balance, for each address:
-
Aggregate relevant txs
- Claims and Buys, required for Unclaimed Balance Calculation
- Transfers, all incoming and outgoing tx from address
- Reclaimable Transfers, every reclaimable has a corresponding transfer [special case]
- The last registration transaction to occur within defined block range
-
Calculate
- Sum Wallet Balance (sum(transfers_in) - sum(transfers_out))
- Calculate Unclaimed Balance
- Sum Reclaimed Transfer Balances
- Sum Balances
- Convert balances from gwei
-
Validate
- Check Wallet Balance
- Validate EOS Key, if valid set
registered
totrue
- If EOS key error, save error to column
register_error
- If all validated, set
valid
to true.
-
Process
-
Save every wallet regardless of validation or balance to
wallets
table -
Resuming:
- resume is set and recalculate_wallets is not set, it will only process the above data for addresses with changes since the last sync and adjust block range for wallets accordingly (in the case of buys, it will aggregate based on period not on block range, because future buys are possible)
-
resume is not set or recalculate_wallets is set, it will truncate wallets table and process all addresses.
-
-
-
Registration Fallback
-
Query invalid addresses, above minimum snapshot threshold and without register error "exclude" (EOSToken/Crowdsale contracts)
-
Attempt to locate public key for each addrses
- if a public key is found, convert it to an EOS Key, update the wallet with the generated key, set fallback to true and set valid to true.
-
-
Deterministic Index and Account Names
- Query addresses
order by first_seen, address
in batches of 10000. - For each batch set the deterministic index and generate the account name
- Update each address.
- Query addresses
-
Test
- Daily Totals from DB against daily totals from EOS Utility Contract, failure here would not fail the below tests, but would instead result in inaccurate unclaimed balances. Difficult problem to detect without this test.
- Total Supply, margin of error should be low due to dust from rounding (generally 0.00000001%)
- Negative Balances, there should be zero negative balances
-
Output
-
snapshot.csv - comma-delimited list of ETH addresses, EOS keys and Total EOS ERC20 Balances (user, key, balance respectively)
- Move all valid entries from
wallets
table tosnapshot
table, ordered by balance DESC. - Generate snapshot.csv from
snapshot
table
- Move all valid entries from
-
snapshot_unregistered.csv - comma-delimited list of ETH addresses and Total EOS ERC20 Balances (user, balance respectively)
- Move all invalid entries whose balance is greater-than or equal-to
minimum_snapshot_balance
fromwallets
table tosnapshot_unregistered
table, ordered by balance DESC. - Generate snapshot_unregistered.csv from
snapshot_unregistered
table
- Move all invalid entries whose balance is greater-than or equal-to
-
distribution.csv - comma-delimited list of ETH addresses and Total EOS ERC20 Balances (user, balance respectively) does not include EOS keys!
- Export all Ethereum Addresses and EOS ERC20 total balances from
wallets
table without any rules of validation imposed to distribution.csv
- Export all Ethereum Addresses and EOS ERC20 total balances from
-
snapshot.json - Snapshot meta data
- Snapshot parameters
- Test results
- General Statistics
- Generate MD5 Checksums
- From generated snapshot.csv file, useful for debugging and auditing
- From mysql checksum for every table in database (useful for debugging)
- Pass any other useful state variables into object
-
- wallet balance - The wallet balance is refers to an address's EOS ERC20 token balance
- unclaimed balance - An unclaimed balance refers to tokens that were never claimed by an address. Despite being unclaimed, these still belong to the contributing address.
- reclaimed balance - A reclaimed balance refers to EOS ERC20 tokens accidentally sent to the EOSCrowdsale or EOSToken contract. The balances of these contracts are not included in distribution calculations, so it's imperitive these balances are calculated to have accurate supply values.
- total balance - The sum of wallet + unclaimed + reclaimed balances. The total balance is what is included as a user's balance in snapshots.
- registered address - A registered address is an address with a balance greater than or equal to the
minimum_snapshot_balance
that has correctly registered their ethereum address with the EOSCrowdsale contract using a valid EOS Public Key. - unregistered address - An unregistered address is an address with a balance greater than or equal to the -
minimum_snapshot_balance
that has either incorrectly registered or failed to register their address with an EOS Public Key. - freeze block - The freeze block is a deterministic value represented by the block number representing the period that tokens were frozen. This block will mark the last block for which actions sent to the crowdsale contract will be honored (such as registrations)
- snapshot - A file containing EOS public keys and balances that can be imported during an EOSIO boot sequence.
- snapshot-unregistered - A file containing Ethereum addressees and balances. This file could potentially be imported during an EOSIO boot sequence into the table of a contract that enables Ethereum based claiming.
- liquid supply - The liquid supply represents total aggregate EOS ERC20 tokens that are presently in circulation and detected by snapshot script, after the crowdsale ends, the liquid supply should equal the total supply.
- expected supply - Expected supply is a mathematically determined value representing what the script expects the liquid supply to equal. Liquid Supply should be within 0.00000001% of expected supply as a result of dust acquired by precision reduction.
- registration fallback - Registering an EOS Public Key for an unregistered address utilizing a discovered public key.