From 3abcc5659b5cdc6cbbfd3736c2e24753e77781f9 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Mon, 28 Jan 2019 15:31:01 -0600 Subject: [PATCH] Statediffing geth * Write state diff to CSV (#2) * port statediff from https://github.com/jpmorganchase/quorum/blob/9b7fd9af8082795eeeb6863d9746f12b82dd5078/statediff/statediff.go; minor fixes * integrating state diff extracting, building, and persisting into geth processes * work towards persisting created statediffs in ipfs; based off github.com/vulcanize/eth-block-extractor * Add a state diff service * Remove diff extractor from blockchain * Update imports * Move statediff on/off check to geth cmd config * Update starting state diff service * Add debugging logs for creating diff * Add statediff extractor and builder tests and small refactoring * Start to write statediff to a CSV * Restructure statediff directory * Pull CSV publishing methods into their own file * Reformatting due to go fmt * Add gomega to vendor dir * Remove testing focuses * Update statediff tests to use golang test pkg instead of ginkgo - builder_test - extractor_test - publisher_test * Use hexutil.Encode instead of deprecated common.ToHex * Remove OldValue from DiffBigInt and DiffUint64 fields * Update builder test * Remove old storage value from updated accounts * Remove old values from created/deleted accounts * Update publisher to account for only storing current account values * Update service loop and fetching previous block * Update testing - remove statediff ginkgo test suite file - move mocks to their own dir * Updates per go fmt * Updates to tests * Pass statediff mode and path in through cli * Return filename from publisher * Remove some duplication in builder * Remove code field from state diff output this is the contract byte code, and it can still be obtained by querying the db by the codeHash * Consolidate acct diff structs for updated & updated/deleted accts * Include block number in csv filename * Clean up error logging * Cleanup formatting, spelling, etc * Address PR comments * Add contract address and storage value to csv * Refactor accumulating account row in csv publisher * Add DiffStorage struct * Add storage key to csv * Address PR comments * Fix publisher to include rows for accounts that don't have store updates * Update builder test after merging in release/1.8 * Update test contract to include storage on contract intialization - so that we're able to test that storage diffing works for created and deleted accounts (not just updated accounts). * Factor out a common trie iterator method in builder * Apply goimports to statediff * Apply gosimple changes to statediff * Gracefully exit geth command(#4) * Statediff for full node (#6) * Open a trie from the in-memory database * Use a node's LeafKey as an identifier instead of the address It was proving difficult to find look the address up from a given path with a full node (sometimes the value wouldn't exist in the disk db). So, instead, for now we are using the node's LeafKey with is a Keccak256 hash of the address, so if we know the address we can figure out which LeafKey it matches up to. * Make sure that statediff has been processed before pruning * Use blockchain stateCache.OpenTrie for storage diffs * Clean up log lines and remove unnecessary fields from builder * Apply go fmt changes * Add a sleep to the blockchain test * refactoring/reorganizing packages * refactoring statediff builder and types and adjusted to relay proofs and paths (still need to make this optional) * refactoring state diff service and adding api which allows for streaming state diff payloads over an rpc websocket subscription * make proofs and paths optional + compress service loop into single for loop (may be missing something here) * option to process intermediate nodes * make state diff rlp serializable * cli parameter to limit statediffing to select account addresses + test * review fixes and fixes for issues ran into in integration * review fixes; proper method signature for api; adjust service so that statediff processing is halted/paused until there is at least one subscriber listening for the results * adjust buffering to improve stability; doc.go; fix notifier err handling * relay receipts with the rest of the data + review fixes/changes * rpc method to get statediff at specific block; requires archival node or the block be within the pruning range * fix linter issues * include total difficulty to the payload * fix state diff builder: emit actual leaf nodes instead of value nodes; diff on the leaf not on the value; emit correct path for intermediate nodes * adjust statediff builder tests to changes and extend to test intermediate nodes; golint * add genesis block to test; handle block 0 in StateDiffAt * rlp files for mainnet blocks 0-3, for tests * builder test on mainnet blocks * common.BytesToHash(path) => crypto.Keaccak256(hash) in builder; BytesToHash produces same hash for e.g. []byte{} and []byte{\x00} - prefix \x00 steps are inconsequential to the hash result * complete tests for early mainnet blocks * diff type for representing deleted accounts * fix builder so that we handle account deletions properly and properly diff storage when an account is moved to a new path; update params * remove cli params; moving them to subscriber defined * remove unneeded bc methods * update service and api; statediffing params are now defined by user through api rather than by service provider by cli * update top level tests * add ability to watch specific storage slots (leaf keys) only * comments; explain logic * update mainnet blocks test * update api_test.go * storage leafkey filter test * cleanup chain maker * adjust chain maker for tests to add an empty account in block1 and switch to EIP-158 afterwards (now we just need to generate enough accounts until one causes the empty account to be touched and removed post-EIP-158 so we can simulate and test that process...); also added 2 new blocks where more contract storage is set and old slots are set to zero so they are removed so we can test that * found an account whose creation causes the empty account to be moved to a new path; this should count as 'touching; the empty account and cause it to be removed according to eip-158... but it doesn't * use new contract in unit tests that has self-destruct ability, so we can test eip-158 since simply moving an account to new path doesn't count as 'touchin' it * handle storage deletions * tests for eip-158 account removal and storage value deletions; there is one edge case left to test where we remove 1 account when only two exist such that the remaining account is moved up and replaces the root branch node * finish testing known edge cases * add endpoint to fetch all state and storage nodes at a given blockheight; useful for generating a recent atate cache/snapshot that we can diff forward from rather than needing to collect all diffs from genesis * test for state trie builder * if statediffing is on, lock tries in triedb until the statediffing service signals they are done using them * fix mock blockchain; golint; bump patch * increase maxRequestContentLength; bump patch * log the sizes of the state objects we are sending * CI build (#20) * CI: run build on PR and on push to master * CI: debug building geth * CI: fix coping file * CI: fix coping file v2 * CI: temporary upload file to release asset * CI: get release upload_url by tag, upload asset to current relase * CI: fix tag name * fix ci build on statediff_at_anyblock-1.9.11 branch * fix publishing assets in release * use context deadline for timeout in eth_call * collect and emit codehash=>code mappings for state objects * subscription endpoint for retrieving all the codehash=>code mappings that exist at provided height * Implement WriteStateDiffAt * Writes state diffs directly to postgres * Adds CLI flags to configure PG * Refactors builder output with callbacks * Copies refactored postgres handling code from ipld-eth-indexer * rename PostgresCIDWriter.{index->upsert}* * go.mod update * rm unused * cleanup * output code & codehash iteratively * had to rf some types for this * prometheus metrics output * duplicate recent eth-indexer changes * migrations and metrics... * [wip] prom.Init() here? another CLI flag? * tidy & DRY * statediff WriteLoop service + CLI flag * [wip] update test mocks * todo - do something meaningful to test write loop * logging * use geth log * port tests to go testing * drop ginkgo/gomega * fix and cleanup tests * fail before defer statement * delete vendor/ dir * fixes after rebase onto 1.9.23 * fix API registration * use golang 1.15.5 version (#34) * bump version meta; add 0.0.11 branch to actions * bump version meta; update github actions workflows * statediff: refactor metrics * Remove redundant statediff/indexer/prom tooling and use existing prometheus integration. * "indexer" namespace for metrics * add reporting loop for db metrics * doc * metrics for statediff stats * metrics namespace/subsystem = statediff/{indexer,service} * statediff: use a worker pool (for direct writes) * fix test * fix chain event subscription * log tweaks * func name * unused import * intermediate chain event channel for metrics * update github actions; linting * add poststate and status to receipt ipld indexes * stateDiffFor endpoints for fetching or writing statediff object by blockhash; bump statediff version * fixes after rebase on to v1.10.1 * update github actions and version meta; go fmt * add leaf key to removed 'nodes' * include Postgres migrations and schema * service documentation * touching up update github actions after rebase fix connection leak (misplaced defer) and perform proper rollback on errs improve error logging; handle PushBlock internal err * build docker image and publish it to Docker Hub on release * add access list tx to unit tests * MarshalBinary and UnmarshalBinary methods for receipt * fix error caused by 2718 by using MarshalBinary instead of EncodeRLP methods * ipld encoding/decoding tests * update TxModel; add AccessListElementModel * index tx type and access lists * add access list metrics * unit tests for tx_type and access list table * unit tests for receipt marshal/unmarshal binary methods * improve documentation of the encoding methods * fix issue identified in linting update github actions and version meta after rebase unit test that fails undeterministically on eip2930 txs, giving same error we are seeing in prod fix bug Include genesis block state diff. Fix linting issue. documentation on versioning, rebasing, releasing; bump version meta Add geth and statediff unit test to CI. Set pgpassword in env. Added comments. --- .github/workflows/checks.yml | 11 + .github/workflows/on-master.yaml | 31 + .github/workflows/on-pr.yml | 66 + .github/workflows/publish.yaml | 41 + .travis.yml | 244 -- Dockerfile.amd64 | 7 + Makefile | 30 + cmd/geth/config.go | 52 + cmd/geth/main.go | 6 + cmd/geth/usage.go | 11 + cmd/utils/flags.go | 46 + core/blockchain.go | 47 +- core/types/receipt.go | 96 +- core/types/receipt_test.go | 107 + core/types/transaction.go | 10 +- docker-compose.yml | 17 + eth/backend.go | 1 + eth/ethconfig/config.go | 4 + go.mod | 19 +- go.sum | 104 +- mobile/android_test.go | 1 + params/version.go | 8 +- rpc/http.go | 2 +- statediff/api.go | 151 ++ statediff/builder.go | 768 ++++++ statediff/builder_test.go | 2322 +++++++++++++++++ .../00001_create_ipfs_blocks_table.sql | 8 + .../migrations/00002_create_nodes_table.sql | 13 + .../db/migrations/00003_create_eth_schema.sql | 5 + .../00004_create_eth_header_cids_table.sql | 23 + .../00005_create_eth_uncle_cids_table.sql | 14 + ...0006_create_eth_transaction_cids_table.sql | 17 + .../00007_create_eth_receipt_cids_table.sql | 20 + .../00008_create_eth_state_cids_table.sql | 15 + .../00009_create_eth_storage_cids_table.sql | 15 + .../00010_create_eth_state_accouts_table.sql | 13 + .../00011_create_postgraphile_comments.sql | 6 + .../migrations/00012_potgraphile_triggers.sql | 69 + .../migrations/00013_create_cid_indexes.sql | 121 + .../00014_create_stored_functions.sql | 158 ++ .../00015_create_access_list_table.sql | 15 + statediff/db/schema.sql | 1333 ++++++++++ statediff/doc.md | 264 ++ statediff/helpers.go | 98 + statediff/indexer/helpers.go | 55 + statediff/indexer/indexer.go | 459 ++++ statediff/indexer/indexer_test.go | 457 ++++ .../ipld/eip2930_test_data/eth-block-12252078 | Bin 0 -> 50536 bytes .../ipld/eip2930_test_data/eth-block-12365585 | Bin 0 -> 60035 bytes .../ipld/eip2930_test_data/eth-block-12365586 | Bin 0 -> 38164 bytes .../eip2930_test_data/eth-receipts-12252078 | Bin 0 -> 132368 bytes .../eip2930_test_data/eth-receipts-12365585 | Bin 0 -> 127320 bytes .../eip2930_test_data/eth-receipts-12365586 | Bin 0 -> 111330 bytes statediff/indexer/ipfs/ipld/eth_account.go | 175 ++ .../indexer/ipfs/ipld/eth_account_test.go | 298 +++ statediff/indexer/ipfs/ipld/eth_header.go | 293 +++ .../indexer/ipfs/ipld/eth_header_test.go | 586 +++++ statediff/indexer/ipfs/ipld/eth_parser.go | 198 ++ .../indexer/ipfs/ipld/eth_parser_test.go | 81 + statediff/indexer/ipfs/ipld/eth_receipt.go | 198 ++ .../indexer/ipfs/ipld/eth_receipt_trie.go | 150 ++ statediff/indexer/ipfs/ipld/eth_state.go | 126 + statediff/indexer/ipfs/ipld/eth_state_test.go | 326 +++ statediff/indexer/ipfs/ipld/eth_storage.go | 112 + .../indexer/ipfs/ipld/eth_storage_test.go | 140 + statediff/indexer/ipfs/ipld/eth_tx.go | 236 ++ statediff/indexer/ipfs/ipld/eth_tx_test.go | 411 +++ statediff/indexer/ipfs/ipld/eth_tx_trie.go | 150 ++ .../indexer/ipfs/ipld/eth_tx_trie_test.go | 504 ++++ statediff/indexer/ipfs/ipld/shared.go | 161 ++ .../error-tx-eth-block-body-json-999999 | 1 + .../ipfs/ipld/test_data/eth-block-body-json-0 | 1 + .../test_data/eth-block-body-json-4139497 | 1 + .../ipld/test_data/eth-block-body-json-997522 | 1 + .../ipld/test_data/eth-block-body-json-999998 | 1 + .../ipld/test_data/eth-block-body-json-999999 | 1 + .../ipld/test_data/eth-block-body-rlp-997522 | Bin 0 -> 1728 bytes .../ipld/test_data/eth-block-body-rlp-999999 | Bin 0 -> 1768 bytes .../test_data/eth-block-header-rlp-999996 | Bin 0 -> 540 bytes .../test_data/eth-block-header-rlp-999997 | Bin 0 -> 539 bytes .../test_data/eth-block-header-rlp-999999 | Bin 0 -> 539 bytes .../ipld/test_data/eth-state-trie-rlp-0e8b34 | Bin 0 -> 115 bytes .../ipld/test_data/eth-state-trie-rlp-56864f | 1 + .../ipld/test_data/eth-state-trie-rlp-6fc2d7 | 5 + .../ipld/test_data/eth-state-trie-rlp-727994 | Bin 0 -> 117 bytes .../ipld/test_data/eth-state-trie-rlp-c9070d | Bin 0 -> 116 bytes .../ipld/test_data/eth-state-trie-rlp-d5be90 | Bin 0 -> 500 bytes .../ipld/test_data/eth-state-trie-rlp-d7f897 | Bin 0 -> 532 bytes .../ipld/test_data/eth-state-trie-rlp-eb2f5f | Bin 0 -> 37 bytes .../test_data/eth-storage-trie-rlp-000dd0 | Bin 0 -> 83 bytes .../test_data/eth-storage-trie-rlp-113049 | 1 + .../test_data/eth-storage-trie-rlp-9d1860 | 1 + .../test_data/eth-storage-trie-rlp-ffbcad | Bin 0 -> 44 bytes .../test_data/eth-storage-trie-rlp-ffc25c | 1 + .../ipld/test_data/eth-uncle-json-997522-0 | 1 + .../ipld/test_data/eth-uncle-json-997522-1 | 1 + statediff/indexer/ipfs/ipld/trie_node.go | 456 ++++ statediff/indexer/ipfs/models.go | 22 + statediff/indexer/metrics.go | 128 + statediff/indexer/mocks/test_data.go | 249 ++ statediff/indexer/models/models.go | 137 + statediff/indexer/node/node.go | 25 + statediff/indexer/postgres/config.go | 59 + statediff/indexer/postgres/errors.go | 38 + statediff/indexer/postgres/postgres.go | 76 + .../indexer/postgres/postgres_suite_test.go | 33 + statediff/indexer/postgres/postgres_test.go | 136 + statediff/indexer/reward.go | 76 + statediff/indexer/shared/chain_type.go | 78 + statediff/indexer/shared/constants.go | 22 + statediff/indexer/shared/data_type.go | 101 + statediff/indexer/shared/functions.go | 124 + statediff/indexer/shared/test_helpers.go | 68 + statediff/indexer/shared/types.go | 44 + statediff/indexer/test_helpers.go | 60 + statediff/indexer/writer.go | 143 + statediff/mainnet_tests/block0_rlp | Bin 0 -> 540 bytes statediff/mainnet_tests/block1_rlp | Bin 0 -> 537 bytes statediff/mainnet_tests/block2_rlp | Bin 0 -> 544 bytes statediff/mainnet_tests/block3_rlp | Bin 0 -> 1079 bytes statediff/mainnet_tests/builder_test.go | 690 +++++ statediff/metrics.go | 54 + statediff/service.go | 686 +++++ statediff/service_test.go | 291 +++ statediff/testhelpers/helpers.go | 124 + statediff/testhelpers/mocks/blockchain.go | 134 + statediff/testhelpers/mocks/builder.go | 67 + statediff/testhelpers/mocks/service.go | 334 +++ statediff/testhelpers/mocks/service_test.go | 247 ++ statediff/testhelpers/test_data.go | 73 + statediff/types.go | 113 + statediff/types/types.go | 61 + trie/encoding.go | 14 +- trie/encoding_test.go | 6 +- trie/iterator.go | 2 +- trie/sync.go | 8 +- 136 files changed, 16227 insertions(+), 294 deletions(-) create mode 100644 .github/workflows/checks.yml create mode 100644 .github/workflows/on-master.yaml create mode 100644 .github/workflows/on-pr.yml create mode 100644 .github/workflows/publish.yaml delete mode 100644 .travis.yml create mode 100644 Dockerfile.amd64 create mode 100644 docker-compose.yml create mode 100644 statediff/api.go create mode 100644 statediff/builder.go create mode 100644 statediff/builder_test.go create mode 100644 statediff/db/migrations/00001_create_ipfs_blocks_table.sql create mode 100644 statediff/db/migrations/00002_create_nodes_table.sql create mode 100644 statediff/db/migrations/00003_create_eth_schema.sql create mode 100644 statediff/db/migrations/00004_create_eth_header_cids_table.sql create mode 100644 statediff/db/migrations/00005_create_eth_uncle_cids_table.sql create mode 100644 statediff/db/migrations/00006_create_eth_transaction_cids_table.sql create mode 100644 statediff/db/migrations/00007_create_eth_receipt_cids_table.sql create mode 100644 statediff/db/migrations/00008_create_eth_state_cids_table.sql create mode 100644 statediff/db/migrations/00009_create_eth_storage_cids_table.sql create mode 100644 statediff/db/migrations/00010_create_eth_state_accouts_table.sql create mode 100644 statediff/db/migrations/00011_create_postgraphile_comments.sql create mode 100644 statediff/db/migrations/00012_potgraphile_triggers.sql create mode 100644 statediff/db/migrations/00013_create_cid_indexes.sql create mode 100644 statediff/db/migrations/00014_create_stored_functions.sql create mode 100644 statediff/db/migrations/00015_create_access_list_table.sql create mode 100644 statediff/db/schema.sql create mode 100644 statediff/doc.md create mode 100644 statediff/helpers.go create mode 100644 statediff/indexer/helpers.go create mode 100644 statediff/indexer/indexer.go create mode 100644 statediff/indexer/indexer_test.go create mode 100644 statediff/indexer/ipfs/ipld/eip2930_test_data/eth-block-12252078 create mode 100644 statediff/indexer/ipfs/ipld/eip2930_test_data/eth-block-12365585 create mode 100644 statediff/indexer/ipfs/ipld/eip2930_test_data/eth-block-12365586 create mode 100644 statediff/indexer/ipfs/ipld/eip2930_test_data/eth-receipts-12252078 create mode 100644 statediff/indexer/ipfs/ipld/eip2930_test_data/eth-receipts-12365585 create mode 100644 statediff/indexer/ipfs/ipld/eip2930_test_data/eth-receipts-12365586 create mode 100644 statediff/indexer/ipfs/ipld/eth_account.go create mode 100644 statediff/indexer/ipfs/ipld/eth_account_test.go create mode 100644 statediff/indexer/ipfs/ipld/eth_header.go create mode 100644 statediff/indexer/ipfs/ipld/eth_header_test.go create mode 100644 statediff/indexer/ipfs/ipld/eth_parser.go create mode 100644 statediff/indexer/ipfs/ipld/eth_parser_test.go create mode 100644 statediff/indexer/ipfs/ipld/eth_receipt.go create mode 100644 statediff/indexer/ipfs/ipld/eth_receipt_trie.go create mode 100644 statediff/indexer/ipfs/ipld/eth_state.go create mode 100644 statediff/indexer/ipfs/ipld/eth_state_test.go create mode 100644 statediff/indexer/ipfs/ipld/eth_storage.go create mode 100644 statediff/indexer/ipfs/ipld/eth_storage_test.go create mode 100644 statediff/indexer/ipfs/ipld/eth_tx.go create mode 100644 statediff/indexer/ipfs/ipld/eth_tx_test.go create mode 100644 statediff/indexer/ipfs/ipld/eth_tx_trie.go create mode 100644 statediff/indexer/ipfs/ipld/eth_tx_trie_test.go create mode 100644 statediff/indexer/ipfs/ipld/shared.go create mode 100644 statediff/indexer/ipfs/ipld/test_data/error-tx-eth-block-body-json-999999 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-0 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-4139497 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-997522 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999998 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999999 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-body-rlp-997522 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-body-rlp-999999 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-header-rlp-999996 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-header-rlp-999997 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-block-header-rlp-999999 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-0e8b34 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-56864f create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-6fc2d7 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-727994 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-c9070d create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-d5be90 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-d7f897 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-state-trie-rlp-eb2f5f create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-000dd0 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-113049 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-9d1860 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-ffbcad create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-ffc25c create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-0 create mode 100644 statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-1 create mode 100644 statediff/indexer/ipfs/ipld/trie_node.go create mode 100644 statediff/indexer/ipfs/models.go create mode 100644 statediff/indexer/metrics.go create mode 100644 statediff/indexer/mocks/test_data.go create mode 100644 statediff/indexer/models/models.go create mode 100644 statediff/indexer/node/node.go create mode 100644 statediff/indexer/postgres/config.go create mode 100644 statediff/indexer/postgres/errors.go create mode 100644 statediff/indexer/postgres/postgres.go create mode 100644 statediff/indexer/postgres/postgres_suite_test.go create mode 100644 statediff/indexer/postgres/postgres_test.go create mode 100644 statediff/indexer/reward.go create mode 100644 statediff/indexer/shared/chain_type.go create mode 100644 statediff/indexer/shared/constants.go create mode 100644 statediff/indexer/shared/data_type.go create mode 100644 statediff/indexer/shared/functions.go create mode 100644 statediff/indexer/shared/test_helpers.go create mode 100644 statediff/indexer/shared/types.go create mode 100644 statediff/indexer/test_helpers.go create mode 100644 statediff/indexer/writer.go create mode 100644 statediff/mainnet_tests/block0_rlp create mode 100644 statediff/mainnet_tests/block1_rlp create mode 100644 statediff/mainnet_tests/block2_rlp create mode 100644 statediff/mainnet_tests/block3_rlp create mode 100644 statediff/mainnet_tests/builder_test.go create mode 100644 statediff/metrics.go create mode 100644 statediff/service.go create mode 100644 statediff/service_test.go create mode 100644 statediff/testhelpers/helpers.go create mode 100644 statediff/testhelpers/mocks/blockchain.go create mode 100644 statediff/testhelpers/mocks/builder.go create mode 100644 statediff/testhelpers/mocks/service.go create mode 100644 statediff/testhelpers/mocks/service_test.go create mode 100644 statediff/testhelpers/test_data.go create mode 100644 statediff/types.go create mode 100644 statediff/types/types.go diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 000000000000..ee86e72a8902 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,11 @@ +name: checks + +on: [pull_request] + +jobs: + linter-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run linter + run: go run build/ci.go lint \ No newline at end of file diff --git a/.github/workflows/on-master.yaml b/.github/workflows/on-master.yaml new file mode 100644 index 000000000000..9546cdb0db76 --- /dev/null +++ b/.github/workflows/on-master.yaml @@ -0,0 +1,31 @@ +name: Docker Build and publish to Github + +on: + push: + branches: + - v1.10.3-statediff + - v1.10.2-statediff + - v1.10.1-statediff + - v1.9.25-statediff + - v1.9.24-statediff + - v1.9.23-statediff + - v1.9.11-statediff + +jobs: + build: + name: Run docker build and publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run docker build + run: docker build -t vulcanize/go-ethereum -f Dockerfile . + - name: Get the version + id: vars + run: echo ::set-output name=sha::$(echo ${GITHUB_SHA:0:7}) + - name: Tag docker image + run: docker tag vulcanize/go-ethereum docker.pkg.github.com/vulcanize/go-ethereum/go-ethereum:${{steps.vars.outputs.sha}} + - name: Docker Login + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login https://docker.pkg.github.com -u vulcanize --password-stdin + - name: Docker Push + run: docker push docker.pkg.github.com/vulcanize/go-ethereum/go-ethereum:${{steps.vars.outputs.sha}} + diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml new file mode 100644 index 000000000000..7f9ea6fae25b --- /dev/null +++ b/.github/workflows/on-pr.yml @@ -0,0 +1,66 @@ +name: Build and test + +on: [pull_request] + +jobs: + build: + name: Run docker build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run docker build + run: docker build -t vulcanize/go-ethereum . + + geth-unit-test: + name: Run geth unit test + strategy: + matrix: + go-version: [ 1.15.x] + platform: [ubuntu-latest] + runs-on: ${{ matrix.platform }} + env: + GO111MODULE: on + GOPATH: /tmp/go + steps: + - name: Create GOPATH + run: mkdir -p /tmp/go + + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Run unit tests + run: | + make test + + statediff-unit-test: + name: Run state diff unit test + env: + GOPATH: /tmp/go + strategy: + matrix: + go-version: [ 1.15.x] + platform: [ubuntu-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Create GOPATH + run: mkdir -p /tmp/go + + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Start database + run: docker-compose -f docker-compose.yml up -d db + + - name: Run unit tests + run: + make statedifftest \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000000..475725e647af --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,41 @@ +name: Publish geth to release +on: + release: + types: [published] +jobs: + push_to_registries: + name: Publish assets to Release + runs-on: ubuntu-latest + steps: + - name: Get the version + id: vars + run: | + echo ::set-output name=sha::$(echo ${GITHUB_SHA:0:7}) + echo ::set-output name=tag::$(echo ${GITHUB_REF#refs/tags/}) + - name: Docker Login to Github Registry + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login https://docker.pkg.github.com -u vulcanize --password-stdin + - name: Docker Pull + run: docker pull docker.pkg.github.com/vulcanize/go-ethereum/go-ethereum:${{steps.vars.outputs.sha}} + - name: Copy ethereum binary file + run: docker run --rm --entrypoint cat docker.pkg.github.com/vulcanize/go-ethereum/go-ethereum:${{steps.vars.outputs.sha}} /usr/local/bin/geth > geth-linux-amd64 + - name: Docker Login to Docker Registry + run: echo ${{ secrets.VULCANIZEJENKINS_PAT }} | docker login -u vulcanizejenkins --password-stdin + - name: Tag docker image + run: docker tag docker.pkg.github.com/vulcanize/go-ethereum/go-ethereum:${{steps.vars.outputs.sha}} vulcanize/vdb-geth:${{steps.vars.outputs.tag}} + - name: Docker Push to Docker Hub + run: docker push vulcanize/vdb-geth:${{steps.vars.outputs.tag}} + - name: Get release + id: get_release + uses: bruceadams/get-release@v1.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.get_release.outputs.upload_url }} + asset_path: geth-linux-amd64 + asset_name: geth-linux-amd64 + asset_content_type: application/octet-stream diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1c56f5d5db18..000000000000 --- a/.travis.yml +++ /dev/null @@ -1,244 +0,0 @@ -language: go -go_import_path: github.com/ethereum/go-ethereum -sudo: false -jobs: - allow_failures: - - stage: build - os: osx - go: 1.15.x - env: - - azure-osx - - azure-ios - - cocoapods-ios - - include: - # This builder only tests code linters on latest version of Go - - stage: lint - os: linux - dist: bionic - go: 1.16.x - env: - - lint - git: - submodules: false # avoid cloning ethereum/tests - script: - - go run build/ci.go lint - - # This builder does the Docker Hub build and upload for amd64 - - stage: build - if: type = push - os: linux - dist: bionic - go: 1.16.x - env: - - docker - services: - - docker - git: - submodules: false # avoid cloning ethereum/tests - script: - - go run build/ci.go docker -upload karalabe/geth-docker-test - - # This builder does the Ubuntu PPA upload - - stage: build - if: type = push - os: linux - dist: bionic - go: 1.16.x - env: - - ubuntu-ppa - - GO111MODULE=on - git: - submodules: false # avoid cloning ethereum/tests - addons: - apt: - packages: - - devscripts - - debhelper - - dput - - fakeroot - - python-bzrlib - - python-paramiko - script: - - echo '|1|7SiYPr9xl3uctzovOTj4gMwAC1M=|t6ReES75Bo/PxlOPJ6/GsGbTrM0= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0aKz5UTUndYgIGG7dQBV+HaeuEZJ2xPHo2DS2iSKvUL4xNMSAY4UguNW+pX56nAQmZKIZZ8MaEvSj6zMEDiq6HFfn5JcTlM80UwlnyKe8B8p7Nk06PPQLrnmQt5fh0HmEcZx+JU9TZsfCHPnX7MNz4ELfZE6cFsclClrKim3BHUIGq//t93DllB+h4O9LHjEUsQ1Sr63irDLSutkLJD6RXchjROXkNirlcNVHH/jwLWR5RcYilNX7S5bIkK8NlWPjsn/8Ua5O7I9/YoE97PpO6i73DTGLh5H9JN/SITwCKBkgSDWUt61uPK3Y11Gty7o2lWsBjhBUm2Y38CBsoGmBw==' >> ~/.ssh/known_hosts - - go run build/ci.go debsrc -upload ethereum/ethereum -sftp-user geth-ci -signer "Go Ethereum Linux Builder " - - # This builder does the Linux Azure uploads - - stage: build - if: type = push - os: linux - dist: bionic - sudo: required - go: 1.16.x - env: - - azure-linux - - GO111MODULE=on - git: - submodules: false # avoid cloning ethereum/tests - addons: - apt: - packages: - - gcc-multilib - script: - # Build for the primary platforms that Trusty can manage - - go run build/ci.go install -dlgo - - go run build/ci.go archive -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - go run build/ci.go install -dlgo -arch 386 - - go run build/ci.go archive -arch 386 -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - # Switch over GCC to cross compilation (breaks 386, hence why do it here only) - - sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross - - sudo ln -s /usr/include/asm-generic /usr/include/asm - - - GOARM=5 go run build/ci.go install -dlgo -arch arm -cc arm-linux-gnueabi-gcc - - GOARM=5 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - GOARM=6 go run build/ci.go install -dlgo -arch arm -cc arm-linux-gnueabi-gcc - - GOARM=6 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - GOARM=7 go run build/ci.go install -dlgo -arch arm -cc arm-linux-gnueabihf-gcc - - GOARM=7 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - go run build/ci.go install -dlgo -arch arm64 -cc aarch64-linux-gnu-gcc - - go run build/ci.go archive -arch arm64 -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - # This builder does the Linux Azure MIPS xgo uploads - - stage: build - if: type = push - os: linux - dist: bionic - services: - - docker - go: 1.16.x - env: - - azure-linux-mips - - GO111MODULE=on - git: - submodules: false # avoid cloning ethereum/tests - script: - - go run build/ci.go xgo --alltools -- --targets=linux/mips --ldflags '-extldflags "-static"' -v - - for bin in build/bin/*-linux-mips; do mv -f "${bin}" "${bin/-linux-mips/}"; done - - go run build/ci.go archive -arch mips -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - - go run build/ci.go xgo --alltools -- --targets=linux/mipsle --ldflags '-extldflags "-static"' -v - - for bin in build/bin/*-linux-mipsle; do mv -f "${bin}" "${bin/-linux-mipsle/}"; done - - go run build/ci.go archive -arch mipsle -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - - go run build/ci.go xgo --alltools -- --targets=linux/mips64 --ldflags '-extldflags "-static"' -v - - for bin in build/bin/*-linux-mips64; do mv -f "${bin}" "${bin/-linux-mips64/}"; done - - go run build/ci.go archive -arch mips64 -type tar -signer LINUX_SIGNING_KEY signify SIGNIFY_KEY -upload gethstore/builds - - - go run build/ci.go xgo --alltools -- --targets=linux/mips64le --ldflags '-extldflags "-static"' -v - - for bin in build/bin/*-linux-mips64le; do mv -f "${bin}" "${bin/-linux-mips64le/}"; done - - go run build/ci.go archive -arch mips64le -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - # This builder does the Android Maven and Azure uploads - - stage: build - if: type = push - os: linux - dist: bionic - addons: - apt: - packages: - - openjdk-8-jdk - env: - - azure-android - - maven-android - - GO111MODULE=on - git: - submodules: false # avoid cloning ethereum/tests - before_install: - # Install Android and it's dependencies manually, Travis is stale - - export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 - - curl https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -o android.zip - - unzip -q android.zip -d $HOME/sdk && rm android.zip - - mv $HOME/sdk/cmdline-tools $HOME/sdk/latest && mkdir $HOME/sdk/cmdline-tools && mv $HOME/sdk/latest $HOME/sdk/cmdline-tools - - export PATH=$PATH:$HOME/sdk/cmdline-tools/latest/bin - - export ANDROID_HOME=$HOME/sdk - - - yes | sdkmanager --licenses >/dev/null - - sdkmanager "platform-tools" "platforms;android-15" "platforms;android-19" "platforms;android-24" "ndk-bundle" - - # Install Go to allow building with - - curl https://dl.google.com/go/go1.16.linux-amd64.tar.gz | tar -xz - - export PATH=`pwd`/go/bin:$PATH - - export GOROOT=`pwd`/go - - export GOPATH=$HOME/go - script: - # Build the Android archive and upload it to Maven Central and Azure - - mkdir -p $GOPATH/src/github.com/ethereum - - ln -s `pwd` $GOPATH/src/github.com/ethereum/go-ethereum - - go run build/ci.go aar -signer ANDROID_SIGNING_KEY -signify SIGNIFY_KEY -deploy https://oss.sonatype.org -upload gethstore/builds - - # This builder does the OSX Azure, iOS CocoaPods and iOS Azure uploads - - stage: build - if: type = push - os: osx - go: 1.16.x - env: - - azure-osx - - azure-ios - - cocoapods-ios - - GO111MODULE=on - git: - submodules: false # avoid cloning ethereum/tests - script: - - go run build/ci.go install -dlgo - - go run build/ci.go archive -type tar -signer OSX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - - # Build the iOS framework and upload it to CocoaPods and Azure - - gem uninstall cocoapods -a -x - - gem install cocoapods - - - mv ~/.cocoapods/repos/master ~/.cocoapods/repos/master.bak - - sed -i '.bak' 's/repo.join/!repo.join/g' $(dirname `gem which cocoapods`)/cocoapods/sources_manager.rb - - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then git clone --depth=1 https://github.com/CocoaPods/Specs.git ~/.cocoapods/repos/master && pod setup --verbose; fi - - - xctool -version - - xcrun simctl list - - # Workaround for https://github.com/golang/go/issues/23749 - - export CGO_CFLAGS_ALLOW='-fmodules|-fblocks|-fobjc-arc' - - go run build/ci.go xcode -signer IOS_SIGNING_KEY -signify SIGNIFY_KEY -deploy trunk -upload gethstore/builds - - # These builders run the tests - - stage: build - os: linux - arch: amd64 - dist: bionic - go: 1.16.x - env: - - GO111MODULE=on - script: - - go run build/ci.go test -coverage $TEST_PACKAGES - - - stage: build - if: type = pull_request - os: linux - arch: arm64 - dist: bionic - go: 1.16.x - env: - - GO111MODULE=on - script: - - go run build/ci.go test -coverage $TEST_PACKAGES - - - stage: build - os: linux - dist: bionic - go: 1.15.x - env: - - GO111MODULE=on - script: - - go run build/ci.go test -coverage $TEST_PACKAGES - - # This builder does the Azure archive purges to avoid accumulating junk - - stage: build - if: type = cron - os: linux - dist: bionic - go: 1.16.x - env: - - azure-purge - - GO111MODULE=on - git: - submodules: false # avoid cloning ethereum/tests - script: - - go run build/ci.go purge -store gethstore/builds -days 14 diff --git a/Dockerfile.amd64 b/Dockerfile.amd64 new file mode 100644 index 000000000000..7a35376c9710 --- /dev/null +++ b/Dockerfile.amd64 @@ -0,0 +1,7 @@ +# Build Geth in a stock Go builder container +FROM golang:1.15.5 as builder + +#RUN apk add --no-cache make gcc musl-dev linux-headers git + +ADD . /go-ethereum +RUN cd /go-ethereum && make geth diff --git a/Makefile b/Makefile index cb5a87dad0ee..214bd54c7627 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,31 @@ .PHONY: geth-darwin geth-darwin-386 geth-darwin-amd64 .PHONY: geth-windows geth-windows-386 geth-windows-amd64 +BIN = $(GOPATH)/bin + +## Migration tool +GOOSE = $(BIN)/goose +$(BIN)/goose: + go get -u github.com/pressly/goose/cmd/goose + GOBIN = ./build/bin GO ?= latest GORUN = env GO111MODULE=on go run +#Database +HOST_NAME = localhost +PORT = 5432 +USER = postgres +PASSWORD = password + +# Set env variable +# `PGPASSWORD` is used by `createdb` and `dropdb` +export PGPASSWORD=$(PASSWORD) + +#Test +TEST_DB = vulcanize_testing +TEST_CONNECT_STRING = postgresql://$(USER):$(PASSWORD)@$(HOST_NAME):$(PORT)/$(TEST_DB)?sslmode=disable + geth: $(GORUN) build/ci.go install ./cmd/geth @echo "Done building." @@ -32,6 +53,15 @@ ios: @echo "Done building." @echo "Import \"$(GOBIN)/Geth.framework\" to use the library." + +.PHONY: statedifftest +statedifftest: | $(GOOSE) + dropdb -h $(HOST_NAME) -p $(PORT) -U $(USER) --if-exists $(TEST_DB) + createdb -h $(HOST_NAME) -p $(PORT) -U $(USER) $(TEST_DB) + $(GOOSE) -dir ./statediff/db/migrations postgres "$(TEST_CONNECT_STRING)" up + @echo " > \033[32mRunning StateDiff Tests...\033[0m " + MODE=statediff go test ./statediff/... -v + test: all $(GORUN) build/ci.go test diff --git a/cmd/geth/config.go b/cmd/geth/config.go index c4ebf648813b..9138c252eac6 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -25,6 +25,8 @@ import ( "reflect" "unicode" + "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/statediff" "gopkg.in/urfave/cli.v1" "github.com/ethereum/go-ethereum/cmd/utils" @@ -134,6 +136,9 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name) } applyMetricConfig(ctx, &cfg) + if ctx.GlobalBool(utils.StateDiffFlag.Name) { + cfg.Eth.Diffing = true + } return stack, cfg } @@ -144,6 +149,11 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { if ctx.GlobalIsSet(utils.OverrideLondonFlag.Name) { cfg.Eth.OverrideLondon = new(big.Int).SetUint64(ctx.GlobalUint64(utils.OverrideLondonFlag.Name)) } + + if cfg.Eth.SyncMode == downloader.LightSync { + return makeLightNode(ctx, stack, cfg) + } + backend, eth := utils.RegisterEthService(stack, &cfg.Eth) // Configure catalyst. @@ -156,6 +166,34 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { } } + if ctx.GlobalBool(utils.StateDiffFlag.Name) { + var dbParams *statediff.DBParams + if ctx.GlobalIsSet(utils.StateDiffDBFlag.Name) { + dbParams = new(statediff.DBParams) + dbParams.ConnectionURL = ctx.GlobalString(utils.StateDiffDBFlag.Name) + if ctx.GlobalIsSet(utils.StateDiffDBNodeIDFlag.Name) { + dbParams.ID = ctx.GlobalString(utils.StateDiffDBNodeIDFlag.Name) + } else { + utils.Fatalf("Must specify node ID for statediff DB output") + } + if ctx.GlobalIsSet(utils.StateDiffDBClientNameFlag.Name) { + dbParams.ClientName = ctx.GlobalString(utils.StateDiffDBClientNameFlag.Name) + } else { + utils.Fatalf("Must specify client name for statediff DB output") + } + } else { + if ctx.GlobalBool(utils.StateDiffWritingFlag.Name) { + utils.Fatalf("Must pass DB parameters if enabling statediff write loop") + } + } + p := statediff.ServiceParams{ + DBParams: dbParams, + EnableWriteLoop: ctx.GlobalBool(utils.StateDiffWritingFlag.Name), + NumWorkers: ctx.GlobalUint(utils.StateDiffWorkersFlag.Name), + } + utils.RegisterStateDiffService(stack, eth, &cfg.Eth, p) + } + // Configure GraphQL if requested if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) { utils.RegisterGraphQLService(stack, backend, cfg.Node) @@ -167,6 +205,20 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { return stack, backend } +func makeLightNode(ctx *cli.Context, stack *node.Node, cfg gethConfig) (*node.Node, ethapi.Backend) { + backend := utils.RegisterLesEthService(stack, &cfg.Eth) + + // Configure GraphQL if requested + if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) { + utils.RegisterGraphQLService(stack, backend.ApiBackend, cfg.Node) + } + // Add the Ethereum Stats daemon if requested. + if cfg.Ethstats.URL != "" { + utils.RegisterEthStatsService(stack, backend.ApiBackend, cfg.Ethstats.URL) + } + return stack, backend.ApiBackend +} + // dumpConfig is the dumpconfig command. func dumpConfig(ctx *cli.Context) error { _, cfg := makeConfigNode(ctx) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 79dce67483d4..a80cd0463e41 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -151,6 +151,12 @@ var ( utils.EWASMInterpreterFlag, utils.EVMInterpreterFlag, utils.MinerNotifyFullFlag, + utils.StateDiffFlag, + utils.StateDiffDBFlag, + utils.StateDiffDBNodeIDFlag, + utils.StateDiffDBClientNameFlag, + utils.StateDiffWritingFlag, + utils.StateDiffWorkersFlag, configFileFlag, utils.CatalystFlag, } diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 44444bdd5687..6fd33eba9924 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -230,6 +230,17 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.LegacyRPCApiFlag, }, }, + { + Name: "STATE DIFF", + Flags: []cli.Flag{ + utils.StateDiffFlag, + utils.StateDiffDBFlag, + utils.StateDiffDBNodeIDFlag, + utils.StateDiffDBClientNameFlag, + utils.StateDiffWritingFlag, + utils.StateDiffWorkersFlag, + }, + }, { Name: "MISC", Flags: []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index cc5af5625cc4..aae11b87c586 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -66,6 +66,8 @@ import ( "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/statediff" + pcsclite "github.com/gballet/go-libpcsclite" gopsutil "github.com/shirou/gopsutil/mem" "gopkg.in/urfave/cli.v1" @@ -775,6 +777,30 @@ var ( Name: "catalyst", Usage: "Catalyst mode (eth2 integration testing)", } + StateDiffFlag = cli.BoolFlag{ + Name: "statediff", + Usage: "Enables the processing of state diffs between each block", + } + StateDiffDBFlag = cli.StringFlag{ + Name: "statediff.db", + Usage: "PostgreSQL database connection string for writing state diffs", + } + StateDiffDBNodeIDFlag = cli.StringFlag{ + Name: "statediff.dbnodeid", + Usage: "Node ID to use when writing state diffs to database", + } + StateDiffDBClientNameFlag = cli.StringFlag{ + Name: "statediff.dbclientname", + Usage: "Client name to use when writing state diffs to database", + } + StateDiffWritingFlag = cli.BoolFlag{ + Name: "statediff.writing", + Usage: "Activates progressive writing of state diffs to database as new block are synced", + } + StateDiffWorkersFlag = cli.UintFlag{ + Name: "statediff.workers", + Usage: "Number of concurrent workers to use during statediff processing (0 = 1)", + } ) // MakeDataDir retrieves the currently requested data directory, terminating @@ -1015,6 +1041,10 @@ func setWS(ctx *cli.Context, cfg *node.Config) { if ctx.GlobalIsSet(WSPathPrefixFlag.Name) { cfg.WSPathPrefix = ctx.GlobalString(WSPathPrefixFlag.Name) } + + if ctx.GlobalBool(StateDiffFlag.Name) { + cfg.WSModules = append(cfg.WSModules, "statediff") + } } // setIPC creates an IPC path configuration from the set command line flags, @@ -1742,6 +1772,15 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend return backend.APIBackend, backend } +// RegisterLesEthService adds an Ethereum les client to the stack. +func RegisterLesEthService(stack *node.Node, cfg *eth.Config) *les.LightEthereum { + backend, err := les.New(stack, cfg) + if err != nil { + Fatalf("Failed to register the Ethereum service: %v", err) + } + return backend +} + // RegisterEthStatsService configures the Ethereum Stats daemon and adds it to // the given node. func RegisterEthStatsService(stack *node.Node, backend ethapi.Backend, url string) { @@ -1757,6 +1796,13 @@ func RegisterGraphQLService(stack *node.Node, backend ethapi.Backend, cfg node.C } } +// RegisterStateDiffService configures and registers a service to stream state diff data over RPC +func RegisterStateDiffService(stack *node.Node, ethServ *eth.Ethereum, cfg *ethconfig.Config, params statediff.ServiceParams) { + if err := statediff.New(stack, ethServ, cfg, params); err != nil { + Fatalf("Failed to register the Statediff service: %v", err) + } +} + func SetupMetrics(ctx *cli.Context) { if metrics.Enabled { log.Info("Enabling metrics collection") diff --git a/core/blockchain.go b/core/blockchain.go index 18e126657c49..e1c16526cdd9 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -131,6 +131,7 @@ type CacheConfig struct { Preimages bool // Whether to store preimage of trie key to the disk SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it + StateDiffing bool // Whether or not the statediffing service is running } // defaultCacheConfig are the default caching values if none are specified by the @@ -209,6 +210,10 @@ type BlockChain struct { shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. terminateInsert func(common.Hash, uint64) bool // Testing hook used to terminate ancient receipt chain insertion. + + // Locked roots and their mutex + trieLock sync.Mutex + lockedRoots map[common.Hash]bool } // NewBlockChain returns a fully initialised block chain using information @@ -245,6 +250,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par futureBlocks: futureBlocks, engine: engine, vmConfig: vmConfig, + lockedRoots: make(map[common.Hash]bool), } bc.validator = NewBlockValidator(chainConfig, bc, engine) bc.prefetcher = newStatePrefetcher(chainConfig, bc, engine) @@ -1031,7 +1037,10 @@ func (bc *BlockChain) Stop() { } } for !bc.triegc.Empty() { - triedb.Dereference(bc.triegc.PopItem().(common.Hash)) + pruneRoot := bc.triegc.PopItem().(common.Hash) + if !bc.TrieLocked(pruneRoot) { + triedb.Dereference(pruneRoot) + } } if size, _ := triedb.Size(); size != 0 { log.Error("Dangling trie nodes after full cleanup") @@ -1488,6 +1497,11 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. triedb.Reference(root, common.Hash{}) // metadata reference to keep trie alive bc.triegc.Push(root, -int64(block.NumberU64())) + // If we are statediffing, lock the trie until the statediffing service is done using it + if bc.cacheConfig.StateDiffing { + bc.LockTrie(root) + } + if current := block.NumberU64(); current > TriesInMemory { // If we exceeded our memory allowance, flush matured singleton nodes to disk var ( @@ -1526,7 +1540,11 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. bc.triegc.Push(root, number) break } - triedb.Dereference(root.(common.Hash)) + pruneRoot := root.(common.Hash) + if !bc.TrieLocked(pruneRoot) { + log.Debug("Dereferencing", "root", root.(common.Hash).Hex()) + triedb.Dereference(pruneRoot) + } } } } @@ -2510,3 +2528,28 @@ func (bc *BlockChain) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscript func (bc *BlockChain) SubscribeBlockProcessingEvent(ch chan<- bool) event.Subscription { return bc.scope.Track(bc.blockProcFeed.Subscribe(ch)) } + +// TrieLocked returns whether the trie associated with the provided root is locked for use +func (bc *BlockChain) TrieLocked(root common.Hash) bool { + bc.trieLock.Lock() + locked, ok := bc.lockedRoots[root] + bc.trieLock.Unlock() + if !ok { + return false + } + return locked +} + +// LockTrie prevents dereferencing of the provided root +func (bc *BlockChain) LockTrie(root common.Hash) { + bc.trieLock.Lock() + bc.lockedRoots[root] = true + bc.trieLock.Unlock() +} + +// UnlockTrie allows dereferencing of the provided root- provided it was previously locked +func (bc *BlockChain) UnlockTrie(root common.Hash) { + bc.trieLock.Lock() + bc.lockedRoots[root] = false + bc.trieLock.Unlock() +} diff --git a/core/types/receipt.go b/core/types/receipt.go index b949bd2bd5f3..4e06b306389f 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -115,6 +115,9 @@ func NewReceipt(root []byte, failed bool, cumulativeGasUsed uint64) *Receipt { // EncodeRLP implements rlp.Encoder, and flattens the consensus fields of a receipt // into an RLP stream. If no post state is present, byzantium fork is assumed. +// For a legacy Receipt this returns RLP([PostStateOrStatus, CumulativeGasUsed, Bloom, Logs]) +// For a EIP-2718 Receipt this returns RLP(TxType || ReceiptPayload) +// For a EIP-2930 Receipt, TxType == 0x01 and ReceiptPayload == RLP([PostStateOrStatus, CumulativeGasUsed, Bloom, Logs]) func (r *Receipt) EncodeRLP(w io.Writer) error { data := &receiptRLP{r.statusEncoding(), r.CumulativeGasUsed, r.Bloom, r.Logs} if r.Type == LegacyTxType { @@ -123,13 +126,32 @@ func (r *Receipt) EncodeRLP(w io.Writer) error { buf := encodeBufferPool.Get().(*bytes.Buffer) defer encodeBufferPool.Put(buf) buf.Reset() - buf.WriteByte(r.Type) - if err := rlp.Encode(buf, data); err != nil { + if err := r.encodeTyped(data, buf); err != nil { return err } return rlp.Encode(w, buf.Bytes()) } +// encodeTyped writes the canonical encoding of a typed receipt to w. +func (r *Receipt) encodeTyped(data *receiptRLP, w *bytes.Buffer) error { + w.WriteByte(r.Type) + return rlp.Encode(w, data) +} + +// MarshalBinary returns the canonical consensus encoding of the receipt. +// For a legacy Receipt this returns RLP([PostStateOrStatus, CumulativeGasUsed, Bloom, Logs]) +// For a EIP-2718 Receipt this returns TxType || ReceiptPayload +// For a EIP-2930, TxType == 0x01 and ReceiptPayload == RLP([PostStateOrStatus, CumulativeGasUsed, Bloom, Logs]) +func (r *Receipt) MarshalBinary() ([]byte, error) { + if r.Type == LegacyTxType { + return rlp.EncodeToBytes(r) + } + data := &receiptRLP{r.statusEncoding(), r.CumulativeGasUsed, r.Bloom, r.Logs} + var buf bytes.Buffer + err := r.encodeTyped(data, &buf) + return buf.Bytes(), err +} + // DecodeRLP implements rlp.Decoder, and loads the consensus fields of a receipt // from an RLP stream. func (r *Receipt) DecodeRLP(s *rlp.Stream) error { @@ -168,6 +190,42 @@ func (r *Receipt) DecodeRLP(s *rlp.Stream) error { } } +// UnmarshalBinary decodes the canonical encoding of receipts. +// It supports legacy RLP receipts and EIP-2718 typed receipts. +func (r *Receipt) UnmarshalBinary(b []byte) error { + if len(b) > 0 && b[0] > 0x7f { + // It's a legacy receipt decode the RLP + var data receiptRLP + err := rlp.DecodeBytes(b, &data) + if err != nil { + return err + } + r.Type = LegacyTxType + return r.setFromRLP(data) + } + // It's an EIP2718 typed transaction envelope. + return r.decodeTyped(b) +} + +// decodeTyped decodes a typed receipt from the canonical format. +func (r *Receipt) decodeTyped(b []byte) error { + if len(b) == 0 { + return errEmptyTypedReceipt + } + switch b[0] { + case AccessListTxType: + var data receiptRLP + err := rlp.DecodeBytes(b[1:], &data) + if err != nil { + return err + } + r.Type = AccessListTxType + return r.setFromRLP(data) + default: + return ErrTxTypeNotSupported + } +} + func (r *Receipt) setFromRLP(data receiptRLP) error { r.CumulativeGasUsed, r.Bloom, r.Logs = data.CumulativeGasUsed, data.Bloom, data.Logs return r.setStatus(data.PostStateOrStatus) @@ -271,42 +329,42 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { // DeriveFields fills the receipts with their computed fields based on consensus // data and contextual infos like containing block and transactions. -func (r Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, number uint64, txs Transactions) error { +func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, number uint64, txs Transactions) error { signer := MakeSigner(config, new(big.Int).SetUint64(number)) logIndex := uint(0) - if len(txs) != len(r) { + if len(txs) != len(rs) { return errors.New("transaction and receipt count mismatch") } - for i := 0; i < len(r); i++ { + for i := 0; i < len(rs); i++ { // The transaction type and hash can be retrieved from the transaction itself - r[i].Type = txs[i].Type() - r[i].TxHash = txs[i].Hash() + rs[i].Type = txs[i].Type() + rs[i].TxHash = txs[i].Hash() // block location fields - r[i].BlockHash = hash - r[i].BlockNumber = new(big.Int).SetUint64(number) - r[i].TransactionIndex = uint(i) + rs[i].BlockHash = hash + rs[i].BlockNumber = new(big.Int).SetUint64(number) + rs[i].TransactionIndex = uint(i) // The contract address can be derived from the transaction itself if txs[i].To() == nil { // Deriving the signer is expensive, only do if it's actually needed from, _ := Sender(signer, txs[i]) - r[i].ContractAddress = crypto.CreateAddress(from, txs[i].Nonce()) + rs[i].ContractAddress = crypto.CreateAddress(from, txs[i].Nonce()) } // The used gas can be calculated based on previous r if i == 0 { - r[i].GasUsed = r[i].CumulativeGasUsed + rs[i].GasUsed = rs[i].CumulativeGasUsed } else { - r[i].GasUsed = r[i].CumulativeGasUsed - r[i-1].CumulativeGasUsed + rs[i].GasUsed = rs[i].CumulativeGasUsed - rs[i-1].CumulativeGasUsed } // The derived log fields can simply be set from the block and transaction - for j := 0; j < len(r[i].Logs); j++ { - r[i].Logs[j].BlockNumber = number - r[i].Logs[j].BlockHash = hash - r[i].Logs[j].TxHash = r[i].TxHash - r[i].Logs[j].TxIndex = uint(i) - r[i].Logs[j].Index = logIndex + for j := 0; j < len(rs[i].Logs); j++ { + rs[i].Logs[j].BlockNumber = number + rs[i].Logs[j].BlockHash = hash + rs[i].Logs[j].TxHash = rs[i].TxHash + rs[i].Logs[j].TxIndex = uint(i) + rs[i].Logs[j].Index = logIndex logIndex++ } } diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 87fc16a5105b..31e9ac4a4feb 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -20,6 +20,7 @@ import ( "bytes" "math" "math/big" + "reflect" "testing" "github.com/ethereum/go-ethereum/common" @@ -28,6 +29,42 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +var ( + legacyReceipt = &Receipt{ + Status: ReceiptStatusFailed, + CumulativeGasUsed: 1, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + { + Address: common.BytesToAddress([]byte{0x01, 0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + }, + } + accessListReceipt = &Receipt{ + Status: ReceiptStatusFailed, + CumulativeGasUsed: 1, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + { + Address: common.BytesToAddress([]byte{0x01, 0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + }, + Type: AccessListTxType, + } +) + func TestDecodeEmptyTypedReceipt(t *testing.T) { input := []byte{0x80} var r Receipt @@ -37,6 +74,76 @@ func TestDecodeEmptyTypedReceipt(t *testing.T) { } } +func TestReceiptMarshalBinary(t *testing.T) { + // Legacy Receipt + legacyReceipt.Bloom = CreateBloom(Receipts{legacyReceipt}) + have, err := legacyReceipt.MarshalBinary() + if err != nil { + t.Fatalf("marshal binary error: %v", err) + } + legacyReceipts := Receipts{legacyReceipt} + buf := new(bytes.Buffer) + legacyReceipts.EncodeIndex(0, buf) + haveEncodeIndex := buf.Bytes() + if !bytes.Equal(have, haveEncodeIndex) { + t.Errorf("BinaryMarshal and EncodeIndex mismatch, got %x want %x", have, haveEncodeIndex) + } + buf.Reset() + if err := legacyReceipt.EncodeRLP(buf); err != nil { + t.Fatalf("encode rlp error: %v", err) + } + haveRLPEncode := buf.Bytes() + if !bytes.Equal(have, haveRLPEncode) { + t.Errorf("BinaryMarshal and EncodeRLP mismatch for legacy tx, got %x want %x", have, haveRLPEncode) + } + legacyWant := common.FromHex("f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + if !bytes.Equal(have, legacyWant) { + t.Errorf("encoded RLP mismatch, got %x want %x", have, legacyWant) + } + + // 2930 Receipt + buf.Reset() + accessListReceipt.Bloom = CreateBloom(Receipts{accessListReceipt}) + have, err = accessListReceipt.MarshalBinary() + if err != nil { + t.Fatalf("marshal binary error: %v", err) + } + accessListReceipts := Receipts{accessListReceipt} + accessListReceipts.EncodeIndex(0, buf) + haveEncodeIndex = buf.Bytes() + if !bytes.Equal(have, haveEncodeIndex) { + t.Errorf("BinaryMarshal and EncodeIndex mismatch, got %x want %x", have, haveEncodeIndex) + } + accessListWant := common.FromHex("01f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + if !bytes.Equal(have, accessListWant) { + t.Errorf("encoded RLP mismatch, got %x want %x", have, accessListWant) + } +} + +func TestReceiptUnmarshalBinary(t *testing.T) { + // Legacy Receipt + legacyBinary := common.FromHex("f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + gotLegacyReceipt := new(Receipt) + if err := gotLegacyReceipt.UnmarshalBinary(legacyBinary); err != nil { + t.Fatalf("unmarshal binary error: %v", err) + } + legacyReceipt.Bloom = CreateBloom(Receipts{legacyReceipt}) + if !reflect.DeepEqual(gotLegacyReceipt, legacyReceipt) { + t.Errorf("receipt unmarshalled from binary mismatch, got %v want %v", gotLegacyReceipt, legacyReceipt) + } + + // 2930 Receipt + accessListBinary := common.FromHex("01f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + gotAccessListReceipt := new(Receipt) + if err := gotAccessListReceipt.UnmarshalBinary(accessListBinary); err != nil { + t.Fatalf("unmarshal binary error: %v", err) + } + accessListReceipt.Bloom = CreateBloom(Receipts{accessListReceipt}) + if !reflect.DeepEqual(gotAccessListReceipt, accessListReceipt) { + t.Errorf("receipt unmarshalled from binary mismatch, got %v want %v", gotAccessListReceipt, accessListReceipt) + } +} + // Tests that receipt data can be correctly derived from the contextual infos func TestDeriveFields(t *testing.T) { // Create a few transactions to have receipts for diff --git a/core/types/transaction.go b/core/types/transaction.go index 920318a16e07..d2896f856e54 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -88,6 +88,9 @@ type TxData interface { } // EncodeRLP implements rlp.Encoder +// For a legacy Transaction this returns RLP([AccountNonce, GasPrice, GasLimit, Recipient, Amount, Data, V, R, S]) +// For a EIP-2718 Transaction this returns RLP(TxType || TxPayload) +// For a EIP-2930 Transaction, TxType == 0x01 and TxPayload == RLP([ChainID, AccountNonce, GasPrice, GasLimit, Recipient, Amount, Data, AccessList, V, R, S] func (tx *Transaction) EncodeRLP(w io.Writer) error { if tx.Type() == LegacyTxType { return rlp.Encode(w, tx.inner) @@ -108,9 +111,10 @@ func (tx *Transaction) encodeTyped(w *bytes.Buffer) error { return rlp.Encode(w, tx.inner) } -// MarshalBinary returns the canonical encoding of the transaction. -// For legacy transactions, it returns the RLP encoding. For EIP-2718 typed -// transactions, it returns the type and payload. +// MarshalBinary returns the canonical consensus encoding of the transaction. +// For a legacy Transaction this returns RLP([AccountNonce, GasPrice, GasLimit, Recipient, Amount, Data, V, R, S]) +// For a EIP-2718 Transaction this returns TxType || TxPayload +// For a EIP-2930 Transaction, TxType == 0x01 and TxPayload == RLP([ChainID, AccountNonce, GasPrice, GasLimit, Recipient, Amount, Data, AccessList, V, R, S] func (tx *Transaction) MarshalBinary() ([]byte, error) { if tx.Type() == LegacyTxType { return rlp.EncodeToBytes(tx.inner) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000000..7a2d62d2de9c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.2' + +services: + db: + restart: always + image: postgres:10.12-alpine + environment: + POSTGRES_USER: "postgres" + POSTGRES_DB: "vulcanize_testing" + POSTGRES_PASSWORD: "password" + volumes: + - geth_node:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + +volumes: + geth_node: \ No newline at end of file diff --git a/eth/backend.go b/eth/backend.go index 3e770fe83d92..ec6013edb3a2 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -188,6 +188,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { TrieTimeLimit: config.TrieTimeout, SnapshotLimit: config.SnapshotCache, Preimages: config.Preimages, + StateDiffing: config.Diffing, } ) eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve, &config.TxLookupLimit) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 349c8da6cc23..b36daad5941c 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -203,6 +203,10 @@ type Config struct { // Berlin block override (TODO: remove after the fork) OverrideLondon *big.Int `toml:",omitempty"` + + // Signify whether or not we are producing statediffs + // If we are, do not dereference state roots until the statediffing service is done with them + Diffing bool } // CreateConsensusEngine creates a consensus engine for the given chain configuration. diff --git a/go.mod b/go.mod index d35ac1c573ff..158e6a89728f 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff github.com/go-ole/go-ole v1.2.1 // indirect github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect + github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-stack/stack v1.8.0 github.com/golang/protobuf v1.4.3 github.com/golang/snappy v0.0.3 @@ -39,13 +40,22 @@ require ( github.com/holiman/uint256 v1.2.0 github.com/huin/goupnp v1.0.1-0.20210310174557-0ca763054c88 github.com/influxdata/influxdb v1.8.3 + github.com/ipfs/go-block-format v0.0.2 + github.com/ipfs/go-cid v0.0.7 + github.com/ipfs/go-ipfs-blockstore v1.0.1 + github.com/ipfs/go-ipfs-ds-help v1.0.0 + github.com/ipfs/go-ipld-format v0.2.0 github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e + github.com/jmoiron/sqlx v1.2.0 github.com/julienschmidt/httprouter v1.2.0 github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356 github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.0 - github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 + github.com/lib/pq v1.10.2 + github.com/mattn/go-colorable v0.1.1 + github.com/mattn/go-isatty v0.0.5 + github.com/mattn/go-sqlite3 v1.14.7 // indirect + github.com/multiformats/go-multihash v0.0.14 github.com/naoina/go-stringutil v0.1.0 // indirect github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 github.com/olekukonko/tablewriter v0.0.5 @@ -59,11 +69,12 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef - golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007 golang.org/x/text v0.3.4 golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 + golang.org/x/tools v0.1.2 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6 gopkg.in/urfave/cli.v1 v1.20.0 diff --git a/go.sum b/go.sum index 889d47e20e4f..c73d3f9ce2ee 100644 --- a/go.sum +++ b/go.sum @@ -149,11 +149,16 @@ github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug= github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= @@ -195,6 +200,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -204,8 +210,11 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29 h1:sezaKhEfPFg8W0Enm61B9Gs911H8iesGY5R8NDPtd1M= github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU= +github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -227,13 +236,42 @@ github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19y github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= +github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= +github.com/ipfs/go-block-format v0.0.2 h1:qPDvcP19izTjU8rgo6p7gTXZlkMkF5bz5G3fqIsSCPE= +github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY= +github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= +github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= +github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog= +github.com/ipfs/go-cid v0.0.7 h1:ysQJVJA3fNDF1qigJbsSQOdjhVLsOEoPdh0+R97k3jY= +github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/ipfs/go-datastore v0.4.1/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA= +github.com/ipfs/go-datastore v0.4.2 h1:h8/n7WPzhp239kkLws+epN3Ic7YtcBPgcaXfEfdVDWM= +github.com/ipfs/go-datastore v0.4.2/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA= +github.com/ipfs/go-ipfs-blockstore v1.0.1 h1:fnuVj4XdZp4yExhd0CnUwAiMNJHiPnfInhiuwz4lW1w= +github.com/ipfs/go-ipfs-blockstore v1.0.1/go.mod h1:MGNZlHNEnR4KGgPHM3/k8lBySIOK2Ve+0KjZubKlaOE= +github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= +github.com/ipfs/go-ipfs-ds-help v1.0.0 h1:bEQ8hMGs80h0sR8O4tfDgV6B01aaF9qeTrujrTLYV3g= +github.com/ipfs/go-ipfs-ds-help v1.0.0/go.mod h1:ujAbkeIgkKAWtxxNkoZHWLCyk5JpPoKnGyCcsoF6ueE= +github.com/ipfs/go-ipfs-util v0.0.1 h1:Wz9bL2wB2YBJqggkA4dD7oSmqB4cAnpNbGrlHJulv50= +github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc= +github.com/ipfs/go-ipld-format v0.2.0 h1:xGlJKkArkmBvowr+GMCX0FEZtkro71K1AwiKnL37mwA= +github.com/ipfs/go-ipld-format v0.2.0/go.mod h1:3l3C1uKoadTPbeNfrDi+xMInYKlx2Cvg1BuydPSdzQs= +github.com/ipfs/go-log v0.0.1 h1:9XTUN/rW64BCG1YhPK9Hoy3q8nr4gOmHHBpgFdfw6Lc= +github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM= +github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= +github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 h1:6OvNmYgJyexcZ3pYbTI9jWx5tHo1Dee/tWbLMfPe2TA= github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/goprocess v0.0.0-20160826012719-b497e2f366b8 h1:bspPhN+oKYFk5fcGNuQzp6IGzYQSenLEgH3s6jkXrWw= +github.com/jbenet/goprocess v0.0.0-20160826012719-b497e2f366b8/go.mod h1:Ly/wlsjFq/qrU3Rar62tu1gASgGw6chQbSh/XgIIXCY= github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e h1:UvSe12bq+Uj2hWd8aOlwPmoZ+CITRFrdit+sDGfAg8U= github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -246,6 +284,7 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356 h1:I/yrLt2WilKxlQKCM52clh5rGzTKpVctGT1lH4Dc8Jw= github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= @@ -266,24 +305,51 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= -github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d h1:oNAwILwmgWKFpuU+dXvI6dl9jG2mAWAZLX3r9s0PPiw= github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 h1:USWjF42jDCSEeikX/G1g40ZWnsPXN5WkZ4jMHZWyBK4= -github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= +github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771 h1:MHkK1uRtFbVqvAgvWxafZe54+5uBxLluGylDiKgdhwo= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= +github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.14 h1:QoBceQYQQtNUuf6s7wHxnE2c8bhbMqhfGzNI032se/I= +github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= @@ -349,6 +415,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -372,25 +440,33 @@ github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZF github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4= github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc h1:9lDbC6Rz4bwmou+oE6Dt4Cb2BGMur5eR/GYptkKUVHo= +github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -411,6 +487,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -418,6 +495,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -426,6 +504,7 @@ golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -439,8 +518,9 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -462,6 +542,8 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -483,8 +565,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988 h1:EjgCl+fVlIaPJSori0ikSz3uV0DOHKWOJFpv1sAAhBM= golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -497,6 +582,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -516,6 +602,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -524,6 +611,8 @@ golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -575,8 +664,9 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= diff --git a/mobile/android_test.go b/mobile/android_test.go index c85314c15725..f61b145cfb3f 100644 --- a/mobile/android_test.go +++ b/mobile/android_test.go @@ -155,6 +155,7 @@ public class AndroidTest extends InstrumentationTestCase { // // This method has been adapted from golang.org/x/mobile/bind/java/seq_test.go/runTest func TestAndroid(t *testing.T) { + t.Skip("Skipping this test for statediff as this is not relevant") // Skip tests on Windows altogether if runtime.GOOS == "windows" { t.Skip("cannot test Android bindings on Windows, skipping") diff --git a/params/version.go b/params/version.go index 9ce38916841d..1255213dde24 100644 --- a/params/version.go +++ b/params/version.go @@ -21,10 +21,10 @@ import ( ) const ( - VersionMajor = 1 // Major version component of the current release - VersionMinor = 10 // Minor version component of the current release - VersionPatch = 4 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionMajor = 1 // Major version component of the current release + VersionMinor = 10 // Minor version component of the current release + VersionPatch = 4 // Patch version component of the current release + VersionMeta = "statediff-0.0.23" // Version metadata to append to the version string ) // Version holds the textual version string. diff --git a/rpc/http.go b/rpc/http.go index 32f4e7d90a25..4c845dc1ffa3 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -32,7 +32,7 @@ import ( ) const ( - maxRequestContentLength = 1024 * 1024 * 5 + maxRequestContentLength = 1024 * 1024 * 12 contentType = "application/json" ) diff --git a/statediff/api.go b/statediff/api.go new file mode 100644 index 000000000000..923a0073f102 --- /dev/null +++ b/statediff/api.go @@ -0,0 +1,151 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statediff + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + . "github.com/ethereum/go-ethereum/statediff/types" +) + +// APIName is the namespace used for the state diffing service API +const APIName = "statediff" + +// APIVersion is the version of the state diffing service API +const APIVersion = "0.0.1" + +// PublicStateDiffAPI provides an RPC subscription interface +// that can be used to stream out state diffs as they +// are produced by a full node +type PublicStateDiffAPI struct { + sds IService +} + +// NewPublicStateDiffAPI creates an rpc subscription interface for the underlying statediff service +func NewPublicStateDiffAPI(sds IService) *PublicStateDiffAPI { + return &PublicStateDiffAPI{ + sds: sds, + } +} + +// Stream is the public method to setup a subscription that fires off statediff service payloads as they are created +func (api *PublicStateDiffAPI) Stream(ctx context.Context, params Params) (*rpc.Subscription, error) { + // ensure that the RPC connection supports subscriptions + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return nil, rpc.ErrNotificationsUnsupported + } + + // create subscription and start waiting for events + rpcSub := notifier.CreateSubscription() + + go func() { + // subscribe to events from the statediff service + payloadChannel := make(chan Payload, chainEventChanSize) + quitChan := make(chan bool, 1) + api.sds.Subscribe(rpcSub.ID, payloadChannel, quitChan, params) + // loop and await payloads and relay them to the subscriber with the notifier + for { + select { + case payload := <-payloadChannel: + if err := notifier.Notify(rpcSub.ID, payload); err != nil { + log.Error("Failed to send state diff packet; error: " + err.Error()) + if err := api.sds.Unsubscribe(rpcSub.ID); err != nil { + log.Error("Failed to unsubscribe from the state diff service; error: " + err.Error()) + } + return + } + case err := <-rpcSub.Err(): + if err != nil { + log.Error("State diff service rpcSub error: " + err.Error()) + err = api.sds.Unsubscribe(rpcSub.ID) + if err != nil { + log.Error("Failed to unsubscribe from the state diff service; error: " + err.Error()) + } + return + } + case <-quitChan: + // don't need to unsubscribe, service does so before sending the quit signal + return + } + } + }() + + return rpcSub, nil +} + +// StateDiffAt returns a state diff payload at the specific blockheight +func (api *PublicStateDiffAPI) StateDiffAt(ctx context.Context, blockNumber uint64, params Params) (*Payload, error) { + return api.sds.StateDiffAt(blockNumber, params) +} + +// StateDiffFor returns a state diff payload for the specific blockhash +func (api *PublicStateDiffAPI) StateDiffFor(ctx context.Context, blockHash common.Hash, params Params) (*Payload, error) { + return api.sds.StateDiffFor(blockHash, params) +} + +// StateTrieAt returns a state trie payload at the specific blockheight +func (api *PublicStateDiffAPI) StateTrieAt(ctx context.Context, blockNumber uint64, params Params) (*Payload, error) { + return api.sds.StateTrieAt(blockNumber, params) +} + +// StreamCodeAndCodeHash writes all of the codehash=>code pairs out to a websocket channel +func (api *PublicStateDiffAPI) StreamCodeAndCodeHash(ctx context.Context, blockNumber uint64) (*rpc.Subscription, error) { + // ensure that the RPC connection supports subscriptions + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return nil, rpc.ErrNotificationsUnsupported + } + + // create subscription and start waiting for events + rpcSub := notifier.CreateSubscription() + payloadChan := make(chan CodeAndCodeHash, chainEventChanSize) + quitChan := make(chan bool) + api.sds.StreamCodeAndCodeHash(blockNumber, payloadChan, quitChan) + go func() { + for { + select { + case payload := <-payloadChan: + if err := notifier.Notify(rpcSub.ID, payload); err != nil { + log.Error("Failed to send code and codehash packet", "err", err) + return + } + case err := <-rpcSub.Err(): + log.Error("State diff service rpcSub error", "err", err) + return + case <-quitChan: + return + } + } + }() + + return rpcSub, nil +} + +// WriteStateDiffAt writes a state diff object directly to DB at the specific blockheight +func (api *PublicStateDiffAPI) WriteStateDiffAt(ctx context.Context, blockNumber uint64, params Params) error { + return api.sds.WriteStateDiffAt(blockNumber, params) +} + +// WriteStateDiffFor writes a state diff object directly to DB for the specific block hash +func (api *PublicStateDiffAPI) WriteStateDiffFor(ctx context.Context, blockHash common.Hash, params Params) error { + return api.sds.WriteStateDiffFor(blockHash, params) +} diff --git a/statediff/builder.go b/statediff/builder.go new file mode 100644 index 000000000000..49b920ee1872 --- /dev/null +++ b/statediff/builder.go @@ -0,0 +1,768 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains a batch of utility type declarations used by the tests. As the node +// operates on unique types, a lot of them are needed to check various features. + +package statediff + +import ( + "bytes" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + . "github.com/ethereum/go-ethereum/statediff/types" + "github.com/ethereum/go-ethereum/trie" +) + +var ( + nullHashBytes = common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000000") + emptyNode, _ = rlp.EncodeToBytes([]byte{}) + emptyContractRoot = crypto.Keccak256Hash(emptyNode) + nullCodeHash = crypto.Keccak256Hash([]byte{}).Bytes() +) + +// Builder interface exposes the method for building a state diff between two blocks +type Builder interface { + BuildStateDiffObject(args Args, params Params) (StateObject, error) + BuildStateTrieObject(current *types.Block) (StateObject, error) + WriteStateDiffObject(args StateRoots, params Params, output StateNodeSink, codeOutput CodeSink) error +} + +type builder struct { + stateCache state.Database +} + +func resolveNode(it trie.NodeIterator, trieDB *trie.Database) (StateNode, []interface{}, error) { + nodePath := make([]byte, len(it.Path())) + copy(nodePath, it.Path()) + node, err := trieDB.Node(it.Hash()) + if err != nil { + return StateNode{}, nil, err + } + var nodeElements []interface{} + if err := rlp.DecodeBytes(node, &nodeElements); err != nil { + return StateNode{}, nil, err + } + ty, err := CheckKeyType(nodeElements) + if err != nil { + return StateNode{}, nil, err + } + return StateNode{ + NodeType: ty, + Path: nodePath, + NodeValue: node, + }, nodeElements, nil +} + +// convenience +func stateNodeAppender(nodes *[]StateNode) StateNodeSink { + return func(node StateNode) error { + *nodes = append(*nodes, node) + return nil + } +} +func storageNodeAppender(nodes *[]StorageNode) StorageNodeSink { + return func(node StorageNode) error { + *nodes = append(*nodes, node) + return nil + } +} +func codeMappingAppender(codeAndCodeHashes *[]CodeAndCodeHash) CodeSink { + return func(c CodeAndCodeHash) error { + *codeAndCodeHashes = append(*codeAndCodeHashes, c) + return nil + } +} + +// NewBuilder is used to create a statediff builder +func NewBuilder(stateCache state.Database) Builder { + return &builder{ + stateCache: stateCache, // state cache is safe for concurrent reads + } +} + +// BuildStateTrieObject builds a state trie object from the provided block +func (sdb *builder) BuildStateTrieObject(current *types.Block) (StateObject, error) { + currentTrie, err := sdb.stateCache.OpenTrie(current.Root()) + if err != nil { + return StateObject{}, fmt.Errorf("error creating trie for block %d: %v", current.Number(), err) + } + it := currentTrie.NodeIterator([]byte{}) + stateNodes, codeAndCodeHashes, err := sdb.buildStateTrie(it) + if err != nil { + return StateObject{}, fmt.Errorf("error collecting state nodes for block %d: %v", current.Number(), err) + } + return StateObject{ + BlockNumber: current.Number(), + BlockHash: current.Hash(), + Nodes: stateNodes, + CodeAndCodeHashes: codeAndCodeHashes, + }, nil +} + +func (sdb *builder) buildStateTrie(it trie.NodeIterator) ([]StateNode, []CodeAndCodeHash, error) { + stateNodes := make([]StateNode, 0) + codeAndCodeHashes := make([]CodeAndCodeHash, 0) + for it.Next(true) { + // skip value nodes + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + node, nodeElements, err := resolveNode(it, sdb.stateCache.TrieDB()) + if err != nil { + return nil, nil, err + } + switch node.NodeType { + case Leaf: + var account state.Account + if err := rlp.DecodeBytes(nodeElements[1].([]byte), &account); err != nil { + return nil, nil, fmt.Errorf("error decoding account for leaf node at path %x nerror: %v", node.Path, err) + } + partialPath := trie.CompactToHex(nodeElements[0].([]byte)) + valueNodePath := append(node.Path, partialPath...) + encodedPath := trie.HexToCompact(valueNodePath) + leafKey := encodedPath[1:] + node.LeafKey = leafKey + if !bytes.Equal(account.CodeHash, nullCodeHash) { + var storageNodes []StorageNode + err := sdb.buildStorageNodesEventual(account.Root, nil, true, storageNodeAppender(&storageNodes)) + if err != nil { + return nil, nil, fmt.Errorf("failed building eventual storage diffs for account %+v\r\nerror: %v", account, err) + } + node.StorageNodes = storageNodes + // emit codehash => code mappings for cod + codeHash := common.BytesToHash(account.CodeHash) + code, err := sdb.stateCache.ContractCode(common.Hash{}, codeHash) + if err != nil { + return nil, nil, fmt.Errorf("failed to retrieve code for codehash %s\r\n error: %v", codeHash.String(), err) + } + codeAndCodeHashes = append(codeAndCodeHashes, CodeAndCodeHash{ + Hash: codeHash, + Code: code, + }) + } + stateNodes = append(stateNodes, node) + case Extension, Branch: + stateNodes = append(stateNodes, node) + default: + return nil, nil, fmt.Errorf("unexpected node type %s", node.NodeType) + } + } + return stateNodes, codeAndCodeHashes, it.Error() +} + +// BuildStateDiffObject builds a statediff object from two blocks and the provided parameters +func (sdb *builder) BuildStateDiffObject(args Args, params Params) (StateObject, error) { + var stateNodes []StateNode + var codeAndCodeHashes []CodeAndCodeHash + err := sdb.WriteStateDiffObject( + StateRoots{OldStateRoot: args.OldStateRoot, NewStateRoot: args.NewStateRoot}, + params, stateNodeAppender(&stateNodes), codeMappingAppender(&codeAndCodeHashes)) + if err != nil { + return StateObject{}, err + } + return StateObject{ + BlockHash: args.BlockHash, + BlockNumber: args.BlockNumber, + Nodes: stateNodes, + CodeAndCodeHashes: codeAndCodeHashes, + }, nil +} + +// Writes a statediff object to output callback +func (sdb *builder) WriteStateDiffObject(args StateRoots, params Params, output StateNodeSink, codeOutput CodeSink) error { + if !params.IntermediateStateNodes || len(params.WatchedAddresses) > 0 { + // if we are watching only specific accounts then we are only diffing leaf nodes + return sdb.buildStateDiffWithoutIntermediateStateNodes(args, params, output, codeOutput) + } else { + return sdb.buildStateDiffWithIntermediateStateNodes(args, params, output, codeOutput) + } +} + +func (sdb *builder) buildStateDiffWithIntermediateStateNodes(args StateRoots, params Params, output StateNodeSink, codeOutput CodeSink) error { + // Load tries for old and new states + oldTrie, err := sdb.stateCache.OpenTrie(args.OldStateRoot) + if err != nil { + return fmt.Errorf("error creating trie for oldStateRoot: %v", err) + } + newTrie, err := sdb.stateCache.OpenTrie(args.NewStateRoot) + if err != nil { + return fmt.Errorf("error creating trie for newStateRoot: %v", err) + } + + // collect a slice of all the intermediate nodes that were touched and exist at B + // a map of their leafkey to all the accounts that were touched and exist at B + // and a slice of all the paths for the nodes in both of the above sets + diffAccountsAtB, diffPathsAtB, err := sdb.createdAndUpdatedStateWithIntermediateNodes( + oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{}), + output) + if err != nil { + return fmt.Errorf("error collecting createdAndUpdatedNodes: %v", err) + } + + // collect a slice of all the nodes that existed at a path in A that doesn't exist in B + // a map of their leafkey to all the accounts that were touched and exist at A + diffAccountsAtA, err := sdb.deletedOrUpdatedState( + oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{}), + diffPathsAtB, output) + if err != nil { + return fmt.Errorf("error collecting deletedOrUpdatedNodes: %v", err) + } + + // collect and sort the leafkey keys for both account mappings into a slice + createKeys := sortKeys(diffAccountsAtB) + deleteKeys := sortKeys(diffAccountsAtA) + + // and then find the intersection of these keys + // these are the leafkeys for the accounts which exist at both A and B but are different + // this also mutates the passed in createKeys and deleteKeys, removing the intersection keys + // and leaving the truly created or deleted keys in place + updatedKeys := findIntersection(createKeys, deleteKeys) + + // build the diff nodes for the updated accounts using the mappings at both A and B as directed by the keys found as the intersection of the two + err = sdb.buildAccountUpdates( + diffAccountsAtB, diffAccountsAtA, updatedKeys, + params.WatchedStorageSlots, params.IntermediateStorageNodes, output) + if err != nil { + return fmt.Errorf("error building diff for updated accounts: %v", err) + } + // build the diff nodes for created accounts + err = sdb.buildAccountCreations(diffAccountsAtB, params.WatchedStorageSlots, params.IntermediateStorageNodes, output, codeOutput) + if err != nil { + return fmt.Errorf("error building diff for created accounts: %v", err) + } + return nil +} + +func (sdb *builder) buildStateDiffWithoutIntermediateStateNodes(args StateRoots, params Params, output StateNodeSink, codeOutput CodeSink) error { + // Load tries for old (A) and new (B) states + oldTrie, err := sdb.stateCache.OpenTrie(args.OldStateRoot) + if err != nil { + return fmt.Errorf("error creating trie for oldStateRoot: %v", err) + } + newTrie, err := sdb.stateCache.OpenTrie(args.NewStateRoot) + if err != nil { + return fmt.Errorf("error creating trie for newStateRoot: %v", err) + } + + // collect a map of their leafkey to all the accounts that were touched and exist at B + // and a slice of all the paths for the nodes in both of the above sets + diffAccountsAtB, diffPathsAtB, err := sdb.createdAndUpdatedState( + oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{}), + params.WatchedAddresses) + if err != nil { + return fmt.Errorf("error collecting createdAndUpdatedNodes: %v", err) + } + + // collect a slice of all the nodes that existed at a path in A that doesn't exist in B + // a map of their leafkey to all the accounts that were touched and exist at A + diffAccountsAtA, err := sdb.deletedOrUpdatedState( + oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{}), + diffPathsAtB, output) + if err != nil { + return fmt.Errorf("error collecting deletedOrUpdatedNodes: %v", err) + } + + // collect and sort the leafkeys for both account mappings into a slice + createKeys := sortKeys(diffAccountsAtB) + deleteKeys := sortKeys(diffAccountsAtA) + + // and then find the intersection of these keys + // these are the leafkeys for the accounts which exist at both A and B but are different + // this also mutates the passed in createKeys and deleteKeys, removing in intersection keys + // and leaving the truly created or deleted keys in place + updatedKeys := findIntersection(createKeys, deleteKeys) + + // build the diff nodes for the updated accounts using the mappings at both A and B as directed by the keys found as the intersection of the two + err = sdb.buildAccountUpdates( + diffAccountsAtB, diffAccountsAtA, updatedKeys, + params.WatchedStorageSlots, params.IntermediateStorageNodes, output) + if err != nil { + return fmt.Errorf("error building diff for updated accounts: %v", err) + } + // build the diff nodes for created accounts + err = sdb.buildAccountCreations(diffAccountsAtB, params.WatchedStorageSlots, params.IntermediateStorageNodes, output, codeOutput) + if err != nil { + return fmt.Errorf("error building diff for created accounts: %v", err) + } + return nil +} + +// createdAndUpdatedState returns +// a mapping of their leafkeys to all the accounts that exist in a different state at B than A +// and a slice of the paths for all of the nodes included in both +func (sdb *builder) createdAndUpdatedState(a, b trie.NodeIterator, watchedAddresses []common.Address) (AccountMap, map[string]bool, error) { + diffPathsAtB := make(map[string]bool) + diffAcountsAtB := make(AccountMap) + it, _ := trie.NewDifferenceIterator(a, b) + for it.Next(true) { + // skip value nodes + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + node, nodeElements, err := resolveNode(it, sdb.stateCache.TrieDB()) + if err != nil { + return nil, nil, err + } + if node.NodeType == Leaf { + // created vs updated is important for leaf nodes since we need to diff their storage + // so we need to map all changed accounts at B to their leafkey, since account can change pathes but not leafkey + var account state.Account + if err := rlp.DecodeBytes(nodeElements[1].([]byte), &account); err != nil { + return nil, nil, fmt.Errorf("error decoding account for leaf node at path %x nerror: %v", node.Path, err) + } + partialPath := trie.CompactToHex(nodeElements[0].([]byte)) + valueNodePath := append(node.Path, partialPath...) + encodedPath := trie.HexToCompact(valueNodePath) + leafKey := encodedPath[1:] + if isWatchedAddress(watchedAddresses, leafKey) { + diffAcountsAtB[common.Bytes2Hex(leafKey)] = accountWrapper{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + LeafKey: leafKey, + Account: &account, + } + } + } + // add both intermediate and leaf node paths to the list of diffPathsAtB + diffPathsAtB[common.Bytes2Hex(node.Path)] = true + } + return diffAcountsAtB, diffPathsAtB, it.Error() +} + +// createdAndUpdatedStateWithIntermediateNodes returns +// a slice of all the intermediate nodes that exist in a different state at B than A +// a mapping of their leafkeys to all the accounts that exist in a different state at B than A +// and a slice of the paths for all of the nodes included in both +func (sdb *builder) createdAndUpdatedStateWithIntermediateNodes(a, b trie.NodeIterator, output StateNodeSink) (AccountMap, map[string]bool, error) { + diffPathsAtB := make(map[string]bool) + diffAcountsAtB := make(AccountMap) + it, _ := trie.NewDifferenceIterator(a, b) + for it.Next(true) { + // skip value nodes + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + node, nodeElements, err := resolveNode(it, sdb.stateCache.TrieDB()) + if err != nil { + return nil, nil, err + } + switch node.NodeType { + case Leaf: + // created vs updated is important for leaf nodes since we need to diff their storage + // so we need to map all changed accounts at B to their leafkey, since account can change paths but not leafkey + var account state.Account + if err := rlp.DecodeBytes(nodeElements[1].([]byte), &account); err != nil { + return nil, nil, fmt.Errorf("error decoding account for leaf node at path %x nerror: %v", node.Path, err) + } + partialPath := trie.CompactToHex(nodeElements[0].([]byte)) + valueNodePath := append(node.Path, partialPath...) + encodedPath := trie.HexToCompact(valueNodePath) + leafKey := encodedPath[1:] + diffAcountsAtB[common.Bytes2Hex(leafKey)] = accountWrapper{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + LeafKey: leafKey, + Account: &account, + } + case Extension, Branch: + // create a diff for any intermediate node that has changed at b + // created vs updated makes no difference for intermediate nodes since we do not need to diff storage + if err := output(StateNode{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + }); err != nil { + return nil, nil, err + } + default: + return nil, nil, fmt.Errorf("unexpected node type %s", node.NodeType) + } + // add both intermediate and leaf node paths to the list of diffPathsAtB + diffPathsAtB[common.Bytes2Hex(node.Path)] = true + } + return diffAcountsAtB, diffPathsAtB, it.Error() +} + +// deletedOrUpdatedState returns a slice of all the pathes that are emptied at B +// and a mapping of their leafkeys to all the accounts that exist in a different state at A than B +func (sdb *builder) deletedOrUpdatedState(a, b trie.NodeIterator, diffPathsAtB map[string]bool, output StateNodeSink) (AccountMap, error) { + diffAccountAtA := make(AccountMap) + it, _ := trie.NewDifferenceIterator(b, a) + for it.Next(true) { + // skip value nodes + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + node, nodeElements, err := resolveNode(it, sdb.stateCache.TrieDB()) + if err != nil { + return nil, err + } + switch node.NodeType { + case Leaf: + // map all different accounts at A to their leafkey + var account state.Account + if err := rlp.DecodeBytes(nodeElements[1].([]byte), &account); err != nil { + return nil, fmt.Errorf("error decoding account for leaf node at path %x nerror: %v", node.Path, err) + } + partialPath := trie.CompactToHex(nodeElements[0].([]byte)) + valueNodePath := append(node.Path, partialPath...) + encodedPath := trie.HexToCompact(valueNodePath) + leafKey := encodedPath[1:] + diffAccountAtA[common.Bytes2Hex(leafKey)] = accountWrapper{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + LeafKey: leafKey, + Account: &account, + } + // if this node's path did not show up in diffPathsAtB + // that means the node at this path was deleted (or moved) in B + // emit an empty "removed" diff to signify as such + if _, ok := diffPathsAtB[common.Bytes2Hex(node.Path)]; !ok { + if err := output(StateNode{ + Path: node.Path, + NodeValue: []byte{}, + NodeType: Removed, + LeafKey: leafKey, + }); err != nil { + return nil, err + } + } + case Extension, Branch: + // if this node's path did not show up in diffPathsAtB + // that means the node at this path was deleted (or moved) in B + // emit an empty "removed" diff to signify as such + if _, ok := diffPathsAtB[common.Bytes2Hex(node.Path)]; !ok { + if err := output(StateNode{ + Path: node.Path, + NodeValue: []byte{}, + NodeType: Removed, + }); err != nil { + return nil, err + } + } + // fall through, we did everything we need to do with these node types + default: + return nil, fmt.Errorf("unexpected node type %s", node.NodeType) + } + } + return diffAccountAtA, it.Error() +} + +// buildAccountUpdates uses the account diffs maps for A => B and B => A and the known intersection of their leafkeys +// to generate the statediff node objects for all of the accounts that existed at both A and B but in different states +// needs to be called before building account creations and deletions as this mutates +// those account maps to remove the accounts which were updated +func (sdb *builder) buildAccountUpdates(creations, deletions AccountMap, updatedKeys []string, + watchedStorageKeys []common.Hash, intermediateStorageNodes bool, output StateNodeSink) error { + var err error + for _, key := range updatedKeys { + createdAcc := creations[key] + deletedAcc := deletions[key] + var storageDiffs []StorageNode + if deletedAcc.Account != nil && createdAcc.Account != nil { + oldSR := deletedAcc.Account.Root + newSR := createdAcc.Account.Root + err = sdb.buildStorageNodesIncremental( + oldSR, newSR, watchedStorageKeys, intermediateStorageNodes, + storageNodeAppender(&storageDiffs)) + if err != nil { + return fmt.Errorf("failed building incremental storage diffs for account with leafkey %s\r\nerror: %v", key, err) + } + } + if err = output(StateNode{ + NodeType: createdAcc.NodeType, + Path: createdAcc.Path, + NodeValue: createdAcc.NodeValue, + LeafKey: createdAcc.LeafKey, + StorageNodes: storageDiffs, + }); err != nil { + return err + } + delete(creations, key) + delete(deletions, key) + } + + return nil +} + +// buildAccountCreations returns the statediff node objects for all the accounts that exist at B but not at A +// it also returns the code and codehash for created contract accounts +func (sdb *builder) buildAccountCreations(accounts AccountMap, watchedStorageKeys []common.Hash, intermediateStorageNodes bool, output StateNodeSink, codeOutput CodeSink) error { + for _, val := range accounts { + diff := StateNode{ + NodeType: val.NodeType, + Path: val.Path, + LeafKey: val.LeafKey, + NodeValue: val.NodeValue, + } + if !bytes.Equal(val.Account.CodeHash, nullCodeHash) { + // For contract creations, any storage node contained is a diff + var storageDiffs []StorageNode + err := sdb.buildStorageNodesEventual(val.Account.Root, watchedStorageKeys, intermediateStorageNodes, storageNodeAppender(&storageDiffs)) + if err != nil { + return fmt.Errorf("failed building eventual storage diffs for node %x\r\nerror: %v", val.Path, err) + } + diff.StorageNodes = storageDiffs + // emit codehash => code mappings for cod + codeHash := common.BytesToHash(val.Account.CodeHash) + code, err := sdb.stateCache.ContractCode(common.Hash{}, codeHash) + if err != nil { + return fmt.Errorf("failed to retrieve code for codehash %s\r\n error: %v", codeHash.String(), err) + } + if err := codeOutput(CodeAndCodeHash{ + Hash: codeHash, + Code: code, + }); err != nil { + return err + } + } + if err := output(diff); err != nil { + return err + } + } + + return nil +} + +// buildStorageNodesEventual builds the storage diff node objects for a created account +// i.e. it returns all the storage nodes at this state, since there is no previous state +func (sdb *builder) buildStorageNodesEventual(sr common.Hash, watchedStorageKeys []common.Hash, intermediateNodes bool, output StorageNodeSink) error { + if bytes.Equal(sr.Bytes(), emptyContractRoot.Bytes()) { + return nil + } + log.Debug("Storage Root For Eventual Diff", "root", sr.Hex()) + sTrie, err := sdb.stateCache.OpenTrie(sr) + if err != nil { + log.Info("error in build storage diff eventual", "error", err) + return err + } + it := sTrie.NodeIterator(make([]byte, 0)) + err = sdb.buildStorageNodesFromTrie(it, watchedStorageKeys, intermediateNodes, output) + if err != nil { + return err + } + return nil +} + +// buildStorageNodesFromTrie returns all the storage diff node objects in the provided node interator +// if any storage keys are provided it will only return those leaf nodes +// including intermediate nodes can be turned on or off +func (sdb *builder) buildStorageNodesFromTrie(it trie.NodeIterator, watchedStorageKeys []common.Hash, intermediateNodes bool, output StorageNodeSink) error { + for it.Next(true) { + // skip value nodes + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + node, nodeElements, err := resolveNode(it, sdb.stateCache.TrieDB()) + if err != nil { + return err + } + switch node.NodeType { + case Leaf: + partialPath := trie.CompactToHex(nodeElements[0].([]byte)) + valueNodePath := append(node.Path, partialPath...) + encodedPath := trie.HexToCompact(valueNodePath) + leafKey := encodedPath[1:] + if isWatchedStorageKey(watchedStorageKeys, leafKey) { + if err := output(StorageNode{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + LeafKey: leafKey, + }); err != nil { + return err + } + } + case Extension, Branch: + if intermediateNodes { + if err := output(StorageNode{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + }); err != nil { + return err + } + } + default: + return fmt.Errorf("unexpected node type %s", node.NodeType) + } + } + return it.Error() +} + +// buildStorageNodesIncremental builds the storage diff node objects for all nodes that exist in a different state at B than A +func (sdb *builder) buildStorageNodesIncremental(oldSR common.Hash, newSR common.Hash, watchedStorageKeys []common.Hash, intermediateNodes bool, output StorageNodeSink) error { + if bytes.Equal(newSR.Bytes(), oldSR.Bytes()) { + return nil + } + log.Debug("Storage Roots for Incremental Diff", "old", oldSR.Hex(), "new", newSR.Hex()) + oldTrie, err := sdb.stateCache.OpenTrie(oldSR) + if err != nil { + return err + } + newTrie, err := sdb.stateCache.OpenTrie(newSR) + if err != nil { + return err + } + + diffPathsAtB, err := sdb.createdAndUpdatedStorage( + oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{}), + watchedStorageKeys, intermediateNodes, output) + if err != nil { + return err + } + err = sdb.deletedOrUpdatedStorage(oldTrie.NodeIterator([]byte{}), newTrie.NodeIterator([]byte{}), + diffPathsAtB, watchedStorageKeys, intermediateNodes, output) + if err != nil { + return err + } + return nil +} + +func (sdb *builder) createdAndUpdatedStorage(a, b trie.NodeIterator, watchedKeys []common.Hash, intermediateNodes bool, output StorageNodeSink) (map[string]bool, error) { + diffPathsAtB := make(map[string]bool) + it, _ := trie.NewDifferenceIterator(a, b) + for it.Next(true) { + // skip value nodes + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + node, nodeElements, err := resolveNode(it, sdb.stateCache.TrieDB()) + if err != nil { + return nil, err + } + switch node.NodeType { + case Leaf: + partialPath := trie.CompactToHex(nodeElements[0].([]byte)) + valueNodePath := append(node.Path, partialPath...) + encodedPath := trie.HexToCompact(valueNodePath) + leafKey := encodedPath[1:] + if isWatchedStorageKey(watchedKeys, leafKey) { + if err := output(StorageNode{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + LeafKey: leafKey, + }); err != nil { + return nil, err + } + } + case Extension, Branch: + if intermediateNodes { + if err := output(StorageNode{ + NodeType: node.NodeType, + Path: node.Path, + NodeValue: node.NodeValue, + }); err != nil { + return nil, err + } + } + default: + return nil, fmt.Errorf("unexpected node type %s", node.NodeType) + } + diffPathsAtB[common.Bytes2Hex(node.Path)] = true + } + return diffPathsAtB, it.Error() +} + +func (sdb *builder) deletedOrUpdatedStorage(a, b trie.NodeIterator, diffPathsAtB map[string]bool, watchedKeys []common.Hash, intermediateNodes bool, output StorageNodeSink) error { + it, _ := trie.NewDifferenceIterator(b, a) + for it.Next(true) { + // skip value nodes + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + node, nodeElements, err := resolveNode(it, sdb.stateCache.TrieDB()) + if err != nil { + return err + } + // if this node path showed up in diffPathsAtB + // that means this node was updated at B and we already have the updated diff for it + // otherwise that means this node was deleted in B and we need to add a "removed" diff to represent that event + if _, ok := diffPathsAtB[common.Bytes2Hex(node.Path)]; ok { + continue + } + switch node.NodeType { + case Leaf: + partialPath := trie.CompactToHex(nodeElements[0].([]byte)) + valueNodePath := append(node.Path, partialPath...) + encodedPath := trie.HexToCompact(valueNodePath) + leafKey := encodedPath[1:] + if isWatchedStorageKey(watchedKeys, leafKey) { + if err := output(StorageNode{ + NodeType: Removed, + Path: node.Path, + NodeValue: []byte{}, + LeafKey: leafKey, + }); err != nil { + return err + } + } + case Extension, Branch: + if intermediateNodes { + if err := output(StorageNode{ + NodeType: Removed, + Path: node.Path, + NodeValue: []byte{}, + }); err != nil { + return err + } + } + default: + return fmt.Errorf("unexpected node type %s", node.NodeType) + } + } + return it.Error() +} + +// isWatchedAddress is used to check if a state account corresponds to one of the addresses the builder is configured to watch +func isWatchedAddress(watchedAddresses []common.Address, stateLeafKey []byte) bool { + // If we aren't watching any specific addresses, we are watching everything + if len(watchedAddresses) == 0 { + return true + } + for _, addr := range watchedAddresses { + addrHashKey := crypto.Keccak256(addr.Bytes()) + if bytes.Equal(addrHashKey, stateLeafKey) { + return true + } + } + return false +} + +// isWatchedStorageKey is used to check if a storage leaf corresponds to one of the storage slots the builder is configured to watch +func isWatchedStorageKey(watchedKeys []common.Hash, storageLeafKey []byte) bool { + // If we aren't watching any specific addresses, we are watching everything + if len(watchedKeys) == 0 { + return true + } + for _, hashKey := range watchedKeys { + if bytes.Equal(hashKey.Bytes(), storageLeafKey) { + return true + } + } + return false +} diff --git a/statediff/builder_test.go b/statediff/builder_test.go new file mode 100644 index 000000000000..7ad49f565fed --- /dev/null +++ b/statediff/builder_test.go @@ -0,0 +1,2322 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statediff_test + +import ( + "bytes" + "fmt" + "math/big" + "os" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/statediff" + "github.com/ethereum/go-ethereum/statediff/testhelpers" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" +) + +var ( + contractLeafKey []byte + emptyDiffs = make([]sdtypes.StateNode, 0) + emptyStorage = make([]sdtypes.StorageNode, 0) + block0, block1, block2, block3, block4, block5, block6 *types.Block + builder statediff.Builder + miningReward = int64(2000000000000000000) + minerAddress = common.HexToAddress("0x0") + minerLeafKey = testhelpers.AddressToLeafKey(minerAddress) + + balanceChange10000 = int64(10000) + balanceChange1000 = int64(1000) + block1BankBalance = int64(99990000) + block1Account1Balance = int64(10000) + block2Account2Balance = int64(1000) + + slot0 = common.HexToHash("0") + slot1 = common.HexToHash("1") + slot2 = common.HexToHash("2") + slot3 = common.HexToHash("3") + + slot0StorageKey = crypto.Keccak256Hash(slot0[:]) + slot1StorageKey = crypto.Keccak256Hash(slot1[:]) + slot2StorageKey = crypto.Keccak256Hash(slot2[:]) + slot3StorageKey = crypto.Keccak256Hash(slot3[:]) + + slot0StorageValue = common.Hex2Bytes("94703c4b2bd70c169f5717101caee543299fc946c7") // prefixed AccountAddr1 + slot1StorageValue = common.Hex2Bytes("01") + slot2StorageValue = common.Hex2Bytes("09") + slot3StorageValue = common.Hex2Bytes("03") + + slot0StorageLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("390decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563"), + slot0StorageValue, + }) + slot1StorageLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("310e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6"), + slot1StorageValue, + }) + slot2StorageLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("305787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace"), + slot2StorageValue, + }) + slot3StorageLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("32575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b"), + slot3StorageValue, + }) + slot0StorageLeafRootNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("20290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563"), + slot0StorageValue, + }) + + contractAccountAtBlock2, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 1, + Balance: big.NewInt(0), + CodeHash: common.HexToHash("0xaaea5efba4fd7b45d7ec03918ac5d8b31aa93b48986af0e6b591f0f087c80127").Bytes(), + Root: crypto.Keccak256Hash(block2StorageBranchRootNode), + }) + contractAccountAtBlock2LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3114658a74d9cc9f7acf2c5cd696c3494d7c344d78bfec3add0d91ec4e8d1c45"), + contractAccountAtBlock2, + }) + contractAccountAtBlock3, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 1, + Balance: big.NewInt(0), + CodeHash: common.HexToHash("0xaaea5efba4fd7b45d7ec03918ac5d8b31aa93b48986af0e6b591f0f087c80127").Bytes(), + Root: crypto.Keccak256Hash(block3StorageBranchRootNode), + }) + contractAccountAtBlock3LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3114658a74d9cc9f7acf2c5cd696c3494d7c344d78bfec3add0d91ec4e8d1c45"), + contractAccountAtBlock3, + }) + contractAccountAtBlock4, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 1, + Balance: big.NewInt(0), + CodeHash: common.HexToHash("0xaaea5efba4fd7b45d7ec03918ac5d8b31aa93b48986af0e6b591f0f087c80127").Bytes(), + Root: crypto.Keccak256Hash(block4StorageBranchRootNode), + }) + contractAccountAtBlock4LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3114658a74d9cc9f7acf2c5cd696c3494d7c344d78bfec3add0d91ec4e8d1c45"), + contractAccountAtBlock4, + }) + contractAccountAtBlock5, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 1, + Balance: big.NewInt(0), + CodeHash: common.HexToHash("0xaaea5efba4fd7b45d7ec03918ac5d8b31aa93b48986af0e6b591f0f087c80127").Bytes(), + Root: crypto.Keccak256Hash(slot0StorageLeafRootNode), + }) + contractAccountAtBlock5LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3114658a74d9cc9f7acf2c5cd696c3494d7c344d78bfec3add0d91ec4e8d1c45"), + contractAccountAtBlock5, + }) + + minerAccountAtBlock1, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(miningReward), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + minerAccountAtBlock1LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3380c7b7ae81a58eb98d9c78de4a1fd7fd9535fc953ed2be602daaa41767312a"), + minerAccountAtBlock1, + }) + minerAccountAtBlock2, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(miningReward + miningReward), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + minerAccountAtBlock2LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3380c7b7ae81a58eb98d9c78de4a1fd7fd9535fc953ed2be602daaa41767312a"), + minerAccountAtBlock2, + }) + + account1AtBlock1, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(balanceChange10000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account1AtBlock1LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3926db69aaced518e9b9f0f434a473e7174109c943548bb8f23be41ca76d9ad2"), + account1AtBlock1, + }) + account1AtBlock2, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 2, + Balance: big.NewInt(block1Account1Balance - balanceChange1000 + balanceChange1000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account1AtBlock2LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3926db69aaced518e9b9f0f434a473e7174109c943548bb8f23be41ca76d9ad2"), + account1AtBlock2, + }) + account1AtBlock5, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 2, + Balance: big.NewInt(block1Account1Balance - balanceChange1000 + balanceChange1000 + miningReward), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account1AtBlock5LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3926db69aaced518e9b9f0f434a473e7174109c943548bb8f23be41ca76d9ad2"), + account1AtBlock5, + }) + account1AtBlock6, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 3, + Balance: big.NewInt(block1Account1Balance - balanceChange1000 + balanceChange1000 + miningReward), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account1AtBlock6LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3926db69aaced518e9b9f0f434a473e7174109c943548bb8f23be41ca76d9ad2"), + account1AtBlock6, + }) + + account2AtBlock2, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(balanceChange1000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account2AtBlock2LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3957f3e2f04a0764c3a0491b175f69926da61efbcc8f61fa1455fd2d2b4cdd45"), + account2AtBlock2, + }) + account2AtBlock3, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(block2Account2Balance + miningReward), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account2AtBlock3LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3957f3e2f04a0764c3a0491b175f69926da61efbcc8f61fa1455fd2d2b4cdd45"), + account2AtBlock3, + }) + account2AtBlock4, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(block2Account2Balance + miningReward*2), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account2AtBlock4LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3957f3e2f04a0764c3a0491b175f69926da61efbcc8f61fa1455fd2d2b4cdd45"), + account2AtBlock4, + }) + account2AtBlock6, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(block2Account2Balance + miningReward*3), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + account2AtBlock6LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3957f3e2f04a0764c3a0491b175f69926da61efbcc8f61fa1455fd2d2b4cdd45"), + account2AtBlock6, + }) + + bankAccountAtBlock0, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(testhelpers.TestBankFunds.Int64()), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock0LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("2000bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock0, + }) + bankAccountAtBlock1, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 1, + Balance: big.NewInt(testhelpers.TestBankFunds.Int64() - balanceChange10000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock1LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("30bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock1, + }) + bankAccountAtBlock2, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 2, + Balance: big.NewInt(block1BankBalance - balanceChange1000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock2LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("30bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock2, + }) + bankAccountAtBlock3, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 3, + Balance: big.NewInt(99989000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock3LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("30bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock3, + }) + bankAccountAtBlock4, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 6, + Balance: big.NewInt(99989000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock4LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("30bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock4, + }) + bankAccountAtBlock5, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 7, + Balance: big.NewInt(99989000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock5LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("30bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock5, + }) + + block1BranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + crypto.Keccak256(bankAccountAtBlock1LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(minerAccountAtBlock1LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(account1AtBlock1LeafNode), + []byte{}, + []byte{}, + }) + block2BranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + crypto.Keccak256(bankAccountAtBlock2LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(minerAccountAtBlock2LeafNode), + crypto.Keccak256(contractAccountAtBlock2LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(account2AtBlock2LeafNode), + []byte{}, + crypto.Keccak256(account1AtBlock2LeafNode), + []byte{}, + []byte{}, + }) + block3BranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + crypto.Keccak256(bankAccountAtBlock3LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(minerAccountAtBlock2LeafNode), + crypto.Keccak256(contractAccountAtBlock3LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(account2AtBlock3LeafNode), + []byte{}, + crypto.Keccak256(account1AtBlock2LeafNode), + []byte{}, + []byte{}, + }) + block4BranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + crypto.Keccak256(bankAccountAtBlock4LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(minerAccountAtBlock2LeafNode), + crypto.Keccak256(contractAccountAtBlock4LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(account2AtBlock4LeafNode), + []byte{}, + crypto.Keccak256(account1AtBlock2LeafNode), + []byte{}, + []byte{}, + }) + block5BranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + crypto.Keccak256(bankAccountAtBlock5LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(minerAccountAtBlock2LeafNode), + crypto.Keccak256(contractAccountAtBlock5LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(account2AtBlock4LeafNode), + []byte{}, + crypto.Keccak256(account1AtBlock5LeafNode), + []byte{}, + []byte{}, + }) + block6BranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + crypto.Keccak256(bankAccountAtBlock5LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(minerAccountAtBlock2LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(account2AtBlock6LeafNode), + []byte{}, + crypto.Keccak256(account1AtBlock6LeafNode), + []byte{}, + []byte{}, + }) + + block2StorageBranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + []byte{}, + []byte{}, + crypto.Keccak256(slot0StorageLeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(slot1StorageLeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) + block3StorageBranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + []byte{}, + []byte{}, + crypto.Keccak256(slot0StorageLeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(slot1StorageLeafNode), + crypto.Keccak256(slot3StorageLeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) + block4StorageBranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + []byte{}, + []byte{}, + crypto.Keccak256(slot0StorageLeafNode), + []byte{}, + crypto.Keccak256(slot2StorageLeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) +) + +func init() { + if os.Getenv("MODE") != "statediff" { + fmt.Println("Skipping statediff test") + os.Exit(0) + } +} + +func TestBuilder(t *testing.T) { + blocks, chain := testhelpers.MakeChain(3, testhelpers.Genesis, testhelpers.TestChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + block2 = blocks[1] + block3 = blocks[2] + params := statediff.Params{} + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + { + "testEmptyDiff", + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: emptyDiffs, + }, + }, + { + "testBlock0", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: testhelpers.NullHash, + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock0LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock1", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block1.Root(), + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountAtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock2", + // 1000 transferred from testBankAddress to account1Addr + // 1000 transferred from account1Addr to account2Addr + // account1addr creates a new contract + statediff.Args{ + OldStateRoot: block1.Root(), + NewStateRoot: block2.Root(), + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountAtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock2LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot0StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + { + "testBlock3", + //the contract's storage is changed + //and the block is mined by account 2 + statediff.Args{ + OldStateRoot: block2.Root(), + NewStateRoot: block3.Root(), + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock3LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock3LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: slot3StorageKey.Bytes(), + NodeValue: slot3StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock3LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\nexpected state diff: %+v", diff, test.expected) + } + } +} + +func TestBuilderWithIntermediateNodes(t *testing.T) { + blocks, chain := testhelpers.MakeChain(3, testhelpers.Genesis, testhelpers.TestChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + block2 = blocks[1] + block3 = blocks[2] + blocks = append([]*types.Block{block0}, blocks...) + params := statediff.Params{ + IntermediateStateNodes: true, + IntermediateStorageNodes: true, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + { + "testEmptyDiff", + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: emptyDiffs, + }, + }, + { + "testBlock0", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: testhelpers.NullHash, + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock0LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock1", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block1.Root(), + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block1BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountAtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock2", + // 1000 transferred from testBankAddress to account1Addr + // 1000 transferred from account1Addr to account2Addr + // account1addr creates a new contract + statediff.Args{ + OldStateRoot: block1.Root(), + NewStateRoot: block2.Root(), + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block2BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountAtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock2LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block2StorageBranchRootNode, + }, + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot0StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + { + "testBlock3", + //the contract's storage is changed + //and the block is mined by account 2 + statediff.Args{ + OldStateRoot: block2.Root(), + NewStateRoot: block3.Root(), + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block3BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock3LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock3LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block3StorageBranchRootNode, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: slot3StorageKey.Bytes(), + NodeValue: slot3StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock3LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + } + + for i, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\r\n\r\n\r\nexpected state diff: %+v", diff, test.expected) + } + // Let's also confirm that our root state nodes form the state root hash in the headers + if i > 0 { + block := blocks[i-1] + expectedStateRoot := block.Root() + for _, node := range test.expected.Nodes { + if bytes.Equal(node.Path, []byte{}) { + stateRoot := crypto.Keccak256Hash(node.NodeValue) + if !bytes.Equal(expectedStateRoot.Bytes(), stateRoot.Bytes()) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual stateroot: %x\r\nexpected stateroot: %x", stateRoot.Bytes(), expectedStateRoot.Bytes()) + } + } + } + } + } +} + +func TestBuilderWithWatchedAddressList(t *testing.T) { + blocks, chain := testhelpers.MakeChain(3, testhelpers.Genesis, testhelpers.TestChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + block2 = blocks[1] + block3 = blocks[2] + params := statediff.Params{ + WatchedAddresses: []common.Address{testhelpers.Account1Addr, testhelpers.ContractAddr}, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + { + "testEmptyDiff", + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: emptyDiffs, + }, + }, + { + "testBlock0", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: testhelpers.NullHash, + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: emptyDiffs, + }, + }, + { + "testBlock1", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block1.Root(), + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock2", + //1000 transferred from testBankAddress to account1Addr + //1000 transferred from account1Addr to account2Addr + statediff.Args{ + OldStateRoot: block1.Root(), + NewStateRoot: block2.Root(), + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock2LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot0StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + { + "testBlock3", + //the contract's storage is changed + //and the block is mined by account 2 + statediff.Args{ + OldStateRoot: block2.Root(), + NewStateRoot: block3.Root(), + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock3LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: slot3StorageKey.Bytes(), + NodeValue: slot3StorageLeafNode, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\nexpected state diff: %+v", diff, test.expected) + } + } +} + +func TestBuilderWithWatchedAddressAndStorageKeyList(t *testing.T) { + blocks, chain := testhelpers.MakeChain(3, testhelpers.Genesis, testhelpers.TestChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + block2 = blocks[1] + block3 = blocks[2] + params := statediff.Params{ + WatchedAddresses: []common.Address{testhelpers.Account1Addr, testhelpers.ContractAddr}, + WatchedStorageSlots: []common.Hash{slot1StorageKey}, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + { + "testEmptyDiff", + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: emptyDiffs, + }, + }, + { + "testBlock0", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: testhelpers.NullHash, + NewStateRoot: block0.Root(), + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block0.Number(), + BlockHash: block0.Hash(), + Nodes: emptyDiffs, + }, + }, + { + "testBlock1", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block1.Root(), + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock2", + //1000 transferred from testBankAddress to account1Addr + //1000 transferred from account1Addr to account2Addr + statediff.Args{ + OldStateRoot: block1.Root(), + NewStateRoot: block2.Root(), + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock2LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + { + "testBlock3", + //the contract's storage is changed + //and the block is mined by account 2 + statediff.Args{ + OldStateRoot: block2.Root(), + NewStateRoot: block3.Root(), + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock3LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\nexpected state diff: %+v", diff, test.expected) + } + } +} + +func TestBuilderWithRemovedAccountAndStorage(t *testing.T) { + blocks, chain := testhelpers.MakeChain(6, testhelpers.Genesis, testhelpers.TestChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block3 = blocks[2] + block4 = blocks[3] + block5 = blocks[4] + block6 = blocks[5] + params := statediff.Params{ + IntermediateStateNodes: true, + IntermediateStorageNodes: true, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + // blocks 0-3 are the same as in TestBuilderWithIntermediateNodes + { + "testBlock4", + statediff.Args{ + OldStateRoot: block3.Root(), + NewStateRoot: block4.Root(), + BlockNumber: block4.Number(), + BlockHash: block4.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block4.Number(), + BlockHash: block4.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block4BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock4LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock4LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block4StorageBranchRootNode, + }, + { + Path: []byte{'\x04'}, + NodeType: sdtypes.Leaf, + LeafKey: slot2StorageKey.Bytes(), + NodeValue: slot2StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Removed, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: []byte{}, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Removed, + LeafKey: slot3StorageKey.Bytes(), + NodeValue: []byte{}, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock4LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock5", + statediff.Args{ + OldStateRoot: block4.Root(), + NewStateRoot: block5.Root(), + BlockNumber: block5.Number(), + BlockHash: block5.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block5.Number(), + BlockHash: block5.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block5BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock5LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock5LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Leaf, + NodeValue: slot0StorageLeafRootNode, + LeafKey: slot0StorageKey.Bytes(), + }, + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Removed, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: []byte{}, + }, + { + Path: []byte{'\x04'}, + NodeType: sdtypes.Removed, + LeafKey: slot2StorageKey.Bytes(), + NodeValue: []byte{}, + }, + }, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock5LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock6", + statediff.Args{ + OldStateRoot: block5.Root(), + NewStateRoot: block6.Root(), + BlockNumber: block6.Number(), + BlockHash: block6.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block6.Number(), + BlockHash: block6.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block6BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Removed, + LeafKey: contractLeafKey, + NodeValue: []byte{}, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock6LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock6LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\r\n\r\n\r\nexpected state diff: %+v", diff, test.expected) + } + } +} + +func TestBuilderWithRemovedAccountAndStorageWithoutIntermediateNodes(t *testing.T) { + blocks, chain := testhelpers.MakeChain(6, testhelpers.Genesis, testhelpers.TestChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block3 = blocks[2] + block4 = blocks[3] + block5 = blocks[4] + block6 = blocks[5] + params := statediff.Params{ + IntermediateStateNodes: false, + IntermediateStorageNodes: false, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + // blocks 0-3 are the same as in TestBuilderWithIntermediateNodes + { + "testBlock4", + statediff.Args{ + OldStateRoot: block3.Root(), + NewStateRoot: block4.Root(), + BlockNumber: block4.Number(), + BlockHash: block4.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block4.Number(), + BlockHash: block4.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock4LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock4LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{'\x04'}, + NodeType: sdtypes.Leaf, + LeafKey: slot2StorageKey.Bytes(), + NodeValue: slot2StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Removed, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: []byte{}, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Removed, + LeafKey: slot3StorageKey.Bytes(), + NodeValue: []byte{}, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock4LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock5", + statediff.Args{ + OldStateRoot: block4.Root(), + NewStateRoot: block5.Root(), + BlockNumber: block5.Number(), + BlockHash: block5.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block5.Number(), + BlockHash: block5.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock5LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock5LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot0StorageLeafRootNode, + }, + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Removed, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: []byte{}, + }, + { + Path: []byte{'\x04'}, + NodeType: sdtypes.Removed, + LeafKey: slot2StorageKey.Bytes(), + NodeValue: []byte{}, + }, + }, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock5LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock6", + statediff.Args{ + OldStateRoot: block5.Root(), + NewStateRoot: block6.Root(), + BlockNumber: block6.Number(), + BlockHash: block6.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block6.Number(), + BlockHash: block6.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Removed, + LeafKey: contractLeafKey, + NodeValue: []byte{}, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock6LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock6LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\r\n\r\n\r\nexpected state diff: %+v", diff, test.expected) + } + } +} + +var ( + slot00StorageValue = common.Hex2Bytes("9471562b71999873db5b286df957af199ec94617f7") // prefixed TestBankAddress + + slot00StorageLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("390decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563"), + slot00StorageValue, + }) + + contractAccountAtBlock01, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 1, + Balance: big.NewInt(0), + CodeHash: common.HexToHash("0xaaea5efba4fd7b45d7ec03918ac5d8b31aa93b48986af0e6b591f0f087c80127").Bytes(), + Root: crypto.Keccak256Hash(block01StorageBranchRootNode), + }) + contractAccountAtBlock01LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3cb2583748c26e89ef19c2a8529b05a270f735553b4d44b6f2a1894987a71c8b"), + contractAccountAtBlock01, + }) + + bankAccountAtBlock01, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 1, + Balance: big.NewInt(testhelpers.TestBankFunds.Int64() + miningReward), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock01LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("30bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock01, + }) + bankAccountAtBlock02, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 2, + Balance: big.NewInt(testhelpers.TestBankFunds.Int64() + miningReward*2), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + bankAccountAtBlock02LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("2000bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccountAtBlock02, + }) + + block01BranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + crypto.Keccak256Hash(bankAccountAtBlock01LeafNode), + crypto.Keccak256Hash(contractAccountAtBlock01LeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) + + block01StorageBranchRootNode, _ = rlp.EncodeToBytes([]interface{}{ + []byte{}, + []byte{}, + crypto.Keccak256(slot00StorageLeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + crypto.Keccak256(slot1StorageLeafNode), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) +) + +func TestBuilderWithMovedAccount(t *testing.T) { + blocks, chain := testhelpers.MakeChain(2, testhelpers.Genesis, testhelpers.TestSelfDestructChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + block2 = blocks[1] + params := statediff.Params{ + IntermediateStateNodes: true, + IntermediateStorageNodes: true, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + { + "testBlock1", + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block1.Root(), + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block01BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock01LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x01'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock01LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block01StorageBranchRootNode, + }, + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot00StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + }, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + { + "testBlock2", + statediff.Args{ + OldStateRoot: block1.Root(), + NewStateRoot: block2.Root(), + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock02LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x01'}, + NodeType: sdtypes.Removed, + LeafKey: contractLeafKey, + NodeValue: []byte{}, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Removed, + LeafKey: testhelpers.BankLeafKey, + NodeValue: []byte{}, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\r\n\r\n\r\nexpected state diff: %+v", diff, test.expected) + } + } +} + +func TestBuilderWithMovedAccountOnlyLeafs(t *testing.T) { + blocks, chain := testhelpers.MakeChain(2, testhelpers.Genesis, testhelpers.TestSelfDestructChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + block2 = blocks[1] + params := statediff.Params{ + IntermediateStateNodes: false, + IntermediateStorageNodes: false, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + { + "testBlock1", + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block1.Root(), + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock01LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x01'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock01LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot00StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + }, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + { + "testBlock2", + statediff.Args{ + OldStateRoot: block1.Root(), + NewStateRoot: block2.Root(), + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock02LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x01'}, + NodeType: sdtypes.Removed, + LeafKey: contractLeafKey, + NodeValue: []byte{}, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Removed, + LeafKey: testhelpers.BankLeafKey, + NodeValue: []byte{}, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\r\n\r\n\r\nexpected state diff: %+v", diff, test.expected) + } + } +} + +func TestBuildStateTrie(t *testing.T) { + blocks, chain := testhelpers.MakeChain(3, testhelpers.Genesis, testhelpers.TestChainGen) + contractLeafKey = testhelpers.AddressToLeafKey(testhelpers.ContractAddr) + defer chain.Stop() + block1 = blocks[0] + block2 = blocks[1] + block3 = blocks[2] + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + block *types.Block + expected *statediff.StateObject + }{ + { + "testBlock1", + block1, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block1BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountAtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock1LeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock2", + block2, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block2BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountAtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock2LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block2StorageBranchRootNode, + }, + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot0StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + { + "testBlock3", + block3, + &statediff.StateObject{ + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block3BranchRootNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountAtBlock3LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountAtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1AtBlock2LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: contractLeafKey, + NodeValue: contractAccountAtBlock3LeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + NodeValue: block3StorageBranchRootNode, + }, + { + Path: []byte{'\x02'}, + NodeType: sdtypes.Leaf, + LeafKey: slot0StorageKey.Bytes(), + NodeValue: slot0StorageLeafNode, + }, + { + Path: []byte{'\x0b'}, + NodeType: sdtypes.Leaf, + LeafKey: slot1StorageKey.Bytes(), + NodeValue: slot1StorageLeafNode, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: slot3StorageKey.Bytes(), + NodeValue: slot3StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account2LeafKey, + NodeValue: account2AtBlock3LeafNode, + StorageNodes: emptyStorage, + }, + }, + CodeAndCodeHashes: []sdtypes.CodeAndCodeHash{ + { + Hash: testhelpers.CodeHash, + Code: testhelpers.ByteCodeAfterDeployment, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateTrieObject(test.block) + if err != nil { + t.Error(err) + } + receivedStateTrieRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateTrieRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateTrieRlp, func(i, j int) bool { return receivedStateTrieRlp[i] < receivedStateTrieRlp[j] }) + sort.Slice(expectedStateTrieRlp, func(i, j int) bool { return expectedStateTrieRlp[i] < expectedStateTrieRlp[j] }) + if !bytes.Equal(receivedStateTrieRlp, expectedStateTrieRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state trie: %+v\r\n\r\n\r\nexpected state trie: %+v", diff, test.expected) + } + } +} + +/* +pragma solidity ^0.5.10; + +contract test { + address payable owner; + + modifier onlyOwner { + require( + msg.sender == owner, + "Only owner can call this function." + ); + _; + } + + uint256[100] data; + + constructor() public { + owner = msg.sender; + data = [1]; + } + + function Put(uint256 addr, uint256 value) public { + data[addr] = value; + } + + function close() public onlyOwner { //onlyOwner is custom modifier + selfdestruct(owner); // `owner` is the owners address + } +} +*/ diff --git a/statediff/db/migrations/00001_create_ipfs_blocks_table.sql b/statediff/db/migrations/00001_create_ipfs_blocks_table.sql new file mode 100644 index 000000000000..6e3941e04363 --- /dev/null +++ b/statediff/db/migrations/00001_create_ipfs_blocks_table.sql @@ -0,0 +1,8 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS public.blocks ( + key TEXT UNIQUE NOT NULL, + data BYTEA NOT NULL +); + +-- +goose Down +DROP TABLE public.blocks; diff --git a/statediff/db/migrations/00002_create_nodes_table.sql b/statediff/db/migrations/00002_create_nodes_table.sql new file mode 100644 index 000000000000..e70c144bbc96 --- /dev/null +++ b/statediff/db/migrations/00002_create_nodes_table.sql @@ -0,0 +1,13 @@ +-- +goose Up +CREATE TABLE nodes ( + id SERIAL PRIMARY KEY, + client_name VARCHAR, + genesis_block VARCHAR(66), + network_id VARCHAR, + node_id VARCHAR(128), + chain_id INTEGER DEFAULT 1, + CONSTRAINT node_uc UNIQUE (genesis_block, network_id, node_id, chain_id) +); + +-- +goose Down +DROP TABLE nodes; diff --git a/statediff/db/migrations/00003_create_eth_schema.sql b/statediff/db/migrations/00003_create_eth_schema.sql new file mode 100644 index 000000000000..84d6f4b685ba --- /dev/null +++ b/statediff/db/migrations/00003_create_eth_schema.sql @@ -0,0 +1,5 @@ +-- +goose Up +CREATE SCHEMA eth; + +-- +goose Down +DROP SCHEMA eth; \ No newline at end of file diff --git a/statediff/db/migrations/00004_create_eth_header_cids_table.sql b/statediff/db/migrations/00004_create_eth_header_cids_table.sql new file mode 100644 index 000000000000..339eb427b16a --- /dev/null +++ b/statediff/db/migrations/00004_create_eth_header_cids_table.sql @@ -0,0 +1,23 @@ +-- +goose Up +CREATE TABLE eth.header_cids ( + id SERIAL PRIMARY KEY, + block_number BIGINT NOT NULL, + block_hash VARCHAR(66) NOT NULL, + parent_hash VARCHAR(66) NOT NULL, + cid TEXT NOT NULL, + mh_key TEXT NOT NULL REFERENCES public.blocks (key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + td NUMERIC NOT NULL, + node_id INTEGER NOT NULL REFERENCES nodes (id) ON DELETE CASCADE, + reward NUMERIC NOT NULL, + state_root VARCHAR(66) NOT NULL, + tx_root VARCHAR(66) NOT NULL, + receipt_root VARCHAR(66) NOT NULL, + uncle_root VARCHAR(66) NOT NULL, + bloom BYTEA NOT NULL, + timestamp NUMERIC NOT NULL, + times_validated INTEGER NOT NULL DEFAULT 1, + UNIQUE (block_number, block_hash) +); + +-- +goose Down +DROP TABLE eth.header_cids; \ No newline at end of file diff --git a/statediff/db/migrations/00005_create_eth_uncle_cids_table.sql b/statediff/db/migrations/00005_create_eth_uncle_cids_table.sql new file mode 100644 index 000000000000..c46cafb9c8b5 --- /dev/null +++ b/statediff/db/migrations/00005_create_eth_uncle_cids_table.sql @@ -0,0 +1,14 @@ +-- +goose Up +CREATE TABLE eth.uncle_cids ( + id SERIAL PRIMARY KEY, + header_id INTEGER NOT NULL REFERENCES eth.header_cids (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + block_hash VARCHAR(66) NOT NULL, + parent_hash VARCHAR(66) NOT NULL, + cid TEXT NOT NULL, + mh_key TEXT NOT NULL REFERENCES public.blocks (key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + reward NUMERIC NOT NULL, + UNIQUE (header_id, block_hash) +); + +-- +goose Down +DROP TABLE eth.uncle_cids; \ No newline at end of file diff --git a/statediff/db/migrations/00006_create_eth_transaction_cids_table.sql b/statediff/db/migrations/00006_create_eth_transaction_cids_table.sql new file mode 100644 index 000000000000..fc65932d53fb --- /dev/null +++ b/statediff/db/migrations/00006_create_eth_transaction_cids_table.sql @@ -0,0 +1,17 @@ +-- +goose Up +CREATE TABLE eth.transaction_cids ( + id SERIAL PRIMARY KEY, + header_id INTEGER NOT NULL REFERENCES eth.header_cids (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + tx_hash VARCHAR(66) NOT NULL, + index INTEGER NOT NULL, + cid TEXT NOT NULL, + mh_key TEXT NOT NULL REFERENCES public.blocks (key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + dst VARCHAR(66) NOT NULL, + src VARCHAR(66) NOT NULL, + tx_data BYTEA, + tx_type BYTEA, + UNIQUE (header_id, tx_hash) +); + +-- +goose Down +DROP TABLE eth.transaction_cids; diff --git a/statediff/db/migrations/00007_create_eth_receipt_cids_table.sql b/statediff/db/migrations/00007_create_eth_receipt_cids_table.sql new file mode 100644 index 000000000000..e8d0e27d6a4f --- /dev/null +++ b/statediff/db/migrations/00007_create_eth_receipt_cids_table.sql @@ -0,0 +1,20 @@ +-- +goose Up +CREATE TABLE eth.receipt_cids ( + id SERIAL PRIMARY KEY, + tx_id INTEGER NOT NULL REFERENCES eth.transaction_cids (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + cid TEXT NOT NULL, + mh_key TEXT NOT NULL REFERENCES public.blocks (key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + contract VARCHAR(66), + contract_hash VARCHAR(66), + topic0s VARCHAR(66)[], + topic1s VARCHAR(66)[], + topic2s VARCHAR(66)[], + topic3s VARCHAR(66)[], + log_contracts VARCHAR(66)[], + post_state VARCHAR(66), + post_status INTEGER, + UNIQUE (tx_id) +); + +-- +goose Down +DROP TABLE eth.receipt_cids; \ No newline at end of file diff --git a/statediff/db/migrations/00008_create_eth_state_cids_table.sql b/statediff/db/migrations/00008_create_eth_state_cids_table.sql new file mode 100644 index 000000000000..ccece96418f3 --- /dev/null +++ b/statediff/db/migrations/00008_create_eth_state_cids_table.sql @@ -0,0 +1,15 @@ +-- +goose Up +CREATE TABLE eth.state_cids ( + id BIGSERIAL PRIMARY KEY, + header_id INTEGER NOT NULL REFERENCES eth.header_cids (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + state_leaf_key VARCHAR(66), + cid TEXT NOT NULL, + mh_key TEXT NOT NULL REFERENCES public.blocks (key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + state_path BYTEA, + node_type INTEGER NOT NULL, + diff BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE (header_id, state_path) +); + +-- +goose Down +DROP TABLE eth.state_cids; \ No newline at end of file diff --git a/statediff/db/migrations/00009_create_eth_storage_cids_table.sql b/statediff/db/migrations/00009_create_eth_storage_cids_table.sql new file mode 100644 index 000000000000..954fb465d793 --- /dev/null +++ b/statediff/db/migrations/00009_create_eth_storage_cids_table.sql @@ -0,0 +1,15 @@ +-- +goose Up +CREATE TABLE eth.storage_cids ( + id BIGSERIAL PRIMARY KEY, + state_id BIGINT NOT NULL REFERENCES eth.state_cids (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + storage_leaf_key VARCHAR(66), + cid TEXT NOT NULL, + mh_key TEXT NOT NULL REFERENCES public.blocks (key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + storage_path BYTEA, + node_type INTEGER NOT NULL, + diff BOOLEAN NOT NULL DEFAULT FALSE, + UNIQUE (state_id, storage_path) +); + +-- +goose Down +DROP TABLE eth.storage_cids; \ No newline at end of file diff --git a/statediff/db/migrations/00010_create_eth_state_accouts_table.sql b/statediff/db/migrations/00010_create_eth_state_accouts_table.sql new file mode 100644 index 000000000000..8a7e870836e9 --- /dev/null +++ b/statediff/db/migrations/00010_create_eth_state_accouts_table.sql @@ -0,0 +1,13 @@ +-- +goose Up +CREATE TABLE eth.state_accounts ( + id SERIAL PRIMARY KEY, + state_id BIGINT NOT NULL REFERENCES eth.state_cids (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + balance NUMERIC NOT NULL, + nonce INTEGER NOT NULL, + code_hash BYTEA NOT NULL, + storage_root VARCHAR(66) NOT NULL, + UNIQUE (state_id) +); + +-- +goose Down +DROP TABLE eth.state_accounts; \ No newline at end of file diff --git a/statediff/db/migrations/00011_create_postgraphile_comments.sql b/statediff/db/migrations/00011_create_postgraphile_comments.sql new file mode 100644 index 000000000000..16a051f40483 --- /dev/null +++ b/statediff/db/migrations/00011_create_postgraphile_comments.sql @@ -0,0 +1,6 @@ +-- +goose Up +COMMENT ON TABLE public.nodes IS E'@name NodeInfo'; +COMMENT ON TABLE eth.transaction_cids IS E'@name EthTransactionCids'; +COMMENT ON TABLE eth.header_cids IS E'@name EthHeaderCids'; +COMMENT ON COLUMN public.nodes.node_id IS E'@name ChainNodeID'; +COMMENT ON COLUMN eth.header_cids.node_id IS E'@name EthNodeID'; diff --git a/statediff/db/migrations/00012_potgraphile_triggers.sql b/statediff/db/migrations/00012_potgraphile_triggers.sql new file mode 100644 index 000000000000..34705337ef00 --- /dev/null +++ b/statediff/db/migrations/00012_potgraphile_triggers.sql @@ -0,0 +1,69 @@ +-- +goose Up +-- +goose StatementBegin +CREATE FUNCTION eth.graphql_subscription() returns TRIGGER as $$ +declare + table_name text = TG_ARGV[0]; + attribute text = TG_ARGV[1]; + id text; +begin + execute 'select $1.' || quote_ident(attribute) + using new + into id; + perform pg_notify('postgraphile:' || table_name, + json_build_object( + '__node__', json_build_array( + table_name, + id + ) + )::text + ); + return new; +end; +$$ language plpgsql; +-- +goose StatementEnd + +CREATE TRIGGER header_cids_ai + after INSERT ON eth.header_cids + for each row + execute procedure eth.graphql_subscription('header_cids', 'id'); + +CREATE TRIGGER receipt_cids_ai + after INSERT ON eth.receipt_cids + for each row + execute procedure eth.graphql_subscription('receipt_cids', 'id'); + +CREATE TRIGGER state_accounts_ai + after INSERT ON eth.state_accounts + for each row + execute procedure eth.graphql_subscription('state_accounts', 'id'); + +CREATE TRIGGER state_cids_ai + after INSERT ON eth.state_cids + for each row + execute procedure eth.graphql_subscription('state_cids', 'id'); + +CREATE TRIGGER storage_cids_ai + after INSERT ON eth.storage_cids + for each row + execute procedure eth.graphql_subscription('storage_cids', 'id'); + +CREATE TRIGGER transaction_cids_ai + after INSERT ON eth.transaction_cids + for each row + execute procedure eth.graphql_subscription('transaction_cids', 'id'); + +CREATE TRIGGER uncle_cids_ai + after INSERT ON eth.uncle_cids + for each row + execute procedure eth.graphql_subscription('uncle_cids', 'id'); + +-- +goose Down +DROP TRIGGER uncle_cids_ai ON eth.uncle_cids; +DROP TRIGGER transaction_cids_ai ON eth.transaction_cids; +DROP TRIGGER storage_cids_ai ON eth.storage_cids; +DROP TRIGGER state_cids_ai ON eth.state_cids; +DROP TRIGGER state_accounts_ai ON eth.state_accounts; +DROP TRIGGER receipt_cids_ai ON eth.receipt_cids; +DROP TRIGGER header_cids_ai ON eth.header_cids; + +DROP FUNCTION eth.graphql_subscription(); diff --git a/statediff/db/migrations/00013_create_cid_indexes.sql b/statediff/db/migrations/00013_create_cid_indexes.sql new file mode 100644 index 000000000000..bc38c5a26c8f --- /dev/null +++ b/statediff/db/migrations/00013_create_cid_indexes.sql @@ -0,0 +1,121 @@ +-- +goose Up +-- header indexes +CREATE INDEX block_number_index ON eth.header_cids USING brin (block_number); + +CREATE INDEX block_hash_index ON eth.header_cids USING btree (block_hash); + +CREATE INDEX header_cid_index ON eth.header_cids USING btree (cid); + +CREATE INDEX header_mh_index ON eth.header_cids USING btree (mh_key); + +CREATE INDEX state_root_index ON eth.header_cids USING btree (state_root); + +CREATE INDEX timestamp_index ON eth.header_cids USING brin (timestamp); + +-- transaction indexes +CREATE INDEX tx_header_id_index ON eth.transaction_cids USING btree (header_id); + +CREATE INDEX tx_hash_index ON eth.transaction_cids USING btree (tx_hash); + +CREATE INDEX tx_cid_index ON eth.transaction_cids USING btree (cid); + +CREATE INDEX tx_mh_index ON eth.transaction_cids USING btree (mh_key); + +CREATE INDEX tx_dst_index ON eth.transaction_cids USING btree (dst); + +CREATE INDEX tx_src_index ON eth.transaction_cids USING btree (src); + +-- receipt indexes +CREATE INDEX rct_tx_id_index ON eth.receipt_cids USING btree (tx_id); + +CREATE INDEX rct_cid_index ON eth.receipt_cids USING btree (cid); + +CREATE INDEX rct_mh_index ON eth.receipt_cids USING btree (mh_key); + +CREATE INDEX rct_contract_index ON eth.receipt_cids USING btree (contract); + +CREATE INDEX rct_contract_hash_index ON eth.receipt_cids USING btree (contract_hash); + +CREATE INDEX rct_topic0_index ON eth.receipt_cids USING gin (topic0s); + +CREATE INDEX rct_topic1_index ON eth.receipt_cids USING gin (topic1s); + +CREATE INDEX rct_topic2_index ON eth.receipt_cids USING gin (topic2s); + +CREATE INDEX rct_topic3_index ON eth.receipt_cids USING gin (topic3s); + +CREATE INDEX rct_log_contract_index ON eth.receipt_cids USING gin (log_contracts); + +-- state node indexes +CREATE INDEX state_header_id_index ON eth.state_cids USING btree (header_id); + +CREATE INDEX state_leaf_key_index ON eth.state_cids USING btree (state_leaf_key); + +CREATE INDEX state_cid_index ON eth.state_cids USING btree (cid); + +CREATE INDEX state_mh_index ON eth.state_cids USING btree (mh_key); + +CREATE INDEX state_path_index ON eth.state_cids USING btree (state_path); + +-- storage node indexes +CREATE INDEX storage_state_id_index ON eth.storage_cids USING btree (state_id); + +CREATE INDEX storage_leaf_key_index ON eth.storage_cids USING btree (storage_leaf_key); + +CREATE INDEX storage_cid_index ON eth.storage_cids USING btree (cid); + +CREATE INDEX storage_mh_index ON eth.storage_cids USING btree (mh_key); + +CREATE INDEX storage_path_index ON eth.storage_cids USING btree (storage_path); + +-- state accounts indexes +CREATE INDEX account_state_id_index ON eth.state_accounts USING btree (state_id); + +CREATE INDEX storage_root_index ON eth.state_accounts USING btree (storage_root); + +-- +goose Down +-- state account indexes +DROP INDEX eth.storage_root_index; +DROP INDEX eth.account_state_id_index; + +-- storage node indexes +DROP INDEX eth.storage_path_index; +DROP INDEX eth.storage_mh_index; +DROP INDEX eth.storage_cid_index; +DROP INDEX eth.storage_leaf_key_index; +DROP INDEX eth.storage_state_id_index; + +-- state node indexes +DROP INDEX eth.state_path_index; +DROP INDEX eth.state_mh_index; +DROP INDEX eth.state_cid_index; +DROP INDEX eth.state_leaf_key_index; +DROP INDEX eth.state_header_id_index; + +-- receipt indexes +DROP INDEX eth.rct_log_contract_index; +DROP INDEX eth.rct_topic3_index; +DROP INDEX eth.rct_topic2_index; +DROP INDEX eth.rct_topic1_index; +DROP INDEX eth.rct_topic0_index; +DROP INDEX eth.rct_contract_hash_index; +DROP INDEX eth.rct_contract_index; +DROP INDEX eth.rct_mh_index; +DROP INDEX eth.rct_cid_index; +DROP INDEX eth.rct_tx_id_index; + +-- transaction indexes +DROP INDEX eth.tx_src_index; +DROP INDEX eth.tx_dst_index; +DROP INDEX eth.tx_mh_index; +DROP INDEX eth.tx_cid_index; +DROP INDEX eth.tx_hash_index; +DROP INDEX eth.tx_header_id_index; + +-- header indexes +DROP INDEX eth.timestamp_index; +DROP INDEX eth.state_root_index; +DROP INDEX eth.header_mh_index; +DROP INDEX eth.header_cid_index; +DROP INDEX eth.block_hash_index; +DROP INDEX eth.block_number_index; \ No newline at end of file diff --git a/statediff/db/migrations/00014_create_stored_functions.sql b/statediff/db/migrations/00014_create_stored_functions.sql new file mode 100644 index 000000000000..8db60a301453 --- /dev/null +++ b/statediff/db/migrations/00014_create_stored_functions.sql @@ -0,0 +1,158 @@ +-- +goose Up +-- +goose StatementBegin +-- returns if a storage node at the provided path was removed in the range > the provided height and <= the provided block hash +CREATE OR REPLACE FUNCTION was_storage_removed(path BYTEA, height BIGINT, hash VARCHAR(66)) RETURNS BOOLEAN +AS $$ +SELECT exists(SELECT 1 + FROM eth.storage_cids + INNER JOIN eth.state_cids ON (storage_cids.state_id = state_cids.id) + INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) + WHERE storage_path = path + AND block_number > height + AND block_number <= (SELECT block_number + FROM eth.header_cids + WHERE block_hash = hash) + AND storage_cids.node_type = 3 + LIMIT 1); +$$ LANGUAGE SQL; +-- +goose StatementEnd + +-- +goose StatementBegin +-- returns if a state node at the provided path was removed in the range > the provided height and <= the provided block hash +CREATE OR REPLACE FUNCTION was_state_removed(path BYTEA, height BIGINT, hash VARCHAR(66)) RETURNS BOOLEAN +AS $$ +SELECT exists(SELECT 1 + FROM eth.state_cids + INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) + WHERE state_path = path + AND block_number > height + AND block_number <= (SELECT block_number + FROM eth.header_cids + WHERE block_hash = hash) + AND state_cids.node_type = 3 + LIMIT 1); +$$ LANGUAGE SQL; +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE TYPE child_result AS ( + has_child BOOLEAN, + children eth.header_cids[] +); + +CREATE OR REPLACE FUNCTION has_child(hash VARCHAR(66), height BIGINT) RETURNS child_result AS +$BODY$ +DECLARE + child_height INT; + temp_child eth.header_cids; + new_child_result child_result; +BEGIN + child_height = height + 1; + -- short circuit if there are no children + SELECT exists(SELECT 1 + FROM eth.header_cids + WHERE parent_hash = hash + AND block_number = child_height + LIMIT 1) + INTO new_child_result.has_child; + -- collect all the children for this header + IF new_child_result.has_child THEN + FOR temp_child IN + SELECT * FROM eth.header_cids WHERE parent_hash = hash AND block_number = child_height + LOOP + new_child_result.children = array_append(new_child_result.children, temp_child); + END LOOP; + END IF; +RETURN new_child_result; +END +$BODY$ +LANGUAGE 'plpgsql'; +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE OR REPLACE FUNCTION canonical_header_from_array(headers eth.header_cids[]) RETURNS eth.header_cids AS +$BODY$ +DECLARE + canonical_header eth.header_cids; + canonical_child eth.header_cids; + header eth.header_cids; + current_child_result child_result; + child_headers eth.header_cids[]; + current_header_with_child eth.header_cids; + has_children_count INT DEFAULT 0; +BEGIN + -- for each header in the provided set + FOREACH header IN ARRAY headers + LOOP + -- check if it has any children + current_child_result = has_child(header.block_hash, header.block_number); + IF current_child_result.has_child THEN + -- if it does, take note + has_children_count = has_children_count + 1; + current_header_with_child = header; + -- and add the children to the growing set of child headers + child_headers = array_cat(child_headers, current_child_result.children); + END IF; + END LOOP; + -- if none of the headers had children, none is more canonical than the other + IF has_children_count = 0 THEN + -- return the first one selected + SELECT * INTO canonical_header FROM unnest(headers) LIMIT 1; + -- if only one header had children, it can be considered the heaviest/canonical header of the set + ELSIF has_children_count = 1 THEN + -- return the only header with a child + canonical_header = current_header_with_child; + -- if there are multiple headers with children + ELSE + -- find the canonical header from the child set + canonical_child = canonical_header_from_array(child_headers); + -- the header that is parent to this header, is the canonical header at this level + SELECT * INTO canonical_header FROM unnest(headers) + WHERE block_hash = canonical_child.parent_hash; + END IF; + RETURN canonical_header; +END +$BODY$ +LANGUAGE 'plpgsql'; +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE OR REPLACE FUNCTION canonical_header_id(height BIGINT) RETURNS INTEGER AS +$BODY$ +DECLARE + canonical_header eth.header_cids; + headers eth.header_cids[]; + header_count INT; + temp_header eth.header_cids; +BEGIN + -- collect all headers at this height + FOR temp_header IN + SELECT * FROM eth.header_cids WHERE block_number = height + LOOP + headers = array_append(headers, temp_header); + END LOOP; + -- count the number of headers collected + header_count = array_length(headers, 1); + -- if we have less than 1 header, return NULL + IF header_count IS NULL OR header_count < 1 THEN + RETURN NULL; + -- if we have one header, return its id + ELSIF header_count = 1 THEN + RETURN headers[1].id; + -- if we have multiple headers we need to determine which one is canonical + ELSE + canonical_header = canonical_header_from_array(headers); + RETURN canonical_header.id; + END IF; +END; +$BODY$ +LANGUAGE 'plpgsql'; +-- +goose StatementEnd + +-- +goose Down +DROP FUNCTION was_storage_removed; +DROP FUNCTION was_state_removed; +DROP FUNCTION canonical_header_id; +DROP FUNCTION canonical_header_from_array; +DROP FUNCTION has_child; +DROP TYPE child_result; \ No newline at end of file diff --git a/statediff/db/migrations/00015_create_access_list_table.sql b/statediff/db/migrations/00015_create_access_list_table.sql new file mode 100644 index 000000000000..2e30cd5f3ccc --- /dev/null +++ b/statediff/db/migrations/00015_create_access_list_table.sql @@ -0,0 +1,15 @@ +-- +goose Up +CREATE TABLE eth.access_list_element ( + id SERIAL PRIMARY KEY, + tx_id INTEGER NOT NULL REFERENCES eth.transaction_cids (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + index INTEGER NOT NULL, + address VARCHAR(66), + storage_keys VARCHAR(66)[], + UNIQUE (tx_id, index) +); + +CREATE INDEX accesss_list_element_address_index ON eth.access_list_element USING btree (address); + +-- +goose Down +DROP INDEX eth.accesss_list_element_address_index; +DROP TABLE eth.access_list_element; diff --git a/statediff/db/schema.sql b/statediff/db/schema.sql new file mode 100644 index 000000000000..7c606bff2c99 --- /dev/null +++ b/statediff/db/schema.sql @@ -0,0 +1,1333 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 12.4 +-- Dumped by pg_dump version 12.4 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: eth; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA eth; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: header_cids; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.header_cids ( + id integer NOT NULL, + block_number bigint NOT NULL, + block_hash character varying(66) NOT NULL, + parent_hash character varying(66) NOT NULL, + cid text NOT NULL, + mh_key text NOT NULL, + td numeric NOT NULL, + node_id integer NOT NULL, + reward numeric NOT NULL, + state_root character varying(66) NOT NULL, + tx_root character varying(66) NOT NULL, + receipt_root character varying(66) NOT NULL, + uncle_root character varying(66) NOT NULL, + bloom bytea NOT NULL, + "timestamp" numeric NOT NULL, + times_validated integer DEFAULT 1 NOT NULL +); + + +-- +-- Name: TABLE header_cids; Type: COMMENT; Schema: eth; Owner: - +-- + +COMMENT ON TABLE eth.header_cids IS '@name EthHeaderCids'; + + +-- +-- Name: COLUMN header_cids.node_id; Type: COMMENT; Schema: eth; Owner: - +-- + +COMMENT ON COLUMN eth.header_cids.node_id IS '@name EthNodeID'; + + +-- +-- Name: child_result; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.child_result AS ( + has_child boolean, + children eth.header_cids[] +); + + +-- +-- Name: graphql_subscription(); Type: FUNCTION; Schema: eth; Owner: - +-- + +CREATE FUNCTION eth.graphql_subscription() RETURNS trigger + LANGUAGE plpgsql + AS $_$ +declare + table_name text = TG_ARGV[0]; + attribute text = TG_ARGV[1]; + id text; +begin + execute 'select $1.' || quote_ident(attribute) + using new + into id; + perform pg_notify('postgraphile:' || table_name, + json_build_object( + '__node__', json_build_array( + table_name, + id + ) + )::text + ); + return new; +end; +$_$; + + +-- +-- Name: canonical_header(bigint); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.canonical_header(height bigint) RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + current_weight INT; + heaviest_weight INT DEFAULT 0; + heaviest_id INT; + r eth.header_cids%ROWTYPE; +BEGIN + FOR r IN SELECT * FROM eth.header_cids + WHERE block_number = height + LOOP + SELECT INTO current_weight * FROM header_weight(r.block_hash); + IF current_weight > heaviest_weight THEN + heaviest_weight := current_weight; + heaviest_id := r.id; + END IF; + END LOOP; + RETURN heaviest_id; +END +$$; + + +-- +-- Name: canonical_header_from_array(eth.header_cids[]); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.canonical_header_from_array(headers eth.header_cids[]) RETURNS eth.header_cids + LANGUAGE plpgsql + AS $$ +DECLARE + canonical_header eth.header_cids; + canonical_child eth.header_cids; + header eth.header_cids; + current_child_result child_result; + child_headers eth.header_cids[]; + current_header_with_child eth.header_cids; + has_children_count INT DEFAULT 0; +BEGIN + -- for each header in the provided set + FOREACH header IN ARRAY headers + LOOP + -- check if it has any children + current_child_result = has_child(header.block_hash, header.block_number); + IF current_child_result.has_child THEN + -- if it does, take note + has_children_count = has_children_count + 1; + current_header_with_child = header; + -- and add the children to the growing set of child headers + child_headers = array_cat(child_headers, current_child_result.children); + END IF; + END LOOP; + -- if none of the headers had children, none is more canonical than the other + IF has_children_count = 0 THEN + -- return the first one selected + SELECT * INTO canonical_header FROM unnest(headers) LIMIT 1; + -- if only one header had children, it can be considered the heaviest/canonical header of the set + ELSIF has_children_count = 1 THEN + -- return the only header with a child + canonical_header = current_header_with_child; + -- if there are multiple headers with children + ELSE + -- find the canonical header from the child set + canonical_child = canonical_header_from_array(child_headers); + -- the header that is parent to this header, is the canonical header at this level + SELECT * INTO canonical_header FROM unnest(headers) + WHERE block_hash = canonical_child.parent_hash; + END IF; + RETURN canonical_header; +END +$$; + + +-- +-- Name: canonical_header_id(bigint); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.canonical_header_id(height bigint) RETURNS integer + LANGUAGE plpgsql + AS $$ +DECLARE + canonical_header eth.header_cids; + headers eth.header_cids[]; + header_count INT; + temp_header eth.header_cids; +BEGIN + -- collect all headers at this height + FOR temp_header IN + SELECT * FROM eth.header_cids WHERE block_number = height + LOOP + headers = array_append(headers, temp_header); + END LOOP; + -- count the number of headers collected + header_count = array_length(headers, 1); + -- if we have less than 1 header, return NULL + IF header_count IS NULL OR header_count < 1 THEN + RETURN NULL; + -- if we have one header, return its id + ELSIF header_count = 1 THEN + RETURN headers[1].id; + -- if we have multiple headers we need to determine which one is canonical + ELSE + canonical_header = canonical_header_from_array(headers); + RETURN canonical_header.id; + END IF; +END; +$$; + + +-- +-- Name: ethHeaderCidByBlockNumber(bigint); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public."ethHeaderCidByBlockNumber"(n bigint) RETURNS SETOF eth.header_cids + LANGUAGE sql STABLE + AS $_$ +SELECT * FROM eth.header_cids WHERE block_number=$1 ORDER BY id +$_$; + + +-- +-- Name: has_child(character varying, bigint); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.has_child(hash character varying, height bigint) RETURNS public.child_result + LANGUAGE plpgsql + AS $$ +DECLARE + child_height INT; + temp_child eth.header_cids; + new_child_result child_result; +BEGIN + child_height = height + 1; + -- short circuit if there are no children + SELECT exists(SELECT 1 + FROM eth.header_cids + WHERE parent_hash = hash + AND block_number = child_height + LIMIT 1) + INTO new_child_result.has_child; + -- collect all the children for this header + IF new_child_result.has_child THEN + FOR temp_child IN + SELECT * FROM eth.header_cids WHERE parent_hash = hash AND block_number = child_height + LOOP + new_child_result.children = array_append(new_child_result.children, temp_child); + END LOOP; + END IF; + RETURN new_child_result; +END +$$; + + +-- +-- Name: header_weight(character varying); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.header_weight(hash character varying) RETURNS bigint + LANGUAGE sql + AS $$ + WITH RECURSIVE validator AS ( + SELECT block_hash, parent_hash, block_number + FROM eth.header_cids + WHERE block_hash = hash + UNION + SELECT eth.header_cids.block_hash, eth.header_cids.parent_hash, eth.header_cids.block_number + FROM eth.header_cids + INNER JOIN validator + ON eth.header_cids.parent_hash = validator.block_hash + AND eth.header_cids.block_number = validator.block_number + 1 + ) + SELECT COUNT(*) FROM validator; +$$; + + +-- +-- Name: was_state_removed(bytea, bigint, character varying); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.was_state_removed(path bytea, height bigint, hash character varying) RETURNS boolean + LANGUAGE sql + AS $$ +SELECT exists(SELECT 1 + FROM eth.state_cids + INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) + WHERE state_path = path + AND block_number > height + AND block_number <= (SELECT block_number + FROM eth.header_cids + WHERE block_hash = hash) + AND state_cids.node_type = 3 + LIMIT 1); +$$; + + +-- +-- Name: was_storage_removed(bytea, bigint, character varying); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.was_storage_removed(path bytea, height bigint, hash character varying) RETURNS boolean + LANGUAGE sql + AS $$ +SELECT exists(SELECT 1 + FROM eth.storage_cids + INNER JOIN eth.state_cids ON (storage_cids.state_id = state_cids.id) + INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) + WHERE storage_path = path + AND block_number > height + AND block_number <= (SELECT block_number + FROM eth.header_cids + WHERE block_hash = hash) + AND storage_cids.node_type = 3 + LIMIT 1); +$$; + + +-- +-- Name: access_list_entry; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.access_list_entry ( + id integer NOT NULL, + index integer NOT NULL, + tx_id integer NOT NULL, + address character varying(66), + storage_keys character varying(66)[] +); + + +-- +-- Name: access_list_entry_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.access_list_entry_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: access_list_entry_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.access_list_entry_id_seq OWNED BY eth.access_list_entry.id; + + +-- +-- Name: header_cids_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.header_cids_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: header_cids_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.header_cids_id_seq OWNED BY eth.header_cids.id; + + +-- +-- Name: receipt_cids; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.receipt_cids ( + id integer NOT NULL, + tx_id integer NOT NULL, + cid text NOT NULL, + mh_key text NOT NULL, + contract character varying(66), + contract_hash character varying(66), + topic0s character varying(66)[], + topic1s character varying(66)[], + topic2s character varying(66)[], + topic3s character varying(66)[], + log_contracts character varying(66)[], + post_state character varying(66), + post_status integer +); + + +-- +-- Name: receipt_cids_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.receipt_cids_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: receipt_cids_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.receipt_cids_id_seq OWNED BY eth.receipt_cids.id; + + +-- +-- Name: state_accounts; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.state_accounts ( + id integer NOT NULL, + state_id bigint NOT NULL, + balance numeric NOT NULL, + nonce integer NOT NULL, + code_hash bytea NOT NULL, + storage_root character varying(66) NOT NULL +); + + +-- +-- Name: state_accounts_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.state_accounts_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: state_accounts_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.state_accounts_id_seq OWNED BY eth.state_accounts.id; + + +-- +-- Name: state_cids; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.state_cids ( + id bigint NOT NULL, + header_id integer NOT NULL, + state_leaf_key character varying(66), + cid text NOT NULL, + mh_key text NOT NULL, + state_path bytea, + node_type integer NOT NULL, + diff boolean DEFAULT false NOT NULL +); + + +-- +-- Name: state_cids_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.state_cids_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: state_cids_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.state_cids_id_seq OWNED BY eth.state_cids.id; + + +-- +-- Name: storage_cids; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.storage_cids ( + id bigint NOT NULL, + state_id bigint NOT NULL, + storage_leaf_key character varying(66), + cid text NOT NULL, + mh_key text NOT NULL, + storage_path bytea, + node_type integer NOT NULL, + diff boolean DEFAULT false NOT NULL +); + + +-- +-- Name: storage_cids_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.storage_cids_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: storage_cids_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.storage_cids_id_seq OWNED BY eth.storage_cids.id; + + +-- +-- Name: transaction_cids; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.transaction_cids ( + id integer NOT NULL, + header_id integer NOT NULL, + tx_hash character varying(66) NOT NULL, + index integer NOT NULL, + cid text NOT NULL, + mh_key text NOT NULL, + dst character varying(66) NOT NULL, + src character varying(66) NOT NULL, + tx_data bytea, + tx_type bytea +); + + +-- +-- Name: TABLE transaction_cids; Type: COMMENT; Schema: eth; Owner: - +-- + +COMMENT ON TABLE eth.transaction_cids IS '@name EthTransactionCids'; + + +-- +-- Name: transaction_cids_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.transaction_cids_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: transaction_cids_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.transaction_cids_id_seq OWNED BY eth.transaction_cids.id; + + +-- +-- Name: uncle_cids; Type: TABLE; Schema: eth; Owner: - +-- + +CREATE TABLE eth.uncle_cids ( + id integer NOT NULL, + header_id integer NOT NULL, + block_hash character varying(66) NOT NULL, + parent_hash character varying(66) NOT NULL, + cid text NOT NULL, + mh_key text NOT NULL, + reward numeric NOT NULL +); + + +-- +-- Name: uncle_cids_id_seq; Type: SEQUENCE; Schema: eth; Owner: - +-- + +CREATE SEQUENCE eth.uncle_cids_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: uncle_cids_id_seq; Type: SEQUENCE OWNED BY; Schema: eth; Owner: - +-- + +ALTER SEQUENCE eth.uncle_cids_id_seq OWNED BY eth.uncle_cids.id; + + +-- +-- Name: blocks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blocks ( + key text NOT NULL, + data bytea NOT NULL +); + + +-- +-- Name: goose_db_version; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.goose_db_version ( + id integer NOT NULL, + version_id bigint NOT NULL, + is_applied boolean NOT NULL, + tstamp timestamp without time zone DEFAULT now() +); + + +-- +-- Name: goose_db_version_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.goose_db_version_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: goose_db_version_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.goose_db_version_id_seq OWNED BY public.goose_db_version.id; + + +-- +-- Name: nodes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.nodes ( + id integer NOT NULL, + client_name character varying, + genesis_block character varying(66), + network_id character varying, + node_id character varying(128), + chain_id integer DEFAULT 1 +); + + +-- +-- Name: TABLE nodes; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON TABLE public.nodes IS '@name NodeInfo'; + + +-- +-- Name: COLUMN nodes.node_id; Type: COMMENT; Schema: public; Owner: - +-- + +COMMENT ON COLUMN public.nodes.node_id IS '@name ChainNodeID'; + + +-- +-- Name: nodes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.nodes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: nodes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.nodes_id_seq OWNED BY public.nodes.id; + + +-- +-- Name: access_list_entry id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.access_list_entry ALTER COLUMN id SET DEFAULT nextval('eth.access_list_entry_id_seq'::regclass); + + +-- +-- Name: header_cids id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.header_cids ALTER COLUMN id SET DEFAULT nextval('eth.header_cids_id_seq'::regclass); + + +-- +-- Name: receipt_cids id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.receipt_cids ALTER COLUMN id SET DEFAULT nextval('eth.receipt_cids_id_seq'::regclass); + + +-- +-- Name: state_accounts id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_accounts ALTER COLUMN id SET DEFAULT nextval('eth.state_accounts_id_seq'::regclass); + + +-- +-- Name: state_cids id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_cids ALTER COLUMN id SET DEFAULT nextval('eth.state_cids_id_seq'::regclass); + + +-- +-- Name: storage_cids id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.storage_cids ALTER COLUMN id SET DEFAULT nextval('eth.storage_cids_id_seq'::regclass); + + +-- +-- Name: transaction_cids id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.transaction_cids ALTER COLUMN id SET DEFAULT nextval('eth.transaction_cids_id_seq'::regclass); + + +-- +-- Name: uncle_cids id; Type: DEFAULT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.uncle_cids ALTER COLUMN id SET DEFAULT nextval('eth.uncle_cids_id_seq'::regclass); + + +-- +-- Name: goose_db_version id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.goose_db_version ALTER COLUMN id SET DEFAULT nextval('public.goose_db_version_id_seq'::regclass); + + +-- +-- Name: nodes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.nodes ALTER COLUMN id SET DEFAULT nextval('public.nodes_id_seq'::regclass); + + +-- +-- Name: access_list_entry access_list_entry_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.access_list_entry + ADD CONSTRAINT access_list_entry_pkey PRIMARY KEY (id); + + +-- +-- Name: access_list_entry access_list_entry_tx_id_index_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.access_list_entry + ADD CONSTRAINT access_list_entry_tx_id_index_key UNIQUE (tx_id, index); + + +-- +-- Name: header_cids header_cids_block_number_block_hash_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.header_cids + ADD CONSTRAINT header_cids_block_number_block_hash_key UNIQUE (block_number, block_hash); + + +-- +-- Name: header_cids header_cids_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.header_cids + ADD CONSTRAINT header_cids_pkey PRIMARY KEY (id); + + +-- +-- Name: receipt_cids receipt_cids_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.receipt_cids + ADD CONSTRAINT receipt_cids_pkey PRIMARY KEY (id); + + +-- +-- Name: receipt_cids receipt_cids_tx_id_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.receipt_cids + ADD CONSTRAINT receipt_cids_tx_id_key UNIQUE (tx_id); + + +-- +-- Name: state_accounts state_accounts_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_accounts + ADD CONSTRAINT state_accounts_pkey PRIMARY KEY (id); + + +-- +-- Name: state_accounts state_accounts_state_id_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_accounts + ADD CONSTRAINT state_accounts_state_id_key UNIQUE (state_id); + + +-- +-- Name: state_cids state_cids_header_id_state_path_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_cids + ADD CONSTRAINT state_cids_header_id_state_path_key UNIQUE (header_id, state_path); + + +-- +-- Name: state_cids state_cids_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_cids + ADD CONSTRAINT state_cids_pkey PRIMARY KEY (id); + + +-- +-- Name: storage_cids storage_cids_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.storage_cids + ADD CONSTRAINT storage_cids_pkey PRIMARY KEY (id); + + +-- +-- Name: storage_cids storage_cids_state_id_storage_path_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.storage_cids + ADD CONSTRAINT storage_cids_state_id_storage_path_key UNIQUE (state_id, storage_path); + + +-- +-- Name: transaction_cids transaction_cids_header_id_tx_hash_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.transaction_cids + ADD CONSTRAINT transaction_cids_header_id_tx_hash_key UNIQUE (header_id, tx_hash); + + +-- +-- Name: transaction_cids transaction_cids_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.transaction_cids + ADD CONSTRAINT transaction_cids_pkey PRIMARY KEY (id); + + +-- +-- Name: uncle_cids uncle_cids_header_id_block_hash_key; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.uncle_cids + ADD CONSTRAINT uncle_cids_header_id_block_hash_key UNIQUE (header_id, block_hash); + + +-- +-- Name: uncle_cids uncle_cids_pkey; Type: CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.uncle_cids + ADD CONSTRAINT uncle_cids_pkey PRIMARY KEY (id); + + +-- +-- Name: blocks blocks_key_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blocks + ADD CONSTRAINT blocks_key_key UNIQUE (key); + + +-- +-- Name: goose_db_version goose_db_version_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.goose_db_version + ADD CONSTRAINT goose_db_version_pkey PRIMARY KEY (id); + + +-- +-- Name: nodes node_uc; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.nodes + ADD CONSTRAINT node_uc UNIQUE (genesis_block, network_id, node_id, chain_id); + + +-- +-- Name: nodes nodes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.nodes + ADD CONSTRAINT nodes_pkey PRIMARY KEY (id); + + +-- +-- Name: accesss_list_address_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX accesss_list_address_index ON eth.access_list_entry USING btree (address); + + +-- +-- Name: account_state_id_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX account_state_id_index ON eth.state_accounts USING btree (state_id); + + +-- +-- Name: block_hash_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX block_hash_index ON eth.header_cids USING btree (block_hash); + + +-- +-- Name: block_number_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX block_number_index ON eth.header_cids USING brin (block_number); + + +-- +-- Name: header_cid_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX header_cid_index ON eth.header_cids USING btree (cid); + + +-- +-- Name: header_mh_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX header_mh_index ON eth.header_cids USING btree (mh_key); + + +-- +-- Name: rct_cid_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_cid_index ON eth.receipt_cids USING btree (cid); + + +-- +-- Name: rct_contract_hash_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_contract_hash_index ON eth.receipt_cids USING btree (contract_hash); + + +-- +-- Name: rct_contract_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_contract_index ON eth.receipt_cids USING btree (contract); + + +-- +-- Name: rct_log_contract_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_log_contract_index ON eth.receipt_cids USING gin (log_contracts); + + +-- +-- Name: rct_mh_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_mh_index ON eth.receipt_cids USING btree (mh_key); + + +-- +-- Name: rct_topic0_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_topic0_index ON eth.receipt_cids USING gin (topic0s); + + +-- +-- Name: rct_topic1_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_topic1_index ON eth.receipt_cids USING gin (topic1s); + + +-- +-- Name: rct_topic2_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_topic2_index ON eth.receipt_cids USING gin (topic2s); + + +-- +-- Name: rct_topic3_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_topic3_index ON eth.receipt_cids USING gin (topic3s); + + +-- +-- Name: rct_tx_id_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX rct_tx_id_index ON eth.receipt_cids USING btree (tx_id); + + +-- +-- Name: state_cid_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX state_cid_index ON eth.state_cids USING btree (cid); + + +-- +-- Name: state_header_id_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX state_header_id_index ON eth.state_cids USING btree (header_id); + + +-- +-- Name: state_leaf_key_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX state_leaf_key_index ON eth.state_cids USING btree (state_leaf_key); + + +-- +-- Name: state_mh_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX state_mh_index ON eth.state_cids USING btree (mh_key); + + +-- +-- Name: state_path_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX state_path_index ON eth.state_cids USING btree (state_path); + + +-- +-- Name: state_root_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX state_root_index ON eth.header_cids USING btree (state_root); + + +-- +-- Name: storage_cid_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX storage_cid_index ON eth.storage_cids USING btree (cid); + + +-- +-- Name: storage_leaf_key_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX storage_leaf_key_index ON eth.storage_cids USING btree (storage_leaf_key); + + +-- +-- Name: storage_mh_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX storage_mh_index ON eth.storage_cids USING btree (mh_key); + + +-- +-- Name: storage_path_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX storage_path_index ON eth.storage_cids USING btree (storage_path); + + +-- +-- Name: storage_root_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX storage_root_index ON eth.state_accounts USING btree (storage_root); + + +-- +-- Name: storage_state_id_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX storage_state_id_index ON eth.storage_cids USING btree (state_id); + + +-- +-- Name: timestamp_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX timestamp_index ON eth.header_cids USING brin ("timestamp"); + + +-- +-- Name: tx_cid_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX tx_cid_index ON eth.transaction_cids USING btree (cid); + + +-- +-- Name: tx_dst_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX tx_dst_index ON eth.transaction_cids USING btree (dst); + + +-- +-- Name: tx_hash_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX tx_hash_index ON eth.transaction_cids USING btree (tx_hash); + + +-- +-- Name: tx_header_id_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX tx_header_id_index ON eth.transaction_cids USING btree (header_id); + + +-- +-- Name: tx_mh_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX tx_mh_index ON eth.transaction_cids USING btree (mh_key); + + +-- +-- Name: tx_src_index; Type: INDEX; Schema: eth; Owner: - +-- + +CREATE INDEX tx_src_index ON eth.transaction_cids USING btree (src); + + +-- +-- Name: header_cids header_cids_ai; Type: TRIGGER; Schema: eth; Owner: - +-- + +CREATE TRIGGER header_cids_ai AFTER INSERT ON eth.header_cids FOR EACH ROW EXECUTE FUNCTION eth.graphql_subscription('header_cids', 'id'); + + +-- +-- Name: receipt_cids receipt_cids_ai; Type: TRIGGER; Schema: eth; Owner: - +-- + +CREATE TRIGGER receipt_cids_ai AFTER INSERT ON eth.receipt_cids FOR EACH ROW EXECUTE FUNCTION eth.graphql_subscription('receipt_cids', 'id'); + + +-- +-- Name: state_accounts state_accounts_ai; Type: TRIGGER; Schema: eth; Owner: - +-- + +CREATE TRIGGER state_accounts_ai AFTER INSERT ON eth.state_accounts FOR EACH ROW EXECUTE FUNCTION eth.graphql_subscription('state_accounts', 'id'); + + +-- +-- Name: state_cids state_cids_ai; Type: TRIGGER; Schema: eth; Owner: - +-- + +CREATE TRIGGER state_cids_ai AFTER INSERT ON eth.state_cids FOR EACH ROW EXECUTE FUNCTION eth.graphql_subscription('state_cids', 'id'); + + +-- +-- Name: storage_cids storage_cids_ai; Type: TRIGGER; Schema: eth; Owner: - +-- + +CREATE TRIGGER storage_cids_ai AFTER INSERT ON eth.storage_cids FOR EACH ROW EXECUTE FUNCTION eth.graphql_subscription('storage_cids', 'id'); + + +-- +-- Name: transaction_cids transaction_cids_ai; Type: TRIGGER; Schema: eth; Owner: - +-- + +CREATE TRIGGER transaction_cids_ai AFTER INSERT ON eth.transaction_cids FOR EACH ROW EXECUTE FUNCTION eth.graphql_subscription('transaction_cids', 'id'); + + +-- +-- Name: uncle_cids uncle_cids_ai; Type: TRIGGER; Schema: eth; Owner: - +-- + +CREATE TRIGGER uncle_cids_ai AFTER INSERT ON eth.uncle_cids FOR EACH ROW EXECUTE FUNCTION eth.graphql_subscription('uncle_cids', 'id'); + + +-- +-- Name: access_list_entry access_list_entry_tx_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.access_list_entry + ADD CONSTRAINT access_list_entry_tx_id_fkey FOREIGN KEY (tx_id) REFERENCES eth.transaction_cids(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: header_cids header_cids_mh_key_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.header_cids + ADD CONSTRAINT header_cids_mh_key_fkey FOREIGN KEY (mh_key) REFERENCES public.blocks(key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: header_cids header_cids_node_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.header_cids + ADD CONSTRAINT header_cids_node_id_fkey FOREIGN KEY (node_id) REFERENCES public.nodes(id) ON DELETE CASCADE; + + +-- +-- Name: receipt_cids receipt_cids_mh_key_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.receipt_cids + ADD CONSTRAINT receipt_cids_mh_key_fkey FOREIGN KEY (mh_key) REFERENCES public.blocks(key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: receipt_cids receipt_cids_tx_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.receipt_cids + ADD CONSTRAINT receipt_cids_tx_id_fkey FOREIGN KEY (tx_id) REFERENCES eth.transaction_cids(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: state_accounts state_accounts_state_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_accounts + ADD CONSTRAINT state_accounts_state_id_fkey FOREIGN KEY (state_id) REFERENCES eth.state_cids(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: state_cids state_cids_header_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_cids + ADD CONSTRAINT state_cids_header_id_fkey FOREIGN KEY (header_id) REFERENCES eth.header_cids(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: state_cids state_cids_mh_key_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.state_cids + ADD CONSTRAINT state_cids_mh_key_fkey FOREIGN KEY (mh_key) REFERENCES public.blocks(key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: storage_cids storage_cids_mh_key_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.storage_cids + ADD CONSTRAINT storage_cids_mh_key_fkey FOREIGN KEY (mh_key) REFERENCES public.blocks(key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: storage_cids storage_cids_state_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.storage_cids + ADD CONSTRAINT storage_cids_state_id_fkey FOREIGN KEY (state_id) REFERENCES eth.state_cids(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: transaction_cids transaction_cids_header_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.transaction_cids + ADD CONSTRAINT transaction_cids_header_id_fkey FOREIGN KEY (header_id) REFERENCES eth.header_cids(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: transaction_cids transaction_cids_mh_key_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.transaction_cids + ADD CONSTRAINT transaction_cids_mh_key_fkey FOREIGN KEY (mh_key) REFERENCES public.blocks(key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: uncle_cids uncle_cids_header_id_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.uncle_cids + ADD CONSTRAINT uncle_cids_header_id_fkey FOREIGN KEY (header_id) REFERENCES eth.header_cids(id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: uncle_cids uncle_cids_mh_key_fkey; Type: FK CONSTRAINT; Schema: eth; Owner: - +-- + +ALTER TABLE ONLY eth.uncle_cids + ADD CONSTRAINT uncle_cids_mh_key_fkey FOREIGN KEY (mh_key) REFERENCES public.blocks(key) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/statediff/doc.md b/statediff/doc.md new file mode 100644 index 000000000000..0d8163e02e15 --- /dev/null +++ b/statediff/doc.md @@ -0,0 +1,264 @@ +# Statediff + +This package provides an auxiliary service that asynchronously processes state diff objects from chain events, +either relaying the state objects to RPC subscribers or writing them directly to Postgres as IPLD objects. + +It also exposes RPC endpoints for fetching or writing to Postgres the state diff at a specific block height +or for a specific block hash, this operates on historical block and state data and so depends on a complete state archive. + +Data is emitted in this differential format in order to make it feasible to IPLD-ize and index the *entire* Ethereum state +(including intermediate state and storage trie nodes). If this state diff process is ran continuously from genesis, +the entire state at any block can be materialized from the cumulative differentials up to that point. + +## Statediff object +A state diff `StateObject` is the collection of all the state and storage trie nodes that have been updated in a given block. +For convenience, we also associate these nodes with the block number and hash, and optionally the set of code hashes and code for any +contracts deployed in this block. + +A complete state diff `StateObject` will include all state and storage intermediate nodes, which is necessary for generating proofs and for +traversing the tries. + +```go +// StateObject is a collection of state (and linked storage nodes) as well as the associated block number, block hash, +// and a set of code hashes and their code +type StateObject struct { + BlockNumber *big.Int `json:"blockNumber" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Nodes []StateNode `json:"nodes" gencodec:"required"` + CodeAndCodeHashes []CodeAndCodeHash `json:"codeMapping"` +} + +// StateNode holds the data for a single state diff node +type StateNode struct { + NodeType NodeType `json:"nodeType" gencodec:"required"` + Path []byte `json:"path" gencodec:"required"` + NodeValue []byte `json:"value" gencodec:"required"` + StorageNodes []StorageNode `json:"storage"` + LeafKey []byte `json:"leafKey"` +} + +// StorageNode holds the data for a single storage diff node +type StorageNode struct { + NodeType NodeType `json:"nodeType" gencodec:"required"` + Path []byte `json:"path" gencodec:"required"` + NodeValue []byte `json:"value" gencodec:"required"` + LeafKey []byte `json:"leafKey"` +} + +// CodeAndCodeHash struct for holding codehash => code mappings +// we can't use an actual map because they are not rlp serializable +type CodeAndCodeHash struct { + Hash common.Hash `json:"codeHash"` + Code []byte `json:"code"` +} +``` +These objects are packed into a `Payload` structure which can additionally associate the `StateObject` +with the block (header, uncles, and transactions), receipts, and total difficulty. +This `Payload` encapsulates all of the differential data at a given block, and allows us to index the entire Ethereum data structure +as hash-linked IPLD objects. + +```go +// Payload packages the data to send to state diff subscriptions +type Payload struct { + BlockRlp []byte `json:"blockRlp"` + TotalDifficulty *big.Int `json:"totalDifficulty"` + ReceiptsRlp []byte `json:"receiptsRlp"` + StateObjectRlp []byte `json:"stateObjectRlp" gencodec:"required"` + + encoded []byte + err error +} +``` + +## Usage +This state diffing service runs as an auxiliary service concurrent to the regular syncing process of the geth node. + + +### CLI configuration +This service introduces a CLI flag namespace `statediff` + +`--statediff` flag is used to turn on the service +`--statediff.writing` is used to tell the service to write state diff objects it produces from synced ChainEvents directly to a configured Postgres database +`--statediff.workers` is used to set the number of concurrent workers to process state diff objects and write them into the database +`--statediff.db` is the connection string for the Postgres database to write to +`--statediff.dbnodeid` is the node id to use in the Postgres database +`--statediff.dbclientname` is the client name to use in the Postgres database + +The service can only operate in full sync mode (`--syncmode=full`), but only the historical RPC endpoints require an archive node (`--gcmode=archive`) + +e.g. +` +./build/bin/geth --syncmode=full --gcmode=archive --statediff --statediff.writing --statediff.db=postgres://localhost:5432/vulcanize_testing?sslmode=disable --statediff.dbnodeid={nodeId} --statediff.dbclientname={dbClientName} +` + +### RPC endpoints +The state diffing service exposes both a WS subscription endpoint, and a number of HTTP unary endpoints. + +Each of these endpoints requires a set of parameters provided by the caller + +```go +// Params is used to carry in parameters from subscribing/requesting clients configuration +type Params struct { + IntermediateStateNodes bool + IntermediateStorageNodes bool + IncludeBlock bool + IncludeReceipts bool + IncludeTD bool + IncludeCode bool + WatchedAddresses []common.Address + WatchedStorageSlots []common.Hash +} +``` + +Using these params we can tell the service whether to include state and/or storage intermediate nodes; whether +to include the associated block (header, uncles, and transactions); whether to include the associated receipts; +whether to include the total difficulty for this block; whether to include the set of code hashes and code for +contracts deployed in this block; whether to limit the diffing process to a list of specific addresses; and/or +whether to limit the diffing process to a list of specific storage slot keys. + +#### Subscription endpoint +A websocket supporting RPC endpoint is exposed for subscribing to state diff `StateObjects` that come off the head of the chain while the geth node syncs. + +```go +// Stream is a subscription endpoint that fires off state diff payloads as they are created +Stream(ctx context.Context, params Params) (*rpc.Subscription, error) +``` + +To expose this endpoint the node needs to have the websocket server turned on (`--ws`), +and the `statediff` namespace exposed (`--ws.api=statediff`). + +Go code subscriptions to this endpoint can be created using the `rpc.Client.Subscribe()` method, +with the "statediff" namespace, a `statediff.Payload` channel, and the name of the statediff api's rpc method: "stream". + +e.g. + +```go + +cli, err := rpc.Dial("ipcPathOrWsURL") +if err != nil { + // handle error +} +stateDiffPayloadChan := make(chan statediff.Payload, 20000) +methodName := "stream" +params := statediff.Params{ + IncludeBlock: true, + IncludeTD: true, + IncludeReceipts: true, + IntermediateStorageNodes: true, + IntermediateStateNodes: true, +} +rpcSub, err := cli.Subscribe(context.Background(), statediff.APIName, stateDiffPayloadChan, methodName, params) +if err != nil { + // handle error +} +for { + select { + case stateDiffPayload := <- stateDiffPayloadChan: + // process the payload + case err := <- rpcSub.Err(): + // handle rpc subscription error + } +} +``` + +#### Unary endpoints +The service also exposes unary RPC endpoints for retrieving the state diff `StateObject` for a specific block height/hash. +```go +// StateDiffAt returns a state diff payload at the specific blockheight +StateDiffAt(ctx context.Context, blockNumber uint64, params Params) (*Payload, error) + +// StateDiffFor returns a state diff payload for the specific blockhash +StateDiffFor(ctx context.Context, blockHash common.Hash, params Params) (*Payload, error) +``` + +To expose this endpoint the node needs to have the HTTP server turned on (`--http`), +and the `statediff` namespace exposed (`--http.api=statediff`). + +### Direct indexing into Postgres +If `--statediff.writing` is set, the service will convert the state diff `StateObject` data into IPLD objects, persist them directly to Postgres, +and generate secondary indexes around the IPLD data. + +The schema and migrations for this Postgres database are provided in `statediff/db/`. + +#### Postgres setup +We use [pressly/goose](https://github.com/pressly/goose) as our Postgres migration manager. +You can also load the Postgres schema directly into a database using + +`psql database_name < schema.sql` + +This will only work on a version 12.4 Postgres database. + +#### Schema overview +Our Postgres schemas are built around a single IPFS backing Postgres IPLD blockstore table (`public.blocks`) that conforms with [go-ds-sql](https://github.com/ipfs/go-ds-sql/blob/master/postgres/postgres.go). +All IPLD objects are stored in this table, where `key` is the blockstore-prefixed multihash key for the IPLD object and `data` contains +the bytes for the IPLD block (in the case of all Ethereum IPLDs, this is the RLP byte encoding of the Ethereum object). + +The IPLD objects in this table can be traversed using an IPLD DAG interface, but since this table only maps multihash to raw IPLD object +it is not particularly useful for searching through the data by looking up Ethereum objects by their constituent fields +(e.g. by block number, tx source/recipient, state/storage trie node path). To improve the accessibility of these objects +we create an Ethereum [advanced data layout](https://github.com/ipld/specs#schemas-and-advanced-data-layouts) (ADL) by generating secondary +indexes on top of the raw IPLDs in other Postgres tables. + +These secondary index tables fall under the `eth` schema and follow an `{objectType}_cids` naming convention. +These tables provide a view into individual fields of the underlying Ethereum IPLD objects, allowing lookups on these fields, and reference the raw IPLD objects stored in `public.blocks` +by foreign keys to their multihash keys. +Additionally, these tables maintain the hash-linked nature of Ethereum objects to one another. E.g. a storage trie node entry in the `storage_cids` +table contains a `state_id` foreign key which references the `id` for the `state_cids` entry that contains the state leaf node for the contract that storage node belongs to, +and in turn that `state_cids` entry contains a `header_id` foreign key which references the `id` of the `header_cids` entry that contains the header for the block these state and storage nodes were updated (diffed). + +### Optimization +On mainnet this process is extremely IO intensive and requires significant resources to allow it to keep up with the head of the chain. +The state diff processing time for a specific block is dependent on the number and complexity of the state changes that occur in a block and +the number of updated state nodes that are available in the in-memory cache vs must be retrieved from disc. + +If memory permits, one means of improving the efficiency of this process is to increase the in-memory trie cache allocation. +This can be done by increasing the overall `--cache` allocation and/or by increasing the % of the cache allocated to trie +usage with `--cache.trie`. + +## Versioning, Branches, Rebasing, and Releasing +Internal tagged releases are maintained for building the latest version of statediffing geth or using it as a go mod dependency. +When a new core go-ethereum version is released, statediffing geth is rebased onto and adjusted to work with the new tag. + +We want to maintain a complete record of our git history, but in order to make frequent and timely rebases feasible we also +need to be able to squash our work before performing a rebase. To this end we retain multiple branches with partial incremental history that culminate in +the full incremental history. + +### Versioning +Versioning for of statediffing geth follows the below format: + +`{Root Version}-statediff-{Statediff Version}` + +Where "root version" is the version of the tagged release from the core go-ethereum repository that our release is rebased on top of +and "statediff version" is the version tracking the state of the statediffing service code. + +E.g. the version at the time of writing this is v1.10.3-statediff-0.0.23, v0.0.23 of the statediffing code rebased on top of the v1.10.3 core tag. + +The statediff version is included in the `VersionMeta` in params/version.go + +### Branches +We maintain two official kinds of branches: + +Major Branch: `{Root Version}-statediff` +Major branches retain the cumulative state of all changes made before the latest root version rebase and track the full incremental history of changes made between the latest root version rebase and the next. +Aside from creating the branch by performing the rebase described in the section below, these branches are never worked off of or committed to directly. + +Feature Branch: `{Root Version}-statediff-{Statediff Version}` +Feature branches are checked out from a major branch in order to work on a new feature or fix for the statediffing code. +The statediff version of a feature branch is the new version it affects on the major branch when merged. Internal tagged releases +are cut against these branches after they are merged back to the major branch. + +If a developer is unsure what version their patch should affect, they should remain working on an unofficial branch. From there +they can open a PR against the targeted root branch and be directed to the appropriate feature version and branch. + +### Rebasing +When a new root tagged release comes out we rebase our statediffing code on top of the new tag using the following process: +1. Checkout a new major branch for the tag from the current major branch +2. On the new major branch, squash all our commits since the last major rebase +3. On the new major branch, perform the rebase against the new tag +4. Push the new major branch to the remote +5. From the new major branch, checkout a new feature branch based on the new major version and the last statediff version +6. On this new feature branch, add the new major branch to the .github/workflows/on-master.yml list of "on push" branches +7. On this new feature branch, make any fixes/adjustments required for all statediffing geth tests to pass +8. PR this feature branch into the new major branch, this PR will trigger CI tests and builds. +9. After merging PR, rebase feature branch onto major branch +10. Cut a new release targeting the feature branch, this release should have the new root version but the same statediff version as the last release diff --git a/statediff/helpers.go b/statediff/helpers.go new file mode 100644 index 000000000000..51ac5c1be80f --- /dev/null +++ b/statediff/helpers.go @@ -0,0 +1,98 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains a batch of utility type declarations used by the tests. As the node +// operates on unique types, a lot of them are needed to check various features. + +package statediff + +import ( + "fmt" + "sort" + "strings" + + sdtypes "github.com/ethereum/go-ethereum/statediff/types" +) + +func sortKeys(data AccountMap) []string { + keys := make([]string, 0, len(data)) + for key := range data { + keys = append(keys, key) + } + sort.Strings(keys) + + return keys +} + +// findIntersection finds the set of strings from both arrays that are equivalent +// a and b must first be sorted +// this is used to find which keys have been both "deleted" and "created" i.e. they were updated +func findIntersection(a, b []string) []string { + lenA := len(a) + lenB := len(b) + iOfA, iOfB := 0, 0 + updates := make([]string, 0) + if iOfA >= lenA || iOfB >= lenB { + return updates + } + for { + switch strings.Compare(a[iOfA], b[iOfB]) { + // -1 when a[iOfA] < b[iOfB] + case -1: + iOfA++ + if iOfA >= lenA { + return updates + } + // 0 when a[iOfA] == b[iOfB] + case 0: + updates = append(updates, a[iOfA]) + iOfA++ + iOfB++ + if iOfA >= lenA || iOfB >= lenB { + return updates + } + // 1 when a[iOfA] > b[iOfB] + case 1: + iOfB++ + if iOfB >= lenB { + return updates + } + } + } + +} + +// CheckKeyType checks what type of key we have +func CheckKeyType(elements []interface{}) (sdtypes.NodeType, error) { + if len(elements) > 2 { + return sdtypes.Branch, nil + } + if len(elements) < 2 { + return sdtypes.Unknown, fmt.Errorf("node cannot be less than two elements in length") + } + switch elements[0].([]byte)[0] / 16 { + case '\x00': + return sdtypes.Extension, nil + case '\x01': + return sdtypes.Extension, nil + case '\x02': + return sdtypes.Leaf, nil + case '\x03': + return sdtypes.Leaf, nil + default: + return sdtypes.Unknown, fmt.Errorf("unknown hex prefix") + } +} diff --git a/statediff/indexer/helpers.go b/statediff/indexer/helpers.go new file mode 100644 index 000000000000..bb62fd079e04 --- /dev/null +++ b/statediff/indexer/helpers.go @@ -0,0 +1,55 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package indexer + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/statediff/types" +) + +func ResolveFromNodeType(nodeType types.NodeType) int { + switch nodeType { + case types.Branch: + return 0 + case types.Extension: + return 1 + case types.Leaf: + return 2 + case types.Removed: + return 3 + default: + return -1 + } +} + +// ChainConfig returns the appropriate ethereum chain config for the provided chain id +func ChainConfig(chainID uint64) (*params.ChainConfig, error) { + switch chainID { + case 1: + return params.MainnetChainConfig, nil + case 3: + return params.RopstenChainConfig, nil + case 4: + return params.RinkebyChainConfig, nil + case 5: + return params.GoerliChainConfig, nil + default: + return nil, fmt.Errorf("chain config for chainid %d not available", chainID) + } +} diff --git a/statediff/indexer/indexer.go b/statediff/indexer/indexer.go new file mode 100644 index 000000000000..942bf19523b6 --- /dev/null +++ b/statediff/indexer/indexer.go @@ -0,0 +1,459 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// This package provides an interface for pushing and indexing IPLD objects into a Postgres database +// Metrics for reporting processing and connection stats are defined in ./metrics.go +package indexer + +import ( + "fmt" + "math/big" + "time" + + "github.com/lib/pq" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/statediff/indexer/ipfs/ipld" + "github.com/ethereum/go-ethereum/statediff/indexer/models" + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" + "github.com/ethereum/go-ethereum/statediff/indexer/shared" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" + + node "github.com/ipfs/go-ipld-format" + "github.com/jmoiron/sqlx" + "github.com/multiformats/go-multihash" +) + +var ( + indexerMetrics = RegisterIndexerMetrics(metrics.DefaultRegistry) + dbMetrics = RegisterDBMetrics(metrics.DefaultRegistry) +) + +// Indexer interface to allow substitution of mocks for testing +type Indexer interface { + PushBlock(block *types.Block, receipts types.Receipts, totalDifficulty *big.Int) (*BlockTx, error) + PushStateNode(tx *BlockTx, stateNode sdtypes.StateNode) error + PushCodeAndCodeHash(tx *BlockTx, codeAndCodeHash sdtypes.CodeAndCodeHash) error + ReportDBMetrics(delay time.Duration, quit <-chan bool) +} + +// StateDiffIndexer satisfies the Indexer interface for ethereum statediff objects +type StateDiffIndexer struct { + chainConfig *params.ChainConfig + dbWriter *PostgresCIDWriter +} + +// NewStateDiffIndexer creates a pointer to a new PayloadConverter which satisfies the PayloadConverter interface +func NewStateDiffIndexer(chainConfig *params.ChainConfig, db *postgres.DB) *StateDiffIndexer { + return &StateDiffIndexer{ + chainConfig: chainConfig, + dbWriter: NewPostgresCIDWriter(db), + } +} + +type BlockTx struct { + dbtx *sqlx.Tx + BlockNumber uint64 + headerID int64 + Close func(err error) error +} + +// Reporting function to run as goroutine +func (sdi *StateDiffIndexer) ReportDBMetrics(delay time.Duration, quit <-chan bool) { + if !metrics.Enabled { + return + } + ticker := time.NewTicker(delay) + go func() { + for { + select { + case <-ticker.C: + dbMetrics.Update(sdi.dbWriter.db.Stats()) + case <-quit: + ticker.Stop() + return + } + } + }() +} + +// Pushes and indexes block data in database, except state & storage nodes (includes header, uncles, transactions & receipts) +// Returns an initiated DB transaction which must be Closed via defer to commit or rollback +func (sdi *StateDiffIndexer) PushBlock(block *types.Block, receipts types.Receipts, totalDifficulty *big.Int) (*BlockTx, error) { + start, t := time.Now(), time.Now() + blockHash := block.Hash() + blockHashStr := blockHash.String() + height := block.NumberU64() + traceMsg := fmt.Sprintf("indexer stats for statediff at %d with hash %s:\r\n", height, blockHashStr) + transactions := block.Transactions() + // Derive any missing fields + if err := receipts.DeriveFields(sdi.chainConfig, blockHash, height, transactions); err != nil { + return nil, err + } + // Generate the block iplds + headerNode, uncleNodes, txNodes, txTrieNodes, rctNodes, rctTrieNodes, err := ipld.FromBlockAndReceipts(block, receipts) + if err != nil { + return nil, fmt.Errorf("error creating IPLD nodes from block and receipts: %v", err) + } + if len(txNodes) != len(txTrieNodes) && len(rctNodes) != len(rctTrieNodes) && len(txNodes) != len(rctNodes) { + return nil, fmt.Errorf("expected number of transactions (%d), transaction trie nodes (%d), receipts (%d), and receipt trie nodes (%d)to be equal", len(txNodes), len(txTrieNodes), len(rctNodes), len(rctTrieNodes)) + } + // Calculate reward + reward := CalcEthBlockReward(block.Header(), block.Uncles(), block.Transactions(), receipts) + t = time.Now() + // Begin new db tx for everything + tx, err := sdi.dbWriter.db.Beginx() + if err != nil { + return nil, err + } + defer func() { + if p := recover(); p != nil { + shared.Rollback(tx) + panic(p) + } else if err != nil { + shared.Rollback(tx) + } + }() + blockTx := &BlockTx{ + dbtx: tx, + // handle transaction commit or rollback for any return case + Close: func(err error) error { + if p := recover(); p != nil { + shared.Rollback(tx) + panic(p) + } else if err != nil { + shared.Rollback(tx) + } else { + tDiff := time.Since(t) + indexerMetrics.tStateStoreCodeProcessing.Update(tDiff) + traceMsg += fmt.Sprintf("state, storage, and code storage processing time: %s\r\n", tDiff.String()) + t = time.Now() + err = tx.Commit() + tDiff = time.Since(t) + indexerMetrics.tPostgresCommit.Update(tDiff) + traceMsg += fmt.Sprintf("postgres transaction commit duration: %s\r\n", tDiff.String()) + } + traceMsg += fmt.Sprintf(" TOTAL PROCESSING DURATION: %s\r\n", time.Since(start).String()) + log.Debug(traceMsg) + return err + }, + } + tDiff := time.Since(t) + indexerMetrics.tFreePostgres.Update(tDiff) + + traceMsg += fmt.Sprintf("time spent waiting for free postgres tx: %s:\r\n", tDiff.String()) + t = time.Now() + + // Publish and index header, collect headerID + var headerID int64 + headerID, err = sdi.processHeader(tx, block.Header(), headerNode, reward, totalDifficulty) + if err != nil { + return nil, err + } + tDiff = time.Since(t) + indexerMetrics.tHeaderProcessing.Update(tDiff) + traceMsg += fmt.Sprintf("header processing time: %s\r\n", tDiff.String()) + t = time.Now() + // Publish and index uncles + err = sdi.processUncles(tx, headerID, height, uncleNodes) + if err != nil { + return nil, err + } + tDiff = time.Since(t) + indexerMetrics.tUncleProcessing.Update(tDiff) + traceMsg += fmt.Sprintf("uncle processing time: %s\r\n", tDiff.String()) + t = time.Now() + // Publish and index receipts and txs + err = sdi.processReceiptsAndTxs(tx, processArgs{ + headerID: headerID, + blockNumber: block.Number(), + receipts: receipts, + txs: transactions, + rctNodes: rctNodes, + rctTrieNodes: rctTrieNodes, + txNodes: txNodes, + txTrieNodes: txTrieNodes, + }) + if err != nil { + return nil, err + } + tDiff = time.Since(t) + indexerMetrics.tTxAndRecProcessing.Update(tDiff) + traceMsg += fmt.Sprintf("tx and receipt processing time: %s\r\n", tDiff.String()) + t = time.Now() + + blockTx.BlockNumber = height + blockTx.headerID = headerID + return blockTx, err +} + +// processHeader publishes and indexes a header IPLD in Postgres +// it returns the headerID +func (sdi *StateDiffIndexer) processHeader(tx *sqlx.Tx, header *types.Header, headerNode node.Node, reward, td *big.Int) (int64, error) { + // publish header + if err := shared.PublishIPLD(tx, headerNode); err != nil { + return 0, fmt.Errorf("error publishing header IPLD: %v", err) + } + // index header + return sdi.dbWriter.upsertHeaderCID(tx, models.HeaderModel{ + CID: headerNode.Cid().String(), + MhKey: shared.MultihashKeyFromCID(headerNode.Cid()), + ParentHash: header.ParentHash.String(), + BlockNumber: header.Number.String(), + BlockHash: header.Hash().String(), + TotalDifficulty: td.String(), + Reward: reward.String(), + Bloom: header.Bloom.Bytes(), + StateRoot: header.Root.String(), + RctRoot: header.ReceiptHash.String(), + TxRoot: header.TxHash.String(), + UncleRoot: header.UncleHash.String(), + Timestamp: header.Time, + }) +} + +func (sdi *StateDiffIndexer) processUncles(tx *sqlx.Tx, headerID int64, blockNumber uint64, uncleNodes []*ipld.EthHeader) error { + // publish and index uncles + for _, uncleNode := range uncleNodes { + if err := shared.PublishIPLD(tx, uncleNode); err != nil { + return fmt.Errorf("error publishing uncle IPLD: %v", err) + } + uncleReward := CalcUncleMinerReward(blockNumber, uncleNode.Number.Uint64()) + uncle := models.UncleModel{ + CID: uncleNode.Cid().String(), + MhKey: shared.MultihashKeyFromCID(uncleNode.Cid()), + ParentHash: uncleNode.ParentHash.String(), + BlockHash: uncleNode.Hash().String(), + Reward: uncleReward.String(), + } + if err := sdi.dbWriter.upsertUncleCID(tx, uncle, headerID); err != nil { + return err + } + } + return nil +} + +// processArgs bundles arguments to processReceiptsAndTxs +type processArgs struct { + headerID int64 + blockNumber *big.Int + receipts types.Receipts + txs types.Transactions + rctNodes []*ipld.EthReceipt + rctTrieNodes []*ipld.EthRctTrie + txNodes []*ipld.EthTx + txTrieNodes []*ipld.EthTxTrie +} + +// processReceiptsAndTxs publishes and indexes receipt and transaction IPLDs in Postgres +func (sdi *StateDiffIndexer) processReceiptsAndTxs(tx *sqlx.Tx, args processArgs) error { + // Process receipts and txs + signer := types.MakeSigner(sdi.chainConfig, args.blockNumber) + for i, receipt := range args.receipts { + // tx that corresponds with this receipt + trx := args.txs[i] + from, err := types.Sender(signer, trx) + if err != nil { + return fmt.Errorf("error deriving tx sender: %v", err) + } + + // Publishing + // publish trie nodes, these aren't indexed directly + if err := shared.PublishIPLD(tx, args.txTrieNodes[i]); err != nil { + return fmt.Errorf("error publishing tx trie node IPLD: %v", err) + } + if err := shared.PublishIPLD(tx, args.rctTrieNodes[i]); err != nil { + return fmt.Errorf("error publishing rct trie node IPLD: %v", err) + } + // publish the txs and receipts + txNode, rctNode := args.txNodes[i], args.rctNodes[i] + if err := shared.PublishIPLD(tx, txNode); err != nil { + return fmt.Errorf("error publishing tx IPLD: %v", err) + } + if err := shared.PublishIPLD(tx, rctNode); err != nil { + return fmt.Errorf("error publishing rct IPLD: %v", err) + } + + // Indexing + // extract topic and contract data from the receipt for indexing + topicSets := make([][]string, 4) + mappedContracts := make(map[string]bool) // use map to avoid duplicate addresses + for _, log := range receipt.Logs { + for i, topic := range log.Topics { + topicSets[i] = append(topicSets[i], topic.Hex()) + } + mappedContracts[log.Address.String()] = true + } + // these are the contracts seen in the logs + logContracts := make([]string, 0, len(mappedContracts)) + for addr := range mappedContracts { + logContracts = append(logContracts, addr) + } + // this is the contract address if this receipt is for a contract creation tx + contract := shared.HandleZeroAddr(receipt.ContractAddress) + var contractHash string + if contract != "" { + contractHash = crypto.Keccak256Hash(common.HexToAddress(contract).Bytes()).String() + } + // index tx first so that the receipt can reference it by FK + txModel := models.TxModel{ + Dst: shared.HandleZeroAddrPointer(trx.To()), + Src: shared.HandleZeroAddr(from), + TxHash: trx.Hash().String(), + Index: int64(i), + Data: trx.Data(), + CID: txNode.Cid().String(), + MhKey: shared.MultihashKeyFromCID(txNode.Cid()), + } + txType := trx.Type() + if txType != types.LegacyTxType { + txModel.Type = &txType + } + txID, err := sdi.dbWriter.upsertTransactionCID(tx, txModel, args.headerID) + if err != nil { + return err + } + + // AccessListEntryModel is the db model for eth.access_list_entry + type AccessListElementModel struct { + ID int64 `db:"id"` + Index int64 `db:"index"` + TxID int64 `db:"tx_id"` + Address string `db:"address"` + StorageKeys pq.StringArray `db:"storage_keys"` + } + // index access list if this is one + for j, accessListElement := range trx.AccessList() { + storageKeys := make([]string, len(accessListElement.StorageKeys)) + for k, storageKey := range accessListElement.StorageKeys { + storageKeys[k] = storageKey.Hex() + } + accessListElementModel := models.AccessListElementModel{ + Index: int64(j), + Address: accessListElement.Address.Hex(), + StorageKeys: storageKeys, + } + if err := sdi.dbWriter.upsertAccessListElement(tx, accessListElementModel, txID); err != nil { + return err + } + } + // index the receipt + rctModel := models.ReceiptModel{ + Topic0s: topicSets[0], + Topic1s: topicSets[1], + Topic2s: topicSets[2], + Topic3s: topicSets[3], + Contract: contract, + ContractHash: contractHash, + LogContracts: logContracts, + CID: rctNode.Cid().String(), + MhKey: shared.MultihashKeyFromCID(rctNode.Cid()), + } + if len(receipt.PostState) == 0 { + rctModel.PostStatus = receipt.Status + } else { + rctModel.PostState = common.Bytes2Hex(receipt.PostState) + } + if err := sdi.dbWriter.upsertReceiptCID(tx, rctModel, txID); err != nil { + return err + } + } + return nil +} + +func (sdi *StateDiffIndexer) PushStateNode(tx *BlockTx, stateNode sdtypes.StateNode) error { + // publish the state node + stateCIDStr, err := shared.PublishRaw(tx.dbtx, ipld.MEthStateTrie, multihash.KECCAK_256, stateNode.NodeValue) + if err != nil { + return fmt.Errorf("error publishing state node IPLD: %v", err) + } + mhKey, _ := shared.MultihashKeyFromCIDString(stateCIDStr) + stateModel := models.StateNodeModel{ + Path: stateNode.Path, + StateKey: common.BytesToHash(stateNode.LeafKey).String(), + CID: stateCIDStr, + MhKey: mhKey, + NodeType: ResolveFromNodeType(stateNode.NodeType), + } + // index the state node, collect the stateID to reference by FK + stateID, err := sdi.dbWriter.upsertStateCID(tx.dbtx, stateModel, tx.headerID) + if err != nil { + return err + } + // if we have a leaf, decode and index the account data + if stateNode.NodeType == sdtypes.Leaf { + var i []interface{} + if err := rlp.DecodeBytes(stateNode.NodeValue, &i); err != nil { + return fmt.Errorf("error decoding state leaf node rlp: %s", err.Error()) + } + if len(i) != 2 { + return fmt.Errorf("eth IPLDPublisher expected state leaf node rlp to decode into two elements") + } + var account state.Account + if err := rlp.DecodeBytes(i[1].([]byte), &account); err != nil { + return fmt.Errorf("error decoding state account rlp: %s", err.Error()) + } + accountModel := models.StateAccountModel{ + Balance: account.Balance.String(), + Nonce: account.Nonce, + CodeHash: account.CodeHash, + StorageRoot: account.Root.String(), + } + if err := sdi.dbWriter.upsertStateAccount(tx.dbtx, accountModel, stateID); err != nil { + return err + } + } + // if there are any storage nodes associated with this node, publish and index them + for _, storageNode := range stateNode.StorageNodes { + storageCIDStr, err := shared.PublishRaw(tx.dbtx, ipld.MEthStorageTrie, multihash.KECCAK_256, storageNode.NodeValue) + if err != nil { + return fmt.Errorf("error publishing storage node IPLD: %v", err) + } + mhKey, _ := shared.MultihashKeyFromCIDString(storageCIDStr) + storageModel := models.StorageNodeModel{ + Path: storageNode.Path, + StorageKey: common.BytesToHash(storageNode.LeafKey).String(), + CID: storageCIDStr, + MhKey: mhKey, + NodeType: ResolveFromNodeType(storageNode.NodeType), + } + if err := sdi.dbWriter.upsertStorageCID(tx.dbtx, storageModel, stateID); err != nil { + return err + } + } + + return nil +} + +// Publishes code and codehash pairs to the ipld database +func (sdi *StateDiffIndexer) PushCodeAndCodeHash(tx *BlockTx, codeAndCodeHash sdtypes.CodeAndCodeHash) error { + // codec doesn't matter since db key is multihash-based + mhKey, err := shared.MultihashKeyFromKeccak256(codeAndCodeHash.Hash) + if err != nil { + return fmt.Errorf("error deriving multihash key from codehash: %v", err) + } + if err := shared.PublishDirect(tx.dbtx, mhKey, codeAndCodeHash.Code); err != nil { + return fmt.Errorf("error publishing code IPLD: %v", err) + } + return nil +} diff --git a/statediff/indexer/indexer_test.go b/statediff/indexer/indexer_test.go new file mode 100644 index 000000000000..faa178197264 --- /dev/null +++ b/statediff/indexer/indexer_test.go @@ -0,0 +1,457 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package indexer_test + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/ethereum/go-ethereum/core/types" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/statediff/indexer" + "github.com/ethereum/go-ethereum/statediff/indexer/ipfs/ipld" + "github.com/ethereum/go-ethereum/statediff/indexer/mocks" + "github.com/ethereum/go-ethereum/statediff/indexer/models" + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" + "github.com/ethereum/go-ethereum/statediff/indexer/shared" + + "github.com/ipfs/go-cid" + blockstore "github.com/ipfs/go-ipfs-blockstore" + dshelp "github.com/ipfs/go-ipfs-ds-help" + "github.com/multiformats/go-multihash" +) + +var ( + db *postgres.DB + err error + ind *indexer.StateDiffIndexer + ipfsPgGet = `SELECT data FROM public.blocks + WHERE key = $1` + tx1, tx2, tx3, tx4, rct1, rct2, rct3, rct4 []byte + mockBlock *types.Block + headerCID, trx1CID, trx2CID, trx3CID, trx4CID cid.Cid + rct1CID, rct2CID, rct3CID, rct4CID cid.Cid + state1CID, state2CID, storageCID cid.Cid +) + +func expectTrue(t *testing.T, value bool) { + if !value { + t.Fatalf("Assertion failed") + } +} + +func init() { + if os.Getenv("MODE") != "statediff" { + fmt.Println("Skipping statediff test") + os.Exit(0) + } + + mockBlock = mocks.MockBlock + txs, rcts := mocks.MockBlock.Transactions(), mocks.MockReceipts + + buf := new(bytes.Buffer) + txs.EncodeIndex(0, buf) + tx1 = make([]byte, buf.Len()) + copy(tx1, buf.Bytes()) + buf.Reset() + + txs.EncodeIndex(1, buf) + tx2 = make([]byte, buf.Len()) + copy(tx2, buf.Bytes()) + buf.Reset() + + txs.EncodeIndex(2, buf) + tx3 = make([]byte, buf.Len()) + copy(tx3, buf.Bytes()) + buf.Reset() + + txs.EncodeIndex(3, buf) + tx4 = make([]byte, buf.Len()) + copy(tx4, buf.Bytes()) + buf.Reset() + + rcts.EncodeIndex(0, buf) + rct1 = make([]byte, buf.Len()) + copy(rct1, buf.Bytes()) + buf.Reset() + + rcts.EncodeIndex(1, buf) + rct2 = make([]byte, buf.Len()) + copy(rct2, buf.Bytes()) + buf.Reset() + + rcts.EncodeIndex(2, buf) + rct3 = make([]byte, buf.Len()) + copy(rct3, buf.Bytes()) + buf.Reset() + + rcts.EncodeIndex(3, buf) + rct4 = make([]byte, buf.Len()) + copy(rct4, buf.Bytes()) + buf.Reset() + + headerCID, _ = ipld.RawdataToCid(ipld.MEthHeader, mocks.MockHeaderRlp, multihash.KECCAK_256) + trx1CID, _ = ipld.RawdataToCid(ipld.MEthTx, tx1, multihash.KECCAK_256) + trx2CID, _ = ipld.RawdataToCid(ipld.MEthTx, tx2, multihash.KECCAK_256) + trx3CID, _ = ipld.RawdataToCid(ipld.MEthTx, tx3, multihash.KECCAK_256) + trx4CID, _ = ipld.RawdataToCid(ipld.MEthTx, tx4, multihash.KECCAK_256) + rct1CID, _ = ipld.RawdataToCid(ipld.MEthTxReceipt, rct1, multihash.KECCAK_256) + rct2CID, _ = ipld.RawdataToCid(ipld.MEthTxReceipt, rct2, multihash.KECCAK_256) + rct3CID, _ = ipld.RawdataToCid(ipld.MEthTxReceipt, rct3, multihash.KECCAK_256) + rct4CID, _ = ipld.RawdataToCid(ipld.MEthTxReceipt, rct4, multihash.KECCAK_256) + state1CID, _ = ipld.RawdataToCid(ipld.MEthStateTrie, mocks.ContractLeafNode, multihash.KECCAK_256) + state2CID, _ = ipld.RawdataToCid(ipld.MEthStateTrie, mocks.AccountLeafNode, multihash.KECCAK_256) + storageCID, _ = ipld.RawdataToCid(ipld.MEthStorageTrie, mocks.StorageLeafNode, multihash.KECCAK_256) +} + +func setup(t *testing.T) { + db, err = shared.SetupDB() + if err != nil { + t.Fatal(err) + } + ind = indexer.NewStateDiffIndexer(params.MainnetChainConfig, db) + var tx *indexer.BlockTx + tx, err = ind.PushBlock( + mockBlock, + mocks.MockReceipts, + mocks.MockBlock.Difficulty()) + if err != nil { + t.Fatal(err) + } + defer tx.Close(err) + for _, node := range mocks.StateDiffs { + err = ind.PushStateNode(tx, node) + if err != nil { + t.Fatal(err) + } + } + + shared.ExpectEqual(t, tx.BlockNumber, mocks.BlockNumber.Uint64()) +} + +func tearDown(t *testing.T) { + indexer.TearDownDB(t, db) +} + +func TestPublishAndIndexer(t *testing.T) { + t.Run("Publish and index header IPLDs in a single tx", func(t *testing.T) { + setup(t) + defer tearDown(t) + pgStr := `SELECT cid, td, reward, id + FROM eth.header_cids + WHERE block_number = $1` + // check header was properly indexed + type res struct { + CID string + TD string + Reward string + ID int + } + header := new(res) + err = db.QueryRowx(pgStr, mocks.BlockNumber.Uint64()).StructScan(header) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, header.CID, headerCID.String()) + shared.ExpectEqual(t, header.TD, mocks.MockBlock.Difficulty().String()) + shared.ExpectEqual(t, header.Reward, "2000000000000021250") + dc, err := cid.Decode(header.CID) + if err != nil { + t.Fatal(err) + } + mhKey := dshelp.MultihashToDsKey(dc.Hash()) + prefixedKey := blockstore.BlockPrefix.String() + mhKey.String() + var data []byte + err = db.Get(&data, ipfsPgGet, prefixedKey) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, data, mocks.MockHeaderRlp) + }) + + t.Run("Publish and index transaction IPLDs in a single tx", func(t *testing.T) { + setup(t) + defer tearDown(t) + // check that txs were properly indexed + trxs := make([]string, 0) + pgStr := `SELECT transaction_cids.cid FROM eth.transaction_cids INNER JOIN eth.header_cids ON (transaction_cids.header_id = header_cids.id) + WHERE header_cids.block_number = $1` + err = db.Select(&trxs, pgStr, mocks.BlockNumber.Uint64()) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, len(trxs), 4) + expectTrue(t, shared.ListContainsString(trxs, trx1CID.String())) + expectTrue(t, shared.ListContainsString(trxs, trx2CID.String())) + expectTrue(t, shared.ListContainsString(trxs, trx3CID.String())) + expectTrue(t, shared.ListContainsString(trxs, trx4CID.String())) + // and published + for _, c := range trxs { + dc, err := cid.Decode(c) + if err != nil { + t.Fatal(err) + } + mhKey := dshelp.MultihashToDsKey(dc.Hash()) + prefixedKey := blockstore.BlockPrefix.String() + mhKey.String() + var data []byte + err = db.Get(&data, ipfsPgGet, prefixedKey) + if err != nil { + t.Fatal(err) + } + switch c { + case trx1CID.String(): + shared.ExpectEqual(t, data, tx1) + var txType *uint8 + pgStr = `SELECT tx_type FROM eth.transaction_cids WHERE cid = $1` + err = db.Get(&txType, pgStr, c) + if err != nil { + t.Fatal(err) + } + if txType != nil { + t.Fatalf("expected nil tx_type, got %d", *txType) + } + case trx2CID.String(): + shared.ExpectEqual(t, data, tx2) + var txType *uint8 + pgStr = `SELECT tx_type FROM eth.transaction_cids WHERE cid = $1` + err = db.Get(&txType, pgStr, c) + if err != nil { + t.Fatal(err) + } + if txType != nil { + t.Fatalf("expected nil tx_type, got %d", *txType) + } + case trx3CID.String(): + shared.ExpectEqual(t, data, tx3) + var txType *uint8 + pgStr = `SELECT tx_type FROM eth.transaction_cids WHERE cid = $1` + err = db.Get(&txType, pgStr, c) + if err != nil { + t.Fatal(err) + } + if txType != nil { + t.Fatalf("expected nil tx_type, got %d", *txType) + } + case trx4CID.String(): + shared.ExpectEqual(t, data, tx4) + var txType *uint8 + pgStr = `SELECT tx_type FROM eth.transaction_cids WHERE cid = $1` + err = db.Get(&txType, pgStr, c) + if err != nil { + t.Fatal(err) + } + if *txType != types.AccessListTxType { + t.Fatalf("expected AccessListTxType (1), got %d", *txType) + } + accessListElementModels := make([]models.AccessListElementModel, 0) + pgStr = `SELECT access_list_element.* FROM eth.access_list_element INNER JOIN eth.transaction_cids ON (tx_id = transaction_cids.id) WHERE cid = $1 ORDER BY access_list_element.index ASC` + err = db.Select(&accessListElementModels, pgStr, c) + if err != nil { + t.Fatal(err) + } + if len(accessListElementModels) != 2 { + t.Fatalf("expected two access list entries, got %d", len(accessListElementModels)) + } + model1 := models.AccessListElementModel{ + Index: accessListElementModels[0].Index, + Address: accessListElementModels[0].Address, + } + model2 := models.AccessListElementModel{ + Index: accessListElementModels[1].Index, + Address: accessListElementModels[1].Address, + StorageKeys: accessListElementModels[1].StorageKeys, + } + shared.ExpectEqual(t, model1, mocks.AccessListEntry1Model) + shared.ExpectEqual(t, model2, mocks.AccessListEntry2Model) + } + } + }) + + t.Run("Publish and index receipt IPLDs in a single tx", func(t *testing.T) { + setup(t) + defer tearDown(t) + // check receipts were properly indexed + rcts := make([]string, 0) + pgStr := `SELECT receipt_cids.cid FROM eth.receipt_cids, eth.transaction_cids, eth.header_cids + WHERE receipt_cids.tx_id = transaction_cids.id + AND transaction_cids.header_id = header_cids.id + AND header_cids.block_number = $1` + err = db.Select(&rcts, pgStr, mocks.BlockNumber.Uint64()) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, len(rcts), 4) + expectTrue(t, shared.ListContainsString(rcts, rct1CID.String())) + expectTrue(t, shared.ListContainsString(rcts, rct2CID.String())) + expectTrue(t, shared.ListContainsString(rcts, rct3CID.String())) + expectTrue(t, shared.ListContainsString(rcts, rct4CID.String())) + // and published + for _, c := range rcts { + dc, err := cid.Decode(c) + if err != nil { + t.Fatal(err) + } + mhKey := dshelp.MultihashToDsKey(dc.Hash()) + prefixedKey := blockstore.BlockPrefix.String() + mhKey.String() + var data []byte + err = db.Get(&data, ipfsPgGet, prefixedKey) + if err != nil { + t.Fatal(err) + } + switch c { + case rct1CID.String(): + shared.ExpectEqual(t, data, rct1) + var postStatus uint64 + pgStr = `SELECT post_status FROM eth.receipt_cids WHERE cid = $1` + err = db.Get(&postStatus, pgStr, c) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, postStatus, mocks.ExpectedPostStatus) + case rct2CID.String(): + shared.ExpectEqual(t, data, rct2) + var postState string + pgStr = `SELECT post_state FROM eth.receipt_cids WHERE cid = $1` + err = db.Get(&postState, pgStr, c) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, postState, mocks.ExpectedPostState1) + case rct3CID.String(): + shared.ExpectEqual(t, data, rct3) + var postState string + pgStr = `SELECT post_state FROM eth.receipt_cids WHERE cid = $1` + err = db.Get(&postState, pgStr, c) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, postState, mocks.ExpectedPostState2) + case rct4CID.String(): + shared.ExpectEqual(t, data, rct4) + var postState string + pgStr = `SELECT post_state FROM eth.receipt_cids WHERE cid = $1` + err = db.Get(&postState, pgStr, c) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, postState, mocks.ExpectedPostState3) + } + } + }) + + t.Run("Publish and index state IPLDs in a single tx", func(t *testing.T) { + setup(t) + defer tearDown(t) + // check that state nodes were properly indexed and published + stateNodes := make([]models.StateNodeModel, 0) + pgStr := `SELECT state_cids.id, state_cids.cid, state_cids.state_leaf_key, state_cids.node_type, state_cids.state_path, state_cids.header_id + FROM eth.state_cids INNER JOIN eth.header_cids ON (state_cids.header_id = header_cids.id) + WHERE header_cids.block_number = $1` + err = db.Select(&stateNodes, pgStr, mocks.BlockNumber.Uint64()) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, len(stateNodes), 2) + for _, stateNode := range stateNodes { + var data []byte + dc, err := cid.Decode(stateNode.CID) + if err != nil { + t.Fatal(err) + } + mhKey := dshelp.MultihashToDsKey(dc.Hash()) + prefixedKey := blockstore.BlockPrefix.String() + mhKey.String() + err = db.Get(&data, ipfsPgGet, prefixedKey) + if err != nil { + t.Fatal(err) + } + pgStr = `SELECT * from eth.state_accounts WHERE state_id = $1` + var account models.StateAccountModel + err = db.Get(&account, pgStr, stateNode.ID) + if err != nil { + t.Fatal(err) + } + if stateNode.CID == state1CID.String() { + shared.ExpectEqual(t, stateNode.NodeType, 2) + shared.ExpectEqual(t, stateNode.StateKey, common.BytesToHash(mocks.ContractLeafKey).Hex()) + shared.ExpectEqual(t, stateNode.Path, []byte{'\x06'}) + shared.ExpectEqual(t, data, mocks.ContractLeafNode) + shared.ExpectEqual(t, account, models.StateAccountModel{ + ID: account.ID, + StateID: stateNode.ID, + Balance: "0", + CodeHash: mocks.ContractCodeHash.Bytes(), + StorageRoot: mocks.ContractRoot, + Nonce: 1, + }) + } + if stateNode.CID == state2CID.String() { + shared.ExpectEqual(t, stateNode.NodeType, 2) + shared.ExpectEqual(t, stateNode.StateKey, common.BytesToHash(mocks.AccountLeafKey).Hex()) + shared.ExpectEqual(t, stateNode.Path, []byte{'\x0c'}) + shared.ExpectEqual(t, data, mocks.AccountLeafNode) + shared.ExpectEqual(t, account, models.StateAccountModel{ + ID: account.ID, + StateID: stateNode.ID, + Balance: "1000", + CodeHash: mocks.AccountCodeHash.Bytes(), + StorageRoot: mocks.AccountRoot, + Nonce: 0, + }) + } + } + }) + + t.Run("Publish and index storage IPLDs in a single tx", func(t *testing.T) { + setup(t) + defer tearDown(t) + // check that storage nodes were properly indexed + storageNodes := make([]models.StorageNodeWithStateKeyModel, 0) + pgStr := `SELECT storage_cids.cid, state_cids.state_leaf_key, storage_cids.storage_leaf_key, storage_cids.node_type, storage_cids.storage_path + FROM eth.storage_cids, eth.state_cids, eth.header_cids + WHERE storage_cids.state_id = state_cids.id + AND state_cids.header_id = header_cids.id + AND header_cids.block_number = $1` + err = db.Select(&storageNodes, pgStr, mocks.BlockNumber.Uint64()) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, len(storageNodes), 1) + shared.ExpectEqual(t, storageNodes[0], models.StorageNodeWithStateKeyModel{ + CID: storageCID.String(), + NodeType: 2, + StorageKey: common.BytesToHash(mocks.StorageLeafKey).Hex(), + StateKey: common.BytesToHash(mocks.ContractLeafKey).Hex(), + Path: []byte{}, + }) + var data []byte + dc, err := cid.Decode(storageNodes[0].CID) + if err != nil { + t.Fatal(err) + } + mhKey := dshelp.MultihashToDsKey(dc.Hash()) + prefixedKey := blockstore.BlockPrefix.String() + mhKey.String() + err = db.Get(&data, ipfsPgGet, prefixedKey) + if err != nil { + t.Fatal(err) + } + shared.ExpectEqual(t, data, mocks.StorageLeafNode) + }) +} diff --git a/statediff/indexer/ipfs/ipld/eip2930_test_data/eth-block-12252078 b/statediff/indexer/ipfs/ipld/eip2930_test_data/eth-block-12252078 new file mode 100644 index 0000000000000000000000000000000000000000..baee170abf465b03c2a68b3909af9291fab7049f GIT binary patch literal 50536 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 literal 0 HcmV?d00001 diff --git a/statediff/indexer/ipfs/ipld/eip2930_test_data/eth-block-12365585 b/statediff/indexer/ipfs/ipld/eip2930_test_data/eth-block-12365585 new file mode 100644 index 0000000000000000000000000000000000000000..6f7d87645cb411716955c529a4a24370f5cd0aab GIT binary patch literal 60035 zcmdSCcOcd8`#*k;y?6G?&Pv%)wg}mqjI4~1%FeN(Bk6xo}QQ79ohl38)c z_#M6K)uEHq>%BhT-}4XWy06E5-`9O#_jO4pNr7P2C*|OSc%?dDds?CnJntJ62m)Sn)d`0_`Oo0x1`zrZzn(VgptC+LU zsy3=+{aAd$&tzE5gJc&h*`JT!1$1paT9ea5#rTGXCirn!acx96gdR_XOkFrd@h*Wk zVi^#4|3-Z>t?YV!puWOw?&+L6c4}yX)r=ffDo5RM1ccE6)AOvFS{uP6MFaKpJ-d#< zZ*%VCzc%6en%@0x`jmcSN(C4cXZz~HrHm=g=-W9n!Bji`q0P|e68C^i%-*!w(|23v z49H_&%HFfCd9VGc(AZ4baESf2p5c)&_ki937Kn$zr=u7Sz42noJeLNuxRTF}I{6H9 zZ{e7eXWxu(R=vA;m9YB2-LlQbk%{NUD?nV~lg8oUE6W0AkBNl<8*j9p z7)3jyIkDuauO~#Mwq3Qn&^t=+x|6>0)6je1AO|vjKF!-0qJL7Xs`8lP({z&C8|N24 z)#9&FQUcLd-cQEmRdbTeRmo^qL+Fg|Rh*(653DL84V=t&2%HS%2r`d+?r{6K(luw< zOUJ=j>;Qqo>Z!b&Bbr=qdUFH6@H0rgAEtU8#$8A`k0$e?(X}u%SvK(4&KAbAjoHgE zrvQ4i;FCnVS)c$d9H@v);&F`K+V!$)6;7&8QrUDYQe{F&tpk0qu7V&OfESu~0yUjl zHYGhbuU8k-%klxftKtq;?k@S|_;Yxe{D30}cQ=(Fu*&bE)OWUoI39JsxY&E1&)lE6HeW`xJ% zpufm({r)64IyUBM*KKmIXRwEX#D#YdBsx&(T%C=CHmT)?#hzdI&=kwbKny9D&&9l7 z2|{=r`R?(!i@z(mnCaH3&UG)$kI88Q0Y)G>7pxX#((j^yKpX(;_2XKs=4?EhU74mi z4(#R9txt`Yg?ft zg&9X{jRx1H`e$rph~p=hb@D0z3>L>`tL*U=?FYjQsdBd_rDY^x)0C_891UD2;-Io6 zaQ!HGK1L7-lwXQ!IT|KX-Z`ajA z37;4B=e9xi3@G>$Df_|}VX|29Dj7Apyijli6@bOaidR6|3s@%1-U7U;i~8=TKs@it zBwg?2%_&_i2LzcRyR((FD}yb)8Jlp4#Dnh_`pm<3#V%6T;%$`&(R0q4z zL2DacM-IT(BT&BdL&q({j&u?B5b_U}W&J>OTSzz6Xw<0%847iPm1~ygr8Z@s$w(&4 zhxXva0P>d)$J}2V3=702xKGzjz^#tBFdRXm9!OV{dI2QPgCk*Bq3ZOQaoI~dg#Ugt z{#DSPQe8;z3k}%FO43sfJ3MoV6Hv6u6f9XFprb2}w2vk=J_=CQvSXZhhT|=ct9p)j z8aihfbN}*9ic`RrU#@svhi$L?MF{G)CXL!+al zZLFQekP0x{w7b~!L0dWy1`dS32f>e6Z>n%RP$Nh8xQkKr3m=Z(O8I?^{|JJPmavEABX+r46G)+7VdK{HW8E6i1_g$B`j2sGH$ zp{zZLUktPLJ5Z+40%oq)3h_Ur>a_!0u; zK{zt}Z)-?(GRb@0sU@F_yLBeN=lu%190;yp3no$3mi#uXWfEc+miYu_5O<|OLe<*~ zU6kngLQ2E61fu~EgbuL09GZKVK@(5%R>p+n^xM{~G7Y7XLNjF=PDt`maqR%$9Np@; z$I3B0c1f-I+cx`f9WdF)O{rJ3)Xp3yl~_@(OJ*Lg0Q$E|;|c9#69Y+K&t7v35OX%Tmqc7R z;r>-UP51>)t>OmQDFBQSjNiKR3>1hadKc=&PN*kP+N~Wg{*KqQptQwsRVf_a)iJ0$ zihk48`A9+M&*?jCzWbf+fVfT=FoL@7t|3zy0uMl-LBVw$n8x@PTQ}8IbjoUrX-8)l zR1xumw0Y!h7evMbuL6FeAW$85p7>+dop3xN z6LroHZ*U~9IX18ZGwMW*Z)_vl9&W+PuWkvdg=`=7d@W?t$mMsADY$f<5a2Cu{@~SO zo^~}*_?^uSWSOiU*j(ie0aUEI{ZpQec31XVcxNscpYZZiNFe$VlYgOw z_w+&#z`QLZfT$rvn(A|eXg61!I3f=T4IuLP1^twUFXHeLS-nsgMCfY4+;6nCRfW^WTjw$X2 z_(dRwcR(=Srty17Ali#HC>i5!IVJHGEO~V!{a#5gdA4-XOn8^F5+~C-^0PP~(5OLF z#plnSot341&o`^Ag7!Cqg(j1C|Kw~Iy zRIm&a>-q8A%?-!0wyGzvwYhYHHX6%Oa+V`o;myHIm!@cOmPMBHyq}j_UkL?V{Z8`L zOmkeUeu&NG9t|r>?9*u4*P=+I(F0Vz5#qe!@uIC~{d!}yTik3JJ#}Je%C9}5W2L)j zy$u3X*RR~g#{N18I5IsUTUV{@$hC7kp3Qkz7E%x4c!i$?A3ktNgYmv7ljsKG(r`cp zK3>_l8}P+9T~GU)Ko!2geE9GI0;S;{MfXFG8~|BWsef+GU7xW(t?aJP^%Z;$Nx?w( zj-gNH@oz+*%a;kvc~~E2aEs|UJFqL!m8NgC69#KEZg~e_O!B^%=6U==9`E%zciWao zW<9_|$2n5gi4Fbv>b4-Gxw_Fisf7FCk9THXzN0lhhM`Hm0oD#!uK!kCoj}ZEanQeH z`yIg__@XMEq2_b?LMnW;o3DQo*a<@8_zR_LSs8$l@q)lGY=eB+#$w+bY3=P$h)Rb; zo{9+;QeTLyN^o8|G1V)D2N8?E^AyGX^WNPh-s|e16__!vPI@($3jJpa2CHojMMw1 zREYAGu>Nav!}{tH{fLh?6q4!V0xyLCMqDUubZo5h>_t2P78$>5e5PZ!)Nk}FkGs*O z_(dJdy(vqKcradBkLtH6OXsQb1RU?X3HNB;leLexP^!+aiG~oaT_n=0+Z&c8o?@`b zXwnW+!srbe|uX${qd+g^W`~-zHlwp z#L&-NX{3ri+TY}z`YCVPWusj${^ZBu0++J+z)xmZVm;r5FbHwJy6>_#<0o$`w+6{d zM7E8005RGLi%W~-JD09+ZU|hnRJ>z!H551lWr!G92?>?iS~7E^G)+#YJR+ysLK&CK zo*D9LvoQOcJc*z3$dib*J8I3osA0>du1qsx2wvAY6Vi0C<8;aFUj1Bg?};#+9T9hQ zel45*G46TPR?l+=8EuoNA=hR<5pTQz^k#b3KIckYo8dg;tlc1-p+{cPTBfyBt6!3N zkG*=;2fm~M8`b`l*Aa~8DPu--9`x+w=`F9B_Ru3VE1_sd2V*lla`q~C>G_w*t#`M} zd|pcXY>$-RI6H(P?H+O~B5B$~0o~?HI^ZU}#i;k>y?KIHi|l-F32t62XGztg7koZ* z%)G7xPZ{?rXjJ!8AWycI-?W0N%PS^oS(2a_d|z$p(GXgf>X&b;_T)j!QoTtWLZhmi z-)<9M&eaXNaIJIl7`16%{^@2DGlOm564kt#vZ((#I#uAj#{K(QyT#9?1?d!Dw`lg< zym8YA409m)DQq_5x}9oorfa2tluo>PIk^}i&vHnSyN3f_ZXo(vJR z3Kce`>I*@>5bAwZ^zLKKj040(CN>K+7wl>`xS366EA5l07Xr)Ax)!qVTD}+G^~HtH zV!%dU{}T9Z7Ly}3xn7kPc5LXf0QGghEV+gcTl%5k)Tw^OB6tv z#$5f@esH(;qXSkgsr4Uu$*<}YOPWS-@~no1c7JHdr?#Y|j?O)K?Rr?AuiPQKSLX8C zB^P?5HyLXwiZ>}@FLn>nTEjORfHPpubB4ukqQ~wpCiK+uNi%8J9n-*!Ft%wO${Tm& zbNr^#hM)4zT?a2&5aw@r#U%>Jx2G0wi&hDTf*qGhQx!}11d`Nrr~BVIj?;FEVk|DO zHKutc>@C*q0g0YsjvK~>ANm0+O&+Iv&&^`$)ZP#k`AQ7hHWVmV4rKI5R0*1FmOfmD zeGC9U`L0p4EefbdFr@R?_?L}iVL`2@t506o;%~u)ln32N!%If=cczlsRwGm(T!*@4-2r@m-3-r5wf?$qH> z9Hf~R0*g+dX}HxHTz+T%G?)T#Pc$HTT?py9dQ$>o&KELrt?bg;QveuJ#+c zQT^PDcUQROH7a3FYEGOWX{!F^2O>dx{J4p1#btNki`VCmW>IZ z4&HI?tZj6=D%PI0sGB=l#wjQ+Ele-PymWo|X`+PM23Rc+tt}@QFCobn5{R9~3{5{9 za*}9NwLTew5A&AEj}>nkN`{nAwHRD7`MX`<1hN)K#A|BlPmCE^=7nQ}POZ2vdqjmM z4kF^xphxZ(xPnQ;OMD*%;@{rRXlgrTGYOtjRYe*xJrWu~!uv-oI>1{T(|IhBjce+( z{{sCYZdDJ?TXl+<1rBS@91bv(vjjld$mA4jR=X`#qY?isF!8H{&a%JXkV(Cx-Q#J- zrn43B4PUgPOTl=T6;22RqA4#!DVVum#l7cZH2$EXSN1e2#cJ6{7fJJr<6g>qiQO{8 z@KxOTF^r2%klMdi14Iv8V@W%-q;IdcXw_ zPw>GTz_@y0WmPlk0v0zQ5<=wL{|4`l))8t~w^luSLw{-OT0pKK1z%OQFpzjCYxL8it18qcR=v zurms+x|WI*bFr-MB`(#Qo8i)L?QHlr(_askuF*Th1A0~}#eFMO;m@TgaL#C_^QWoJ z8fvXcjp!|38=!`PynkvYqaRumbFC$lxo`2YK^*_rSY2Aq*pIO!Z;6V<9k>{xcy=CC;$S{1Kpx-UGIXNaB(ZQs&wfj!CB_{`0Ptw@5%zs z#x!n4;s7!2f#-itokd3!??ZmTuhx7CTg zo+w@-HAg|0)q?R_0K1#s*06m@>g8Es*7m)*%2R9}NvSqk?CYiRis!DlB{^iEJa^UD+H&f2y^Lp_A^joHKI8 zbRFi%8H}rneg(3(qe@<8@`p<4>iuHF`KMXanJuyIDa6;G4rFXaVx&?g(_eSaBz}r# zcrDoHkXzO+viv*u{Y^6gEZ*-qfErcR3cU+gSVN3nGhRp)WV2{66RlM?3rlqT2wA(SW6`v^+LhT&4|HDT`k{$v?5qN z_UW0Ch#DWjjL}YNhw0LbGgNlPh9}X^i*nM$Y%X={By)(q8Fa$HhdILg!(J=w)l8G* z$vB9ojMLyF6NZ(C4z8K!z;GxyAz})X2eK#a*!z3a6p_VW~1W^zw}0g0rW0~)+| z>(i8#&y*it0aTuyi2|jj;xGBHfn8lw9-GQ(@6?T`_NRWzSLstY9SF;l{_nTBx5XoP zS;NnC(M;FS%HObAj-8Ip_TFAxNO^Gon7mWMc!s6JqkMo#IV@|E$>bW3$E{N@a?S-> zJcd{i1at`qsiSHd=w?6abr>#NO^~`P| zR@1?;JHUk*cj9Zc-J;SH8h&MQk_;<}OQji!Ebp_U2?c3KT9jb9;P0(ib{!g0LH*t_ z+CRLw)&4w{{Mo{pnLa6aSHiuJU4rOD7}p=@cjw4m9t(+_TzwQ-9^*Gew2R_l2cAtc z@5;^vy^^oBTNf>xbH}LS_Ak+Id&*gkACx@gTmZO?K`QiVja{R%`JC+3SDdzFhwlcP zDeojShhL7NEpUSSM2vUy->qP8;)Xr)W_ei6l^>=n_E@IvdT?*VkuZ31J2uKy5R3bR zMvFu%hvL!vy*xXv}h42Sb>2A!IEBsrn! zyF%rAP3k-^a3y?F2PD#FeO!r*mBP`xlZn%`_GW>20ZZu3v@JvQYcPi&hVVN_qeYAe5vCa6KzB=5viljnDC?n}IZ5Fk`e!AzhVx|vk|W=s@= zPT|fDu<{V!Q?MdGE%Z`zaN;-JEFkEHOW8O{u`$C3Dl_HZX#{nEK-TCS7(cxR9Sl)Ntzy}=C9Xm~cpt#66Zyw)k zp*=LL??kbxu5Gdo=>t}Pv^-gBV$-N^VsB4W%jAQSFoicd3ZzAn9cw3+@C4h7-w0CDhY=P?VW3ogB=6z;!AYiN40iUTH`T&TilV)F5 z07+DN8i6o{)xwyO6wdv9mjn>w?$Lj+NJ2+Uqv+lFrZ1I6;sr#m{QR9TE-p?_VniI( zA`00?00$8CUd&qM3;}tH!80p{lS&p%41EUOF$|(A9pO)WRzev8*`yrDb~JMACwIK= zaFW(8+R2|#<*~h^XzLO1gwT8Y^#=IlwP2iNvyW9O!;Ef=}N+HrCqw#cW`bPPPJVbi9Az|)4_T5K_JVY9; zVfzZD8U&FAs%{uaY&EznLwof?X@Uy=Q!B~3{(TV+1oj$@19;+PxNAd}&fV78(4MMt zp_5D9swV>&9&xnKh^W88l>`v%6b+Wp7w2>q-WTC7#iJ~jTr@C0tH0g09h|1pwIvSA z#r{iY0c~bbWkz!6^a`tJ-c) zp-PxT0NSnZ4GacJK`DJoR^^=oQc5_5RBR?(d$X55CPIQFy9e+&fCmcN5w4jtHHB}> zSVoH`SKh}$WSbKWD|{{B7i1$wsR8z|IO13AX`T#p&z|>lv8~lQ`){W{V?6bY>B;%V z6~h$RW&j`*jAPMc2MWZN@`v8U?|Ylub=JsYrnmG0Wux`lV1x~%Ji0AK&SEQavo-ih zmuHTk<`N{Dr!+{8G2HIHa@~Oj`#VUlZ_)GM)Ah$_AYx>PZ%EI>h>@M0{hcEMVa5)m z_!7RFI1Od`Ky4V-N7z1^WgRSc_o@t4J%MH92Yd=)Ku^2J^$w&^*kL@tnDze3c8N`Q zni0P~&JC{<)d6{9G_alk4z2HX$4iDnJsub*N!09?*vq|%G_zLYyfmyk3n{nTtGnbY zoOO}7w9gS4Cxak? zNxa{B1{V%_QiTKNDk)}}_0Dl2{)Ax$@~@Nu9vZ9WXdrYDt}X~fs0Tmt;{XbE`D$r0 z^=wJjQqC>R+2^-&9(fZaKALq!Q2G3B0wE)ya850&I5p8i(OhkG+GJz{la=~-8?%e` zMTOT3G`p0?D!`w>8{j2y02=G}dL1#TgMKKT%a>Em>YN*`B+|mPt9wt(l?(URLCvo) zzMr8YDE2!^j{&{cu%t!$EAk@(>&eHdzeWEPa6=IhK!NYO7JIIK&7jXjHa| zYCJ%eeH|DWzHb`9Lb^@*b#FyuqQ)pmgEbsi|FCYvX|ImLBJ)c;v6sSo`4p%#zsOQ1p*g?krFnE9Pmk`+e7uG%o zcx+!E*@t}#{6+xSq3BT#ZBjm^->*r&b7*#wX1A1n3;!XouXxBz4>2E9T@W$ucY5EH z4w<^A_IQa zAf)PvYZC8kR0R+?DPNHcqvZ!R8@-a?HC*B}o_!EB5Pj;tiBP-aIcL%hbo4;*lwL5- z$s^|AKhHwu$7DOl`?_(9s&T&tSTeBDMz=t{^c_aiOR zUxxGF{4!E(kIA4K{Tpoj>x^}Q1s4c(yArIY>ywO;PIt9E!i!Na;>G>`&a6!aTwd{|$;Mllz)(_rAt+O<-KX{10 z;P83(C*=gMQv{fPyN1|{zj?#rGW0~97{+x^J5$B^jPUN`pkx(?qp{Z6eRL--~Z-6zy!8rVvoZ$6 zoI=V>4dZAeX3D3&O1zapEGq(yr`mc?Pf`qlgPepQvcKwLCvldv5xYeazd}Qga&W~i zKV&f6Tdek~A*M${@AE9E+C(ub#_Jl+P2v#8`*_?IH+)l|rYao3^0=uP@in}NZVcdf z(`7H=rCGWK#hkwMErr)PpC48Z0WSJX?$-+m&hZp+JPL3uDZDRp{do8X{F}nYgxxyZ zLwXzFEAX9&Z#qRkMTcCcu;MhLvmg-ss`VkqqHMuNP+)KNnhuHx( zJ@QREVU;({xXrQ*j@3om$yK^`imVA(ySiIJ?qpBY!pPIaV z+}{;cN|1&>@%Y_+Gq@rDt#rO=VN zlWhB)iAg(Obh=Yc3c)uivLE&L7nCxu%SJ!Rl?P0>TUYVl*c&v&4hCxAq^uVcUAneh zSeas%5sS@Hp-2bI4>6`zrH`GTw$t z(*Ye}+7s)5Ph_6b*Z!!+O$^KkeSVGm{R0V&c$B0R?=gW3qZ*S|5(#tmI`(GIj^5I{ zWt4Dgj@3r}GWbFFspuSvZ`*%+oo??8$SCFZG?$jd(P}?=ZVd-c%;Swg{itAZ)* zL3`VOTz9;%^(ehP3&^lcg9X+CZkvKMvay9kf^TDNzWUe)0vt3%y`1mrbLTMgzmu{Z zUdl#eA35JbYvUC~0p(uu>4o2kqXNSh%HMi9_$C0&CE4KaDpHEbT0)~zFSo>b=+5#i z6od1&nV_;yHiRA+3BY*!sWT%hBiuBJez|gfw@Z1sFzhj=$*f5-o4|qt;LxInrb{*Y zXl>>ZJ9JDkzSzm`QHG$e!0M;=ewJ%0xG+bI?@a-(-+h{5WzYDXlxsIRLepa@AubA+ zG(+iXUML+|4Be}rHa3IS8PVhG1vI3-d9kH5PqrMXy$M>E{3Jb=*zyx<00oKV!UbZX ztKH_;)+r*A74CZ`IoSKYgpfq|YE-v(pM`fPwC}aU&{;;_ov68Eu%HGgy3<~3E08o5 z9&GgkVF+Tl6{qO(htS}p$EV+@vb?tmE~d$-Lc|Z!|LN`&1bTDMqJTv(E7Yd&A_TkksW zkI@ZT2kVa*7Qq{+gI^I2F|lG!wBRn+dA^K+vuSymy6TacoMwsP5t61IZc9wyT9o2d zzw{pE!Qzh2clJ1w#5d2d(s0`0aS9jUk~>&S!W=OFD(hZv;^pWN^)5Ff=i-{q#$X_3 z=HWae6@npW)m-^)=RReY5KD3RgVD)hokVmiS1i7gi!xPX1FkK+l$+%jFV_K9ceBVU zF0Kr2SjvX%%FnlyfS#R^-ib08T^ggk8z-0rH#U|%X({kYh$9VhV>?SY^)3K~?>>8T z)F5XiwTJ*wAxLNlBJavB&dN)&@8UukX3Tm1zc@$h=fkq8bb=3G3^q` zPm1SXV#=6QtpR;mQbo_qg1~i(j!LJjO>ad%`W)VYIYqU#BimYQyARayU=V`H=(cf3 zOJ9mmQ})e`ZoBo!dgg+VIwF3M{$J>n9dL@qRKs4Xc1+kj#p+Si=RTh;jHxL9G0_f#o2;I9QR& zFZI-V%MkqrzlEbn4c0rQ5bp5^7;`Qx4?b=Crc$r~8&nXyc=78o*u_;riC2pD9g1Cn ztTXUah=U&l$i69$?@aq-H$t7^=GUW+HhGn@>A%!&k6|>X>^xK~GED=3*nwNaL^@Pb z(}j0c2{!U$ik=v!K5QpEyE~7beidCx)z}nZynRd0VS5`7_v3TjkGri$Ez)nalilbg zag1HBff7*B?%iqmR@ObII6H3xAI`rHK6O^o`!k>=9<+eroBg}W{y~98}WubO0VK?{^o&|#T}Md zpvih|B3zDjHl+T_(P*&qZx@trDP!}_)1x~87zT~i4_ty+qeDDtsE(2zDK84vVU)^^ z(T(+8C)K6P*?W%&&FP1~xYml?w(%}o(V;!SJ#_wz$>>)`E~O<6-@QNdfgqxl{1`K* zgZt<4DN9Z)yL`^M>l}6k5kE-7{l&yV-huExvTdrRYk4dqW{#W}%LUFoR+nDXd3Bo| z*t~YGLjO%_#xb&J337F09&#Y;`Ov{a)8zt4=-)x1*}na*dT*TVnp9) z?W4BMN&1JvEdnM3low#R0^ggk`qA%Z&j&BGRHk|R=gY(>7>{GiXz~%cY7xtI9H`p9 zAhhHI4+o$zN=<>I{dzp+YFqN;V2gF)&Qf@_5=n@RVH$bN*UA7uR$ww#p?TkBqc534U-RfeC1=uP$T-lOPR}~F z?;xf>uH@L~%Cw;SvC01dP+$pStbOE&h$GVIj9GS%0uVXB(qQM$Yrk^$aWM9UehGko z;rufODFj7)W)wIk3LH2b-{+E$M8!pso}dm%A_NvO2!as$KOj+w&um}e-}gQfDjP&) z<0$6V=>I|=JXCF{DtKt(e;-649&73m%L;r#aOi^~56m&d)c;(6Skj2;kZFH71K8TH6B-W2{Anq`cOxRSU?>7 zFC<&F)D@(dXR*ZR^hj7@+{uvS_n%}Tibs+!I$LQ?U_TN7k!6k;Ktdl{e-H=_IFQDv zFzt%CrIq&$>f{p)*ZiKsPvCz(|J=rZru|nauv=c)@6w5TOP=tqZu%YkAK}Aq9Mm3` z5nKGiKK=X}cmF&D2|o5KZy$&4^A(F?L=LLC3S!(ppnu69RUiC=JQ1VF(gk@>csB-_ z`;ovt=kt7q(o-&QnTxB*OziCZw;c1ji)mrtc ze)U4|J<>AkTy0zv2Id{r@fOKTkjo z&l1hbFXBCLcpMM}MJDoy>HoR!0*@i4|39E1C}d`0#*^){K~%mW#X2htv&SCuJod1Q z`A44eDxMf}6f*uEKhRqEY}Ls<&40uv9D7odZS^DcyI(Qt-ywg%mv^bIfFJz+d{EWz zXUxASuv>xKLEisQ!jQwu2@WbL60JKt4hRAq9$}yD{DE)!+ZMiOyD$6)=?^c>KSv=b zlKiq48il{wPyAqvVjIiGS-~RURlZMHABsHo)e%M9|CeCM;f47#`$3V(KKuEDzIjlW z`@(;a{_raOa}-7OK)=|}KiTK`UYvb({3k_Z2WpZYgx5-3FUjfc^0-}xedf$2diIU@ zE&IDNEyK^nu2uqKts@n;AY8j;Ty&PJRpR6KiU&;6KIF{68UkgiRx#pWjt<}VRN;H@ z@g64_my(VPrx5A`3YE(#3nctRp)>`-CmTHB`{L|?x3MF@*f6lc(TBOZ8R`3!Ghx85 z1oyRia;AUs(B(NQKriy^r!BkQHq7TX4DKa7Ma6Ct%8P*|7)fGkCq$1M$-*4IAENE; z_@~zlq|}gMT2DI{8{OTyvB-ZwR>AIyHoLK&`S_M){r@ zUN;mKD4{4=Q75gsH1NO`da8RJ%2A6aq9QxpEh*`%#5FY8Nf%Q?>>)Uv`_|tC?cFJr ztR^y(<|d+aNaUx4#W5f1I502yMURa7H`4GAWE?=Pr3_2uaW9rTZ|i$YEkh z3`|Kk>0@_Y%13!ro;5#=7ovZ}q!3HOsvI~j=S=O-#8|K@(Gr7267qUJ&qc@HFHGM< z-n(cc*I~QTdLx$H*8gbA>DI3|eInTb*mJd^9xJd$2_`sk39KI$^o+HzM|q-U3<|pgSM% zM^0nUJ5OT9gnHCHf4fN;x%bxN!Q+X4<^3Oa;3X@5_U9+vwyoQ3veaG%4($d`yB2Xe~ zz+KcZ888%bzxPNEr<#QO&6{pVnUo{1Wn<$%=15d*y{UW@pr>O{Q`*3+N}qrByfFUq z&?Tt-ke@AU#CqF@F*{lQELbk?cayo%2c3KWB4c#;LBd(cvn|7h!jbCe^LfU$GFY#g8jJ0@u&QsZL*wO=Y40e_POqO zQ@34kN6pFZJ0us=nx*mVTQEFA&xV{EjL zV5Fy~pAj_9K1S$6&N5wG_Cw<@RHOxKe zr*;S7*ALO7znfbv_;=@o2R#(B^Ep&+D8fC|bR3?B>4!&ap@A;p4^9sr=h?ohamAuB z{GlNmkIm&CfOJDZFCZ=V*eDd^d|Di94W87vxd@H`!JGzjtnZCR_$Me3?gkJhu&El1 zHYrfg^ng6Zw5glQxi648?@`Z?O z;bpbg5*NUAtvt<9QaFq}?s_IB#obXBT0XqA;g;2#IldkC;yakIeE<0$9*WfEcd9vf zZ6eMd68n`fOE^B&0$)B^H%MOC<+odrdp4oSGG9XnjBRvf*=OWkG5?W@hT_2&`64kAC_{7 z@pt7!#1GQuk)8VvsSe`s@IUGutH9KDYK?N5zb$uu*LojS{Z5vGXOzz#?S|Pa! z-TLDd%Be#3vE$XN&9-@@IChoqj$s@t>huHrWNBa@%A}+5hsd28e8NT76i%iEU3|dd z@4qe#FtsLafF1oGes4u|9tNbX2!Ejt!;`js-~I-o`fj(H9l2=Ml8DILN6U?j8wT}S z?Bo3!01#Oo8osvU=41NiDdqD;qrPQ$Z%4Bc@q_d~ZC+$|s}b`=<{UYIXQM1DAKNB( zE1dHQTl_YHiDGXh4A$IvJ!<1aiRB(N1)Ktp&G)$%pIuI));x1XF#7Sl@d86X=g2Dq zdd@7U&jic?>$~fN&v^2x-wsx93FEq7pSNHIp1Suha^DQ`W*l9%T-}@TxMr{WkJFQC zEFPIm)4$Fi=ss0MX3LWhlNDJiI#J6_0cdx$ymh*&(HcJCxOzEfE(JF*Gs5oDjiSrl zIgY+X_ipcfdF8K}U~lN}K0$cU&;BbjK@Om(#~^Doc$qoh?&3@S%VVM9K{h9wNxMu0 z^!fTzVTIKIk5Sd4)HO!qf;QpsBRh$o2IZz{NEM6#{g&DFN$(e_uw1I|MQt;HimH}3 zxC%_PkEzQGNJ$22R*YKI|A!{69KgHv=Z5I2Y?cq6Vb;p7oEBLss~m|G(oT{c!$DtXrYH5WK3_j=6j?>z=h+xoXX=Jy}l z|I5yNIX){RS{x#c)6T^sZ`w|T$U{N{h`bd0OiQ0nsZO*pMHOyG#6RUmjXNRY2Wj|; z(Z)gEf$%_=U=|?HQBq9g4iwEJ+X&r=#@Pr?7E^=jQWAVgelrXZ zx_g;LMyTRraf%6d@oqL;k);~A&YE@6&Z3z*G+D?1=HUFZ$5gCUe7JCF4(oNy+1}_9 zvODiNa56(!x&v(>o+)r|I2yYx7RG4Rgr;wPEGhlwQ>(ZuC9iAoC7m$`hRpQjg#kQ? z{2tuPcUqgzE6~yQzYiQ906QhFqowgO;P2{be(9%=Ku$Az`p|W?fN3Itn^=R z5axWvErOmadA6QgGM<0zHKhDHGh97nC-zi8@g>K2?-!Yh_!*Bhsqd(*0Ud6xU7&un zyZQ_igy%^67xNnKHm)&{5EiLPpJk%fqSgPXV;CKH>uZ_C@zn=oWkTC2{Mig(^({i~ zU7_9`$}7X8Yac;HHZX+&D6;f*15jjHz}d)hsRIMjm5Fa0O~mzu+P2?45pfR#dDZ#$ zpQ+5g)go`@akh{bkO@aSGosexWNn>QF*M5}raoR1cHBeO<&Nm>iq)63$?ydgTHALu zff=U$^aFAgnTzL5qer_jqLWlUaHyNUozDHdr@Y@hxbku@KH-dp6Xobkj~{VEs~0*dWrU~&}5iF$VTYRyyWyej?pqbj7&06f*eyqWm3 zmEw$%F+JKI=J!q;Ono-b7QBD^Gehch)YlD6!T_*;Fb>O0W_WQq5~1F+*v{uOGauq$ zo3P_d`46-!_VPd~Ffwy8>Gy5wAdC02j3AKm*>cEvJ|*?|`f>VcYmGRW1iO82H8bbu z5EsoooWO1|fo4oq6Dbwt?zeQUZwXBt{4y}mtQPIeJ2&yzF@J$r>zYGh4#Ic(Z-!qMVYd=kdexs8P-B)|Oq-h~#iTZ1|Ihy3LY1B@i0pC;}5?Q@K(3+nHn^~T~Bs{0z;3neIDzD@qVcyHd^ zwYx=dV|aC0g{bbD|`qw~4eQBahzTC=+!j0PK~1S~R%lfO-Y-{8yC0TCAIX z1TAI$WvWtQvH2s!%X|NAN9yW#_lsr~_b*NHL<5FiBuD;UpV$_kI7&pgV84&?4FM2w zH@^4Eri;&KWL2dan=YRnC~X!!kBA?n;X6VP5AqI#_YG%h*wVYHYr*^lL0#P&)aMzT zIP3Hg<0Es=@VC+!1~6-wc{&v1Mb*10%NV$*Tv|gKeVUZKh1~D(dyfGWKBgi0<-C3Q z-1)w#r$l-RPU6kVC*3;Juo7uzSvIDyw=?n}w?NdNzH{Zve%s2>P8_C6eK3BU2))3jOr z$C?-x^8Eu`wuP<>1;DfqgWNER^#8{*G!QEJsBje3Lj_|2hsVH{MuCqVj^jWM$8nia z;L?ZVAUag|>N^gBCs?z2c3xsWr}{QtGKcU~-;}WosU%=C#1?+)#pY33%9FM*{_ZGg z;$Si~a#J44cWpai@L?;-{p+e1ivHd7D9VQl{>L+5RAbn$0`d2ak-y@Ha(*e_zdOeK zD(&AZ-(G&GW+uPN{GV~YtsqcI`+vCnAa-C1kPpS2c==V3xh8!0*+tsj%@+@$CLB33 zLlL;aF*GWG4Q#FWXd3315uz@yoC}u5mKoi`4UZMahq*VUCO_78qD!T&-8I zBKH;;NoU!kGb`Li=|%_#{n13T#Ud&+FYBz+E?WYK1oQFh%U+8F~8M?#w;;UTcVbqE0v zl7E1_h=@CK6~f|ulHA4PdS*qP_$NK(osT2psLJ=~sl@%|iRzuZ>~h3L?J-zP5634E z(<9P9)PTrCq%X!iz@LWb41lZ8~;Z( zz?{5H{OAL!rS{+Aku9VD2LB;JW7K?KP|ZmOMKTBq9HVEFuoy+0zv2I6`Tx%JZ~}5T z{OkSK;WN;YHO!kTo?GL0d^WC5dx99ff0xIf=nz!_ewX)uh>uGAFYn-@sz0hWMB(Gn zQTh1)P`&?G*`UzxfBqexf381NKK{S1Dik^b{ql|v3LE-Y3Vd)aiYok52Blo^XiF}t z@0a|H|10&&J3jxUe*elm>Bob)qC+LJRqT0HKXK0WvX#{B`q`Q%jPNlnB zTBH#qR5}zqbO=fd(v5^7DUB!yf=G!&|ASZW9e785@Be*%^E^DP*yo(J*Iv8VUR(Ep z^~*@Y|dFbqRr69|o zL?w}RY!?g-r1aMm3c+gr|7V8={LJX16yeM-H00oWcgk;x=ocwYSO6>^w;`PF_%hX1 zA5=sqDvq|V_Oka+ZP3G}J%OP+G+)lr&fuqZXr_~u4cM8PW^%uhr_k3_U$L=2eJTuI zw+<~@1mUi`9Hk~Hllp-NLo%-#-jrF4r)L)5lqKLV@s_&^@Hv7w_rW}@%BGJdNeKW- zzDpeK4JeAqJj|pA%07Y6uyfE8GvBpsvH0(`tyf{~;L;nj9B=<~Z9HF{wSS@gyNM70 zs~rJS%x9NWy2c!sIp+jD&fg+%xgrb8KMVg`Z6}tPmS(Dr#gR_!s8BgHQ=hYFe+m0e zc-p21EYQeaMvl$q5>f~Hd`U@}{aAg4GcghINDQMaFdBr44;a4>LHOcCzSgZ<^i`+? zQ|EU3CC;fB3Q7kioJ$?GGUCv?tH9DpVW_K^N)jO_QApwW6~S@IZ>*Pp^5wXys8YAv zq4XT@yCdS1SXgn-!a{j@AbQ|SN!*3_WKK@of@_pZ_}$}_g2JI$+<9%c@Q7E|vvOnr z%Ja)u(O-}(Kr?J;LgpE0R(|F7o>gTyy7e6)%fgsgC!RH^AMXxBT~)%(5}NVy#g=rP zE1iufbk%!=AI&{wafTzSs=Ze3t%WMKQ`-J4Ma(ZG-~tzSSFNS;zQ`Ej_TZ z+PHq~=iYy{5C%%CRz*!5BVy|SVEJcY@Lg{QMFya=GIQoef{6naK{Uo;L#^~<250AxJlSn1)XKU+bng8E zejX=K+hcq6k{XvZV2}+x(cz-TvEJIEcqx1Zd*=DOC5kKY^+drkPaNNB;z5dpew4Fq zX+nbS070Ps@w1_sMk8;*i~gEM?ICc=S<<{hD^Q|*+Bi`*BGDqo2iMS(_KY^ZqYCd| z&WZ`H2A_CPks>xx@K(!{97>UKjVSXoULzh{T)uSPQV^^`(L%cz2#{Zjd}-$xq>uO+ z2UpTDg%is_1&C5R#_vl+&(kZMdaU9S{2`s z6P3F5vxPOp@QeC`Hwgow!W!KVe_jZ^M$`HF-kM|e#pXT;&jXq2TST8h+LDr}WLUu&$>X}Ommg~Z>Bu_`R#Jny7k)$`1ZTtkeh zwXymsP}F5UIJ@6jqX$EAt)I;Z&5r`c8M9@((>p%H*DHwHr_tADmg54n{-ibR2U|39 zntkzHdLCt{F6GaWuwZBy=)6^F>@LZSt_skaz2#-#*6g26Q&S2R%OdUBS`1sw44;3= zQG~RNralfS691uAj=_W^L!XUIu)VILS&9=uPh_d`;cEjpHV~xFtgie@#=qnKnBgdA zUrn#pfEs*RC6b8b(Hcbn5LDI;@+P!GFuk%Jk8^8kO1F*J)rGEfbw&!&UeVjAoirhU zJp}pf1>dQ3w_Ppgok+DSccfmg?|wZP7qY5Dgx=*w{asjuY|)=}TEDu>Es67r ztX1;GCb*pnlC7G9#B4tWCxz{)E*ehGp!xadH*`G7*s_mPSTNoG_RVzjvyRc1-T!d^ z6Z$VYEkxL-cJvL@k=pkTuezkCn`DN4oHJ)U72yqDrv>{wyiN~##>{@tPDPFkTR7Z&}8Ne12B#^29Mk!s%S0}Wc6t|SBw|_^xSROUMg+&Fw zKzJ9CJMepHAU0zpL{Lgx^t|4x9z?PFkWXcovN?tm^T(04T7(W`U2SGqq2R!QaA^M> z>HEpK-z0AS4Ikav$x^||+(w7{Hwodh|J`&RUMNfW?}bvYIMoFgiu}1KCSEsf5y%l9B^%b+;i0u>)bz;;6IUe_QdL68vcJi0z>&< z$s;Hb=8f<`@Q(z-j))R#57eG15-{Vx(*9tZu~p0_vnT&K`ka#_ z!!4vA#kAUex&Fyu`pM^FSUwzB0uBw96x!m+dTJ^}sl9I|OUWk_g`?UI9Fsd;T7G#d z!uId3R)Kvv6b3h#QVpD;eMfOL>o^x=thnsU;Pd~YG5{f_m~XLj?_J)keg2&GF(4}K z7+P~9_#NLPCw`MQN!u~NaEoH#ZT5`hgJ(wpsdieO=^k2vH%tea#RUuQ>0#5EKpoM* zbbqNkxbk)a$FfTH$Z)0yiA0cevgQoYlig85I9>5b{TH!IL2pxuT(-mug4#1a>SD(H zZo8BL=y{l&`*(oi1B+ti30^fKcbv^wMfW*&ylpnSNGVlr=uJz)1c{VlJdiw> z{jTUaS?Y|ODIL9CRXW}{`I1k42E``sgLsk^}|90~9FJ;A{RaS(K{iclc zVvo>Nc3)vyV1N23k!LF5i|^-Dku8ROEV((tQ`RQH+|4^1WtDfNbG ziPA6W#irlBP2x;%eNFtuAp{-2mP~5Z*4jDb>5S?4t!BhC~ViOv9!r z-fLZ`M_=r$ZCgD`r9?Blwnw`L(9*nGW3*pJJFlNzSC zRrGXnH-(}gMY2C+{e#Uw=aMV*c3E#9vyleeq~Y|rDhf;jM6ylO1^GKvT&n7+R~6@< zwgN0AgAciY8!GK4kW8TiO*ekAd*Fk@t(Nnm;?J*XtbKtLLf5RmHINfH3D!XXM9DuR z)Uh|TFt3>$W9PH!uqZ%YmrYhcPpe!>Ew~lC8PJdrL9W(bBa@U21=v0}3 zd2~&(cXa?QUa_E!uwX!J+!K`|^=KAb>cpGvwk_j3kCn}jed(i^h4;*&>y%N+o_3Q%1`O2_6FN{%fRW1=zOkdaJVl@{i0wbCIIdFupYu7A!1 zgst|Rp|McjA2o=MSgYDTe3pGfyjA?`CTX8B8@uDQw(30fFpVdv0*^K*xfMyXj@_Ijdvj}_f9!xyJ@d>Rh;J!Au0 ztt`1+Aw}N}4%$Hesfr9{LEeN~oQSBCL43Q^12U6w3q7CFgRqs59u}~(F2f4b&V0r- z8^G&X-#|mbqQ>s@>iT zjP|3p?6q!x6xa}bKN(>Nl@zU zTze=xq;@cnx$Y`>65jvUib;O0n3aKP6UYDrMt@NFJk(l*f1^QAK?rd+ z8?ZhYdFfgLaDDeCdvaGT)kducRqriv7X$9}dX##b(bpx0kibaff!}|OJ;XYl{K82o zFuH`HTJCy?=?7e(5Gh|}?+EbF=ATm5s7CC+wDpW*5|F<-0rK50Md|kD>e{~lhP^FO zmV-s|@z9O9cjv`Rv4SA>e80(=-T2ULX z<6g}TLi`S6Q30M-`=O!Tdwu4s3to=Trfa6|)aG4zK))tAntJ}}(;PjB!}syx$w(S* zD}gR>zT9lD_#|tUrPAlDX_hR%?)O+VHxo9?JiD|LW%@1&a-B9}JtO@KfQ2B4=e={M z$y3X0zC}H7UITA}-D1mvdY5|$B(uDT6xK|8je{ICjQ+#p5= zuw3>wAutxVL{>}X>MWmo;)k5oP&rusDHsvdNN}6|RA7>)zUm&l0eeNoX(b<8T3e*J zc(_bXJp+p%8A>B&?OVW-0^&ZffWzC*-+Qz7od1jE$8(|lKVK?=qyMcf>s`u)LI1mp z_a|8U(_g>kL2`$E|0x)!UW@k0r$$)XDB*_`!u@)&-dhz<=*rEfh}sAr!17PQaGocX zZ?L*^6WMKs7gzV(mmJrk=Q+?h=1+duG@1dbbUE|@o02}p$I4>ju&2=_Z^4u|P{&l7 z7)(ht86OGC<*jE;0|7mV*5{^nB@y%nz1;KfT%-{s;QYMqkGWkN;l^eAdGI74*00+- zYW9z4R19~ZEkggwSFGi5w`)h4hB;LR0ym#uyI`zm)%;{LfTyP=x|a};bh(<)pr)tP zM?UpfQZwBrxRazTJ}I+Vb}RG>qlT*8$wC0=H7XG(~75{(Mxfy`XW5l-U@;)mcwAZ>qMR*HV`Pk@rt+5w!b_jB% ztaizObF5|Wc?CVzim=inURo4aR)%;i__EgK`Hn9l6S#4pL7VSyhKKx+y4EKI<)W?zoNhR%kNO`dx})BNmc{=K+paMTVhYp9vTvBpJklJ!KYDJ423@(&^Ljlfc~!+vj2DT`0p07b#Yc^ z?=%O{J9yYQ6lW1JJt(6%)kB_A2o&+82cF4K88^+a%=Q-ukcAL27lbb^58zKCxZqii z7kAP$3j*{45y9aPm}u{n7V~@Pczqa@OM8M2k=VQ=x%uH;+}(@cE-w7mpoW0Aj^H*@ z;dTjQs?xuS6$WB{Yd-HRdUR^3`K-WDJP?-l@#*j;z5i-mPr{s4I89zYUbfVASpHcU zdL#8ujq-ciQ(Jz&+fs8duD}0&?4C=X8vP_lU$9h=@BotmVCj2kXU5i03KD$kX?P?v zA4+r1F)5O*;r1jkg|6IcFd#1Qu*fUpJaOgYlrf2mp$*0wL%OaYW9x28`|`KaDJ;;Y z=-c@Jq$yhLfZr4$p(uD3O#ktw=*MTyYVr);d z+;H`FIV;c)9Pv;#W3h4R2YU9u!fca&sNqJp=~>2U{1;6TI^tL>70IdO|JO~?|4trH z*Su31`U^R=r-sdtg-fsrY394Mw}@7`m`0a=da!=>c_jv*^?WnO<9wR}lk#Dmh32_g z^^ch}TBVC$lbz*Dl2{{;0q16nLl%Wi>+6e;))O&kr97)vnY;#OFJD5KuPwkUzYZz7 z@_o1jX8)WJH9L@%isqS0aYdyODc1C^APq||oUvU@k)`&sP-o#JZNX${yyy;I3?9@| z1?~?uVGB*Y#;O^*Omo?|%V5o7*yg~8o0IQwx2ucv@H%g!LSte0OutVgayXzRuK40! zlXx)+z31Bu1H$e?N*ql2xW`VjZ#ZeJYrV5}5r%$LaWsmR1g}qg*k(P>Y(&B9SOuju z@((lT=b;y1YZ{6|cOq*MEG%9`pW5q(D<2H;cYAb=^)lX_%R~kJ6as&hrI5HhVrjtwp9v6HSdH0e>r;vTxC=gxNkf^#_YVXxa6vV$K zS0@WD<@C0fH`N{cjvwaocvgc`^{?Lww{rc0cMszv!sRO8OnN8tGHxby;l(chd}1lv zc*Viz5VI(u%*yKlS0(`tqaoZk*Y9-nGB(k)=v4`%E4Mw^Dd5pAuAif~XXeO1iHH5y z$GVjhkd-Z-iHb(jJQej+IrOYA@=_Hpht~G9Q4k-!9%8Pu3HVH`s`1F#vESMDL#_Ak zw{$Se3YfXVmaZeNAIA3H6H6^||*k7gira*KhV z94fgVMQ5yIn7+whlrnN9hg< zKiPJ6&1n7w%_&<4&Lr80b%?{G^aINnU+dmAf2^C$Zq5GK) ze5aqXQ#fgVRawDqam)m8p7}unJ0lPn&;xxb){p`tvCxDP#LWw>{-R5J%;@+r+b<-j z9}j+F#U%!qZhqN%=W;(g%!@FkCkX|QO-~tRF!DL^Yo4%yK5;PBk$b|K+=d7 zxpW@G`XZpjHaJf6nv8K`gU;2Xsg>8qN8}=^Jl?H);}IK;nJrw0CgJsvz0oJx)gqaCpb=*H-S2IU9r@Ao10c>jD2!^G}w{J?HI zWvrO9u6e*VN@J5+c+pnZrI;(1p?32|1RFtg39`860U1Gkm^nNrOH^h0E< zd1f1MVOdJ;zVWcVX*>j*TUbt*GE|30|B%Ac`Q~Lghe==t5*bwS!)*376DH)z8E?Jh z)KA_?oQG{pz`^ih=l1jxrp$8lyKavzaqAdgkwcK63DSK>) z@UqVXY0-bkk*_z!s;CH%d$C>KF8HaB1(U97Xis*$*cTw)9nrrrpb2Jrpa!}Fjm#Z% z_q#9%v?lQ96UM9hCZ7X;8G9Jad&pAwexP0M)DquW!J&8{ENuk`70n3AK&apvS*=A_ zpCPfgEITa!EDQ}!>w1=U20vAmem9?ReI7ey)0u@biD4I4SvDgc=PJ#{0PGNVY#=As zrLq~XmTOJvxQD&Sc{RRmr<3iQ`!*t{2n$fC^a|^J^SJ_x@459Hf!Yeed{+0x#RQ+K zeuo!CHd`}Lhe^~Q1x)S&SEo>CqhC2pLNkR>M@#>~^i1x)qcrB)567Vny^Bon`F~S^ zzac2lUNIDFEx(NQmU!f9?KQbgJM-%ju>7+ybRjA9EbR<_%3+fAI#ue^Y z7>1gTZw(4TR%%RuH&%ichy4F!JcCGLbvtVfI(Gc|&&9eu^4-tb{}tzyx&t~bLEVa; zCH@cNrEebSwTZD!EW^I#cj|^$TxZ8Se$TO=6-wA}$%Zs~xpu5h(7$hoHt=wNmvl0c zC!n!#jpzZt!9mG5^$E5cPYUF^dOh9JOn5}&cZy>7L{h0V+tx<`v#T!SUr70z4RGF9 za}wUIZ&@?mA@h&-lE_aW*C_}igcL#hn%fSzpb!Lw^#n^q2Be8{gr~bDka$pkWdj!S zBW*Y{3u51R?xt(>I$Qd~ukHK;-^LbMc*2zwglQ-pML6)n>BQyW9qkuGjZ3FS zS3w>l`{wH-{wH6D@D-G;m(UGY0LFoBWYyP)7pi7IyhUUfEq)L^W~Cl@Tp`UASCGtU zEB5Ww#-EcFZU2(D;A5QjK&Y(Z>ea{jBwjSQj?1HkV8h0d%a3#I{gznDNV%FA@{UIV z{k(v%R>6fSlcbT?sIfIujR|a9l92Q%pF52SVJo`H-yFI^uuua3j9$?*M~VXUMIN2# zl{)_hsmBA~ly>LVTJ4(YJxQe1nmg&(tM6ynscL(%z5o~Rp%5?N6h4NKAYnrMkeH^b z(4;q*YxzYsj*cL^iP4ZE<1o}eCo76lMa@FmttuBeYzi zFDL7p_t~Gb0j#(gu2;F|pdQ`~Kv7e;bWV)b`4E;?<5CrC^eC5k11b~$%Y0OZA#t`m zA^<{JOX#Y8l8)n$W7g7BeAJ0@8B7G7cLEQBIJMk}OFlu0pmVW5Co2&fLf2cxoxl@W zMJrKz-ig#1Zi&+RByyggzvkboM&<&TIC;^HOh)+&u*?%`20gOh1r zc_Tx63?jw0ZDmvW0J@gvDkVP4t8ew?l#E_InO5x;(WC+TETtzfHfU*4oMgQX%6X;N-QKktgo!TW>}&OnCL}}m}U%iIM<^|gY-u7 z0ljJjHD{O4s+{-rBtq3Zo|io**M1qJLEOCKmaQ3w(EEpN0F~Yk(XhR7{6w!eX1T>O zEO`+3n-K2V)tlPp$5ZZ(V5Joly{r!pefpUaK)&4&ro)FZW;-cc&vVOfjj&T%TTEIN zQh76tnkVIh`k(G(M|}csgd=UHKxGA^Kf;j`vRS1D6YBRbFlYk7%UDsQ*(G5z;$J=> z*RKFqu(hVmEcbfn(jtkHmx7Fr+{|9wR~g|RvdbhMM8SLyDdPE)Hh?Y%!U5e-B%XuH zsLu}1)Ffj>TzD);ck zaz}zn?K3U~Q9HIT7oiO-;h&Th_ciUVUD|>4$w@>i$ZPw&U=fe&+HRlLb1xCiblP!B zB-jd$F@ZlZi-m~*yp-W~@v7@YC0A1jo~^ayoR7e|J4YS66^SC3G|z|e4pMYJ4E4r! zS*VM*K;VDt;?35WMg7>B?{4mNg`w~Cn?iJ2o15?luzyj#coU$q(xGESQ>I+qcoS!- zVaTd3rE~4|8&>^6CQ`lKFX$}!Vu16<<$E6}6G6?bPc9}bgI=?S_!(WN6JU6S3Q;3Z z$9MzvnT`DS!iLSnT%GRkh|kD#+RBrgeytMEX7dYS=|*Nno9Q1#uhLXBd?lIi6u8JJ z7BoE;T+EYyc&_{XCjQI~%miwH8OuMGBEv_s|Ay5haqqS3%r3g&=Zje zy($5``R|1dRu)37cdHigG+I258UNacXguxOFN8&#^|>s;0My>Z#%IHG_B<#$=SkBm z%;6;%$ktIl^VRFCxKpt{nFGMs7?$A>?u6iJG^txG3FuKTBbo(meR#nyGDHcM?FJwY zMt{kAvJ6>(H9|R6LCP9s)l2ZIhK3;X3iUrF5=OW2no`Pcss#q#nCz6VdLnX#DY52}V z>^=VZ+5z?u_lGAz#9I&3*uI*H(H90C$;#(`PDB_1DUVNMN z{96NmskNniEn5t|zvf<F)>BSR1tuKuNqHYuf-A(xwqEi#dq^O0U5DB4dOZV26l`qEHPM6qd~q7 zV>FZtbD(yL7~a~A?G%HHDff^=aCPC#C+iA8+V`GxZD5bxqvh>DQ^fE(7Y<)q!Q|}& z8r`}|31~A{f4}K=()k;uxNRaz{t z(Ks)4bo5Gm|Ff~$xmMhG@o1s;df z3EGcnjTb^cmJsBI+VQL^!|hwPh9(jPQXjKX6?eF{XU<* zmUcECZq#UxgeRpumh8w_do{pv0Rl8IC%;8?vCGY8TZi6L9yrQFygums6J)PJ*<|i z!_$`ujKS|UFPdPz-)&}{{lzSB;S0KlJETzLL{j%L8zYyf6+iorT+Twe-Z z#*NPaoc)gfzgwU560wAucE9RAlCf;3nBn2-*r-URGi*&xO$fdI)xVGtFlW0Uplb#R zhg9PEczw3tK)R+h&oBMR>67TxfjpOx22`;k&~pzb!iIovkR*(77b>`)tKf9Rd)Vc* zcmEu~)2{)?!k4*&_LWo#8?rR--z3=^LLN({BZIG2WFsmTtw@FYAZUj}T~Vw0l6``f z@r$5#YO&yFdmfqY^k;|$whJ8+Z4@y?Z)=Lz^amid=%fRbfi0do8=E$9iC?J-D)Z zd>d>DY7bou>XA6u1#^*pFGs|vuW(nnBM=0f84jTH>zej0FWJ(IW`_skdhwiM~B@*sQu+PK74<0YGVL_LRU}>;00&ntNZ!uU32=q$knwPbT zq4jaD2+h}qnWZ@Y`{z!lIE|UyD;=c=hzrjRD3)cTcw|2{D{QMubz4Ju^KP36gP1tj zJ*nw=6TtIUr=H51fJk~>!oxpSz^#s5KXkd((f_qOYZ@{>_nTd$l3-Ma?`lJ>_f2rO zTWNM=c}MPAtS*&CI-=s^?Y#`Wkw_-6UhUKl|7mHyVG?j?FeI=E3<3uPgOEr^*RBi* zU7<`K_li%t;a8M18td>myMeDMLaEYyi~5}M2OLx0w#wP@kXa8Oo3W^-i+F+i98x@}vtQO|dJ8|! zt-qx+0;hhB;<8J_#2O58_0P68EQ=rf@|i5_mFcUy)vHymJ&--8wN7zSG&o9uC7ts2 zhh@Nn5$_23xtl+c8@BISkF_!;iMKRCM0p8u@zJxXCmRG!rO~G9z85pNpyz-T+V#P( z!bbC!FG#Q`W({e?^*J(O8m^8ek&}U+96KkCOth3rACB zcQ$nBBV~HK<4x3}KR)xJ`bean0opCgO20Qa&P8#cb^Rs_D*s?=pCD z{zC%;s0;wU^M%3$tO$8&+bWHyh5jhxfb06444Jw6yOwdR1>9BAW}M7tljN^e_111380N+h-X3U+vce&AXh44vZU4`= z;6e27@iCPB+7QH?NF00z8}Pq$aF+DGYHJ@wG)s7dJlr(t3-76aL6i7R+V=8SdN}8^ zNiWE+40Ma}9<&G%V4L)ya5-RxpxRQd#4NElZ7N0DJIZ1Cb=!GJs5$#egGdQTQ%PPO zx-cb5ljutYuWU%Zzis!-9bwil?a7eB1BKgMBOlFrCG_$}`%81Hwb>NBo7yCg9twng zT&798KdtKieuy8BPR7@`e4@~tT~jDjx@-GVG-mjXh`y1e5>BbFG)Sq#Wri>23f>u+ zcrv{Vk{;_ywcj;Zhg6hGD+n+1r)aOJ*ywTdOMy&^f)E1new9aPd*yr8_h` zS|$*BU~LWM@@?B;qQ|%WysTuznzR<_s!~Q}kedueud2HcH368`dvl+qa``DAtfr+E z$10i#mog&qLMBy2OTHDm2}4(AQCnm^&jwhGX$b069M4`J{C0t%sogjMW?# zX)Yv)z2=DEq!6WRrG|nNA9Q{C!4IR8o=kY2E}6Plx#@Jot3G`HT`=M84c_N)#zA=u zX8K2tUBf5a+H*;|Z#GFaa$pVZ46L-JnkYmMa1&zjN?RI_DMf;Hnu%m}t47L6u;z+| z)w5`1am?=>07QDzU$UQowwrEbN2_FW1#)Nxj?3GN9Z}>j^VfLtjP4?^1zQ@1p}HWn ziiaS)JWSXd*EhM-A%sPFo9gji$(#G+IAPq-4d(U{4&PH7l73=hPn7S7;&oEFkC6B! zvF5rf3ZIej9~33alYyk_r~F^vl7&GQ`ymg@hTJ{wpBJ8jF5)QAa5;W`C3K1kgV+%1 zNn8JN)&-OJJLq#L7VvI~tJ4k>WL%$m`GHXwH45}k2+lCq=gJGGa5wIX-3pRvRr7QJQPthFwFSLeLtR$!J(m zQ=eRZAhB+8Z$OwV17LqlrNNGJTizj1Ep$XQ9Mir4_lnuXdwDg2+3tIsrn|df!eD0T z5D;oFpb#8hg9Ptf{RhI0#}#(cKZTjXRY*>fy3J9h7g8K!_kz9S{fy`Um21%graXR7mR&O7+#MD|n;PxgaWq z?|@I8`gR^~mfbjJe%v|RO0N6lc*te#=V2qe_H2k43RX`qm~pg%UW@~O6D>sTqc7j#pkQ~$lnh` z0eH{D^5NwJmJbUf<;&M&XWe)SOFIjj#F5*brJcbkA_2qzgt5Hew^&tdh!R9fJ>L96t9;HKJ^>h&a=9%}MLI3tU zKSTc;m8?)n0PSBR@n3oJb=ga2)ON2>PyTA=jfg0ErbIQp&_MhH#O>_OCw@5VHumi0b4Fv?Kyf5q0}mEaJ)9b({R0 z`AbtB!p$wagP9uL?60(JrMjg~E>Z-SeHRy;AD3WML!I#|06S4X7(-LSLXP#5bdf%^ zi$P+Uh~$FR+04cSA`0a+i_uO;+=Uk2na+!Nj<9?fnCya;e&~J(3zRiUP#_{>efHdCc3;&gMPIUN!#UR}J;k4*X$( zQ8#H?2|YJlo`$0bcmUXGqIYk8 z9GQ7?(Zyx=TEJaj2FDY>-dp0|+Sz|ZKLcS`@t>jmjbhDJSL9p#js6FKus>p8Ru%Xk z^iu@J80a$2)qvL;oD)P(+p?Yyi8;weeKz+mwNr0YOvZRD&{@%X4R9+EnPT-4lhXIF z$s@ZzV5IvpOp=tsF4@ioSypFA6awi9g6^n%s|etO=7fFiyZvl*5l%78I$j2*=_|Aw zTAu@JELu~81x|hZKamUnNjgOV!(ug%x`4)3o`D(*u(KOccsq%+B`E`-t%%(}ljJ}3&HbK9 z05-EX!Q>-t z2i7?_x0_(o!}A2B2P)`ADG}cFO)Xt}aXd4PvFZ~&Xj){lk&@ZD!gEB7r3?txjiw(5 z@o8MxW#4@oxXdf$Feg1CIyC?4qIWXmN_!NR_+OI$JX+)xGEyb z-*&Q;;tC9{i45eR2h5f)5;)I1_e0F$G|H69yecqEg&uG0yXd#qt1z$WmI7GjkoiUM z6{B_!0d|_csd1bl_MKXxBJTm=doDl$@i@dO1RT`&*Vj&VMz9XJ^JsbdAQ-0jG|>Z! zlJ8p!P@#JjL=>e4CmUQ>&x;{QWG%^2B#*Xzy^yUoB7@~q_PIF0RqO*Q(yKl%z~@Vh jl;n&2&r@B}BcnlV?Q-t3ju_G@vXf2-SNO_2*FF2G`1^=hK!sz%y%$YRx4>~Xw}fZb5R^?1 zmBAVI**>E?W~owvnE>U0%Hnjlv42U#bKB~HQL+&N8FFx}=f#a2$s_U66ghBX5AlX> zDi+Q*0qTn}0uu{{zK;pOZ4Kl7vV>b;X@a8T=uSgoqngD4d9Cf;o<*Y*{f3xf0KxLr zgq-DTc}2wRz(WnUpft1wyMpP+Y8yp5&%hDC6`&tWSO@>v9uMVcW(}w|MB}k?#~j+j z?)6P^>dfZ*2v6!j2%c6}&m684WB7I1Xk}GoK3^W(Gy4)|&4+=Z<2w2+o7^=2;%oXB zhB|%`o0gWmppongH+V|FtFT8fU|e)0*#H4N`bLIn9n zLODVh5(^2KGo3M2LMfHks!tXJ5OlNa@4UyC{A{4pq z&P?!J8%x_f>S5_^$6k+6<$f#(OiBZPsNMZgnnL8*5*Khfnl`clfYCwSb=%wtjenx(Q zdYN>VHb&Df8be>>I*j@sQ22fy0t0}Lv|Suf2eC|V<|;tgx~~tSC&I-z(${!5KmA4q zky;MG^FpbkkZiDF*6qyf4L)43n|>n~&mOFdsb1BO;GhhKJVuVdCU--J+8PP`TU#yj zQlERinH3Rjw2y4o2d~>oJuUi&fiMJ4s?-t#aP>J)id9f<_{QzJoZXJ_8PdC7<|-+; z!!YQKEE+7C41p`xr@rbQKZ{_y(EkGUplgUe`sqhS?5CoUthfBJUQDQ%)%vM#Z4?BWQF0CGbOYl&oiR(D<&`Xif0U4(%ei(c9Om@5Vxm$Ad# z>H(tNwoAsLHQ4gfbv4IEFVF zRo*_Oa!$8t@F(L3?a!h?ORaB3lk&gUshR4~XG9A?^M`@8Bm2RX@(qG>Zr%J`Dkbir5c#)fQ|HXS- z?HEAs+ud#{t_Cfu4uk9$uf3ba_@=%Q=2Y)IeSE1GdxLxt^7zu9kwNP(^}nqX%xtAc zHBye$&q5{diTL-u(+;S71C!ZjA)->^$0kO=pNh~I0`a-(!yYO~>J5;9#C356M=yK` zy|)L_Z>wwsq@BxIR$bg^H1+xi=q?W}<>ZCYu9&F}?m9{}1{J^8H(5jS)L5e=7SVy(PXxYpUQR8O+p-wOY(iYO`Ztq9NgLLeBt}q(sq@apl)ubpk z-JWG*Z|z491Z4ehVZUOp_tmVp8T**!)k#}_*0W3ly>&f_@+a*g!g%{2l`!;-!wAfb z%-g|XXgSp2T)8N2+-c>u2f+Yn>Ahf@|ly^z4mbQwy(yIHlL06HKe5GQIDa zu#%pt%;ON}9|Wr(*HVa~2wi`@9Mv#GjV%?R_uLEut3Qhg)e%z9a%W`VjQpQPa$qzW z-`W!e=&764rSp8&qPALIuYsUe0hyMdp-D4H$^lpk_k5DeTKBU-aabmC{>PKup3HQL zefPL-M=yuEtd{)g&!hkM{(P|fiPS&;#vn5x`G;MRSIpcAjo{xop!h?4@bC0c9dM5R zTr~wSpd3$==TkWO2^=o@F)L-nu_XONBKlZPYanG>?~NF9K&rg0MAjCSD>!IAOQMU^ z4#{nX2bAzNF{NcrtFY){@;>NJC`dU16Y*pXI@r6M0`_OX(c!8Z(oDbhf!(;Ys!M-U z{!VZmh+c4&_!>NeP&Sd6rp;D%xZo*axXfGbNiZTj2*8rb#w_(3?dm6JU)R3elhj_q z?la_u)t|*?i7$7~a%W^DSUI)KygWy3ESdEEt}2yd>t!sf&X=(IR@TSYD(Q~zVB#9x zU|N2MF5>0b4y%V(KN$dJJj#m*_?iN(4|DOYI^J6Q_@Ui4DX_j82-t=gNKKmqT5@~k z-y)ju)4x7aG`jxH0&nYTy)LJI92NSEHOg2t!afQ|C{axWMkj6|!XM4WP+OO(J9#T_ zNsJU?O^Cv??i^I7;5y{a-e51d`2k~NFbswRPq_o0eg+QtA1FjKn1^v0b;?tsbMI3S zKIp3N;gUl@h-(1hVPT>nVIZI*f>5z>P>^9B|5p(9_u&P=gGc`4sJ;pQwzoM~e}whh z_^Hm*pR`Z?v<2Qb)j9gh7d{5V3-^<|OkhX-6T|zyOvqaTkN?U2|E?#ybn?>{c;A;V z?`y&1VK`KQ6l+$(upsu_qIc=#Mf<{E);o~{1E&hw@O5KCqz<=;r&jRLu{1_Os%(oN z)AfQBYby4;8Yn!1?0NCl_p0v^;OvLo76>Z!m4Zko3zQ!4&zcO_Je{eDscv9N;Q+9bi zn=(g}j2vS+bTsm%xLYgvHx|So@JS7w+{$Aw9`HJlEqH&s-9ynxi4ogkS0FudL`SvT z?t`2{GO*A(+jn{N^Xmaxp3WXXurd?zOC$0Q?dZp(JJmNpvGF9TS~g2`;1;&8cbAu1 z(lip8tJa^n^_Vf-?r12yGX3NQ74>jN*5ZJG%}8e&F7tzUM&OS3)Xu^tZl_iM1+r2K z88y2@+KYDY-{_BO_CD~@r+5LVunSviRk0el%(0hdT_vyBcwjg1p?%$6eAnc7X}Vqv z;uw1QO9aN#I{-pxhZa-uQlB%o>OIwW_a zKCf!PDO~J{KwI5A=P>N`dmTcu}Hn*p=G~f;lnLOhomNH267LxUC)d1 znKL519~QO`!Vc9;i@=EMGe8JK_9p^sMD=yCH0$5m5duFbzMKy<3` zu6rw$#!4jwD#UY|4s3k&?3ja0!pl(@P8OHNFBNJ;7G~!!2`3Lh~*heCPxO~-Ag-bb}w8>P8G?b z&nqg$8Si}b9m5w}9VIMz)2T4V0DMQP%8xCgHBvl<r zr}hy?@gmS4m~unMW34^lXi9N9Z~b=l5Q;BZ{Hptut#`PvqcQH`#xNEmAk-mRo9b$_ zoK5K&IrxnHuf?CyoD-n+96gS?y}O5xxvR3ht)1s@2XSzJ2owyL{(}Vh5jdfT|5}fL zV3ydAaYpa-B**=lg;hWx$%CtuUO_b}uzH}@KBmpVg{_~s_tf~;N=j0Br`!V!F^B`w4^yEliih1!ZuQ_GeI)EO zj<8pApp?mslPGC!m%y9Z-xp5|4E(n+m_#0jv?Isl@jK!gR6hF5(M{1s&?-voyXw?d$9=gBL%dlvkY(HmbVin3%PBgy;@&t{f9$EuK`y=YD2i z$^4o|CVoh9@b|Pngf3!Vzz3@m@n3#h)nJxgC=!y}Ogir2=IaVwsKqdO^bFQ?qvi~G zOg|JYaei=J+=*KOLt>Ee$vGcJK&cj!qLsL9Q2ofFzmh#Y*>WMQRwPrK&U-wYW^J@o zB-?~vKp}bpaJo5)_`_bk z9+B%@8FuyceYYatm;1B@hfj@P4rK(aM@WrQ?kjE()whR~9?rTW7ad@>&1z2VWslZr zR%Z4Cy5?rZ{e-QUO48Szy5d_O+(;0BPO)$-)8_q$rR!89pRnq1k9Gu(8-n9| zhp}pVh7R)q`+Ql&P3^h~)(f7gMAX)vaUZQjn?IXfrW%%d3+`5kfIMc3Kzp){3C#i> z1n}`Ti8ffqRc#=PE(3>)6#W<_paRZXIl0Mmp3Se`fSPnboa{XaFF~z|7;F~7V5EMx zoy#xuz89CN@V8A~W7>_I%8usqG%s8mWs>jbW-bdpIzCKElDn$|P}g6_useW|mcALi zzboW7`0mP00Q{kyhpop$giq6(8}j&K1X`ry1?X5tVFPT9#E3jmiTHHC43dH{Sk;7s zlC2HL)=T~=JS}<*I$MULuWkkge@uF8&8Z<7yh;rFgYbrIjDQ8&U@vZI zzCg-^$1Pong(ePi&XUd2i0pRGYbz%t6iI-_Dmk~QPNY#{r|m7F*Yk(MsbWnsMpqL9 zQR&ANjwECEL2=MZ^M|*QIPUz)+rL~Q`)T3#&ADHE#K1WGS;2dSZ-1fxNdrH@_wSPT zh-hppW@53r*SCT`4&T*$w}1w#Ka2l`w@Axkf zy%8FE6e2OPfh|$R2qqLEi;AQN5I4Ks6SZ7?DHlQ$Lvl7OacnbKPI`O_^gl;VP?&(T zAOoR_kO(wa#CGT$EM*2PS{A>F;FleGlu&)wER=eDYXveLTx(S@ru7Qenwjis+&j*J z6f4c4?ieO!W)&Io)Woc`zi&eA7G4qt< z0QLQsZ1@_3Tm3Fq`mZ$He@mvl;MIp*y!f6~%3pOc!U4EGcX97nV3VU)$NYNQjVW== z>WmB@LetO-@wRkI_M}_TvPJYGF-Y{)|D&=+&k~{~AMIuM)50^~qz_AY9&Py^=APlt ziU2Ip+oOQ4{Z;PJUc{O0n6)m;^y{tPhKH7|N3h}H;8SIbq9JT$P@BV~CFJUQeAc^n z_0J?5VZ)qPwr-Oh1i0O;O_}7%D`_3-LVrB8QqBPEAEed@l>%54t(i<3Yllj%@($2{nR#rTf#Wuq=p7yc7~8~fej+k!3t6(A`P|O{s>Cbe z=3)u;T6qxx)~>$cw-4f$|LN_T&v3mRje0RswvYkUr?ztoWsDTosLL7RMaBj@EtJF- z`wUHG?mLJg_)q=80#Y$?T;FU8X%qE2YOT zojiRAt3QkXMT2pwi2hIZYZ!o<1KB06Bwj?58dAErS#&FKsg0Mz#gD9Wxl_TUcXv_& z>6$J5Ng_sDzh2@iCl8b?c@PLc#_3RLb#yydrR8l`?Sq^`L6txJ4f0L~zu$;9X>n}T zZ;i1#z=oA>lw{xcP@eU{Pnl{6RbM z(E#ZUQ&suc&m3#rw%PaE5%XTOx#nblpc?A`T6Q_#GsX@Q1UJ z>@I-6O_hz~Ei?gGiI?m51d%rBVn1s~UEIqR9d9PgM_Fr+IVVLR2HnhKOgRH{b4dzVY@8qHC`0iFfF2dZr=iZ9rON;F~h? z=go@?^tA~U#r_x9(gIqpx=&oIieeZoK$C@@)Ijz8>EG!Dut$a4jb|-S4hXhf1J?WV zs7>$VTR>+y1qDa7u+|8_T#fB438=L^2=9~642(VR+_)D_;)irBuXUYQ{y)M0tLQ{ z^>n_gx=&1oY5}IDaU^R7V2Y=ekM?~);>gTH`s}2@F6BT7*F$mCWGD5RqCCEru|VJs zp41W6_^f$5C1(bYVrIPVXKntlwRE!D$t?Wfx&&yahX2E8#JB3d?9>+L1Wvi_w#I!! z5eWiYGvGjkEKOtl5#aMbUCc!;fC##HHeWP86~S2n;=O*B`XM6&D0o-BTs5VCq5qTK z6Z{OTr;SVMVyRFtLCC!-zE{J3VW@xD6IOo~e=j49fK<$JY>>;UzsHkgwC2}Kcm)sR zXgINi@a2mPOw^|dCIQ|@l2j=DO+rIW%(F*Nqqy{?(7+Ci3MrdL4StiFIx&#PdOw|= z778v&@C5nCcej}y4!YYQ`Qb2*P4__?f!T%%=)%fxKc_lq*e8+L-q&8N(!yf^=x-kf zSbv)0SfY0SR+8mrjch|Cr!bXgtfX>CI-!k04xs5?`a(jJk;du5c&~^cM3(C9iar5% zJ@<8mlWwoH%m`?c1-kyz+DZ@@BH-@&6gyIdO}NCP;Zm0&ino`-zq1wx)M@=L}h@cv(TLpb_?IO#hnU!gg^oj$x3$F zEB%T{9uES$l3$5In=Hhi#?C6kEyvM>{P_%#1ce#@Y_c>+=G}*eW~voYZ=e`TRSov8 zj}*c)6w38_Ti73=BoqGX3`mb>7*A#YSAEf6ghwEx=*%R!FER+LXQ!^F65-)7BQp9T z#>3xf*04+^3#&hi|16FYR|uMwKG;(GQbkN}KXgNvI>;zp4vED1;O@h=fGs`^u*-*H zX!)zoqd$?nu5UYebu>yfN1F*9jz4$|+Wp-51H!ZG6*RbofqZZTumVQQH`Xe+&DC_HVlXZwMC! z!)5;_&HjdP5&82#+U6^~i*6_R*l)_COZ2nGZxXT=>mmw-K+g|6uT-0U|TMETDL7M2-UC$Rg zVu8AL4}eRQ#LH>%o_9Je;@COs);^Az=^&zV?H$~&u0qLIF~Ekbgv|Xcq={i*tyR4B zud+c8F6!Mrx<>1SzDAaY3f%=Llw14)n}n(;#yp!xA_2LVmYWRH1yHx^#t&ijXR&a> zUDLB1tPIuvVhZx-Q&7DlBcM@cszi9;Zv3DlVP!%Ao6YelMP0pU{{+V&U{OhZ2@K#_ ze<{MwFc7-qyF$uC+c%l0u1fYzv;|qr2cuWCJ%smK@$g-6&Mrlj|^zl5mj*KL06JLYmR?tVW3UQ*k)b$o}w`PG?7ubhdn$sz>EW5J)!zU>8`eDD}k z?U~q(rQy;`IUgu_Ow;sD{#|PgYw9VCN39^(d{+XK5g=uojpX;CD)*ZbQPsaQ>@3RB zwvmKN*KoI4e{_6ypB3_u0&$$z=q)7M;VjA?gHN zwN@MSS4_Z%l{HtsR}~DTpSc&=1h@M-r>b}q1_{?^NYDOqPaM_haGp$Hljs-e`AGvm z!T0YvM%y{%YA;@evTduc@gKuPo-)qB>d#`cr0Yy)xij*qp-Yh1`zaEfW_i-4jN`@R zOrzCjWh=1H!>cNczzU0=sNSVeU(w~_hacxMuT~u!jNlK?S1wmuez>ixGLm9a6XlTO!>~O20Gh zs;TK6^nrQ-pqovaYfO(NV8eNGZ~k($Ue=p#x2J0Ox2OC#5Y=^s2O%Y)$XI`7M`-sd z_zg38<*PMtqyrLgI5@h5(9^G$1Z}y_?ia&5E`82+@d+c)t6g++UqfgYk0s|Qf8YI* z=vQ{_ImVMO#h%%i1?J|%fWbR)U#yF}b^Gn*g8Bkqh~r{pJVvdr2j&QjQ@e|fe}>!) z$^Kb%zLq%UO`T9rO706J=f~KYaR0BlMb3yb-qIyT`@f9;qK7Z-Zl+;y*wSqy_(Mznq-l z)~INlnd)bctj;-*7QzyJi7)rx?yNhrr}22UJN-{nP(NwiYPN=LIT3?DRAU{waDV6-ASy{TIP-V+GwE`8d?5o9}y=MMEAq3sL6z2qrh($Y}dIi$Zl#} zaL2wxmj$QW!@*MFGTdXQ4gkQrCIB1Y490=T zoj2(|6XHLL&hAwx{AE+)5S|ag7eIh-?Qsto|lEDe)^C zS#s&gcG-WW{fiI&3qiq#NXOfET;J|xQ6G%d8QGF#rsMu{tnoeEzo=xV7P!uE$XsuMv!EKuED$i18HJ2S%@k))2j1?f?zl3`oIiWRmzZ6^<;a18b|6(vK9^X8n zSCu>M`14?5-u~boBk+O3lMU?y1^v4&^jn`Q-pNibcT*mS(@q=ea_zmO@ZJG#LVD#d z(E?W|!OR83Ib$}^HRoM92@WlS;=KDS3;4PFAa$IdJ&6MVzmUt}%zp?yrz1fC1wLG9 zL?ylOSL)z{U|3p{YM0Anz`RHAPN$KBtNQ!HXfaDz{aFkx<7H>LGxC4xNW_oa8NAXT z6L!;)1&}>vIb5{c^?_cV9)25eZB13+MnKz448Unumy>6SyRQOf9rC8-y7z-m2w6O`!$P7o{H2y7+l3x6CvgO|o&=$W(aR!&iT zX5}#Iqe+g6E33%bFR@nSZ@=V^caiNwBlqY9)DqI}cH&J0b&w_J);)81QDL8y`p7_v zf3Q_+;j7|09>fYS0y~q^F!=j@@L$DZ-p9ZGdW+6!<$;{)j`imco)_$|j|PP7h=*6L z91sXS2T0i8_0fG4Ii#$*!oD}QrMv3pUP=BgiY?*gT`c}Ixr_U#WucP%KS~Hr0|1_0 ziKLN59T*+xMejhAwTkGnx_%H`hbm24$xw7AFV2eMXNl7?O6zI)tb}5AmN_Gzqeq^B zeV%O%_hg}k|Iu`moP*?su+m2jf@oO%KQXe|Keh!g+o6 zspkD3p`mZeVEYI8CsC2(ebc3)3Hz5<8-ETZe-R$>md!o#U8Y=~U+61Msk5mX+j>uJ zS-?Dted~&*q0P82D+~t*f@a{5|B5=tety2*vKQ09?|%yFzw_2MP!`>Lq8|Qn~aeWZZBT0`v~Qt+~5jlhRXcG^bu_ZFO|1yP^# z1YoZ1r;GBTo;UybC!SKVY3U>VEpOGWI}0qgEIC!Wj zNP=8L?FSGfmOO!JiD$v`v^bIimxb;EzBxV03l@%zG`5CMkRB4Z8O8c*P;f-BMht$g zJgki9Dja>p!E$Qf^1mA*u`{nivOSVc>7k|U0aH2@-Gh3)&xy5U ztaqR2q>pQ+$=c7Piz0)3+}{w=`acPNDca)p<<5`x0a|R$(^yd3dqGbYd`U7cT-kxV6#@Of~Eg30Hmb1QhuQfCSgv3W7 zoU}z~6LT$>ZsZ~kZXL!fStA!^i#l7dGS*_4LR?k;?5skaXupRjde2s*0mawjF~P2AGQbC%?Y`opTT^44`Mftv(;nwoqkldLZvP?}8UBo=R=8 zr5C=bPe$H;@&qzd09^c(RXwIV*X{gnP_CF|jxz{7Z#S3cPKWKQMMiglZr499vi*6Y z6w!$W?r1MsAavs8gT&tt_b|zl77B6f$UT#PXEan7469cd$xEZT*sGfFZ-`1@xgyRb zta}Sqe-{655BF|~i9Jz?5ykJK@_a*bD~+wdQ&E@^*h>c!(?C9sK{DnS$fO+6C=hcm zvxyWycB)&GcY4A%0hbC2FSOMqV^18G>kV0mB;%v}Cx~5TluW@v>7G?|75kvlkklU| zB;>mVaJ>qhu}Dw96Fk+!!8C647Ml z5P|xqPyQmHAhBk;MHhwH`;9IlCffpUMueQOKCJ#M{ueXMvq8?v zYH`5Z2B|m?ymw)0KXmu91|?*~=tpLuNRxmE?ql7Ode3T#C|p+~IB$r%Tkeb;gJhtq zt8%mitpiqa$~&jFmTxoJO>>vmV)^rGwK`1nD%wwV7sICVvVTe6vIXz?@v8eq>Cg-V>Kw#P#6|sj;Whi>3#pCii_IaiOSwKXL#|BOJV|)-isEU^2omZtoTO=NAU5 zXzzQ2^CuB*dcpJ69~@F?y)xTs7X#p${uoEvC26zm_0zYde$ne3?~igZkliV9Wfs3+ z&IPgn_^qS2O<7tk!&R@x*OyW~tEcR+eW!=wNri9og&0e7*uu!w!?bBRD zOt(TzmADwzb5>JRr@1D1NXtG56Yq!H{=*g?{ihrs8IO02tJ)+|>8ZRp*X|LVs^tF< zxA6WtG0p(AZlhj|&DA|Nqm0~TX;OdIKPrLRIrU8zUac5(dCzGTlV;>`$ zwxXwv)*o7j!}C}2!hTtneyad=5&4^bVXaBrgMBP-nkGvg4LznoUOk9JUF}`c0M%t! z{b`KbNIZ!_%?O;hbRJg(UWAB0%=q^C=9QtCy4PKC&+-f`*%$NNgRYbT9Lf)-Tp3F@ z%_x_}CEsWyhY3CWjMycv-yt~`#Y{0dUhM?wl-r-s+kc&G}R40jx zHED1@;KpHhI{v?;TaSYZ&_Lg&4;g{96D~=82GKxP1x!TTo4Xuwb3%_EpwSIlFjU%D zQhxOZZoB&ETx4?0`mEF`URK7{WLaPg{9TVxAeZncq%v(+0^;_+KA*a%JoimMI7H|(EGLLT$}$Uh|KWUxyV zzl1lVQJKr3YZVlky4zu&)Xm8N?1RMT-Y$uEJ~|r#h%u#0mlivt=BmCQ zN0a>7+?XGL;VlQlX&Z^~2T3VrSPG*hkrsTmaUU#3ct3O5#?5%@u^0MqjAbnp8Lqrr)p5CKe#xYb zgs2&mG?(34eH5ss%M=GhZQr;Z+6R@Cha@@1*3!_waEu`Y zgh?kxBD&wwN=yr0j7WHFM#}f}6K!gV&@B3~!Ddpn4Apkug@DmATj`sOq3RC9~&M}ed3?%UPo`2h`AfD zJ(h3wrp@m5)0@wB0UyfwZLSf=r~Nvi*VM7!bf;Y(R%^Ko_=vJ(j0{@6oPvA_1xfuh zYHAn^FC(FcrCMfgpE{`ruUNq3?mr|V$4oSa0Eu6;KQgHe*wlGfp5a`YxM2J z_(7iN&-3ar9Bo%J=OPY*08hf}bX-Cb60RRG)UV9dW(FTMQS9o-E~Vsm)dq+n;zQ?b zkUtX}8r=ZiO~>f$+S;t}^JT_)85iKs-dG<8EiPy0Y_DD@P9Z4Cb+=rp+86jL&AwZ{ z^@}|nq#@-xDn!cXvSN(DtaDv|8+oB>9kK<%m)d54Kdy$(l!^-fV4Qr2|0?%2fZ5x5 zzK=Vh@3X3*MIpZE9p#s%$5B0m7Mz`k;m*k_x{zY%oy_l-07H9R!4`ZE$J9&?%}*$G zjv+L915Xd=#^99f|H%-A0l*jt4WqHGUwrErO#T6H3oAZ3KQ#)UOI=Fi^=O?)VK0C` z*ToK2BzDh&IO^Qj+?(eqzBV*Fr#>b@-|SAd>P`sh)A><^ko4KW`a7ztVpNZ=mbH}6 z6D#0ls?CvKfLVHp^@`}oQ$Fy7?N)Go<9SUoI8I!bl^ zLY9!n0EFvZ+imC_@GqFXHYd&#Rzw zr$N(W%pR@6{wQnOo!BdcHQ+0x81%#K-9ccjHBfUkuBRr;=OQ<<*Khl2#03BM+-@c~ z$bXa4GV<19f`WeVErxI19`|$OEOR6Koqz$}SLFb1r`VgUkRppB)z`R^Z%gVvuM2Z? zYSM9DnEH0X=nW0@mb8P~55I$UTjBP_-*?P@b4i-S8pJdp+5_v#Q<(AE(FIm%t(GsR z*nbKDEQLnl-J_jv=XU*LWP4(pN%7~+>yEJcv-rPG_q&;eZC0qBe~aV}LjU+Y!~u<( z=F|#zs*BtXW)jG9ye0M4PPmh2rTplL04jUBq9n1nzW9LhPJ==Wa5@Nc*jfC9%KT^zKM7U+g1+ z8J;bby%$obX_$h;_e%P2oeAd=QJc+bvpV?0iT zTu!yymal3_5nG6P3py-dKZ%DhS4C`EiP zYjx6Q9Ypf#0w z?m-keK?1<<`}8@YR%8b+Xb`<<{U)HqRkxC{ zQr6_r@YLW^)zs9o_0y2^RZ`~W1X96 z=&i}^s-@|mXrQH|s;=YY$fLpI=%%kL>#r&=>!ZUXsi1jPR#}P1PSH~4nzSa5ox7^D zogLIJKJBZnUKZNdvg;RJ3JW+_fBJcyze!WOSVk zR4x2GRm^!@RaKl6eALuMPc7!b`h)_wzb4ZrL(KK1iSFIE#eL4v{m_U%tM z6NF7@>jNOii&v-nnEWuyL?kw0Ejxn|3$Q$y$N^fk_mOEu9NCNGL7^fq25p*#JEiuM zl$nqTCPGeB9|Xumez-Vf4B$Py3`$%J9&?X8h&9+i-kQDe#FAX33%Ld2_~S6u_u~ri zqKigvl9y8jZ-0Lw&y7f@rFUQU@Wsc9uGVU|SQ;J0QDM>JJ#!*36fU7OYX%g)=2`h> zB}vC;r@!g#G2M)+G{<{XM`2xhchkb#1?d^Y3H6-Bfj(7dcm%usk+`Fkmy_4`Bue7r zP$|Z*^|w0pF8d#iX&tPYV=^J&PSXK=3i_8oaTZTSSjq8CDMcCVC^ipXClMVX+M`du z=g_u=IR3Z|_3Ok1E)V8N1w4c%ZsaLpdVZBhRj2=5GLSjF>{Ed#tQ#EJl7i17`CQ)B zF9Fvb9&CArx4x36M5nuB7lYO8(ZhCQk4s$^O^f5*egO344eCEne}X(8gP+uytxEUg zzYLF8TbH>k$EwDwQyJw*lDf;JjMfa)U z*@GM3J28AsBc4`aL#Wub3yA!U)g}LuTW= z3IjingSRCU<Il?d zhB$Jb6C09N%6veXpDQ%BsN$zuJa?krq7u?|vK1D68*1X`+*@yBca5L*^H%A<1#n_! z=3H05-}c(%~mtn5?0IDfBQ$xt2O$Bd8L6y zv>>4Jmd^Y|FD!qQ)vCalXK%j6IOzMg z53rD{0It2gbE_y^e=0^cF5ww>5`n{&05zQh`pDI7tvnym2Mpg|V)y&lkeQs7t1Y2v zV+863M$BV}v-GGPoa>GI6IMQQ#QRHv3AU7wZa*Ti>D~s21&hlqK}L#Qg+gy9s}};C z*gLdh1~wS(Hu&`oP#8%-Vt%yHVTd0rzy@AiG%uO^Up#to*@LY}(BrMg_&{)e^>fo- zoE3&{ifgTdXj;E72M*k0e1p0NxMzJzHD1baNx2i0j>yK*59a-Hhm<>n-5}EwGHh%g<|&E!=2&1&!c}u17x*WSg7{7q_T_DQUhdx?1+EJ=$GZ=IN00_cb zvqA0hnoQ+7vP2|v2>L~QE1`be&N$4g#47Zp>k!8uBivs$-XO(^Qmvb6-;2=TmHa7> zBu8x*JbByY^}D)aZ_^)65_;9(-?~)V=Zi}MTyxN**v($Kl%#P`v|TZoQV9BN^KpCK zJs@7`xKYA13gY}1Yg}70t8CoDj0CkQ)&_s;8;u9PaHlc2#5EzVpN}29QR2Rp97jjIG=(%qMqM z4lBBZawnRgM0)vQ^%7qoOZ_D`8LVwJYWFnhKI%K$!v|M|r z?R67D_pFHIb#@&9B>|DX$ISWdNzB%QU`-dym?!VmJ9?&KNl3h^upUsFe*Z_D-`2*c z$~D_0710uCWK`rF&+YhZUP)+^Szz4b!p2tfh!fgTu}e$6?=pcN^a!}c&-%~`Q6%d1 zKqk&D)3n*_4RvPXX!OS&UO35W3u|NNN4C8(T$`5oJ!R$Yczqu%j`UQMwPrlt2)W`97+PU7bSHPjO#X>YTmSAmI{Sj;Zet@S4=K`fXbg$3y z&!mN+ZSR2LZ8}4*>*W(Qo|=~b$20hzH#N>V@rZzM-mL9ZXFPCM!j!wX*O{fpBs~b#bXBX7;02J@Oph6m4uFcJ@ z!e?;J8G!Kb#2yt47b1|m? zn#p+;DIKIpi>5pzj0Y4W))5JU=N;K_4`-hk_1(K!UZLGK97db|Omp%}B}F#G5%>3H zDjHOknf@y2Qz|qze-XNu>7++;tXdQI=1zJ^XdBuG*L6r~VkJf#sJ6t~flIts)otS} z*upvzhzy13QG7$)K5@-lx?D}fgRJ*-ngFu=vxP^YOZ@-%HKlIq9xA%N*0z?~x@tb$ z)~@ny+euMO7aDnlDdb@WR!ygjvE2hpWbU8931E6huGD zG?I%p+*Qh`irhv{=q(56lGbltqK#fl>*eI7J#^|tG^YyUpF%J!cgcFj4LzW9pJTjp znN-LufI40-2UdR;|BJ=QQ|E2ZTg5)FZsy;L+X_l9Syd!(m_ z@cbB^aZ0SsE8o1n%E#9y0NxXx`AlLxp`X76a7?QKsDn?RU(y%j>%nz*CEKYC#VA}< zHZLA}9W|Sg0X>@(TCo4a?I1l(FwxcOxi{nxJWB=L4TF@m1Y3m7z^R7FH zpY_ohx%G^ER#bytQlT!ta6HPy8v9Yx@xKBDLQccil1f@U)_^vW?6(otZPq+`&XcU2cYpIP-8Xc>{-;yXB;#; zB^Q;hC(U2nMCQl;J1d2OhtWwHVi_F9O;iFJLmzEbRS1vsi`O_cJo0cUNa-IwbB(fm{`gYApeBi2}vjovE_l2d*~yKv1!72d+& zuvvTpo*LWySA?7GK4HpJEcaftoi*);jQsWg6nEwEQ10Jj@>4L2l_$i8IB zmMl?8Q?_hlM#>h(ntiFszC71bma;@;4Wkgr7Lr{mSt^w*iQo8s<<2ln-+Oz#e)G?q z^FE*Ld7jVme9q^b^L&23XLw*Wj%;tjVs0eeJhJ!QkFTw!g~pkK*Dl_zRlPYiUq(3- z-~Jf5?3^4~FXt3*&9GlGs6Q%R`qm1Q2bO1~*3cpYgVg(sJ991nIi4dUA2#*Ncr{=#uK?G}w)hj9`7-^6J=WR;sKre~j`yD1ejQaaz}uJ_m| z`CZF3Oa!+#ATr^+vg77gUxr@4)B^e^Pi-eH=PVnW>UN_>LPMb!^yYJgehz}&NQ^|= z$q?n5`r~uM-;bBm?5>g;oK7R6aV@GafTYH#9aBakqIvS2C}7sQht?bQUjp(;fBCvP zEQMv}i^6?44ub(X7mMQqJ#9P-V}x(|(eSl4t?+Phz1K3a3Z>*%sMLxA4lVNPn!dO; zZf-*%Eo8PkjAQ1K`~=H;9Z#RBS}o;SqrXp{N_PLM4vAI0Pcz}9urBrSxFYDJPr}U- zZ<7s8C192KF5?uY-+FgaEQAI@n>iKbe9VSkPf-5gxtnsK!Cr3f-N#p+DFCw7G%cr$ zN`~Ff8X;2}`VV?&@KVc^E<9{$&xrBAJK2RhHMTNeyRnc6TFhea8FFMjpJ3^DQL|CVo`1y&@ zN<=U`hz!7Eh>&DTL_j67C^yP^aT|b&HRZNxy5AgrN7Y0te{gg{-JWp?k z-a!x-|BdeWoBT(Va*y>W574#H-Tt|aOq%iDTUhqIv`l6v{U07uc7hr?#adx|;boIB zvYfruqU;_>XFg5mgdrWUMJyzRAsrRuM@lD3qE*WB%*@OAQGCVm4WgRCa}x1Q-V;+2 z0z2$&2Dp%+-$tZQSd1l#o>51?L(ft0^;CaKa6FK^WSNMPvPN{caj-G}+xm|^yy_ST zl>-77Wi(Zj>#kfxnlIi`@9-8ct9aTr9`YB9RN%M<{PJ6G%DDYW3ZI*6zs|n&7{mrq zEbPtX(*Gj=Z|nCdH^}O9O3f}+QPXB(?U~Iek+h$Ka_#rc03Tw?L{&o>$4k4Ea!>#0 z9{Hp7BP9$Lk-8MLM_;hI%!N-z<{~Gl^JoF!_87NWPKBisR?<+78q@qpNZ-=o00($^ zPyLa44*8BQz|`rjc-I~KkOb4i>8Va2H{-ariOwEGct}Uk=|qJETsZc-evsI;^@jYF z>{cP2HhC6YiVaINsedD{eCS-42f}XD%c1YU7X1P8M%v}!Kyf>PA^|V02GoDY*z*J< zA3ThJ)RxxPUNLSP;0)(?Ywd~yzqw5&IqM!4VLGZ!bww1N4B&DMq-n^=wp$A_mC z?Q&ecm7&ESK1iyNSdzH)xrkRWiC0qQ3%8nvmynO93G>mza%OCbkrabvHFm<06|_y$ zefCz=iL?(CaW9$8%XH(-$r4#D)P{qgR0Fn1R5az$%ILOHXx-o^&`v<%1A8s!=TH&$ z>VxI!vp%#VFEKxaE7)V+JY`-M$iwC>kZZ50hyBR{{V<4vVb>v<_QXNXk-VX>AzlgO z@ep(w*_f4+yyd3$69RBTb`v+e2_XQ$Z~}HS^Y?ydvuqV%BMz^l%F9+4&%afE?T-Xdg7udeFi%=i%%P#yCiY4^TBQXP`nQ~o zHm2Qzhm-JgA$U9cU)#4@Ck!x+5_!&G+_Kj=s!?_t;$M{VfJ^0`_ny!v#a?RfL9a6b zi)UpGxM)tTj8MmcJK+PuTybZ1wUwmvbN5J(Dc1e*Z0o%xMB2pb6v_tE>qt)8Cm$om8xK#9nqm_x? z;lozqNYrV!gC42~JyU(8k*Tw-_zCrcC&Zmm8j|9gM{I4S?5*7|>Jl$x8x&Xm`HQZO z7m$X^u9wwumFd}?wm{k_+ZYhWUQs7VRXcBb4f7ZzPWC!OLlnr zB{(I0GR;GDCR<(g*e<SxZ0>6g%%w=hdvMN7leG99*HpHc~a{$BqPh;}H(h*uIT)@^Mk zctJb5M5dbj*gSySgeMIH&yo_Bp^)be*p9Pt2!&I^eES4Ev_0LE5a~FL*qZv%FW3*0 zRpPXeeT|Ja>RvBO{Yb>nljWHf+%Wj(mfGM8U;L4BJxreaVE~PH(e3J5%@^!5Kzfo{zJjVKeM-eR;^Zg z0n*|p_A}BQc$o~tPDv?7JCz|`d@^HcIYEQtv=3i7x(s5e$=2wE|K#p!sKH$WOJt>* zv!=gi4^%Fv-k-*o6QQ>_^j%L`vrZ8sz;I(?@ULUwVG?P_6iw0yRtefE_86lTw5a=` zr5Hp{lYZ6P0G!d@zxu&RWI3B{4B zVvF3QVPz$#>>OrKWqVbjaez9{{v@v`v%-{Uh8b9HSa3Y*u8sG}+qIS*-hY~e__*~% z-wVquDWbMSal->88`7osagyABZ~|?y556xAwF?=rqeh|6>RMdF8q+hqUT~wp4VOwp z-C+ROAy~F|F^fBhKZF)gxF46TX#`OdIJu8zCJvRCiN$>%5bu0dCEdu3LL#>9#1*cg zGeT}NCzmCouT_6h>{u#XJ@Oos`OB@ZZ;@rs{Gq8UI;fSa>~*8A48GCV#G$4}Q=$t1 z{|~Ai#plLNTAR4ddK(2xEWLTV1STJ;4>t;oq-ZqC%;)_1-#N#xQ2?{Ax$Okq6q#Ux zNH+*!v9K8FBk}OJEq7`nagl(vq;P5yl{Pco*nP3D#m7~EQfWjvOOQOpH!I+{Ym1TT z$7T&CI8!-Q_=W88L6NPAMLWmx7M`P*N>y3_pk(J z?$TTV^m>mZ+E^445AsqNt6_4&38pM*xMNOir30*amP;9X9hCR3)}G*g;9vV14G_Wb zp<#SxGY#h1j1eFk-fzJFs}^KKeuA=B8B*5=!WmJa8M+$B>l7OY-)mV7K0ih^*r;<; zvj{7X-1RsuyC@rQl2Ju5E_rnAdxSnUITe#H;wM&enXmoH_1A2}V?x|l=k2xDMmo-9 zOFUNx5@{DOP?8q$**dr1S~Ej6`5HNLLc{xAHSUDn%Z=l#QH)dZmr`u`O?7+O`$0(> zofk^O#Hc^W6=Uc@0DpU<_5;oH*f8f;bBx>?-&#^lkG^p{BqI1es^c>DNX&ma$C^Rf zZ|xlZ5w?gQiw(}+<*vgh-8mmau7Hc1ZYLoC5VD)N;Y|nu0OlcJH!~X^?r-lLkj+&R zA_71^r(jNE9$P-+WnO9!t^GoMvTj#whN};A)R;N7#%^QaFq1=oq#AM{_@&a~ZmdTi z+6mr#(MqDTDQx`D*fAH5c_-?%&ap-OmTUY$pPgz!p#Ow#fdSG5@hO`0gW8{LvaT7c&CLBzi}09Ag>$A|iS$=nPMPnqrq~>O8s_P+1whDs@G9 z0W7rs%4VX8@vmr#wUQD$;9%BEHcjt+Ui$!j61w#9gZPZ~H@UD+PfFG-Sq}H)O7gsM!dgrw=j87%Waak=;ZhKb66G3o9dAD`$c^ z3@;N5$dgccZbFw|V3XSO8KIl@efhFCl)>Mue5op^$LGOZ7I3PkX9?jI!D()9;mdTu z^MiFLXTr*Wi?vC^*@{I9Oxmi2`%lwj2s0UmDw1_)8PCV6o`TmheRewKVs*$e+&aCW z^g0l)Vi5gJ;k}oizHUdpW3RJZq zj;KSqfs2Hn+bD~86|9g(Gh_Y+49@I?D0!7671IXnilZ!r7|BNf4BL@iE&A>?|h@0p=xrDNL!@PxKC1}*?E@taJPM&qML zk-p;A&qCSi3UiKFwzdtl7TwXnmv5wTt1&4jE`k9_YPBU6UIqsEYl|fM4SeHf^-N;b z9}n}|-{l(9D>@Yam{ZZWr0i@X0MRp470jqb6yzv!CU8CQHV^mB8vn~A>bq43U04TL ze;)aBfEIip9$*PACCp?zWz3cF9W2Vs~+tL z%>hhr$?d+uI9#5sJMOC9?2>W3I{9*C^SN7O3Cy|(!I}@aE#TtXoDh5kHEX$2rKq^PjMK#UVe@TK!3LY~_A}6>EH)>|{uk&w!Is71?~wPp68;JD6OA zub@9?%Gl{Q_CiOaU{h3XB#c=smr|nT9fqT$7BP@X!R8J&j_V zel06hcu=>>ukM>QojUex&QLP_Q_uaSpIizbeKu-TUV~+O)>Pg3ap?I-Tj#F>yL@$u zeNPINDij&-M(y4o7jVsNblA$sGJ7+NRD9D~B(S)$mzQ;l)A^Tkk`@$yz4`coH4bA% z0&s1Hr9JKqe-xwu#*NxKbk>eP1{E1`q?Fsxh_0KjHEUl%u5ns*x9HrUd%)074=1e2 z*j#7T@ZfPXyWf;DuDIv%ZDYO`UEU{WMkuP{OtMVNavRn@o>ZYsO^0mXF?I(QKfQb? zG5C35ld6Wh^M4#qJ%+<=6=c*zKpOu0hHH_Z&%4g>=HdgvFh&o$;+*W;gTL+*c%k z!sQsDUd8WmIIl`b^T0~v^K8&7c9)rVWOXg$v$exs)FRKsPc7@T(pzq?KsAo!l>lN> zk+$t`Tn+PD+g};^cJau z6Xs8YA6vJv1-$~LTIan2g^Ogb_-(Jk4U$E3!0RTEp&CcR_GpcCu(08kooUxE<(hzA zfvWgV^a_QdchJ?!S$2b9buhhqPoceS?SiX-UV&1r^In0%MY30bNtYQeu*JHhIpA}Z z=0u>u{zdwQk0K#gxlHh(fE-GeP?ccQ zqLq_Ep^}s+#IbWWf=MLpjQ^TtHJ&?d!Msh@w^?_lSzx(Dxs%8@%NjdsJorTf44qo` z`n}Q3uDB{ANKOlN9F}@2yW!WXNeay1tm96%LXP{$1|k7in4-%pB|>ryQ8+mSXQnP9&NpojJ%XQVjxK!Ki&A(U=efsK& z&)oRQBeAUGB&^uWaNe1v4bv=~QYh9y^YJ>M3x;Wrd`(-4gPl3WkYT6OJ2G^iVngbq8JIX*~>vpn3 ztuBrE->54g*Pso+V%uH@3qq4sAmZSX`IwLc0vjLO)S?RzqeGM+4k(NZW+JX?LR=TJ zh*(6m0%Q?h2+LJA2JwPUn+i*K*9!@esw3GccX(D%~m&Vse9)0 z*Kg*yr*@mq-Wz_o-v^as}eupH)LP9^XX0AGkvo)qLdim0$gSB6&+{?p# za`23t=JSTV$z5<+Za0cb>VhrWHZdjXYEPH#eZNlWXYl+23G0^AjlSRC6;mO+heE-4 z>U1?VPw_R=*V)(<&T~}>jAIC3u^csLAw0zumRMjg*=jX2Y@9EwjSzP}LPebMl*NTA z36{f@2fp3F;+Kw(9O3VCzd7)ftTA<}J^Len0|{$GdI~+H z1TO6<(>>>;Jq4@4;e?3Bgp`_V_$-{JGYhFDqE60r_BSVM2v+=S{ikYXZ9zOhPAIFCc zclR{JuNu=>g6eZ5>@}3%MFNnp*_S@e@0>P~cB{-; zreMYi^9f9S4+?K#6SwvqyK*b^eJ~Wem^L8i3LI*mBne6)3v&g8u)=Yq;y;dvVi;ct zL2$8)K;&s-X`E$b-EfOL_v#8`0&AW@Qj@Mhlg> zMuEye%V~e8c?+&xl_qEg1r6;M4kJN~Vkgi^K&eb4f)qm;ftidG!P!jq2>hBr0G1Nc zpfZ11Q~6+Hx`2Tzoxmmf3r6Qk?Q#vL=PCz<4sT?W*5N_t2NtuNcUcwjqpv~dTQ9&= zGf%mvg;Yt3Pl_@ZUI?)WKWSWgi1+%hC$pRaD>rf0jxZ*wLOrof)dtkHDD`3tXYiqz z8de>gMcGyhr+#bS%4Eh)^Q>i6Ess>Ra$fjeB)}&O`d(`Bt!7(Xp@0WHisp+Th@5mZ zxfio@?#*j_vter~k+a3S!JuN{84U4_}q4c*Z$c?Z`}>z#$2cxU_`Bxh}R;>y!qpwOAwmdK2Q*dqn3L4xD z1y|?`dtB9HLERXpu!y>)$AY~{UL;znj)jbwLL4qY$Pz~_?ITEB0V_}4))j>}I)SOw zhQnzYHp9gjnvP}?SHxtUs~o7_*6xzG>xO1i$^6*@7919j|GcGu?7S5Sa4V@ z_R?d)Vew$J9t#$W;?*bVvEZ=qpP|Qs!y+|6j|GcG38%$+EI2HtuGC|}VUf6Aj|Gc` zt$m~(3l57hyY*ObSe%O1W5Hr!S0ql41&4*#89f#p7O_csELbc`8mH>9;IQa-UylWc z#qLZ!7918ovh-N6SlD;@sK#^XlSZkxlg2Upay&ek| zi_(o9^jL6MEUlu)g2UobEj<=27G+!;>9OFjnAcK|1&7704tgwDEXr2up~r&5V)_6* z7919rz4TbHSd=R>T8{;X#rR2jEI2F@X6UhCu_#|OK#v87#jwSCEI2HVtkh${Vu6{h z*JHt9F)&h(1&77H-FhrIEPh4nv0$-q=pLuXg2Q6V89f#p7GIL|SWsAiPmenu_h7M* z`#Lo01zzZ_YSBy4I7(PSP+a6&)l`P40*ax)=snn>Ox5~UHO`er9Ch|qwe`c5#{C)| zdN$9;FKDD9W6gBq+;1eTFxRzwWOyONRS)|2>+oWghh_Y(!-Plx7N%&!2Dt?7DW~K9 zntH9;cQ2^Ip41MxL7qb9BbbnIV6bV4WMpXk>b@OOfS3!HKqSIm6Q_`4nH9!&} z91lK1`7j$r)1g(ZxAT%1PyG(TdJ5F3J{VQY6RHf_Q$+^XjM;qjX3D$P9WjHjFQY=1 zCb`M|KA45Uh?w4aJECuAY^rBeXR-6ZMrn^pSetk0I!E{R7}?FW*dHTzyId&0DrB0g zNB|Nx?O(TS^o!zQjHfC&y@Nd^$YBBrQ6fL6i-2sA1YJ_1x8qE;lAclrTwNbi1-+Nh z0!hH6O659>ekSXE?v1V!;XR~D=k7MeF7#h>yVn#FR(w0L#0uAam3>}xjk7DeD3Cdr73HG#3?niV%baFu7$7CS>vvk?$A?PvNjU zisl7$=BY6WGa|wojmLHeIgjn>bbEH~pM~s5SoOx7?n5ptS~Bcnr9ZaQPz+g zQ&c(g4rcS1F;)3sNhwbu+79bfp=oKxQ%dNA∋tNwKG(e8E#3(-pP&;4S9Z9V+yW7jg~4-Yr@pT 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 e|7Xol!TKwzhYtV!. + +package ipld + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" +) + +// EthAccountSnapshot (eth-account-snapshot codec 0x97) +// represents an ethereum account, i.e. a wallet address or +// a smart contract +type EthAccountSnapshot struct { + *EthAccount + + cid cid.Cid + rawdata []byte +} + +// EthAccount is the building block of EthAccountSnapshot. +// Or, is the former stripped of its cid and rawdata components. +type EthAccount struct { + Nonce uint64 + Balance *big.Int + Root []byte // This is the storage root trie + CodeHash []byte // This is the hash of the EVM code +} + +// Static (compile time) check that EthAccountSnapshot satisfies the +// node.Node interface. +var _ node.Node = (*EthAccountSnapshot)(nil) + +/* + INPUT +*/ + +// Input should be managed by EthStateTrie + +/* + OUTPUT +*/ + +// Output should be managed by EthStateTrie + +/* + Block INTERFACE +*/ + +// RawData returns the binary of the RLP encode of the account snapshot. +func (as *EthAccountSnapshot) RawData() []byte { + return as.rawdata +} + +// Cid returns the cid of the transaction. +func (as *EthAccountSnapshot) Cid() cid.Cid { + return as.cid +} + +// String is a helper for output +func (as *EthAccountSnapshot) String() string { + return fmt.Sprintf("", as.cid) +} + +// Loggable returns in a map the type of IPLD Link. +func (as *EthAccountSnapshot) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-account-snapshot", + } +} + +/* + Node INTERFACE +*/ + +// Resolve resolves a path through this node, stopping at any link boundary +// and returning the object found as well as the remaining path to traverse +func (as *EthAccountSnapshot) Resolve(p []string) (interface{}, []string, error) { + if len(p) == 0 { + return as, nil, nil + } + + if len(p) > 1 { + return nil, nil, fmt.Errorf("unexpected path elements past %s", p[0]) + } + + switch p[0] { + case "balance": + return as.Balance, nil, nil + case "codeHash": + return &node.Link{Cid: keccak256ToCid(RawBinary, as.CodeHash)}, nil, nil + case "nonce": + return as.Nonce, nil, nil + case "root": + return &node.Link{Cid: keccak256ToCid(MEthStorageTrie, as.Root)}, nil, nil + default: + return nil, nil, ErrInvalidLink + } +} + +// Tree lists all paths within the object under 'path', and up to the given depth. +// To list the entire object (similar to `find .`) pass "" and -1 +func (as *EthAccountSnapshot) Tree(p string, depth int) []string { + if p != "" || depth == 0 { + return nil + } + return []string{"balance", "codeHash", "nonce", "root"} +} + +// ResolveLink is a helper function that calls resolve and asserts the +// output is a link +func (as *EthAccountSnapshot) ResolveLink(p []string) (*node.Link, []string, error) { + obj, rest, err := as.Resolve(p) + if err != nil { + return nil, nil, err + } + + if lnk, ok := obj.(*node.Link); ok { + return lnk, rest, nil + } + + return nil, nil, fmt.Errorf("resolved item was not a link") +} + +// Copy will go away. It is here to comply with the interface. +func (as *EthAccountSnapshot) Copy() node.Node { + panic("implement me") +} + +// Links is a helper function that returns all links within this object +func (as *EthAccountSnapshot) Links() []*node.Link { + return nil +} + +// Stat will go away. It is here to comply with the interface. +func (as *EthAccountSnapshot) Stat() (*node.NodeStat, error) { + return &node.NodeStat{}, nil +} + +// Size will go away. It is here to comply with the interface. +func (as *EthAccountSnapshot) Size() (uint64, error) { + return 0, nil +} + +/* + EthAccountSnapshot functions +*/ + +// MarshalJSON processes the transaction into readable JSON format. +func (as *EthAccountSnapshot) MarshalJSON() ([]byte, error) { + out := map[string]interface{}{ + "balance": as.Balance, + "codeHash": keccak256ToCid(RawBinary, as.CodeHash), + "nonce": as.Nonce, + "root": keccak256ToCid(MEthStorageTrie, as.Root), + } + return json.Marshal(out) +} diff --git a/statediff/indexer/ipfs/ipld/eth_account_test.go b/statediff/indexer/ipfs/ipld/eth_account_test.go new file mode 100644 index 000000000000..dbfabc9fb0a6 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_account_test.go @@ -0,0 +1,298 @@ +package ipld + +import ( + "encoding/json" + "fmt" + "os" + "regexp" + "testing" +) + +/* + Block INTERFACE +*/ +func init() { + if os.Getenv("MODE") != "statediff" { + fmt.Println("Skipping statediff test") + os.Exit(0) + } +} + +func TestAccountSnapshotBlockElements(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + if fmt.Sprintf("%x", eas.RawData())[:10] != "f84e808a03" { + t.Fatal("Wrong Data") + } + + if eas.Cid().String() != + "baglqcgzasckx2alxk43cksshnztjvhfyvbbh6bkp376gtcndm5cg4fkrkhsa" { + t.Fatal("Wrong Cid") + } +} + +func TestAccountSnapshotString(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + if eas.String() != + "" { + t.Fatalf("Wrong String()") + } +} + +func TestAccountSnapshotLoggable(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + l := eas.Loggable() + if _, ok := l["type"]; !ok { + t.Fatal("Loggable map expected the field 'type'") + } + + if l["type"] != "eth-account-snapshot" { + t.Fatalf("Wrong Loggable 'type' value\r\nexpected %s\r\ngot %s", "eth-account-snapshot", l["type"]) + } +} + +/* + Node INTERFACE +*/ +func TestAccountSnapshotResolve(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + // Empty path + obj, rest, err := eas.Resolve([]string{}) + reas, ok := obj.(*EthAccountSnapshot) + if !ok { + t.Fatalf("Wrong type of returned object\r\nexpected %T\r\ngot %T", &EthAccountSnapshot{}, reas) + } + if reas.Cid() != eas.Cid() { + t.Fatalf("wrong returned CID\r\nexpected %s\r\ngot %s", eas.Cid().String(), reas.Cid().String()) + } + if rest != nil { + t.Fatal("rest should be nil") + } + if err != nil { + t.Fatal("err should be nil") + } + + // len(p) > 1 + badCases := [][]string{ + {"two", "elements"}, + {"here", "three", "elements"}, + {"and", "here", "four", "elements"}, + } + + for _, bc := range badCases { + obj, rest, err = eas.Resolve(bc) + if obj != nil { + t.Fatal("obj should be nil") + } + if rest != nil { + t.Fatal("rest should be nil") + } + if err.Error() != fmt.Sprintf("unexpected path elements past %s", bc[0]) { + t.Fatal("wrong error") + } + } + + moreBadCases := []string{ + "i", + "am", + "not", + "an", + "account", + "field", + } + for _, mbc := range moreBadCases { + obj, rest, err = eas.Resolve([]string{mbc}) + if obj != nil { + t.Fatal("obj should be nil") + } + if rest != nil { + t.Fatal("rest should be nil") + } + if err != ErrInvalidLink { + t.Fatal("wrong error") + } + } + + goodCases := []string{ + "balance", + "codeHash", + "nonce", + "root", + } + for _, gc := range goodCases { + _, _, err = eas.Resolve([]string{gc}) + if err != nil { + t.Fatalf("error should be nil %v", gc) + } + } + +} + +func TestAccountSnapshotTree(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + // Bad cases + tree := eas.Tree("non-empty-string", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = eas.Tree("non-empty-string", 1) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = eas.Tree("", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + // Good cases + tree = eas.Tree("", 1) + lookupElements := map[string]interface{}{ + "balance": nil, + "codeHash": nil, + "nonce": nil, + "root": nil, + } + + if len(tree) != len(lookupElements) { + t.Fatalf("Wrong number of elements\r\nexpected %d\r\ngot %d", len(lookupElements), len(tree)) + } + + for _, te := range tree { + if _, ok := lookupElements[te]; !ok { + t.Fatalf("Unexpected Element: %v", te) + } + } +} + +func TestAccountSnapshotResolveLink(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + // bad case + obj, rest, err := eas.ResolveLink([]string{"supercalifragilist"}) + if obj != nil { + t.Fatalf("Expected obj to be nil") + } + if rest != nil { + t.Fatal("Expected rest to be nil") + } + if err != ErrInvalidLink { + t.Fatal("Wrong error") + } + + // good case + obj, rest, err = eas.ResolveLink([]string{"nonce"}) + if obj != nil { + t.Fatalf("Expected obj to be nil") + } + if rest != nil { + t.Fatal("Expected rest to be nil") + } + if err.Error() != "resolved item was not a link" { + t.Fatal("Wrong error") + } +} + +func TestAccountSnapshotCopy(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + defer func() { + r := recover() + if r == nil { + t.Fatal("Expected panic") + } + if r != "implement me" { + t.Fatalf("Wrong panic message\r\n expected %s\r\ngot %s", "'implement me'", r) + } + }() + + _ = eas.Copy() +} + +func TestAccountSnapshotLinks(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + if eas.Links() != nil { + t.Fatal("Links() expected to return nil") + } +} + +func TestAccountSnapshotStat(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + obj, err := eas.Stat() + if obj == nil { + t.Fatal("Expected a not null object node.NodeStat") + } + + if err != nil { + t.Fatal("Expected a nil error") + } +} + +func TestAccountSnapshotSize(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + size, err := eas.Size() + if size != uint64(0) { + t.Fatalf("Wrong size\r\nexpected %d\r\ngot %d", 0, size) + } + + if err != nil { + t.Fatal("Expected a nil error") + } +} + +/* + EthAccountSnapshot functions +*/ + +func TestAccountSnapshotMarshalJSON(t *testing.T) { + eas := prepareEthAccountSnapshot(t) + + jsonOutput, err := eas.MarshalJSON() + checkError(err, t) + + var data map[string]interface{} + err = json.Unmarshal(jsonOutput, &data) + checkError(err, t) + + balanceExpression := regexp.MustCompile(`{"balance":16011846000000000000000,`) + if !balanceExpression.MatchString(string(jsonOutput)) { + t.Fatal("Balance expression not found") + } + + code, _ := data["codeHash"].(map[string]interface{}) + if fmt.Sprintf("%s", code["/"]) != + "bafkrwigf2jdadbxxem6je7t5wlomoa6a4ualmu6kqittw6723acf3bneoa" { + t.Fatalf("Wrong Marshaled Value\r\nexpected %s\r\ngot %s", "bafkrwigf2jdadbxxem6je7t5wlomoa6a4ualmu6kqittw6723acf3bneoa", fmt.Sprintf("%s", code["/"])) + } + + if fmt.Sprintf("%v", data["nonce"]) != "0" { + t.Fatalf("Wrong Marshaled Value\r\nexpected %s\r\ngot %s", "0", fmt.Sprintf("%v", data["nonce"])) + } + + root, _ := data["root"].(map[string]interface{}) + if fmt.Sprintf("%s", root["/"]) != + "bagmacgzak3ub6fy3zrk2n74dixtjfqhynznurya3tfwk3qabmix3ly3dwqqq" { + t.Fatalf("Wrong Marshaled Value\r\nexpected %s\r\ngot %s", "bagmacgzak3ub6fy3zrk2n74dixtjfqhynznurya3tfwk3qabmix3ly3dwqqq", fmt.Sprintf("%s", root["/"])) + } +} + +/* + AUXILIARS +*/ +func prepareEthAccountSnapshot(t *testing.T) *EthAccountSnapshot { + fi, err := os.Open("test_data/eth-state-trie-rlp-c9070d") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + return output.elements[1].(*EthAccountSnapshot) +} diff --git a/statediff/indexer/ipfs/ipld/eth_header.go b/statediff/indexer/ipfs/ipld/eth_header.go new file mode 100644 index 000000000000..5905bdd7ef79 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_header.go @@ -0,0 +1,293 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + mh "github.com/multiformats/go-multihash" +) + +// EthHeader (eth-block, codec 0x90), represents an ethereum block header +type EthHeader struct { + *types.Header + + cid cid.Cid + rawdata []byte +} + +// Static (compile time) check that EthHeader satisfies the node.Node interface. +var _ node.Node = (*EthHeader)(nil) + +/* + INPUT +*/ + +// NewEthHeader converts a *types.Header into an EthHeader IPLD node +func NewEthHeader(header *types.Header) (*EthHeader, error) { + headerRLP, err := rlp.EncodeToBytes(header) + if err != nil { + return nil, err + } + c, err := RawdataToCid(MEthHeader, headerRLP, mh.KECCAK_256) + if err != nil { + return nil, err + } + return &EthHeader{ + Header: header, + cid: c, + rawdata: headerRLP, + }, nil +} + +/* + OUTPUT +*/ + +// DecodeEthHeader takes a cid and its raw binary data +// from IPFS and returns an EthTx object for further processing. +func DecodeEthHeader(c cid.Cid, b []byte) (*EthHeader, error) { + h := new(types.Header) + if err := rlp.DecodeBytes(b, h); err != nil { + return nil, err + } + return &EthHeader{ + Header: h, + cid: c, + rawdata: b, + }, nil +} + +/* + Block INTERFACE +*/ + +// RawData returns the binary of the RLP encode of the block header. +func (b *EthHeader) RawData() []byte { + return b.rawdata +} + +// Cid returns the cid of the block header. +func (b *EthHeader) Cid() cid.Cid { + return b.cid +} + +// String is a helper for output +func (b *EthHeader) String() string { + return fmt.Sprintf("", b.cid) +} + +// Loggable returns a map the type of IPLD Link. +func (b *EthHeader) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-header", + } +} + +/* + Node INTERFACE +*/ + +// Resolve resolves a path through this node, stopping at any link boundary +// and returning the object found as well as the remaining path to traverse +func (b *EthHeader) Resolve(p []string) (interface{}, []string, error) { + if len(p) == 0 { + return b, nil, nil + } + + first, rest := p[0], p[1:] + + switch first { + case "parent": + return &node.Link{Cid: commonHashToCid(MEthHeader, b.ParentHash)}, rest, nil + case "receipts": + return &node.Link{Cid: commonHashToCid(MEthTxReceiptTrie, b.ReceiptHash)}, rest, nil + case "root": + return &node.Link{Cid: commonHashToCid(MEthStateTrie, b.Root)}, rest, nil + case "tx": + return &node.Link{Cid: commonHashToCid(MEthTxTrie, b.TxHash)}, rest, nil + case "uncles": + return &node.Link{Cid: commonHashToCid(MEthHeaderList, b.UncleHash)}, rest, nil + } + + if len(p) != 1 { + return nil, nil, fmt.Errorf("unexpected path elements past %s", first) + } + + switch first { + case "bloom": + return b.Bloom, nil, nil + case "coinbase": + return b.Coinbase, nil, nil + case "difficulty": + return b.Difficulty, nil, nil + case "extra": + // This is a []byte. By default they are marshalled into Base64. + return fmt.Sprintf("0x%x", b.Extra), nil, nil + case "gaslimit": + return b.GasLimit, nil, nil + case "gasused": + return b.GasUsed, nil, nil + case "mixdigest": + return b.MixDigest, nil, nil + case "nonce": + return b.Nonce, nil, nil + case "number": + return b.Number, nil, nil + case "time": + return b.Time, nil, nil + default: + return nil, nil, ErrInvalidLink + } +} + +// Tree lists all paths within the object under 'path', and up to the given depth. +// To list the entire object (similar to `find .`) pass "" and -1 +func (b *EthHeader) Tree(p string, depth int) []string { + if p != "" || depth == 0 { + return nil + } + + return []string{ + "time", + "bloom", + "coinbase", + "difficulty", + "extra", + "gaslimit", + "gasused", + "mixdigest", + "nonce", + "number", + "parent", + "receipts", + "root", + "tx", + "uncles", + } +} + +// ResolveLink is a helper function that allows easier traversal of links through blocks +func (b *EthHeader) ResolveLink(p []string) (*node.Link, []string, error) { + obj, rest, err := b.Resolve(p) + if err != nil { + return nil, nil, err + } + + if lnk, ok := obj.(*node.Link); ok { + return lnk, rest, nil + } + + return nil, nil, fmt.Errorf("resolved item was not a link") +} + +// Copy will go away. It is here to comply with the Node interface. +func (b *EthHeader) Copy() node.Node { + panic("implement me") +} + +// Links is a helper function that returns all links within this object +// HINT: Use `ipfs refs ` +func (b *EthHeader) Links() []*node.Link { + return []*node.Link{ + {Cid: commonHashToCid(MEthHeader, b.ParentHash)}, + {Cid: commonHashToCid(MEthTxReceiptTrie, b.ReceiptHash)}, + {Cid: commonHashToCid(MEthStateTrie, b.Root)}, + {Cid: commonHashToCid(MEthTxTrie, b.TxHash)}, + {Cid: commonHashToCid(MEthHeaderList, b.UncleHash)}, + } +} + +// Stat will go away. It is here to comply with the Node interface. +func (b *EthHeader) Stat() (*node.NodeStat, error) { + return &node.NodeStat{}, nil +} + +// Size will go away. It is here to comply with the Node interface. +func (b *EthHeader) Size() (uint64, error) { + return 0, nil +} + +/* + EthHeader functions +*/ + +// MarshalJSON processes the block header into readable JSON format, +// converting the right links into their cids, and keeping the original +// hex hash, allowing the user to simplify external queries. +func (b *EthHeader) MarshalJSON() ([]byte, error) { + out := map[string]interface{}{ + "time": b.Time, + "bloom": b.Bloom, + "coinbase": b.Coinbase, + "difficulty": b.Difficulty, + "extra": fmt.Sprintf("0x%x", b.Extra), + "gaslimit": b.GasLimit, + "gasused": b.GasUsed, + "mixdigest": b.MixDigest, + "nonce": b.Nonce, + "number": b.Number, + "parent": commonHashToCid(MEthHeader, b.ParentHash), + "receipts": commonHashToCid(MEthTxReceiptTrie, b.ReceiptHash), + "root": commonHashToCid(MEthStateTrie, b.Root), + "tx": commonHashToCid(MEthTxTrie, b.TxHash), + "uncles": commonHashToCid(MEthHeaderList, b.UncleHash), + } + return json.Marshal(out) +} + +// objJSONHeader defines the output of the JSON RPC API for either +// "eth_BlockByHash" or "eth_BlockByHeader". +type objJSONHeader struct { + Result objJSONHeaderResult `json:"result"` +} + +// objJSONBLockResult is the nested struct that takes +// the contents of the JSON field "result". +type objJSONHeaderResult struct { + types.Header // Use its fields and unmarshaler + *objJSONHeaderResultExt // Add these fields to the parsing +} + +// objJSONBLockResultExt facilitates the composition +// of the field "result", adding to the +// `types.Header` fields, both ommers (their hashes) and transactions. +type objJSONHeaderResultExt struct { + OmmerHashes []common.Hash `json:"uncles"` + Transactions []*types.Transaction `json:"transactions"` +} + +// UnmarshalJSON overrides the function types.Header.UnmarshalJSON, allowing us +// to parse the fields of Header, plus ommer hashes and transactions. +// (yes, ommer hashes. You will need to "eth_getUncleCountByBlockHash" per each ommer) +func (o *objJSONHeaderResult) UnmarshalJSON(input []byte) error { + err := o.Header.UnmarshalJSON(input) + if err != nil { + return err + } + + o.objJSONHeaderResultExt = &objJSONHeaderResultExt{} + err = json.Unmarshal(input, o.objJSONHeaderResultExt) + return err +} diff --git a/statediff/indexer/ipfs/ipld/eth_header_test.go b/statediff/indexer/ipfs/ipld/eth_header_test.go new file mode 100644 index 000000000000..ebbab21297b1 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_header_test.go @@ -0,0 +1,586 @@ +package ipld + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "runtime" + "strconv" + "testing" + + block "github.com/ipfs/go-block-format" + node "github.com/ipfs/go-ipld-format" + "github.com/multiformats/go-multihash" + + "github.com/ethereum/go-ethereum/core/types" +) + +func TestBlockBodyRlpParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-block-body-rlp-999999") + checkError(err, t) + + output, _, _, err := FromBlockRLP(fi) + checkError(err, t) + + testEthBlockFields(output, t) +} + +func TestBlockHeaderRlpParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-block-header-rlp-999999") + checkError(err, t) + + output, _, _, err := FromBlockRLP(fi) + checkError(err, t) + + testEthBlockFields(output, t) +} + +func TestBlockBodyJsonParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-block-body-json-999999") + checkError(err, t) + + output, _, _, err := FromBlockJSON(fi) + checkError(err, t) + + testEthBlockFields(output, t) +} + +func TestEthBlockProcessTransactionsError(t *testing.T) { + // Let's just change one byte in a field of one of these transactions. + fi, err := os.Open("test_data/error-tx-eth-block-body-json-999999") + checkError(err, t) + + _, _, _, err = FromBlockJSON(fi) + if err == nil { + t.Fatal("Expected an error") + } +} + +// TestDecodeBlockHeader should work for both inputs (block header and block body) +// as what we are storing is just the block header +func TestDecodeBlockHeader(t *testing.T) { + storedEthBlock := prepareStoredEthBlock("test_data/eth-block-header-rlp-999999", t) + + ethBlock, err := DecodeEthHeader(storedEthBlock.Cid(), storedEthBlock.RawData()) + checkError(err, t) + + testEthBlockFields(ethBlock, t) +} + +func TestEthBlockString(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + if ethBlock.String() != "" { + t.Fatalf("Wrong String()\r\nexpected %s\r\ngot %s", "", ethBlock.String()) + } +} + +func TestEthBlockLoggable(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + l := ethBlock.Loggable() + if _, ok := l["type"]; !ok { + t.Fatal("Loggable map expected the field 'type'") + } + + if l["type"] != "eth-header" { + t.Fatalf("Wrong Loggable 'type' value\r\nexpected %s\r\ngot %s", "eth-header", l["type"]) + } +} + +func TestEthBlockJSONMarshal(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + jsonOutput, err := ethBlock.MarshalJSON() + checkError(err, t) + + var data map[string]interface{} + err = json.Unmarshal(jsonOutput, &data) + checkError(err, t) + + // Testing all fields is boring, but can help us to avoid + // that dreaded regression + if data["bloom"].(string)[:10] != "0x00000000" { + t.Fatalf("Wrong Bloom\r\nexpected %s\r\ngot %s", "0x00000000", data["bloom"].(string)[:10]) + t.Fatal("Wrong Bloom") + } + if data["coinbase"] != "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5" { + t.Fatalf("Wrong coinbase\r\nexpected %s\r\ngot %s", "0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5", data["coinbase"]) + } + if parseFloat(data["difficulty"]) != "12555463106190" { + t.Fatalf("Wrong Difficulty\r\nexpected %s\r\ngot %s", "12555463106190", parseFloat(data["difficulty"])) + } + if data["extra"] != "0xd783010303844765746887676f312e342e32856c696e7578" { + t.Fatalf("Wrong Extra\r\nexpected %s\r\ngot %s", "0xd783010303844765746887676f312e342e32856c696e7578", data["extra"]) + } + if parseFloat(data["gaslimit"]) != "3141592" { + t.Fatalf("Wrong Gas limit\r\nexpected %s\r\ngot %s", "3141592", parseFloat(data["gaslimit"])) + } + if parseFloat(data["gasused"]) != "231000" { + t.Fatalf("Wrong Gas used\r\nexpected %s\r\ngot %s", "231000", parseFloat(data["gasused"])) + } + if data["mixdigest"] != "0x5b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0" { + t.Fatalf("Wrong Mix digest\r\nexpected %s\r\ngot %s", "0x5b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0", data["mixdigest"]) + } + if data["nonce"] != "0xf491f46b60fe04b3" { + t.Fatalf("Wrong nonce\r\nexpected %s\r\ngot %s", "0xf491f46b60fe04b3", data["nonce"]) + } + if parseFloat(data["number"]) != "999999" { + t.Fatalf("Wrong block number\r\nexpected %s\r\ngot %s", "999999", parseFloat(data["number"])) + } + if parseMapElement(data["parent"]) != "bagiacgza2m6j3xu774hlvjxhd2fsnuv5ufom6ei4ply3mm3jrleeozt7b62a" { + t.Fatalf("Wrong Parent cid\r\nexpected %s\r\ngot %s", "bagiacgza2m6j3xu774hlvjxhd2fsnuv5ufom6ei4ply3mm3jrleeozt7b62a", parseMapElement(data["parent"])) + } + if parseMapElement(data["receipts"]) != "bagkacgzap6qpnsrkagbdecgybaa63ljx4pr2aa5vlsetdg2f5mpzpbrk2iuq" { + t.Fatalf("Wrong Receipt root cid\r\nexpected %s\r\ngot %s", "bagkacgzap6qpnsrkagbdecgybaa63ljx4pr2aa5vlsetdg2f5mpzpbrk2iuq", parseMapElement(data["receipts"])) + } + if parseMapElement(data["root"]) != "baglacgza5wmkus23dhec7m2tmtyikcfobjw6yzs7uv3ghxfjjroxavkm3yia" { + t.Fatalf("Wrong root hash cid\r\nexpected %s\r\ngot %s", "baglacgza5wmkus23dhec7m2tmtyikcfobjw6yzs7uv3ghxfjjroxavkm3yia", parseMapElement(data["root"])) + } + if parseFloat(data["time"]) != "1455404037" { + t.Fatalf("Wrong Time\r\nexpected %s\r\ngot %s", "1455404037", parseFloat(data["time"])) + } + if parseMapElement(data["tx"]) != "bagjacgzair6l3dci6smknejlccbrzx7vtr737s56onoksked2t5anxgxvzka" { + t.Fatalf("Wrong Tx root cid\r\nexpected %s\r\ngot %s", "bagjacgzair6l3dci6smknejlccbrzx7vtr737s56onoksked2t5anxgxvzka", parseMapElement(data["tx"])) + } + if parseMapElement(data["uncles"]) != "bagiqcgzadxge32g6y5oxvk4fwvt3ntgudljreri3ssfhie7qufbp2qgusndq" { + t.Fatalf("Wrong Uncle hash cid\r\nexpected %s\r\ngot %s", "bagiqcgzadxge32g6y5oxvk4fwvt3ntgudljreri3ssfhie7qufbp2qgusndq", parseMapElement(data["uncles"])) + } +} + +func TestEthBlockLinks(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + links := ethBlock.Links() + if links[0].Cid.String() != "bagiacgza2m6j3xu774hlvjxhd2fsnuv5ufom6ei4ply3mm3jrleeozt7b62a" { + t.Fatalf("Wrong cid for parent link\r\nexpected: %s\r\ngot %s", "bagiacgza2m6j3xu774hlvjxhd2fsnuv5ufom6ei4ply3mm3jrleeozt7b62a", links[0].Cid.String()) + } + if links[1].Cid.String() != "bagkacgzap6qpnsrkagbdecgybaa63ljx4pr2aa5vlsetdg2f5mpzpbrk2iuq" { + t.Fatalf("Wrong cid for receipt root link\r\nexpected: %s\r\ngot %s", "bagkacgzap6qpnsrkagbdecgybaa63ljx4pr2aa5vlsetdg2f5mpzpbrk2iuq", links[1].Cid.String()) + } + if links[2].Cid.String() != "baglacgza5wmkus23dhec7m2tmtyikcfobjw6yzs7uv3ghxfjjroxavkm3yia" { + t.Fatalf("Wrong cid for state root link\r\nexpected: %s\r\ngot %s", "baglacgza5wmkus23dhec7m2tmtyikcfobjw6yzs7uv3ghxfjjroxavkm3yia", links[2].Cid.String()) + } + if links[3].Cid.String() != "bagjacgzair6l3dci6smknejlccbrzx7vtr737s56onoksked2t5anxgxvzka" { + t.Fatalf("Wrong cid for tx root link\r\nexpected: %s\r\ngot %s", "bagjacgzair6l3dci6smknejlccbrzx7vtr737s56onoksked2t5anxgxvzka", links[3].Cid.String()) + } + if links[4].Cid.String() != "bagiqcgzadxge32g6y5oxvk4fwvt3ntgudljreri3ssfhie7qufbp2qgusndq" { + t.Fatalf("Wrong cid for uncles root link\r\nexpected: %s\r\ngot %s", "bagiqcgzadxge32g6y5oxvk4fwvt3ntgudljreri3ssfhie7qufbp2qgusndq", links[4].Cid.String()) + } +} + +func TestEthBlockResolveEmptyPath(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + obj, rest, err := ethBlock.Resolve([]string{}) + checkError(err, t) + + if ethBlock != obj.(*EthHeader) { + t.Fatal("Should have returned the same eth-block object") + } + + if len(rest) != 0 { + t.Fatalf("Wrong len of rest of the path returned\r\nexpected %d\r\ngot %d", 0, len(rest)) + } +} + +func TestEthBlockResolveNoSuchLink(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + _, _, err := ethBlock.Resolve([]string{"wewonthavethisfieldever"}) + if err == nil { + t.Fatal("Should have failed with unknown field") + } + + if err != ErrInvalidLink { + t.Fatalf("Wrong error message\r\nexpected %s\r\ngot %s", ErrInvalidLink, err.Error()) + } +} + +func TestEthBlockResolveBloom(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + obj, rest, err := ethBlock.Resolve([]string{"bloom"}) + checkError(err, t) + + // The marshaler of types.Bloom should output it as 0x + bloomInText := fmt.Sprintf("%x", obj.(types.Bloom)) + if bloomInText[:10] != "0000000000" { + t.Fatalf("Wrong Bloom\r\nexpected %s\r\ngot %s", "0000000000", bloomInText[:10]) + } + + if len(rest) != 0 { + t.Fatalf("Wrong len of rest of the path returned\r\nexpected %d\r\ngot %d", 0, len(rest)) + } +} + +func TestEthBlockResolveBloomExtraPathElements(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + obj, rest, err := ethBlock.Resolve([]string{"bloom", "unexpected", "extra", "elements"}) + if obj != nil { + t.Fatal("Returned obj should be nil") + } + + if rest != nil { + t.Fatal("Returned rest should be nil") + } + + if err.Error() != "unexpected path elements past bloom" { + t.Fatalf("Wrong error\r\nexpected %s\r\ngot %s", "unexpected path elements past bloom", err.Error()) + } +} + +func TestEthBlockResolveNonLinkFields(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + testCases := map[string][]string{ + "coinbase": {"%x", "52bc44d5378309ee2abf1539bf71de1b7d7be3b5"}, + "difficulty": {"%s", "12555463106190"}, + "extra": {"%s", "0xd783010303844765746887676f312e342e32856c696e7578"}, + "gaslimit": {"%d", "3141592"}, + "gasused": {"%d", "231000"}, + "mixdigest": {"%x", "5b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0"}, + "nonce": {"%x", "f491f46b60fe04b3"}, + "number": {"%s", "999999"}, + "time": {"%d", "1455404037"}, + } + + for field, value := range testCases { + obj, rest, err := ethBlock.Resolve([]string{field}) + checkError(err, t) + + format := value[0] + result := value[1] + if fmt.Sprintf(format, obj) != result { + t.Fatalf("Wrong %v\r\nexpected %v\r\ngot %s", field, result, fmt.Sprintf(format, obj)) + } + + if len(rest) != 0 { + t.Fatalf("Wrong len of rest of the path returned\r\nexpected %d\r\ngot %d", 0, len(rest)) + } + } +} + +func TestEthBlockResolveNonLinkFieldsExtraPathElements(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + testCases := []string{ + "coinbase", + "difficulty", + "extra", + "gaslimit", + "gasused", + "mixdigest", + "nonce", + "number", + "time", + } + + for _, field := range testCases { + obj, rest, err := ethBlock.Resolve([]string{field, "unexpected", "extra", "elements"}) + if obj != nil { + t.Fatal("Returned obj should be nil") + } + + if rest != nil { + t.Fatal("Returned rest should be nil") + } + + if err.Error() != "unexpected path elements past "+field { + t.Fatalf("Wrong error\r\nexpected %s\r\ngot %s", "unexpected path elements past "+field, err.Error()) + } + + } +} + +func TestEthBlockResolveLinkFields(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + testCases := map[string]string{ + "parent": "bagiacgza2m6j3xu774hlvjxhd2fsnuv5ufom6ei4ply3mm3jrleeozt7b62a", + "receipts": "bagkacgzap6qpnsrkagbdecgybaa63ljx4pr2aa5vlsetdg2f5mpzpbrk2iuq", + "root": "baglacgza5wmkus23dhec7m2tmtyikcfobjw6yzs7uv3ghxfjjroxavkm3yia", + "tx": "bagjacgzair6l3dci6smknejlccbrzx7vtr737s56onoksked2t5anxgxvzka", + "uncles": "bagiqcgzadxge32g6y5oxvk4fwvt3ntgudljreri3ssfhie7qufbp2qgusndq", + } + + for field, result := range testCases { + obj, rest, err := ethBlock.Resolve([]string{field, "anything", "goes", "here"}) + checkError(err, t) + + lnk, ok := obj.(*node.Link) + if !ok { + t.Fatal("Returned object is not a link") + } + + if lnk.Cid.String() != result { + t.Fatalf("Wrong %s cid\r\nexpected %v\r\ngot %v", field, result, lnk.Cid.String()) + } + + for i, p := range []string{"anything", "goes", "here"} { + if rest[i] != p { + t.Fatalf("Wrong rest of the path returned\r\nexpected %s\r\ngot %s", p, rest[i]) + } + } + } +} + +func TestEthBlockTreeBadParams(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + tree := ethBlock.Tree("non-empty-string", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = ethBlock.Tree("non-empty-string", 1) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = ethBlock.Tree("", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } +} + +func TestEThBlockTree(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + tree := ethBlock.Tree("", 1) + lookupElements := map[string]interface{}{ + "bloom": nil, + "coinbase": nil, + "difficulty": nil, + "extra": nil, + "gaslimit": nil, + "gasused": nil, + "mixdigest": nil, + "nonce": nil, + "number": nil, + "parent": nil, + "receipts": nil, + "root": nil, + "time": nil, + "tx": nil, + "uncles": nil, + } + + if len(tree) != len(lookupElements) { + t.Fatalf("Wrong number of elements\r\nexpected %d\r\ngot %d", len(lookupElements), len(tree)) + } + + for _, te := range tree { + if _, ok := lookupElements[te]; !ok { + t.Fatalf("Unexpected Element: %v", te) + } + } +} + +/* + The two functions above: TestEthBlockResolveNonLinkFields and + TestEthBlockResolveLinkFields did all the heavy lifting. Then, we will + just test two use cases. +*/ +func TestEthBlockResolveLinksBadLink(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + obj, rest, err := ethBlock.ResolveLink([]string{"supercalifragilist"}) + if obj != nil { + t.Fatalf("Expected obj to be nil") + } + if rest != nil { + t.Fatal("Expected rest to be nil") + } + + if err != ErrInvalidLink { + t.Fatalf("Expected error\r\nexpected %s\r\ngot %s", ErrInvalidLink, err) + } +} + +func TestEthBlockResolveLinksGoodLink(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + obj, rest, err := ethBlock.ResolveLink([]string{"tx", "0", "0", "0"}) + if obj == nil { + t.Fatalf("Expected valid *node.Link obj to be returned") + } + + if rest == nil { + t.Fatal("Expected rest to be returned") + } + for i, p := range []string{"0", "0", "0"} { + if rest[i] != p { + t.Fatalf("Wrong rest of the path returned\r\nexpected %s\r\ngot %s", p, rest[i]) + } + } + + if err != nil { + t.Fatal("Non error expected") + } +} + +/* + These functions below should go away + We are working on test coverage anyways... +*/ +func TestEthBlockCopy(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + defer func() { + r := recover() + if r == nil { + t.Fatal("Expected panic") + } + if r != "implement me" { + t.Fatalf("Wrong panic message\r\nexpected %s\r\ngot %s", "'implement me'", r) + } + }() + + _ = ethBlock.Copy() +} + +func TestEthBlockStat(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + obj, err := ethBlock.Stat() + if obj == nil { + t.Fatal("Expected a not null object node.NodeStat") + } + + if err != nil { + t.Fatal("Expected a nil error") + } +} + +func TestEthBlockSize(t *testing.T) { + ethBlock := prepareDecodedEthBlock("test_data/eth-block-header-rlp-999999", t) + + size, err := ethBlock.Size() + if size != 0 { + t.Fatalf("Wrong size\r\nexpected %d\r\ngot %d", 0, size) + } + + if err != nil { + t.Fatal("Expected a nil error") + } +} + +/* + AUXILIARS +*/ + +// checkError makes 3 lines into 1. +func checkError(err error, t *testing.T) { + if err != nil { + _, fn, line, _ := runtime.Caller(1) + t.Fatalf("[%v:%v] %v", fn, line, err) + } +} + +// parseFloat is a convenience function to test json output +func parseFloat(v interface{}) string { + return strconv.FormatFloat(v.(float64), 'f', 0, 64) +} + +// parseMapElement is a convenience function to tets json output +func parseMapElement(v interface{}) string { + return v.(map[string]interface{})["/"].(string) +} + +// prepareStoredEthBlock reads the block from a file source to get its rawdata +// and computes its cid, for then, feeding it into a new IPLD block function. +// So we can pretend that we got this block from the datastore +func prepareStoredEthBlock(filepath string, t *testing.T) *block.BasicBlock { + // Prepare the "fetched block". This one is supposed to be in the datastore + // and given away by github.com/ipfs/go-ipfs/merkledag + fi, err := os.Open(filepath) + checkError(err, t) + + b, err := ioutil.ReadAll(fi) + checkError(err, t) + + c, err := RawdataToCid(MEthHeader, b, multihash.KECCAK_256) + checkError(err, t) + + // It's good to clarify that this one below is an IPLD block + storedEthBlock, err := block.NewBlockWithCid(b, c) + checkError(err, t) + + return storedEthBlock +} + +// prepareDecodedEthBlock is more complex than function above, as it stores a +// basic block and RLP-decodes it +func prepareDecodedEthBlock(filepath string, t *testing.T) *EthHeader { + // Get the block from the datastore and decode it. + storedEthBlock := prepareStoredEthBlock("test_data/eth-block-header-rlp-999999", t) + ethBlock, err := DecodeEthHeader(storedEthBlock.Cid(), storedEthBlock.RawData()) + checkError(err, t) + + return ethBlock +} + +// testEthBlockFields checks the fields of EthBlock one by one. +func testEthBlockFields(ethBlock *EthHeader, t *testing.T) { + // Was the cid calculated? + if ethBlock.Cid().String() != "bagiacgzawt5236hkiuvrhfyy4jya3qitlt6icfcqgheew6vsptlraokppm4a" { + t.Fatalf("Wrong cid\r\nexpected %s\r\ngot %s", "bagiacgzawt5236hkiuvrhfyy4jya3qitlt6icfcqgheew6vsptlraokppm4a", ethBlock.Cid().String()) + } + + // Do we have the rawdata available? + if fmt.Sprintf("%x", ethBlock.RawData()[:10]) != "f90218a0d33c9dde9fff" { + t.Fatalf("Wrong Rawdata\r\nexpected %s\r\ngot %s", "f90218a0d33c9dde9fff", fmt.Sprintf("%x", ethBlock.RawData()[:10])) + } + + // Proper Fields of types.Header + if fmt.Sprintf("%x", ethBlock.ParentHash) != "d33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4" { + t.Fatalf("Wrong ParentHash\r\nexpected %s\r\ngot %s", "d33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4", fmt.Sprintf("%x", ethBlock.ParentHash)) + } + if fmt.Sprintf("%x", ethBlock.UncleHash) != "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" { + t.Fatalf("Wrong UncleHash field\r\nexpected %s\r\ngot %s", "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", fmt.Sprintf("%x", ethBlock.UncleHash)) + } + if fmt.Sprintf("%x", ethBlock.Coinbase) != "52bc44d5378309ee2abf1539bf71de1b7d7be3b5" { + t.Fatalf("Wrong Coinbase\r\nexpected %s\r\ngot %s", "52bc44d5378309ee2abf1539bf71de1b7d7be3b5", fmt.Sprintf("%x", ethBlock.Coinbase)) + } + if fmt.Sprintf("%x", ethBlock.Root) != "ed98aa4b5b19c82fb35364f08508ae0a6dec665fa57663dca94c5d70554cde10" { + t.Fatalf("Wrong Root\r\nexpected %s\r\ngot %s", "ed98aa4b5b19c82fb35364f08508ae0a6dec665fa57663dca94c5d70554cde10", fmt.Sprintf("%x", ethBlock.Root)) + } + if fmt.Sprintf("%x", ethBlock.TxHash) != "447cbd8c48f498a6912b10831cdff59c7fbfcbbe735ca92883d4fa06dcd7ae54" { + t.Fatalf("Wrong TxHash\r\nexpected %s\r\ngot %s", "447cbd8c48f498a6912b10831cdff59c7fbfcbbe735ca92883d4fa06dcd7ae54", fmt.Sprintf("%x", ethBlock.TxHash)) + } + if fmt.Sprintf("%x", ethBlock.ReceiptHash) != "7fa0f6ca2a01823208d80801edad37e3e3a003b55c89319b45eb1f97862ad229" { + t.Fatalf("Wrong ReceiptHash\r\nexpected %s\r\ngot %s", "7fa0f6ca2a01823208d80801edad37e3e3a003b55c89319b45eb1f97862ad229", fmt.Sprintf("%x", ethBlock.ReceiptHash)) + } + if len(ethBlock.Bloom) != 256 { + t.Fatalf("Wrong Bloom Length\r\nexpected %d\r\ngot %d", 256, len(ethBlock.Bloom)) + } + if fmt.Sprintf("%x", ethBlock.Bloom[71:76]) != "0000000000" { // You wouldn't want me to print out the whole bloom field? + t.Fatalf("Wrong Bloom\r\nexpected %s\r\ngot %s", "0000000000", fmt.Sprintf("%x", ethBlock.Bloom[71:76])) + } + if ethBlock.Difficulty.String() != "12555463106190" { + t.Fatalf("Wrong Difficulty\r\nexpected %s\r\ngot %s", "12555463106190", ethBlock.Difficulty.String()) + } + if ethBlock.Number.String() != "999999" { + t.Fatalf("Wrong Block Number\r\nexpected %s\r\ngot %s", "999999", ethBlock.Number.String()) + } + if ethBlock.GasLimit != uint64(3141592) { + t.Fatalf("Wrong Gas Limit\r\nexpected %d\r\ngot %d", 3141592, ethBlock.GasLimit) + } + if ethBlock.GasUsed != uint64(231000) { + t.Fatalf("Wrong Gas Used\r\nexpected %d\r\ngot %d", 231000, ethBlock.GasUsed) + } + if ethBlock.Time != uint64(1455404037) { + t.Fatalf("Wrong Time\r\nexpected %d\r\ngot %d", 1455404037, ethBlock.Time) + } + if fmt.Sprintf("%x", ethBlock.Extra) != "d783010303844765746887676f312e342e32856c696e7578" { + t.Fatalf("Wrong Extra\r\nexpected %s\r\ngot %s", "d783010303844765746887676f312e342e32856c696e7578", fmt.Sprintf("%x", ethBlock.Extra)) + } + if fmt.Sprintf("%x", ethBlock.Nonce) != "f491f46b60fe04b3" { + t.Fatalf("Wrong Nonce\r\nexpected %s\r\ngot %s", "f491f46b60fe04b3", fmt.Sprintf("%x", ethBlock.Nonce)) + } + if fmt.Sprintf("%x", ethBlock.MixDigest) != "5b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0" { + t.Fatalf("Wrong MixDigest\r\nexpected %s\r\ngot %s", "5b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0", fmt.Sprintf("%x", ethBlock.MixDigest)) + } +} diff --git a/statediff/indexer/ipfs/ipld/eth_parser.go b/statediff/indexer/ipfs/ipld/eth_parser.go new file mode 100644 index 000000000000..1acaaf06e2d2 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_parser.go @@ -0,0 +1,198 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + + "github.com/multiformats/go-multihash" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) + +// FromBlockRLP takes an RLP message representing +// an ethereum block header or body (header, ommers and txs) +// to return it as a set of IPLD nodes for further processing. +func FromBlockRLP(r io.Reader) (*EthHeader, []*EthTx, []*EthTxTrie, error) { + // We may want to use this stream several times + rawdata, err := ioutil.ReadAll(r) + if err != nil { + return nil, nil, nil, err + } + + // Let's try to decode the received element as a block body + var decodedBlock types.Block + err = rlp.Decode(bytes.NewBuffer(rawdata), &decodedBlock) + if err != nil { + if err.Error()[:41] != "rlp: expected input list for types.Header" { + return nil, nil, nil, err + } + + // Maybe it is just a header... (body sans ommers and txs) + var decodedHeader types.Header + err := rlp.Decode(bytes.NewBuffer(rawdata), &decodedHeader) + if err != nil { + return nil, nil, nil, err + } + + c, err := RawdataToCid(MEthHeader, rawdata, multihash.KECCAK_256) + if err != nil { + return nil, nil, nil, err + } + // It was a header + return &EthHeader{ + Header: &decodedHeader, + cid: c, + rawdata: rawdata, + }, nil, nil, nil + } + + // This is a block body (header + ommers + txs) + // We'll extract the header bits here + headerRawData := getRLP(decodedBlock.Header()) + c, err := RawdataToCid(MEthHeader, headerRawData, multihash.KECCAK_256) + if err != nil { + return nil, nil, nil, err + } + ethBlock := &EthHeader{ + Header: decodedBlock.Header(), + cid: c, + rawdata: headerRawData, + } + + // Process the found eth-tx objects + ethTxNodes, ethTxTrieNodes, err := processTransactions(decodedBlock.Transactions(), + decodedBlock.Header().TxHash[:]) + if err != nil { + return nil, nil, nil, err + } + + return ethBlock, ethTxNodes, ethTxTrieNodes, nil +} + +// FromBlockJSON takes the output of an ethereum client JSON API +// (i.e. parity or geth) and returns a set of IPLD nodes. +func FromBlockJSON(r io.Reader) (*EthHeader, []*EthTx, []*EthTxTrie, error) { + var obj objJSONHeader + dec := json.NewDecoder(r) + err := dec.Decode(&obj) + if err != nil { + return nil, nil, nil, err + } + + headerRawData := getRLP(obj.Result.Header) + c, err := RawdataToCid(MEthHeader, headerRawData, multihash.KECCAK_256) + if err != nil { + return nil, nil, nil, err + } + ethBlock := &EthHeader{ + Header: &obj.Result.Header, + cid: c, + rawdata: headerRawData, + } + + // Process the found eth-tx objects + ethTxNodes, ethTxTrieNodes, err := processTransactions(obj.Result.Transactions, + obj.Result.Header.TxHash[:]) + if err != nil { + return nil, nil, nil, err + } + + return ethBlock, ethTxNodes, ethTxTrieNodes, nil +} + +// FromBlockAndReceipts takes a block and processes it +// to return it a set of IPLD nodes for further processing. +func FromBlockAndReceipts(block *types.Block, receipts []*types.Receipt) (*EthHeader, []*EthHeader, []*EthTx, []*EthTxTrie, []*EthReceipt, []*EthRctTrie, error) { + // Process the header + headerNode, err := NewEthHeader(block.Header()) + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + // Process the uncles + uncleNodes := make([]*EthHeader, len(block.Uncles())) + for i, uncle := range block.Uncles() { + uncleNode, err := NewEthHeader(uncle) + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + uncleNodes[i] = uncleNode + } + // Process the txs + ethTxNodes, ethTxTrieNodes, err := processTransactions(block.Transactions(), + block.Header().TxHash[:]) + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + // Process the receipts + ethRctNodes, ethRctTrieNodes, err := processReceipts(receipts, + block.Header().ReceiptHash[:]) + return headerNode, uncleNodes, ethTxNodes, ethTxTrieNodes, ethRctNodes, ethRctTrieNodes, err +} + +// processTransactions will take the found transactions in a parsed block body +// to return IPLD node slices for eth-tx and eth-tx-trie +func processTransactions(txs []*types.Transaction, expectedTxRoot []byte) ([]*EthTx, []*EthTxTrie, error) { + var ethTxNodes []*EthTx + transactionTrie := newTxTrie() + + for idx, tx := range txs { + ethTx, err := NewEthTx(tx) + if err != nil { + return nil, nil, err + } + ethTxNodes = append(ethTxNodes, ethTx) + if err := transactionTrie.add(idx, ethTx.RawData()); err != nil { + return nil, nil, err + } + } + + if !bytes.Equal(transactionTrie.rootHash(), expectedTxRoot) { + return nil, nil, fmt.Errorf("wrong transaction hash computed") + } + txTrieNodes, err := transactionTrie.getNodes() + return ethTxNodes, txTrieNodes, err +} + +// processReceipts will take in receipts +// to return IPLD node slices for eth-rct and eth-rct-trie +func processReceipts(rcts []*types.Receipt, expectedRctRoot []byte) ([]*EthReceipt, []*EthRctTrie, error) { + var ethRctNodes []*EthReceipt + receiptTrie := newRctTrie() + + for idx, rct := range rcts { + ethRct, err := NewReceipt(rct) + if err != nil { + return nil, nil, err + } + ethRctNodes = append(ethRctNodes, ethRct) + if err := receiptTrie.add(idx, ethRct.RawData()); err != nil { + return nil, nil, err + } + } + + if !bytes.Equal(receiptTrie.rootHash(), expectedRctRoot) { + return nil, nil, fmt.Errorf("wrong receipt hash computed") + } + rctTrieNodes, err := receiptTrie.getNodes() + return ethRctNodes, rctTrieNodes, err +} diff --git a/statediff/indexer/ipfs/ipld/eth_parser_test.go b/statediff/indexer/ipfs/ipld/eth_parser_test.go new file mode 100644 index 000000000000..1a53455c796e --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_parser_test.go @@ -0,0 +1,81 @@ +package ipld + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) + +type kind string + +const ( + legacy kind = "legacy" + eip1559 kind = "eip2930" +) + +var blockFileNames = []string{ + "eth-block-12252078", + "eth-block-12365585", + "eth-block-12365586", +} + +var receiptsFileNames = []string{ + "eth-receipts-12252078", + "eth-receipts-12365585", + "eth-receipts-12365586", +} + +var kinds = []kind{ + eip1559, + eip1559, + legacy, +} + +type testCase struct { + kind kind + block *types.Block + receipts types.Receipts +} + +func loadBlockData(t *testing.T) []testCase { + fileDir := "./eip2930_test_data" + testCases := make([]testCase, len(blockFileNames)) + for i, blockFileName := range blockFileNames { + blockRLP, err := ioutil.ReadFile(filepath.Join(fileDir, blockFileName)) + if err != nil { + t.Fatalf("failed to load blockRLP from file, err %v", err) + } + block := new(types.Block) + if err := rlp.DecodeBytes(blockRLP, block); err != nil { + t.Fatalf("failed to decode blockRLP, err %v", err) + } + receiptsFileName := receiptsFileNames[i] + receiptsRLP, err := ioutil.ReadFile(filepath.Join(fileDir, receiptsFileName)) + if err != nil { + t.Fatalf("failed to load receiptsRLP from file, err %s", err) + } + receipts := make(types.Receipts, 0) + if err := rlp.DecodeBytes(receiptsRLP, &receipts); err != nil { + t.Fatalf("failed to decode receiptsRLP, err %s", err) + } + testCases[i] = testCase{ + block: block, + receipts: receipts, + kind: kinds[i], + } + } + return testCases +} + +func TestFromBlockAndReceipts(t *testing.T) { + testCases := loadBlockData(t) + for _, tc := range testCases { + _, _, _, _, _, _, err := FromBlockAndReceipts(tc.block, tc.receipts) + if err != nil { + t.Fatalf("error generating IPLDs from block and receipts, err %v, kind %s, block hash %s", err, tc.kind, tc.block.Hash()) + } + } +} diff --git a/statediff/indexer/ipfs/ipld/eth_receipt.go b/statediff/indexer/ipfs/ipld/eth_receipt.go new file mode 100644 index 000000000000..822f473452d4 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_receipt.go @@ -0,0 +1,198 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + mh "github.com/multiformats/go-multihash" +) + +type EthReceipt struct { + *types.Receipt + + rawdata []byte + cid cid.Cid +} + +// Static (compile time) check that EthReceipt satisfies the node.Node interface. +var _ node.Node = (*EthReceipt)(nil) + +/* + INPUT +*/ + +// NewReceipt converts a types.ReceiptForStorage to an EthReceipt IPLD node +func NewReceipt(receipt *types.Receipt) (*EthReceipt, error) { + rctRaw, err := receipt.MarshalBinary() + if err != nil { + return nil, err + } + c, err := RawdataToCid(MEthTxReceipt, rctRaw, mh.KECCAK_256) + if err != nil { + return nil, err + } + return &EthReceipt{ + Receipt: receipt, + cid: c, + rawdata: rctRaw, + }, nil +} + +/* + OUTPUT +*/ + +// DecodeEthReceipt takes a cid and its raw binary data +// from IPFS and returns an EthTx object for further processing. +func DecodeEthReceipt(c cid.Cid, b []byte) (*EthReceipt, error) { + r := new(types.Receipt) + if err := r.UnmarshalBinary(b); err != nil { + return nil, err + } + return &EthReceipt{ + Receipt: r, + cid: c, + rawdata: b, + }, nil +} + +/* + Block INTERFACE +*/ + +func (node *EthReceipt) RawData() []byte { + return node.rawdata +} + +func (node *EthReceipt) Cid() cid.Cid { + return node.cid +} + +// String is a helper for output +func (r *EthReceipt) String() string { + return fmt.Sprintf("", r.cid) +} + +// Loggable returns in a map the type of IPLD Link. +func (r *EthReceipt) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-receipt", + } +} + +// Resolve resolves a path through this node, stopping at any link boundary +// and returning the object found as well as the remaining path to traverse +func (r *EthReceipt) Resolve(p []string) (interface{}, []string, error) { + if len(p) == 0 { + return r, nil, nil + } + + if len(p) > 1 { + return nil, nil, fmt.Errorf("unexpected path elements past %s", p[0]) + } + + switch p[0] { + + case "root": + return r.PostState, nil, nil + case "status": + return r.Status, nil, nil + case "cumulativeGasUsed": + return r.CumulativeGasUsed, nil, nil + case "logsBloom": + return r.Bloom, nil, nil + case "logs": + return r.Logs, nil, nil + case "transactionHash": + return r.TxHash, nil, nil + case "contractAddress": + return r.ContractAddress, nil, nil + case "gasUsed": + return r.GasUsed, nil, nil + default: + return nil, nil, ErrInvalidLink + } +} + +// Tree lists all paths within the object under 'path', and up to the given depth. +// To list the entire object (similar to `find .`) pass "" and -1 +func (r *EthReceipt) Tree(p string, depth int) []string { + if p != "" || depth == 0 { + return nil + } + return []string{"root", "status", "cumulativeGasUsed", "logsBloom", "logs", "transactionHash", "contractAddress", "gasUsed"} +} + +// ResolveLink is a helper function that calls resolve and asserts the +// output is a link +func (r *EthReceipt) ResolveLink(p []string) (*node.Link, []string, error) { + obj, rest, err := r.Resolve(p) + if err != nil { + return nil, nil, err + } + + if lnk, ok := obj.(*node.Link); ok { + return lnk, rest, nil + } + + return nil, nil, fmt.Errorf("resolved item was not a link") +} + +// Copy will go away. It is here to comply with the Node interface. +func (*EthReceipt) Copy() node.Node { + panic("implement me") +} + +// Links is a helper function that returns all links within this object +func (*EthReceipt) Links() []*node.Link { + return nil +} + +// Stat will go away. It is here to comply with the interface. +func (r *EthReceipt) Stat() (*node.NodeStat, error) { + return &node.NodeStat{}, nil +} + +// Size will go away. It is here to comply with the interface. +func (r *EthReceipt) Size() (uint64, error) { + return strconv.ParseUint(r.Receipt.Size().String(), 10, 64) +} + +/* + EthReceipt functions +*/ + +// MarshalJSON processes the receipt into readable JSON format. +func (r *EthReceipt) MarshalJSON() ([]byte, error) { + out := map[string]interface{}{ + "root": r.PostState, + "status": r.Status, + "cumulativeGasUsed": r.CumulativeGasUsed, + "logsBloom": r.Bloom, + "logs": r.Logs, + "transactionHash": r.TxHash, + "contractAddress": r.ContractAddress, + "gasUsed": r.GasUsed, + } + return json.Marshal(out) +} diff --git a/statediff/indexer/ipfs/ipld/eth_receipt_trie.go b/statediff/indexer/ipfs/ipld/eth_receipt_trie.go new file mode 100644 index 000000000000..ee371c15a446 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_receipt_trie.go @@ -0,0 +1,150 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "fmt" + + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + "github.com/multiformats/go-multihash" + + "github.com/ethereum/go-ethereum/core/types" +) + +// EthRctTrie (eth-tx-trie codec 0x92) represents +// a node from the transaction trie in ethereum. +type EthRctTrie struct { + *TrieNode +} + +// Static (compile time) check that EthRctTrie satisfies the node.Node interface. +var _ node.Node = (*EthRctTrie)(nil) + +/* + INPUT +*/ + +// To create a proper trie of the eth-tx-trie objects, it is required +// to input all transactions belonging to a forest in a single step. +// We are adding the transactions, and creating its trie on +// block body parsing time. + +/* + OUTPUT +*/ + +// DecodeEthRctTrie returns an EthRctTrie object from its cid and rawdata. +func DecodeEthRctTrie(c cid.Cid, b []byte) (*EthRctTrie, error) { + tn, err := decodeTrieNode(c, b, decodeEthRctTrieLeaf) + if err != nil { + return nil, err + } + return &EthRctTrie{TrieNode: tn}, nil +} + +// decodeEthRctTrieLeaf parses a eth-rct-trie leaf +//from decoded RLP elements +func decodeEthRctTrieLeaf(i []interface{}) ([]interface{}, error) { + r := new(types.Receipt) + if err := r.UnmarshalBinary(i[1].([]byte)); err != nil { + return nil, err + } + c, err := RawdataToCid(MEthTxReceipt, i[1].([]byte), multihash.KECCAK_256) + if err != nil { + return nil, err + } + return []interface{}{ + i[0].([]byte), + &EthReceipt{ + Receipt: r, + cid: c, + rawdata: i[1].([]byte), + }, + }, nil +} + +/* + Block INTERFACE +*/ + +// RawData returns the binary of the RLP encode of the transaction. +func (t *EthRctTrie) RawData() []byte { + return t.rawdata +} + +// Cid returns the cid of the transaction. +func (t *EthRctTrie) Cid() cid.Cid { + return t.cid +} + +// String is a helper for output +func (t *EthRctTrie) String() string { + return fmt.Sprintf("", t.cid) +} + +// Loggable returns in a map the type of IPLD Link. +func (t *EthRctTrie) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-rct-trie", + } +} + +/* + EthRctTrie functions +*/ + +// rctTrie wraps a localTrie for use on the receipt trie. +type rctTrie struct { + *localTrie +} + +// newRctTrie initializes and returns a rctTrie. +func newRctTrie() *rctTrie { + return &rctTrie{ + localTrie: newLocalTrie(), + } +} + +// getNodes invokes the localTrie, which computes the root hash of the +// transaction trie and returns its database keys, to return a slice +// of EthRctTrie nodes. +func (rt *rctTrie) getNodes() ([]*EthRctTrie, error) { + keys, err := rt.getKeys() + if err != nil { + return nil, err + } + var out []*EthRctTrie + + for _, k := range keys { + rawdata, err := rt.db.Get(k) + if err != nil { + return nil, err + } + c, err := RawdataToCid(MEthTxReceiptTrie, rawdata, multihash.KECCAK_256) + if err != nil { + return nil, err + } + tn := &TrieNode{ + cid: c, + rawdata: rawdata, + } + out = append(out, &EthRctTrie{TrieNode: tn}) + } + + return out, nil +} diff --git a/statediff/indexer/ipfs/ipld/eth_state.go b/statediff/indexer/ipfs/ipld/eth_state.go new file mode 100644 index 000000000000..9a2c230e23a0 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_state.go @@ -0,0 +1,126 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "fmt" + "io" + "io/ioutil" + + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + "github.com/multiformats/go-multihash" + + "github.com/ethereum/go-ethereum/rlp" +) + +// EthStateTrie (eth-state-trie, codec 0x96), represents +// a node from the satte trie in ethereum. +type EthStateTrie struct { + *TrieNode +} + +// Static (compile time) check that EthStateTrie satisfies the node.Node interface. +var _ node.Node = (*EthStateTrie)(nil) + +/* + INPUT +*/ + +// FromStateTrieRLPFile takes the RLP representation of an ethereum +// state trie node to return it as an IPLD node for further processing. +func FromStateTrieRLPFile(r io.Reader) (*EthStateTrie, error) { + raw, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + return FromStateTrieRLP(raw) +} + +// FromStateTrieRLP takes the RLP representation of an ethereum +// state trie node to return it as an IPLD node for further processing. +func FromStateTrieRLP(raw []byte) (*EthStateTrie, error) { + c, err := RawdataToCid(MEthStateTrie, raw, multihash.KECCAK_256) + if err != nil { + return nil, err + } + // Let's run the whole mile and process the nodeKind and + // its elements, in case somebody would need this function + // to parse an RLP element from the filesystem + return DecodeEthStateTrie(c, raw) +} + +/* + OUTPUT +*/ + +// DecodeEthStateTrie returns an EthStateTrie object from its cid and rawdata. +func DecodeEthStateTrie(c cid.Cid, b []byte) (*EthStateTrie, error) { + tn, err := decodeTrieNode(c, b, decodeEthStateTrieLeaf) + if err != nil { + return nil, err + } + return &EthStateTrie{TrieNode: tn}, nil +} + +// decodeEthStateTrieLeaf parses a eth-tx-trie leaf +// from decoded RLP elements +func decodeEthStateTrieLeaf(i []interface{}) ([]interface{}, error) { + var account EthAccount + err := rlp.DecodeBytes(i[1].([]byte), &account) + if err != nil { + return nil, err + } + c, err := RawdataToCid(MEthAccountSnapshot, i[1].([]byte), multihash.KECCAK_256) + if err != nil { + return nil, err + } + return []interface{}{ + i[0].([]byte), + &EthAccountSnapshot{ + EthAccount: &account, + cid: c, + rawdata: i[1].([]byte), + }, + }, nil +} + +/* + Block INTERFACE +*/ + +// RawData returns the binary of the RLP encode of the state trie node. +func (st *EthStateTrie) RawData() []byte { + return st.rawdata +} + +// Cid returns the cid of the state trie node. +func (st *EthStateTrie) Cid() cid.Cid { + return st.cid +} + +// String is a helper for output +func (st *EthStateTrie) String() string { + return fmt.Sprintf("", st.cid) +} + +// Loggable returns in a map the type of IPLD Link. +func (st *EthStateTrie) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-state-trie", + } +} diff --git a/statediff/indexer/ipfs/ipld/eth_state_test.go b/statediff/indexer/ipfs/ipld/eth_state_test.go new file mode 100644 index 000000000000..20ff7767016b --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_state_test.go @@ -0,0 +1,326 @@ +package ipld + +import ( + "fmt" + "os" + "testing" + + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" +) + +/* + INPUT + OUTPUT +*/ + +func TestStateTrieNodeEvenExtensionParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-eb2f5f") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if output.nodeKind != "extension" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "extension", output.nodeKind) + } + + if len(output.elements) != 2 { + t.Fatalf("Wrong number of elements for an extension node\r\nexpected %d\r\ngot %d", 2, len(output.elements)) + } + + if fmt.Sprintf("%x", output.elements[0]) != "0d08" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "0d08", fmt.Sprintf("%x", output.elements[0])) + } + + if output.elements[1].(cid.Cid).String() != + "baglacgzalnzmhhnxudxtga6t3do2rctb6ycgyj6mjnycoamlnc733nnbkd6q" { + t.Fatalf("Wrong CID\r\nexpected %s\r\ngot %s", "baglacgzalnzmhhnxudxtga6t3do2rctb6ycgyj6mjnycoamlnc733nnbkd6q", output.elements[1].(cid.Cid).String()) + } +} + +func TestStateTrieNodeOddExtensionParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-56864f") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if output.nodeKind != "extension" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "extension", output.nodeKind) + } + + if len(output.elements) != 2 { + t.Fatalf("Wrong number of elements for an extension node\r\nexpected %d\r\ngot %d", 2, len(output.elements)) + } + + if fmt.Sprintf("%x", output.elements[0]) != "02" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "02", fmt.Sprintf("%x", output.elements[0])) + } + + if output.elements[1].(cid.Cid).String() != + "baglacgzaizf2czb7wztoox4lu23qkwkbfamqsdzcmejzr3rsszrvkaktpfeq" { + t.Fatalf("Wrong CID\r\nexpected %s\r\ngot %s", "baglacgzaizf2czb7wztoox4lu23qkwkbfamqsdzcmejzr3rsszrvkaktpfeq", output.elements[1].(cid.Cid).String()) + } +} + +func TestStateTrieNodeEvenLeafParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-0e8b34") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if output.nodeKind != "leaf" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "leaf", output.nodeKind) + } + + if len(output.elements) != 2 { + t.Fatalf("Wrong number of elements for an extension node\r\nexpected %d\r\ngot %d", 2, len(output.elements)) + } + + // bd66f60e5b954e1af93ded1b02cb575ff0ed6d9241797eff7576b0bf0637 + if fmt.Sprintf("%x", output.elements[0].([]byte)[0:10]) != "0b0d06060f06000e050b" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "0b0d06060f06000e050b", fmt.Sprintf("%x", output.elements[0].([]byte)[0:10])) + } + + if output.elements[1].(*EthAccountSnapshot).String() != + "" { + t.Fatalf("Wrong String()\r\nexpected %s\r\ngot %s", "", output.elements[1].(*EthAccountSnapshot).String()) + } +} + +func TestStateTrieNodeOddLeafParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-c9070d") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if output.nodeKind != "leaf" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "leaf", output.nodeKind) + } + + if len(output.elements) != 2 { + t.Fatalf("Wrong number of elements for an extension node\r\nexpected %d\r\ngot %d", 2, len(output.elements)) + } + + // 6c9db9bb545a03425e300f3ee72bae098110336dd3eaf48c20a2e5b6865fc + if fmt.Sprintf("%x", output.elements[0].([]byte)[0:10]) != "060c090d0b090b0b0504" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "060c090d0b090b0b0504", fmt.Sprintf("%x", output.elements[0].([]byte)[0:10])) + } + + if output.elements[1].(*EthAccountSnapshot).String() != + "" { + t.Fatalf("Wrong String()\r\nexpected %s\r\ngot %s", "", output.elements[1].(*EthAccountSnapshot).String()) + } +} + +/* + Block INTERFACE +*/ +func TestStateTrieBlockElements(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-d7f897") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if fmt.Sprintf("%x", output.RawData())[:10] != "f90211a090" { + t.Fatalf("Wrong Data\r\nexpected %s\r\ngot %s", "f90211a090", fmt.Sprintf("%x", output.RawData())[:10]) + } + + if output.Cid().String() != + "baglacgza274jot5vvr4ntlajtonnkaml5xbm4cts3liye6qxbhndawapavca" { + t.Fatalf("Wrong Cid\r\nexpected %s\r\ngot %s", "baglacgza274jot5vvr4ntlajtonnkaml5xbm4cts3liye6qxbhndawapavca", output.Cid().String()) + } +} + +func TestStateTrieString(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-d7f897") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if output.String() != + "" { + t.Fatalf("Wrong String()\r\nexpected %s\r\ngot %s", "", output.String()) + } +} + +func TestStateTrieLoggable(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-d7f897") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + l := output.Loggable() + if _, ok := l["type"]; !ok { + t.Fatal("Loggable map expected the field 'type'") + } + + if l["type"] != "eth-state-trie" { + t.Fatalf("Wrong Loggable 'type' value\r\nexpected %s\r\ngot %s", "eth-state-trie", l["type"]) + } +} + +/* + TRIE NODE (Through EthStateTrie) + Node INTERFACE +*/ + +func TestTraverseStateTrieWithResolve(t *testing.T) { + var err error + + stMap := prepareStateTrieMap(t) + + // This is the cid of the root of the block 0 + // baglacgza274jot5vvr4ntlajtonnkaml5xbm4cts3liye6qxbhndawapavca + currentNode := stMap["baglacgza274jot5vvr4ntlajtonnkaml5xbm4cts3liye6qxbhndawapavca"] + + // This is the path we want to traverse + // The eth address is 0x5abfec25f74cd88437631a7731906932776356f9 + // Its keccak-256 is cdd3e25edec0a536a05f5e5ab90a5603624c0ed77453b2e8f955cf8b43d4d0fb + // We use the keccak-256(addr) to traverse the state trie in ethereum. + var traversePath []string + for _, s := range "cdd3e25edec0a536a05f5e5ab90a5603624c0ed77453b2e8f955cf8b43d4d0fb" { + traversePath = append(traversePath, string(s)) + } + traversePath = append(traversePath, "balance") + + var obj interface{} + for { + obj, traversePath, err = currentNode.Resolve(traversePath) + link, ok := obj.(*node.Link) + if !ok { + break + } + if err != nil { + t.Fatal("Error should be nil") + } + + currentNode = stMap[link.Cid.String()] + if currentNode == nil { + t.Fatal("state trie node not found in memory map") + } + } + + if fmt.Sprintf("%v", obj) != "11901484239480000000000000" { + t.Fatalf("Wrong balance value\r\nexpected %s\r\ngot %s", "11901484239480000000000000", fmt.Sprintf("%v", obj)) + } +} + +func TestStateTrieResolveLinks(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-eb2f5f") + checkError(err, t) + + stNode, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + // bad case + obj, rest, err := stNode.ResolveLink([]string{"supercalifragilist"}) + if obj != nil { + t.Fatalf("Expected obj to be nil") + } + if rest != nil { + t.Fatal("Expected rest to be nil") + } + if err.Error() != "invalid path element" { + t.Fatalf("Wrong error\r\nexpected %s\r\ngot %s", "invalid path element", err.Error()) + } + + // good case + obj, rest, err = stNode.ResolveLink([]string{"d8"}) + if obj == nil { + t.Fatalf("Expected a not nil obj to be returned") + } + if rest != nil { + t.Fatal("Expected rest to be nil") + } + if err != nil { + t.Fatal("Expected error to be nil") + } +} + +func TestStateTrieCopy(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-eb2f5f") + checkError(err, t) + + stNode, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + defer func() { + r := recover() + if r == nil { + t.Fatal("Expected panic") + } + if r != "implement me" { + t.Fatalf("Wrong panic message\r\nexpected %s\r\ngot %s", "'implement me'", r) + } + }() + + _ = stNode.Copy() +} + +func TestStateTrieStat(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-eb2f5f") + checkError(err, t) + + stNode, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + obj, err := stNode.Stat() + if obj == nil { + t.Fatal("Expected a not null object node.NodeStat") + } + + if err != nil { + t.Fatal("Expected a nil error") + } +} + +func TestStateTrieSize(t *testing.T) { + fi, err := os.Open("test_data/eth-state-trie-rlp-eb2f5f") + checkError(err, t) + + stNode, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + size, err := stNode.Size() + if size != uint64(0) { + t.Fatalf("Wrong size\r\nexpected %d\r\ngot %d", 0, size) + } + + if err != nil { + t.Fatal("Expected a nil error") + } +} + +func prepareStateTrieMap(t *testing.T) map[string]*EthStateTrie { + filepaths := []string{ + "test_data/eth-state-trie-rlp-0e8b34", + "test_data/eth-state-trie-rlp-56864f", + "test_data/eth-state-trie-rlp-6fc2d7", + "test_data/eth-state-trie-rlp-727994", + "test_data/eth-state-trie-rlp-c9070d", + "test_data/eth-state-trie-rlp-d5be90", + "test_data/eth-state-trie-rlp-d7f897", + "test_data/eth-state-trie-rlp-eb2f5f", + } + + out := make(map[string]*EthStateTrie) + + for _, fp := range filepaths { + fi, err := os.Open(fp) + checkError(err, t) + + stateTrieNode, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + out[stateTrieNode.Cid().String()] = stateTrieNode + } + + return out +} diff --git a/statediff/indexer/ipfs/ipld/eth_storage.go b/statediff/indexer/ipfs/ipld/eth_storage.go new file mode 100644 index 000000000000..8b4d6234d45c --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_storage.go @@ -0,0 +1,112 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "fmt" + "io" + "io/ioutil" + + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + "github.com/multiformats/go-multihash" +) + +// EthStorageTrie (eth-storage-trie, codec 0x98), represents +// a node from the storage trie in ethereum. +type EthStorageTrie struct { + *TrieNode +} + +// Static (compile time) check that EthStorageTrie satisfies the node.Node interface. +var _ node.Node = (*EthStorageTrie)(nil) + +/* + INPUT +*/ + +// FromStorageTrieRLPFile takes the RLP representation of an ethereum +// storage trie node to return it as an IPLD node for further processing. +func FromStorageTrieRLPFile(r io.Reader) (*EthStorageTrie, error) { + raw, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + return FromStorageTrieRLP(raw) +} + +// FromStorageTrieRLP takes the RLP representation of an ethereum +// storage trie node to return it as an IPLD node for further processing. +func FromStorageTrieRLP(raw []byte) (*EthStorageTrie, error) { + c, err := RawdataToCid(MEthStorageTrie, raw, multihash.KECCAK_256) + if err != nil { + return nil, err + } + + // Let's run the whole mile and process the nodeKind and + // its elements, in case somebody would need this function + // to parse an RLP element from the filesystem + return DecodeEthStorageTrie(c, raw) +} + +/* + OUTPUT +*/ + +// DecodeEthStorageTrie returns an EthStorageTrie object from its cid and rawdata. +func DecodeEthStorageTrie(c cid.Cid, b []byte) (*EthStorageTrie, error) { + tn, err := decodeTrieNode(c, b, decodeEthStorageTrieLeaf) + if err != nil { + return nil, err + } + return &EthStorageTrie{TrieNode: tn}, nil +} + +// decodeEthStorageTrieLeaf parses a eth-tx-trie leaf +// from decoded RLP elements +func decodeEthStorageTrieLeaf(i []interface{}) ([]interface{}, error) { + return []interface{}{ + i[0].([]byte), + i[1].([]byte), + }, nil +} + +/* + Block INTERFACE +*/ + +// RawData returns the binary of the RLP encode of the storage trie node. +func (st *EthStorageTrie) RawData() []byte { + return st.rawdata +} + +// Cid returns the cid of the storage trie node. +func (st *EthStorageTrie) Cid() cid.Cid { + return st.cid +} + +// String is a helper for output +func (st *EthStorageTrie) String() string { + return fmt.Sprintf("", st.cid) +} + +// Loggable returns in a map the type of IPLD Link. +func (st *EthStorageTrie) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-storage-trie", + } +} diff --git a/statediff/indexer/ipfs/ipld/eth_storage_test.go b/statediff/indexer/ipfs/ipld/eth_storage_test.go new file mode 100644 index 000000000000..ac4b38691c21 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_storage_test.go @@ -0,0 +1,140 @@ +package ipld + +import ( + "fmt" + "os" + "testing" + + "github.com/ipfs/go-cid" +) + +/* + INPUT + OUTPUT +*/ + +func TestStorageTrieNodeExtensionParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-storage-trie-rlp-113049") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if output.nodeKind != "extension" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "extension", output.nodeKind) + } + + if len(output.elements) != 2 { + t.Fatalf("Wrong number of elements for an extension node\r\nexpected %d\r\ngot %d", 2, len(output.elements)) + } + + if fmt.Sprintf("%x", output.elements[0]) != "0a" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "0a", fmt.Sprintf("%x", output.elements[0])) + } + + if output.elements[1].(cid.Cid).String() != + "baglacgzautxeutufae7owyrezfvwpan2vusocmxgzwqhzrhjbwprp2texgsq" { + t.Fatalf("Wrong CID\r\nexpected %s\r\ngot %s", "baglacgzautxeutufae7owyrezfvwpan2vusocmxgzwqhzrhjbwprp2texgsq", output.elements[1].(cid.Cid).String()) + } +} + +func TestStateTrieNodeLeafParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-storage-trie-rlp-ffbcad") + checkError(err, t) + + output, err := FromStorageTrieRLPFile(fi) + checkError(err, t) + + if output.nodeKind != "leaf" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "leaf", output.nodeKind) + } + + if len(output.elements) != 2 { + t.Fatalf("Wrong number of elements for an leaf node\r\nexpected %d\r\ngot %d", 2, len(output.elements)) + } + + // 2ee1ae9c502e48e0ed528b7b39ac569cef69d7844b5606841a7f3fe898a2 + if fmt.Sprintf("%x", output.elements[0].([]byte)[:10]) != "020e0e010a0e090c0500" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "020e0e010a0e090c0500", fmt.Sprintf("%x", output.elements[0].([]byte)[:10])) + } + + if fmt.Sprintf("%x", output.elements[1]) != "89056c31f304b2530000" { + t.Fatalf("Wrong Value\r\nexpected %s\r\ngot %s", "89056c31f304b2530000", fmt.Sprintf("%x", output.elements[1])) + } +} + +func TestStateTrieNodeBranchParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-storage-trie-rlp-ffc25c") + checkError(err, t) + + output, err := FromStateTrieRLPFile(fi) + checkError(err, t) + + if output.nodeKind != "branch" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "branch", output.nodeKind) + } + + if len(output.elements) != 17 { + t.Fatalf("Wrong number of elements for an branch node\r\nexpected %d\r\ngot %d", 17, len(output.elements)) + } + + if fmt.Sprintf("%s", output.elements[4]) != + "baglacgzadqhbmlxrxtw5hplcq5jn74p4dceryzw664w3237ra52dnghbjpva" { + t.Fatalf("Wrong Cid\r\nexpected %s\r\ngot %s", "baglacgzadqhbmlxrxtw5hplcq5jn74p4dceryzw664w3237ra52dnghbjpva", fmt.Sprintf("%s", output.elements[4])) + } + + if fmt.Sprintf("%s", output.elements[10]) != + "baglacgza77d37i2v6uhtzeeq4vngragjbgbwq3lylpoc3lihenvzimybzxmq" { + t.Fatalf("Wrong Cid\r\nexpected %s\r\ngot %s", "baglacgza77d37i2v6uhtzeeq4vngragjbgbwq3lylpoc3lihenvzimybzxmq", fmt.Sprintf("%s", output.elements[10])) + } +} + +/* + Block INTERFACE +*/ +func TestStorageTrieBlockElements(t *testing.T) { + fi, err := os.Open("test_data/eth-storage-trie-rlp-ffbcad") + checkError(err, t) + + output, err := FromStorageTrieRLPFile(fi) + checkError(err, t) + + if fmt.Sprintf("%x", output.RawData())[:10] != "eb9f202ee1" { + t.Fatalf("Wrong Data\r\nexpected %s\r\ngot %s", "eb9f202ee1", fmt.Sprintf("%x", output.RawData())[:10]) + } + + if output.Cid().String() != + "bagmacgza766k3oprj2qxn36eycw55pogmu3dwtfay6zdh6ajrhvw3b2nqg5a" { + t.Fatalf("Wrong Cid\r\nexpected %s\r\ngot %s", "bagmacgza766k3oprj2qxn36eycw55pogmu3dwtfay6zdh6ajrhvw3b2nqg5a", output.Cid().String()) + } +} + +func TestStorageTrieString(t *testing.T) { + fi, err := os.Open("test_data/eth-storage-trie-rlp-ffbcad") + checkError(err, t) + + output, err := FromStorageTrieRLPFile(fi) + checkError(err, t) + + if output.String() != + "" { + t.Fatalf("Wrong String()\r\nexpected %s\r\ngot %s", "", output.String()) + } +} + +func TestStorageTrieLoggable(t *testing.T) { + fi, err := os.Open("test_data/eth-storage-trie-rlp-ffbcad") + checkError(err, t) + + output, err := FromStorageTrieRLPFile(fi) + checkError(err, t) + + l := output.Loggable() + if _, ok := l["type"]; !ok { + t.Fatal("Loggable map expected the field 'type'") + } + + if l["type"] != "eth-storage-trie" { + t.Fatalf("Wrong Loggable 'type' value\r\nexpected %s\r\ngot %s", "eth-storage-trie", l["type"]) + } +} diff --git a/statediff/indexer/ipfs/ipld/eth_tx.go b/statediff/indexer/ipfs/ipld/eth_tx.go new file mode 100644 index 000000000000..deb6158bb68c --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_tx.go @@ -0,0 +1,236 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + mh "github.com/multiformats/go-multihash" +) + +// EthTx (eth-tx codec 0x93) represents an ethereum transaction +type EthTx struct { + *types.Transaction + + cid cid.Cid + rawdata []byte +} + +// Static (compile time) check that EthTx satisfies the node.Node interface. +var _ node.Node = (*EthTx)(nil) + +/* + INPUT +*/ + +// NewEthTx converts a *types.Transaction to an EthTx IPLD node +func NewEthTx(tx *types.Transaction) (*EthTx, error) { + txRaw, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + c, err := RawdataToCid(MEthTx, txRaw, mh.KECCAK_256) + if err != nil { + return nil, err + } + return &EthTx{ + Transaction: tx, + cid: c, + rawdata: txRaw, + }, nil +} + +/* + OUTPUT +*/ + +// DecodeEthTx takes a cid and its raw binary data +// from IPFS and returns an EthTx object for further processing. +func DecodeEthTx(c cid.Cid, b []byte) (*EthTx, error) { + t := new(types.Transaction) + if err := t.UnmarshalBinary(b); err != nil { + return nil, err + } + return &EthTx{ + Transaction: t, + cid: c, + rawdata: b, + }, nil +} + +/* + Block INTERFACE +*/ + +// RawData returns the binary of the RLP encode of the transaction. +func (t *EthTx) RawData() []byte { + return t.rawdata +} + +// Cid returns the cid of the transaction. +func (t *EthTx) Cid() cid.Cid { + return t.cid +} + +// String is a helper for output +func (t *EthTx) String() string { + return fmt.Sprintf("", t.cid) +} + +// Loggable returns in a map the type of IPLD Link. +func (t *EthTx) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-tx", + } +} + +/* + Node INTERFACE +*/ + +// Resolve resolves a path through this node, stopping at any link boundary +// and returning the object found as well as the remaining path to traverse +func (t *EthTx) Resolve(p []string) (interface{}, []string, error) { + if len(p) == 0 { + return t, nil, nil + } + + if len(p) > 1 { + return nil, nil, fmt.Errorf("unexpected path elements past %s", p[0]) + } + + switch p[0] { + + case "gas": + return t.Gas(), nil, nil + case "gasPrice": + return t.GasPrice(), nil, nil + case "input": + return fmt.Sprintf("%x", t.Data()), nil, nil + case "nonce": + return t.Nonce(), nil, nil + case "r": + _, r, _ := t.RawSignatureValues() + return hexutil.EncodeBig(r), nil, nil + case "s": + _, _, s := t.RawSignatureValues() + return hexutil.EncodeBig(s), nil, nil + case "toAddress": + return t.To(), nil, nil + case "v": + v, _, _ := t.RawSignatureValues() + return hexutil.EncodeBig(v), nil, nil + case "value": + return hexutil.EncodeBig(t.Value()), nil, nil + default: + return nil, nil, ErrInvalidLink + } +} + +// Tree lists all paths within the object under 'path', and up to the given depth. +// To list the entire object (similar to `find .`) pass "" and -1 +func (t *EthTx) Tree(p string, depth int) []string { + if p != "" || depth == 0 { + return nil + } + return []string{"gas", "gasPrice", "input", "nonce", "r", "s", "toAddress", "v", "value"} +} + +// ResolveLink is a helper function that calls resolve and asserts the +// output is a link +func (t *EthTx) ResolveLink(p []string) (*node.Link, []string, error) { + obj, rest, err := t.Resolve(p) + if err != nil { + return nil, nil, err + } + + if lnk, ok := obj.(*node.Link); ok { + return lnk, rest, nil + } + + return nil, nil, fmt.Errorf("resolved item was not a link") +} + +// Copy will go away. It is here to comply with the interface. +func (t *EthTx) Copy() node.Node { + panic("implement me") +} + +// Links is a helper function that returns all links within this object +func (t *EthTx) Links() []*node.Link { + return nil +} + +// Stat will go away. It is here to comply with the interface. +func (t *EthTx) Stat() (*node.NodeStat, error) { + return &node.NodeStat{}, nil +} + +// Size will go away. It is here to comply with the interface. It returns the byte size for the transaction +func (t *EthTx) Size() (uint64, error) { + spl := strings.Split(t.Transaction.Size().String(), " ") + size, units := spl[0], spl[1] + floatSize, err := strconv.ParseFloat(size, 64) + if err != nil { + return 0, err + } + var byteSize uint64 + switch units { + case "B": + byteSize = uint64(floatSize) + case "KB": + byteSize = uint64(floatSize * 1000) + case "MB": + byteSize = uint64(floatSize * 1000000) + case "GB": + byteSize = uint64(floatSize * 1000000000) + case "TB": + byteSize = uint64(floatSize * 1000000000000) + default: + return 0, fmt.Errorf("unreconginized units %s", units) + } + return byteSize, nil +} + +/* + EthTx functions +*/ + +// MarshalJSON processes the transaction into readable JSON format. +func (t *EthTx) MarshalJSON() ([]byte, error) { + v, r, s := t.RawSignatureValues() + + out := map[string]interface{}{ + "gas": t.Gas(), + "gasPrice": hexutil.EncodeBig(t.GasPrice()), + "input": fmt.Sprintf("%x", t.Data()), + "nonce": t.Nonce(), + "r": hexutil.EncodeBig(r), + "s": hexutil.EncodeBig(s), + "toAddress": t.To(), + "v": hexutil.EncodeBig(v), + "value": hexutil.EncodeBig(t.Value()), + } + return json.Marshal(out) +} diff --git a/statediff/indexer/ipfs/ipld/eth_tx_test.go b/statediff/indexer/ipfs/ipld/eth_tx_test.go new file mode 100644 index 000000000000..5211cfc3d57a --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_tx_test.go @@ -0,0 +1,411 @@ +package ipld + +import ( + "encoding/hex" + "fmt" + "os" + "strconv" + "strings" + "testing" + + block "github.com/ipfs/go-block-format" + "github.com/multiformats/go-multihash" +) + +/* + EthBlock + INPUT +*/ + +func TestTxInBlockBodyRlpParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-block-body-rlp-999999") + checkError(err, t) + + _, output, _, err := FromBlockRLP(fi) + checkError(err, t) + + if len(output) != 11 { + t.Fatalf("Wrong number of parsed txs\r\nexpected %d\r\ngot %d", 11, len(output)) + } + + // Oh, let's just grab the last element and one from the middle + testTx05Fields(output[5], t) + testTx10Fields(output[10], t) +} + +func TestTxInBlockHeaderRlpParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-block-header-rlp-999999") + checkError(err, t) + + _, output, _, err := FromBlockRLP(fi) + checkError(err, t) + + if len(output) != 0 { + t.Fatalf("Wrong number of txs\r\nexpected %d\r\ngot %d", 0, len(output)) + } +} + +func TestTxInBlockBodyJsonParsing(t *testing.T) { + fi, err := os.Open("test_data/eth-block-body-json-999999") + checkError(err, t) + + _, output, _, err := FromBlockJSON(fi) + checkError(err, t) + + if len(output) != 11 { + t.Fatalf("Wrong number of parsed txs\r\nexpected %d\r\ngot %d", 11, len(output)) + } + + testTx05Fields(output[5], t) + testTx10Fields(output[10], t) +} + +/* + OUTPUT +*/ + +func TestDecodeTransaction(t *testing.T) { + // Prepare the "fetched transaction". + // This one is supposed to be in the datastore already, + // and given away by github.com/ipfs/go-ipfs/merkledag + rawTransactionString := + "f86c34850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f25" + + "8512af0d4000801ba0e9a25c929c26d1a95232ba75aef419a91b470651eb77614695e16c" + + "5ba023e383a0679fb2fc0d0b0f3549967c0894ee7d947f07d238a83ef745bc3ced5143a4af36" + rawTransaction, err := hex.DecodeString(rawTransactionString) + checkError(err, t) + c, err := RawdataToCid(MEthTx, rawTransaction, multihash.KECCAK_256) + checkError(err, t) + + // Just to clarify: This `block` is an IPFS block + storedTransaction, err := block.NewBlockWithCid(rawTransaction, c) + checkError(err, t) + + // Now the proper test + ethTransaction, err := DecodeEthTx(storedTransaction.Cid(), storedTransaction.RawData()) + checkError(err, t) + + testTx05Fields(ethTransaction, t) +} + +/* + Block INTERFACE +*/ + +func TestEthTxLoggable(t *testing.T) { + txs := prepareParsedTxs(t) + + l := txs[0].Loggable() + if _, ok := l["type"]; !ok { + t.Fatal("Loggable map expected the field 'type'") + } + + if l["type"] != "eth-tx" { + t.Fatalf("Wrong Loggable 'type' value\r\nexpected %s\r\ngot %s", "eth-tx", l["type"]) + } +} + +/* + Node INTERFACE +*/ + +func TestEthTxResolve(t *testing.T) { + tx := prepareParsedTxs(t)[0] + + // Empty path + obj, rest, err := tx.Resolve([]string{}) + rtx, ok := obj.(*EthTx) + if !ok { + t.Fatal("Wrong type of returned object") + } + if rtx.Cid() != tx.Cid() { + t.Fatalf("Wrong CID\r\nexpected %s\r\ngot %s", tx.Cid().String(), rtx.Cid().String()) + } + if rest != nil { + t.Fatal("est should be nil") + } + if err != nil { + t.Fatal("err should be nil") + } + + // len(p) > 1 + badCases := [][]string{ + {"two", "elements"}, + {"here", "three", "elements"}, + {"and", "here", "four", "elements"}, + } + + for _, bc := range badCases { + obj, rest, err = tx.Resolve(bc) + if obj != nil { + t.Fatal("obj should be nil") + } + if rest != nil { + t.Fatal("rest should be nil") + } + if err.Error() != fmt.Sprintf("unexpected path elements past %s", bc[0]) { + t.Fatalf("wrong error\r\nexpected %s\r\ngot %s", fmt.Sprintf("unexpected path elements past %s", bc[0]), err.Error()) + } + } + + moreBadCases := []string{ + "i", + "am", + "not", + "a", + "tx", + "field", + } + for _, mbc := range moreBadCases { + obj, rest, err = tx.Resolve([]string{mbc}) + if obj != nil { + t.Fatal("obj should be nil") + } + if rest != nil { + t.Fatal("rest should be nil") + } + + if err != ErrInvalidLink { + t.Fatalf("wrong error\r\nexpected %s\r\ngot %s", ErrInvalidLink, err) + } + } + + goodCases := []string{ + "gas", + "gasPrice", + "input", + "nonce", + "r", + "s", + "toAddress", + "v", + "value", + } + for _, gc := range goodCases { + _, _, err = tx.Resolve([]string{gc}) + if err != nil { + t.Fatalf("error should be nil %v", gc) + } + } + +} + +func TestEthTxTree(t *testing.T) { + tx := prepareParsedTxs(t)[0] + _ = tx + + // Bad cases + tree := tx.Tree("non-empty-string", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = tx.Tree("non-empty-string", 1) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = tx.Tree("", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + // Good cases + tree = tx.Tree("", 1) + lookupElements := map[string]interface{}{ + "gas": nil, + "gasPrice": nil, + "input": nil, + "nonce": nil, + "r": nil, + "s": nil, + "toAddress": nil, + "v": nil, + "value": nil, + } + + if len(tree) != len(lookupElements) { + t.Fatalf("Wrong number of elements\r\nexpected %d\r\ngot %d", len(lookupElements), len(tree)) + } + + for _, te := range tree { + if _, ok := lookupElements[te]; !ok { + t.Fatalf("Unexpected Element: %v", te) + } + } +} + +func TestEthTxResolveLink(t *testing.T) { + tx := prepareParsedTxs(t)[0] + + // bad case + obj, rest, err := tx.ResolveLink([]string{"supercalifragilist"}) + if obj != nil { + t.Fatalf("Expected obj to be nil") + } + if rest != nil { + t.Fatal("Expected rest to be nil") + } + if err != ErrInvalidLink { + t.Fatalf("Wrong error\r\nexpected %s\r\ngot %s", ErrInvalidLink, err.Error()) + } + + // good case + obj, rest, err = tx.ResolveLink([]string{"nonce"}) + if obj != nil { + t.Fatalf("Expected obj to be nil") + } + if rest != nil { + t.Fatal("Expected rest to be nil") + } + if err.Error() != "resolved item was not a link" { + t.Fatalf("Wrong error\r\nexpected %s\r\ngot %s", "resolved item was not a link", err.Error()) + } +} + +func TestEthTxCopy(t *testing.T) { + tx := prepareParsedTxs(t)[0] + + defer func() { + r := recover() + if r == nil { + t.Fatal("Expected panic") + } + if r != "implement me" { + t.Fatalf("Wrong panic message\r\nexpected %s\r\ngot %s", "'implement me'", r) + } + }() + + _ = tx.Copy() +} + +func TestEthTxLinks(t *testing.T) { + tx := prepareParsedTxs(t)[0] + + if tx.Links() != nil { + t.Fatal("Links() expected to return nil") + } +} + +func TestEthTxStat(t *testing.T) { + tx := prepareParsedTxs(t)[0] + + obj, err := tx.Stat() + if obj == nil { + t.Fatal("Expected a not null object node.NodeStat") + } + + if err != nil { + t.Fatal("Expected a nil error") + } +} + +func TestEthTxSize(t *testing.T) { + tx := prepareParsedTxs(t)[0] + + size, err := tx.Size() + checkError(err, t) + + spl := strings.Split(tx.Transaction.Size().String(), " ") + expectedSize, units := spl[0], spl[1] + floatSize, err := strconv.ParseFloat(expectedSize, 64) + checkError(err, t) + + var byteSize uint64 + switch units { + case "B": + byteSize = uint64(floatSize) + case "KB": + byteSize = uint64(floatSize * 1000) + case "MB": + byteSize = uint64(floatSize * 1000000) + case "GB": + byteSize = uint64(floatSize * 1000000000) + case "TB": + byteSize = uint64(floatSize * 1000000000000) + default: + t.Fatal("Unexpected size units") + } + if size != byteSize { + t.Fatalf("Wrong size\r\nexpected %d\r\ngot %d", byteSize, size) + } +} + +/* + AUXILIARS +*/ + +// prepareParsedTxs is a convenienve method +func prepareParsedTxs(t *testing.T) []*EthTx { + fi, err := os.Open("test_data/eth-block-body-rlp-999999") + checkError(err, t) + + _, output, _, err := FromBlockRLP(fi) + checkError(err, t) + + return output +} + +func testTx05Fields(ethTx *EthTx, t *testing.T) { + // Was the cid calculated? + if ethTx.Cid().String() != "bagjqcgzawhfnvdnpmpcfoug7d3tz53k2ht3cidr45pnw3y7snpd46azbpp2a" { + t.Fatalf("Wrong cid\r\nexpected %s\r\ngot %s\r\n", "bagjqcgzawhfnvdnpmpcfoug7d3tz53k2ht3cidr45pnw3y7snpd46azbpp2a", ethTx.Cid().String()) + } + + // Do we have the rawdata available? + if fmt.Sprintf("%x", ethTx.RawData()[:10]) != "f86c34850df847580082" { + t.Fatalf("Wrong Rawdata\r\nexpected %s\r\ngot %s", "f86c34850df847580082", fmt.Sprintf("%x", ethTx.RawData()[:10])) + } + + // Proper Fields of types.Transaction + if fmt.Sprintf("%x", ethTx.To()) != "32be343b94f860124dc4fee278fdcbd38c102d88" { + t.Fatalf("Wrong Recipient\r\nexpected %s\r\ngot %s", "32be343b94f860124dc4fee278fdcbd38c102d88", fmt.Sprintf("%x", ethTx.To())) + } + if len(ethTx.Data()) != 0 { + t.Fatalf("Wrong len of Data\r\nexpected %d\r\ngot %d", 0, len(ethTx.Data())) + } + if fmt.Sprintf("%v", ethTx.Gas()) != "21000" { + t.Fatalf("Wrong Gas\r\nexpected %s\r\ngot %s", "21000", fmt.Sprintf("%v", ethTx.Gas())) + } + if fmt.Sprintf("%v", ethTx.Value()) != "1091424800000000000" { + t.Fatalf("Wrong Value\r\nexpected %s\r\ngot %s", "1091424800000000000", fmt.Sprintf("%v", ethTx.Value())) + } + if fmt.Sprintf("%v", ethTx.Nonce()) != "52" { + t.Fatalf("Wrong Nonce\r\nexpected %s\r\ngot %s", "52", fmt.Sprintf("%v", ethTx.Nonce())) + } + if fmt.Sprintf("%v", ethTx.GasPrice()) != "60000000000" { + t.Fatalf("Wrong Gas Price\r\nexpected %s\r\ngot %s", "60000000000", fmt.Sprintf("%v", ethTx.GasPrice())) + } +} + +func testTx10Fields(ethTx *EthTx, t *testing.T) { + // Was the cid calculated? + if ethTx.Cid().String() != "bagjqcgzaykakwayoec6j55zmq62cbvmplgf5u5j67affge3ksi4ermgitjoa" { + t.Fatalf("Wrong Cid\r\nexpected %s\r\ngot %s", "bagjqcgzaykakwayoec6j55zmq62cbvmplgf5u5j67affge3ksi4ermgitjoa", ethTx.Cid().String()) + } + + // Do we have the rawdata available? + if fmt.Sprintf("%x", ethTx.RawData()[:10]) != "f8708302a120850ba43b" { + t.Fatalf("Wrong Rawdata\r\nexpected %s\r\ngot %s", "f8708302a120850ba43b", fmt.Sprintf("%x", ethTx.RawData()[:10])) + } + + // Proper Fields of types.Transaction + if fmt.Sprintf("%x", ethTx.To()) != "1c51bf013add0857c5d9cf2f71a7f15ca93d4816" { + t.Fatalf("Wrong Recipient\r\nexpected %s\r\ngot %s", "1c51bf013add0857c5d9cf2f71a7f15ca93d4816", fmt.Sprintf("%x", ethTx.To())) + } + if len(ethTx.Data()) != 0 { + t.Fatalf("Wrong len of Data\r\nexpected %d\r\ngot %d", 0, len(ethTx.Data())) + } + if fmt.Sprintf("%v", ethTx.Gas()) != "90000" { + t.Fatalf("Wrong Gas\r\nexpected %s\r\ngot %s", "90000", fmt.Sprintf("%v", ethTx.Gas())) + } + if fmt.Sprintf("%v", ethTx.Value()) != "1049756850000000000" { + t.Fatalf("Wrong Value\r\nexpected %s\r\ngot %s", "1049756850000000000", fmt.Sprintf("%v", ethTx.Value())) + } + if fmt.Sprintf("%v", ethTx.Nonce()) != "172320" { + t.Fatalf("Wrong Nonce\r\nexpected %s\r\ngot %s", "172320", fmt.Sprintf("%v", ethTx.Nonce())) + } + if fmt.Sprintf("%v", ethTx.GasPrice()) != "50000000000" { + t.Fatalf("Wrong Gas Price\r\nexpected %s\r\ngot %s", "50000000000", fmt.Sprintf("%v", ethTx.GasPrice())) + } +} diff --git a/statediff/indexer/ipfs/ipld/eth_tx_trie.go b/statediff/indexer/ipfs/ipld/eth_tx_trie.go new file mode 100644 index 000000000000..7e79ff1648d0 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_tx_trie.go @@ -0,0 +1,150 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "fmt" + + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + "github.com/multiformats/go-multihash" + + "github.com/ethereum/go-ethereum/core/types" +) + +// EthTxTrie (eth-tx-trie codec 0x92) represents +// a node from the transaction trie in ethereum. +type EthTxTrie struct { + *TrieNode +} + +// Static (compile time) check that EthTxTrie satisfies the node.Node interface. +var _ node.Node = (*EthTxTrie)(nil) + +/* + INPUT +*/ + +// To create a proper trie of the eth-tx-trie objects, it is required +// to input all transactions belonging to a forest in a single step. +// We are adding the transactions, and creating its trie on +// block body parsing time. + +/* + OUTPUT +*/ + +// DecodeEthTxTrie returns an EthTxTrie object from its cid and rawdata. +func DecodeEthTxTrie(c cid.Cid, b []byte) (*EthTxTrie, error) { + tn, err := decodeTrieNode(c, b, decodeEthTxTrieLeaf) + if err != nil { + return nil, err + } + return &EthTxTrie{TrieNode: tn}, nil +} + +// decodeEthTxTrieLeaf parses a eth-tx-trie leaf +//from decoded RLP elements +func decodeEthTxTrieLeaf(i []interface{}) ([]interface{}, error) { + t := new(types.Transaction) + if err := t.UnmarshalBinary(i[1].([]byte)); err != nil { + return nil, err + } + c, err := RawdataToCid(MEthTx, i[1].([]byte), multihash.KECCAK_256) + if err != nil { + return nil, err + } + return []interface{}{ + i[0].([]byte), + &EthTx{ + Transaction: t, + cid: c, + rawdata: i[1].([]byte), + }, + }, nil +} + +/* + Block INTERFACE +*/ + +// RawData returns the binary of the RLP encode of the transaction. +func (t *EthTxTrie) RawData() []byte { + return t.rawdata +} + +// Cid returns the cid of the transaction. +func (t *EthTxTrie) Cid() cid.Cid { + return t.cid +} + +// String is a helper for output +func (t *EthTxTrie) String() string { + return fmt.Sprintf("", t.cid) +} + +// Loggable returns in a map the type of IPLD Link. +func (t *EthTxTrie) Loggable() map[string]interface{} { + return map[string]interface{}{ + "type": "eth-tx-trie", + } +} + +/* + EthTxTrie functions +*/ + +// txTrie wraps a localTrie for use on the transaction trie. +type txTrie struct { + *localTrie +} + +// newTxTrie initializes and returns a txTrie. +func newTxTrie() *txTrie { + return &txTrie{ + localTrie: newLocalTrie(), + } +} + +// getNodes invokes the localTrie, which computes the root hash of the +// transaction trie and returns its database keys, to return a slice +// of EthTxTrie nodes. +func (tt *txTrie) getNodes() ([]*EthTxTrie, error) { + keys, err := tt.getKeys() + if err != nil { + return nil, err + } + var out []*EthTxTrie + + for _, k := range keys { + rawdata, err := tt.db.Get(k) + if err != nil { + return nil, err + } + c, err := RawdataToCid(MEthTxTrie, rawdata, multihash.KECCAK_256) + if err != nil { + return nil, err + } + tn := &TrieNode{ + cid: c, + rawdata: rawdata, + } + out = append(out, &EthTxTrie{TrieNode: tn}) + } + + return out, nil +} diff --git a/statediff/indexer/ipfs/ipld/eth_tx_trie_test.go b/statediff/indexer/ipfs/ipld/eth_tx_trie_test.go new file mode 100644 index 000000000000..42637ceb41bd --- /dev/null +++ b/statediff/indexer/ipfs/ipld/eth_tx_trie_test.go @@ -0,0 +1,504 @@ +package ipld + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "os" + "testing" + + block "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" + "github.com/multiformats/go-multihash" +) + +/* + EthBlock +*/ + +func TestTxTriesInBlockBodyJSONParsing(t *testing.T) { + // HINT: 306 txs + // cat test_data/eth-block-body-json-4139497 | jsontool | grep transactionIndex | wc -l + // or, https://etherscan.io/block/4139497 + fi, err := os.Open("test_data/eth-block-body-json-4139497") + checkError(err, t) + + _, _, output, err := FromBlockJSON(fi) + checkError(err, t) + if len(output) != 331 { + t.Fatalf("Wrong number of obtained tx trie nodes\r\nexpected %d\r\n got %d", 331, len(output)) + } +} + +/* + OUTPUT +*/ + +func TestTxTrieDecodeExtension(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieExtension(t) + + if ethTxTrie.nodeKind != "extension" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "extension", ethTxTrie.nodeKind) + } + + if len(ethTxTrie.elements) != 2 { + t.Fatalf("Wrong number of elements for an extension node\r\nexpected %d\r\ngot %d", 2, len(ethTxTrie.elements)) + } + + if fmt.Sprintf("%x", ethTxTrie.elements[0].([]byte)) != "0001" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "0001", fmt.Sprintf("%x", ethTxTrie.elements[0].([]byte))) + } + + if ethTxTrie.elements[1].(cid.Cid).String() != + "bagjacgzak6wdjvshdtb7lrvlteweyd7f5qjr3dmzmh7g2xpi4xrwoujsio2a" { + t.Fatalf("Wrong CID\r\nexpected %s\r\ngot %s", "bagjacgzak6wdjvshdtb7lrvlteweyd7f5qjr3dmzmh7g2xpi4xrwoujsio2a", ethTxTrie.elements[1].(cid.Cid).String()) + } +} + +func TestTxTrieDecodeLeaf(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieLeaf(t) + + if ethTxTrie.nodeKind != "leaf" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "leaf", ethTxTrie.nodeKind) + } + + if len(ethTxTrie.elements) != 2 { + t.Fatalf("Wrong number of elements for a leaf node\r\nexpected %d\r\ngot %d", 2, len(ethTxTrie.elements)) + } + + if fmt.Sprintf("%x", ethTxTrie.elements[0].([]byte)) != "" { + t.Fatalf("Wrong key\r\nexpected %s\r\ngot %s", "", fmt.Sprintf("%x", ethTxTrie.elements[0].([]byte))) + } + + if _, ok := ethTxTrie.elements[1].(*EthTx); !ok { + t.Fatal("Expected element to be an EthTx") + } + + if ethTxTrie.elements[1].(*EthTx).String() != + "" { + t.Fatalf("Wrong String()\r\nexpected %s\r\ngot %s", "", ethTxTrie.elements[1].(*EthTx).String()) + } +} + +func TestTxTrieDecodeBranch(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieBranch(t) + + if ethTxTrie.nodeKind != "branch" { + t.Fatalf("Wrong nodeKind\r\nexpected %s\r\ngot %s", "branch", ethTxTrie.nodeKind) + } + + if len(ethTxTrie.elements) != 17 { + t.Fatalf("Wrong number of elements for a branch node\r\nexpected %d\r\ngot %d", 17, len(ethTxTrie.elements)) + } + + for i, element := range ethTxTrie.elements { + switch { + case i < 9: + if _, ok := element.(cid.Cid); !ok { + t.Fatal("Expected element to be a cid") + } + continue + default: + if element != nil { + t.Fatal("Expected element to be a nil") + } + } + } +} + +/* + Block INTERFACE +*/ + +func TestEthTxTrieBlockElements(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieExtension(t) + + if fmt.Sprintf("%x", ethTxTrie.RawData())[:10] != "e4820001a0" { + t.Fatalf("Wrong Data\r\nexpected %s\r\ngot %s", "e4820001a0", fmt.Sprintf("%x", ethTxTrie.RawData())[:10]) + } + + if ethTxTrie.Cid().String() != + "bagjacgzaw6ccgrfc3qnrl6joodbjjiet4haufnt2xww725luwgfhijnmg36q" { + t.Fatalf("Wrong Cid\r\nexpected %s\r\ngot %s", "bagjacgzaw6ccgrfc3qnrl6joodbjjiet4haufnt2xww725luwgfhijnmg36q", ethTxTrie.Cid().String()) + } +} + +func TestEthTxTrieString(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieExtension(t) + + if ethTxTrie.String() != "" { + t.Fatalf("Wrong String()\r\nexpected %s\r\ngot %s", "", ethTxTrie.String()) + } +} + +func TestEthTxTrieLoggable(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieExtension(t) + l := ethTxTrie.Loggable() + if _, ok := l["type"]; !ok { + t.Fatal("Loggable map expected the field 'type'") + } + + if l["type"] != "eth-tx-trie" { + t.Fatalf("Wrong Loggable 'type' value\r\nexpected %s\r\ngot %s", "eth-tx-trie", l["type"]) + } +} + +/* + Node INTERFACE +*/ + +func TestTxTrieResolveExtension(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieExtension(t) + + _ = ethTxTrie +} + +func TestTxTrieResolveLeaf(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieLeaf(t) + + _ = ethTxTrie +} + +func TestTxTrieResolveBranch(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieBranch(t) + + indexes := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"} + + for j, index := range indexes { + obj, rest, err := ethTxTrie.Resolve([]string{index, "nonce"}) + + switch { + case j < 9: + _, ok := obj.(*node.Link) + if !ok { + t.Fatalf("Returned object is not a link (index: %d)", j) + } + + if rest[0] != "nonce" { + t.Fatalf("Wrong rest of the path returned\r\nexpected %s\r\ngot %s", "nonce", rest[0]) + } + + if err != nil { + t.Fatal("Error should be nil") + } + + default: + if obj != nil { + t.Fatalf("Returned object should have been nil") + } + + if rest != nil { + t.Fatalf("Rest of the path returned should be nil") + } + + if err.Error() != "no such link in this branch" { + t.Fatalf("Wrong error") + } + } + } + + otherSuccessCases := [][]string{ + {"0", "1", "banana"}, + {"1", "banana"}, + {"7bc", "def"}, + {"bc", "def"}, + } + + for i := 0; i < len(otherSuccessCases); i = i + 2 { + osc := otherSuccessCases[i] + expectedRest := otherSuccessCases[i+1] + + obj, rest, err := ethTxTrie.Resolve(osc) + _, ok := obj.(*node.Link) + if !ok { + t.Fatalf("Returned object is not a link") + } + + for j := range expectedRest { + if rest[j] != expectedRest[j] { + t.Fatalf("Wrong rest of the path returned\r\nexpected %s\r\ngot %s", expectedRest[j], rest[j]) + } + } + + if err != nil { + t.Fatal("Error should be nil") + } + + } +} + +func TestTraverseTxTrieWithResolve(t *testing.T) { + var err error + + txMap := prepareTxTrieMap(t) + + // This is the cid of the tx root at the block 4,139,497 + currentNode := txMap["bagjacgzaqolvvlyflkdiylijcu4ts6myxczkb2y3ewxmln5oyrsrkfc4v7ua"] + + // This is the path we want to traverse + // the transaction id 256, which is RLP encoded to 820100 + var traversePath []string + for _, s := range "820100" { + traversePath = append(traversePath, string(s)) + } + traversePath = append(traversePath, "value") + + var obj interface{} + for { + obj, traversePath, err = currentNode.Resolve(traversePath) + link, ok := obj.(*node.Link) + if !ok { + break + } + if err != nil { + t.Fatal("Error should be nil") + } + + currentNode = txMap[link.Cid.String()] + if currentNode == nil { + t.Fatal("transaction trie node not found in memory map") + } + } + + if fmt.Sprintf("%v", obj) != "0xc495a958603400" { + t.Fatalf("Wrong value\r\nexpected %s\r\ngot %s", "0xc495a958603400", fmt.Sprintf("%v", obj)) + } +} + +func TestTxTrieTreeBadParams(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieBranch(t) + + tree := ethTxTrie.Tree("non-empty-string", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = ethTxTrie.Tree("non-empty-string", 1) + if tree != nil { + t.Fatal("Expected nil to be returned") + } + + tree = ethTxTrie.Tree("", 0) + if tree != nil { + t.Fatal("Expected nil to be returned") + } +} + +func TestTxTrieTreeExtension(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieExtension(t) + + tree := ethTxTrie.Tree("", -1) + + if len(tree) != 1 { + t.Fatalf("An extension should have one element") + } + + if tree[0] != "01" { + t.Fatalf("Wrong trie element\r\nexpected %s\r\ngot %s", "01", tree[0]) + } +} + +func TestTxTrieTreeBranch(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieBranch(t) + + tree := ethTxTrie.Tree("", -1) + + lookupElements := map[string]interface{}{ + "0": nil, + "1": nil, + "2": nil, + "3": nil, + "4": nil, + "5": nil, + "6": nil, + "7": nil, + "8": nil, + } + + if len(tree) != len(lookupElements) { + t.Fatalf("Wrong number of elements\r\nexpected %d\r\ngot %d", len(lookupElements), len(tree)) + } + + for _, te := range tree { + if _, ok := lookupElements[te]; !ok { + t.Fatalf("Unexpected Element: %v", te) + } + } +} + +func TestTxTrieLinksBranch(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieBranch(t) + + desiredValues := []string{ + "bagjacgzakhtcfpja453ydiaqxgidqmxhh7jwmxujib663deebwfs3m2n3hoa", + "bagjacgza2p2fuqh4vumknq6x5w7i47usvtu5ixqins6qjjtcks4zge3vx3qq", + "bagjacgza4fkhn7et3ra66yjkzbtvbxjefuketda6jctlut6it7gfahxhywga", + "bagjacgzacnryeybs52xryrka5uxi4eg4hi2mh66esaghu7cetzu6fsukrynq", + "bagjacgzastu5tc7lwz4ap3gznjwkyyepswquub7gvhags5mgdyfynnwbi43a", + "bagjacgza5qgp76ovvorkydni2lchew6ieu5wb55w6hdliiu6vft7zlxtdhjq", + "bagjacgzafnssc4yvln6zxmks5roskw4ckngta5n4yfy2skhlu435ve4b575a", + "bagjacgzagkuei7qxfxefufme2d3xizxokkq4ad3rzl2x4dq2uao6dcr4va2a", + "bagjacgzaxpaehtananrdxjghwukh2wwkkzcqwveppf6xclkrtd26rm27kqwq", + } + + links := ethTxTrie.Links() + + for i, v := range desiredValues { + if links[i].Cid.String() != v { + t.Fatalf("Wrong cid for link %d\r\nexpected %s\r\ngot %s", i, v, links[i].Cid.String()) + } + } +} + +/* + EthTxTrie Functions +*/ + +func TestTxTrieJSONMarshalExtension(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieExtension(t) + + jsonOutput, err := ethTxTrie.MarshalJSON() + checkError(err, t) + + var data map[string]interface{} + err = json.Unmarshal(jsonOutput, &data) + checkError(err, t) + + if parseMapElement(data["01"]) != + "bagjacgzak6wdjvshdtb7lrvlteweyd7f5qjr3dmzmh7g2xpi4xrwoujsio2a" { + t.Fatalf("Wrong Marshaled Value\r\nexpected %s\r\ngot %s", "bagjacgzak6wdjvshdtb7lrvlteweyd7f5qjr3dmzmh7g2xpi4xrwoujsio2a", parseMapElement(data["01"])) + } + + if data["type"] != "extension" { + t.Fatalf("Wrong node type\r\nexpected %s\r\ngot %s", "extension", data["type"]) + } +} + +func TestTxTrieJSONMarshalLeaf(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieLeaf(t) + + jsonOutput, err := ethTxTrie.MarshalJSON() + checkError(err, t) + + var data map[string]interface{} + err = json.Unmarshal(jsonOutput, &data) + checkError(err, t) + + if data["type"] != "leaf" { + t.Fatalf("Wrong node type\r\nexpected %s\r\ngot %s", "leaf", data["type"]) + } + + if fmt.Sprintf("%v", data[""].(map[string]interface{})["nonce"]) != + "40243" { + t.Fatalf("Wrong nonce value\r\nexepcted %s\r\ngot %s", "40243", fmt.Sprintf("%v", data[""].(map[string]interface{})["nonce"])) + } +} + +func TestTxTrieJSONMarshalBranch(t *testing.T) { + ethTxTrie := prepareDecodedEthTxTrieBranch(t) + + jsonOutput, err := ethTxTrie.MarshalJSON() + checkError(err, t) + + var data map[string]interface{} + err = json.Unmarshal(jsonOutput, &data) + checkError(err, t) + + desiredValues := map[string]string{ + "0": "bagjacgzakhtcfpja453ydiaqxgidqmxhh7jwmxujib663deebwfs3m2n3hoa", + "1": "bagjacgza2p2fuqh4vumknq6x5w7i47usvtu5ixqins6qjjtcks4zge3vx3qq", + "2": "bagjacgza4fkhn7et3ra66yjkzbtvbxjefuketda6jctlut6it7gfahxhywga", + "3": "bagjacgzacnryeybs52xryrka5uxi4eg4hi2mh66esaghu7cetzu6fsukrynq", + "4": "bagjacgzastu5tc7lwz4ap3gznjwkyyepswquub7gvhags5mgdyfynnwbi43a", + "5": "bagjacgza5qgp76ovvorkydni2lchew6ieu5wb55w6hdliiu6vft7zlxtdhjq", + "6": "bagjacgzafnssc4yvln6zxmks5roskw4ckngta5n4yfy2skhlu435ve4b575a", + "7": "bagjacgzagkuei7qxfxefufme2d3xizxokkq4ad3rzl2x4dq2uao6dcr4va2a", + "8": "bagjacgzaxpaehtananrdxjghwukh2wwkkzcqwveppf6xclkrtd26rm27kqwq", + } + + for k, v := range desiredValues { + if parseMapElement(data[k]) != v { + t.Fatalf("Wrong Marshaled Value %s\r\nexpected %s\r\ngot %s", k, v, parseMapElement(data[k])) + } + } + + for _, v := range []string{"a", "b", "c", "d", "e", "f"} { + if data[v] != nil { + t.Fatal("Expected value to be nil") + } + } + + if data["type"] != "branch" { + t.Fatalf("Wrong node type\r\nexpected %s\r\ngot %s", "branch", data["type"]) + } +} + +/* + AUXILIARS +*/ + +// prepareDecodedEthTxTrie simulates an IPLD block available in the datastore, +// checks the source RLP and tests for the absence of errors during the decoding fase. +func prepareDecodedEthTxTrie(branchDataRLP string, t *testing.T) *EthTxTrie { + b, err := hex.DecodeString(branchDataRLP) + checkError(err, t) + + c, err := RawdataToCid(MEthTxTrie, b, multihash.KECCAK_256) + checkError(err, t) + + storedEthTxTrie, err := block.NewBlockWithCid(b, c) + checkError(err, t) + + ethTxTrie, err := DecodeEthTxTrie(storedEthTxTrie.Cid(), storedEthTxTrie.RawData()) + checkError(err, t) + + return ethTxTrie +} + +func prepareDecodedEthTxTrieExtension(t *testing.T) *EthTxTrie { + extensionDataRLP := + "e4820001a057ac34d6471cc3f5c6ab992c4c0fe5ec131d8d9961fe6d5de8e5e367513243b4" + return prepareDecodedEthTxTrie(extensionDataRLP, t) +} + +func prepareDecodedEthTxTrieLeaf(t *testing.T) *EthTxTrie { + leafDataRLP := + "f87220b86ff86d829d3384ee6b280083015f9094e0e6c781b8cba08bc840" + + "7eac0101b668d1fa6f4987c495a9586034008026a0981b6223c9d3c31971" + + "6da3cf057da84acf0fef897f4003d8a362d7bda42247dba066be134c4bc4" + + "32125209b5056ef274b7423bcac7cc398cf60b83aaff7b95469f" + return prepareDecodedEthTxTrie(leafDataRLP, t) +} + +func prepareDecodedEthTxTrieBranch(t *testing.T) *EthTxTrie { + branchDataRLP := + "f90131a051e622bd20e77781a010b9903832e73fd3665e89407ded8c840d8b2db34dd9" + + "dca0d3f45a40fcad18a6c3d7edbe8e7e92ace9d45e086cbd04a66254b9931375bee1a0" + + "e15476fc93dc41ef612ac86750dd242d14498c1e48a6ba4fc89fcc501ee7c58ca01363" + + "826032eeaf1c4540ed2e8e10dc3a34c3fbc4900c7a7c449e69e2ca8a8e1ba094e9d98b" + + "ebb67807ecd96a6cac608f95a14a07e6a9c06975861e0b86b6c14736a0ec0cfff9d5ab" + + "a2ac0da8d2c4725bc8253b60f7b6f1c6b4229ea967fcaef319d3a02b652173155b7d9b" + + "b152ec5d255b82534d3075bcc171a928eba737da9381effaa032a8447e172dc85a1584" + + "d0f77466ee52a1c00f71caf57e0e1aa01de18a3ca834a0bbc043cc0d03623ba4c7b514" + + "7d5aca56450b548f797d712d5198f5e8b35f542d8080808080808080" + return prepareDecodedEthTxTrie(branchDataRLP, t) +} + +func prepareTxTrieMap(t *testing.T) map[string]*EthTxTrie { + fi, err := os.Open("test_data/eth-block-body-json-4139497") + checkError(err, t) + + _, _, txTrieNodes, err := FromBlockJSON(fi) + checkError(err, t) + + out := make(map[string]*EthTxTrie) + + for _, txTrieNode := range txTrieNodes { + decodedNode, err := DecodeEthTxTrie(txTrieNode.Cid(), txTrieNode.RawData()) + checkError(err, t) + out[txTrieNode.Cid().String()] = decodedNode + } + + return out +} diff --git a/statediff/indexer/ipfs/ipld/shared.go b/statediff/indexer/ipfs/ipld/shared.go new file mode 100644 index 000000000000..95fbc71c7b72 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/shared.go @@ -0,0 +1,161 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "bytes" + "errors" + + "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +// IPLD Codecs for Ethereum +// See the authoritative document: +// https://github.com/multiformats/multicodec/blob/master/table.csv +const ( + RawBinary = 0x55 + MEthHeader = 0x90 + MEthHeaderList = 0x91 + MEthTxTrie = 0x92 + MEthTx = 0x93 + MEthTxReceiptTrie = 0x94 + MEthTxReceipt = 0x95 + MEthStateTrie = 0x96 + MEthAccountSnapshot = 0x97 + MEthStorageTrie = 0x98 +) + +var ( + nullHashBytes = common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000000") + ErrInvalidLink = errors.New("no such link") +) + +// RawdataToCid takes the desired codec and a slice of bytes +// and returns the proper cid of the object. +func RawdataToCid(codec uint64, rawdata []byte, multiHash uint64) (cid.Cid, error) { + c, err := cid.Prefix{ + Codec: codec, + Version: 1, + MhType: multiHash, + MhLength: -1, + }.Sum(rawdata) + if err != nil { + return cid.Cid{}, err + } + return c, nil +} + +// keccak256ToCid takes a keccak256 hash and returns its cid based on +// the codec given. +func keccak256ToCid(codec uint64, h []byte) cid.Cid { + buf, err := mh.Encode(h, mh.KECCAK_256) + if err != nil { + panic(err) + } + + return cid.NewCidV1(codec, mh.Multihash(buf)) +} + +// commonHashToCid takes a go-ethereum common.Hash and returns its +// cid based on the codec given, +func commonHashToCid(codec uint64, h common.Hash) cid.Cid { + mhash, err := mh.Encode(h[:], mh.KECCAK_256) + if err != nil { + panic(err) + } + + return cid.NewCidV1(codec, mhash) +} + +// localTrie wraps a go-ethereum trie and its underlying memory db. +// It contributes to the creation of the trie node objects. +type localTrie struct { + db ethdb.Database + trieDB *trie.Database + trie *trie.Trie +} + +// newLocalTrie initializes and returns a localTrie object +func newLocalTrie() *localTrie { + var err error + lt := &localTrie{} + lt.db = rawdb.NewMemoryDatabase() + lt.trieDB = trie.NewDatabase(lt.db) + lt.trie, err = trie.New(common.Hash{}, lt.trieDB) + if err != nil { + panic(err) + } + return lt +} + +// add receives the index of an object and its rawdata value +// and includes it into the localTrie +func (lt *localTrie) add(idx int, rawdata []byte) error { + key, err := rlp.EncodeToBytes(uint(idx)) + if err != nil { + panic(err) + } + return lt.trie.TryUpdate(key, rawdata) +} + +// rootHash returns the computed trie root. +// Useful for sanity checks on parsed data. +func (lt *localTrie) rootHash() []byte { + return lt.trie.Hash().Bytes() +} + +// getKeys returns the stored keys of the memory database +// of the localTrie for further processing. +func (lt *localTrie) getKeys() ([][]byte, error) { + // commit trie nodes to trieDB + var err error + _, err = lt.trie.Commit(nil) + if err != nil { + return nil, err + } + // commit trieDB to the underlying ethdb.Database + if err := lt.trieDB.Commit(lt.trie.Hash(), false, nil); err != nil { + return nil, err + } + // collect all of the node keys + it := lt.trie.NodeIterator([]byte{}) + keyBytes := make([][]byte, 0) + for it.Next(true) { + if it.Leaf() || bytes.Equal(nullHashBytes, it.Hash().Bytes()) { + continue + } + keyBytes = append(keyBytes, it.Hash().Bytes()) + } + return keyBytes, nil +} + +// getRLP encodes the given object to RLP returning its bytes. +func getRLP(object interface{}) []byte { + buf := new(bytes.Buffer) + if err := rlp.Encode(buf, object); err != nil { + panic(err) + } + + return buf.Bytes() +} diff --git a/statediff/indexer/ipfs/ipld/test_data/error-tx-eth-block-body-json-999999 b/statediff/indexer/ipfs/ipld/test_data/error-tx-eth-block-body-json-999999 new file mode 100644 index 000000000000..8654b53a9f85 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/error-tx-eth-block-body-json-999999 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"author":"0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5","difficulty":"0xb6b4beb1e8e","extraData":"0xd783010303844765746887676f312e342e32856c696e7578","gasLimit":"0x2fefd8","gasUsed":"0x38658","hash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5","mixHash":"0x5b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0","nonce":"0xf491f46b60fe04b3","number":"0xf423f","parentHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","receiptsRoot":"0x7fa0f6ca2a01823208d80801edad37e3e3a003b55c89319b45eb1f97862ad229","sealFields":["0xa05b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0","0x88f491f46b60fe04b3"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x6e8","stateRoot":"0xed98aa4b5b19c82fb35364f08508ae0a6dec665fa57663dca94c5d70554cde10","timestamp":"0x56bfb405","totalDifficulty":"0x6305496c80ab5c3f","transactions":[{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0xc3665b8a9224ba8da9a20322f31d599cafa52c5c","gas":"0x5209","gasPrice":"0xdf8475800","hash":"0x22879e0bc9602fef59dc0602f9bc385f12632da5cb4eee4b813a0c27159c4d24","input":"0x","networkId":null,"nonce":"0x1d3","publicKey":"0xc3dbee74f1b2b8dbedc417244b7f5a134c6f7769faf9ffe784b3f0fdda7ca52cf914d3f2b3164c009bf939796b77f047ccb4cc113d3bde5b06555b781e0c7149","r":"0x43531017f1569ec692c0bf1ad710ddb5158b60505ea33fb7a21245738539e2d5","raw":"0xf86e8201d3850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d8888102363ac310a4000801ca043531017f1569ec692c0bf1ad710ddb5158b60505ea33fb7a21245738539e2d5a03856c6a1117ff71e9b769ccb6960674038a3326c3dd84c152fc83ada28145a07","s":"0x3856c6a1117ff71e9b769ccb6960674038a3326c3dd84c152fc83ada28145a07","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x0","v":"0x1c","value":"0x102363ac310a4000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x4ce758b0c8aa655b77c14f16bd0190b5715be75a","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x3c634bf5f09f6b5b5ea377df7abb483f422ae5d4ba389c395f14f833de25d362","input":"0x","networkId":null,"nonce":"0x9","publicKey":"0x75022ee25c702fc6a53853843e00e87877e737f9c631a9d831c11693d7e31877a1b09755ab3a5c112decf57339839364b8b9a3c23ada01761b1e3a044e297316","r":"0x8219a4f30cb8dd7d5e1163ac433f207b599d804b0d74ee54c8694014db647700","raw":"0xf86c09850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880ed350879ce50000801ba08219a4f30cb8dd7d5e1163ac433f207b599d804b0d74ee54c8694014db647700a03db2e806986a746d44d675fdbbd7594bb2856946ba257209abfffdd1628141af","s":"0x3db2e806986a746d44d675fdbbd7594bb2856946ba257209abfffdd1628141af","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x1","v":"0x1b","value":"0xed350879ce50000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x30906581413d556de1a018adbe6cc63c88d58512","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x59feccaad599e776cd6635e68b5e19254cca3b38e49437044f1e1d15d00b0576","input":"0x","networkId":null,"nonce":"0x59","publicKey":"0xccf6be26c1eb1c89d5fe958db0112a46e3ac23a95ac0f709ce84a49ae3f20bcf143909bfe67f685caaf362066e1c7e224899f57678bbcecb7a720175bcbb387d","r":"0x1ca26859a6eed116312010359c2e8351d126f31b078a0e2e19aae0acc98d9488","raw":"0xf86c59850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88882b0ca8b9f5f02000801ba01ca26859a6eed116312010359c2e8351d126f31b078a0e2e19aae0acc98d9488a0172c1a299737440a9063af6547d567ca7d269bfc2a9e81ec1de21aa8bd8e17b1","s":"0x172c1a299737440a9063af6547d567ca7d269bfc2a9e81ec1de21aa8bd8e17b1","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x2","v":"0x1b","value":"0x2b0ca8b9f5f02000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x8bec4e6fb1a28820eb1e8ec2d4eae4842ed2f923","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x98a03afa804e248ada5f26e9118ae927d4d3cb60e78c54938dced1cf25ee3567","input":"0x","networkId":null,"nonce":"0x2","publicKey":"0xbc8c89a85804c7859069c13561dbbd8d1d4739ec7d18514c42b3ffea64529cee522a5e20d93373d0074e94c4c7b6eba51c7d2f18ef7c64c37520342acb233795","r":"0xa5aca100a264a8da4a58bef77c5116a6dde42186ac249623c0edcb30189640a","raw":"0xf86c02850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880fd037ba87693800801ba00a5aca100a264a8da4a58bef77c5116a6dde42186ac249623c0edcb30189640aa0783e9439755023b919897574f94337aaac4a1ddc20217e3ac264a7edf813ffdd","s":"0x783e9439755023b919897574f94337aaac4a1ddc20217e3ac264a7edf813ffdd","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x3","v":"0x1b","value":"0xfd037ba87693800"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x4835a9626b02369546502d2949e16b0fda110b0c","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x18f1e6430334ad548bc36fc317016bc9f7a076d1fa50a89fe4e1d095ed3f9562","input":"0x","networkId":null,"nonce":"0xd9","publicKey":"0x91b3b4fe89d112cfc7308619e8aa7de86f14af3f6b6e4e92becb6e29e98207835bbe1a69109c16b14b0eb7285d2b952a9cde6007932afe95e81eefc183f75314","r":"0xb93c6f8dce800a1ec57d70813c4d35e3ffe25a6f1ae9057cf706636cf34d662","raw":"0xf86d81d9850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d888814bac05c835a5400801ba00b93c6f8dce800a1ec57d70813c4d35e3ffe25a6f1ae9057cf706636cf34d662a06d254a5557b7716ef01dd28aa84cc919f397c0a778f3a109a1ee9df2fc530ec0","s":"0x6d254a5557b7716ef01dd28aa84cc919f397c0a778f3a109a1ee9df2fc530ec0","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x4","v":"0x1b","value":"0x14bac05c835a5400"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x9cc72ebf3daaf12c72e48605e1e67b47c95a1911","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xb1cada8daf63c45750df1ee79eed5a3cf6240e3cebdb6de3f26bc7cf03217bf4","input":"0x","networkId":null,"nonce":"0x34","publicKey":"0x90dff18c1c01d566e6d8bf0190e3e965f98e7f51ccbbe6040f9a9972e88f4ad19f1547406454fbc9e1ebcf4c5f2f1e2df9b9371028fe0a552ecca5f5f0aa4129","r":"0xe9a25c929c26d1a95232ba75aef419a91b470651eb77614695e16c5ba023e383","raw":"0xf86c34850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f258512af0d4000801ba0e9a25c929c26d1a95232ba75aef419a91b470651eb77614695e16c5ba023e383a0679fb2fc0d0b0f3549967c0894ee7d947f07d238a83ef745bc3ced5143a4af36","s":"0x679fb2fc0d0b0f3549967c0894ee7d947f07d238a83ef745bc3ced5143a4af36","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x5","v":"0x1b","value":"0xf258512af0d4000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x5c51467399bc655f0cc6db88df15946717534633","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x4fa879b491e0779fc035758ec77b93c4e51d528d65b64eb055c015a58deff103","input":"0x","networkId":null,"nonce":"0x6f","publicKey":"0x0b7e2532afc2daa33763002525aa6c7edc25ea97d63baeeb2c6f5094f18dca4a0212b52061f9a9091aad5c4380a6506f9a51ddd2d014e78742bf144a58d6ffa0","r":"0x9e0b8360a36d6d0320aef19bd811431b1a692504549da9f05f9b4d9e329993b9","raw":"0xf86c6f850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88881c54e302456eb400801ca09e0b8360a36d6d0320aef19bd811431b1a692504549da9f05f9b4d9e329993b9a05acff70bd8cf82d9d70b11d4e59dc5d54937475ec394ec846263495f61e5e6ee","s":"0x5acff70bd8cf82d9d70b11d4e59dc5d54937475ec394ec846263495f61e5e6ee","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x6","v":"0x1c","value":"0x1c54e302456eb400"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x055d9d7ec193d1e062c6ec4fa80ef89b5c1258f4","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x1bea59827ab153b20cee79890d221a80fa6a04e552d667504c592ed314fb6d76","input":"0x","networkId":null,"nonce":"0x46","publicKey":"0xfae19a0ac08d36f0229663d45d0c41ca52c4e295c7af82a1b39515a79025175293400d026e0d41767aac42f8b7e4a6687c5762161457d753f1fc0766614868f9","r":"0xb2803f1bfa237bda762d214f71a4c71a7306f55df2880c77d746024e81ccbaa2","raw":"0xf86c46850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f0447b1edca4000801ca0b2803f1bfa237bda762d214f71a4c71a7306f55df2880c77d746024e81ccbaa2a07aeed35c0cbfbe0ed6552fd55b3f57fdc054eeabd02fc61bf66d9a8843aa593a","s":"0x7aeed35c0cbfbe0ed6552fd55b3f57fdc054eeabd02fc61bf66d9a8843aa593a","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x7","v":"0x1c","value":"0xf0447b1edca4000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x8e68c0c9b5275fa684291304af9cafe6ceaf2772","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0x73e87db1108a2aa852f48e088ca1a2771f9b7c18af8d1bd77a3cdcc72a750c56","input":"0x","networkId":null,"nonce":"0x3","publicKey":"0xa5e423dfcbdbba1fdbb785367a88235fa2569061d72b6c715111ac21cbef8fc1db860acdef85f1408c760f34b28a4f07d950ac15c4b85d5e528e50f546a89b6d","r":"0x6dccb1349919662c40455aee04472ae307195580837510ecf2e6fc428876eb03","raw":"0xf86d03850ba43b740083015f909426016a2b5d872adc1b131a4cd9d4b18789d0d9eb88016345785d8a0000801ba06dccb1349919662c40455aee04472ae307195580837510ecf2e6fc428876eb03a03b84ea9c3c6462ac086a1d789a167c2735896a6b5a40e85a6e45da8884fe27de","s":"0x3b84ea9c3c6462ac086a1d789a167c2735896a6b5a40e85a6e45da8884fe27de","standardV":"0x0","to":"0x26016a2b5d872adc1b131a4cd9d4b18789d0d9eb","transactionIndex":"0x8","v":"0x1b","value":"0x16345785d8a0000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x2a65aca4d5fc5b5c859090a6c34d164135398226","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0x337a5e90b73f44ffebea73cb3d97738c524f63e1032b30735e43212cff731aee","input":"0x","networkId":null,"nonce":"0x2a11f","publicKey":"0x4c3eb5e19c71d8245eaaaba21ef8f94a70e9250848d10ade086f893a7a33a06d7063590e9e6ca88f918d7704840d903298fe802b6047fa7f6d09603eba690c39","r":"0xaa8909295ff178639df961126970f44b5d894326eb47cead161f6910799a98b8","raw":"0xf8708302a11f850ba43b740083015f90945275c3371ece4d4a5b1e14cf6dbfc2277d58ef92880e93ea6a35f2e000801ba0aa8909295ff178639df961126970f44b5d894326eb47cead161f6910799a98b8a0254d7742eccaf2f4c44bfe638378dcf42bdde9465f231b89003cc7927de5d46e","s":"0x254d7742eccaf2f4c44bfe638378dcf42bdde9465f231b89003cc7927de5d46e","standardV":"0x0","to":"0x5275c3371ece4d4a5b1e14cf6dbfc2277d58ef92","transactionIndex":"0x9","v":"0x1b","value":"0xe93ea6a35f2e000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x2a65aca4d5fc5b5c859090a6c34d164135398226","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0xc280ab030e20bc9ef72c87b420d58f598bda753ef80a53136a923848b0c89a5c","input":"0x","networkId":null,"nonce":"0x2a120","publicKey":"0x4c3eb5e19c71d8245eaaaba21ef8f94a70e9250848d10ade086f893a7a33a06d7063590e9e6ca88f918d7704840d903298fe802b6047fa7f6d09603eba690c39","r":"0xcfe3ad31d6612f8d787c45f115cc5b43fb22bcc210b62ae71dc7cbf0a6bea8df","raw":"0xf8708302a120850ba43b740083015f90941c51bf013add0857c5d9cf2f71a7f15ca93d4816880e917c4b10c87400801ca0cfe3ad31d6612f8d787c45f115cc5b43fb22bcc210b62ae71dc7cbf0a6bea8dfa057db8998114fae3c337e99dbd8573d4085691880f4576c6c1f6c5bbfe67d6cf0","s":"0x57db8998114fae3c337e99dbd8573d4085691880f4576c6c1f6c5bbfe67d6cf0","standardV":"0x1","to":"0x1c51bf013add0857c5d9cf2f71a7f15ca93d4816","transactionIndex":"0xa","v":"0x1c","value":"0xe917c4b10c87400"}],"transactionsRoot":"0x447cbd8c48f498a6912b10831cdff59c7fbfcbbe735ca92883d4fa06dcd7ae54","uncles":[]},"id":1} diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-0 b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-0 new file mode 100644 index 000000000000..e7dfbca84118 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-0 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":1,"result":{"author":"0x0000000000000000000000000000000000000000","difficulty":"0x400000000","extraData":"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa","gasLimit":"0x1388","gasUsed":"0x0","hash":"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x0000000000000000000000000000000000000000","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","nonce":"0x0000000000000042","number":"0x0","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000042"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x21c","stateRoot":"0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544","timestamp":"0x0","totalDifficulty":"0x400000000","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}} \ No newline at end of file diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-4139497 b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-4139497 new file mode 100644 index 000000000000..02ef395846ca --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-4139497 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":1,"result":{"difficulty":"0x5c647cfc07f1a","extraData":"0x65746865726d696e652d6173696137","gasLimit":"0x668fd6","gasUsed":"0x655bf3","hash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","logsBloom":"0x00004000840000000004000400000006008800000000000000900000000000000002000000100000000000000000020000000000004000000000080000080000000000000200000000000008000000000001000000000000000000000800000000014000000200000000804040000200000000000100008004000110001000200000020400000000800200000008000000400080008000200000001040000100002000000000000002000000000000000000010000000010080000000000000010080002000000000000002001000000000000040000000120200000000000000100000100000000000000000000000000000000000000000000040800000000","miner":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","mixHash":"0x2a65887132d93df4ad543ea9ab69b2de12bf1ef0d9a5b9128fe557a7cf6e365c","nonce":"0x68b593b0029de941","number":"0x3f29e9","parentHash":"0xf8ef0dc32d00fe925c9ac3039f3fe202ac6988f37b3710840848ecf29a4905d9","receiptsRoot":"0xf17608f36b1fc813fefd9cbd1fd653195de20ab72f2efcc95f7e00c6576080d6","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x8a42","stateRoot":"0x3258ad3d8a73140be9d3895166f3f88b0f65a5575d8176f10dc2a6dddac36b64","timestamp":"0x598c1020","totalDifficulty":"0x23bcce551ec1d5055c","transactions":[{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x55335d56e95151bce1635bce649175ea954aecee","gas":"0x2117a","gasPrice":"0xbdfd63e00","hash":"0x51f9d60ce19d4174224f91be402d4504553f127511a630a18a8735b4c1db072e","input":"0x0f2c9329000000000000000000000000fbb1b73c4f0bda4f67dca266ce6ef42f520fbb98000000000000000000000000e592b0d8baa2cb677034389b76a71b0d1823e0d1","nonce":"0x1","to":"0xe94b04a0fed112f3664e45adb2b8915693dd5ff3","transactionIndex":"0x0","value":"0xb7ce92a6fa0400","v":"0x26","r":"0xaa97e8fb84036ed395fab0e05f4432e219e855539a17a73444e915a3f18d7f15","s":"0x117401fbe04f6c8316ba4c344b37de5d1b5a6fc252160a093e7270d6fd37c2c4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98","gas":"0x2d9dc","gasPrice":"0xbdfd63e00","hash":"0x57a6c52559d193fef65f8b99fdd46f341f0739ba7d4a772a87d8fad89fc2cff5","input":"0xa9059cbb000000000000000000000000744346c50253300694aea6d7e03f55a3ea91f8a30000000000000000000000000000000000000000000000000000013061e0a9ab","nonce":"0xc104d","to":"0x41e5560054824ea6b0732e656e3ad64e20e94e45","transactionIndex":"0x1","value":"0x0","v":"0x25","r":"0xe925321edf5dc905fa0ebf9a08d8915e0ce90463d55c19e8bdf0dc8e5e6ddc73","s":"0x328a5099139ae2e3f3be2736dec30fd2b3240892b77575e588b8f84a0e11307b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xa624ceb708a1e9a3962de82c5a3c5850db0097f1","gas":"0x2117a","gasPrice":"0xbdfd63e00","hash":"0x616694b9e9aea8d913797a50958a9343e18451ccb2abffa1b10b2d06378c612f","input":"0x0f2c9329000000000000000000000000fbb1b73c4f0bda4f67dca266ce6ef42f520fbb98000000000000000000000000e592b0d8baa2cb677034389b76a71b0d1823e0d1","nonce":"0x24","to":"0xe94b04a0fed112f3664e45adb2b8915693dd5ff3","transactionIndex":"0x2","value":"0xa9f1b6b74205400","v":"0x26","r":"0x4b6f583ee70f4aabad8da3c97a0b1d7bd18ef6463aa08fb730696b758abe255c","s":"0x1a13f3c8fef9b92c28151db22b03b9b9894b2d7ef103a38b204ac5ba970073fe"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xb083a0287b4e7f8319eee74b27e42bdd77da4e1a","gas":"0x2117a","gasPrice":"0xbdfd63e00","hash":"0x92a84244da41cd93c1c0ab7b7d13556453d3fd76317a71fa89ba129ad4c9d80e","input":"0x0f2c9329000000000000000000000000fbb1b73c4f0bda4f67dca266ce6ef42f520fbb98000000000000000000000000e592b0d8baa2cb677034389b76a71b0d1823e0d1","nonce":"0x3","to":"0xe94b04a0fed112f3664e45adb2b8915693dd5ff3","transactionIndex":"0x3","value":"0xd51851e1dacc00","v":"0x26","r":"0xc8304a7acbaddcdd4ac10216697ea88d1b154c9d0de42fb75ad9a301fef38cc1","s":"0x76cdd85171fb9da403def3fbfafb8545835aebeb9a541e6207d9d373914e1e8d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xc348b6a2758fb408e5cce34d43feee1726692e0d","gas":"0x13880","gasPrice":"0x719f11100","hash":"0x164a9b95e7914ef6071b6228699635e8e8d58b4d60fd4736aabd87b5bcf8d5fb","input":"0x","nonce":"0x3b","to":"0x7727e5113d1d161373623e5f49fd568b4f543a9e","transactionIndex":"0x4","value":"0x18f7be6e64863700","v":"0x25","r":"0xfb14159445060e4a1809e7d959210da4151fe1535c8b9aa9158b5d7536b0fbac","s":"0x3563cf5da676135b36d9d2305f1ee133452280e2c1abe16bda50fe502557d1d9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xd3273eba07248020bf98a8b560ec1576a612102f","gas":"0x5208","gasPrice":"0x6fc23ac00","hash":"0x5d6f0ac462923b852080c3b96afa862bc93a4bc605e5feb9bda64780d6c89089","input":"0x","nonce":"0x67ac","to":"0xd66f7b11c7da581406d62a501fdee675466f4593","transactionIndex":"0x5","value":"0x5bd6662df2c3c400","v":"0x1c","r":"0xf042ec51b11a4c14cb7f48e50e3c4278965530f9e5c4a17926e47f83dbf09fe5","s":"0x5eee0c65eacdabfb60688656d108ab5dc74dce9ad79f661148bbba7694a5c191"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x3b0bc51ab9de1e5b7b6e34e5b960285805c41736","gas":"0x5208","gasPrice":"0x6fc23ac00","hash":"0x8c951abf8f855e94f1059a0b9f9de8e23e12ffc7d4511e0dcbfe73060ff2e9ee","input":"0x","nonce":"0x6595","to":"0x7c402ca59a701f6b3f077f175b4c964122043221","transactionIndex":"0x6","value":"0x5bd6662df2c3c400","v":"0x1c","r":"0x36d4084792312a9aafd676e0570acc14b29b590bc3f38e0c643ff278653628ae","s":"0x4f25d719cd23e3fb88bd205e955d8127c819d208046e83f9ac9a47c35ec2a814"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x093177dbaa25a001e3ee343d3ec492e71b9367aa","gas":"0x6271","gasPrice":"0x6c7fc3b40","hash":"0xecc2c35c2ca748c7eb2970d76288e34ab514a48c60670ba5fa04ec50d59be1f5","input":"0x","nonce":"0x2","to":"0xda1b2aeac0196d39658186604609fff185e1774d","transactionIndex":"0x7","value":"0x5b09cd3e5e90000","v":"0x26","r":"0xef0a0125e0984c9a59fbe475df19bed2fcbfbe02ced04ad9f5f25530e276a527","s":"0x7ce66b31396aaf02a34d966e87e03ba9f04ac021f56e8ca1cd6124434df61ab1"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xf04ad0c7eb4ed654c52477f8e756800bde9f2341","gas":"0x5208","gasPrice":"0x4e3b29200","hash":"0x7475e0a920d21ee08b85f0ec61b02ed646190ff23ae2805dfef4cfe81c59a46e","input":"0x","nonce":"0x427","to":"0x1e4f986d287bacf4283d35ca61fb342ca91674d6","transactionIndex":"0x8","value":"0x3d48c89a6020000","v":"0x26","r":"0xdab319aff51e0755b832a17fba0e4778895980eb6cb87a2aa4b35edd418163ef","s":"0x10dc3f986fe67347e293177acdd0dbfa7a910d64c9c484a0635221dd652a6191"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xb2930b35844a230f00e51431acae96fe543a0347","gas":"0x186a0","gasPrice":"0x4a817c800","hash":"0x070599a9b0a4e550cdb1b5068d0d3bfe3fc0d60302973d3b3abad3a4762ae81c","input":"0x","nonce":"0x569fe","to":"0x79d56207445e24f5eeb391358924a39c620dd1e0","transactionIndex":"0x9","value":"0x21c60092fff800","v":"0x26","r":"0x77ba2e5b7c617e6ad54a7d4ca14362837cdd3138648a0855436a6fef99033d4d","s":"0x6714b8b257a8c714b2395fca0a8bfd9299fa3d759da9c01a2582d7114a316f05"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xcbf44ffb74ae94a4b696e716964b1d69400c7749","gas":"0x11170","gasPrice":"0x4a817c800","hash":"0xa3031fce94886738b6666b8a58233e845e9fd4ced150f65c043738fc54ccc7bb","input":"0xa9059cbb000000000000000000000000e74db956a107baa7cadc1258a6f539f40fc4fec100000000000000000000000000000000000000000000000000000002caa8e180","nonce":"0x0","to":"0x93e682107d1e9defb0b5ee701c71707a4b2e46bc","transactionIndex":"0xa","value":"0x0","v":"0x26","r":"0xda99eedee485f9f789cc183307b139b63e0885c7135796fbcca1d20415fd884e","s":"0x5103dc4b2fe14ec65fe2c98c331cf9177ffee86a89ab5b8079a3ee285bdab7c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfc203c5f867be784726ef4198c0e8fc1313074db","gas":"0x5208","gasPrice":"0x4a817c800","hash":"0xaf654ac5eaecca624725c4236adcbee10a9b4c76f4bb71c893c373c659a4305a","input":"0x","nonce":"0x1","to":"0xa3da2a2f864a180297adedc48ad51e562d7a9f8a","transactionIndex":"0xb","value":"0x1e81bba24c058138c1","v":"0x25","r":"0x6e1989c52a8d07f84ad0701cc6eae4e9fbb2ca79476b03422098d03e52e6a594","s":"0x6d36b9c8ed63abab0ada4fd9c53541d4b948b23c79ae118cd2f205a010f2c0ee"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0xb2f6b98129aad387041bfe8710bc1bf363bb208f15d49a482b5d15bbd13d1cec","input":"0x","nonce":"0x2aaab9","to":"0xabcd334c3504100e6d26d895c8c658e35fe515f7","transactionIndex":"0xc","value":"0xaf069a8a72ee91","v":"0x25","r":"0x5b5bdfabd8a099a056af2ecef44bc142aa5bfe7623a14505fc0c6f3f059eee0","s":"0x336f76890622529392f3eabbd793be3ec6367b31b65737d6ea2ebedcc934f3d6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x75814b803794e796a4b496765af343121020238e","gas":"0x5208","gasPrice":"0x4a817c800","hash":"0xcf257e096c2cd20debbb4608d00ca28b3c576b705de8109090caead53ccfab17","input":"0x","nonce":"0x1","to":"0xd0bcd02f598c2473395842d647011b6d1cdd0e5c","transactionIndex":"0xd","value":"0x1ee647737e6ec208c1","v":"0x26","r":"0xcd5de53b8c661068d31053854e4e562f276e8481cba387d6853910d415a8e213","s":"0x2d209a8658c9411087c389f3bdeaa9c2ff70eac8950f0b4db413fcc39a4fee2b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x8f5aae245398626bc162b47b862fa09e49190b38","gas":"0xbb00","gasPrice":"0x2540be400","hash":"0xb16f7c1b61134c155cb820d8f51d77e93fa7212c8f46be42dbfc8a3767d176fb","input":"0xa9059cbb0000000000000000000000004f5151785e03b47d0c6641872bb6b29b6de1b77c00000000000000000000000000000000000000000000000bbc4849990fa54400","nonce":"0x0","to":"0x888666ca69e0f178ded6d75b5726cee99a87d698","transactionIndex":"0xe","value":"0x0","v":"0x1c","r":"0x25dca29942900fe444e2e3e27ea41648d6a22947a9d8a38e11ae367b0a064d0f","s":"0x52e06101618b2fe1f3a845f4f39a3092016e920855be9c9b447e3d6828e1b263"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x22b84d5ffea8b801c0422afe752377a64aa738c2","gas":"0x186a0","gasPrice":"0x2540be400","hash":"0xf40a89152e66d51b54ae72df0712e08fd6c121fd1d58f7cbc38f63249a139963","input":"0x","nonce":"0x1af86","to":"0x444d80ab1f1540642d69b3eaeb790903cf4872bd","transactionIndex":"0xf","value":"0x53444835ec580000","v":"0x25","r":"0xebadeffaf6e5a8b53f482372e9b33db8c0380f4a21a388f499b0f0072e8e2afa","s":"0x455bd8083723fbb895f0fe62c02ad1882bc3daa76443e4a77494f984824e9c73"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5ee4fb7764e28e71b9d0ce72741d6df027b4a79f969a71364db380de686cc1f1","input":"0x","nonce":"0x9c5c","to":"0x3c13a69380e27bfd16a5bc5528f4c1d6cc4993ac","transactionIndex":"0x10","value":"0xbec8544eceac00","v":"0x26","r":"0xe7eb23823262f600e33b526a953ac7e32dcc0cf86d9f1febbf8db30edea03b02","s":"0x595d98353ad032557caf00ebe14f21ebe66fab85394a421d8bbd9a47b3ae6627"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5","gas":"0xc350","gasPrice":"0xee6b2800","hash":"0xc0e565782181943c4697199214db1d21a535835b665b2ba771fbe4693ce52de0","input":"0x","nonce":"0x292ad7","to":"0x0fbb3c7bcac281b97f8a8a3292a026d67c3230f1","transactionIndex":"0x11","value":"0xb2e25606328960","v":"0x26","r":"0x837849bae28e40b752586ce7135cee1a4741eb3f68b089cb6ef4dfb4b6291738","s":"0x312d8f5e8a25836687d6eb69be151016074355ee5b580793111314daba9da1a6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98","gas":"0x2d99c","gasPrice":"0xbdfd63e00","hash":"0xc092388bd2e7626c53e3c580b4a5d57de3442b28c97b34fe1ff68042b9026137","input":"0xa9059cbb000000000000000000000000cd2e8348d2f58f02f1859ecdef07d1ecf1f0ced9000000000000000000000000000000000000000000000000000000174867a5c0","nonce":"0xc104e","to":"0x41e5560054824ea6b0732e656e3ad64e20e94e45","transactionIndex":"0x12","value":"0x0","v":"0x26","r":"0xec60ffa5508b41567c20a68f26df77c3de22fa3b11fa853c7562f693df12cc03","s":"0x5fff6d9220b4da3f68358ead8b782e52de0f2ae2def4c07e5d547d513fcfe80"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x34ee80fe753728be177a1e6ed5541565b2c94da9ac8fb5d16e7cc757cea3692a","input":"0x","nonce":"0x2aaaba","to":"0xfd15c258b4191b73c7dde5df066f4732e4392f7f","transactionIndex":"0x13","value":"0xdee2eb356bf15a2","v":"0x25","r":"0xa5737391f905649e6ed6604db0b4040e94aec8bc6ad47afcbb1f1cbd934a7dc2","s":"0x5a52547c6fce0aaf436c26033f92ef7542629b8cfad92a5137979f072f6371af"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfc203c5f867be784726ef4198c0e8fc1313074db","gas":"0x5208","gasPrice":"0x4a817c800","hash":"0x8c9d7cbc1629acab3c2b0a6423a84025e5bc11f15eec3bcfe2e224a505bdd5d2","input":"0x","nonce":"0x2","to":"0x42bd724618c19fd396b95891621e267968707dd3","transactionIndex":"0x14","value":"0x17b2a64c0adf2a073f","v":"0x26","r":"0xe40d950eeef37b63fd058ad8e0e9510b858ba5a67e033d99f89c9023a6fa227e","s":"0x791f7b441d70533f9670b7db0b224921944fea8820b5dfb2f06704f75872bea5"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbe853e3717ddcec5f9d57ed55e6ec1dc6fa1e9545c901b52a156f7b1b9c9cd3b","input":"0x","nonce":"0x9c5d","to":"0x7cb1e28cf73698e0474bf1b7b98d01a8e71204b1","transactionIndex":"0x15","value":"0xf1591cc0b131a400","v":"0x25","r":"0xd96f474d79e265d9dc5bf6bd09c46b54a25627caff37ff549c726e0ea7812920","s":"0x7630e1a32cdd1e3eaee6c00b38d349dfa3048ccbc20431cf651a218c124c1ab3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5","gas":"0xc350","gasPrice":"0xee6b2800","hash":"0xfaacce929d5e0f054479cb584dab3490770c43e616c3bba0c2f8bfd0a074a603","input":"0x","nonce":"0x292ad8","to":"0xda6b3b1bd62b06ca13fb37f660e8daf848b60330","transactionIndex":"0x16","value":"0x2e7c5072cf1e9e0","v":"0x26","r":"0x7d51a1209d5475564a4df31fef6d0a09c8b8aa1cc6d1c87cda42f02a58db4da6","s":"0x5ab60244b91d00e7d588151bd9f51f4fec1349c5c146b2178c2bca94610cbe3d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98","gas":"0x2d99c","gasPrice":"0xbdfd63e00","hash":"0xff1d6ee564b1be371792551a5b047ccdf519e74f3d5513da008318baf6915715","input":"0xa9059cbb00000000000000000000000091b1053eb9486b0b63d44a5cba021c324991027d0000000000000000000000000000000000000000000000000000005981122544","nonce":"0xc104f","to":"0x41e5560054824ea6b0732e656e3ad64e20e94e45","transactionIndex":"0x17","value":"0x0","v":"0x26","r":"0xe13fe6b5356d9abc156dffe6395f7b724a9b35ec58fc4026811241b03bad7a92","s":"0x33ae3ea46a35263b6d5e96574317c233affa15aea7d79209facbe88ce2eed013"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x4a2db708d569b49383b1d8abbff178b574affc87f879d57b5798904b52d0d4fb","input":"0x","nonce":"0x2aaabb","to":"0x029f13b14a1c4c65aa19f03fb12c0d761fc9e662","transactionIndex":"0x18","value":"0xb0297da2f04b2c","v":"0x26","r":"0xbad7b74d953063bb260fd27fc57c3ce40f46ab872fd44d62e30edd2a2da91e02","s":"0x7e513fa35422c73d96c51b455cfd09bd846ae5ea1f6047c6c84151cbfa68e6bd"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5fa1fc424acb1df5a2efe579d9cf301ee4b7415b7086800fe48a1fd2f4127fee","input":"0x","nonce":"0x9c5e","to":"0x1f70dbf8b8c7a47dceea01ffe6749382245fa10f","transactionIndex":"0x19","value":"0x1a21d8eef282000","v":"0x25","r":"0xac50ff5d7c54b976fb08d24e235a1ba4e611a017332e20747818b1091cdf3a2f","s":"0x1523cdd85db8b8fd1e6dbc29bffef1583744f5a5ed278a97999fe44642e6b77f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98","gas":"0x2d9dc","gasPrice":"0xbdfd63e00","hash":"0x231bcf683e12cb3cb50d2979154e5537822b30974a3bf08596a231ae7ffde4e2","input":"0xa9059cbb00000000000000000000000018e3dfeaebe76cfacc75fd724e2c6e4ba140d56a00000000000000000000000000000000000000000000000000000107a24798c0","nonce":"0xc1050","to":"0x41e5560054824ea6b0732e656e3ad64e20e94e45","transactionIndex":"0x1a","value":"0x0","v":"0x25","r":"0x942337149235dbe45a6fb9596ca5bfd47f3d48a49bf17980bb7a424203f48130","s":"0x673e537d0a7edc66bfb3bfd7918adc7541f1850cb5249113cd6af089d25a75d3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x2809c2c670b3a0a57ab0279e369f34972e8aa818743a7b462e6c3812b139aa85","input":"0x","nonce":"0x2aaabc","to":"0x54da15b491babc978b2a3fc31841911a12c5ca0b","transactionIndex":"0x1b","value":"0xae56830ea32b52","v":"0x25","r":"0x8d0aa2d9e685b918186da550d2b00c51a0c471fc78493f6c6427d69b5e25def0","s":"0x79e64a26af42c415adba3b41dd899d030d683bb0c2c18289f0024bec84a34189"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb38ef7a0d9f4ec185696f9328171e38586d5f0c0c725cb2b1adf8a5c8a32b33e","input":"0x","nonce":"0x9c5f","to":"0x55b840e722a5a73b34320a34c48463e67993c0e2","transactionIndex":"0x1c","value":"0xd923293ec5e400","v":"0x26","r":"0x71e3e7c505606dfd773f53badb0f2d081207cbea0000c288781a18bcd6b75c14","s":"0x264bb04158b749785485323ba868ba7ada155985af7951bf948c4fb35bdc0ae7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98","gas":"0x2d99c","gasPrice":"0xbdfd63e00","hash":"0x7b3c92175534b96e35797ba00deb87f606edac372bd573f06ff6636140938f6e","input":"0xa9059cbb000000000000000000000000be69390fbf8871caf82e2b70a92a4f7a87d161c20000000000000000000000000000000000000000000000000000004b585bb7f3","nonce":"0xc1051","to":"0x41e5560054824ea6b0732e656e3ad64e20e94e45","transactionIndex":"0x1d","value":"0x0","v":"0x25","r":"0x8863a36a60b2fa5a621cb01f1d80c324b519c8cd3bc3db559b47cc5e6777d26d","s":"0x3b5ff01f46e137f33f273ab3eaf3d6afb12959d19f8f01a9119d40fa9beb90ea"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x8f0b09787e0356ce6e2f43a2b5a15245137a0f6066a9fbcdf519e8df37a92aa7","input":"0x","nonce":"0x2aaabd","to":"0x863b65fe3b44db9f60dbace119fb08fdd4d2c62e","transactionIndex":"0x1e","value":"0xde5381edb9bafe8","v":"0x25","r":"0x61f134af94880a42bbfaffd277bbd8c80a6fc978e562dff4ca29e9c8b61968ce","s":"0x8a02c26b769e22e966d35d44acc175eb747f57bb9d3fa2c057e23b1529ba9e9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbe4bc2e52dc8bcb6df1a935ddcbd84958a1de639fbefe1da5ed829f8f4f4486b","input":"0x","nonce":"0x9c60","to":"0x585366a5ad43dc56ccbb54e94c48c6f1d931710a","transactionIndex":"0x1f","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x62674294331a2dfb96a2d7480331f18fe9003869e52f32f0a5b88a0094fbff63","s":"0xbf8f9286718f28e13473f4136b8e8989ca247db1075f6cc633fac869532e754"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98","gas":"0x2d9dc","gasPrice":"0xbdfd63e00","hash":"0x3eebb2d806a9ed429d460178c89d72364dd35719d1865942234bdd70bdfb258b","input":"0xa9059cbb0000000000000000000000004c59f430c6ebadaad6ccd25f4b9eeeb8f7a22108000000000000000000000000000000000000000000000000000001029f447f3e","nonce":"0xc1052","to":"0x41e5560054824ea6b0732e656e3ad64e20e94e45","transactionIndex":"0x20","value":"0x0","v":"0x25","r":"0x9e20b3b1429b5672d9f05a859633e1e4facb71e308924277811db2e3ebaefedd","s":"0x24803ead950f32f9863811c17f914ae9e831c48973183c036605d96750ee93a3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0xc94b14d966d087f09dc1bab45d5684d5c1f00167a27042e48391c4b97dbec90a","input":"0x","nonce":"0x2aaabe","to":"0x872bad809a1b1ec9a7dd38ac4d7e9b19920a1faf","transactionIndex":"0x21","value":"0xaf0a678d3ca95b","v":"0x25","r":"0xfad929edfbe500b2be3dffed3b9ebe4d9662bbdd211ae388a1c05a693c0054d8","s":"0x69f5962685c91ce1559d9e350b6322539f6d02dfa824d5253f381fda61f8f663"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x3cfa69cea575486acd281c7517bb9e4c74e6e8179065b5210b8ed06054a1c1a6","input":"0x","nonce":"0x9c61","to":"0x7d1340884d2b767da3e87daa3b59960c4e98b791","transactionIndex":"0x22","value":"0x17aadf094fd1c00","v":"0x25","r":"0xcf01d52255575cd6e8cc9045187c293bb950b56e69d152880fd672f026b71213","s":"0x131fe537936d809d1eebf72caa0019142e348d282bb59394f0a6c531338d95f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98","gas":"0x2d9dc","gasPrice":"0xbdfd63e00","hash":"0x8e2cad0763aea7b8a1e9b45b394aa0b62343dab30a230948bfdbe19988da31ad","input":"0xa9059cbb0000000000000000000000006ccecb1bdbf8f464f2b58adb417d5a88d0300f0a000000000000000000000000000000000000000000000000000002388f52ea80","nonce":"0xc1053","to":"0x41e5560054824ea6b0732e656e3ad64e20e94e45","transactionIndex":"0x23","value":"0x0","v":"0x26","r":"0x12ceb52c978e7a7e67f58068def1924fd7a500fcace1c39840f19dfdef82a130","s":"0x3d3e4826ade71f3e9079b31d9b8942c9f4f0cfc09bcc1cd66a9fefa9f2dcc8af"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0xd49fd3809e5349c4425c5712ab9fc2c69c825161ab706c1bf3179f30a4e8c5fb","input":"0x","nonce":"0x2aaabf","to":"0x959cd73ae36c115df8ee9d20f5d3101ff3181466","transactionIndex":"0x24","value":"0x17057457ca587d4","v":"0x26","r":"0x707870910fb23d9091244655fa4d6b317939f9e0011b89097ce4903f25ee6e8b","s":"0x16707cf3e313a11f694b881e1463322b07730b79f3133f74befa570fbc78ce78"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4c56dac6f3503162683ae12d2445155aa1f705bb131c14742424944bede67517","input":"0x","nonce":"0x9c62","to":"0xc567f4a3d18d42fc49a5f8c54eaeaad0cc0713d0","transactionIndex":"0x25","value":"0xbe0d6ff05a3800","v":"0x26","r":"0xccb97060a133d58c8f40b7d2ecdba4447b19246f40a154f6e69b91368526f0f0","s":"0x5a6006fe0a064b9e378ee5f3ad329735f844b73b7a3837643737b9f02136824a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x90a0eba75638bce9ebe5554c4695fa9c25e95f85fa7ccb3ab134dddd24912f06","input":"0x","nonce":"0x2aaac0","to":"0x02eee5b2f34918340694c0aece742dc7f8ee0ff9","transactionIndex":"0x26","value":"0x161d70598349dc5","v":"0x25","r":"0x347bbd3db97596d8b48e281437e4038078582d6380ce2bdbe5621f2b04cd9acf","s":"0x9996abfaa8d6fa128c3801b033e6980306ec0185fcdf603b1ef425117023a54"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x448da8c7d24be59ec445f4df143cae6782fc194b1dbd61d07d1fbce99a525d2d","input":"0x","nonce":"0x9c63","to":"0x4530afe8ae24f91875b74da5fe251170177bcbfb","transactionIndex":"0x27","value":"0xc4a234146d6000","v":"0x26","r":"0x85303903f9f1301a6479d32cb6dea765c2a1bf114e59a50fb5b05e37a5b23631","s":"0x17478bce1de2c9fe6a984aeed9f831343376a145164c92277df4e63bcfcaabc3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0xd3ac8ce2a58d2f93adb88cfcf9241e1a682f71f69e61e0da04f3de056c0f3f28","input":"0x","nonce":"0x2aaac1","to":"0x04bdde4339294d8a521a28dc696f2286f0acd3d2","transactionIndex":"0x28","value":"0x185f9a12e284964","v":"0x26","r":"0x67403bb19a16ff477d30e264e4e4c0b6664c220e43b85c89c1fb0459085c0362","s":"0x617a3affb24dc4d38376c3ad4d33e972b3721e8ceadb779151a81aac031641d3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x96f04f1c4a5d8f81e8f541871ca1661662fee633aad36c65089a42418bc5dc5d","input":"0x","nonce":"0x9c64","to":"0x14fc32d88632e190beb08c1929c928954c06e336","transactionIndex":"0x29","value":"0xc3d6fc66994000","v":"0x26","r":"0xc567df5c64232efae75e1285c43f542ea8834aa9459674391e59dfe258598bb8","s":"0xf47712241b38e13645d6b07f8e8f95d4a2ac79b98052f04841100341a3fad1c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x5723eeeb5059dd1f44a3edba5d51f584e8a75fc99633990f9aaf1e23e2516079","input":"0x","nonce":"0x2aaac2","to":"0x30d82cc8a274716b616e858e8fa9d2e7c0fe111b","transactionIndex":"0x2a","value":"0xaeaa14152dfe1e","v":"0x26","r":"0xae89dca52ea390dbca8a00900a19a3dd1165a02c4585efa702777acdf3f87115","s":"0x448a134ad23ee92f3674b827374977336d0cad450c341e4e3972c6cd67b2ecf7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa5e865c2964b73e24fede17686ef1df138230decd74fe82e4e39b1a0e0caf4d6","input":"0x","nonce":"0x9c65","to":"0xb29f1c22590d62d3b19eee1e10936263588cbf2b","transactionIndex":"0x2b","value":"0xb83e6e7e3cd000","v":"0x26","r":"0x1f479ee111fb49c8de1095073ab81fb8b048ddcccd272350f8ec4dd00a9ad22e","s":"0x2623cd338a54b042288111c855d915961c5941d1cca6489b46d46d010bc0ca98"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x52cd214a2ee626e53b235b9e87b443ab64c4dec4c45fe50a076d51df2ef6e12a","input":"0x","nonce":"0x2aaac3","to":"0x59ee98400e1456902ab7235d3af1e2fe08ccaf68","transactionIndex":"0x2c","value":"0x160138685419374","v":"0x26","r":"0x5a01a830c72bf2b2942c4f3b1a320117efd63d5e612edf4898db840cba35df0c","s":"0x3a5f1675cf8223cb3d72e226775d2ebe71c76cd0c07607cd747e9ef8edabe43f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5db68a27e4f85fbf00dca00110b4e276a70472b09a253fa0b7c480def7554b7d","input":"0x","nonce":"0x9c66","to":"0x662978339d457e3c5de9ac99177d237cb577de7b","transactionIndex":"0x2d","value":"0x14c9782ba97f000","v":"0x26","r":"0xb244055b1b5e403b97f5f6e34e63b3726f8e5edfecd657895787157b6141e4fd","s":"0x4fab23be1914b18772b54af940f55c30dc6b944ab7c289381c48f2e1fc3164bc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x3c00053e6b0cb4c2c82cc08df1de2886c527bceb37400af19d151c779b691ac2","input":"0x","nonce":"0x2aaac4","to":"0x1060044fb45772fdb205a7880bf10d98b3faa010","transactionIndex":"0x2e","value":"0x7203ddf4a7d9e58","v":"0x26","r":"0x61d38abdee2ec7eec8604f90900c110ecfb09c583433539ba09fc9987b6aa31e","s":"0x2d66a3034838de7aff0bda31653ee67698bde27a029a89c60f65ebc22a60739f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5e8df31f2e5ca74f0b9cc072bcedd6b1aa9c890aae72747b386b35653bde4699","input":"0x","nonce":"0x9c67","to":"0x2b3c2f34d384a84a2db92861ef766d074d5dfe76","transactionIndex":"0x2f","value":"0xe036e48b422c00","v":"0x26","r":"0x147dce0b15914b2a5b9f3d6aceb62efab94a0fc3313bdfafc75456b65a7a850e","s":"0x5ca05bd2af4de11aa5faff07586b28d064365ef30ca9374e2746a68496139cb5"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0xf29b1ee3b69fd803e6a4336e1c2878a369c3b7d26a899502525a3e0a3988b1b1","input":"0x","nonce":"0x2aaac5","to":"0x49c059de3c341674028d3c4bd5438695423d673b","transactionIndex":"0x30","value":"0xae22078638a6d8","v":"0x25","r":"0xc6dc245f7ead2b2ea13a7c521e681733cfaed11e3f96d563cecc7f84689db1b1","s":"0x1a2837bfcc1b8eb5195d795fcdc6804c68058ea0328b42cb1ca3d90f897bc8be"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xefe29afe3225aae766b3698218cdc2ff8334871d2cd3e5a73331e8351a01cc3a","input":"0x","nonce":"0x9c68","to":"0x5ab9c59a3924a89fbeebfb614660ef5cb1dc9b27","transactionIndex":"0x31","value":"0xc510558adcf400","v":"0x25","r":"0x730a25ff42566dbd6acf5493b3dde8dc843b0954bf6474effe8a9ff36cf3a7f7","s":"0x29f4d2b0d9a042604db2082c527c42b0c12379e4b018630878caffedf5f1c8f6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x006a5d2207ef79083b3de8cc384fe4afcd78e28ff9603264eb487553292334d9","input":"0x","nonce":"0x2aaac6","to":"0xd97a422673e9f08c3a48c77fe2d880083745aba7","transactionIndex":"0x32","value":"0xae1e77264ee623","v":"0x26","r":"0x78e26d0ec880a8abbd47750dc27189756cbb45d097201de5524361b5dc1d6d4f","s":"0xe80f78a08e838680f1178a00c49750c6af34c0ff211c6472b22b5e65710b13b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8d2a63ae663da52ae3bbce4b0b193c806d920775cbfe78f1a9e2ce5fea730610","input":"0x","nonce":"0x9c69","to":"0x79e53465796e3ed6e4cfbb6108ed5dff81319a3c","transactionIndex":"0x33","value":"0xc96df5268c8400","v":"0x25","r":"0xbbce5c139e0cdfa424dd768acc1bceda22f89707e186987ea5cd652c541ff63","s":"0x3ae7cf7bcb887a14113e91574067abf179268084a6e4bfc64c97465fc7608532"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x8ec819436b821ad573f6a1fbed2a549cb8352c0035916fbfcb0cbbf007cd651c","input":"0x","nonce":"0x2aaac7","to":"0x1b9e602c4cac19e87b5faa3774414f54e362cc94","transactionIndex":"0x34","value":"0x160eb475460c2ef","v":"0x25","r":"0x9c67f12fd81232cc9b4f35fa39a5869efaf9290426a5b707e43373f2b78e726c","s":"0x14fff7e4a5d2fd57046c32273300541d87b1b8fdb89ca1f6e29083d925e29cdf"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x0f3cda751fbf72166bf419f483ef93fe40d4eceef994330d78bacf0ed1ac217e","input":"0x","nonce":"0x9c6a","to":"0x1c01da024f8674268128229b4486282e3091218e","transactionIndex":"0x35","value":"0xc3d6fc66994000","v":"0x26","r":"0xee64ba98405ef13c1046e20add36f83cffba6ec663217dcbe16cdef00866d781","s":"0x4353e70604753d5df5414b44949e6d5d3b0099ac9e908d3f386761a08dcc7681"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0x7b065e3308d58c1509f1af243bae91e6bf59b3af923b90089da3723d6ec0fd29","input":"0x","nonce":"0x2aaac8","to":"0x24702bcaba2cb34d081740605e57b1c0247fa668","transactionIndex":"0x36","value":"0xaf8e4871185ee8","v":"0x26","r":"0x6a3a5d089e0e69fac6406684950d7f8565ef20128d1bd864a2f885e70c45db67","s":"0x320c30498d3aed57d6549a4d89c96e5f35080177162396178a2c5bd7b465143f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x2a06d6d2c3af82096cb73bf602258342876c73b5072f7861b7f8abadafc28385","input":"0x","nonce":"0x9c6b","to":"0x6860e92acb529568c2c529db2e418ff9d39cb1fd","transactionIndex":"0x37","value":"0xb48cc1d8b16800","v":"0x26","r":"0x740010af0df82d950d2e77c857bf35b394573092008920faf64447a747eedcbc","s":"0x12b85f4e1dda634b7412f9707272036fdda220d5e1ffa867deda3231c1ff4945"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xea674fdde714fd979de3edf0f56aa9716b898ec8","gas":"0xc350","gasPrice":"0x4a817c800","hash":"0xfec8c03831f0a5cd907df0ba7e215ad87762b21771b1fef3ad324ad3825f14bb","input":"0x","nonce":"0x2aaac9","to":"0xba0d3ca997f8a5588dadbb7ce8000ca8ca8f79d7","transactionIndex":"0x38","value":"0x162ee6572dc409a","v":"0x26","r":"0x9f9c5920aa859759ab112d6eaf01c03bd4f8d7229224160d30446217f1ffa66a","s":"0x736877ecd30dbbc288f4f04e0e71da02f2cbf2abb52cc552af92d32fdd07089f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xca44697c858a5c081db2d5d27f0b89f30e8c4eaa92d9405cee3dcf674594753b","input":"0x","nonce":"0x9c6c","to":"0xd80e0dad2034dafbf1e56f9fbd9cf05e6d8f385e","transactionIndex":"0x39","value":"0xbfd66e5a367400","v":"0x26","r":"0x2b93cf57287f3ec365155ed3f511e4a653350e97ee21007de2f64f87821380a","s":"0x326f442a483cdfb9fceeefbefa7433bb2703cac555b583d22551f03b870cfb41"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x63cab64fa2fffa46dceed134e9731d0b54632002fe2b72d661fcb45a924242da","input":"0x","nonce":"0x9c6d","to":"0xb64b2a886be9164531a186a8031606361380c1a7","transactionIndex":"0x3a","value":"0xbfd66e5a367400","v":"0x26","r":"0x101551c907ae74aa1d49a3392d960b4101ce8a4ef7faf18c73cafee1fd81dbb9","s":"0x51d110c6cfd780fbfa6c02dad32bac18b734d7e7b4101efd3f0611d086fde41a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xec7220d4e4722386270508e0714bfdcffcd66475453ec1ced9c122bfe7fbc24c","input":"0x","nonce":"0x9c6e","to":"0x44b889082ca7cffa9f91107110754fe0abd07205","transactionIndex":"0x3b","value":"0xb5d019cc00e800","v":"0x26","r":"0x2e74c71007b9d74d9fa4de92f2c352275c0f8857883736d496388eb8acb2bc34","s":"0x63324a97cc0a246cc4901f9ada5ebf3c4a3c97b3b0697ae07b1370ca717cbea9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x33ded543bcc21f070d6e24f363bbfffffa8b8c39198493fc11252e5df2911e5c","input":"0x","nonce":"0x9c6f","to":"0x9257d8f0bde62f59f2d982ac4cd534e07d9dd345","transactionIndex":"0x3c","value":"0x17c1adfe0b47000","v":"0x25","r":"0xb1105d9b6f6a5382285f9ee15710d63bd484657b6f4563d1c40d83abdb401e12","s":"0x7ba86b5c18900e078abd585a6e63c55d9cfb51d093c4963fba6b5349f0183abd"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x6db11488011ec8e8820654a89b2f5e0e8b07e32973c5e2089f3d5f7065c7d181","input":"0x","nonce":"0x9c70","to":"0x5815bedf684599205589c23760509fa9c38a4703","transactionIndex":"0x3d","value":"0xbfd66e5a367400","v":"0x25","r":"0xadcde1c993ef446a648fe2eb469423418a993aea2315aef7db0040e703aa0e48","s":"0x695fcb0eed75e2fa344b896b0fd5e1ea7e1d2ddab9907a15c1f0d6cd48f29a49"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x30597ea097b887c60351a77a1efc12cd4a4fd2ffb7aa49564644cf43b8d0db9b","input":"0x","nonce":"0x9c71","to":"0x890910ab2c8f838de49a882235c1abb73e79a94b","transactionIndex":"0x3e","value":"0xd10ec777941000","v":"0x26","r":"0xb7669d6396e8abcbced7c20f898446ea3ce66ab6eef939f96dc104881d2ba4a9","s":"0x16a4f13c73e5c0f4885951413d683d59b8f209067e16b4c645814dfc60081ed0"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x3668733911bc9d75cd2ae0f7ec09c7f4f8a5cf979b57d44e718e92a20182358d","input":"0x","nonce":"0x9c72","to":"0x1fbb1f26b26379d9cf4a3fd152df619bc61aba0c","transactionIndex":"0x3f","value":"0xbfd66e5a367400","v":"0x26","r":"0xff3292258ac91736001885ceff8c7ed619af300f91e6abfe4e4386baad893fd1","s":"0x66113adf45b41a6f34cd895b3aef90c8d12be07e763abb0bb23d90c3768fa4b4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5357798120a3efcd659e5c3b6a075bb927aee3cc1d2bede1441bc46717fffeae","input":"0x","nonce":"0x9c73","to":"0x1e3b979311a69a5e4aaf257d2887b2340b23e5ed","transactionIndex":"0x40","value":"0xbca080a4a2e400","v":"0x26","r":"0xdf1b455d46909ed9ad17a630f0bbe1ccbbaf2c6c67639d2235fb4b5f8516f3de","s":"0x5b848302ed3867c09bf756859d72371154295e0ede66432b2e56adbae7e2c824"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4b02dc4da6b4c08222040ae4f3e1a79c0f8fc12f84134161caf188119c82a775","input":"0x","nonce":"0x9c74","to":"0x52f7aaf6429f28359c594831dd720906e9822aa0","transactionIndex":"0x41","value":"0xed90cd1676b800","v":"0x25","r":"0x3c2ead1b59d5090c0c671de8cfc2e68b1df523666f308eb1bce172b4aaaf8189","s":"0x168dd2296d99af982ee467b214d5fbdfb5d3bda5833e98d4a3c2508938a605bc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9d2338fb32b44f71d384510790c1523342b00dce554f089cdc1b76c11cbc2ba6","input":"0x","nonce":"0x9c75","to":"0x5ce8433eb2b8411bd505ee4be968751aa8f3748f","transactionIndex":"0x42","value":"0xe7962d1595e000","v":"0x26","r":"0xf71aab079829b5d26ec7e66ac88a068bf212c5f3c21cfb9fc56adb18429787e8","s":"0x28804ef7db35b88adc0b0d5943e7923a92b21e9df575a5214859f94d085dd4b3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xaa8961638349e54a3cfe762e88df9a0c81801083ae67dd8da1594d59c2e7dacc","input":"0x","nonce":"0x9c76","to":"0xbb585a66faf023a157067aa4a5b9d704945686b4","transactionIndex":"0x43","value":"0xbe0d6ff05a3800","v":"0x26","r":"0xc0e6c5572dfab3dabf6ff6773f20dc1d6088390e4a8aabe2673ee582d7aacab1","s":"0x28f11928e0b87f0fbdc189ba1098c0c2a3935c73955658f28c68772534cc1cb6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4ddba646404a84ce65c8566f72c52019faa62e3daee934bba7ed65dba0344d96","input":"0x","nonce":"0x9c77","to":"0xfcf8483d73472d9fec2c3daf98b05618fc5f659d","transactionIndex":"0x44","value":"0xbfd66e5a367400","v":"0x25","r":"0xaeaa50298fe64ddc28ca9e4e3c292bd7f31acccbbaa8e83a90e26f0d3e5aa826","s":"0x136c478f5a0534dc5aba89bbb597f65a64d54747560bd56e00a7d2ec79b88b1f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe3dc71466400ca4406b73dc9d37360752b5399949e758004c5898c6c8dbd19a9","input":"0x","nonce":"0x9c78","to":"0xecfee0a3eee9ba6daa6ac29e9c0cc18ac4302f5f","transactionIndex":"0x45","value":"0xc3d6fc66994000","v":"0x25","r":"0xecc5896a0b0cd9dd48efe7c4d014be27c6598205e89a10b803a0f744fa9e9618","s":"0x6190c56db4119fc23fa85ab9f2932d0634682dc472715fbd07919c4dda06ecff"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8408dd887fd59a64e665de856b9815f4b020a6721210a13be19eb55c5c21eead","input":"0x","nonce":"0x9c79","to":"0xc52478f306bc7f45ca93f26ec27b03e03eac7c45","transactionIndex":"0x46","value":"0x2a7700844a13400","v":"0x26","r":"0xcf05b89ee3b870440ee0dcc5810ba8818e43e5eaf300f0d078af2579871177cd","s":"0x6c2ff2f96d8239a18b581efbb38da45fa648c27a0f8a709f20ca017a0121c43e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x0c45616865e3111fce61c7f9100d8f8a76ed13ca137b9a3c5b44402c8e82ac50","input":"0x","nonce":"0x9c7a","to":"0x9b49fb099165fb5eb966d2999e04bd3f6f175bb0","transactionIndex":"0x47","value":"0x6b7f99b36c8e400","v":"0x25","r":"0x8c9021e0e864d0e874386aa25fa7dfa2316077327f3672dab7e7b5c343af47a1","s":"0x46f028b207e2dc03a5f0116b07e4e721abad7379e8775c861fa1ef5d25d84b1"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x0a39cb1bbdd38b2a94379ee9b22fc14c8f4d3374c49077bab4cc48ba9779a02e","input":"0x","nonce":"0x9c7b","to":"0x25b672142b7e4f0d28cdacaf94caf4f4ea34c09d","transactionIndex":"0x48","value":"0x3b6432fb1c31800","v":"0x26","r":"0xb9be3c1bb492cdb18d6d50899a3adc0ae0f332584eae98e1049cf3b1096fcda9","s":"0x74bf6457ca137554ddfa6fe9a86d0474bfadd0c83890d7feb226cd79c7ae0de3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x95208af935d245e659f146820af71ff0a7fbfb353c4fa32823e8cdf4062e6dd8","input":"0x","nonce":"0x9c7c","to":"0x55aea382d3f06b0591a12a1b0dfcae08d6a5903d","transactionIndex":"0x49","value":"0xb26646c5657000","v":"0x26","r":"0xf622164cc9bd21c57f1417089e45fb64e589f32663db953cd8581f7d51acbe7","s":"0x10633d8c1ed7597f94e3ffef7d2cea86b1961193cc89e00275ed99fa054e15be"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xad3b5298c4aa0a1ac2d3c5d680214626d69887d3127d5883cf306f02604d9127","input":"0x","nonce":"0x9c7d","to":"0x493c979945440205866ed35bd7df2284cc5e8aef","transactionIndex":"0x4a","value":"0xb81dc0e359cc00","v":"0x25","r":"0xa82e22f5756a82153ae1c457cf16f74f49f42d07a585877922462deb4409e394","s":"0x2b7bcc0575ad15ae9c6bd8b61b62548bb9e4a8aab41c67dd95a7fdda4b117934"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7daecd2bbfc31817012c988b1325deb998bebdf643a3aeeeadf302a534227f24","input":"0x","nonce":"0x9c7e","to":"0xfd47827a6bff38abdc3fcacb145ccf60326ffd1b","transactionIndex":"0x4b","value":"0x17c1adfe0b47000","v":"0x25","r":"0xeb65fe7953e57784d2607de0add1aad78fe5365ba4eb6ba323f0d28550095440","s":"0x5c04a96968949b3dbdd1ec1e7bd83b1f186cc96b5ae3a394cea9adf6cd53d507"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xdf5b469afc9a6ca28bc7be614fe46726bdd93b069cdebc856f52deff0e32f8f4","input":"0x","nonce":"0x9c7f","to":"0x416e269cc2bf8f9cf56cd70038c0714bb2fb2223","transactionIndex":"0x4c","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x51efedde07c47ae99371c25ae1474c669cc52b100f0ffda4be4936e2892b9331","s":"0x53f6f325d1da57dad6ad2cdd961fc67dfce395372dd18a7774337138b1e2dd9f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7c9ef0fb0cb6c77a338310f8fa497f2af24dc03bc9e05fd5946c2096603127d9","input":"0x","nonce":"0x9c80","to":"0x6fda165e0d011eaa77f70e24bf515abf4338ea21","transactionIndex":"0x4d","value":"0xb2664919715400","v":"0x26","r":"0x8393b13618da6fa0a79851675d230d53f5e32db404f80ecbd319049cac7ebb7d","s":"0x5ffe6c3a035ac49c22e89b547030c2384896b6cf09a90a7de67114b15ec81f6d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8a6d742d1266639c3ff41ad8424d8e5632ed7cfda5795933f05b9c117868a9d9","input":"0x","nonce":"0x9c81","to":"0x2e4689b51bf43fdaf874f3baaec1b750ad15f45d","transactionIndex":"0x4e","value":"0x27ac67bbdac0400","v":"0x25","r":"0xd22d20eded3b91d1842bed217d4e6dcec8c1b560114963d8b2eee281b4686bb8","s":"0x15e26c42245398df1b6f64927c88081a9e692e18358e9d73b6f3c28da44dca63"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbae0db1e3b4f15e0dbeadeff006bc099f72c33cb642077d1825ec9e3966ec572","input":"0x","nonce":"0x9c82","to":"0xbd822e7b7db725c3bbfe7576e24d3c0354497981","transactionIndex":"0x4f","value":"0x17c1adfe0b47000","v":"0x26","r":"0xc0c0f50432bc3e6d02e68909742a0a344cc4f593b548915d5382f4a0899bd868","s":"0x76a0db87a98089f7d29830934bf1a42f6ba3e4ba558c86768f2e08ef8ba808f0"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x83b64144c19654f052676ba7c78771f7121d933fad2ae0be8e950d5b99e16f73","input":"0x","nonce":"0x9c83","to":"0x2d322adbb9984eb45d07e5c219e325099420183a","transactionIndex":"0x50","value":"0xb20840bd382800","v":"0x26","r":"0x3df40d99f3f47dd3b6d1be21e466f765d7b3f17cc8782b07542c7ceed3408057","s":"0xc0db1dee0f544135878b3fc6767b6034d3f6dccdb113a1195a781f234f91e6d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x2e630e16782c85a6046333c8d046004c60905a1509e56a9a3ef8d2a87ff39340","input":"0x","nonce":"0x9c84","to":"0xdba59dd839fb2d3535802e6d187b72c6476be686","transactionIndex":"0x51","value":"0x1203212e37ba400","v":"0x26","r":"0xd15f940513577217deb4921cd4875b55e5c236135c1dee2a161bd13bf489d4cc","s":"0x53ace39ee25fdf63c746c6084ef0b2e53b8f9a10b364704169453760a0e28124"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x95268414532c05cfed0aef91909f68b4416251cd21999e47857561557a33eb08","input":"0x","nonce":"0x9c85","to":"0x686dfcf430777442606254a0e36f4dad68ac9292","transactionIndex":"0x52","value":"0x360f3a05f77a400","v":"0x25","r":"0xcfdcb39b9d026e5265bf2373fbefc27633c97dd65865b0131ea353d971f78c7d","s":"0x5ee707a379ae0b4e7c2566a7034de41cc9e7fa6879db31498df02763a1217204"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb49e13713a8ccee95c78b26bdbc900872de90608b44789ef721910ec74b31f12","input":"0x","nonce":"0x9c86","to":"0x6c429bc1c51930f2c4b5e02dcf7c01e5fbab1df7","transactionIndex":"0x53","value":"0xb35229ba10b000","v":"0x25","r":"0x8afe1f6dd3247bbd388f02038524ae92c93f8a418e5e02ca6d4a0d1ef29fde49","s":"0x257449addbe86c06d81f31057f2a4c39a954110d90873ca184d4eccbbcd7776b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb109ba1e2415021fe64320fdb1ac4a130cdf38f1f59921f68aade8f47f23db3a","input":"0x","nonce":"0x9c87","to":"0x9b128e46a15545ef9656806155f940e3466308c5","transactionIndex":"0x54","value":"0xbe31ef6ebf4c00","v":"0x25","r":"0x5079c8212f9291b36a1d9052e6c04412925770ec63828dfdc07c7c17a3a90cef","s":"0x38ead0e006a6e4a7a9e4fe58710cb5b08d388d8fb3fda9195ac799c0e2ebdcb3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xcbb6fab536358150be07d80ebb21d2ae0cce0c8315276b837667ff8ba1c42d54","input":"0x","nonce":"0x9c88","to":"0x58cded315eb642a8806a0327a505dd04ab3e5774","transactionIndex":"0x55","value":"0xc4a234146d6000","v":"0x26","r":"0xde626c5dae253d07b126b37b53e43a2280c1de5fe5bab9dcd335f232dd1035be","s":"0x3beae86e6665767e70196ca3442a8119194150d855b7a5d710c22bbb2f45b8db"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe6e296227aff21dcee63357daaee930a262b8d3de5272889774a3ccc78bb09f5","input":"0x","nonce":"0x9c89","to":"0x55e425eb13f8f9d3ee84da9ef721223ce595f427","transactionIndex":"0x56","value":"0xc1a2d5e17dac00","v":"0x25","r":"0xe6fd8d422bd3fb8ce8c3c2f45f1bcd830ace8c3d7d583eb46ca3e2e319e5fa0d","s":"0x42f037accaf12c030392819efebc9614ea8ff3b950f6b35b85aebc1ab8b5dfa8"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbf934a74e1a949bf6630dbae24c0a5e1ac655f798914bbba2b276d756a8703ef","input":"0x","nonce":"0x9c8a","to":"0xa31fb325f638ce6f900d07159d036079ae7a1888","transactionIndex":"0x57","value":"0x17c1adfe0b47000","v":"0x26","r":"0x3afdbe081ebc912730ed377fed0917bf3a7512c6d12591262e02aaef583b15ce","s":"0x2d7f01ccc1a049ec0fc197980acc3dc80accd133edf4b1c2125c9a506a78c412"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4f895c2f7db9d7977aa003a04448ef070b07e558366c9ef7fc4101f4c5d00f63","input":"0x","nonce":"0x9c8b","to":"0xb7cc039691cb2c51b3202dcec8833f7294adfe54","transactionIndex":"0x58","value":"0x15f98da41d1c800","v":"0x26","r":"0x1520c62dbc3689f64d70fee49ab384a7c98663396618135783dceced04949218","s":"0x5b833f924dd7a046400b82ea2954bec85b63ba7574e46c5301024331d5417150"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x89ef049ce110fd411ae6c8c54e2533d319187e595f5f3945122eaba4e9b427a0","input":"0x","nonce":"0x9c8c","to":"0x2687cd65def8af50e18390199f6e97b0ba72dc2d","transactionIndex":"0x59","value":"0xe4101efecde800","v":"0x26","r":"0xd3961e8f1a2e38c8d75418233e314019eebea31fd7a8cec038a9ab5b7255f857","s":"0x3b5747f6c422427c2353309982c9a625d5217d3d76e0990afc4cabf871cf39f9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1f07df926060bc3230dff2458fc219b979f6477f3776d427da441d46bce8f95e","input":"0x","nonce":"0x9c8d","to":"0xf164395df8e000dd4a491be5111952280b2b223c","transactionIndex":"0x5a","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x38f3e7926c67197215a0cfead5022d8868c8b63d8845ac01970037b28f875f12","s":"0x429a6f8ee5d836ceee89dca0f7b0f320c46f565564ca751e14016ff25808fcda"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe335af1cfe6732b34515d2cabf4a00495620726620352378864ad80734e0570d","input":"0x","nonce":"0x9c8e","to":"0x7610640b90b17452501bf94fd8e8f37bc0adfe62","transactionIndex":"0x5b","value":"0x15e55d178169000","v":"0x25","r":"0x13d75120136fa3d8e604aad3b9de1ce1817508db8018677a982dc4e141e18f6f","s":"0x688869e08f498ba4aa3a701c6c15136e96f09aa2ce93e0b8e932074044ac95c8"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x541eb3a6b744879dc009ecc18f38854f587a48fc6c9d27bd71ecc05808a373a8","input":"0x","nonce":"0x9c8f","to":"0xb1a599d720d092dd00b53994ecbb30cf765dec36","transactionIndex":"0x5c","value":"0xc2542436fd6800","v":"0x25","r":"0x9df012b2523eb9f05084b0b215d65e4491afc3f1e526d77e08b35944b85d2cf","s":"0x644b5c2b1171871ad1f7b9a9a4d4273a08b26897f3cc45b9e325e2be48e73044"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xcb64bf354dc6953f8fefd125c52c6a28df664607d8705b51e412de57d61fc782","input":"0x","nonce":"0x9c90","to":"0x1559563f25677581d36d4e624473cbbf73e15180","transactionIndex":"0x5d","value":"0xc3d6fc66994000","v":"0x26","r":"0xad7031b60b01849774d08381af4a65a3dffde85eac5df96040f008bbc950cb6b","s":"0x44fb8a0a9b3d3f26d548ccaa209ddd3b24d1ba549bf60719e451ba780e0bfb29"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb5f62b55faaea308eb18733a19556e2730ec3e9f18f8ac6be15af6e46899837e","input":"0x","nonce":"0x9c91","to":"0x4d314bab18394b57c359639c876ec5ec6a377fb3","transactionIndex":"0x5e","value":"0x5b414595a77c000","v":"0x25","r":"0xf80db29fc81d5d80c120b363f2321b092de2e48bd2dd254a2de0c6e6b33bfea4","s":"0xf7fdabddeb455a59822caa8242f006b1f39235c8bea947fa1a98f04d15ab37e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb7e77c3bc9ffce6c6657282147030007578f15fb0c2b68bf46946e04f381a22b","input":"0x","nonce":"0x9c92","to":"0xbe007fabe0abc3da8b05e1d6ea261056678b8a2b","transactionIndex":"0x5f","value":"0xd10ec777941000","v":"0x26","r":"0xedd33b9aafdbf64b01298fcb08376dec064cfe65d31ae2707d8ba14774b85bca","s":"0x57caf3b2edff6170a338583b5667f1bc83109bf89b774eb10c5ee19a35fcc81d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9de4222cf2a3f10d6fca1729ac7d1669ca981fa4a3a2da22cbfbd98b394d6707","input":"0x","nonce":"0x9c93","to":"0xc3f8a457c2653306e03141fe75a9877493dd7343","transactionIndex":"0x60","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x918dbce8ebfafe208ff78df2710e99f3c0f2112a60027787a0af8fb495d8454d","s":"0x52fa55ca6bbc1cd081edc5b99f4ff131c6166ef9c1752154b59683ba3235f4de"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xad0546ceafd2779a6749d6e2501eb69e291bafb0830dce469eec76cfea70a773","input":"0x","nonce":"0x9c94","to":"0xb769832eb660e512e07258bcc36a0dcb76efac35","transactionIndex":"0x61","value":"0xbfd66e5a367400","v":"0x26","r":"0xa453717ea557e0951f5ed4d1b1afbb9887cf4a5249782ab9cdca467d88e3b0ec","s":"0x5cb9307f135e689c1c5d6573d7f1eaa7517ebbf398673c5fd853716c7dc0092a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc9bbcc2bbf50c6222cf213528617fb13a490ee05581aa68313d206ea1519b2df","input":"0x","nonce":"0x9c95","to":"0x571eec232518d5bb640abaabb4a0dc90a9923fb6","transactionIndex":"0x62","value":"0x2992f07c93bc400","v":"0x25","r":"0xa04da77da585efc49ff19dfbb002379f48bbdc76fa7b7ca92b49c80eb89b951","s":"0xb1dcad3506d3095da8256d6516aeb0f08d28342e5721f85634ef796627b2a7c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x39ab5625f2e3063484f3b8576f4f3e14c5002341f9c287df9b438e5d8fbd0060","input":"0x","nonce":"0x9c96","to":"0x9d34d6f0b5632fe1a5103eff1b051bcedb4ff55f","transactionIndex":"0x63","value":"0x14c9782ba97f000","v":"0x26","r":"0xe8e7a27dcb4295c3177c50a8516e72285bb27f4700508638060009e22efcafb0","s":"0x39733b86274675763eb8f8ea5015d6f37f220fe3bb86ba088428e9c639c4861c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x160c2bd753685879936c491123d8ed1b6ddfb6c24fac1957a7d2ffc83228ff90","input":"0x","nonce":"0x9c97","to":"0xfe1d3a10df8ec413d60ddbc5f864372785b15a0a","transactionIndex":"0x64","value":"0xc3d6fc66994000","v":"0x25","r":"0xf4d4a11d1ac2380662544373f650b4501357b6938f46a8cc511498e6f9af2fc6","s":"0x5d79de1db6e91984fb9fd0afd231d9c218fca8565746b8bf562c5275c82633e2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbc7636268fa7d2e2dbb0a55f27891941b8175ea87c0ddbe81e6c2af867279ec3","input":"0x","nonce":"0x9c98","to":"0xcd865711992c4ec65c6a160b53c89a7d6ac6ae7f","transactionIndex":"0x65","value":"0x31a272005eb3c00","v":"0x26","r":"0xecd2574b39a2a580e8d3d1471d76001cb926af5e644b6ce99625d5509b534471","s":"0x4e5ecf8ebdc96a1aacefee24f842f45d3a0e26104c75848c3a1a4eba1b1b97e2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x937bbde3396f50bd58b228acdc6075099afe6abf9e4f1cf6ca6bcd70f5768f80","input":"0x","nonce":"0x9c99","to":"0x1a31a3cfd3572351a03e7b28a2c31560a918952a","transactionIndex":"0x66","value":"0xbfd66e5a367400","v":"0x26","r":"0xe26c33c48e8d86df5f1f518bf3d1b68b56c589afc6874836b2968881708b980e","s":"0x1979c6cf8c46b224b55e0018d1fafdf7fe8d8623cdd50b648a83b019ed0806e3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb03550a7b57e201b1a2fd1afb376c66e6bfe3425dd948b89d439849939899e57","input":"0x","nonce":"0x9c9a","to":"0xcc2171d4de600277075fe130e0d36ffefa99b5e7","transactionIndex":"0x67","value":"0xcda1be8c933400","v":"0x26","r":"0x1bfd0376436155b78ebaf2124d9d0acaab4dc2067814529978235d03f8bb5ab2","s":"0x54204e780711ae4c4ae1b7456280fb14973308bbb75e830986430f1f291bc53d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x99e6d8bc383e246a6a7a294135344f16346b53eb9fc228da27aff8118e99d4f6","input":"0x","nonce":"0x9c9b","to":"0x92b264dfe333e5f73122225c41cd73db8cff9337","transactionIndex":"0x68","value":"0x1cbed51d319ac00","v":"0x25","r":"0xbca9dda64074c1b01225e1a1bf5d2f6e54ac94ac9c014fa17987815f9dabf8a5","s":"0x7065c3e426f83910d6e4401c877c071ea94cc3e4393b2fa822d637ac83474348"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1fe4bee972c52aaec26617e86fcf8758eebbbc98b952a8158247af139ed2b54a","input":"0x","nonce":"0x9c9c","to":"0x4ca85ac8e93bc77355db733f4111bb09c345091a","transactionIndex":"0x69","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x42eda4e90e20cba06037bc795dba4c76da79eb3e2bfdcb1bbc08287925816974","s":"0x5b10aa514cc2c1e3c517804ac123e38662ff1a1a67e47b840df5304c2a2af2f7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4597183867eeb0b2195d6d367dffa37c25aa46aff4436b7ba225bccbb3579c7c","input":"0x","nonce":"0x9c9d","to":"0x2b1f68abe6a29b3edd64ceb21ef29158e52590c0","transactionIndex":"0x6a","value":"0xc1da8171cc3000","v":"0x25","r":"0xb7b87b68455e7cb5eb9db18806aba977f6f939ac144bd569da37395d806900e9","s":"0x502a37059a0e7b96840e290676ce5942ecdb1a1aa25757fdd56d7f66976ebdc3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x0bed04a6609b66a5d8b6bd6ce40ef5325bc4c696aeb37776d4d83ec8e7ecd961","input":"0x","nonce":"0x9c9e","to":"0x7924e5578215cdff5f181b64da2927923af16260","transactionIndex":"0x6b","value":"0x14c9782ba97f000","v":"0x26","r":"0x572a3bc3e383ef8bff21c48de4073a88500771cb36b898fcb89dd84522def105","s":"0x7ad3f5a4329166326649743f6ed25a3b2f2556464e767dcb4c36dd837b059ce7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x73c10857457e3f5c45895d9a77fb438b3421eb7c28c26789e2c9cf8151fb9cac","input":"0x","nonce":"0x9c9f","to":"0x0253cd09335b8df37e1c5473ec99a6d70eec1766","transactionIndex":"0x6c","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x3275a10bbe9666457ea5afc4d59e675b0144f76b98663145addd4bc799105e6e","s":"0x5568e0f28186411f3d70bbc4270ab0ab61c08019948350a918390c0e281e241a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb9eddc2db16ff61f58001642ca2a51c8ec95ebfe9e5b036421370077137e193c","input":"0x","nonce":"0x9ca0","to":"0x7651952fcb8ffdf86aa45ba15cc5b17900e2a43b","transactionIndex":"0x6d","value":"0x126409b8e091000","v":"0x25","r":"0xa5e27911e785f9a70a759a769c71e0dca6cf6bbeae4f57c3bdffedfa1bf07fe9","s":"0x301d8a3cc91d42a32377755b8f59a3cb98f6cac73bf437a125a9532aa8d48769"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd31e51cf1dd441bb59f560208a1f623c8c46be73b64e05d4a502c24966ae2ecb","input":"0x","nonce":"0x9ca1","to":"0xd4f6efba0e8afac5070e2f212ea2399890c661af","transactionIndex":"0x6e","value":"0xbfd66e5a367400","v":"0x25","r":"0x25a991a9e975affade3700b25c8f643e0f5b2da33c080884489c0e9d08de74c4","s":"0x3059552205c70ae443d68470007e1df0ab8590f3bf8c3176a81ca0450a3d4779"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8dffae13ab7104649563acd462619eff57ed1c6aceff9b89aae020d377502113","input":"0x","nonce":"0x9ca2","to":"0xd0d6468dce409bab6c90ac104e43cbde0683ec0b","transactionIndex":"0x6f","value":"0xc3d6fc66994000","v":"0x26","r":"0x5f96592f3e82ca7b68650642d9db0f1ad1224a26341408e757c298b0360e83fd","s":"0x312df679b162ea5c7eeed6295bdb93362a03ffb3dcfb74c5012f9acfffc2b071"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x909401b08022a35b0e6bd1efd6ef2b9c3e29e29884f4358ec1047998e06462eb","input":"0x","nonce":"0x9ca3","to":"0xba8a931df397f5821766d764dfc1123a12725866","transactionIndex":"0x70","value":"0xb2664919715400","v":"0x25","r":"0x8703033f77bec9be225e4edaf8fd4c0067d1e94b4842f039bd872dda4b4f44a8","s":"0x2895e3d128915a86b138eadb90a669facd73fc1c0050ac7ee7091d212de08ed4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x58c48a7b652c7382cf7193760b16fed729c000668c5692e0d1185b7486c221ad","input":"0x","nonce":"0x9ca4","to":"0x4ee1bcaef4fbefa28184325e6a9c4a57d6c5bc83","transactionIndex":"0x71","value":"0xbca080a4a2e400","v":"0x26","r":"0x1d306f27cd242ed559942c8dc28da48f0d3aaa37e99c3ff64ddb3f58a7107779","s":"0x106bd74d4b7249b7c410e289892066d91e0da4b63581e2e9f93f40471bf8ed2b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8360665b9a5b6abeed5d63621ce4a22578aa3eb27090429b676db106de0297c1","input":"0x","nonce":"0x9ca5","to":"0x5d6290be073fcf27fd1affd5f7703feef07f3d5d","transactionIndex":"0x72","value":"0xbfd66e5a367400","v":"0x25","r":"0x665380256bac009ca3bd65e34d8829b417e9ad73e5b9994c875472b374becc07","s":"0x707772a8bae78c26f22f8375f34d277b43db9a1be118c3772e9cddbe76c2e31"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xba38bc44a84a1be2d3ef1b104d7d24b9531951587d8801f55b7e71f442ab303e","input":"0x","nonce":"0x9ca6","to":"0xae0a808e2a772a4302cd78aea2ddc3ba526c6ad5","transactionIndex":"0x73","value":"0xc3d6fc66994000","v":"0x25","r":"0xba3fd5e06b4460de11a18ed944efe58d89175295594d7130a2dda4c73b5f9f39","s":"0x20531064f3c4b9dfb9d7c2e08dedfd4ec187b0525a1ed5e965d748cdc29eb5cc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9333f58acf1bc7e92224104f057671998592a1909cd5b3acb9c12ee92b325f0d","input":"0x","nonce":"0x9ca7","to":"0xd482e0fc8213cb979aee9f86dd488da365019e5a","transactionIndex":"0x74","value":"0xbb0aad48175000","v":"0x26","r":"0xeeb8f6c5dee495a379c25a2e6f7be0f4779f48df8a112dda804af184f128260f","s":"0x52f8b6b9a51711a16437ccb853c95dff6099b00c7b30a04661b6ccd24e8f1146"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x34f3c1e3163f4b91dbb9a07eb139a6128e11638c944c688064601c1acd5b0500","input":"0x","nonce":"0x9ca8","to":"0x6254be074cd9a548455bf7b852b4c37b1bfe3833","transactionIndex":"0x75","value":"0xb3d90a82e2a800","v":"0x25","r":"0x3722cdea2984b42025e756114fa881d09cc234b1832bbfe5736f1a7c560b408b","s":"0x282ee2ac69f52de76906ce7586d6d1ae74173dbcd0a24a99f80315d414bfa74f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe50993d20c4c2173666f7407fe8a2d51dd4568f55f938427c7383cb528d7d9e6","input":"0x","nonce":"0x9ca9","to":"0xb6e4350b195042a6e2d614aaf2f55c2a250b5d4a","transactionIndex":"0x76","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x57542388d21ed1659704be1021983b6dd7e8c9969f8a3ae1bfb672088fd26955","s":"0x23163ad0713433796a3590e73d4f1c2975b55684e725506f806a25ee715e1f2d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x6bbf3a2ff8d375155554f2393fa9146a543d0a2d989ee879840c5210d6d8af9e","input":"0x","nonce":"0x9caa","to":"0xcf56e5ed5ae72a3073947495960fa7132e54b3df","transactionIndex":"0x77","value":"0xd3057bf2e29400","v":"0x25","r":"0x6b183cf8cd9beedbeb0a9c6d665f422c3e02fcad2e6034a0e5dcf4efb35110a7","s":"0x135376a5a6bc6d5f902c0111f612546166674dc9ca7872b971f7243d046b5415"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x3d3397dc7750bef59af42dbe05cc0012b6155779f36d18e07bdebbe9503d4886","input":"0x","nonce":"0x9cab","to":"0x8de0498a27ba339552efdd739e9feb820059dce6","transactionIndex":"0x78","value":"0xb63eec35f82c00","v":"0x26","r":"0x58ba1a512c9aba3d9f34b170c2e4e111432c6008a553422484a762d4cf0ebecd","s":"0x12544bf2a888c125beb2a9301163294e3a7c041d3d2b109fcb9e4dc809d68614"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9ab6b22db7be7a6995a8c62b3b00a59ea441f62c56b9b3520a431f3c4555643d","input":"0x","nonce":"0x9cac","to":"0xe0344823f21a2e00f17a0afc808fd4c6e002750d","transactionIndex":"0x79","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x86cee2916626102013acf7ca7de60bac8d37b534664066a4a077454608527efe","s":"0x4012fa62f4bb99242ebca7a0bd15cbd8fcb3eaa3b23f7dc5a79900bb8cef9049"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x527d15925e495fe515e1e3367616577d7ec0744094d35bd9546827c33c0b5530","input":"0x","nonce":"0x9cad","to":"0xd4bba6144da7295055fb1b1d1dbec86da8b4d21c","transactionIndex":"0x7a","value":"0xea7b427a49ac00","v":"0x25","r":"0x67b6019a6422ed9ff8560ba76a46df47a0838139ad2db752682738753b6e84a0","s":"0x2711600ac97e3f070f9de07544b188b5875d14467025e981c02cff15bce86fd4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4d3d5cbc698fce23a2ffa4eda0e151bc0e6ae5d98629c9b4b77234fa22a30a5e","input":"0x","nonce":"0x9cae","to":"0x54801393c02e07ed8c5aad855dfb1cbfc8c9a9ef","transactionIndex":"0x7b","value":"0xc3d6fc66994000","v":"0x26","r":"0x35daeb020d4dfd39721785c2d28591e902e53ae245eed0ecdbc25ac7472528e1","s":"0x28d959e8608cceaf0342bf71bf0332bab49c67ba9b76597a136f79caf1a05492"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd0851fd1a3aafcd50ead7aa1e6dff1c9d2cd09ff4392264718d6d1b8ec027a26","input":"0x","nonce":"0x9caf","to":"0x2fb6665a77c8c6935aae38cc8cd63c79f4978f23","transactionIndex":"0x7c","value":"0x17c1adfe0b47000","v":"0x25","r":"0x9309016ed84c7aae11d7460539ef350906f7b3fe48a92be2571c9773873c86f2","s":"0x1b6dd64e9dc8701c39a17f705b04c81ff8ab46607bdc84c77c212cb293b74617"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x122580b9bf98702f5d63e9541a54dddfc3baca0e9856e2bdf8dcb0cef8209992","input":"0x","nonce":"0x9cb0","to":"0xe7b54f8793c8bb9b29e492ad6e4f8d6d5f5be164","transactionIndex":"0x7d","value":"0xc3d6fc66994000","v":"0x25","r":"0x2c2e7929bdecadb75e1bf53add116c441a11cdf535566f37a6adbdde5dfce143","s":"0x1c684ac9766eac8558f7064e346433bb80baacecf336938c4136e0558efaef64"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xae661bbfc2e410190d8a8adf3af712457502e0700dda717a2a22e7adf94317ed","input":"0x","nonce":"0x9cb1","to":"0xe05ccc1b7a0313fdcb79ff3eb0305e91d5c487a0","transactionIndex":"0x7e","value":"0x2b480f427177c00","v":"0x25","r":"0x6ebe9fd04f7d8a7ed086be355c8385ce99094cd822a42f4b710926c4a04c84e9","s":"0x38cd85fb70f0a9367ced78bf6eed59128ca755f8ab7d3c9a2766b85c6e81cd8a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x399efc768c069ba010a4dd3d6a522e4e215b4e6b183b82adb73bdda2b6ec52ce","input":"0x","nonce":"0x9cb2","to":"0x349510b999a5fda5db4780e2aaead90d1e5ccc50","transactionIndex":"0x7f","value":"0x2f835bfc168e000","v":"0x25","r":"0xd40338eba03278577e0952a6c4323cc678b0953e6a4a6915263562238279fddf","s":"0x180f76e6f04cf4ef6fd8d5cd76aeb38b190641ad858c28e3431b9844623a4c3a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x03c315f23d10b806d1ee3903c4c0ab5bc2648f81489f37e657134a0ebda47a36","input":"0x","nonce":"0x9cb3","to":"0x47871f0665a2e91aba71c73e13de5b11155c8cbe","transactionIndex":"0x80","value":"0x20aa4f2aaf22800","v":"0x25","r":"0x52d4ed029392502b17d06324a92946efc40933fcbf1a36e72ec7671daf11f35","s":"0x4b3b4a12743f4ada37757d363da59bf27c4eb9d923b36b379dd1dd47c3f13e05"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xaf5ba7e04bb64cb7b2f61d81261c6172507d26b18b42b30f49d43affbd8d23d8","input":"0x","nonce":"0x9cb4","to":"0xce260bc65ff5f6ea95efb3dcd5620f4591f5dec1","transactionIndex":"0x81","value":"0xc1da8171cc3000","v":"0x25","r":"0x8f60322824210fdc76f2c9f2d83b64a650ba21788f256da54dffb1278c1ab1c1","s":"0x298ab7cf17efc560d209a520a600f554d5971fa77238fe255b421ff187948e86"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x660d9a64e3c96e78622f870ec011091c3c7fd9fef664ec5573d65d2c0f89f3ef","input":"0x","nonce":"0x9cb5","to":"0x1749deae94e5fa3c9504ab2849168f335c4fffa7","transactionIndex":"0x82","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xa83f70d6054ac06aadb460225c6465d612bbcbc956d022e17b50bcc730f6012d","s":"0x1a66523c602ebe0a5f5bd0ef9c918155dfc17e32bab33f1182f19194f84de284"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc2cf524f088f4496a5731d36a0b6929759153ca770ebb77ec291d9ff27441d3c","input":"0x","nonce":"0x9cb6","to":"0x3bfe9da8c04e53b387196d30ef1b635ff6264bd0","transactionIndex":"0x83","value":"0xcc4e706bbfa800","v":"0x25","r":"0xf0cc09d24d5fdb64e9cb286e1c3ccbf3f110b3d4e110b192d9de42407d692600","s":"0x240ddd910ab5cc583ee199e92c4713db2f7dc9490b8d6d1b9333085b539d7526"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x744b645bf4ab7711762b1ec7a7eff192203f6944efb392038b4dc9c3a27c14f0","input":"0x","nonce":"0x9cb7","to":"0xf2ede79c5a212432ee3e966386e5a01611c79363","transactionIndex":"0x84","value":"0x2a606995ecbc400","v":"0x25","r":"0xf9e9b8cc015f0a903af9981191feb4b5ffccee7356367c20f4bb0d460cb6ece6","s":"0x1d08280e485e095bcc78f50986630abda306524446b1f43c9b3b4a4e5b4703a7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd068bf23a5d7c792e8b4ce6eeb5b19a6a12dfe5fa3e47bb264d8ea4c6149afc8","input":"0x","nonce":"0x9cb8","to":"0x84d12193cd5f827ac842f98c9a3ea8b5f01e6542","transactionIndex":"0x85","value":"0xc649ac5b14c400","v":"0x25","r":"0x67f9035e3dc6399c195e29e1cca206d6271e8817b78bc7ce8045e67fc21ba952","s":"0x74e7a387695d3a38eb1b213d5a4cad1e656af4a1a2e08d0cabc246b633f630d6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7f61182b71012789a75dcc019c21907390108a669ce7ba70f95c7174970a1f5a","input":"0x","nonce":"0x9cb9","to":"0xda62fa9c85567310844408d4ed1af18b971b3d61","transactionIndex":"0x86","value":"0x14c9782ba97f000","v":"0x26","r":"0x82524dfc74b9f7ba43e164dee5ef1513e2ca9173e6813c7c26b08f0bb4137f7","s":"0x12ca085a43e5508497f9e8bd8c35307ebbffb3ca267a9cef2edb1c514419993e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x61fed8d0284933c47e3d83dbf9809e94917087ef5db0343773a53d5dcc494b57","input":"0x","nonce":"0x9cba","to":"0xed63003b5b433e274b225e2669815941ec23a320","transactionIndex":"0x87","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x988fbda5dd633ce6c9848077b0defc7da98497328dadde7e836cab18d272fdd4","s":"0xa3d1c80b43be4d933156820e7e0eee5e720ddfbd96bd59f2f54a6fd7f2d2072"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc1ea753339019b98d452c9492e3df47ab9363513095d5f929daad42417b6f825","input":"0x","nonce":"0x9cbb","to":"0xda46a91896cd97e7c94924b8ddc2715554d1ea7c","transactionIndex":"0x88","value":"0xbe199b367fe000","v":"0x25","r":"0x7c5c5f220ebea15518f78e36be2d2a170d5a4a356a42cc562772e601deb14b6","s":"0x59217c69942162de414af122a0de01a1ec00bc9c03246c0530de3e15db91a73"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x999eb2b04f57d490da06645c9acb2f0cbea22ce3ab327f4a9636c61ecda5d598","input":"0x","nonce":"0x9cbc","to":"0x4df8fec170166dafb5600350c9947aa999647934","transactionIndex":"0x89","value":"0xc069c2969eb000","v":"0x25","r":"0xa8a89f485bc8d8f53856cc044d795424e4bfe33d5a5f840b1c3f37ec7ebef4b","s":"0x39e73c09306b3f47a8166abe6164f305ca88999df5b02b7cd0527849beacf002"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc711ac0e81667190af33ef264b76d3c7770699347543ff69cae7d9c6175d74da","input":"0x","nonce":"0x9cbd","to":"0xbd4f0ba5c8584b9ec978745f9a03db5784a08527","transactionIndex":"0x8a","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x6ecd6d48196853285f57103c76cc997c26b3ddfe19e26f3880565459d16ac5ed","s":"0x663e7698cf7d5bafb65cd1f2f8626758fccc1a30c47c9a80ff5cc37b5ae905b1"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa04a3aaf82b481df7a09c053a6b8de58997f9f273b4990939ba0735c62c773d5","input":"0x","nonce":"0x9cbe","to":"0xead137868089c6354d4bd1339e8320729d68b57b","transactionIndex":"0x8b","value":"0xc3d6fc66994000","v":"0x26","r":"0xc6208f84be9cdff67d2556cc6b410165fbb6882994d7d617dda95254ed020260","s":"0x30da6485220fb714888736a77bacb0ac87137748bfee53a901dd49f88e40dad"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x95608d38b8b00bc8b9426727f856eadcc7e705f784f4d3f48ae29dab2e888527","input":"0x","nonce":"0x9cbf","to":"0x39647d3170161f7d338914206f6d58d13798b505","transactionIndex":"0x8c","value":"0xbfd66e5a367400","v":"0x26","r":"0x1a46be403068d8143d02a852fabdfa3770f18c800a929fa4a01d565b34d28657","s":"0x1b136a2d0eea449ad8c746343c5d1d847d093665064f6613825df371dd4773f5"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x00e2d1d6e88dace86c8437fd412997d09a16ed8e782dd86e857c9fea42e22ab4","input":"0x","nonce":"0x9cc0","to":"0xac48389a295b51028822a6962ac5b426bc452a34","transactionIndex":"0x8d","value":"0xc3d6fc66994000","v":"0x25","r":"0x738ef925228e34f3c3d84f1bf731f406fdf88281f2ee392ab86373327ea3ed1b","s":"0x5600337bf5efaf09ff42730158087f1180c40c36fd2961ed2c94ff45e38725b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x2ffbee15977867989ab6023d600214a10810269c270c1a26c758aa3a152af8b4","input":"0x","nonce":"0x9cc1","to":"0x257a3600c0e58fc720cd34bdf13ea61ad38a743d","transactionIndex":"0x8e","value":"0x3b6432fb1c31800","v":"0x26","r":"0x5f57524a2a89ec1652fca3a9f72bce25904d2acaabf1e660c0da25592cdfe43a","s":"0x12707d6a77a4e179f99904fa282bd5e4a0d535bf98a097c4a2d72b455bbb5de0"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x970db809f14039e6b35adf1519596cfa20f60fcca7637b822b04c8c7d5f8ab94","input":"0x","nonce":"0x9cc2","to":"0xf3dcd496198ebab1411ca134792304f895a19eaa","transactionIndex":"0x8f","value":"0xb5d019cc00e800","v":"0x25","r":"0xb5d199c236bc60b3535928ff849cb95087b56e075e2ee663f4800f01235d542e","s":"0x4460e24e3729d10d797f4be88ebbef94890b386c550f96e043b28653d1d1ec6d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5e5903f0fc43e2be2bc343d9e89d8f0ba621922dc4b6a4ac82e0704ee11f4905","input":"0x","nonce":"0x9cc3","to":"0xa6572c15100a418abd29ea3217b051954e5b48ce","transactionIndex":"0x90","value":"0xfbd1cd91dc2800","v":"0x25","r":"0x288fbc105e6886f096d102d1dc96f90c32016109007fb7680bff77ffc0400019","s":"0x306024b2a455c39222088d37b3569044b39cc2d600c2738e57eda44851b2a00c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xfc0a931a740e71774ecc63e0b764e4c9d0b4f836d7f00ae4d0ae28017099a55b","input":"0x","nonce":"0x9cc4","to":"0xfbe3eaa8fbe8dbd66fcd449c99511c0a57c591fe","transactionIndex":"0x91","value":"0xb482a4c50b0800","v":"0x26","r":"0xef378b58c91bed44c7ced17f4c36ff225e78105169cf2a82bcfcd16a142a71df","s":"0x26881c33faec172f7fe83fd7cdd026fd12573f88c16a17aa5e78f5e3f6ac0506"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4231bb2bec57016216b4e6025dbf126565119c2d168ede494d90a7f1d382e873","input":"0x","nonce":"0x9cc5","to":"0x9b7990106b63911055a652a2026cce6d972db134","transactionIndex":"0x92","value":"0xc3d6fc66994000","v":"0x26","r":"0x1c5f1ac94cc55c5e563100eac213a43ea80c87d6184da054c3521f6fe923b14c","s":"0x54f018e4f182cacd9ecacbe446b23b928beb6171deb28e0b2c8b8ed1e1a710a3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x726daaafbf1ba791f50cce8752e9bf1a6929fe47e54ac1b93aa79222ac7b1680","input":"0x","nonce":"0x9cc6","to":"0x7bddda613c69dc409d67a5e7f922850b95e027ee","transactionIndex":"0x93","value":"0xfcc510c832b400","v":"0x25","r":"0x8ea0986a9fc06f2855011e7fdbd2d5aed22ed88ce7e7b77a034e7c877cb897f7","s":"0x624cd5c8a3140f96dc40a83f56dddbdf9fe84b9b1d557263ac5f6b251d30ed82"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd1cc0903afdd4a618d7df56324428a98a9b9582bfb254173ed064e6f649020c3","input":"0x","nonce":"0x9cc7","to":"0xa1fa1c9cc2681f806fe184e4f7283fb3080bee60","transactionIndex":"0x94","value":"0x2e6ad7727d09000","v":"0x25","r":"0x510a0c635065b30bbec14934a7ed8d799a6eff849d9fed17688be1bd78fd4e2c","s":"0x648bbea70e0e648a4179e0d3e889f7a26baae54e85403b60b07a2af6edfab618"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x323d707d53a05ec3db824e36ec43814b3bd35e49a084dbd6a3ce4f777f0a1a56","input":"0x","nonce":"0x9cc8","to":"0x39728384467996ec57dcdef0cf4982c82e751885","transactionIndex":"0x95","value":"0xb48cc1d8b16800","v":"0x26","r":"0x621c9fdfbd1496173119f38f3030d3b5eefda05c302a34ef76e08704ce3416c","s":"0x2c5f7eadf8a68d1d4be38db5ff9ef93cddb81722196a472f1348f28d852424d7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x77e76f2309ef91cd2366417b36a430b6a3c485ce48dc3814fc94bdcd34b029c8","input":"0x","nonce":"0x9cc9","to":"0xd4da32ade44649a93b7b08ef193d981dac0f5750","transactionIndex":"0x96","value":"0xeacc67a0b1d400","v":"0x26","r":"0xc536f136181fed929f24ace9936b185b08a412bfa944d7d86b0810f748baf04c","s":"0x6b06d0a6444b72eeaec10b551d126594ce20c0fd9e7bc9ee13c2385673d9bbaa"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc88bfdf60759e31503e65a53cd66efb3f0fc37720aded93ee2c5c1c45a1119d7","input":"0x","nonce":"0x9cca","to":"0x635611df213c557d53afa326effaa65d4ea0ef04","transactionIndex":"0x97","value":"0xbe0d6ff05a3800","v":"0x26","r":"0xcec675c38b42f9557c97581c8c4488619243e3f8a4f1f644b3dd08b900a9e440","s":"0x1821058a9a2ec71f10ff33189005f8545ed003a1d8b2e0f06cb74afba8ca3b61"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xec991c9d8160e0a0a8c8427559f6dbcf6714b472e7ce296be5d21f9c0f904336","input":"0x","nonce":"0x9ccb","to":"0x11e55784679b3c232c089277bcf10e80f1cbadf0","transactionIndex":"0x98","value":"0xbe0d6ff05a3800","v":"0x26","r":"0xbba064582085abd6bb0c35fd2e1c3160ac095d0ddaa0c44f415049d218f63d8","s":"0x5ede010809dd5e0efe1455814e1bf20b108a445afd3d8d7e8ccdb77af513e8e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x621a8230b5080228901854d87058a2d00475d0d4c40787249c51f325a099ca2a","input":"0x","nonce":"0x9ccc","to":"0x24ad36f1264980e4c0cf4f8f3cae33c22681f5fc","transactionIndex":"0x99","value":"0xe13ab79a2d8c00","v":"0x26","r":"0xa74a4e20cdaa096a62e344062d9178ff63ce2f7788eba0dad142905545f49209","s":"0xe42d9884686d6b933753ca48c3f41f0afd1fa0d46058bed61a80b89f0e38033"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd7bb8fcb3c156badf23c8cd7e51fc3eecad18e01c9116a3a908b4c3619c8de11","input":"0x","nonce":"0x9ccd","to":"0x3bd4147e2843e22a404b3a7ec4e623dcc1d03e2c","transactionIndex":"0x9a","value":"0xc3d6fc66994000","v":"0x26","r":"0xe39c6c476a58562df02ef7a65c0fe91fa1b160461d58fad3dc3e78773ecb209e","s":"0x3918944fcbb70032d45ce38ae1b8433f21acdfd2d8e8cf254860d82bda5cac6f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8cc27b7bab23324ddcc94badd25686dc5b4fc6a6d5dc5e8621a53928f7694154","input":"0x","nonce":"0x9cce","to":"0x539be33e02a71c2794b473ffd7d93457133bd53c","transactionIndex":"0x9b","value":"0x2b97e1c95848000","v":"0x26","r":"0x4e3192b3f87e0ca5c4a0e88d7586d1f82e14aea5afd34216edf5d237a5e1ddeb","s":"0x44be3d194141d488ac5be4e6ee28f8220b745828a8c295419592e7f4ac9108e3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8b8d69a1253996f85ffc76260e1e24440c0aeb426f8a84261c33a5bd325922df","input":"0x","nonce":"0x9ccf","to":"0x895aec706916932f6ff92f396b822a7b742f8894","transactionIndex":"0x9c","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x73220c3777d9143003dea0505379d52edd2b0def10229d662daed56b524187ae","s":"0x1b4962a36536e15098f5cf2a0de19b6235b0417306a0fb0eedbe449a5d35b776"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbfc34a1bbf3f08c83dbfe4f831ef8748d517a4bf8fd329e726cf42fa579cb4cb","input":"0x","nonce":"0x9cd0","to":"0x08fa1cf2fd7584a60e036d28aa1f15e428ead213","transactionIndex":"0x9d","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x8d382c30c2a6b5d163ccf772aa01c5783ab82f9136130446649eafb4e96ddfc0","s":"0x4ce80fdc02b364a26b565a8dfecb12a7245898a21d66591b77a52d4a688583f6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x0fbc84a0583a6bffd51e6eedf56ca022923632aef0e82b9dd542d7adc5d61791","input":"0x","nonce":"0x9cd1","to":"0x906df0ac2b8e313a423698601881cd7019daf577","transactionIndex":"0x9e","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x31f4781e47379574227e2a230ae927e83a55d773ab4cffcc91c0e5608b2c6bfe","s":"0x174d3e978fba0bd7bb4ebd1f7fad798c402b684995f8ec44b46af843ffa5b5b4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe3314f9fdda1e58fb733b7931387e789b56d36c11855a815286417dab541e1be","input":"0x","nonce":"0x9cd2","to":"0x8e88bb62681f28a830d237fd023f2f2d20e7c04d","transactionIndex":"0x9f","value":"0xd28720c9962000","v":"0x25","r":"0xafe02a7c2b2699acfd9c933be9253453c7abca1dfc380dae2969e7ae45c1f8df","s":"0xa0e16b9d8eb149deaa599d6eeb952fb2a7494ddb7047c93babd582eb23e14ea"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x755a581c3a6846514c20e1ae3edd96a0867a2861256197a37ee5bdeddb5d2e69","input":"0x","nonce":"0x9cd3","to":"0x33269065d17f426432be4bfbb773debb4c96f1c8","transactionIndex":"0xa0","value":"0xc3d6fc66994000","v":"0x26","r":"0x196ab468529ac624cdcdee3722add7003460ff30d8fe7acf46e33d20bceecc06","s":"0x71a4036e59d768bd4d343f9477d3190cbc83b402a5ecd8d416a3b616fedbbea"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9df7142ec0c5ee2ab46290936e726a940e445106d6f05be08a3765178cc93b86","input":"0x","nonce":"0x9cd4","to":"0xf957e85e2418b0cb016f61b94e77ce0acc269c50","transactionIndex":"0xa1","value":"0xbca080a4a2e400","v":"0x25","r":"0xfa8205bf60de23ff80b8633931644e68b824a0785f393d565a5da518a1c2630","s":"0x538220e9adeb78c3fe3c4c96936e660a9fd1d1452e87729809cf254a339526d1"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1730ed3d9f3a10b52de777e0e122a302399da8c1bd6d5da9d9bfa86ee2f89ca9","input":"0x","nonce":"0x9cd5","to":"0x3cd376f5b979e46f0cb5b68c3902860ae43ce85f","transactionIndex":"0xa2","value":"0xe4101efecde800","v":"0x25","r":"0xea5079eb2952368693662d052a2f8f0ef43a9febcd18a85b33491d3b82c93090","s":"0x7698917e0c0c3e64b15da00503d507a6f8b9dd86806e16e38bb16f6aed4f02ca"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x288d87d47b19ba2e65a60fe5e5c90da5d363209d7969057c198583a15bb305af","input":"0x","nonce":"0x9cd6","to":"0x1b2fd12e8b9abf91d99991dad4d9a306765c0367","transactionIndex":"0xa3","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x82bc034aa4e4ee35e3218f0dae7cc8373ffa45acc115d677ca788b716fe6426f","s":"0x3c8be8b725ea4f35d0afc236baa4b2aad175c41ce7b0093a15fdfd77d379d18c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc7dd7fcb098b82b800dc16b3532196c89c3b0faddabdfdae739da5c7c72f3409","input":"0x","nonce":"0x9cd7","to":"0x94e0323df94c4065979c1a421479d08d8df1fa1e","transactionIndex":"0xa4","value":"0xb5bd33937c3000","v":"0x26","r":"0xcf456d759f0cad5e1fc14c1ee8c3d01a6d406684d59752a549717eaa2e9207b1","s":"0x14efaef72e72e1c8198efd2bb11087af1d3b0f4100cea3fca248dad012a405c2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x2145c573f84f33f6c003ccf1eaf9f84c9c0601b5fd5a5e8382e8c1757828b14d","input":"0x","nonce":"0x9cd8","to":"0x7bf32bd578ba355bf700811e599247e810618ee6","transactionIndex":"0xa5","value":"0xc65e071b07fc00","v":"0x25","r":"0xe1b492da8fad24cfd7d349294477e0fad66921bc91623152fc06409de2fb3cca","s":"0x6b16cd9a28dac6af29e3c143ec361d576cddcc8c71eac0c4481618614c62ad57"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x58611b2610635a8b76df6f7e605ff0702df017348abd2238782a3359cb71dcfc","input":"0x","nonce":"0x9cd9","to":"0x2bd7aea169058a604d456297373eb84f5a34cf04","transactionIndex":"0xa6","value":"0x1db2197d8e18c00","v":"0x26","r":"0x891409b0f022fd802e451215ae2ee96c81dc06229cdbd05ddcb9939da5ecd579","s":"0x68f48717051b13bfb8578afe51c449f31a3bdde25ab3ad83b28d8cfdb4611f6d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x87c02b06f25d71f35374a271bb60fe2f99a79af6508d9e33aeec498ae434f73f","input":"0x","nonce":"0x9cda","to":"0x1655649294a57e5c11172c8ab523eda86e4fd1af","transactionIndex":"0xa7","value":"0xc3d6fc66994000","v":"0x25","r":"0xd80e0eaba17f95a944c5b658477975f19c28ac94fb8b9fa2566f3b38f603474c","s":"0x1580339fe9298d464f58e8c4fea9389f7006ca82b20c930b8909b75279c25e9d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x2b3c06aa279ab80455f276671a61a838fdf05b76a54a8818fa64cf95c4490b36","input":"0x","nonce":"0x9cdb","to":"0xcd3a553544775d8611e698467795f358bd7fe55f","transactionIndex":"0xa8","value":"0x11ba73f98f3ac00","v":"0x25","r":"0x1dbfd861141e35072522bda3c1219194eb01c0e330c89b6022ec730ac93dcc31","s":"0x5af83895a635f553e3b9c82a798a7d53a5a3a86488a26fb27737a33264ac9f24"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x56004a943b7e36b67e741870b1890849c34990d048a0a455e808cb132540d496","input":"0x","nonce":"0x9cdc","to":"0x56fc63ad1fdff5630f17543342af12d0aa15d247","transactionIndex":"0xa9","value":"0x154e819e545b400","v":"0x25","r":"0x377d06a741b2450b091d5128e18b1ae4c760ba5207e8d9efbfc6e774b1cf60b8","s":"0x4f0083b5968aefbb5acefd1b338180f8b1004ff1d6c913d029c66432ec007659"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa29176cca49a109d53efa403efc3c7b35ba2a1b195484f8c05e69b849b894a8a","input":"0x","nonce":"0x9cdd","to":"0x297c6ab093a5e9c17a19bd83005e309aa6bf90fd","transactionIndex":"0xaa","value":"0xbec3e418240c00","v":"0x25","r":"0x511df109f77d1b9d2c7011b0cc8a8bba940abbdc53890544d45beff6c3a61d06","s":"0x11d11e39276e64593c4275673271f41c650c423cdec21468f6309c7c82fdf886"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1cd5a9fd79b22626d1df97c78709f4db9abe62157196a34ac537c59280490fe9","input":"0x","nonce":"0x9cde","to":"0x287ff03eb0ab5dab611ee1e8e7808289cf122197","transactionIndex":"0xab","value":"0x17186bc5e484400","v":"0x26","r":"0xfb6464f449ed3133bfab98300f69a9af9c2fcfcf70348f4a3cb804e9ea668abe","s":"0x697002a0151af289d5bc4a5b42dd810d34e456ca4ced24b882ad7ab084785f6b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x00c308e3e62fc3b58f7d7702e3882d6d2aeb809859e4487e8fa997943e7a0bb2","input":"0x","nonce":"0x9cdf","to":"0xa85a7429620085477373ccc651ce6aa411c610e7","transactionIndex":"0xac","value":"0xc3d6fc66994000","v":"0x25","r":"0xbbf0e35e491aee18fbb86d3710083e914cc858268a6af2d16426bf7ccb2fd973","s":"0x38a6b8278e842d9223bb24cbe7d32ac4cef3b83cbe567c9c8ba257bf2a38ddd3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xaa0bc5f611f5828d51be4888401de4557a173262078eb6f11315a917fb6c2ebd","input":"0x","nonce":"0x9ce0","to":"0x3f18f9a66a30cbe9d6c9b02ec54254057eacc43b","transactionIndex":"0xad","value":"0x185af0237b74c00","v":"0x26","r":"0x22f3ba4c1b250346e58fce9f135541005c440aef5636ad99bb831fdc64c59be4","s":"0x60978c1e9808dad73a541cf557de560135634d090ba229bebf0524b28b3ffc7d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd2a4d7e6549ce0578679a0d11d2d2b5f2c4f64172951362739f9b16903e82621","input":"0x","nonce":"0x9ce1","to":"0xa0fd5398b2102ea03918a547cfc58a1fbf4c2403","transactionIndex":"0xae","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x6055a4f416f5e818f9918bea1eaef0a9fe14a3661149a12daae0f48ee88d0998","s":"0x4b03acb5deb9c71ca143971abd748e935db2b76ad8430c8a3cbb2c757ff8fadc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb42f3a400da89e618a4c4d35f0ed6153bfad69d3346909bd88ee8171d5be5d4a","input":"0x","nonce":"0x9ce2","to":"0xcefce92fb15f3164268589b706191c8362601e97","transactionIndex":"0xaf","value":"0x1db2197d8e18c00","v":"0x25","r":"0xd22d3e17926de80a691c83667373b97e88753d8507b3f61764b494b624ff0e92","s":"0x2c2406f7bcc907e877d2145b1b29ce4b818d14e97d37d2c6dcf0271b22d26af7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1e8bcea74e6ce6b5ed1c81c6fc9882e0488b4e82614cddb5fad905544d434fca","input":"0x","nonce":"0x9ce3","to":"0x724ac56002fa96bb4476838cee9c22621d392e11","transactionIndex":"0xb0","value":"0xe10d49b62be000","v":"0x26","r":"0x3dd9c54a927146032bb7d6104b7790467ce1c6441524020ed704acf458d58887","s":"0x4ddf7517d33b421d07605d2939c1e3a0a80a10b46ae21ea0e717f23700376112"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x217fe1e5d79996d4d3c2f384a516f58fbc4aa5618ed37be8d8176e1318e4bd2b","input":"0x","nonce":"0x9ce4","to":"0x211544a96613f246545b0b8308ad688697e02b4b","transactionIndex":"0xb1","value":"0xbe0d6ff05a3800","v":"0x26","r":"0xe7e43e38fd4c5ac224564611b60dc13ff3b6834ca9210954033a778a744e8a35","s":"0x2e29744b11609e3758cf7f0486448b82f89296114af13a39e14a573ea491f769"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1ee443f2fe6e52c762b7e3d305d77c427ebd30cee71c465da6f199f53e37b5a1","input":"0x","nonce":"0x9ce5","to":"0x3a10cba0ec57be6d905e3ae2a3d446b1e2b6f8c3","transactionIndex":"0xb2","value":"0xc2cdc6fc2ea000","v":"0x25","r":"0x17a76b0755c0b70ed372cf66f081d4ca093069d3f6b0b6b01d8b0e30a2b4e80e","s":"0x3f46b120b112c7a3688d51e4cb8712ed64776d7ffbc2d0ec63fc9d3cd07065e2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4dbf985934569c076b2f6190838817453a990ae27aba71e59a4cef7f7d8de7c9","input":"0x","nonce":"0x9ce6","to":"0x13eedb523e8e5c84afade1a43b8a4e447d417c06","transactionIndex":"0xb3","value":"0xbfd66e5a367400","v":"0x25","r":"0x213e26c9232cf2b74adeafb0e055aa261c66cc014d34d0fce46a581c60788eee","s":"0x21a635177917aeee4653bfb94d44db6b218b75009c62f1ea882fea1fc35af5a4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xec9bb7e6c141a37985ab43588ed88f7969395614615636d03c1b79ed7ebe5e59","input":"0x","nonce":"0x9ce7","to":"0xff509eaf1c3cf5ebfdd485fd46ef3122ab080768","transactionIndex":"0xb4","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xba1b67a6465f30389cfa278c492d906b1a122fc7ac4a861719402a6d32b21ed0","s":"0x3116dae25a6df9bb99297ecb492c10dcf5bc87ebf09cd43892f9974eb645fe59"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1408a4b31b8be29ad4955109cab7bb2caeed3d07abb7477cc5c2e102aa16dc00","input":"0x","nonce":"0x9ce8","to":"0x9ceb693dbc8d0e83b281dc9f2f0c9fbc80cd2179","transactionIndex":"0xb5","value":"0x10488f2b8489c00","v":"0x26","r":"0x94a445991efb25f3f0f172c75af1ab84cd698302b658c7ac1ad1d92e165072e5","s":"0x5a2ba979d90c2f4d78d39b903c88be1859fb22d2edb1275683dcbb500ff0b9d5"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x860d308e608ed19833ff274428e2a9718afcbb9e599bcf7d5b29846b77f938c3","input":"0x","nonce":"0x9ce9","to":"0x18cd86558de106863e994c35a5c63bad30e23838","transactionIndex":"0xb6","value":"0xb2664919715400","v":"0x25","r":"0x769a58a1d432a1caf7b847257659d5f9e90af72db57035a42c64d268ea98a3c9","s":"0xa88b914c5243ceecae1d96273f5c04b5add4e0688b1f7b355a28e270e0747ab"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe9d76862e1bc46fa061bb8d1598c659038ca0c1c621c17ccad338c989dab12d0","input":"0x","nonce":"0x9cea","to":"0xc13c2d8ba7889fab62d820722b2123a13b26e4c2","transactionIndex":"0xb7","value":"0x17c1adfe0b47000","v":"0x25","r":"0x1d31fcf986b4464ea69ebf1ef99c90aa34f8bdd254cfeb1b6e3f62a55a026ecb","s":"0x19461dc3be2733c3ea1319232d8d2247aafbe43dd8f7e898f235f1c065e6b56e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7f816b4793eccbba701e79f0e1aff842515eac816e9984609bb6beb37a42040c","input":"0x","nonce":"0x9ceb","to":"0x845878661700257c0b2b51028272edcbfdd4d0a2","transactionIndex":"0xb8","value":"0x1524b1cfcc2e400","v":"0x25","r":"0x2ed8c352f733813b45fec2a7f4454294cff0e937e0e79a3cc69c1381bcbda3cc","s":"0x44a26812b96e80f40823db93ab2e595f4e317c324b08c92e8b66f9a9cfccab4d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xaf7d3927d7434976786fcef6700fd0ffab006d66508f52d48e0d771453c6d662","input":"0x","nonce":"0x9cec","to":"0xbace08e3c0c1c2f232d83ac08eb506d4528d879d","transactionIndex":"0xb9","value":"0xc223c19fe34800","v":"0x25","r":"0xa9d9eb89ed7f59e74199d6d2520911a726222e6f9874d52be5bd189d9a199df6","s":"0x3b17a05d1304b7219c3a5c09de56979b03fab9f77e7bab3cfb6c9d6bd770abf1"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x06e15c18ef71316b4fcd19ae69a0bc4a78de770e27b18059901136122a9c4e03","input":"0x","nonce":"0x9ced","to":"0x33bfeb8ae567ce99992a353463819f7fc6735d8b","transactionIndex":"0xba","value":"0xbfd66e5a367400","v":"0x26","r":"0x641c4ce339ba76bf21a3d1a629de3a1162b9ca5ca8564eb1bc38608c2eadc0f8","s":"0x637b595c9180335cd72ceab2a6ee5fd489b6ef201f65906cbfcbd755fa3794d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x1ba46c696e1030964f9824bb8ee284d1ff6254ee5404170b9421195ab141c7b2","input":"0x","nonce":"0x9cee","to":"0x34debcfd3992a938f17b58585ad9f5d73a673fd9","transactionIndex":"0xbb","value":"0xc3d6fc66994000","v":"0x25","r":"0xeadc532404bd692779019e4e2cb6dca4c38ca2075661984595b91b18fdd196c4","s":"0x5689b7383296d9233b98af8f422a67c4ca1a7c2e6d286575e6b889f38829b9a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb9487d0a7f752637586666f40fa99896ccccc2803c47cf003333c09275046113","input":"0x","nonce":"0x9cef","to":"0x4e205689f178a5903422ab4fd6410b435a82b165","transactionIndex":"0xbc","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xcc38dca840bd2912df3667aeccfe3711a98420eecf41ca3c14e61f525f191ce3","s":"0x2d469cbb6a1fc81854cf1d976c1653fdbf3ca79bdcb28b8cdb84611f3874728a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xce0293178b71291de5d02b8124f1c252f4018c1d55768dcbbf193f7d361c53a2","input":"0x","nonce":"0x9cf0","to":"0x03f4c3ac41b38e8d9f349e675d0fb4c509b522db","transactionIndex":"0xbd","value":"0x2992f07c93bc400","v":"0x25","r":"0x9eeec756163c4b7c1e73fdb0b4edb4808d325045f47eec192d5097034ebef0d8","s":"0x73102b81a6f71f09fdb6c1931ff817f2ed984c3a9b1d22f84913606f80bc2ed5"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x2cd24cbb022a9bc0352e4c532d939c48ed3f71d644ee841f816fce064f5c2b70","input":"0x","nonce":"0x9cf1","to":"0x0a57963ddfa8cc90383cef7f06fc6e7ab0b35d22","transactionIndex":"0xbe","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x10f8c7721ca343a0cc32e711538ad4eb3d37ba56fab12be5c1f8894aef67a406","s":"0x28f65ad322f0d0a1d381da1053bac2032a392118ff7f5eb9608eb8c6abbfadda"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc3cc47c5c88b194f48aa0d8bec7d2c9ab31dc4e81e7380fc99942b9af503e6f2","input":"0x","nonce":"0x9cf2","to":"0xc6bd787851fc8eb27e9b0328b570549663877735","transactionIndex":"0xbf","value":"0xbfd66e5a367400","v":"0x26","r":"0x3e9d7f4fe67506178fff36ddc6423fb32c489b874210ec4e28882aabc3f3cc75","s":"0x7b0bb7cea70dcae0f136e052d6608062ba7bf41d83e245f2ef6e722e52b469bf"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7cea8c95bee7eb2189dbb6d4444bbab4784c1494336ded6c8d1e761f9b94d618","input":"0x","nonce":"0x9cf3","to":"0x13726a3c3fde08d00532e221957004ff6d1342d3","transactionIndex":"0xc0","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x8a9f17141816d27034ad606ef936ca7c566bba5301cef78511cee9ef5e428d1b","s":"0x21a40f36f9bdf2c4a57f0dfe12ac4d5fbbf114e0188067e84d905f313a847253"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x089ba7df9163e818675a85e53e4236e543c16994423ad1b64a81f43c37b9005b","input":"0x","nonce":"0x9cf4","to":"0x8773379bb3de3de7fe976122cdbdd801f55e4820","transactionIndex":"0xc1","value":"0x187adf8cd328000","v":"0x26","r":"0xa488b0f12d31783b85845bcfc5b1b4ba5ffcfe736acb1f9d35444d1b3905b1b6","s":"0x6a8086dd57efbdc84bf54322738de8faa9ba607f4042ff7dab2c0e267bfb08dc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb66b54b00b9fcf3c61267c7d0b1762e403bf6f409a8e7275d84a0994946752da","input":"0x","nonce":"0x9cf5","to":"0x78d53308e6ad14799789d7558ce78c73827fb780","transactionIndex":"0xc2","value":"0xbca080a4a2e400","v":"0x25","r":"0xd59825ea762c091be2f0717d1e049bf4a0b818c657d358ac04991c1680d720d2","s":"0x639e1beef12560bc313b2454615a38d84aca04671f5d41979b363cb18852f0f6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x71641cb80e0f1f39ad689acf6e56a429f1c82d7ce64694f30636cc61c98ec174","input":"0x","nonce":"0x9cf6","to":"0x4d73cb2b71fa1f7e5e63a7ab58967cb92bf4b921","transactionIndex":"0xc3","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xbb686da884114b60ecc2ebb307391098cbb273ee4b92d13c1ef7696a8bce3fea","s":"0x19d16886c84b1fcfc7b81ba07c05a57efb42438c410217268d3f4f12deb1a65e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x78d7967c296e433208d48b24a9f9332a38fd0b18781881b893c6eb2c5dd4a570","input":"0x","nonce":"0x9cf7","to":"0xfc9481332ace0c3a7ec57bf0cd4bd39fa115eceb","transactionIndex":"0xc4","value":"0xb731e73ede1c00","v":"0x25","r":"0xfcaad74772d1a076f8188b4a6157a898e0d85670c71cdd842d151aa281b0a3ee","s":"0x32ff5cd65b7379190f099ae6cf86ec0ee383d3ecef36f661d013928676a7d216"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x29f864a1bedabb6457faa0a7fbfb103dd6dc01b7a0ca0be7b1bbec5f93044f4e","input":"0x","nonce":"0x9cf8","to":"0x654240e37aa1beb5b40a18bb9cc69334b3a56175","transactionIndex":"0xc5","value":"0x15064943c09e800","v":"0x25","r":"0xe3ebe65dc975500e1f4743ceb3ae145b8326e72d5668ac8f0db9b65e0c8e9977","s":"0x6b7f1ecf321444dc3100aa1e3af67f4620853b6d3555cca6d44e5b51a9a3fd6d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb1293a984151655a0dcc5ab9059b8276e3f83eed10c0ccbfe4884c318935f106","input":"0x","nonce":"0x9cf9","to":"0xbe5cd7c23c060cd74f64b91424481bc40bb4db83","transactionIndex":"0xc6","value":"0xd76c7c0a756000","v":"0x26","r":"0x2a1c53b2a71916243828174412e55ba03951286cc82947d8490c6fb2e61babc0","s":"0x39a2e24783ae14e66facf41c5b8e44e529e352712bc9962d1ef71bfbe5475b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc793ec632f476aa4edaebee4f358485d245b0026804811b7f6528b49175691ae","input":"0x","nonce":"0x9cfa","to":"0xb82e0f3c72820861037bd7c3d911a96e6cb25497","transactionIndex":"0xc7","value":"0x17c1adfe0b47000","v":"0x26","r":"0x14aabd73b35d878b51c152c0ce5dd892cb5da4796b63f3ae1d3a9c467142d2b8","s":"0x320772c6ba1256843ede366dc1ce288d20af17f36ad9d908bf8b3ab35a6b1aee"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x12169fb22d1405f853c977bbd994f3baf65aeb9ea4482ac9060161c6a4f0cce7","input":"0x","nonce":"0x9cfb","to":"0x32e700e832d99ae47a00227cb068fb5cf3da5edc","transactionIndex":"0xc8","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xbc7b96700d6e7f4ba17e1528574d87ec8ecb2bde20bcf3714e36ba51fbc1351","s":"0x12ccc6c7288102727ff6a4a054afafc3e77237fc35f8b0598aa05588c9eafe6d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x89ba13a7b91f35ef7b98fa20a5f60fec657ded837b72cd7a69c5ee2cf5250edf","input":"0x","nonce":"0x9cfc","to":"0x776438b8e2e99ae520c68424362fec87cccf0eb4","transactionIndex":"0xc9","value":"0x3573c77b995fc00","v":"0x25","r":"0x1687de92e6a9e03f5a26d7e9adf01d703687ae98723f7616059a2eba1042bf4e","s":"0x4cc82767b8bb816344996892375244c67114845fae15c5a4d314f81278bca8c2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x6471d332d78de2d77d20c68621f01bfbfd402f1f5174d5d23f1f65fe6b8835e9","input":"0x","nonce":"0x9cfd","to":"0x9d11002318a9dc9d1933c86f01bc629d51e6a3ec","transactionIndex":"0xca","value":"0x1db2197d8e18c00","v":"0x26","r":"0xf3665db4603eeec0d6b9c126da18d1d0c4e723635416d496d122b51bea8e5c38","s":"0x665537b02e8c6b542695af06167d86232ac78f5f37e9f303aed334bb81715443"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x27aa3212eb0a239a711f186d8a63a42512a18bd9332d7838f523b95118f99749","input":"0x","nonce":"0x9cfe","to":"0x9246543d9461a606b2840433f7c392b5aef8b285","transactionIndex":"0xcb","value":"0xd6c261b9bf0400","v":"0x26","r":"0x4d1af5be4a0c757b54eb66058c3feed92c2a1a85b1baa62dd4e9ce9dfbaf04b0","s":"0x7e284bad216625aa8ce5ae05b475cfbd3c5863ea51885de4b5c90f290cbbde8a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x787da5b7891661565543d353b5dfa70e5873ff85c7e566192963aa3885084aa8","input":"0x","nonce":"0x9cff","to":"0x808c940bf3acbd75bb3499318b352db2432d614f","transactionIndex":"0xcc","value":"0xbfd66e5a367400","v":"0x25","r":"0x5a2bc1e4a21cd2ad8c7819b3bb1da0b14baf103a217a076d719ed41132f57adf","s":"0x19b6341660bc14bccc747f7737be6ab023bf8a9041402a5051013faf812947ec"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa887294974aa257f4f9a16b7c13d266d55ae0913c59b40da033c3d853b4ec752","input":"0x","nonce":"0x9d00","to":"0xaf758aaae27a66b03dc018e30b8effba820187f8","transactionIndex":"0xcd","value":"0xd10ec777941000","v":"0x26","r":"0x3efc22d04b40946916b5dc10ff039c45a26eecc4c024a11b2480777cae4af45b","s":"0x5364886cfddbbf40cf8fe12aa986ec4579478dc56f4fe0ca12892fe6f3efc591"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xaa9a352aff5cc1bed522ccdd197a644257d9ee7cdc6e8f61b68126e0819e8ab7","input":"0x","nonce":"0x9d01","to":"0xfbf330ad8f876cdd7b89232cfe4b593722882852","transactionIndex":"0xce","value":"0x2e86359cc169800","v":"0x25","r":"0x835f89cae0dded62ea8c6350d3d3bcf652047b57f13bac1ee26d112b7aa59214","s":"0xc6e496eeb284948bed201735ff3bf63c6499910f3d4ce5b7d6b172dde27af23"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x425b4f3ceab69dcc0d05ddd2604dfa40e78160d8cf839630c3e7919cf954ca1e","input":"0x","nonce":"0x9d02","to":"0xeb7d710b47c38c4992da2c3289ba57a85920ebe3","transactionIndex":"0xcf","value":"0xb35229ba10b000","v":"0x25","r":"0x35b710be13362ded9c96271d2d401cfa8ff606f3553827e8327477fd612e3c7c","s":"0x7b4290776818db42b4411a812ab0eb57aaa0884051369a30357e86112446f267"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa4e40b677c8f444c3a7b97d23533e43d4a3ae21dde8fad55771d4c7ef5937c5c","input":"0x","nonce":"0x9d03","to":"0x28e8318732b762515981ef37804cd4eb6a5758e0","transactionIndex":"0xd0","value":"0xc3d6fc66994000","v":"0x25","r":"0xc783a1e9e5c08743c5427c6847ed19864e9c5adaf95a3e46912380fc377a8f4b","s":"0x4b83d0068197957b2479c6778f88df8bc6728aaf8175bb5b7221de1d689a9360"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xfbacc6bfb7f44322b7709bf52429cd5dd9d9d69d5c247338b1bbe84f015494cc","input":"0x","nonce":"0x9d04","to":"0xf00d3f4ae5b4214a302e464b3d12f031b127d483","transactionIndex":"0xd1","value":"0xb3d90a82e2a800","v":"0x25","r":"0x3f511bbba6e703af96fdf15b9adec24067f8390faa99917226e705617b0093f7","s":"0x154bb661e8272e7134fbd138b127b1b84cb5db49f1ae2a3f778c307d72bed1e4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8b84a4966d38d776c6b8962a1917bbb4f059729e34b99610e2c7ddf79ca49228","input":"0x","nonce":"0x9d05","to":"0xff8d7b0bff0fb85b52d10e5d7945b73161cce477","transactionIndex":"0xd2","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x3f5a312c1d08ec8dec4f42a512d85edebf264f008965941bbc5353e597feb38e","s":"0x715c0b3fe338250faa707432a7cfbd4e52c9bb2308d8a02bcf74b3041e1b57e6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x3e0fc3160fa5c53b4c1e2e3e46b4290771144d0e990ae89804f60c99f32b3cfa","input":"0x","nonce":"0x9d06","to":"0x255157a27d51fabc579ece5361622eaf8c1813c1","transactionIndex":"0xd3","value":"0xc3d6fc66994000","v":"0x26","r":"0xc713976a750fe379a85211f4f02479a7dd0b225ef43576d566f7533acbdda3ba","s":"0x1cc622ba98693076e2d9a21e141f524eac7fb9a888c7bfa889f058c63fa67c88"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8d53b68772193d037d5975e35fab74223b481d40feeea6861fde738bf4ef2671","input":"0x","nonce":"0x9d07","to":"0xd3e8de3b5a63b284bbed2d5cfe9794e3d5aaf221","transactionIndex":"0xd4","value":"0x2bf31b6d7af7400","v":"0x25","r":"0x6c3f46638dce4a49f9d5c743960bc20d6c3db6209ab199eb63ffac809aa8d860","s":"0x777cb49838ed0c4d553aa1ab1614d56b863422ff77b580c8fdc42612fac7daa9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x92d4a4e90c1ce7e3862c41aa95aea5c3354c7bb10b6a4516ccec5504b05cd033","input":"0x","nonce":"0x9d08","to":"0x9f52e533d0d336b0205cd27513d0368ecd27723a","transactionIndex":"0xd5","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x3bd544c739b57fc40be9937ec9af4a6d89e6e48d357a8280b27bd39a320064d1","s":"0x5624ef908fd74fe087ff1e81ce64d11030dc92644f9cf3f51a791fb13482e5f6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xdac1ca5e90336bd34ac6395fdb8d2838abd22a6d22298adc66d402d54bd81587","input":"0x","nonce":"0x9d09","to":"0xb18ed27b948855cb6b70355d15022c5ae1bedf2a","transactionIndex":"0xd6","value":"0x10488f2b8489c00","v":"0x25","r":"0x1499d499a1d314ad6f96ce73f641db22d1bcc69b992a4fe2db823f58182ff833","s":"0x6eb9b31a603012a831b78f14d5b902d2b9d5bc78f365ad8274415eae0b33955a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xab0c181188235dd287a7039351e65ed31a1c3b6ca3e25265672d1ffce9e26d74","input":"0x","nonce":"0x9d0a","to":"0x6f607c25b954d8ecbcdbbd9963339670f266e394","transactionIndex":"0xd7","value":"0x2c8b2629b4c6000","v":"0x25","r":"0x525127a98bbd7ac6bd66e2ed099fcdbaa6bc31fc232916099823fcaa7867132d","s":"0x2477abe88708caf7091f55ede6b4bb822d77a1e025d051f602157b851d092daf"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x83b03938fa0948f26c1b00a62f399c46155988aee9d6d2f01c10b2c4fd185e5b","input":"0x","nonce":"0x9d0b","to":"0x5a5dcb51cd6ce7b05303ab28429edf8d9d3b062e","transactionIndex":"0xd8","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x60a9c574271e060b3fe30f2c91e16824c27ab6487103aa4844d4b21a9161a6ef","s":"0x2af5c7fada52e0fd32d0c79e0decdd6942deecda5433a12695c99a19957fcf5f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9fc136a17fffb9382333c373b4179a3eb7c331885b86a422c31f5257da22a55a","input":"0x","nonce":"0x9d0c","to":"0x6f153c34ccb387a3c65c456f2bc73d02dcd74aa5","transactionIndex":"0xd9","value":"0xbca080a4a2e400","v":"0x26","r":"0x3dd0047baec92ffff8217aead0db0dedb1eee7269bc576612c753832f9d9f226","s":"0x7face9f9fa7c5cafb479f8779f083a74376a15f23b1d45678c5f96fc242e1765"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbf29e878bf6ac8691125f38d804c2c7ff3f73627a554a83609e3c11423da6903","input":"0x","nonce":"0x9d0d","to":"0x3cc6361ffa45d348a6baf3bba05c4fe0eaf15b07","transactionIndex":"0xda","value":"0x1708302ebdfb000","v":"0x26","r":"0x5df3e470d3dc803d9c85224ce70047fc39a523a9d8e0aa269e9e9849696aa7e4","s":"0x50ac36fc9444ccd19262ada9e5c9f5d41eb67a1962dec1b4a76ecde83f7da264"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x351f9816a5f1a92031a5cdd7ced1a49b4626d215df0306ba9d49d99b9a8dcd9a","input":"0x","nonce":"0x9d0e","to":"0x9de1d52959d35e32a2698975a137f183f9511e3f","transactionIndex":"0xdb","value":"0x14c9782ba97f000","v":"0x26","r":"0x548a968998e3260944e30d7a1176218e72fe8add244019aba026ed26dcccfeb1","s":"0x49697d323bb12ebe772e5f62768b98ced32b127d1270ad5be5da2fb57041d8b6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5d2499b5492e6de32086142ddcb47f24d1e1e7e46c7088af168eb9f74a9332b4","input":"0x","nonce":"0x9d0f","to":"0xd695c7dbd84e5d58c7ba1f26d20b2593e15a1fec","transactionIndex":"0xdc","value":"0x2f55bf3ca595800","v":"0x26","r":"0x191363910d31ca0643f9d1aae7a3f8c8eb81158022f1e7c73dbb2115c8e00917","s":"0x5991eb14537e7801cfa75e0750fab12c6dacac01540783bd9873116ca9adf9f2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x45483edcdeda70664203bfb599bbd94e27673d6f4c43f4566ce9957c468768bf","input":"0x","nonce":"0x9d10","to":"0x745d85da1aa5d82f151fb90a76723e94e7c4cb48","transactionIndex":"0xdd","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xd66e41d88dec87395300a329068cfc53854af5f9c74295a79604b769f6bf9d00","s":"0x389c13b049434448195df4d4198dab5adce0eb7c54f89b234e21c4002277c05b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x180450baae621a037e6325cda67da0d5299f3bc53ab5fb53cd2063db30ad857d","input":"0x","nonce":"0x9d11","to":"0x1f078100f770dca9bc0de8a2e56281e68d10efc6","transactionIndex":"0xde","value":"0xbfd66e5a367400","v":"0x25","r":"0xc5282a113557bc82f1891870d82e0fdfa866631c59fe0ef8fcf492a81b240a84","s":"0x76499a4831ca6ffa0a522d16f08095a03475ec091361196a0cab29e9a64ddd08"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xddd7bb565dc8195e56ec8678041e817be52defefbf1c61ff8e50aa5d2f4995fb","input":"0x","nonce":"0x9d12","to":"0xb88585c62dea87d736c29f0fd4217f70c07c057f","transactionIndex":"0xdf","value":"0x10a4fa1c3e61800","v":"0x25","r":"0xbe3003f71dc134804a94488bab38476c3c783ef50dfa6825669757e3901656f2","s":"0x186151902e221bba4f3c0e6d83da1bff751dec4520bb4d4e411d3fa71429c984"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xf4a3fefea93abbb34748b07264fd97a86239666f0e42b83327e4bb154af88554","input":"0x","nonce":"0x9d13","to":"0xbe2c3874af4ab4ddb7bf24586fdb6cf13780e453","transactionIndex":"0xe0","value":"0xbe0d6ff05a3800","v":"0x26","r":"0xa6b34a07eb15597019cfd7af199a232a6103ff79ff851cd67ce8379817d56ee8","s":"0x578fe3780418a7c7b5c0abe6ba2916eee7654b11ed204d0df84d5893bd31e417"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd44ef22db1fb7e12e2da4978630583d0371faf280491ba4ea14aa67c05f2f2d3","input":"0x","nonce":"0x9d14","to":"0xa15c242c4311f878eca821af6ca6b2fe2392991a","transactionIndex":"0xe1","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x62ad380c829c8957a7d67a33afd0cbdcf52236b61e0b319f8c44ed8208901179","s":"0x7e7bcb8ce95f10253eabca57b68bfc94094c23da7a15c16a9c3142a8a571ccbc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa5fb48b4ecc1e86facb80d4846465650fe8c27953674f66efeb29edc343a2e96","input":"0x","nonce":"0x9d15","to":"0x1c94dd84c1d0ec757ed568c1676541f039c06a6f","transactionIndex":"0xe2","value":"0xc3d6fc66994000","v":"0x26","r":"0x5ce4bf66e7027de1c39cf920e19fee8f5da51ba6231fa06853a08d8826e2ebf0","s":"0x4d536ff7d2dc81be76ce0b9a2fcbe6e7f0e7e0e92517b94d8edff2c7103a934a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x6949634372f1fe260cbacd3db19bb9f5c61b44bd858a50c3af3dce9fe0ccad46","input":"0x","nonce":"0x9d16","to":"0xb3328cb02b0759d71b1837ede36e5674a77c6da2","transactionIndex":"0xe3","value":"0xd248715f3438c00","v":"0x26","r":"0x6961ab1637e1e2b367c49e9ab0e59f1bb4475acc61feab020b3cc65d470f2b01","s":"0x22531490dd06c32be73504df385bf0b39f17c6b710f04930ce2955b9053aff3e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x5d581a05dc437f6a0d451a6d4f068257abbb2ce0fe1bc98aceb6fcd8e03268ba","input":"0x","nonce":"0x9d17","to":"0x4ad9178b47868752beb5aac9685388cac1f1cb7a","transactionIndex":"0xe4","value":"0xb51ebb2a2df000","v":"0x26","r":"0x360b546750e04cacf502754024ac71be0377edc32c1349b7e7eed2937bb7caaa","s":"0x2fafc99957e967677cc43fc67f8a5fd304a7261a08e67e91f9cec5de4fe28500"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9714470a91c3dc2e7dae6ac9d4152d70272ea323ccde232ea35f9057790c21e4","input":"0x","nonce":"0x9d18","to":"0x8fae8ae3f4431c4d4faba4b4756b45de98759e48","transactionIndex":"0xe5","value":"0x41549e7a9f03400","v":"0x26","r":"0x27208885dbd18638b93026f4c30acf509dd027a5c52d8db1228ac2edd4ab87be","s":"0x4b49789d178fa09a9371e15c18d0aaf1dd172a4af9dcb3364613dd58a863a1cc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa2b3186efa03b1b54d00998aa0d8897cf278678f404a8060816b6cd806629e04","input":"0x","nonce":"0x9d19","to":"0xfc6418f560acae4419be48f7f299f0aa2185f525","transactionIndex":"0xe6","value":"0xdc51de47784c00","v":"0x26","r":"0x12cb0e577acb62d2dc1ec52f0ddf0e113a4b6ac6f9fb5f9b410dd6852ff137e4","s":"0x10787f0526a00e60d31de51b6066e5bfdf9aadf8b0575c7f9485c49477fca7f9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x73200aa7eb57161747d1bd9a2d11917b45bd6d79caa5d26b7344f7a7502952ef","input":"0x","nonce":"0x9d1a","to":"0xc4a11e92427a5554364ac7e314670adce6c9422c","transactionIndex":"0xe7","value":"0x17c1adfe0b47000","v":"0x26","r":"0xe1faccd7a599b682df63b68836e6a4f4d45223b8ebf2446e7deeed2d01a6201","s":"0x487e703d6e11239513aba25bbfd20b31cf76871b9437a7c16e2faf35f32f939"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x954b6bfcc16c66d3d3cc657348b4bbcc6d4a06f6f9ba779ce4eb96e483634352","input":"0x","nonce":"0x9d1b","to":"0xc11990d182af08898b244393d729d082c04d1e16","transactionIndex":"0xe8","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x701fc2458fc289813b711df4cf032cc35b121fa830dd09e0d6475ee6ba8123f8","s":"0x1abc1a548024efc3d7827607408b1a001856f5490e7a22039e6903341aec37cc"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x91df18865f6a5df609741a128228d2aebdf09aa37b15f97f641e8d9dd88ba034","input":"0x","nonce":"0x9d1c","to":"0xcc5ffda4eb02a170d7182d0dd4f75f25c564ba11","transactionIndex":"0xe9","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x34c6e45d650823ff297591136710152176d81c093e1990da19a1bc4725b18cb1","s":"0x206c8b68f07b35099132551e9b10509585ff4f702f4d05951e71f709ed2b761"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x163d3003030a7a4d384acfc07e3d32ce388749efb2f6ecae44af7de5e3730894","input":"0x","nonce":"0x9d1d","to":"0xf2355719899495d08429900681a14bca060d9879","transactionIndex":"0xea","value":"0xb78eb0a0ba4400","v":"0x26","r":"0xfae4248749423ab2587efc0bb3091a8507e6910ea118f35a0ff44967f2a4d732","s":"0x4f15fc50959fa68880e1c38bc0d75ac501a1cfab2f8dd3b8856ba71f50efbc3c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8c59316f9c6cdc642313e218d1966894991c706395d7b70e4c8c6f73eb3c06eb","input":"0x","nonce":"0x9d1e","to":"0x0ae5b31bb58974b41961d06a865e8ffc1751a3bf","transactionIndex":"0xeb","value":"0x17c1adfe0b47000","v":"0x25","r":"0xcff9d3c7dbfe980e210d13ce817a6852e844b1a281b1df27a89608e655272724","s":"0x4af41ea19ac9119abf9befae40e384be08144a5dea0bab0a6d7ef94371790bf4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x34d76ba55e293dc71439735338540abc154a6b934fdbc1d9a887aeb8e6b00055","input":"0x","nonce":"0x9d1f","to":"0x9be6e5003ebd8c12fc8453adc0bea7c040907145","transactionIndex":"0xec","value":"0xc3d6fc66994000","v":"0x26","r":"0xc9362d7253138a9f4851835862970bc14af545d5414033b0be3d8df042b2263e","s":"0x3f8a0678a5a528458c63e08a0a9412d656bdb972bba090416fa895aefdde73a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x754bbd76215fcf913198131686c42b14790cf6d231b3299dd7d173bcc2989d49","input":"0x","nonce":"0x9d20","to":"0x493ed6708e1709d51aae0f4635dccfe695e17a42","transactionIndex":"0xed","value":"0x1ee22ef601b6400","v":"0x25","r":"0xec291fdd9183fb067ba1297fab3ee2f44eefddab9a84be982145e01c3b1ac225","s":"0x7dbd0d4dbc7a551ab7daaef7b3dff1b4af48d0f666741740222c2af2d7bd233a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xeabc4b6188418f2dad5b245a39c6ffd8771ed87f9d453d254dff1af9371b4a0b","input":"0x","nonce":"0x9d21","to":"0xe0dd007e4c1858198d5333188d1e51a50fe7fa24","transactionIndex":"0xee","value":"0xbf373008f58000","v":"0x26","r":"0x53d0edbefcbf73c8e024d30293fd1ebbbde41f2e0559fb6505256c89b2d404a0","s":"0x4b70ff90da557741e490c44b5d6187541378d038383b0cadf07ae7b122d538c9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8f07bc246af8f3ddaed18a610cf6c270ef1c2dfe109e822e544946c9135b4b67","input":"0x","nonce":"0x9d22","to":"0x840a86928ecc07417570a52a2fadfa07b92fa249","transactionIndex":"0xef","value":"0xc3d6fc66994000","v":"0x25","r":"0x4898903d6c230f74ba3e9ef279ac0ebf89ec7fee7cee57f484449d0c00934f43","s":"0x5bb1e090a72b44aca5108e59616396d53fafa5a099276340c8714ad151f05095"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x69c58c9688d17f6e3a41bb507d45a8a3e466f8288e3b946ad4efca1d45ebb973","input":"0x","nonce":"0x9d23","to":"0xbdaaec2bd3aaa7d7dc7bbb1632ea8407a0400ac8","transactionIndex":"0xf0","value":"0x2f91cda05a5a800","v":"0x25","r":"0x13b6bc5a8cc3e3f573082bf9c5a116676005af6cfa83b09637fa6d5d49ff69eb","s":"0x30d62a01c5facfafe6aa9ec72420df4ab58960c035efc82cb0d74b4dbe47ffe3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x55c64f655d8dea53f2ce64166fd895407dd9137d1c6ab71b5557521b013762cb","input":"0x","nonce":"0x9d24","to":"0xab7cd1de895d8f6acf3a33dc0cff1dbc5d3cf8f8","transactionIndex":"0xf1","value":"0x243a8fb94ab9400","v":"0x26","r":"0xe571d5ec1a3ad2f7ee2e4921ec990fee738f790b8b9cbaa41ce6199dc271557e","s":"0x4281a9021c3baee73922944a32640e83b563d12f1ad7d7080d0f56226957d613"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7e9e4fc7893cf0a9622eb220c1fda03f6de22989ed09c07b5d4e962280a26fa5","input":"0x","nonce":"0x9d25","to":"0x0ed7fd37ac6d0cd11556a390ef5755cfe7e11ee4","transactionIndex":"0xf2","value":"0xb5ab95a5840c00","v":"0x25","r":"0x8c5ecc5b3eee2219e9abace46b7512f1cfd545342db9bb86055a00ad4d01a513","s":"0x29a5fbe512591d06682e59ef9c6189d3bf8452363d2e4b9bf306dc0d0ef8532e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x01da483cc7dd23a9eb7789d099a98dd7defc20ce93445cc3f9b27a3c45b88567","input":"0x","nonce":"0x9d26","to":"0xf90454bbf19f7a77f6b0af28be2c5f488f494246","transactionIndex":"0xf3","value":"0xb236dafb37b800","v":"0x25","r":"0x296b8e9e002db193de14cbac2dba792ac3e10aac099e516efcb426ce0fffa1a8","s":"0x5cb237747f3d97eb69fd34c77464b048ab8a130d35eff139a69f99ebb3a67bfb"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x11415c478db04180237ee25f0d9f25051d28b253fb036a67191d14c794f0aa7c","input":"0x","nonce":"0x9d27","to":"0x13e36fd42db0af1af5daf99cccdbd5d3abd84c75","transactionIndex":"0xf4","value":"0x3c4843281346c00","v":"0x26","r":"0x4a9805021177372d9e45eb50f1c7215124f767adbe27f6d50239745afbaaf2e0","s":"0xc32497ec2419af80fd422f8b513bcf2aaf694b19e82f5be710015ed47be6cc2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9d0098cc74b6c0fd63e186cd7082f1230532cd8c7139c059b3be9418744e7e24","input":"0x","nonce":"0x9d28","to":"0x1aa676e5951dc81d5d423448eab4be659bff8af9","transactionIndex":"0xf5","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xb66285b17cbf0145ec370a5e9f38c931b77d0c2b9dbc1cb105eae92df68cb3d1","s":"0x516b4cf19aa021d5d4547d8b107eab6a71be2141d0e09735835537e32179fb64"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x38022390b5784a49bb4f7b77abeae78d2a4929be84390600a273b3c60e71427f","input":"0x","nonce":"0x9d29","to":"0x137ae004483aa3930b86d70c61e2704a8ed15f92","transactionIndex":"0xf6","value":"0xc78e1bb3f72400","v":"0x26","r":"0x619b7886c3459782bb7a12d9819792a9830ef4006aff306494f513d25adb63ca","s":"0x20e165e8f873c59618ec2f45177391a3d329987f2f269ae849f6449affd432f2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbdd61eb9abee735e5f27d7183ce30a25996e14eaed40604b316b0612795a6c64","input":"0x","nonce":"0x9d2a","to":"0x91a5d62b126dfaad6c9f84208fa7265e35f654e5","transactionIndex":"0xf7","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x1659de2ebd90e88a745c6b6fed1781f709d14740b44cd08cf2a4b89b38120842","s":"0x4ba65b21017b960635bb239784b26ca9cf9cf619b3ebaea46f549a39f813073e"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x4054d4a66cc62b44cbd482e7a7c9a3d47961ee4c92f01383dd4bd217af49f029","input":"0x","nonce":"0x9d2b","to":"0x653df565ec7fd75e6d11c93d2e418df3059c42a2","transactionIndex":"0xf8","value":"0xbfd66e5a367400","v":"0x25","r":"0xb996499cc7de072f5aa5e00195b371b10600226e422fbcce26a66b19e895b460","s":"0x6774c8b83b1c4e02bdc628ac26536e44551b4c0d16f2c69adcba53094af21361"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x14feb3ec93a5784e8e8ea8086c8b0b14bc8a1ba18c2c020b0faca2a3282f233f","input":"0x","nonce":"0x9d2c","to":"0x48ce0a4b875f12a67491cfff924d6ffa26a15095","transactionIndex":"0xf9","value":"0x10488f2b8489c00","v":"0x26","r":"0x5f1487c5db3f0f6810fd94a2358417e199f37fd8b83b12dc730ec254ad66ecc1","s":"0x6d0b167e2cdc8a783b0c4d344ae2f53c8506918c7507d4b786a1829e1b930b1d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbe8033c700e32c4cc9357f236c22e38b46248bc969c1877b1edd5667caa0275e","input":"0x","nonce":"0x9d2d","to":"0xd4f2d58076871ab57b6bfacefc77d89e25520c7b","transactionIndex":"0xfa","value":"0xbfd66e5a367400","v":"0x25","r":"0xaae12497417754109c27af289a5c076bb921bc128502b05afd3707bcde72315c","s":"0x1bf7d8b4fd7ac51a136b09bfaa77baf90adf1a54c06e74e5958c4afe12f7583a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xa1f25d0f7e6bf6871bd49184e89c6281f69df9533d22f1fd85aa6a91aa86bcc6","input":"0x","nonce":"0x9d2e","to":"0xc09c32d40513584b21c1cf9c281ef0606512c2cc","transactionIndex":"0xfb","value":"0xd10ec777941000","v":"0x25","r":"0x8d560c372f294da15f779d0dac2e381cd73571c65f311d7fb681fd73a1424981","s":"0xcfefef34555e00a3be0a99ae73b599ea5af3af892b68305e3eabb1a5c4cd8cb"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9b8740e51e99cfd9aeac76684a2aadaf8a4becb51680e8acf6e67d7885f6ced2","input":"0x","nonce":"0x9d2f","to":"0x466521aebc4b3d385fe15ad735aaea12112b127a","transactionIndex":"0xfc","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xe773f734e166160eab39e86abe317b09fa87dda79dfbf5d6b1549c50e2efbd80","s":"0x20fa53a197715b410e631c5ea0ce32734e4611733104d5d44bfa42eeb50ad84"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x61aa8b97e2c95243396ad3d8987a9514f3cc34cd35a7f2e5ec60625c446c713c","input":"0x","nonce":"0x9d30","to":"0xdb909d1093c83d34ada5d9627560f467344872d2","transactionIndex":"0xfd","value":"0xb63eec35f82c00","v":"0x26","r":"0x2675c0ab6ab44114434e174fd737ad8ebdca6a6a75bd1e6042af22abc7b77095","s":"0x562f6f3642db195e37855c3d8451c82d2e64b1df0de6bb041faa4563ab3d1711"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xaaf865c9b11a37d154dc3a0d82a8b5751ae480cea7dc78144815014a1d47a131","input":"0x","nonce":"0x9d31","to":"0x890b451b2ff30f1da26e5ff815b8e2903609e78f","transactionIndex":"0xfe","value":"0xbca080a4a2e400","v":"0x26","r":"0x927281130e5da54aeafbaefdefba33888fa696a6ae4011397db46e32556bcffe","s":"0x63537c39427a59de124acce253ec54eb36f7f1350e6a31f4da019391d11b52f4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x04b414f36ce448d4382e29c61acc81cd1ff5397fab63fa3e520d367d0b12c907","input":"0x","nonce":"0x9d32","to":"0xf060b2a6f01a05eee307ae90201afa5b13f6670e","transactionIndex":"0xff","value":"0x1c8203dfd9bd000","v":"0x26","r":"0x4c1b44608814b2c80472721e83e9ca5471b48226e8a697ac530c91f90a64a0c7","s":"0x2efd8eb43d46ad4a08c341d855b2feac58d7571ad609155d79ff8f81f1c5b46d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x84835297b78c0fd5b83761e87046aa80c5c4b25028172a6af2e3c3845fe3a973","input":"0x","nonce":"0x9d33","to":"0xe0e6c781b8cba08bc8407eac0101b668d1fa6f49","transactionIndex":"0x100","value":"0xc495a958603400","v":"0x26","r":"0x981b6223c9d3c319716da3cf057da84acf0fef897f4003d8a362d7bda42247db","s":"0x66be134c4bc432125209b5056ef274b7423bcac7cc398cf60b83aaff7b95469f"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xdac06da3dfb3f3f6a0f9c79038e3d08ee33f525ae9868ca0af5d5a9dbbf39a08","input":"0x","nonce":"0x9d34","to":"0xc70f9ad86ccf27090c331a20c11e09e161badb35","transactionIndex":"0x101","value":"0xb555380c72ac00","v":"0x26","r":"0xdfac45d18340cdbe65b97e769ae1845841e580698feaa730b7357211d222a305","s":"0x2a60cb17e470d16b323026e3f048f0a6de30b2629bbfcbdbae5d264f8e51e019"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x27356a5d6167fbf721b223e0be046c9214449802e55498426acdcd2dc96b69bb","input":"0x","nonce":"0x9d35","to":"0x58d0bf6c45fd77edba9e0ad3e46e69dbe1ab2d15","transactionIndex":"0x102","value":"0xbff52062f95000","v":"0x26","r":"0xed00d8e5d37a76921bc78481e6b0f4a137b4a03b151b3a6bac8962484f077778","s":"0x5c9229481247f3af1cf80f7cf0a8292594e35093e038b5c3afeca3a167d2ec77"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xba4819c207044620e3989e499e61e7c03197864bb8b6e815e3691079763695ac","input":"0x","nonce":"0x9d36","to":"0xeb53460104b5b5ce5add099abb75932da9904af5","transactionIndex":"0x103","value":"0xe07fdf4fb6c6c00","v":"0x26","r":"0x86f96350bea35565fb74884e356f9810c9ce1b75502292ecd311a286a4b7fe2d","s":"0x5d234756ad837a45d9c67b9d85f25eadb0fa0e839746a3abf12660b923e07fc2"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xf366be96a8c24c2c5939135c036e1dabc81b8b22118b68ce18600795069685df","input":"0x","nonce":"0x9d37","to":"0x6a16c0c1fef68d68711cc9b35fd5491e89bb2506","transactionIndex":"0x104","value":"0xc3d6fc66994000","v":"0x26","r":"0xeb1e4254f3d1f1c8acaa79c750c3928f2327fd88cf2c02eeae75b6ca74986cea","s":"0x2e700cf3266f445e9d68b9bae03798d5e052c514c1d4bd08703fabae97ca69d9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x77f3beb8f13797edf0979091a9894abc3a2d37ad00ea6c2283d364b2bbc53749","input":"0x","nonce":"0x9d38","to":"0x3b88c148c85f265d0cc2e1bbd22706440266fcc0","transactionIndex":"0x105","value":"0xc3d6fc66994000","v":"0x26","r":"0xa850344302e0bf95410b8307c6bf967b0abdff41f46d03d78332f56c98e4b61c","s":"0x975632db6f8f95168bfdf0f14b46b02d9841235cbb0bc8c2be6833b6e48700b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe8e77fc19f52a337633d20318dac2583b10094a5d886aa12ba86b40d8c445b99","input":"0x","nonce":"0x9d39","to":"0xb89ed0d7c1bab4562d6c9f62ae46e1ca978ac3d7","transactionIndex":"0x106","value":"0xc160e100a6dc00","v":"0x26","r":"0xe857a3fa7b82349a1e49abb8cbca936d234737a4fd9db5fe43af59054e8cf806","s":"0x4530fc86a8dfefe73a8edd8ade26867b0cf704c56a63902bbdd87f8cf2f633c5"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x39b8dcaf327d4494a5d7e334924f063f5115b8f88d5d3e0fb11120857154cf7b","input":"0x","nonce":"0x9d3a","to":"0xbd630d86d647dd1cf11693c8cf1712431596e757","transactionIndex":"0x107","value":"0x3b6432fb1c31800","v":"0x26","r":"0xc962522d9db8c32ec37d6e1d2542f92999d7c92748a1f79d5d535b1f0ab64e7","s":"0x2d9784082a45fa85b38dbb5bc86b1e695bf3461c3319526f7b00524e77b47180"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8687c226e36b53a0e243b950f842f8063252a8131b8dcf5bced13a3d374460b7","input":"0x","nonce":"0x9d3b","to":"0xe8beb6602e9fa7261fb7217772e74a0e0eff5b32","transactionIndex":"0x108","value":"0x274d60dc4dc9000","v":"0x26","r":"0xcc877996f15ea692f268ec668049d9f1e9e5d4e06d294bdedc0e5dd849c044f6","s":"0x7894390aad202383f3513e0280e368b8806b3c84457fcda33865124905fcd2ce"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xd3c6851ad1b73607f350e94caac79d6bf8164db95e2550a176c17c82d686a436","input":"0x","nonce":"0x9d3c","to":"0x7259671a99d6727afc719b6be335b3d12f23315a","transactionIndex":"0x109","value":"0xb3d90a82e2a800","v":"0x26","r":"0xe80d30a2e0221d11e8c8aeeed9415b61a46b8f75717f520757f0a04a30dcb2a7","s":"0x6e8e19c90a794ddcadfe87c155fa907dd120e78230442a8fdd84a3eaad6b8fb9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe3c463f1d97b6783e4d3bab38371467db884276dc506b28d3f499b7dc8633d0f","input":"0x","nonce":"0x9d3d","to":"0x3fa58fe438957db67fec7d98830733cc20ef78e1","transactionIndex":"0x10a","value":"0xbef19c7da23800","v":"0x25","r":"0x12b6c4b531ea1ed93893813ca4ba83711ef77f0aeb5d50496338d61ed4a8073f","s":"0x615ef3572bccaa7a2b67e2016fe27cd5e92476be30dbdd896421a0e885462987"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xdf8d0be70c7f0c1b363ce33d040f854db2dd283018bca934592bf5a0bcd7d9c7","input":"0x","nonce":"0x9d3e","to":"0xc6484480165ad0be7837d9699879f471598f47fb","transactionIndex":"0x10b","value":"0xb213bd63e20400","v":"0x25","r":"0x88d47a6ff2e2adff1b749dc2d98ecfcccc34a431a12f6e6c8f609afaca81e292","s":"0x2b7b96247e151a7d80e1cb2007328c35640b5e88d248861d0c04aa6d5a77dffa"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x31b9b1f7476fec5dfd5fd18d4215a91c59e8f6347890945f4c8cd0efb7bd68b5","input":"0x","nonce":"0x9d3f","to":"0xdf918af8a6fcea8aca4e41033a83f376822c5af3","transactionIndex":"0x10c","value":"0xc2fe6d18a19400","v":"0x25","r":"0xd4604addbb94448503460ff0817f0f282ca9d6593502a55f4a9b614cb0da1862","s":"0x72822737b98c32e340abc5e1d6ee981b5744bfc10a561b9042c9cd4256ff9923"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xeb29d192acdb57fc681038476f689fab44f12b7c75016085f6c3841bdd5081c8","input":"0x","nonce":"0x9d40","to":"0xb8f4c6ebc5adee28bfddfcfb4b99969a3d4d3f00","transactionIndex":"0x10d","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x6a7142f6a976e021731d4565247432a41c9480eea32b2a92b8379242a5582d47","s":"0x1c919c1ed41b784fd02b03f5c34db4e11d073c741683b25e80271cdd277612e7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x68418311e4bd7dd19b15b5c38344aace68b5eedec39aa24835e76e17ab44e3f9","input":"0x","nonce":"0x9d41","to":"0x171125195a8be9c1bfa055ea4cfd111e5ddcbf24","transactionIndex":"0x10e","value":"0x20278dafea97800","v":"0x25","r":"0xdd1b1aef77828c1775ea8fe40e284d38e61215af17a7f71f275853b212091fa5","s":"0x16cf60f614ffa64806b57a6395392fdcc682ee642b5628fbc5efdf09b9a63af1"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xff5a78bf3cdf7ceefb03d933c01ef9d7422bc3560ac872bc2f7e31ae06d610ef","input":"0x","nonce":"0x9d42","to":"0x778ad400d43bd2f7f41e3ff77093bad2cd91be12","transactionIndex":"0x10f","value":"0xc78e1bb3f72400","v":"0x25","r":"0x6928d8a9aa1c15cc31debd4c39279dbdddc877124acf5e9002e75ea90c581a74","s":"0x6cf6d08af094cae7180a0cde1a328c4b224eb6a8d794380ae01e52823cc548ca"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc5caf93d7b27eb0a5d0a0d48df676b8c8788867566e4e23924746ecfefe05e31","input":"0x","nonce":"0x9d43","to":"0x986c672311415938d7586e79a5f638f2b29a3927","transactionIndex":"0x110","value":"0xba0c3c94ab3000","v":"0x26","r":"0x5732311fa0e31c3b8d3d2247ec44072c3ac4b3058b8f8393d3b397c0a8945742","s":"0x492aba48035675bb962b3b9af6d9e7f41251e68982e7f109a065452d3df106b9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x88bdc57f8dd898de0d50a5c5e15648570f2059b4154123923bf0d1676f4ac029","input":"0x","nonce":"0x9d44","to":"0x4c647225087bfff6da1536b4d3542ebf13cc46ba","transactionIndex":"0x111","value":"0x7cb8d1507a76800","v":"0x26","r":"0x746f8df66a4584f2defc5b791ef251bc4a67472c01d173aed64fe7b4a92517af","s":"0x4a20a791bbd9eaa7ce682068fc770ef5139feaafa37d8a00c4a8a694a87c0953"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe79ceed82b17fc949cfbc6136dc826ce72d5b67e9ce4a922a586697ae4e6873e","input":"0x","nonce":"0x9d45","to":"0x4798994ff85419670aa86bcf026e7c5976833249","transactionIndex":"0x112","value":"0x4fe1a5db4928400","v":"0x26","r":"0x2fdf8b414249d409056a19be0b0b55df2d00a18ce9cfe9a63841bceb9ae0eda2","s":"0x7437f0548a236bb86ee90e3789c7167bd60197065a93da26a006efad3600a0f3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc2af406032f7e8684cf7de96048c604d2fff9e3e326c66be0a8ba7b901510b87","input":"0x","nonce":"0x9d46","to":"0x4bddbd1cbe7aaa14d1461178e2cf4943c12fa20b","transactionIndex":"0x113","value":"0x1db2197d8e18c00","v":"0x25","r":"0x88e7c916c1699248231e7b0b01d6045d64efdf5c3e910337a3f1a395b87d1dc6","s":"0x755e8ca9e5bc9abf2a5b086fbdb37c05e505a118c28e58efdbfd4d1da854da2a"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x19ed7609e5fc47fa26b198bd9b58365c4b6067ad02fcc6547b768fa5080be8ba","input":"0x","nonce":"0x9d47","to":"0x60c977bcf64316c88fdba52391d0dda45b129352","transactionIndex":"0x114","value":"0x22726f849d4d000","v":"0x26","r":"0x81b8c25c00abc5654b307058428192835818c810de464c3bb0ad6db58756951","s":"0x4deb54a63c82641983d8497dd69544755933718cc892165a5bddfd5cfc069dfe"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9c432dca59d47809df0c50b93fafe25c5edac9497617b55629d714dca7373596","input":"0x","nonce":"0x9d48","to":"0x374547eed2c3738f09f591fff7bfa417b9a75901","transactionIndex":"0x115","value":"0xc0aa6cd8dd0800","v":"0x26","r":"0xa987421bfb2d3b853b84891b6f85216d66c22c2b2fca15f39150f912ccecf727","s":"0x7f10ab7897ab16da3797ea41272558d65d7def38be91e4c1003348051f412185"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9c5cd08d5ceb6f4d4c3863046b643f3fafa9bbb351533abc71debf1687c18c0e","input":"0x","nonce":"0x9d49","to":"0x3c068db8f6ef4182e75565f5d37eaa8543177c25","transactionIndex":"0x116","value":"0x11de480c08dc400","v":"0x26","r":"0x3525843199367aaa9044153ae0d85e54e3707cb7698cca38097876b02dcd068a","s":"0x77ddfed3ea1e5f7943b2d89610d2371e2c83dc62c00267f2f94b6c0cbb21d962"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x3d22ae6592e88c4ed761f2b9ff688f1aa41982a1bb370ecedf49019843c94630","input":"0x","nonce":"0x9d4a","to":"0xfb3de54d4a6130598e8ff6a039ef30f0b59082aa","transactionIndex":"0x117","value":"0xf711768607c000","v":"0x26","r":"0x2acfc1043321833c91b0b59efc785cd3f6cbdf19dd3419bc2789cec5212e5ebe","s":"0x62460ef4770c05061fa67960019f181056798b8db278626e22851a7856dc0132"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x689d96eb460e8e30e8ea87d0b98a647c0edfcacd594cc5e6eaa1e062cc77b313","input":"0x","nonce":"0x9d4b","to":"0x5d795994944b3aee38fe866c8fe77b68d4b55f22","transactionIndex":"0x118","value":"0xbfd66e5a367400","v":"0x26","r":"0xb1742bec9a7df83d804cec1d6655ff3a3e921806e0b6e9a97b84138ef0b1d075","s":"0x59c1eda35bccbceb17161743d4f44788f7654f1f92afe15cf03ac4cd66d57ba6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x0ecdf674c318bd00f62fbe5413466ec13175c523cb0cb16c4122df6d4d2c24f7","input":"0x","nonce":"0x9d4c","to":"0xe417e7027b38ba90f4250deb71ee602aea6de5c8","transactionIndex":"0x119","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xaca1fc6427a7e3c699b3669cc6ff3ba6c8f2cfa573f97091424997be5d752cc","s":"0x475a62702cd690b0ef846b65868bbb2d726938ff7c2e6b1aa394c49298535c15"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x3dff56ba42771c98a208c77ddf52d77ca3cb19a47392795f5a109b4ed50aaa20","input":"0x","nonce":"0x9d4d","to":"0xf3bc692f1b8a25495c63a5e21906ed7c16cc976a","transactionIndex":"0x11a","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xef42a333834e0ff5c47ac0a96651e3701d2c5f59e424d7f22f0512ed2ba55127","s":"0x535fa706628decb9b4bf85420b8d25eaf94a67eb0d0749b8746762f61c84ca10"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xcdf47726cea581aa0378b01dc18fecc863fbdcf375ca39c5e2bbffe1bfecadd1","input":"0x","nonce":"0x9d4e","to":"0x88483fbc3eac6a4c27e180394cdfe01780b971d9","transactionIndex":"0x11b","value":"0x3b6432fb1c31800","v":"0x26","r":"0x399b92ff667f02a249af27e3fb783eedf9a8fd48745b6609bd0e81641b88c176","s":"0x7a15dda763017a4d4962e716d4e153fa04d9021955250863828c80a5b4a1f35c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7cd35a6c78cdcb0d0cdf2976c4ccc8bc22675d40f87b5be6e309f05f138deebe","input":"0x","nonce":"0x9d4f","to":"0xf31b2602804d986d6298f06f7850fbb1dee44c07","transactionIndex":"0x11c","value":"0x11d1427e8875400","v":"0x26","r":"0x6182f241240e0a693ae127473d0632b75192ec86f25abcd3093d510de46eb7ac","s":"0x71da8f4e8c4df4c4f2cc6489c3799199e7a4dae6be816b0a99cc9338c1ead5c4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xb0628801347233aad9abf0bd2d4cd745cd636e180c573d99f902d467585cb655","input":"0x","nonce":"0x9d50","to":"0x3007abf58617a21fa38383a8d978cf12824e5083","transactionIndex":"0x11d","value":"0xc3d6fc66994000","v":"0x26","r":"0x968d3a6101bf5c9b4d2696815b70d9c2058f9bc771cdb070a191e067e32388ed","s":"0x1e5188201fde674eda698ac00137288ea1c128b00f55ba120d0a1acb47663a3b"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x9cd75bbf02e58624edc86f7fb73648c527dffd236c6b8fdd6809c60f39c69290","input":"0x","nonce":"0x9d51","to":"0xed2fee621473e633b7ae70b35d5a371745b5d7c0","transactionIndex":"0x11e","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x57381d57fbfd2bd4de4581dbef6e526025be89d3b909397b94ab9101c67b240e","s":"0x72cdb9ee50a130bab459b7b5d3571fbaf65143bb4cb92b13d7523e12828233e3"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8295129f3e7f07933f31954ac3119b79d397f4a1442ba43dd8aece46eafad0bb","input":"0x","nonce":"0x9d52","to":"0x996af40e6f835cfe4f6ef7901e841c638183255c","transactionIndex":"0x11f","value":"0x17c1adfe0b47000","v":"0x25","r":"0x3acf5d97079faa59d7f10eb15cac69d606055e9490be84cce0d3f9e9da21b783","s":"0x1ded8203056f75ce13ab52d94a1d7d199b603ad8560a59841e175e6c47766dd7"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x8c4793c0372011a897f8c4114ce8fcdfd02cb568815fba4245a8612c840d22f3","input":"0x","nonce":"0x9d53","to":"0x6b6a72cc53bf65645cd90378ab7235344f57f3d1","transactionIndex":"0x120","value":"0x14316d94b06e800","v":"0x25","r":"0xe1a5e98c7f70e0d6537fe3ddee2c41a5620dc9f485ba57b1b0da9bc19f257fb0","s":"0x14437aff3705bbd139d76c9ac83a00d02ca5dcc5c1deda0855ce506ccb78cbf4"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x527a3a6945c5800af57396b53c89f99d9bca46d64f6b266aa76e3abb7825bb51","input":"0x","nonce":"0x9d54","to":"0x05b03715ab29e54485ee847b926921905779cd4e","transactionIndex":"0x121","value":"0xe8d3be8f66d400","v":"0x26","r":"0x204f995758024eff4af8904d07489f365563e631b88192ab3b19ed98c9729a3","s":"0x77d1b4ee8746bdbb5a3450e3cb5b559095bb67fab461d3d17334ca2749dd70e6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x20185783ecb691a6e0d0e315fa4af57310596745b2f1dd34f8c05418f8e49e67","input":"0x","nonce":"0x9d55","to":"0x84f26e299f3ffcc72e30bcc17057379b9b059450","transactionIndex":"0x122","value":"0xbe0d6ff05a3800","v":"0x25","r":"0x7aea1f615f63ca364d9add4f75f3260367fcb01d072bbf512895ffcfb4d461dc","s":"0x20107d0a72dda0f2e1e76ea3e2bcc6c9afca31c0c54243b6376e1028279c7a32"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xf67b6ef1fd47f4d11e54fa7e9455da9bceb2546bfd7dc8746d0fc90463e29ba2","input":"0x","nonce":"0x9d56","to":"0x989e5a5f88b26d0d8cdb5d575ae4582010cbd9ff","transactionIndex":"0x123","value":"0xbe0d6ff05a3800","v":"0x26","r":"0x1be2409382789e78f0c8415b49b98c9842b7ffe8984594b821c38eae4d1b404e","s":"0xab5b5427ddf4e70ef1bbc627ed1789209c307f86e6805f53fadb0bc6c617317"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x125fb4f1f64f0e3a26fed148a7ddefa52ef94e328bc85d203e4d9f93835d6334","input":"0x","nonce":"0x9d57","to":"0x1bfbeb992ded2e68e6783110048053279c27aaee","transactionIndex":"0x124","value":"0xcda1be8c933400","v":"0x25","r":"0x6eee9dae37a2eb68c5ad7413f36caf03eee0916190894f399dcb101a608be46a","s":"0x6ed3cb6e041a39b01d5a617f74f83f95f092e4504ba8935f3686ca6f75b97f65"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x206ddd0eb0467b94918220c9194ea76c4fd38cd7f1270ba4055bc062947a09e2","input":"0x","nonce":"0x9d58","to":"0x3094c5a507916ad1d30b32704fcba3c781b3b038","transactionIndex":"0x125","value":"0xbca080a4a2e400","v":"0x25","r":"0x12c952bcaa4a479491966d189ab00e94787004433d1cf3f27e44db1533b4fb89","s":"0x1ec37deb9c3c19ab870e9d8d0a28664ba5cdb24827cb415387024752d32ece86"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc8fcbe49bd48d18f1e643d9c30f7eff5b91580ecf18e3cf51a64dc33efef8945","input":"0x","nonce":"0x9d59","to":"0xc1e6d014845c3e9be49b7c7ff404d57eb70bde55","transactionIndex":"0x126","value":"0xb26646c5657000","v":"0x26","r":"0xfc6a142536a53f2c193415f71b30e70873616851a326ff8603d0e2f94bba5e55","s":"0xb6bb5f256d1ac716eeec46450d0be5bc097c1cea3893942edf19c236eda5404"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x808632c02997d355498cfd0958bc6d6234ed895c5714f8038b8156e77092a1dd","input":"0x","nonce":"0x9d5a","to":"0x6eec88f110b7634b7c454ecf6db811bb4e20d1a6","transactionIndex":"0x127","value":"0x30546aba3df2800","v":"0x25","r":"0x5efc9d9e4413f191250d1fa3649568081b18438d460469f38cdf4c4c64e21395","s":"0x548dcec1f369ed12e15e7853b651a9bf123255d7dff536e9651a0732319dba65"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xe501873db84664299077c024d19d5469c9e133f5e9bd473c9f83e1fcc55be399","input":"0x","nonce":"0x9d5b","to":"0x5e27e82fde06a884b709d688a3b054cfbc5d92f3","transactionIndex":"0x128","value":"0xb497a2803e9800","v":"0x26","r":"0x9df43eb8a4464fbf55658e8a1b11acaba33cbb90b8a000a14bd448f1d004799c","s":"0x52e6890fb71ee8e65f2d5127eac2fe795a204455b29909472343477a216c06d"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x3c4957db98859d5b73191adf0adc2721f7fbb1cdb2b89313b7497f2534539622","input":"0x","nonce":"0x9d5c","to":"0x4b916d1e67a42e29365ca2310da3c5c2b4956bb4","transactionIndex":"0x129","value":"0x1762a743bf0e000","v":"0x25","r":"0x2272d8f5f8367dc892ad8fc4d7faac48ae1803eb1cd36f6eed5fdc6c4a40ac9c","s":"0x7e6d5fae5c321780cfd6ea79dc1a2b84ae259128ae1b2b68df70e567d6acc327"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xbca55fac4feaf192382e16e23b7c208e30c86a232a3e217ce03105ce776d4023","input":"0x","nonce":"0x9d5d","to":"0x007ce001301ee96abaa5dbd73e26c1e7b9a16ef5","transactionIndex":"0x12a","value":"0x1e4a2439c7e7800","v":"0x26","r":"0x8fbd9c517cfc6fa4b4d7ea0557f4f60801fb0ae1d955758d03dfce8a7ea068c4","s":"0x6e69a2ed3fa784cea7f7f82e14ed3bd722061607129432d4bb06334b7b80c4ec"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x00a04953f6f4ff9b130e2092117664aa9b8eaedaa7040b0bad7592bb72baafc3","input":"0x","nonce":"0x9d5e","to":"0x0f845cd3da369321429220e6d6e7c3788414e574","transactionIndex":"0x12b","value":"0xb900a526153800","v":"0x25","r":"0x3c743941f289cff5c55e8c83c42dbca60b45919cbede34f337b671bab93de60e","s":"0x12b65e6314dba335249edba1d6bc88caaeec3cddb739203a9b6c40472f0dbfc9"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7f0d62ed1e4e25dabb2f70af5054792df085ca81c0f6ac64121fa97bbb9e39ee","input":"0x","nonce":"0x9d5f","to":"0x3c5b89b3d97e9e56880e4141e24ead232340e4a3","transactionIndex":"0x12c","value":"0xb5d019cc00e800","v":"0x26","r":"0x63972ff9a057b81f446fb119776e16d055399858b236a6d329e45b3452dca643","s":"0x2626b9cc6f3f156b96f5109544afbd5ec4b8ebb125e2b451c3ffdcded38564c6"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xc1c8699fcd8fd3ba414d9d593a3c1de30ed1c03f18a614c5b1f1e2f63de11b8c","input":"0x","nonce":"0x9d60","to":"0xb6bfb46ed86dc95b9a4ac4f9dc54e5eda66f555c","transactionIndex":"0x12d","value":"0x1db2197d8e18c00","v":"0x25","r":"0xa5c8b14f86f3e193d494437b97cfcc44619ccc2fc5ca6930a83efd20f2497443","s":"0x683705920b7dfae3751b43f068e26aba4332a744f7732f362cfcd25334575540"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0xfc57b690a9eae5e5315cefe3dfd24285dce4a4ed089ab9245acf44d3ddabd446","input":"0x","nonce":"0x9d61","to":"0x5304725b936791740704de8795eec60c8bccc3c6","transactionIndex":"0x12e","value":"0xd1cc30c6e63800","v":"0x26","r":"0xa12ad1d0fc0419d5741a47c63b52e007043e5b18d7fc50212138c50fee9adfc7","s":"0x30685b751f0469cc649b7c3cb8c1a7d9fd92c1bdd0448d16063973a43362245c"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x7dcc1722e10be952d4d7c473965d4c82669a7242ea500b4e55ccbbb23777e19e","input":"0x","nonce":"0x9d62","to":"0x5e708092318a8604d4d353d0f1820e256dfbc618","transactionIndex":"0x12f","value":"0xbe0d6ff05a3800","v":"0x26","r":"0xea10d857e88859602a70352d68ee1222554c472fb6be25ffc21afaac7d645bb","s":"0x1f2ce0b79d3297c8d96089d968f0ae94a7d5485ca9e21270f5316dc6fe5dc081"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x89e7853a2fe1e32daeb2c2b06d4cdb1148587c93c049f63bf45c6e302f498c32","input":"0x","nonce":"0x9d63","to":"0x3992c699ddba35a6c706973c6dedbc92eb99462a","transactionIndex":"0x130","value":"0xbe0d6ff05a3800","v":"0x25","r":"0xeec1bdc4d6689af10104b650081fbc49d70b22502afa77b329f7f2d3f617e148","s":"0x1425e1c182fb4496f44e15ef096f634fbdc003003298c3c5220bffc77a7cc804"},{"blockHash":"0x16f37b728aacdb8491eaf8caa84c090285f204d9f6332931144e2fb7fa9c622b","blockNumber":"0x3f29e9","from":"0xfe92a3cf1843b5ec7ccf27b2ae753fac1289fa9d","gas":"0x15f90","gasPrice":"0xee6b2800","hash":"0x983b78add24766c3f9a35cf0c1a471489e92a897d042d0fb8cb4bea11d760015","input":"0x","nonce":"0x9d64","to":"0x2f19943cc9b0352f0cf60924997a49847eef3699","transactionIndex":"0x131","value":"0x12152a80d452c00","v":"0x26","r":"0x13afc637ad749e2aa15f4756ec96dc14504ba5bbadd3dd1f1163aae862e43d1c","s":"0x56876b68b6f58e4c4347e0125aade9cb493bc845eff0037365e3aef08f90452b"}],"transactionsRoot":"0x83975aaf055a868c2d091539397998b8b2a0eb1b25aec5b7aec46515145cafe8","uncles":[]}} diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-997522 b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-997522 new file mode 100644 index 000000000000..9c385bef3953 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-997522 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"author":"0x4bb96091ee9d802ed039c4d1a5f6216f90f81b01","difficulty":"0xae22b2113ed","extraData":"0xd783010400844765746887676f312e352e31856c696e7578","gasLimit":"0x2fefd8","gasUsed":"0x5208","hash":"0x79851e1adb52a8c5490da2df5d8c060b1cc44a3b6eeaada2e20edba5a8e84523","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x4bb96091ee9d802ed039c4d1a5f6216f90f81b01","mixHash":"0x2565992ba4dbd7ab3bb08d1da34051ae1d90c79bc637a21aa2f51f6380bf5f6a","nonce":"0xf7a14147c2320b2d","number":"0xf3892","parentHash":"0x8ad6d5cbe7ec75ed71d5153dd58f2fd413b17c398ad2a7d9309459ce884e6c9b","receiptsRoot":"0xa73a95d90de29c66220c8b8da825cf34ae969efc7f9a878d8ed893565e4b4676","sealFields":["0xa02565992ba4dbd7ab3bb08d1da34051ae1d90c79bc637a21aa2f51f6380bf5f6a","0x88f7a14147c2320b2d"],"sha3Uncles":"0x08793b633d0b21b980107f3e3277c6693f2f3739e0c676a238cbe24d9ae6e252","size":"0x6c0","stateRoot":"0x11e5ea49ecbee25a9b8f267492a5d296ac09cf6179b43bc334242d052bac5963","timestamp":"0x56bf10c5","totalDifficulty":"0x629a0a89232bcd5b","transactions":[{"blockHash":"0x79851e1adb52a8c5490da2df5d8c060b1cc44a3b6eeaada2e20edba5a8e84523","blockNumber":"0xf3892","condition":null,"creates":null,"from":"0x4bb96091ee9d802ed039c4d1a5f6216f90f81b01","gas":"0x15f90","gasPrice":"0xa","hash":"0xd0fc6b051f16468862c462c672532427efef537ea3737b25b10716949d0e2228","input":"0x","networkId":null,"nonce":"0x7c37","publicKey":"0xa9177f27b99a4ad938359d77e0dca4b64e7ce3722c835d8087d4eecb27c8a54d59e2917e6b31ec12e44b1064d102d35815f9707af9571f15e92d1b6fbcd207e9","r":"0x76933e91718154f18db2e993bc96e82abd9a0fac2bae284875341cbecafa837b","raw":"0xf86a827c370a83015f909404a6c6a293340fc3f2244d097b0cfd84d5317ba58844b1eec616322c1c801ba076933e91718154f18db2e993bc96e82abd9a0fac2bae284875341cbecafa837ba02f165c2c4b5f4b786a95e106c48bccc7e065647af5a1942025b6fbfafeabbbf6","s":"0x2f165c2c4b5f4b786a95e106c48bccc7e065647af5a1942025b6fbfafeabbbf6","standardV":"0x0","to":"0x04a6c6a293340fc3f2244d097b0cfd84d5317ba5","transactionIndex":"0x0","v":"0x1b","value":"0x44b1eec616322c1c"}],"transactionsRoot":"0x7ab22cfcf6db5d1628ac888c25e6bc49aba2faaa200fc880f800f1db1e8bd3cc","uncles":["0x319e0dc9a53711579c4ba88062c927a0045443cca57625903ef471d760506a94","0x0324272e484e509c3c9e9e75ad8b48c7d34556e6b269dd72331033fd5cdc1b2a"]},"id":1} diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999998 b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999998 new file mode 100644 index 000000000000..5e9d4d77bff4 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999998 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","id":1,"result":{"author":"0xf8b483dba2c3b7176a3da549ad41a48bb3121069","difficulty":"0xb6cb9824e57","extraData":"0xd983010302844765746887676f312e342e328777696e646f7773","gasLimit":"0x2fefd8","gasUsed":"0x3d860","hash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0xf8b483dba2c3b7176a3da549ad41a48bb3121069","mixHash":"0xcaf27314d80cb3e888d32646402d617d8f8379ca23a6b0255e974e407ffdd846","nonce":"0xbc7609306a77d0a2","number":"0xf423e","parentHash":"0xc6fd988b2d086a7b6eee3d25bad453830391014ba268cf6cc5d139741cb51273","receiptsRoot":"0xb0310e47b0cc7d3bb24c65ec21ec0ddf8dcf1672bc9866d6ba67e83d33215568","sealFields":["0xcaf27314d80cb3e888d32646402d617d8f8379ca23a6b0255e974e407ffdd846","0xbc7609306a77d0a2"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x764","stateRoot":"0xee8306f6cebba17153516cb6586de61d6294b49bc5534eb9378acb848907b277","timestamp":"0x56bfb3ed","totalDifficulty":"0x63053e0134c03db1","transactions":[{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x6b5da959786d801c1bedda58f8a071a40f992f03","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x679c178c832194d3f40afbda60421e8cb12f2c6b879a925d2e60b15a2b4d212e","input":"0x","networkId":null,"nonce":"0x111","publicKey":"0x1acb54447b8e66222a23fe267f75e9c7ff46538e5c7b286ee14bcf7ec587f9656c5eb2163e6e3d7dbffd677de22e50d7e067dff34de403d14f5ead2eaf8368a5","r":"0xd5ad60765e2006490e73bf06f4bc9b382b2ea434eb066b60bc4f577cb056603a","raw":"0xf86e820111850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f64f66ddf683000801ca0d5ad60765e2006490e73bf06f4bc9b382b2ea434eb066b60bc4f577cb056603aa00e8d699411b71b08f550a278b05fb1d36174509758ad7370528ae06cb1965a8f","s":"0xe8d699411b71b08f550a278b05fb1d36174509758ad7370528ae06cb1965a8f","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x0","v":"0x1c","value":"0xf64f66ddf683000"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x9da7521d2b2281b3cd477b553a5dc18b58674f07","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xfe3189ab9a3c3aaa97a08e9410b6569f7528e38a4c86077ea20ddf33bd2c7ea5","input":"0x","networkId":null,"nonce":"0x79","publicKey":"0xa150bdb9419cf198e7430552880e8b050a09952ae53d1fd82d70941c6be318f21b98dcf93a974b763948c1621e460ec8cead12080fc2759c2e3e4dc884d2308b","r":"0xb31d8d88bfcf7a3dd705bc78a078c75542ca1a993860a3c95b2af317ee3a4b0d","raw":"0xf86c79850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880ef726f7729a1000801ca0b31d8d88bfcf7a3dd705bc78a078c75542ca1a993860a3c95b2af317ee3a4b0da076d529630cef5d1acf0d649faf281ebcb13768effce3eb02a96f5228ad2f5333","s":"0x76d529630cef5d1acf0d649faf281ebcb13768effce3eb02a96f5228ad2f5333","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x1","v":"0x1c","value":"0xef726f7729a1000"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x707868ea3bfb73007106cfd30f678fdb94d12173","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xcb7508e8703535fbc801146fa3c7d04798d71a9a0e3bb97a0a14beb733559672","input":"0x","networkId":null,"nonce":"0x251","publicKey":"0x030ad57f373be3cd858bb949365b1438b4383b94fa1b95af0ab5337719539fded4494868e0a82e6df40cddeb9415d8e45a6506ea77c1909c71dd2ec37316da0a","r":"0xbfc3a164f96f95f04ec50af58645d5cf51eaa2473872af9bf23ceab22560e8d6","raw":"0xf86e820251850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88881fc1efd41e37c800801ba0bfc3a164f96f95f04ec50af58645d5cf51eaa2473872af9bf23ceab22560e8d6a053f43d489fd83f8e2c9acbf2d14695c63838c18f420021771f111750aac8efba","s":"0x53f43d489fd83f8e2c9acbf2d14695c63838c18f420021771f111750aac8efba","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x2","v":"0x1b","value":"0x1fc1efd41e37c800"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0xd614cc8e7d44e6e5d48b9b3efd5ffec36098f403","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xf333f42badd731da2869ce92d95a255f75ac2a16ed043e6b343ed91d4fdbb579","input":"0x","networkId":null,"nonce":"0x18c","publicKey":"0x34ff9f742cb0c7feaf8109a722d4518fd504abedc4f66e4e6bf8ece0726841c132e5660bbabe5dbe83414cda8ddb5b0aae4a649661747a817cfb79045c22d419","r":"0x32a184bbbe6168a2ebfba1be61d3535d45ce580b130eed8df8f5024be97f5bf8","raw":"0xf86e82018c850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880eeee41c060f2400801ca032a184bbbe6168a2ebfba1be61d3535d45ce580b130eed8df8f5024be97f5bf8a071c020aef32840e0f4f5ea2b095faa4602586a471d33c62563146314c4970a93","s":"0x71c020aef32840e0f4f5ea2b095faa4602586a471d33c62563146314c4970a93","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x3","v":"0x1c","value":"0xeeee41c060f2400"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x078838304c9ee678209ea0959587da9b6f31ebff","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xfa50db902c56466492e9f32fd543edaa1554a47b2e288175c262685df0537106","input":"0x","networkId":null,"nonce":"0xf46","publicKey":"0x866ede0bed987e0e8736cc94244640df1124b5b789b780bc012b936c2559cc630102e32c1c454f92626542eca44802f3ee44437a031fa1eaabcbdf323891eb93","r":"0x9a569d066c62c64ec8b93c6d268499a276fe882289f6090e65748911ec81b256","raw":"0xf86e820f46850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880e62a83e59ffa400801ca09a569d066c62c64ec8b93c6d268499a276fe882289f6090e65748911ec81b256a01e7b9216b86d6a5517b88a2aaef666732c51486214948fdecd89b9043a30750c","s":"0x1e7b9216b86d6a5517b88a2aaef666732c51486214948fdecd89b9043a30750c","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x4","v":"0x1c","value":"0xe62a83e59ffa400"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x460825a3542f4823818184020ba3861da1e26872","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x0b4a6c8459c02f647d8a5c667e292de3e45c5f03558a0e814377e5356ebc6234","input":"0x","networkId":null,"nonce":"0x113","publicKey":"0xee1a6b3dc03e8b5329d99b77c33f64767196ce47236b4c9ee2baa87827a6348488926ae6da54abbf788f5d2602dff65984a60020407e7e8b2da160f32e80a344","r":"0x87842eacb46cc63064a8a8f0932ce3f18c0d27f81a8124d2c3a9f751293b11d0","raw":"0xf86e820113850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880efd50e050f64400801ca087842eacb46cc63064a8a8f0932ce3f18c0d27f81a8124d2c3a9f751293b11d0a04e7678e22ce8ec60a04c36fa5685421a3bf8b9d0ff68280a8f31d6db49629afe","s":"0x4e7678e22ce8ec60a04c36fa5685421a3bf8b9d0ff68280a8f31d6db49629afe","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x5","v":"0x1c","value":"0xefd50e050f64400"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0xa29862fb7f9b37374d0c9062ab52bdd74d1af867","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xc4ea04477167cc599788100bef3306eca140549e747ba531db579eb2a72b1b11","input":"0x","networkId":null,"nonce":"0x59a","publicKey":"0xa3e333b30947a5a685b47b387a92f65a7c5d7b61f6f3016777f720e83fea9fbe5faf6fcb3296e0cd9da6ec9acf30920d5d67c2c4636a79f940b6e2fbe46c14a7","r":"0x90ddc9473c323eebd5c4a35251cd437e62563c883e8e87b141389fde111c5b24","raw":"0xf86e82059a850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f32a22e7fc0f800801ca090ddc9473c323eebd5c4a35251cd437e62563c883e8e87b141389fde111c5b24a039a1dfc3e2b85c74fce62ed7369ac1a62de13b31f4fb47e5fb02232aeefd83f4","s":"0x39a1dfc3e2b85c74fce62ed7369ac1a62de13b31f4fb47e5fb02232aeefd83f4","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x6","v":"0x1c","value":"0xf32a22e7fc0f800"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x771dd02681c793eb34eff34528309e3657f843fb","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xf73f661edcb6e8fc0b48a5bb5292e8b5db8ea911e4664ed1f8af1b2e66f6f585","input":"0x","networkId":null,"nonce":"0x211","publicKey":"0xca6db6e9182a094b5cbfa68741ab7c31450582eb65f1c558798a08b230de63a2f25deedc62d276a5f3eef3526282e28c7efdbbcba8e3ed4dad086c2201f10855","r":"0x7ecfd78b2838d73283f6de62bee1a046830fac75fb5b85ede279dbac097feec6","raw":"0xf86e820211850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d888811a2bd08b7075400801ba07ecfd78b2838d73283f6de62bee1a046830fac75fb5b85ede279dbac097feec6a01cfc1ced8140efc2dc71e217d6693665942ef1424affd7d61c134ed462605922","s":"0x1cfc1ced8140efc2dc71e217d6693665942ef1424affd7d61c134ed462605922","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x7","v":"0x1b","value":"0x11a2bd08b7075400"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0xfbe56e8afb28e097a871b2747800079ad5c29c03","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x9b2569e1b26d29730cf262756a6033834e34345f4a18caa241117747ce8cf746","input":"0x","networkId":null,"nonce":"0x6c","publicKey":"0x7c2ee029ec45aa73444091d1a0c3f830bb7f91797b30a1f53c11a2fbec10f7bb7706a9569350da382cc623c2b65d03b480ae96bc168021da4f0df60146f9e16c","r":"0xb1f3b2754a9189b376bc32d03a1097d4fe0cfaae3e55e45a4249127b9b541399","raw":"0xf86c6c850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f94ad612cf85000801ca0b1f3b2754a9189b376bc32d03a1097d4fe0cfaae3e55e45a4249127b9b541399a025b51f84e621e9193dfb7172dfdea0379bbf8d5d73e25de0e2d0dc50f657e249","s":"0x25b51f84e621e9193dfb7172dfdea0379bbf8d5d73e25de0e2d0dc50f657e249","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x8","v":"0x1c","value":"0xf94ad612cf85000"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0xe6ea7febb65f6fb46dc42dea2f873c67aadb1f72","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x5ac04be22ee89dce8c33f334a41ab05e1cbeca16669003c5ffe2c220f772b097","input":"0x","networkId":null,"nonce":"0x170","publicKey":"0xa6238a7419a3321706c6612d7cc647bce4568ec6ce4a999d081077feac54ec8d1e2627484782a15a4a2c2eca0a71bee25b5a82a7ca74c84b75f89ec2f8bbb5ea","r":"0x3c26e80876f0901d3007a8798f9792d426b6f78079dcd06d91019677850b9356","raw":"0xf86e820170850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d8888115740dac6be2400801ca03c26e80876f0901d3007a8798f9792d426b6f78079dcd06d91019677850b9356a028a644324a777b7beade6b8432d6f95f85112863e08c50bd3e22d1594244014c","s":"0x28a644324a777b7beade6b8432d6f95f85112863e08c50bd3e22d1594244014c","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x9","v":"0x1c","value":"0x115740dac6be2400"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x2a65aca4d5fc5b5c859090a6c34d164135398226","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0x46a83d066750df27119aa3e314641fb3b3ec6e1afc1e768d3da4ac941a6a0a8d","input":"0x","networkId":null,"nonce":"0x2a11d","publicKey":"0x4c3eb5e19c71d8245eaaaba21ef8f94a70e9250848d10ade086f893a7a33a06d7063590e9e6ca88f918d7704840d903298fe802b6047fa7f6d09603eba690c39","r":"0x85bada12a37f21016e8801d6136cd7793192346a0f29f4fd37782d774378a7df","raw":"0xf8708302a11d850ba43b740083015f90945d65e227f4e7bc798cf62526f4bdd47c82e6a590880eb35d6f4e620c00801ca085bada12a37f21016e8801d6136cd7793192346a0f29f4fd37782d774378a7dfa07e1c78a62e1c16b955dc1b56f657c51fe2dfb739c2c1d11fe4845583706719a8","s":"0x7e1c78a62e1c16b955dc1b56f657c51fe2dfb739c2c1d11fe4845583706719a8","standardV":"0x1","to":"0x5d65e227f4e7bc798cf62526f4bdd47c82e6a590","transactionIndex":"0xa","v":"0x1c","value":"0xeb35d6f4e620c00"},{"blockHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","blockNumber":"0xf423e","condition":null,"creates":null,"from":"0x2a65aca4d5fc5b5c859090a6c34d164135398226","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0x15dd5bba84901824fb3aa75618a92b7cbacb454c53eaa962a2ca8667acb06a78","input":"0x","networkId":null,"nonce":"0x2a11e","publicKey":"0x4c3eb5e19c71d8245eaaaba21ef8f94a70e9250848d10ade086f893a7a33a06d7063590e9e6ca88f918d7704840d903298fe802b6047fa7f6d09603eba690c39","r":"0x1611395215c0ede475af6fd3b647c674d18735851060ccad0e0e7a7c150831c9","raw":"0xf8708302a11e850ba43b740083015f909436fab08874deb6cd0e7f916ddee8957630073d47880eb1fbb47be3f800801ca01611395215c0ede475af6fd3b647c674d18735851060ccad0e0e7a7c150831c9a0333716a13f040cbd8ac43462b9cfa8d602d4a3413825d283705bc3d4b22af8de","s":"0x333716a13f040cbd8ac43462b9cfa8d602d4a3413825d283705bc3d4b22af8de","standardV":"0x1","to":"0x36fab08874deb6cd0e7f916ddee8957630073d47","transactionIndex":"0xb","v":"0x1c","value":"0xeb1fbb47be3f800"}],"transactionsRoot":"0x6414d72a4c223bce7d1309869332b148670eb66af4e3b3ba6d1a55aa0bb3fd4f","uncles":[]}} \ No newline at end of file diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999999 b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999999 new file mode 100644 index 000000000000..de007b641fb5 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-json-999999 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"author":"0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5","difficulty":"0xb6b4beb1e8e","extraData":"0xd783010303844765746887676f312e342e32856c696e7578","gasLimit":"0x2fefd8","gasUsed":"0x38658","hash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5","mixHash":"0x5b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0","nonce":"0xf491f46b60fe04b3","number":"0xf423f","parentHash":"0xd33c9dde9fff0ebaa6e71e8b26d2bda15ccf111c7af1b633698ac847667f0fb4","receiptsRoot":"0x7fa0f6ca2a01823208d80801edad37e3e3a003b55c89319b45eb1f97862ad229","sealFields":["0xa05b10f4a08a6c209d426f6158bd24b574f4f7b7aa0099c67c14a1f693b4dd04d0","0x88f491f46b60fe04b3"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x6e8","stateRoot":"0xed98aa4b5b19c82fb35364f08508ae0a6dec665fa57663dca94c5d70554cde10","timestamp":"0x56bfb405","totalDifficulty":"0x6305496c80ab5c3f","transactions":[{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0xc3665b8a9224ba8da9a20322f31d599cafa52c5c","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x22879e0bc9602fef59dc0602f9bc385f12632da5cb4eee4b813a0c27159c4d24","input":"0x","networkId":null,"nonce":"0x1d3","publicKey":"0xc3dbee74f1b2b8dbedc417244b7f5a134c6f7769faf9ffe784b3f0fdda7ca52cf914d3f2b3164c009bf939796b77f047ccb4cc113d3bde5b06555b781e0c7149","r":"0x43531017f1569ec692c0bf1ad710ddb5158b60505ea33fb7a21245738539e2d5","raw":"0xf86e8201d3850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d8888102363ac310a4000801ca043531017f1569ec692c0bf1ad710ddb5158b60505ea33fb7a21245738539e2d5a03856c6a1117ff71e9b769ccb6960674038a3326c3dd84c152fc83ada28145a07","s":"0x3856c6a1117ff71e9b769ccb6960674038a3326c3dd84c152fc83ada28145a07","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x0","v":"0x1c","value":"0x102363ac310a4000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x4ce758b0c8aa655b77c14f16bd0190b5715be75a","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x3c634bf5f09f6b5b5ea377df7abb483f422ae5d4ba389c395f14f833de25d362","input":"0x","networkId":null,"nonce":"0x9","publicKey":"0x75022ee25c702fc6a53853843e00e87877e737f9c631a9d831c11693d7e31877a1b09755ab3a5c112decf57339839364b8b9a3c23ada01761b1e3a044e297316","r":"0x8219a4f30cb8dd7d5e1163ac433f207b599d804b0d74ee54c8694014db647700","raw":"0xf86c09850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880ed350879ce50000801ba08219a4f30cb8dd7d5e1163ac433f207b599d804b0d74ee54c8694014db647700a03db2e806986a746d44d675fdbbd7594bb2856946ba257209abfffdd1628141af","s":"0x3db2e806986a746d44d675fdbbd7594bb2856946ba257209abfffdd1628141af","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x1","v":"0x1b","value":"0xed350879ce50000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x30906581413d556de1a018adbe6cc63c88d58512","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x59feccaad599e776cd6635e68b5e19254cca3b38e49437044f1e1d15d00b0576","input":"0x","networkId":null,"nonce":"0x59","publicKey":"0xccf6be26c1eb1c89d5fe958db0112a46e3ac23a95ac0f709ce84a49ae3f20bcf143909bfe67f685caaf362066e1c7e224899f57678bbcecb7a720175bcbb387d","r":"0x1ca26859a6eed116312010359c2e8351d126f31b078a0e2e19aae0acc98d9488","raw":"0xf86c59850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88882b0ca8b9f5f02000801ba01ca26859a6eed116312010359c2e8351d126f31b078a0e2e19aae0acc98d9488a0172c1a299737440a9063af6547d567ca7d269bfc2a9e81ec1de21aa8bd8e17b1","s":"0x172c1a299737440a9063af6547d567ca7d269bfc2a9e81ec1de21aa8bd8e17b1","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x2","v":"0x1b","value":"0x2b0ca8b9f5f02000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x8bec4e6fb1a28820eb1e8ec2d4eae4842ed2f923","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x98a03afa804e248ada5f26e9118ae927d4d3cb60e78c54938dced1cf25ee3567","input":"0x","networkId":null,"nonce":"0x2","publicKey":"0xbc8c89a85804c7859069c13561dbbd8d1d4739ec7d18514c42b3ffea64529cee522a5e20d93373d0074e94c4c7b6eba51c7d2f18ef7c64c37520342acb233795","r":"0xa5aca100a264a8da4a58bef77c5116a6dde42186ac249623c0edcb30189640a","raw":"0xf86c02850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880fd037ba87693800801ba00a5aca100a264a8da4a58bef77c5116a6dde42186ac249623c0edcb30189640aa0783e9439755023b919897574f94337aaac4a1ddc20217e3ac264a7edf813ffdd","s":"0x783e9439755023b919897574f94337aaac4a1ddc20217e3ac264a7edf813ffdd","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x3","v":"0x1b","value":"0xfd037ba87693800"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x4835a9626b02369546502d2949e16b0fda110b0c","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x18f1e6430334ad548bc36fc317016bc9f7a076d1fa50a89fe4e1d095ed3f9562","input":"0x","networkId":null,"nonce":"0xd9","publicKey":"0x91b3b4fe89d112cfc7308619e8aa7de86f14af3f6b6e4e92becb6e29e98207835bbe1a69109c16b14b0eb7285d2b952a9cde6007932afe95e81eefc183f75314","r":"0xb93c6f8dce800a1ec57d70813c4d35e3ffe25a6f1ae9057cf706636cf34d662","raw":"0xf86d81d9850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d888814bac05c835a5400801ba00b93c6f8dce800a1ec57d70813c4d35e3ffe25a6f1ae9057cf706636cf34d662a06d254a5557b7716ef01dd28aa84cc919f397c0a778f3a109a1ee9df2fc530ec0","s":"0x6d254a5557b7716ef01dd28aa84cc919f397c0a778f3a109a1ee9df2fc530ec0","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x4","v":"0x1b","value":"0x14bac05c835a5400"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x9cc72ebf3daaf12c72e48605e1e67b47c95a1911","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0xb1cada8daf63c45750df1ee79eed5a3cf6240e3cebdb6de3f26bc7cf03217bf4","input":"0x","networkId":null,"nonce":"0x34","publicKey":"0x90dff18c1c01d566e6d8bf0190e3e965f98e7f51ccbbe6040f9a9972e88f4ad19f1547406454fbc9e1ebcf4c5f2f1e2df9b9371028fe0a552ecca5f5f0aa4129","r":"0xe9a25c929c26d1a95232ba75aef419a91b470651eb77614695e16c5ba023e383","raw":"0xf86c34850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f258512af0d4000801ba0e9a25c929c26d1a95232ba75aef419a91b470651eb77614695e16c5ba023e383a0679fb2fc0d0b0f3549967c0894ee7d947f07d238a83ef745bc3ced5143a4af36","s":"0x679fb2fc0d0b0f3549967c0894ee7d947f07d238a83ef745bc3ced5143a4af36","standardV":"0x0","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x5","v":"0x1b","value":"0xf258512af0d4000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x5c51467399bc655f0cc6db88df15946717534633","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x4fa879b491e0779fc035758ec77b93c4e51d528d65b64eb055c015a58deff103","input":"0x","networkId":null,"nonce":"0x6f","publicKey":"0x0b7e2532afc2daa33763002525aa6c7edc25ea97d63baeeb2c6f5094f18dca4a0212b52061f9a9091aad5c4380a6506f9a51ddd2d014e78742bf144a58d6ffa0","r":"0x9e0b8360a36d6d0320aef19bd811431b1a692504549da9f05f9b4d9e329993b9","raw":"0xf86c6f850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88881c54e302456eb400801ca09e0b8360a36d6d0320aef19bd811431b1a692504549da9f05f9b4d9e329993b9a05acff70bd8cf82d9d70b11d4e59dc5d54937475ec394ec846263495f61e5e6ee","s":"0x5acff70bd8cf82d9d70b11d4e59dc5d54937475ec394ec846263495f61e5e6ee","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x6","v":"0x1c","value":"0x1c54e302456eb400"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x055d9d7ec193d1e062c6ec4fa80ef89b5c1258f4","gas":"0x5208","gasPrice":"0xdf8475800","hash":"0x1bea59827ab153b20cee79890d221a80fa6a04e552d667504c592ed314fb6d76","input":"0x","networkId":null,"nonce":"0x46","publicKey":"0xfae19a0ac08d36f0229663d45d0c41ca52c4e295c7af82a1b39515a79025175293400d026e0d41767aac42f8b7e4a6687c5762161457d753f1fc0766614868f9","r":"0xb2803f1bfa237bda762d214f71a4c71a7306f55df2880c77d746024e81ccbaa2","raw":"0xf86c46850df84758008252089432be343b94f860124dc4fee278fdcbd38c102d88880f0447b1edca4000801ca0b2803f1bfa237bda762d214f71a4c71a7306f55df2880c77d746024e81ccbaa2a07aeed35c0cbfbe0ed6552fd55b3f57fdc054eeabd02fc61bf66d9a8843aa593a","s":"0x7aeed35c0cbfbe0ed6552fd55b3f57fdc054eeabd02fc61bf66d9a8843aa593a","standardV":"0x1","to":"0x32be343b94f860124dc4fee278fdcbd38c102d88","transactionIndex":"0x7","v":"0x1c","value":"0xf0447b1edca4000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x8e68c0c9b5275fa684291304af9cafe6ceaf2772","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0x73e87db1108a2aa852f48e088ca1a2771f9b7c18af8d1bd77a3cdcc72a750c56","input":"0x","networkId":null,"nonce":"0x3","publicKey":"0xa5e423dfcbdbba1fdbb785367a88235fa2569061d72b6c715111ac21cbef8fc1db860acdef85f1408c760f34b28a4f07d950ac15c4b85d5e528e50f546a89b6d","r":"0x6dccb1349919662c40455aee04472ae307195580837510ecf2e6fc428876eb03","raw":"0xf86d03850ba43b740083015f909426016a2b5d872adc1b131a4cd9d4b18789d0d9eb88016345785d8a0000801ba06dccb1349919662c40455aee04472ae307195580837510ecf2e6fc428876eb03a03b84ea9c3c6462ac086a1d789a167c2735896a6b5a40e85a6e45da8884fe27de","s":"0x3b84ea9c3c6462ac086a1d789a167c2735896a6b5a40e85a6e45da8884fe27de","standardV":"0x0","to":"0x26016a2b5d872adc1b131a4cd9d4b18789d0d9eb","transactionIndex":"0x8","v":"0x1b","value":"0x16345785d8a0000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x2a65aca4d5fc5b5c859090a6c34d164135398226","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0x337a5e90b73f44ffebea73cb3d97738c524f63e1032b30735e43212cff731aee","input":"0x","networkId":null,"nonce":"0x2a11f","publicKey":"0x4c3eb5e19c71d8245eaaaba21ef8f94a70e9250848d10ade086f893a7a33a06d7063590e9e6ca88f918d7704840d903298fe802b6047fa7f6d09603eba690c39","r":"0xaa8909295ff178639df961126970f44b5d894326eb47cead161f6910799a98b8","raw":"0xf8708302a11f850ba43b740083015f90945275c3371ece4d4a5b1e14cf6dbfc2277d58ef92880e93ea6a35f2e000801ba0aa8909295ff178639df961126970f44b5d894326eb47cead161f6910799a98b8a0254d7742eccaf2f4c44bfe638378dcf42bdde9465f231b89003cc7927de5d46e","s":"0x254d7742eccaf2f4c44bfe638378dcf42bdde9465f231b89003cc7927de5d46e","standardV":"0x0","to":"0x5275c3371ece4d4a5b1e14cf6dbfc2277d58ef92","transactionIndex":"0x9","v":"0x1b","value":"0xe93ea6a35f2e000"},{"blockHash":"0xb4fbadf8ea452b139718e2700dc1135cfc81145031c84b7ab27cd710394f7b38","blockNumber":"0xf423f","condition":null,"creates":null,"from":"0x2a65aca4d5fc5b5c859090a6c34d164135398226","gas":"0x15f90","gasPrice":"0xba43b7400","hash":"0xc280ab030e20bc9ef72c87b420d58f598bda753ef80a53136a923848b0c89a5c","input":"0x","networkId":null,"nonce":"0x2a120","publicKey":"0x4c3eb5e19c71d8245eaaaba21ef8f94a70e9250848d10ade086f893a7a33a06d7063590e9e6ca88f918d7704840d903298fe802b6047fa7f6d09603eba690c39","r":"0xcfe3ad31d6612f8d787c45f115cc5b43fb22bcc210b62ae71dc7cbf0a6bea8df","raw":"0xf8708302a120850ba43b740083015f90941c51bf013add0857c5d9cf2f71a7f15ca93d4816880e917c4b10c87400801ca0cfe3ad31d6612f8d787c45f115cc5b43fb22bcc210b62ae71dc7cbf0a6bea8dfa057db8998114fae3c337e99dbd8573d4085691880f4576c6c1f6c5bbfe67d6cf0","s":"0x57db8998114fae3c337e99dbd8573d4085691880f4576c6c1f6c5bbfe67d6cf0","standardV":"0x1","to":"0x1c51bf013add0857c5d9cf2f71a7f15ca93d4816","transactionIndex":"0xa","v":"0x1c","value":"0xe917c4b10c87400"}],"transactionsRoot":"0x447cbd8c48f498a6912b10831cdff59c7fbfcbbe735ca92883d4fa06dcd7ae54","uncles":[]},"id":1} diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-body-rlp-997522 b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-rlp-997522 new file mode 100644 index 0000000000000000000000000000000000000000..ca176613e46a0b03ceaf15c4c5012d079726c562 GIT binary patch literal 1728 zcmey#w)ZEK_=2u$S5H5GQ~I{>s;KSNe*G)L8*41PE-k-lFeUO_hhNU@1ss*u$+p~z zI~xS*?TpHgW!meTTRu2ew#ef2Bi~ui9tBPD-kC7*-P{Jf3zkPNF8!vMKjDWo;{w5_ zuRP!EdlWUhU#(=)(o56UaGp=B++uy$L`9cXdrf5Wf~rkAf4<$071LPL(WClokLT(| zzg8*mpJ@2O@bR`>_vJGS!d}RWOP>i{_P^Qn*`xzM@}fN+NYBh!dw?-Xf9vDqEs8rC z8Adv2<9ej6DEzjW-(pg;{`(tEK^!e%`vs29xZcdj!qDQLT9VP8o^PmUs%O}mlbKgq zu|PF-ruLHC*H>F_=#^dU5V%fu!tvS1%oj;5`YNB?us=SlUV`4@+G7ZuT$|l=QENl$<*t_ZF z~O?}9Ar2EYA2dODlUl&eMP~Gf)_TCMz-=qnxjaM^}+R7KvwfcEfgZCk}{^l8c?~g5zJ>&c0 z-tpL~)va68x1G5nby>(&dP-M`@P~y?e;uw&cAt__8RNBJZoc2;r}ysuIJDNla~b2U z1wRd@Ofu41?V@7eW1+%Pvbpxc(d1nx;-wlh9oL+6-6Tser4dvxniLt@xE{Cfopc76 z6#GF*u>q72r{hkDy8f5`VoIEF4)?7Zq~lLnap%Ct*^J2b869= z3VT<|9D{Gp^F?=4@5rqNv)3Cl+~%ZRPabQ`zz+rbOB5kkF1_^t0P73as{LZ@{2 MWXe8D+O+v50A5(%GXMYp literal 0 HcmV?d00001 diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-body-rlp-999999 b/statediff/indexer/ipfs/ipld/test_data/eth-block-body-rlp-999999 new file mode 100644 index 0000000000000000000000000000000000000000..3719c36d3ca61c6709df9dc288efe2268d9f37ad GIT binary patch literal 1768 zcmd_p`#aMM00!{Q9OgEgTWxb$$Dx|ChiDRU%q+$!qPgEDnl^FT*eT29YuR*pik%a= z^qnLm9-`vJJSv2zxnBx(+M^Vf%gS`nIr{1R<_|dUKk)hCecx5J=~b2O;Gk3S)Xi^* z=JNNZ7cB=`#Gz7@@!7=(`xrT0l-M-g2GFF}XJ)E1?Cf2BW88z@Ao17i z)?J74C?dbd?!!btLyngi9*`tWVR`3djo^HK6*UCiMYw+~W`Pf{(PYiVM%+%OPgHt` zB?fp;Awl=lmP>~f`Q_KFkbv>)&qZmHp4Ky=mAinUuWA#cHU3~4_@x^U&9sG&!J+f_ zh%y-nYYY|I6q0Anu3x|p>~4ZW{_nYmd|7)aJXOyQUT^Lam8+@qXJugYGf-`9#%mLI#Lb9U-B9zRr>^r zYl>5m8>;tLRXdaknb1MLj*=1x$@GJZY+LOPijzvc3*YG77b0%*w6)jh`)hU@OTWGp{#O7>O+<^txPj`{JjRXQ+7Al-u_B zcF$-h#Ke85Fh&#P9ughk95ru33pwmn60xe<%VYw#g%$<3q{XwuK#o}+-1X}e6i*#O_~_HUuriJ4YDk}0%#AD5*ADbaw@!^ zqnEF@-Qg~aHN^7brL|*-HYL+;3r*L8uUD({?@X}Zhtkm8BFp|tKig(bjRIYXrKtJN zrbK!C{ueU@G?xKj+|Bwm9W7mZ&p%S(A~{c#ras_MacG77*lFIER93rR$vnMZ5jFizu4H;_q3Nnp>gKz@U!XqS1^dJB}okbz;t_eGXd^ zXs3*NsQi-A+gu8sl@Eq$OIi^l0fgZY7utH8zx-|=p#%Gcbwfa^3UXAkV0^8zgKP)@ zjkqeZgvMKiW#cEX`WWx=p}K5g-}tNmN+)x}a=w3n)mvv*fR5ecN)DE*Qt&c$bU-@5 zLC!6`TXPd6&%(fiSs#m>Vxp_z@h03GMyWgPh4H6?U1x$5$S(z18#||ziGYgO>_4I( z=LymDg^yQ=sezOwY2V2U{FEy|Uc4Hm$4FFA!-OQuS;~uh zMrI7;Z#VKEffha~ZgbsBil@{KI>4P!SWSNLh%mBY?;`F$qOq?8>Np9f zJsX!2lI|>qRyrRx5+JUnQjuNk%>t#edp09cgv;Di@}gmH2(=ybXRb(H7IKxI(p4h-VWHDshbxoar~KH`e0$O1?c!OsOFh>*F6rJZB#^nF`G6Q> zHiuV2$t$C}dH2jy8K*^cNr=cxZ(jF(`EjHDZx$>%nb4an(7dgjQQc_2$XsQ8rpvl+ zA<7%iY%|ET*mWgv!SQnv^Y>)*Ux{vU>R57(F=rWrZu!Pv^Gd|UlGHeUPZrwA$S~4D z8+ZOx%exiL{7yE_`tNTvEfHu5+rRn1teee@%uFrrsU;ch>G_6wCVEEg<(YXY`Q^n6 wHh0`J{Iqo5iBspzEYbv|GFd`meg7}LHr3pzj`!Q=Rr(!auD`wn=%m~P0D29FEC2ui literal 0 HcmV?d00001 diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-header-rlp-999997 b/statediff/indexer/ipfs/ipld/test_data/eth-block-header-rlp-999997 new file mode 100644 index 0000000000000000000000000000000000000000..3d3cf65afe5412dd633622fc2ff1424362faad06 GIT binary patch literal 539 zcmey#B(b17)pWt*$8PW1A4s}{-Ha@p(QDb5Jo`Iae1fjv_O-8eE|5Lr`{Lg5*s9g7 zThq6lxgvF0$W?kuSBdb4g-(ARu1t2H612zVs(CZ#JFWeqmir6uN!M0C-nt;=rqBb6 z&R6ascLgl>S}dGaZXC7dvgIrrE2|Pi*N&?#3v_hU9wyCpmb&_j*>=L0H!<3gSIo~# znQ$mh{rbIdb8+l~)`M}2(%&vfdg9L29-seMIJP(WYvL2muLp{CLM;!+O7CQ380nym zJ5Sx+Y;QBalWnv9`y0(nk33q!_HVv6<9ahAGjof3YDq?WdcL8aiJnnwPG(+d#e%n& u8+L4CaGTS4J^A-`8@Ve&jVXtAoH-(Q-PTU*$E6kbI;v+e+Sx7(It2i#k%%h* literal 0 HcmV?d00001 diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-block-header-rlp-999999 b/statediff/indexer/ipfs/ipld/test_data/eth-block-header-rlp-999999 new file mode 100644 index 0000000000000000000000000000000000000000..6b79b705624905bda92b127e7afe6b319c841be8 GIT binary patch literal 539 zcmey#B(dPK&D?wQ|MTry_FS%8?b6tGlWr6G&-xv3e$5yRw z-I~7b%oVB2Lax$Nx=MsUEOh$oaAmUll%PE>SIwI_-)Zd^wcKBLPrA1H@zw=zXRPv$ zmOP=qIXLA*E5|yn+&5|QOUsh)tn`U32=%!qu)w8eZ;!{98OtVW3pC5z|2n6B|LJ|j zF)KBiul!=WbA4UNg8BvDPH8bV8FAd;V0^pQ{PE)j%v)nR4QIQ)mY?3HbxCt4Bg04s zZQR-3ujTrh`JL>W_21uUW^RjU3ERJgb;k8(MrP&~_tcV%_Vj#1Jrg~n)||||(uxJq v0$&z%LY=pPpw%vGlUgGJ_&!1!)kNZ=cpY^}fnVlTDhW3(Zl#nMIrhW8^pl74-% X+{XjJ~9y(nbM>_ai)#etojs W#{N7^8RR_+e8E3|qp-aG z$l=^GKA=k37!h2RAf#7WM|B--s(ZHFu2ZmqZq%jz4Hq-uy1AgD4o!dph?a1V{tQ6o z&41;!ul3vzExbTnwIEMxE<(GY*R{QfNUSbQcvKG|coOF3%%fK9@e3{Y?Fb2sux1bw zpcc`n6}uYP|9Aw%Uz&TM4e;{|WcLBBL`|AJL_zD43xJ^hf%;bu(Uav8Jwh?Ag`yROC4Y31Oqmp9S8@Zd5AJgDkl412tlsS?ZG=6n}dXR{G=-=5^K*E7G}Q5-{{y1q7_YZ3`B zNf;X7Ordl8XwjubZy3@Kqb#~}l|WLxphrH>iheezDd$i*qp(%w8-p^-@G~TGt&A)ve3G<7|Cro)b4KQ;nH==GKnk{zBO5`%L7z}1G-`QV^VM9S>{MahXDboQbZ zXElg!?e_7fGh&XsfG)?Kghoi9rStOlR3S*obC-OI9u-VO0RG7@bzgW!@wYV7s~wQ% zpy(ro4^Y2yx~`oUGoxxmO151JZ>qXLPX&X{45ZrGEui>|JIj-ZlLvi)g>SwV_b`fzA{0 literal 0 HcmV?d00001 diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-000dd0 b/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-000dd0 new file mode 100644 index 0000000000000000000000000000000000000000..2fbe90bd675650b967d5ab654454d827235560fe GIT binary patch literal 83 zcmewn*Z>3zsH$nd}lg jJwogdTiMw)3m9(Rs@>i{rS8?LSHZWX>?D3VgA@S(z^*A@ literal 0 HcmV?d00001 diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-113049 b/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-113049 new file mode 100644 index 000000000000..e7407c417811 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-113049 @@ -0,0 +1 @@ +â ¤îJN…>ëb$Ékgº­$á2æÍ |Äé Ÿêd¹¥ \ No newline at end of file diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-9d1860 b/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-9d1860 new file mode 100644 index 000000000000..d39f6324f90c --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-9d1860 @@ -0,0 +1 @@ +ä‚ ¾Ëšþ?ÂÕùL=d@•Ki+ee&R-Er?*Mv(}280@aKj@gEiirhmG4lknQvd++ C9u!Xi literal 0 HcmV?d00001 diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-ffc25c b/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-ffc25c new file mode 100644 index 000000000000..3044ec772e82 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-storage-trie-rlp-ffc25c @@ -0,0 +1 @@ +øQ€€€€ .ñ¼íÓ½b‡Rßñü‰fÞ÷-½oñt6˜áKꀀ€€€ ÿÇ¿£Uõ<åZh€É ƒhmx[Ü-­#k”3ÍÙ€€€€€€ \ No newline at end of file diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-0 b/statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-0 new file mode 100644 index 000000000000..bf647e86361e --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-0 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"author":"0x68795c4aa09d6f4ed3e5deddf8c2ad3049a601da","difficulty":"0xae387bd92cc","extraData":"0xd783010400844765746887676f312e352e31856c696e7578","gasLimit":"0x2fefd8","gasUsed":"0x0","hash":"0x319e0dc9a53711579c4ba88062c927a0045443cca57625903ef471d760506a94","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x68795c4aa09d6f4ed3e5deddf8c2ad3049a601da","mixHash":"0x2d4fd3be43fd50f5c0ba7f1c86c8f468b5c14f75b6143da927a2994383f26640","nonce":"0x0aaaa7fe9d7cf7f4","number":"0xf388f","parentHash":"0xac74216bbdb0ebec6612ad5f26301ab50e588aabe75a804bc2068f83980eefc6","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sealFields":["0xa02d4fd3be43fd50f5c0ba7f1c86c8f468b5c14f75b6143da927a2994383f26640","0x880aaaa7fe9d7cf7f4"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":null,"stateRoot":"0xf9309492322aab44243f8c38240874b37dd0c563bac85f1a816941acc945b21d","timestamp":"0x56bf1097","totalDifficulty":"0x6299e9e3fdb6eb4d","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]},"id":1} diff --git a/statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-1 b/statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-1 new file mode 100644 index 000000000000..2d9e1ae3773e --- /dev/null +++ b/statediff/indexer/ipfs/ipld/test_data/eth-uncle-json-997522-1 @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"author":"0x68795c4aa09d6f4ed3e5deddf8c2ad3049a601da","difficulty":"0xae22b4c9b9a","extraData":"0xd783010400844765746887676f312e352e31856c696e7578","gasLimit":"0x2fefd8","gasUsed":"0xf618","hash":"0x0324272e484e509c3c9e9e75ad8b48c7d34556e6b269dd72331033fd5cdc1b2a","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x68795c4aa09d6f4ed3e5deddf8c2ad3049a601da","mixHash":"0x0f3bdea5170d6af74b70fcf0df81969f6bb1b740f4a6c78df1d354f172865594","nonce":"0x4c691de262b2b3d9","number":"0xf3890","parentHash":"0xcb9efe9bc3c59be7fb673576d661aff9ca75b1522f58fd38d03d3d49b32bddb3","receiptsRoot":"0x5cf73738487f67f1c0a1c2d1083ae014f38e1aab5eb26a8929a511c48b07ea03","sealFields":["0xa00f3bdea5170d6af74b70fcf0df81969f6bb1b740f4a6c78df1d354f172865594","0x884c691de262b2b3d9"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":null,"stateRoot":"0x968e8d8d099572ac783f4511724ec646f59bb33f7395edf858f98b37c8c3b265","timestamp":"0x56bf10b1","totalDifficulty":"0x6299f4c6290386e7","transactions":[],"transactionsRoot":"0x9cea6a59a5df69111ead7406a431c764b2357120e5b61425388df62f87cbcbc3","uncles":[]},"id":1} diff --git a/statediff/indexer/ipfs/ipld/trie_node.go b/statediff/indexer/ipfs/ipld/trie_node.go new file mode 100644 index 000000000000..a344bab4f860 --- /dev/null +++ b/statediff/indexer/ipfs/ipld/trie_node.go @@ -0,0 +1,456 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipld + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum/go-ethereum/rlp" + "github.com/ipfs/go-cid" + node "github.com/ipfs/go-ipld-format" +) + +const ( + extension = "extension" + leaf = "leaf" + branch = "branch" +) + +// TrieNode is the general abstraction for +//ethereum IPLD trie nodes. +type TrieNode struct { + // leaf, extension or branch + nodeKind string + + // If leaf or extension: [0] is key, [1] is val. + // If branch: [0] - [16] are children. + elements []interface{} + + // IPLD block information + cid cid.Cid + rawdata []byte +} + +/* + OUTPUT +*/ + +type trieNodeLeafDecoder func([]interface{}) ([]interface{}, error) + +// decodeTrieNode returns a TrieNode object from an IPLD block's +// cid and rawdata. +func decodeTrieNode(c cid.Cid, b []byte, + leafDecoder trieNodeLeafDecoder) (*TrieNode, error) { + var ( + i, decoded, elements []interface{} + nodeKind string + err error + ) + + if err = rlp.DecodeBytes(b, &i); err != nil { + return nil, err + } + + codec := c.Type() + switch len(i) { + case 2: + nodeKind, decoded, err = decodeCompactKey(i) + if err != nil { + return nil, err + } + + if nodeKind == extension { + elements, err = parseTrieNodeExtension(decoded, codec) + if err != nil { + return nil, err + } + } + if nodeKind == leaf { + elements, err = leafDecoder(decoded) + if err != nil { + return nil, err + } + } + if nodeKind != extension && nodeKind != leaf { + return nil, fmt.Errorf("unexpected nodeKind returned from decoder") + } + case 17: + nodeKind = branch + elements, err = parseTrieNodeBranch(i, codec) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unknown trie node type") + } + + return &TrieNode{ + nodeKind: nodeKind, + elements: elements, + rawdata: b, + cid: c, + }, nil +} + +// decodeCompactKey takes a compact key, and returns its nodeKind and value. +func decodeCompactKey(i []interface{}) (string, []interface{}, error) { + first := i[0].([]byte) + last := i[1].([]byte) + + switch first[0] / 16 { + case '\x00': + return extension, []interface{}{ + nibbleToByte(first)[2:], + last, + }, nil + case '\x01': + return extension, []interface{}{ + nibbleToByte(first)[1:], + last, + }, nil + case '\x02': + return leaf, []interface{}{ + nibbleToByte(first)[2:], + last, + }, nil + case '\x03': + return leaf, []interface{}{ + nibbleToByte(first)[1:], + last, + }, nil + default: + return "", nil, fmt.Errorf("unknown hex prefix") + } +} + +// parseTrieNodeExtension helper improves readability +func parseTrieNodeExtension(i []interface{}, codec uint64) ([]interface{}, error) { + return []interface{}{ + i[0].([]byte), + keccak256ToCid(codec, i[1].([]byte)), + }, nil +} + +// parseTrieNodeBranch helper improves readability +func parseTrieNodeBranch(i []interface{}, codec uint64) ([]interface{}, error) { + var out []interface{} + + for i, vi := range i { + v, ok := vi.([]byte) + // Sometimes this throws "panic: interface conversion: interface {} is []interface {}, not []uint8" + // Figure out why, and if it is okay to continue + if !ok { + return nil, fmt.Errorf("unable to decode branch node entry into []byte at position: %d value: %+v", i, vi) + } + + switch len(v) { + case 0: + out = append(out, nil) + case 32: + out = append(out, keccak256ToCid(codec, v)) + default: + return nil, fmt.Errorf("unrecognized object: %v", v) + } + } + + return out, nil +} + +/* + Node INTERFACE +*/ + +// Resolve resolves a path through this node, stopping at any link boundary +// and returning the object found as well as the remaining path to traverse +func (t *TrieNode) Resolve(p []string) (interface{}, []string, error) { + switch t.nodeKind { + case extension: + return t.resolveTrieNodeExtension(p) + case leaf: + return t.resolveTrieNodeLeaf(p) + case branch: + return t.resolveTrieNodeBranch(p) + default: + return nil, nil, fmt.Errorf("nodeKind case not implemented") + } +} + +// Tree lists all paths within the object under 'path', and up to the given depth. +// To list the entire object (similar to `find .`) pass "" and -1 +func (t *TrieNode) Tree(p string, depth int) []string { + if p != "" || depth == 0 { + return nil + } + + var out []string + + switch t.nodeKind { + case extension: + var val string + for _, e := range t.elements[0].([]byte) { + val += fmt.Sprintf("%x", e) + } + return []string{val} + case branch: + for i, elem := range t.elements { + if _, ok := elem.(cid.Cid); ok { + out = append(out, fmt.Sprintf("%x", i)) + } + } + return out + + default: + return nil + } +} + +// ResolveLink is a helper function that calls resolve and asserts the +// output is a link +func (t *TrieNode) ResolveLink(p []string) (*node.Link, []string, error) { + obj, rest, err := t.Resolve(p) + if err != nil { + return nil, nil, err + } + + lnk, ok := obj.(*node.Link) + if !ok { + return nil, nil, fmt.Errorf("was not a link") + } + + return lnk, rest, nil +} + +// Copy will go away. It is here to comply with the interface. +func (t *TrieNode) Copy() node.Node { + panic("implement me") +} + +// Links is a helper function that returns all links within this object +func (t *TrieNode) Links() []*node.Link { + var out []*node.Link + + for _, i := range t.elements { + c, ok := i.(cid.Cid) + if ok { + out = append(out, &node.Link{Cid: c}) + } + } + + return out +} + +// Stat will go away. It is here to comply with the interface. +func (t *TrieNode) Stat() (*node.NodeStat, error) { + return &node.NodeStat{}, nil +} + +// Size will go away. It is here to comply with the interface. +func (t *TrieNode) Size() (uint64, error) { + return 0, nil +} + +/* + TrieNode functions +*/ + +// MarshalJSON processes the transaction trie into readable JSON format. +func (t *TrieNode) MarshalJSON() ([]byte, error) { + var out map[string]interface{} + + switch t.nodeKind { + case extension: + fallthrough + case leaf: + var hexPrefix string + for _, e := range t.elements[0].([]byte) { + hexPrefix += fmt.Sprintf("%x", e) + } + + // if we got a byte we need to do this casting otherwise + // it will be marshaled to a base64 encoded value + if _, ok := t.elements[1].([]byte); ok { + var hexVal string + for _, e := range t.elements[1].([]byte) { + hexVal += fmt.Sprintf("%x", e) + } + + t.elements[1] = hexVal + } + + out = map[string]interface{}{ + "type": t.nodeKind, + hexPrefix: t.elements[1], + } + + case branch: + out = map[string]interface{}{ + "type": branch, + "0": t.elements[0], + "1": t.elements[1], + "2": t.elements[2], + "3": t.elements[3], + "4": t.elements[4], + "5": t.elements[5], + "6": t.elements[6], + "7": t.elements[7], + "8": t.elements[8], + "9": t.elements[9], + "a": t.elements[10], + "b": t.elements[11], + "c": t.elements[12], + "d": t.elements[13], + "e": t.elements[14], + "f": t.elements[15], + } + default: + return nil, fmt.Errorf("nodeKind %s not supported", t.nodeKind) + } + + return json.Marshal(out) +} + +// nibbleToByte expands the nibbles of a byte slice into their own bytes. +func nibbleToByte(k []byte) []byte { + var out []byte + + for _, b := range k { + out = append(out, b/16) + out = append(out, b%16) + } + + return out +} + +// Resolve reading conveniences +func (t *TrieNode) resolveTrieNodeExtension(p []string) (interface{}, []string, error) { + nibbles := t.elements[0].([]byte) + idx, rest := shiftFromPath(p, len(nibbles)) + if len(idx) < len(nibbles) { + return nil, nil, fmt.Errorf("not enough nibbles to traverse this extension") + } + + for _, i := range idx { + if getHexIndex(string(i)) == -1 { + return nil, nil, fmt.Errorf("invalid path element") + } + } + + for i, n := range nibbles { + if string(idx[i]) != fmt.Sprintf("%x", n) { + return nil, nil, fmt.Errorf("no such link in this extension") + } + } + + return &node.Link{Cid: t.elements[1].(cid.Cid)}, rest, nil +} + +func (t *TrieNode) resolveTrieNodeLeaf(p []string) (interface{}, []string, error) { + nibbles := t.elements[0].([]byte) + + if len(nibbles) != 0 { + idx, rest := shiftFromPath(p, len(nibbles)) + if len(idx) < len(nibbles) { + return nil, nil, fmt.Errorf("not enough nibbles to traverse this leaf") + } + + for _, i := range idx { + if getHexIndex(string(i)) == -1 { + return nil, nil, fmt.Errorf("invalid path element") + } + } + + for i, n := range nibbles { + if string(idx[i]) != fmt.Sprintf("%x", n) { + return nil, nil, fmt.Errorf("no such link in this extension") + } + } + + p = rest + } + + link, ok := t.elements[1].(node.Node) + if !ok { + return nil, nil, fmt.Errorf("leaf children is not an IPLD node") + } + + return link.Resolve(p) +} + +func (t *TrieNode) resolveTrieNodeBranch(p []string) (interface{}, []string, error) { + idx, rest := shiftFromPath(p, 1) + hidx := getHexIndex(idx) + if hidx == -1 { + return nil, nil, fmt.Errorf("incorrect path") + } + + child := t.elements[hidx] + if child != nil { + return &node.Link{Cid: child.(cid.Cid)}, rest, nil + } + return nil, nil, fmt.Errorf("no such link in this branch") +} + +// shiftFromPath extracts from a given path (as a slice of strings) +// the given number of elements as a single string, returning whatever +// it has not taken. +// +// Examples: +// ["0", "a", "something"] and 1 -> "0" and ["a", "something"] +// ["ab", "c", "d", "1"] and 2 -> "ab" and ["c", "d", "1"] +// ["abc", "d", "1"] and 2 -> "ab" and ["c", "d", "1"] +func shiftFromPath(p []string, i int) (string, []string) { + var ( + out string + rest []string + ) + + for _, pe := range p { + re := "" + for _, c := range pe { + if len(out) < i { + out += string(c) + } else { + re += string(c) + } + } + + if len(out) == i && re != "" { + rest = append(rest, re) + } + } + + return out, rest +} + +// getHexIndex returns to you the integer 0 - 15 equivalent to your +// string character if applicable, or -1 otherwise. +func getHexIndex(s string) int { + if len(s) != 1 { + return -1 + } + + c := s[0] + switch { + case '0' <= c && c <= '9': + return int(c - '0') + case 'a' <= c && c <= 'f': + return int(c - 'a' + 10) + } + + return -1 +} diff --git a/statediff/indexer/ipfs/models.go b/statediff/indexer/ipfs/models.go new file mode 100644 index 000000000000..eb0312beb585 --- /dev/null +++ b/statediff/indexer/ipfs/models.go @@ -0,0 +1,22 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package ipfs + +type BlockModel struct { + CID string `db:"key"` + Data []byte `db:"data"` +} diff --git a/statediff/indexer/metrics.go b/statediff/indexer/metrics.go new file mode 100644 index 000000000000..e1da3919cb49 --- /dev/null +++ b/statediff/indexer/metrics.go @@ -0,0 +1,128 @@ +package indexer + +import ( + "database/sql" + "strings" + + "github.com/ethereum/go-ethereum/metrics" +) + +const ( + namespace = "statediff" +) + +// Build a fully qualified metric name +func metricName(subsystem, name string) string { + if name == "" { + return "" + } + parts := []string{namespace, name} + if subsystem != "" { + parts = []string{namespace, subsystem, name} + } + // Prometheus uses _ but geth metrics uses / and replaces + return strings.Join(parts, "/") +} + +type indexerMetricsHandles struct { + // The total number of processed blocks + blocks metrics.Counter + // The total number of processed transactions + transactions metrics.Counter + // The total number of processed receipts + receipts metrics.Counter + // The total number of access list entries processed + accessListEntries metrics.Counter + // Time spent waiting for free postgres tx + tFreePostgres metrics.Timer + // Postgres transaction commit duration + tPostgresCommit metrics.Timer + // Header processing time + tHeaderProcessing metrics.Timer + // Uncle processing time + tUncleProcessing metrics.Timer + // Tx and receipt processing time + tTxAndRecProcessing metrics.Timer + // State, storage, and code combined processing time + tStateStoreCodeProcessing metrics.Timer +} + +func RegisterIndexerMetrics(reg metrics.Registry) indexerMetricsHandles { + ctx := indexerMetricsHandles{ + blocks: metrics.NewCounter(), + transactions: metrics.NewCounter(), + receipts: metrics.NewCounter(), + accessListEntries: metrics.NewCounter(), + tFreePostgres: metrics.NewTimer(), + tPostgresCommit: metrics.NewTimer(), + tHeaderProcessing: metrics.NewTimer(), + tUncleProcessing: metrics.NewTimer(), + tTxAndRecProcessing: metrics.NewTimer(), + tStateStoreCodeProcessing: metrics.NewTimer(), + } + subsys := "indexer" + reg.Register(metricName(subsys, "blocks"), ctx.blocks) + reg.Register(metricName(subsys, "transactions"), ctx.transactions) + reg.Register(metricName(subsys, "receipts"), ctx.receipts) + reg.Register(metricName(subsys, "access_list_entries"), ctx.accessListEntries) + reg.Register(metricName(subsys, "t_free_postgres"), ctx.tFreePostgres) + reg.Register(metricName(subsys, "t_postgres_commit"), ctx.tPostgresCommit) + reg.Register(metricName(subsys, "t_header_processing"), ctx.tHeaderProcessing) + reg.Register(metricName(subsys, "t_uncle_processing"), ctx.tUncleProcessing) + reg.Register(metricName(subsys, "t_tx_receipt_processing"), ctx.tTxAndRecProcessing) + reg.Register(metricName(subsys, "t_state_store_code_processing"), ctx.tStateStoreCodeProcessing) + return ctx +} + +type dbMetricsHandles struct { + // Maximum number of open connections to the database + maxOpen metrics.Gauge + // The number of established connections both in use and idle + open metrics.Gauge + // The number of connections currently in use + inUse metrics.Gauge + // The number of idle connections + idle metrics.Gauge + // The total number of connections waited for + waitedFor metrics.Counter + // The total time blocked waiting for a new connection + blockedMilliseconds metrics.Counter + // The total number of connections closed due to SetMaxIdleConns + closedMaxIdle metrics.Counter + // The total number of connections closed due to SetConnMaxLifetime + closedMaxLifetime metrics.Counter +} + +func RegisterDBMetrics(reg metrics.Registry) dbMetricsHandles { + ctx := dbMetricsHandles{ + maxOpen: metrics.NewGauge(), + open: metrics.NewGauge(), + inUse: metrics.NewGauge(), + idle: metrics.NewGauge(), + waitedFor: metrics.NewCounter(), + blockedMilliseconds: metrics.NewCounter(), + closedMaxIdle: metrics.NewCounter(), + closedMaxLifetime: metrics.NewCounter(), + } + subsys := "connections" + reg.Register(metricName(subsys, "max_open"), ctx.maxOpen) + reg.Register(metricName(subsys, "open"), ctx.open) + reg.Register(metricName(subsys, "in_use"), ctx.inUse) + reg.Register(metricName(subsys, "idle"), ctx.idle) + reg.Register(metricName(subsys, "waited_for"), ctx.waitedFor) + reg.Register(metricName(subsys, "blocked_milliseconds"), ctx.blockedMilliseconds) + reg.Register(metricName(subsys, "closed_max_idle"), ctx.closedMaxIdle) + reg.Register(metricName(subsys, "closed_max_lifetime"), ctx.closedMaxLifetime) + return ctx +} + +func (met *dbMetricsHandles) Update(stats sql.DBStats) { + met.maxOpen.Update(int64(stats.MaxOpenConnections)) + met.open.Update(int64(stats.OpenConnections)) + met.inUse.Update(int64(stats.InUse)) + met.idle.Update(int64(stats.Idle)) + met.waitedFor.Inc(stats.WaitCount) + met.blockedMilliseconds.Inc(stats.WaitDuration.Milliseconds()) + met.closedMaxIdle.Inc(stats.MaxIdleClosed) + met.closedMaxLifetime.Inc(stats.MaxLifetimeClosed) +} diff --git a/statediff/indexer/mocks/test_data.go b/statediff/indexer/mocks/test_data.go new file mode 100644 index 000000000000..d0b693fba1d3 --- /dev/null +++ b/statediff/indexer/mocks/test_data.go @@ -0,0 +1,249 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package mocks + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "math/big" + + "github.com/ethereum/go-ethereum/statediff/indexer/models" + + "github.com/ethereum/go-ethereum/trie" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/statediff/testhelpers" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" +) + +// Test variables +var ( + // block data + BlockNumber = big.NewInt(12244001) + MockHeader = types.Header{ + Time: 0, + Number: new(big.Int).Set(BlockNumber), + Root: common.HexToHash("0x0"), + TxHash: common.HexToHash("0x0"), + ReceiptHash: common.HexToHash("0x0"), + Difficulty: big.NewInt(5000000), + Extra: []byte{}, + } + MockTransactions, MockReceipts, SenderAddr = createTransactionsAndReceipts() + MockBlock = types.NewBlock(&MockHeader, MockTransactions, nil, MockReceipts, new(trie.Trie)) + MockHeaderRlp, _ = rlp.EncodeToBytes(MockBlock.Header()) + Address = common.HexToAddress("0xaE9BEa628c4Ce503DcFD7E305CaB4e29E7476592") + AnotherAddress = common.HexToAddress("0xaE9BEa628c4Ce503DcFD7E305CaB4e29E7476593") + ContractAddress = crypto.CreateAddress(SenderAddr, MockTransactions[2].Nonce()) + MockContractByteCode = []byte{0, 1, 2, 3, 4, 5} + mockTopic11 = common.HexToHash("0x04") + mockTopic12 = common.HexToHash("0x06") + mockTopic21 = common.HexToHash("0x05") + mockTopic22 = common.HexToHash("0x07") + ExpectedPostStatus uint64 = 1 + ExpectedPostState1 = common.Bytes2Hex(common.HexToHash("0x1").Bytes()) + ExpectedPostState2 = common.Bytes2Hex(common.HexToHash("0x2").Bytes()) + ExpectedPostState3 = common.Bytes2Hex(common.HexToHash("0x3").Bytes()) + MockLog1 = &types.Log{ + Address: Address, + Topics: []common.Hash{mockTopic11, mockTopic12}, + Data: []byte{}, + } + MockLog2 = &types.Log{ + Address: AnotherAddress, + Topics: []common.Hash{mockTopic21, mockTopic22}, + Data: []byte{}, + } + + // access list entries + AccessListEntry1 = types.AccessTuple{ + Address: Address, + } + AccessListEntry2 = types.AccessTuple{ + Address: AnotherAddress, + StorageKeys: []common.Hash{common.BytesToHash(StorageLeafKey), common.BytesToHash(MockStorageLeafKey)}, + } + AccessListEntry1Model = models.AccessListElementModel{ + Index: 0, + Address: Address.Hex(), + } + AccessListEntry2Model = models.AccessListElementModel{ + Index: 1, + Address: AnotherAddress.Hex(), + StorageKeys: []string{common.BytesToHash(StorageLeafKey).Hex(), common.BytesToHash(MockStorageLeafKey).Hex()}, + } + + // statediff data + storageLocation = common.HexToHash("0") + StorageLeafKey = crypto.Keccak256Hash(storageLocation[:]).Bytes() + mockStorageLocation = common.HexToHash("1") + MockStorageLeafKey = crypto.Keccak256Hash(mockStorageLocation[:]).Bytes() + StorageValue = common.Hex2Bytes("01") + StoragePartialPath = common.Hex2Bytes("20290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563") + StorageLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + StoragePartialPath, + StorageValue, + }) + + nonce1 = uint64(1) + ContractRoot = "0x821e2556a290c86405f8160a2d662042a431ba456b9db265c79bb837c04be5f0" + ContractCodeHash = common.HexToHash("0x753f98a8d4328b15636e46f66f2cb4bc860100aa17967cc145fcd17d1d4710ea") + ContractLeafKey = testhelpers.AddressToLeafKey(ContractAddress) + ContractAccount, _ = rlp.EncodeToBytes(state.Account{ + Nonce: nonce1, + Balance: big.NewInt(0), + CodeHash: ContractCodeHash.Bytes(), + Root: common.HexToHash(ContractRoot), + }) + ContractPartialPath = common.Hex2Bytes("3114658a74d9cc9f7acf2c5cd696c3494d7c344d78bfec3add0d91ec4e8d1c45") + ContractLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + ContractPartialPath, + ContractAccount, + }) + + nonce0 = uint64(0) + AccountRoot = "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" + AccountCodeHash = common.HexToHash("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + AccountLeafKey = testhelpers.Account2LeafKey + Account, _ = rlp.EncodeToBytes(state.Account{ + Nonce: nonce0, + Balance: big.NewInt(1000), + CodeHash: AccountCodeHash.Bytes(), + Root: common.HexToHash(AccountRoot), + }) + AccountPartialPath = common.Hex2Bytes("3957f3e2f04a0764c3a0491b175f69926da61efbcc8f61fa1455fd2d2b4cdd45") + AccountLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + AccountPartialPath, + Account, + }) + + StateDiffs = []sdtypes.StateNode{ + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Leaf, + LeafKey: ContractLeafKey, + NodeValue: ContractLeafNode, + StorageNodes: []sdtypes.StorageNode{ + { + Path: []byte{}, + NodeType: sdtypes.Leaf, + LeafKey: StorageLeafKey, + NodeValue: StorageLeafNode, + }, + }, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Leaf, + LeafKey: AccountLeafKey, + NodeValue: AccountLeafNode, + StorageNodes: []sdtypes.StorageNode{}, + }, + } +) + +/* +// AccessListTx is the data of EIP-2930 access list transactions. +type AccessListTx struct { + ChainID *big.Int // destination chain ID + Nonce uint64 // nonce of sender account + GasPrice *big.Int // wei per gas + Gas uint64 // gas limit + To *common.Address `rlp:"nil"` // nil means contract creation + Value *big.Int // wei amount + Data []byte // contract invocation input data + AccessList AccessList // EIP-2930 access list + V, R, S *big.Int // signature values +} + +*/ + +// createTransactionsAndReceipts is a helper function to generate signed mock transactions and mock receipts with mock logs +func createTransactionsAndReceipts() (types.Transactions, types.Receipts, common.Address) { + // make transactions + trx1 := types.NewTransaction(0, Address, big.NewInt(1000), 50, big.NewInt(100), []byte{}) + trx2 := types.NewTransaction(1, AnotherAddress, big.NewInt(2000), 100, big.NewInt(200), []byte{}) + trx3 := types.NewContractCreation(2, big.NewInt(1500), 75, big.NewInt(150), MockContractByteCode) + trx4 := types.NewTx(&types.AccessListTx{ + ChainID: big.NewInt(1), + Nonce: 0, + GasPrice: big.NewInt(100), + Gas: 50, + To: &AnotherAddress, + Value: big.NewInt(1000), + Data: []byte{}, + AccessList: types.AccessList{ + AccessListEntry1, + AccessListEntry2, + }, + }) + + transactionSigner := types.NewEIP2930Signer(params.MainnetChainConfig.ChainID) + mockCurve := elliptic.P256() + mockPrvKey, err := ecdsa.GenerateKey(mockCurve, rand.Reader) + if err != nil { + log.Crit(err.Error()) + } + signedTrx1, err := types.SignTx(trx1, transactionSigner, mockPrvKey) + if err != nil { + log.Crit(err.Error()) + } + signedTrx2, err := types.SignTx(trx2, transactionSigner, mockPrvKey) + if err != nil { + log.Crit(err.Error()) + } + signedTrx3, err := types.SignTx(trx3, transactionSigner, mockPrvKey) + if err != nil { + log.Crit(err.Error()) + } + signedTrx4, err := types.SignTx(trx4, transactionSigner, mockPrvKey) + if err != nil { + println(err.Error()) + log.Crit(err.Error()) + } + senderAddr, err := types.Sender(transactionSigner, signedTrx1) // same for both trx + if err != nil { + log.Crit(err.Error()) + } + // make receipts + mockReceipt1 := types.NewReceipt(nil, false, 50) + mockReceipt1.Logs = []*types.Log{MockLog1} + mockReceipt1.TxHash = signedTrx1.Hash() + mockReceipt2 := types.NewReceipt(common.HexToHash("0x1").Bytes(), false, 100) + mockReceipt2.Logs = []*types.Log{MockLog2} + mockReceipt2.TxHash = signedTrx2.Hash() + mockReceipt3 := types.NewReceipt(common.HexToHash("0x2").Bytes(), false, 75) + mockReceipt3.Logs = []*types.Log{} + mockReceipt3.TxHash = signedTrx3.Hash() + mockReceipt4 := &types.Receipt{ + Type: types.AccessListTxType, + PostState: common.HexToHash("0x3").Bytes(), + Status: types.ReceiptStatusSuccessful, + CumulativeGasUsed: 175, + Logs: []*types.Log{}, + TxHash: signedTrx4.Hash(), + } + + return types.Transactions{signedTrx1, signedTrx2, signedTrx3, signedTrx4}, types.Receipts{mockReceipt1, mockReceipt2, mockReceipt3, mockReceipt4}, senderAddr +} diff --git a/statediff/indexer/models/models.go b/statediff/indexer/models/models.go new file mode 100644 index 000000000000..604cf6b624c6 --- /dev/null +++ b/statediff/indexer/models/models.go @@ -0,0 +1,137 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package models + +import "github.com/lib/pq" + +// HeaderModel is the db model for eth.header_cids +type HeaderModel struct { + ID int64 `db:"id"` + BlockNumber string `db:"block_number"` + BlockHash string `db:"block_hash"` + ParentHash string `db:"parent_hash"` + CID string `db:"cid"` + MhKey string `db:"mh_key"` + TotalDifficulty string `db:"td"` + NodeID int64 `db:"node_id"` + Reward string `db:"reward"` + StateRoot string `db:"state_root"` + UncleRoot string `db:"uncle_root"` + TxRoot string `db:"tx_root"` + RctRoot string `db:"receipt_root"` + Bloom []byte `db:"bloom"` + Timestamp uint64 `db:"timestamp"` + TimesValidated int64 `db:"times_validated"` +} + +// UncleModel is the db model for eth.uncle_cids +type UncleModel struct { + ID int64 `db:"id"` + HeaderID int64 `db:"header_id"` + BlockHash string `db:"block_hash"` + ParentHash string `db:"parent_hash"` + CID string `db:"cid"` + MhKey string `db:"mh_key"` + Reward string `db:"reward"` +} + +// TxModel is the db model for eth.transaction_cids +type TxModel struct { + ID int64 `db:"id"` + HeaderID int64 `db:"header_id"` + Index int64 `db:"index"` + TxHash string `db:"tx_hash"` + CID string `db:"cid"` + MhKey string `db:"mh_key"` + Dst string `db:"dst"` + Src string `db:"src"` + Data []byte `db:"tx_data"` + Type *uint8 `db:"tx_type"` +} + +// AccessListEntryModel is the db model for eth.access_list_entry +type AccessListElementModel struct { + ID int64 `db:"id"` + Index int64 `db:"index"` + TxID int64 `db:"tx_id"` + Address string `db:"address"` + StorageKeys pq.StringArray `db:"storage_keys"` +} + +// ReceiptModel is the db model for eth.receipt_cids +type ReceiptModel struct { + ID int64 `db:"id"` + TxID int64 `db:"tx_id"` + CID string `db:"cid"` + MhKey string `db:"mh_key"` + PostStatus uint64 `db:"post_status"` + PostState string `db:"post_state"` + Contract string `db:"contract"` + ContractHash string `db:"contract_hash"` + LogContracts pq.StringArray `db:"log_contracts"` + Topic0s pq.StringArray `db:"topic0s"` + Topic1s pq.StringArray `db:"topic1s"` + Topic2s pq.StringArray `db:"topic2s"` + Topic3s pq.StringArray `db:"topic3s"` +} + +// StateNodeModel is the db model for eth.state_cids +type StateNodeModel struct { + ID int64 `db:"id"` + HeaderID int64 `db:"header_id"` + Path []byte `db:"state_path"` + StateKey string `db:"state_leaf_key"` + NodeType int `db:"node_type"` + CID string `db:"cid"` + MhKey string `db:"mh_key"` + Diff bool `db:"diff"` +} + +// StorageNodeModel is the db model for eth.storage_cids +type StorageNodeModel struct { + ID int64 `db:"id"` + StateID int64 `db:"state_id"` + Path []byte `db:"storage_path"` + StorageKey string `db:"storage_leaf_key"` + NodeType int `db:"node_type"` + CID string `db:"cid"` + MhKey string `db:"mh_key"` + Diff bool `db:"diff"` +} + +// StorageNodeWithStateKeyModel is a db model for eth.storage_cids + eth.state_cids.state_key +type StorageNodeWithStateKeyModel struct { + ID int64 `db:"id"` + StateID int64 `db:"state_id"` + Path []byte `db:"storage_path"` + StateKey string `db:"state_leaf_key"` + StorageKey string `db:"storage_leaf_key"` + NodeType int `db:"node_type"` + CID string `db:"cid"` + MhKey string `db:"mh_key"` + Diff bool `db:"diff"` +} + +// StateAccountModel is a db model for an eth state account (decoded value of state leaf node) +type StateAccountModel struct { + ID int64 `db:"id"` + StateID int64 `db:"state_id"` + Balance string `db:"balance"` + Nonce uint64 `db:"nonce"` + CodeHash []byte `db:"code_hash"` + StorageRoot string `db:"storage_root"` +} diff --git a/statediff/indexer/node/node.go b/statediff/indexer/node/node.go new file mode 100644 index 000000000000..527546efa277 --- /dev/null +++ b/statediff/indexer/node/node.go @@ -0,0 +1,25 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package node + +type Info struct { + GenesisBlock string + NetworkID string + ChainID uint64 + ID string + ClientName string +} diff --git a/statediff/indexer/postgres/config.go b/statediff/indexer/postgres/config.go new file mode 100644 index 000000000000..c2de0a6bfcc9 --- /dev/null +++ b/statediff/indexer/postgres/config.go @@ -0,0 +1,59 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package postgres + +import ( + "fmt" +) + +// Env variables +const ( + DATABASE_NAME = "DATABASE_NAME" + DATABASE_HOSTNAME = "DATABASE_HOSTNAME" + DATABASE_PORT = "DATABASE_PORT" + DATABASE_USER = "DATABASE_USER" + DATABASE_PASSWORD = "DATABASE_PASSWORD" + DATABASE_MAX_IDLE_CONNECTIONS = "DATABASE_MAX_IDLE_CONNECTIONS" + DATABASE_MAX_OPEN_CONNECTIONS = "DATABASE_MAX_OPEN_CONNECTIONS" + DATABASE_MAX_CONN_LIFETIME = "DATABASE_MAX_CONN_LIFETIME" +) + +type ConnectionParams struct { + Hostname string + Name string + User string + Password string + Port int +} + +type ConnectionConfig struct { + MaxIdle int + MaxOpen int + MaxLifetime int +} + +func DbConnectionString(params ConnectionParams) string { + if len(params.User) > 0 && len(params.Password) > 0 { + return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=disable", + params.User, params.Password, params.Hostname, params.Port, params.Name) + } + if len(params.User) > 0 && len(params.Password) == 0 { + return fmt.Sprintf("postgresql://%s@%s:%d/%s?sslmode=disable", + params.User, params.Hostname, params.Port, params.Name) + } + return fmt.Sprintf("postgresql://%s:%d/%s?sslmode=disable", params.Hostname, params.Port, params.Name) +} diff --git a/statediff/indexer/postgres/errors.go b/statediff/indexer/postgres/errors.go new file mode 100644 index 000000000000..effa74aa125e --- /dev/null +++ b/statediff/indexer/postgres/errors.go @@ -0,0 +1,38 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package postgres + +import ( + "fmt" +) + +const ( + DbConnectionFailedMsg = "db connection failed" + SettingNodeFailedMsg = "unable to set db node" +) + +func ErrDBConnectionFailed(connectErr error) error { + return formatError(DbConnectionFailedMsg, connectErr.Error()) +} + +func ErrUnableToSetNode(setErr error) error { + return formatError(SettingNodeFailedMsg, setErr.Error()) +} + +func formatError(msg, err string) error { + return fmt.Errorf("%s: %s", msg, err) +} diff --git a/statediff/indexer/postgres/postgres.go b/statediff/indexer/postgres/postgres.go new file mode 100644 index 000000000000..455dac306778 --- /dev/null +++ b/statediff/indexer/postgres/postgres.go @@ -0,0 +1,76 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package postgres + +import ( + "time" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" //postgres driver + + "github.com/ethereum/go-ethereum/statediff/indexer/node" +) + +type DB struct { + *sqlx.DB + Node node.Info + NodeID int64 +} + +func NewDB(connectString string, config ConnectionConfig, node node.Info) (*DB, error) { + db, connectErr := sqlx.Connect("postgres", connectString) + if connectErr != nil { + return &DB{}, ErrDBConnectionFailed(connectErr) + } + if config.MaxOpen > 0 { + db.SetMaxOpenConns(config.MaxOpen) + } + if config.MaxIdle > 0 { + db.SetMaxIdleConns(config.MaxIdle) + } + if config.MaxLifetime > 0 { + lifetime := time.Duration(config.MaxLifetime) * time.Second + db.SetConnMaxLifetime(lifetime) + } + pg := DB{DB: db, Node: node} + nodeErr := pg.CreateNode(&node) + if nodeErr != nil { + return &DB{}, ErrUnableToSetNode(nodeErr) + } + return &pg, nil +} + +func (db *DB) CreateNode(node *node.Info) error { + var nodeID int64 + err := db.QueryRow( + `INSERT INTO nodes (genesis_block, network_id, node_id, client_name, chain_id) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (genesis_block, network_id, node_id, chain_id) + DO UPDATE + SET genesis_block = $1, + network_id = $2, + node_id = $3, + client_name = $4, + chain_id = $5 + RETURNING id`, + node.GenesisBlock, node.NetworkID, node.ID, node.ClientName, node.ChainID).Scan(&nodeID) + if err != nil { + return ErrUnableToSetNode(err) + } + db.NodeID = nodeID + return nil +} diff --git a/statediff/indexer/postgres/postgres_suite_test.go b/statediff/indexer/postgres/postgres_suite_test.go new file mode 100644 index 000000000000..a020e088e103 --- /dev/null +++ b/statediff/indexer/postgres/postgres_suite_test.go @@ -0,0 +1,33 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package postgres_test + +import ( + "fmt" + "os" + + "github.com/ethereum/go-ethereum/log" +) + +func init() { + if os.Getenv("MODE") != "statediff" { + fmt.Println("Skipping statediff test") + os.Exit(0) + } + + log.Root().SetHandler(log.DiscardHandler()) +} diff --git a/statediff/indexer/postgres/postgres_test.go b/statediff/indexer/postgres/postgres_test.go new file mode 100644 index 000000000000..a7bf123d98d8 --- /dev/null +++ b/statediff/indexer/postgres/postgres_test.go @@ -0,0 +1,136 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package postgres_test + +import ( + "fmt" + "math/big" + "strings" + "testing" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + + "github.com/ethereum/go-ethereum/statediff/indexer/node" + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" + "github.com/ethereum/go-ethereum/statediff/indexer/shared" +) + +var DBParams = postgres.ConnectionParams{ + Name: "vulcanize_testing", + Password: "password", + Port: 5432, + Hostname: "localhost", + User: "postgres", +} + +func expectContainsSubstring(t *testing.T, full string, sub string) { + if !strings.Contains(full, sub) { + t.Fatalf("Expected \"%v\" to contain substring \"%v\"\n", full, sub) + } +} + +func TestPostgresDB(t *testing.T) { + var sqlxdb *sqlx.DB + + t.Run("connects to the database", func(t *testing.T) { + var err error + pgConfig := postgres.DbConnectionString(DBParams) + + sqlxdb, err = sqlx.Connect("postgres", pgConfig) + + if err != nil { + t.Fatalf("failed to connect to db with connection string: %s err: %v", pgConfig, err) + } + if sqlxdb == nil { + t.Fatal("DB is nil") + } + }) + + t.Run("serializes big.Int to db", func(t *testing.T) { + // postgres driver doesn't support go big.Int type + // various casts in golang uint64, int64, overflow for + // transaction value (in wei) even though + // postgres numeric can handle an arbitrary + // sized int, so use string representation of big.Int + // and cast on insert + + pgConnectString := postgres.DbConnectionString(DBParams) + db, err := sqlx.Connect("postgres", pgConnectString) + if err != nil { + t.Fatal(err) + } + if err != nil { + t.Fatal(err) + } + + bi := new(big.Int) + bi.SetString("34940183920000000000", 10) + shared.ExpectEqual(t, bi.String(), "34940183920000000000") + + defer db.Exec(`DROP TABLE IF EXISTS example`) + _, err = db.Exec("CREATE TABLE example ( id INTEGER, data NUMERIC )") + if err != nil { + t.Fatal(err) + } + + sqlStatement := ` + INSERT INTO example (id, data) + VALUES (1, cast($1 AS NUMERIC))` + _, err = db.Exec(sqlStatement, bi.String()) + if err != nil { + t.Fatal(err) + } + + var data string + err = db.QueryRow(`SELECT data FROM example WHERE id = 1`).Scan(&data) + if err != nil { + t.Fatal(err) + } + + shared.ExpectEqual(t, bi.String(), data) + actual := new(big.Int) + actual.SetString(data, 10) + shared.ExpectEqual(t, actual, bi) + }) + + t.Run("throws error when can't connect to the database", func(t *testing.T) { + invalidDatabase := postgres.ConnectionParams{} + node := node.Info{GenesisBlock: "GENESIS", NetworkID: "1", ID: "x123", ClientName: "geth"} + + _, err := postgres.NewDB(postgres.DbConnectionString(invalidDatabase), + postgres.ConnectionConfig{}, node) + + if err == nil { + t.Fatal("Expected an error") + } + + expectContainsSubstring(t, err.Error(), postgres.DbConnectionFailedMsg) + }) + + t.Run("throws error when can't create node", func(t *testing.T) { + badHash := fmt.Sprintf("x %s", strings.Repeat("1", 100)) + node := node.Info{GenesisBlock: badHash, NetworkID: "1", ID: "x123", ClientName: "geth"} + + _, err := postgres.NewDB(postgres.DbConnectionString(DBParams), postgres.ConnectionConfig{}, node) + + if err == nil { + t.Fatal("Expected an error") + } + expectContainsSubstring(t, err.Error(), postgres.SettingNodeFailedMsg) + }) +} diff --git a/statediff/indexer/reward.go b/statediff/indexer/reward.go new file mode 100644 index 000000000000..47e3f17b9151 --- /dev/null +++ b/statediff/indexer/reward.go @@ -0,0 +1,76 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package indexer + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/core/types" +) + +func CalcEthBlockReward(header *types.Header, uncles []*types.Header, txs types.Transactions, receipts types.Receipts) *big.Int { + staticBlockReward := staticRewardByBlockNumber(header.Number.Uint64()) + transactionFees := calcEthTransactionFees(txs, receipts) + uncleInclusionRewards := calcEthUncleInclusionRewards(header, uncles) + tmp := transactionFees.Add(transactionFees, uncleInclusionRewards) + return tmp.Add(tmp, staticBlockReward) +} + +func CalcUncleMinerReward(blockNumber, uncleBlockNumber uint64) *big.Int { + staticBlockReward := staticRewardByBlockNumber(blockNumber) + rewardDiv8 := staticBlockReward.Div(staticBlockReward, big.NewInt(8)) + mainBlock := new(big.Int).SetUint64(blockNumber) + uncleBlock := new(big.Int).SetUint64(uncleBlockNumber) + uncleBlockPlus8 := uncleBlock.Add(uncleBlock, big.NewInt(8)) + uncleBlockPlus8MinusMainBlock := uncleBlockPlus8.Sub(uncleBlockPlus8, mainBlock) + return rewardDiv8.Mul(rewardDiv8, uncleBlockPlus8MinusMainBlock) +} + +func staticRewardByBlockNumber(blockNumber uint64) *big.Int { + staticBlockReward := new(big.Int) + //https://blog.ethereum.org/2017/10/12/byzantium-hf-announcement/ + if blockNumber >= 7280000 { + staticBlockReward.SetString("2000000000000000000", 10) + } else if blockNumber >= 4370000 { + staticBlockReward.SetString("3000000000000000000", 10) + } else { + staticBlockReward.SetString("5000000000000000000", 10) + } + return staticBlockReward +} + +func calcEthTransactionFees(txs types.Transactions, receipts types.Receipts) *big.Int { + transactionFees := new(big.Int) + for i, transaction := range txs { + receipt := receipts[i] + gasPrice := big.NewInt(transaction.GasPrice().Int64()) + gasUsed := big.NewInt(int64(receipt.GasUsed)) + transactionFee := gasPrice.Mul(gasPrice, gasUsed) + transactionFees = transactionFees.Add(transactionFees, transactionFee) + } + return transactionFees +} + +func calcEthUncleInclusionRewards(header *types.Header, uncles []*types.Header) *big.Int { + uncleInclusionRewards := new(big.Int) + for range uncles { + staticBlockReward := staticRewardByBlockNumber(header.Number.Uint64()) + staticBlockReward.Div(staticBlockReward, big.NewInt(32)) + uncleInclusionRewards.Add(uncleInclusionRewards, staticBlockReward) + } + return uncleInclusionRewards +} diff --git a/statediff/indexer/shared/chain_type.go b/statediff/indexer/shared/chain_type.go new file mode 100644 index 000000000000..c3dedfe38e75 --- /dev/null +++ b/statediff/indexer/shared/chain_type.go @@ -0,0 +1,78 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +import ( + "errors" + "strings" +) + +// ChainType enum for specifying blockchain +type ChainType int + +const ( + UnknownChain ChainType = iota + Ethereum + Bitcoin + Omni + EthereumClassic +) + +func (c ChainType) String() string { + switch c { + case Ethereum: + return "Ethereum" + case Bitcoin: + return "Bitcoin" + case Omni: + return "Omni" + case EthereumClassic: + return "EthereumClassic" + default: + return "" + } +} + +func (c ChainType) API() string { + switch c { + case Ethereum: + return "eth" + case Bitcoin: + return "btc" + case Omni: + return "omni" + case EthereumClassic: + return "etc" + default: + return "" + } +} + +func NewChainType(name string) (ChainType, error) { + switch strings.ToLower(name) { + case "ethereum", "eth": + return Ethereum, nil + case "bitcoin", "btc", "xbt": + return Bitcoin, nil + case "omni": + return Omni, nil + case "classic", "etc": + return EthereumClassic, nil + default: + return UnknownChain, errors.New("invalid name for chain") + } +} diff --git a/statediff/indexer/shared/constants.go b/statediff/indexer/shared/constants.go new file mode 100644 index 000000000000..3dc2994c4216 --- /dev/null +++ b/statediff/indexer/shared/constants.go @@ -0,0 +1,22 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +const ( + DefaultMaxBatchSize uint64 = 100 + DefaultMaxBatchNumber int64 = 50 +) diff --git a/statediff/indexer/shared/data_type.go b/statediff/indexer/shared/data_type.go new file mode 100644 index 000000000000..01fed57f76af --- /dev/null +++ b/statediff/indexer/shared/data_type.go @@ -0,0 +1,101 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +import ( + "fmt" + "strings" +) + +// DataType is an enum to loosely represent type of chain data +type DataType int + +const ( + UnknownDataType DataType = iota - 1 + Full + Headers + Uncles + Transactions + Receipts + State + Storage +) + +// String() method to resolve ReSyncType enum +func (r DataType) String() string { + switch r { + case Full: + return "full" + case Headers: + return "headers" + case Uncles: + return "uncles" + case Transactions: + return "transactions" + case Receipts: + return "receipts" + case State: + return "state" + case Storage: + return "storage" + default: + return "unknown" + } +} + +// GenerateDataTypeFromString +func GenerateDataTypeFromString(str string) (DataType, error) { + switch strings.ToLower(str) { + case "full", "f": + return Full, nil + case "headers", "header", "h": + return Headers, nil + case "uncles", "u": + return Uncles, nil + case "transactions", "transaction", "trxs", "txs", "trx", "tx", "t": + return Transactions, nil + case "receipts", "receipt", "rcts", "rct", "r": + return Receipts, nil + case "state": + return State, nil + case "storage": + return Storage, nil + default: + return UnknownDataType, fmt.Errorf("unrecognized resync type: %s", str) + } +} + +func SupportedDataType(d DataType) (bool, error) { + switch d { + case Full: + return true, nil + case Headers: + return true, nil + case Uncles: + return true, nil + case Transactions: + return true, nil + case Receipts: + return true, nil + case State: + return true, nil + case Storage: + return true, nil + default: + return true, nil + } +} diff --git a/statediff/indexer/shared/functions.go b/statediff/indexer/shared/functions.go new file mode 100644 index 000000000000..92d5e6f2fae1 --- /dev/null +++ b/statediff/indexer/shared/functions.go @@ -0,0 +1,124 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/statediff/indexer/ipfs/ipld" + + "github.com/ipfs/go-cid" + blockstore "github.com/ipfs/go-ipfs-blockstore" + dshelp "github.com/ipfs/go-ipfs-ds-help" + format "github.com/ipfs/go-ipld-format" + "github.com/jmoiron/sqlx" + "github.com/multiformats/go-multihash" +) + +// HandleZeroAddrPointer will return an empty string for a nil address pointer +func HandleZeroAddrPointer(to *common.Address) string { + if to == nil { + return "" + } + return to.Hex() +} + +// HandleZeroAddr will return an empty string for a 0 value address +func HandleZeroAddr(to common.Address) string { + if to.Hex() == "0x0000000000000000000000000000000000000000" { + return "" + } + return to.Hex() +} + +// Rollback sql transaction and log any error +func Rollback(tx *sqlx.Tx) { + if err := tx.Rollback(); err != nil { + log.Error(err.Error()) + } +} + +// PublishIPLD is used to insert an IPLD into Postgres blockstore with the provided tx +func PublishIPLD(tx *sqlx.Tx, i format.Node) error { + dbKey := dshelp.MultihashToDsKey(i.Cid().Hash()) + prefixedKey := blockstore.BlockPrefix.String() + dbKey.String() + raw := i.RawData() + _, err := tx.Exec(`INSERT INTO public.blocks (key, data) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING`, prefixedKey, raw) + return err +} + +// FetchIPLD is used to retrieve an ipld from Postgres blockstore with the provided tx and cid string +func FetchIPLD(tx *sqlx.Tx, cid string) ([]byte, error) { + mhKey, err := MultihashKeyFromCIDString(cid) + if err != nil { + return nil, err + } + pgStr := `SELECT data FROM public.blocks WHERE key = $1` + var block []byte + return block, tx.Get(&block, pgStr, mhKey) +} + +// FetchIPLDByMhKey is used to retrieve an ipld from Postgres blockstore with the provided tx and mhkey string +func FetchIPLDByMhKey(tx *sqlx.Tx, mhKey string) ([]byte, error) { + pgStr := `SELECT data FROM public.blocks WHERE key = $1` + var block []byte + return block, tx.Get(&block, pgStr, mhKey) +} + +// MultihashKeyFromCID converts a cid into a blockstore-prefixed multihash db key string +func MultihashKeyFromCID(c cid.Cid) string { + dbKey := dshelp.MultihashToDsKey(c.Hash()) + return blockstore.BlockPrefix.String() + dbKey.String() +} + +// MultihashKeyFromCIDString converts a cid string into a blockstore-prefixed multihash db key string +func MultihashKeyFromCIDString(c string) (string, error) { + dc, err := cid.Decode(c) + if err != nil { + return "", err + } + dbKey := dshelp.MultihashToDsKey(dc.Hash()) + return blockstore.BlockPrefix.String() + dbKey.String(), nil +} + +// PublishRaw derives a cid from raw bytes and provided codec and multihash type, and writes it to the db tx +func PublishRaw(tx *sqlx.Tx, codec, mh uint64, raw []byte) (string, error) { + c, err := ipld.RawdataToCid(codec, raw, mh) + if err != nil { + return "", err + } + dbKey := dshelp.MultihashToDsKey(c.Hash()) + prefixedKey := blockstore.BlockPrefix.String() + dbKey.String() + _, err = tx.Exec(`INSERT INTO public.blocks (key, data) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING`, prefixedKey, raw) + return c.String(), err +} + +// MultihashKeyFromKeccak256 converts keccak256 hash bytes into a blockstore-prefixed multihash db key string +func MultihashKeyFromKeccak256(hash common.Hash) (string, error) { + mh, err := multihash.Encode(hash.Bytes(), multihash.KECCAK_256) + if err != nil { + return "", err + } + dbKey := dshelp.MultihashToDsKey(mh) + return blockstore.BlockPrefix.String() + dbKey.String(), nil +} + +// PublishDirect diretly writes a previously derived mhkey => value pair to the ipld database +func PublishDirect(tx *sqlx.Tx, key string, value []byte) error { + _, err := tx.Exec(`INSERT INTO public.blocks (key, data) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING`, key, value) + return err +} diff --git a/statediff/indexer/shared/test_helpers.go b/statediff/indexer/shared/test_helpers.go new file mode 100644 index 000000000000..eb23032b5fe7 --- /dev/null +++ b/statediff/indexer/shared/test_helpers.go @@ -0,0 +1,68 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +import ( + "reflect" + "testing" + + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" + + "github.com/ethereum/go-ethereum/statediff/indexer/node" + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" +) + +func ExpectEqual(t *testing.T, got interface{}, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Fatalf("Expected: %v\nActual: %v", want, got) + } +} + +// SetupDB is use to setup a db for watcher tests +func SetupDB() (*postgres.DB, error) { + uri := postgres.DbConnectionString(postgres.ConnectionParams{ + User: "postgres", + Password: "password", + Hostname: "localhost", + Name: "vulcanize_testing", + Port: 5432, + }) + return postgres.NewDB(uri, postgres.ConnectionConfig{}, node.Info{}) +} + +// ListContainsString used to check if a list of strings contains a particular string +func ListContainsString(sss []string, s string) bool { + for _, str := range sss { + if s == str { + return true + } + } + return false +} + +// TestCID creates a basic CID for testing purposes +func TestCID(b []byte) cid.Cid { + pref := cid.Prefix{ + Version: 1, + Codec: cid.Raw, + MhType: multihash.KECCAK_256, + MhLength: -1, + } + c, _ := pref.Sum(b) + return c +} diff --git a/statediff/indexer/shared/types.go b/statediff/indexer/shared/types.go new file mode 100644 index 000000000000..544d4e07e06e --- /dev/null +++ b/statediff/indexer/shared/types.go @@ -0,0 +1,44 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package shared + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/statediff/indexer/models" + "github.com/ethereum/go-ethereum/statediff/types" +) + +// Trie struct used to flag node as leaf or not +type TrieNode struct { + Path []byte + LeafKey common.Hash + Value []byte + Type types.NodeType +} + +// CIDPayload is a struct to hold all the CIDs and their associated meta data for indexing in Postgres +// Returned by IPLDPublisher +// Passed to CIDIndexer +type CIDPayload struct { + HeaderCID models.HeaderModel + UncleCIDs []models.UncleModel + TransactionCIDs []models.TxModel + ReceiptCIDs map[common.Hash]models.ReceiptModel + StateNodeCIDs []models.StateNodeModel + StateAccounts map[string]models.StateAccountModel + StorageNodeCIDs map[string][]models.StorageNodeModel +} diff --git a/statediff/indexer/test_helpers.go b/statediff/indexer/test_helpers.go new file mode 100644 index 000000000000..024bb58f01ae --- /dev/null +++ b/statediff/indexer/test_helpers.go @@ -0,0 +1,60 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package indexer + +import ( + "testing" + + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" +) + +// TearDownDB is used to tear down the watcher dbs after tests +func TearDownDB(t *testing.T, db *postgres.DB) { + tx, err := db.Beginx() + if err != nil { + t.Fatal(err) + } + + _, err = tx.Exec(`DELETE FROM eth.header_cids`) + if err != nil { + t.Fatal(err) + } + _, err = tx.Exec(`DELETE FROM eth.transaction_cids`) + if err != nil { + t.Fatal(err) + } + _, err = tx.Exec(`DELETE FROM eth.receipt_cids`) + if err != nil { + t.Fatal(err) + } + _, err = tx.Exec(`DELETE FROM eth.state_cids`) + if err != nil { + t.Fatal(err) + } + _, err = tx.Exec(`DELETE FROM eth.storage_cids`) + if err != nil { + t.Fatal(err) + } + _, err = tx.Exec(`DELETE FROM blocks`) + if err != nil { + t.Fatal(err) + } + err = tx.Commit() + if err != nil { + t.Fatal(err) + } +} diff --git a/statediff/indexer/writer.go b/statediff/indexer/writer.go new file mode 100644 index 000000000000..e8c0a03983cb --- /dev/null +++ b/statediff/indexer/writer.go @@ -0,0 +1,143 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package indexer + +import ( + "fmt" + + "github.com/jmoiron/sqlx" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/statediff/indexer/models" + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" +) + +var ( + nullHash = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000") +) + +// Handles processing and writing of indexed IPLD objects to Postgres +type PostgresCIDWriter struct { + db *postgres.DB +} + +// NewPostgresCIDWriter creates a new pointer to a Indexer which satisfies the PostgresCIDWriter interface +func NewPostgresCIDWriter(db *postgres.DB) *PostgresCIDWriter { + return &PostgresCIDWriter{ + db: db, + } +} + +func (in *PostgresCIDWriter) upsertHeaderCID(tx *sqlx.Tx, header models.HeaderModel) (int64, error) { + var headerID int64 + err := tx.QueryRowx(`INSERT INTO eth.header_cids (block_number, block_hash, parent_hash, cid, td, node_id, reward, state_root, tx_root, receipt_root, uncle_root, bloom, timestamp, mh_key, times_validated) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + ON CONFLICT (block_number, block_hash) DO UPDATE SET (parent_hash, cid, td, node_id, reward, state_root, tx_root, receipt_root, uncle_root, bloom, timestamp, mh_key, times_validated) = ($3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, eth.header_cids.times_validated + 1) + RETURNING id`, + header.BlockNumber, header.BlockHash, header.ParentHash, header.CID, header.TotalDifficulty, in.db.NodeID, header.Reward, header.StateRoot, header.TxRoot, + header.RctRoot, header.UncleRoot, header.Bloom, header.Timestamp, header.MhKey, 1).Scan(&headerID) + if err != nil { + return 0, fmt.Errorf("error upserting header_cids entry: %v", err) + } + indexerMetrics.blocks.Inc(1) + return headerID, nil +} + +func (in *PostgresCIDWriter) upsertUncleCID(tx *sqlx.Tx, uncle models.UncleModel, headerID int64) error { + _, err := tx.Exec(`INSERT INTO eth.uncle_cids (block_hash, header_id, parent_hash, cid, reward, mh_key) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (header_id, block_hash) DO UPDATE SET (parent_hash, cid, reward, mh_key) = ($3, $4, $5, $6)`, + uncle.BlockHash, headerID, uncle.ParentHash, uncle.CID, uncle.Reward, uncle.MhKey) + if err != nil { + return fmt.Errorf("error upserting uncle_cids entry: %v", err) + } + return nil +} + +func (in *PostgresCIDWriter) upsertTransactionCID(tx *sqlx.Tx, transaction models.TxModel, headerID int64) (int64, error) { + var txID int64 + err := tx.QueryRowx(`INSERT INTO eth.transaction_cids (header_id, tx_hash, cid, dst, src, index, mh_key, tx_data, tx_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (header_id, tx_hash) DO UPDATE SET (cid, dst, src, index, mh_key, tx_data, tx_type) = ($3, $4, $5, $6, $7, $8, $9) + RETURNING id`, + headerID, transaction.TxHash, transaction.CID, transaction.Dst, transaction.Src, transaction.Index, transaction.MhKey, transaction.Data, transaction.Type).Scan(&txID) + if err != nil { + return 0, fmt.Errorf("error upserting transaction_cids entry: %v", err) + } + indexerMetrics.transactions.Inc(1) + return txID, nil +} + +func (in *PostgresCIDWriter) upsertAccessListElement(tx *sqlx.Tx, accessListElement models.AccessListElementModel, txID int64) error { + _, err := tx.Exec(`INSERT INTO eth.access_list_element (tx_id, index, address, storage_keys) VALUES ($1, $2, $3, $4) + ON CONFLICT (tx_id, index) DO UPDATE SET (address, storage_keys) = ($3, $4)`, + txID, accessListElement.Index, accessListElement.Address, accessListElement.StorageKeys) + if err != nil { + return fmt.Errorf("error upserting access_list_element entry: %v", err) + } + indexerMetrics.accessListEntries.Inc(1) + return nil +} + +func (in *PostgresCIDWriter) upsertReceiptCID(tx *sqlx.Tx, rct models.ReceiptModel, txID int64) error { + _, err := tx.Exec(`INSERT INTO eth.receipt_cids (tx_id, cid, contract, contract_hash, topic0s, topic1s, topic2s, topic3s, log_contracts, mh_key, post_state, post_status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + ON CONFLICT (tx_id) DO UPDATE SET (cid, contract, contract_hash, topic0s, topic1s, topic2s, topic3s, log_contracts, mh_key, post_state, post_status) = ($2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, + txID, rct.CID, rct.Contract, rct.ContractHash, rct.Topic0s, rct.Topic1s, rct.Topic2s, rct.Topic3s, rct.LogContracts, rct.MhKey, rct.PostState, rct.PostStatus) + if err != nil { + return fmt.Errorf("error upserting receipt_cids entry: %v", err) + } + indexerMetrics.receipts.Inc(1) + return nil +} + +func (in *PostgresCIDWriter) upsertStateCID(tx *sqlx.Tx, stateNode models.StateNodeModel, headerID int64) (int64, error) { + var stateID int64 + var stateKey string + if stateNode.StateKey != nullHash.String() { + stateKey = stateNode.StateKey + } + err := tx.QueryRowx(`INSERT INTO eth.state_cids (header_id, state_leaf_key, cid, state_path, node_type, diff, mh_key) VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (header_id, state_path) DO UPDATE SET (state_leaf_key, cid, node_type, diff, mh_key) = ($2, $3, $5, $6, $7) + RETURNING id`, + headerID, stateKey, stateNode.CID, stateNode.Path, stateNode.NodeType, true, stateNode.MhKey).Scan(&stateID) + if err != nil { + return 0, fmt.Errorf("error upserting state_cids entry: %v", err) + } + return stateID, nil +} + +func (in *PostgresCIDWriter) upsertStateAccount(tx *sqlx.Tx, stateAccount models.StateAccountModel, stateID int64) error { + _, err := tx.Exec(`INSERT INTO eth.state_accounts (state_id, balance, nonce, code_hash, storage_root) VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (state_id) DO UPDATE SET (balance, nonce, code_hash, storage_root) = ($2, $3, $4, $5)`, + stateID, stateAccount.Balance, stateAccount.Nonce, stateAccount.CodeHash, stateAccount.StorageRoot) + if err != nil { + return fmt.Errorf("error upserting state_accounts entry: %v", err) + } + return nil +} + +func (in *PostgresCIDWriter) upsertStorageCID(tx *sqlx.Tx, storageCID models.StorageNodeModel, stateID int64) error { + var storageKey string + if storageCID.StorageKey != nullHash.String() { + storageKey = storageCID.StorageKey + } + _, err := tx.Exec(`INSERT INTO eth.storage_cids (state_id, storage_leaf_key, cid, storage_path, node_type, diff, mh_key) VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (state_id, storage_path) DO UPDATE SET (storage_leaf_key, cid, node_type, diff, mh_key) = ($2, $3, $5, $6, $7)`, + stateID, storageKey, storageCID.CID, storageCID.Path, storageCID.NodeType, true, storageCID.MhKey) + if err != nil { + return fmt.Errorf("error upserting storage_cids entry: %v", err) + } + return nil +} diff --git a/statediff/mainnet_tests/block0_rlp b/statediff/mainnet_tests/block0_rlp new file mode 100644 index 0000000000000000000000000000000000000000..eb912911d159fcb7116d8bb9aee0f20f04a1bdcd GIT binary patch literal 540 zcmey#B>9s`WB~&Kut4^V?~8lKW2;uTZcX2I=8Dv1Ay?@sT_wUF7CQZPxH8#&3N`~4 zT>mlMf9slxn`<~{&$`Ok{r1o~uA*BPo2tY)Zy7}Jv$`w@dm%3_eI|6-|7O=`lMejI zi}rXRJu_$R0mdZ#t&fwpC=yh#lM$Q6BTB551?1g^CgF~Th6RGVU)=UHsrKucTqDz5 i@MP;-eqDFg%TGgWSKmh^`?h+u1H-La+RLaRU-Uhq0?W7E0f))urAHh4LTt%Cei$r$3@0SR&|VnN5tTvzjbOKu*B6$+WjzF9T#@_xfFQR|9*?g-8ocF5VbK=zF9i+jgo zt5&yeP2YCriqvHxSLrETCBh#TI{kIHGTD8~U46Mc&nBi9x&0wd>mySemVB_$S>Wk> z(=oY3P@rVWt$8Q*u$ay*mj27JUE7!M&R(BOZ#FwF2zwzfE`26++5cwOXOj;6$cy%P zAU!i@?E%Il{jHCawsml^6A=o#o5 zB&Q^so0*vF=Va!UR_Lea8|s-X&}W};bd}-iDUxyWDbCki*GHEg$zl2ZbE3GQWEani SzqdMe1piW#YFjn$zySc*K!fW5 literal 0 HcmV?d00001 diff --git a/statediff/mainnet_tests/block3_rlp b/statediff/mainnet_tests/block3_rlp new file mode 100644 index 0000000000000000000000000000000000000000..86f90a83a6e553ed112ceb9f361344f32f5a48b5 GIT binary patch literal 1079 zcmey#V)BzoV!@WF3$H&*GYnccsi1dBi?R962$2NQMGg9nla~mvxXn1ZAX|K=#jz*v zB{pTi{_P^Qn*`xzM@}fN+ zNYBh!dw?-Xf9vDqEs6vc>||sZ>7bSQUxWTX<|g5ehL+G>P6-R#Q%f@R%MA4l^bB;< zlFcnsjEz$Cb29TvEA-Rz4fRYGq(0Ppqx>&5^x2-WI0^@n(n$#;=+V1lV`Ku^zd#x z-M@H&>>1w|_m0O_t!~|#zU|BvsmnsH(o?!hgg-2F`s;9Ivip=1ug@I)nlXPx>fGi+ zW5=g!B~7MyERf4%W^Mc{QM{$XsiAew<;@4YR)i{i=rQp5`Y!qMK01? literal 0 HcmV?d00001 diff --git a/statediff/mainnet_tests/builder_test.go b/statediff/mainnet_tests/builder_test.go new file mode 100644 index 000000000000..bb6443f753d4 --- /dev/null +++ b/statediff/mainnet_tests/builder_test.go @@ -0,0 +1,690 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statediff_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "math/big" + "os" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/statediff" + "github.com/ethereum/go-ethereum/statediff/testhelpers" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" +) + +var ( + db ethdb.Database + genesisBlock, block0, block1, block2, block3 *types.Block + block1CoinbaseAddr, block2CoinbaseAddr, block3CoinbaseAddr common.Address + block1CoinbaseHash, block2CoinbaseHash, block3CoinbaseHash common.Hash + builder statediff.Builder + emptyStorage = make([]sdtypes.StorageNode, 0) + + // block 1 data + block1CoinbaseAccount, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(5000000000000000000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + block1CoinbaseLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("38251692195afc818c92b485fcb8a4691af89cbe5a2ab557b83a4261be2a9a"), + block1CoinbaseAccount, + }) + block1CoinbaseLeafNodeHash = crypto.Keccak256(block1CoinbaseLeafNode) + block1x040bBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("cc947d5ebb80600bad471f12c6ad5e4981e3525ecf8a2d982cc032536ae8b66d"), + common.Hex2Bytes("e80e52462e635a834e90e86ccf7673a6430384aac17004d626f4db831f0624bc"), + common.Hex2Bytes("59a8f11f60cb0a8488831f242da02944a26fd269d0608a44b8b873ded9e59e1b"), + common.Hex2Bytes("1ffb51e987e3cbd2e1dc1a64508d2e2b265477e21698b0d10fdf137f35027f40"), + []byte{}, + common.Hex2Bytes("ce5077f49a13ff8199d0e77715fdd7bfd6364774effcd5499bd93cba54b3c644"), + common.Hex2Bytes("f5146783c048e66ce1a776ae990b4255e5fba458ece77fcb83ff6e91d6637a88"), + common.Hex2Bytes("6a0558b6c38852e985cf01c2156517c1c6a1e64c787a953c347825f050b236c6"), + common.Hex2Bytes("56b6e93958b99aaae158cc2329e71a1865ba6f39c67b096922c5cf3ed86b0ae5"), + []byte{}, + common.Hex2Bytes("50d317a89a3405367d66668902f2c9f273a8d0d7d5d790dc516bca142f4a84af"), + common.Hex2Bytes("c72ca72750fdc1af3e6da5c7c5d82c54e4582f15b488a8aa1674058a99825dae"), + common.Hex2Bytes("e1a489df7b18cde818da6d38e235b026c2e61bcd3d34880b3ed0d67e0e4f0159"), + common.Hex2Bytes("b58d5062f2609fd2d68f00d14ab33fef2b373853877cf40bf64729e85b8fdc54"), + block1CoinbaseLeafNodeHash, + []byte{}, + []byte{}, + }) + block1x040bBranchNodeHash = crypto.Keccak256(block1x040bBranchNode) + block1x04BranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("a9317a59365ca09cefcd384018696590afffc432e35a97e8f85aa48907bf3247"), + common.Hex2Bytes("e0bc229254ce7a6a736c3953e570ab18b4a7f5f2a9aa3c3057b5f17d250a1cad"), + common.Hex2Bytes("a2484ec8884dbe0cf24ece99d67df0d1fe78992d67cc777636a817cb2ef205aa"), + common.Hex2Bytes("12b78d4078c607747f06bb88bd08f839eaae0e3ac6854e5f65867d4f78abb84e"), + common.Hex2Bytes("359a51862df5462e4cd302f69cb338512f21eb37ce0791b9a562e72ec48b7dbf"), + common.Hex2Bytes("13f8d617b6a734da9235b6ac80bdd7aeaff6120c39aa223638d88f22d4ba4007"), + common.Hex2Bytes("02055c6400e0ec3440a8bb8fdfd7d6b6c57b7bf83e37d7e4e983d416fdd8314e"), + common.Hex2Bytes("4b1cca9eb3e47e805e7f4c80671a9fcd589fd6ddbe1790c3f3e177e8ede01b9e"), + common.Hex2Bytes("70c3815efb23b986018089e009a38e6238b8850b3efd33831913ca6fa9240249"), + common.Hex2Bytes("7084699d2e72a193fd75bb6108ae797b4661696eba2d631d521fc94acc7b3247"), + common.Hex2Bytes("b2b3cd9f1e46eb583a6185d9a96b4e80125e3d75e6191fdcf684892ef52935cb"), + block1x040bBranchNodeHash, + common.Hex2Bytes("34d9ff0fee6c929424e52268dedbc596d10786e909c5a68d6466c2aba17387ce"), + common.Hex2Bytes("7484d5e44b6ee6b10000708c37e035b42b818475620f9316beffc46531d1eebf"), + common.Hex2Bytes("30c8a283adccf2742272563cd3d6710c89ba21eac0118bf5310cfb231bcca77f"), + common.Hex2Bytes("4bae8558d2385b8d3bc6e6ede20bdbc5dbb0b5384c316ba8985682f88d2e506d"), + []byte{}, + }) + block1x04BranchNodeHash = crypto.Keccak256(block1x04BranchNode) + block1RootBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("90dcaf88c40c7bbc95a912cbdde67c175767b31173df9ee4b0d733bfdd511c43"), + common.Hex2Bytes("babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bd"), + common.Hex2Bytes("473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021"), + common.Hex2Bytes("bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0"), + block1x04BranchNodeHash, + common.Hex2Bytes("a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7"), + common.Hex2Bytes("e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92d"), + common.Hex2Bytes("f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721"), + common.Hex2Bytes("7117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681"), + common.Hex2Bytes("69eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472"), + common.Hex2Bytes("203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8e"), + common.Hex2Bytes("9287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208"), + common.Hex2Bytes("6fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94ec"), + common.Hex2Bytes("7b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475"), + common.Hex2Bytes("51f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0"), + common.Hex2Bytes("89d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb"), + []byte{}, + }) + + // block 2 data + block2CoinbaseAccount, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: big.NewInt(5000000000000000000), + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + block2CoinbaseLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("20679cbcf198c1741a6f4e4473845659a30caa8b26f8d37a0be2e2bc0d8892"), + block2CoinbaseAccount, + }) + block2CoinbaseLeafNodeHash = crypto.Keccak256(block2CoinbaseLeafNode) + block2MovedPremineBalance, _ = new(big.Int).SetString("4000000000000000000000", 10) + block2MovedPremineAccount, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: block2MovedPremineBalance, + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + block2MovedPremineLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("20f2e24db7943eab4415f99e109698863b0fecca1cf9ffc500f38cefbbe29e"), + block2MovedPremineAccount, + }) + block2MovedPremineLeafNodeHash = crypto.Keccak256(block2MovedPremineLeafNode) + block2x00080dBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + block2MovedPremineLeafNodeHash, + []byte{}, + []byte{}, + []byte{}, + block2CoinbaseLeafNodeHash, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) + block2x00080dBranchNodeHash = crypto.Keccak256(block2x00080dBranchNode) + block2x0008BranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("def97a26f824fc3911cf7f8c41dfc9bc93cc36ae2248de22ecae01d6950b2dc9"), + common.Hex2Bytes("234a575e2c5badab8de0f6515b6723195323a0562fbe1316255888637043f1c1"), + common.Hex2Bytes("29659740af1c23306ee8f8294c71a5632ace8c80b1eb61cfdf7022f47ff52305"), + common.Hex2Bytes("cf2681d23bb666d89dec8123bce9e626240a7e2ce7a1e8316b1ee88181c9471c"), + common.Hex2Bytes("18d8de6967fe34b9fd411c74fecc45f8a737961791e70d8ece967bb07cf4d4dc"), + common.Hex2Bytes("7cad60c7cbca8c79c2db5a8fc1baa9381484d43d6c37dfb97718c3a109d47dfc"), + common.Hex2Bytes("2138f5a9062b750b6320e5fac5b134da90a9edbda06ef3e1ae64fb1366ca998c"), + common.Hex2Bytes("532826502a9661fcae7c0f5d2a4c8cb287dfc521e828349543c5a461a9d591ed"), + common.Hex2Bytes("30543537413dd086d4b1560f46b90e8da0f43de5584a138ab036d74e84657523"), + common.Hex2Bytes("c98042928af640bfa1142aca895cd76e146332dce94ddad3426e74ed519ca1e0"), + common.Hex2Bytes("43de3e62cc3148193899d018dff813c04c5b636ce95bd7e828416204292d9ff9"), + []byte{}, + common.Hex2Bytes("78d533b9182bb42f6c16e9ebd5734f0d280179ba1c9b6316c2c1df73f7dd8a54"), + block2x00080dBranchNodeHash, + common.Hex2Bytes("934b736b57a892aaa15a03c7e37746bb096313727135f9841cb64c263785cf81"), + common.Hex2Bytes("38ce97150e90dfd7258901a0ddee72d8e30760a3d0419dbb80135c66588739a2"), + []byte{}, + }) + block2x0008BranchNodeHash = crypto.Keccak256(block2x0008BranchNode) + block2x00BranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("e45a9e85cab1b6eb18b30df2c6acc448bbac6a30d81646823b31223e16e5063e"), + common.Hex2Bytes("33bd7171d556b981f6849064eb09412b24fedc0812127db936067043f53db1b9"), + common.Hex2Bytes("ca56945f074da4f15587404593faf3a50d17ea0e21a418ad6ec99bdf4bf3f914"), + common.Hex2Bytes("da23e9004f782df128eea1adff77952dc85f91b7f7ca4893aac5f21d24c3a1c9"), + common.Hex2Bytes("ba5ec61fa780ee02af19db99677c37560fc4f0df5c278d9dfa2837f30f72bc6b"), + common.Hex2Bytes("8310ad91625c2e3429a74066b7e2e0c958325e4e7fa3ec486b73b7c8300cfef7"), + common.Hex2Bytes("732e5c103bf4d5adfef83773026809d9405539b67e93293a02342e83ad2fb766"), + common.Hex2Bytes("30d14ff0c2aab57d1fbaf498ab14519b4e9d94f149a3dc15f0eec5adf8df25e1"), + block2x0008BranchNodeHash, + common.Hex2Bytes("5a43bd92e55aa78df60e70b6b53b6366c4080fd6a5bdd7b533b46aff4a75f6f2"), + common.Hex2Bytes("a0c410aa59efe416b1213166fab680ce330bd46c3ebf877ff14609ee6a383600"), + common.Hex2Bytes("2f41e918786e557293068b1eda9b3f9f86ed4e65a6a5363ee3262109f6e08b17"), + common.Hex2Bytes("01f42a40f02f6f24bb97b09c4d3934e8b03be7cfbb902acc1c8fd67a7a5abace"), + common.Hex2Bytes("0acbdce2787a6ea177209bd13bfc9d0779d7e2b5249e0211a2974164e14312f5"), + common.Hex2Bytes("dadbe113e4132e0c0c3cd4867e0a2044d0e5a3d44b350677ed42fc9244d004d4"), + common.Hex2Bytes("aa7441fefc17d76aedfcaf692fe71014b94c1547b6d129562b34fc5995ca0d1a"), + []byte{}, + }) + block2x00BranchNodeHash = crypto.Keccak256(block2x00BranchNode) + block2RootBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + block2x00BranchNodeHash, + common.Hex2Bytes("babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bd"), + common.Hex2Bytes("473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021"), + common.Hex2Bytes("bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0"), + block1x04BranchNodeHash, + common.Hex2Bytes("a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7"), + common.Hex2Bytes("e823850f50bf72baae9d1733a36a444ab65d0a6faaba404f0583ce0ca4dad92d"), + common.Hex2Bytes("f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721"), + common.Hex2Bytes("7117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681"), + common.Hex2Bytes("69eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472"), + common.Hex2Bytes("203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8e"), + common.Hex2Bytes("9287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208"), + common.Hex2Bytes("6fc2d754e304c48ce6a517753c62b1a9c1d5925b89707486d7fc08919e0a94ec"), + common.Hex2Bytes("7b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475"), + common.Hex2Bytes("51f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0"), + common.Hex2Bytes("89d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb"), + []byte{}, + }) + + // block3 data + // path 060e0f + blcok3CoinbaseBalance, _ = new(big.Int).SetString("5156250000000000000", 10) + block3CoinbaseAccount, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: blcok3CoinbaseBalance, + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + block3CoinbaseLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3a174f00e64521a535f35e67c1aa241951c791639b2f3d060f49c5d9fa8b9e"), + block3CoinbaseAccount, + }) + block3CoinbaseLeafNodeHash = crypto.Keccak256(block3CoinbaseLeafNode) + // path 0c0e050703 + block3MovedPremineBalance1, _ = new(big.Int).SetString("3750000000000000000", 10) + block3MovedPremineAccount1, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: block3MovedPremineBalance1, + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + block3MovedPremineLeafNode1, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3ced93917e658d10e2d9009470dad72b63c898d173721194a12f2ae5e190"), // ce573ced93917e658d10e2d9009470dad72b63c898d173721194a12f2ae5e190 + block3MovedPremineAccount1, + }) + block3MovedPremineLeafNodeHash1 = crypto.Keccak256(block3MovedPremineLeafNode1) + // path 0c0e050708 + block3MovedPremineBalance2, _ = new(big.Int).SetString("1999944000000000000000", 10) + block3MovedPremineAccount2, _ = rlp.EncodeToBytes(state.Account{ + Nonce: 0, + Balance: block3MovedPremineBalance2, + CodeHash: testhelpers.NullCodeHash.Bytes(), + Root: testhelpers.EmptyContractRoot, + }) + block3MovedPremineLeafNode2, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("33bc1e69eedf90f402e11f6862da14ed8e50156635a04d6393bbae154012"), // ce5783bc1e69eedf90f402e11f6862da14ed8e50156635a04d6393bbae154012 + block3MovedPremineAccount2, + }) + block3MovedPremineLeafNodeHash2 = crypto.Keccak256(block3MovedPremineLeafNode2) + + block3x0c0e0507BranchNode, _ = rlp.EncodeToBytes([]interface{}{ + []byte{}, + []byte{}, + []byte{}, + block3MovedPremineLeafNodeHash1, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + block3MovedPremineLeafNodeHash2, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) + block3x0c0e0507BranchNodeHash = crypto.Keccak256(block3x0c0e0507BranchNode) + + block3x0c0e05BranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("452e3beb503b1d87ae7c672b98a8e3fd043a671405502562ae1043dc97151a50"), + []byte{}, + common.Hex2Bytes("2f5bb16f77086f67ce8c4258cb9061cb299e597b2ad4ad6d7ccc474d6d88e85e"), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + block3x0c0e0507BranchNodeHash, + []byte{}, + common.Hex2Bytes("44623e5a9319f83870db0ea4611a25fca1e1da3eeea2be4a091dfc15ab45689e"), + common.Hex2Bytes("b41e047a97f44fa4cb8146467b88c8f4705811029d9e170abb0aba7d0af9f0da"), + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + []byte{}, + }) + block3x0c0e05BranchNodeHash = crypto.Keccak256(block3x0c0e05BranchNode) + + block3x060eBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("94d77c7c30b88829c9989948b206cda5e532b38b49534261c517aebf4a3e6fdb"), + common.Hex2Bytes("a5cf57a50da8204964e834a12a53f9bed7afc9b700a4a81b440122d60c7603a7"), + []byte{}, + common.Hex2Bytes("3730ec0571f34b6c3b178dc26ccb31a3f50c29da9b1921e41b9477ddab41b0fe"), + []byte{}, + common.Hex2Bytes("543952bb9566c2018cf8d7b90d6a7903cdfff3d79ac36189be5322de42fc3fc0"), + []byte{}, + common.Hex2Bytes("c4a49b66f0bcc08531e50cdea5577a281d111fa542eaefd9a9aead8febb0735e"), + common.Hex2Bytes("362ad58916c71463b98c079649fc486c5f082c4f548bd4ab501515f0c5641cb4"), + common.Hex2Bytes("36aae109f6f55f0bd05eb05bb365af2332dfe5f06d3d17903e88534c319eb709"), + common.Hex2Bytes("430dcfc5cc49a6b490dd54138920e8f94e427239c2bccc14705cfd4ff6cc4383"), + common.Hex2Bytes("73ed77563dfed2fdb38900b474db88b2270f449167e0d877fda9e2229f119fe8"), + common.Hex2Bytes("5dfe06013f2a41f1779194ceb07769d019f518b2a694a82fa1661e60fd973eaa"), + common.Hex2Bytes("80bdfd85fbb6b45850bad6e34136aaa1b04711e47469fa2f0d19eca52089efb5"), + []byte{}, + block3CoinbaseLeafNodeHash, + []byte{}, + }) + block3x060eBranchNodeHash = crypto.Keccak256(block3x060eBranchNode) + + block3x0c0eBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("70647f11b2b995d718f9e8aceb44c8839e0055641930d216fa6090280a9d63d5"), + common.Hex2Bytes("fdfb17cd2fba2a14219981cb7886a1977cd85dbef5c767c562f4a5f547febff0"), + common.Hex2Bytes("ff87313253ec6f860142b7bf62efb4cb07ea668c57aa90cbe9ef22b72fee15c7"), + common.Hex2Bytes("3a77b3c26a54ad37bdf4e19c1bce93493ec0f79d9ad90190b70bc840b54918e1"), + common.Hex2Bytes("af1b3b14324561b68f2e24dbcc28673ab35ce3fd0230fe2bc86b3d1931745195"), + block3x0c0e05BranchNodeHash, + common.Hex2Bytes("647dcbfe6aabcd9d219ff40422af4326bfc1ec66703195a78eb48618ddef248d"), + common.Hex2Bytes("2d2bf06159cc8928283c3419a03f08ea34c493a9d002a0ec76d5c429508ccaf4"), + common.Hex2Bytes("d7147251b3f48f25e1e4c6d8f83a00b1eca66e99a4ea0d238942ce72d0ba6414"), + common.Hex2Bytes("cb859370869967594fb29f4e2904413310146733d7fcbd11407f3e47626e0e34"), + common.Hex2Bytes("b93ab9b0bd83963860fbe0b7d543879cfde756ea1618d2a40d85483058cc5a26"), + common.Hex2Bytes("45aee096499d209931457ce251c5c7e5543f22524f67785ff8f0f3f02588b0ed"), + []byte{}, + common.Hex2Bytes("aa2ae9379797c5066bba646108074ae8677e82c923d584b6d1c1268ca3708c5c"), + common.Hex2Bytes("e6eb055f0d8e194c083471479a3de87fa0f90c0f4aaa518416ec1e469ec32e3a"), + common.Hex2Bytes("0cc9c50fc7eba162fb17f2e04e3599c13abbf210d9781864d0edec401ecaebba"), + []byte{}, + }) + block3x0c0eBranchNodeHash = crypto.Keccak256(block3x0c0eBranchNode) + + block3x06BranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("68f7ff8c074d6e4cccd55b5b1c2116a6dd7047d4332090e6db8839362991b0ae"), + common.Hex2Bytes("c446eb4377c750701374c56e50759e6ba68b7adf4d543e718c8b28a99ae3b6ad"), + common.Hex2Bytes("ef2c49ec64cb65eae0d99684e74c8af2bd0206c9a0214d9d3eddf0881dd8412a"), + common.Hex2Bytes("7096c4cc7e8125f0b142d8644ad681f8a8142e210c806f33f3f7004f0e9d6002"), + common.Hex2Bytes("bc9a8ae647b234cd6607b6b0245e3b3d5ec4f7ea006e7eda1f92d02f0ea91116"), + common.Hex2Bytes("a87720deb92ff2f899e809befab9970a61c86148c4fa09d04b77505ee4a5bda5"), + common.Hex2Bytes("2460e5b6ded7c0001de29c15db124614432fef6486370cc9970f63b0d95fd5e2"), + common.Hex2Bytes("ed1c447d4a32bc31e9e32259dc63da10df91231e786332e3df122b301b1f8fc3"), + common.Hex2Bytes("0d27dfc201d995c2323b792860dbca087da7cc56d1698c39b7c4b9277729c5ca"), + common.Hex2Bytes("f6d2be168d9c17643c9ea80c29322b364604cdfd36eef40123d83fad364e43fa"), + common.Hex2Bytes("004bf1c30a5730f464de1a0ba4ac5b5618df66d6106073d08742166e33a7eeb5"), + common.Hex2Bytes("7298d019a57a1b04ac31ed874d654ba0d3c249704c5d9efa1d08959fc89e0779"), + common.Hex2Bytes("fb3d50b7af6f839e371ff8ebd0322e94e6b6fb7888416737f88cf55bcf5859ec"), + common.Hex2Bytes("4e7a2618fa1fc560a73c24839657adf7e48d600ecfb12333678115936597a913"), + block3x060eBranchNodeHash, + common.Hex2Bytes("1909706c5db040f54c19f4050659ad484982145b02474653917de379f15ebb36"), + []byte{}, + }) + block3x06BranchNodeHash = crypto.Keccak256(block3x06BranchNode) + + block3x0cBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("dae48f5b47930c28bb116fbd55e52cd47242c71bf55373b55eb2805ee2e4a929"), + common.Hex2Bytes("0f1f37f337ec800e2e5974e2e7355f10f1a4832b39b846d916c3597a460e0676"), + common.Hex2Bytes("da8f627bb8fbeead17b318e0a8e4f528db310f591bb6ab2deda4a9f7ca902ab5"), + common.Hex2Bytes("971c662648d58295d0d0aa4b8055588da0037619951217c22052802549d94a2f"), + common.Hex2Bytes("ccc701efe4b3413fd6a61a6c9f40e955af774649a8d9fd212d046a5a39ddbb67"), + common.Hex2Bytes("d607cdb32e2bd635ee7f2f9e07bc94ddbd09b10ec0901b66628e15667aec570b"), + common.Hex2Bytes("5b89203dc940e6fa70ec19ad4e01d01849d3a5baa0a8f9c0525256ed490b159f"), + common.Hex2Bytes("b84227d48df68aecc772939a59afa9e1a4ab578f7b698bdb1289e29b6044668e"), + common.Hex2Bytes("fd1c992070b94ace57e48cbf6511a16aa770c645f9f5efba87bbe59d0a042913"), + common.Hex2Bytes("e16a7ccea6748ae90de92f8aef3b3dc248a557b9ac4e296934313f24f7fced5f"), + common.Hex2Bytes("42373cf4a00630d94de90d0a23b8f38ced6b0f7cb818b8925fee8f0c2a28a25a"), + common.Hex2Bytes("5f89d2161c1741ff428864f7889866484cef622de5023a46e795dfdec336319f"), + common.Hex2Bytes("7597a017664526c8c795ce1da27b8b72455c49657113e0455552dbc068c5ba31"), + common.Hex2Bytes("d5be9089012fda2c585a1b961e988ea5efcd3a06988e150a8682091f694b37c5"), + block3x0c0eBranchNodeHash, + common.Hex2Bytes("49bf6e8df0acafd0eff86defeeb305568e44d52d2235cf340ae15c6034e2b241"), + []byte{}, + }) + block3x0cBranchNodeHash = crypto.Keccak256(block3x0cBranchNode) + + block3RootBranchNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("f646da473c426e79f1c796b00d4873f47de1dbe1c9d19d63993a05eeb8b4041d"), + common.Hex2Bytes("babe369f6b12092f49181ae04ca173fb68d1a5456f18d20fa32cba73954052bd"), + common.Hex2Bytes("473ecf8a7e36a829e75039a3b055e51b8332cbf03324ab4af2066bbd6fbf0021"), + common.Hex2Bytes("bbda34753d7aa6c38e603f360244e8f59611921d9e1f128372fec0d586d4f9e0"), + common.Hex2Bytes("d9cff5d5f2418afd16a4da5c221fdc8bd47520c5927922f69a68177b64da6ac0"), + common.Hex2Bytes("a5f3f2f7542148c973977c8a1e154c4300fec92f755f7846f1b734d3ab1d90e7"), + block3x06BranchNodeHash, + common.Hex2Bytes("f7a00cbe7d4b30b11faea3ae61b7f1f2b315b61d9f6bd68bfe587ad0eeceb721"), + common.Hex2Bytes("7117ef9fc932f1a88e908eaead8565c19b5645dc9e5b1b6e841c5edbdfd71681"), + common.Hex2Bytes("69eb2de283f32c11f859d7bcf93da23990d3e662935ed4d6b39ce3673ec84472"), + common.Hex2Bytes("203d26456312bbc4da5cd293b75b840fc5045e493d6f904d180823ec22bfed8e"), + common.Hex2Bytes("9287b5c21f2254af4e64fca76acc5cd87399c7f1ede818db4326c98ce2dc2208"), + block3x0cBranchNodeHash, + common.Hex2Bytes("7b1c54f15e299bd58bdfef9741538c7828b5d7d11a489f9c20d052b3471df475"), + common.Hex2Bytes("51f9dd3739a927c89e357580a4c97b40234aa01ed3d5e0390dc982a7975880a0"), + common.Hex2Bytes("89d613f26159af43616fd9455bb461f4869bfede26f2130835ed067a8b967bfb"), + []byte{}, + }) +) + +func init() { + if os.Getenv("MODE") != "statediff" { + fmt.Println("Skipping statediff test") + os.Exit(0) + } + db = rawdb.NewMemoryDatabase() + genesisBlock = core.DefaultGenesisBlock().MustCommit(db) + genBy, err := rlp.EncodeToBytes(genesisBlock) + if err != nil { + log.Fatal(err) + } + var block0RLP []byte + block0, block0RLP, err = loadBlockFromRLPFile("./block0_rlp") + if err != nil { + log.Fatal(err) + } + if !bytes.Equal(genBy, block0RLP) { + log.Fatal("mainnet genesis blocks do not match") + } + block1, _, err = loadBlockFromRLPFile("./block1_rlp") + if err != nil { + log.Fatal(err) + } + block1CoinbaseAddr = block1.Coinbase() + block1CoinbaseHash = crypto.Keccak256Hash(block1CoinbaseAddr.Bytes()) + block2, _, err = loadBlockFromRLPFile("./block2_rlp") + if err != nil { + log.Fatal(err) + } + block2CoinbaseAddr = block2.Coinbase() + block2CoinbaseHash = crypto.Keccak256Hash(block2CoinbaseAddr.Bytes()) + block3, _, err = loadBlockFromRLPFile("./block3_rlp") + if err != nil { + log.Fatal(err) + } + block3CoinbaseAddr = block3.Coinbase() + block3CoinbaseHash = crypto.Keccak256Hash(block3CoinbaseAddr.Bytes()) +} + +func loadBlockFromRLPFile(filename string) (*types.Block, []byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, nil, err + } + defer f.Close() + blockRLP, err := ioutil.ReadAll(f) + if err != nil { + return nil, nil, err + } + block := new(types.Block) + return block, blockRLP, rlp.DecodeBytes(blockRLP, block) +} + +func TestBuilderOnMainnetBlocks(t *testing.T) { + chain, _ := core.NewBlockChain(db, nil, params.MainnetChainConfig, ethash.NewFaker(), vm.Config{}, nil, nil) + _, err := chain.InsertChain([]*types.Block{block1, block2, block3}) + if err != nil { + t.Error(err) + } + params := statediff.Params{ + IntermediateStateNodes: true, + } + builder = statediff.NewBuilder(chain.StateCache()) + + var tests = []struct { + name string + startingArguments statediff.Args + expected *statediff.StateObject + }{ + // note that block0 (genesis) has over 1000 nodes due to the pre-allocation for the crowd-sale + // it is not feasible to write a unit test of that size at this time + { + "testBlock1", + //10000 transferred from testBankAddress to account1Addr + statediff.Args{ + OldStateRoot: block0.Root(), + NewStateRoot: block1.Root(), + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block1RootBranchNode, + }, + { + Path: []byte{'\x04'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block1x04BranchNode, + }, + { + Path: []byte{'\x04', '\x0b'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block1x040bBranchNode, + }, + { + Path: []byte{'\x04', '\x0b', '\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: block1CoinbaseHash.Bytes(), + NodeValue: block1CoinbaseLeafNode, + StorageNodes: emptyStorage, + }, + }, + }, + }, + { + "testBlock2", + // 1000 transferred from testBankAddress to account1Addr + // 1000 transferred from account1Addr to account2Addr + // account1addr creates a new contract + statediff.Args{ + OldStateRoot: block1.Root(), + NewStateRoot: block2.Root(), + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block2.Number(), + BlockHash: block2.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block2RootBranchNode, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block2x00BranchNode, + }, + { + Path: []byte{'\x00', '\x08'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block2x0008BranchNode, + }, + { + Path: []byte{'\x00', '\x08', '\x0d'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block2x00080dBranchNode, + }, + // this new leaf at x00 x08 x0d x00 was "created" when a premine account (leaf) was moved from path x00 x08 x0d + // this occurred because of the creation of the new coinbase receiving account (leaf) at x00 x08 x0d x04 + // which necessitates we create a branch at x00 x08 x0d (as shown in the below UpdateAccounts) + { + Path: []byte{'\x00', '\x08', '\x0d', '\x00'}, + NodeType: sdtypes.Leaf, + StorageNodes: emptyStorage, + LeafKey: common.HexToHash("08d0f2e24db7943eab4415f99e109698863b0fecca1cf9ffc500f38cefbbe29e").Bytes(), + NodeValue: block2MovedPremineLeafNode, + }, + { + Path: []byte{'\x00', '\x08', '\x0d', '\x04'}, + NodeType: sdtypes.Leaf, + StorageNodes: emptyStorage, + LeafKey: block2CoinbaseHash.Bytes(), + NodeValue: block2CoinbaseLeafNode, + }, + }, + }, + }, + { + "testBlock3", + //the contract's storage is changed + //and the block is mined by account 2 + statediff.Args{ + OldStateRoot: block2.Root(), + NewStateRoot: block3.Root(), + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + }, + &statediff.StateObject{ + BlockNumber: block3.Number(), + BlockHash: block3.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block3RootBranchNode, + }, + { + Path: []byte{'\x06'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block3x06BranchNode, + }, + { + Path: []byte{'\x06', '\x0e'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block3x060eBranchNode, + }, + { + Path: []byte{'\x0c'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block3x0cBranchNode, + }, + { + Path: []byte{'\x0c', '\x0e'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block3x0c0eBranchNode, + }, + { + Path: []byte{'\x0c', '\x0e', '\x05'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block3x0c0e05BranchNode, + }, + { + Path: []byte{'\x0c', '\x0e', '\x05', '\x07'}, + NodeType: sdtypes.Branch, + StorageNodes: emptyStorage, + NodeValue: block3x0c0e0507BranchNode, + }, + { // How was this account created??? + Path: []byte{'\x0c', '\x0e', '\x05', '\x07', '\x03'}, + NodeType: sdtypes.Leaf, + StorageNodes: emptyStorage, + LeafKey: common.HexToHash("ce573ced93917e658d10e2d9009470dad72b63c898d173721194a12f2ae5e190").Bytes(), + NodeValue: block3MovedPremineLeafNode1, + }, + { // This account (leaf) used to be at 0c 0e 05 07, likely moves because of the new account above + Path: []byte{'\x0c', '\x0e', '\x05', '\x07', '\x08'}, + NodeType: sdtypes.Leaf, + StorageNodes: emptyStorage, + LeafKey: common.HexToHash("ce5783bc1e69eedf90f402e11f6862da14ed8e50156635a04d6393bbae154012").Bytes(), + NodeValue: block3MovedPremineLeafNode2, + }, + { // this is the new account created due to the coinbase mining a block, it's creation shouldn't affect 0x 0e 05 07 + Path: []byte{'\x06', '\x0e', '\x0f'}, + NodeType: sdtypes.Leaf, + StorageNodes: emptyStorage, + LeafKey: block3CoinbaseHash.Bytes(), + NodeValue: block3CoinbaseLeafNode, + }, + }, + }, + }, + } + + for _, test := range tests { + diff, err := builder.BuildStateDiffObject(test.startingArguments, params) + if err != nil { + t.Error(err) + } + receivedStateDiffRlp, err := rlp.EncodeToBytes(diff) + if err != nil { + t.Error(err) + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(test.expected) + if err != nil { + t.Error(err) + } + sort.Slice(receivedStateDiffRlp, func(i, j int) bool { return receivedStateDiffRlp[i] < receivedStateDiffRlp[j] }) + sort.Slice(expectedStateDiffRlp, func(i, j int) bool { return expectedStateDiffRlp[i] < expectedStateDiffRlp[j] }) + if !bytes.Equal(receivedStateDiffRlp, expectedStateDiffRlp) { + t.Logf("Test failed: %s", test.name) + t.Errorf("actual state diff: %+v\nexpected state diff: %+v", diff, test.expected) + } + } +} diff --git a/statediff/metrics.go b/statediff/metrics.go new file mode 100644 index 000000000000..7e7d6e328c91 --- /dev/null +++ b/statediff/metrics.go @@ -0,0 +1,54 @@ +package statediff + +import ( + "strings" + + "github.com/ethereum/go-ethereum/metrics" +) + +const ( + namespace = "statediff" +) + +// Build a fully qualified metric name +func metricName(subsystem, name string) string { + if name == "" { + return "" + } + parts := []string{namespace, name} + if subsystem != "" { + parts = []string{namespace, subsystem, name} + } + // Prometheus uses _ but geth metrics uses / and replaces + return strings.Join(parts, "/") +} + +type statediffMetricsHandles struct { + // Height of latest synced by core.BlockChain + // FIXME + lastSyncHeight metrics.Gauge + // Height of the latest block received from chainEvent channel + lastEventHeight metrics.Gauge + // Height of latest state diff + lastStatediffHeight metrics.Gauge + // Current length of chainEvent channels + serviceLoopChannelLen metrics.Gauge + writeLoopChannelLen metrics.Gauge +} + +func RegisterStatediffMetrics(reg metrics.Registry) statediffMetricsHandles { + ctx := statediffMetricsHandles{ + lastSyncHeight: metrics.NewGauge(), + lastEventHeight: metrics.NewGauge(), + lastStatediffHeight: metrics.NewGauge(), + serviceLoopChannelLen: metrics.NewGauge(), + writeLoopChannelLen: metrics.NewGauge(), + } + subsys := "service" + reg.Register(metricName(subsys, "last_sync_height"), ctx.lastSyncHeight) + reg.Register(metricName(subsys, "last_event_height"), ctx.lastEventHeight) + reg.Register(metricName(subsys, "last_statediff_height"), ctx.lastStatediffHeight) + reg.Register(metricName(subsys, "service_loop_channel_len"), ctx.serviceLoopChannelLen) + reg.Register(metricName(subsys, "write_loop_channel_len"), ctx.writeLoopChannelLen) + return ctx +} diff --git a/statediff/service.go b/statediff/service.go new file mode 100644 index 000000000000..7935c4887a9a --- /dev/null +++ b/statediff/service.go @@ -0,0 +1,686 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statediff + +import ( + "bytes" + "math/big" + "strconv" + "sync" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" + + ind "github.com/ethereum/go-ethereum/statediff/indexer" + nodeinfo "github.com/ethereum/go-ethereum/statediff/indexer/node" + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" + . "github.com/ethereum/go-ethereum/statediff/types" +) + +const chainEventChanSize = 20000 +const genesisBlockNumber = 0 + +var writeLoopParams = Params{ + IntermediateStateNodes: true, + IntermediateStorageNodes: true, + IncludeBlock: true, + IncludeReceipts: true, + IncludeTD: true, + IncludeCode: true, +} + +var statediffMetrics = RegisterStatediffMetrics(metrics.DefaultRegistry) + +type blockChain interface { + SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription + GetBlockByHash(hash common.Hash) *types.Block + GetBlockByNumber(number uint64) *types.Block + GetReceiptsByHash(hash common.Hash) types.Receipts + GetTdByHash(hash common.Hash) *big.Int + UnlockTrie(root common.Hash) + StateCache() state.Database +} + +// IService is the state-diffing service interface +type IService interface { + // Start() and Stop() + node.Lifecycle + // Method to getting API(s) for this service + APIs() []rpc.API + // Main event loop for processing state diffs + Loop(chainEventCh chan core.ChainEvent) + // Method to subscribe to receive state diff processing output + Subscribe(id rpc.ID, sub chan<- Payload, quitChan chan<- bool, params Params) + // Method to unsubscribe from state diff processing + Unsubscribe(id rpc.ID) error + // Method to get state diff object at specific block + StateDiffAt(blockNumber uint64, params Params) (*Payload, error) + // Method to get state diff object at specific block + StateDiffFor(blockHash common.Hash, params Params) (*Payload, error) + // Method to get state trie object at specific block + StateTrieAt(blockNumber uint64, params Params) (*Payload, error) + // Method to stream out all code and codehash pairs + StreamCodeAndCodeHash(blockNumber uint64, outChan chan<- CodeAndCodeHash, quitChan chan<- bool) + // Method to write state diff object directly to DB + WriteStateDiffAt(blockNumber uint64, params Params) error + // Method to write state diff object directly to DB + WriteStateDiffFor(blockHash common.Hash, params Params) error + // Event loop for progressively processing and writing diffs directly to DB + WriteLoop(chainEventCh chan core.ChainEvent) +} + +// Wraps consructor parameters +type ServiceParams struct { + DBParams *DBParams + // Whether to enable writing state diffs directly to track blochain head + EnableWriteLoop bool + // Size of the worker pool + NumWorkers uint +} + +// Service is the underlying struct for the state diffing service +type Service struct { + // Used to sync access to the Subscriptions + sync.Mutex + // Used to build the state diff objects + Builder Builder + // Used to subscribe to chain events (blocks) + BlockChain blockChain + // Used to signal shutdown of the service + QuitChan chan bool + // A mapping of rpc.IDs to their subscription channels, mapped to their subscription type (hash of the Params rlp) + Subscriptions map[common.Hash]map[rpc.ID]Subscription + // A mapping of subscription params rlp hash to the corresponding subscription params + SubscriptionTypes map[common.Hash]Params + // Cache the last block so that we can avoid having to lookup the next block's parent + BlockCache blockCache + // Whether or not we have any subscribers; only if we do, do we processes state diffs + subscribers int32 + // Interface for publishing statediffs as PG-IPLD objects + indexer ind.Indexer + // Whether to enable writing state diffs directly to track blochain head + enableWriteLoop bool + // Size of the worker pool + numWorkers uint +} + +// Wrap the cached last block for safe access from different service loops +type blockCache struct { + sync.Mutex + blocks map[common.Hash]*types.Block + maxSize uint +} + +func NewBlockCache(max uint) blockCache { + return blockCache{ + blocks: make(map[common.Hash]*types.Block), + maxSize: max, + } +} + +// New creates a new statediff.Service +// func New(stack *node.Node, ethServ *eth.Ethereum, dbParams *DBParams, enableWriteLoop bool) error { +func New(stack *node.Node, ethServ *eth.Ethereum, cfg *ethconfig.Config, params ServiceParams) error { + blockChain := ethServ.BlockChain() + var indexer ind.Indexer + if params.DBParams != nil { + info := nodeinfo.Info{ + GenesisBlock: blockChain.Genesis().Hash().Hex(), + NetworkID: strconv.FormatUint(cfg.NetworkId, 10), + ChainID: blockChain.Config().ChainID.Uint64(), + ID: params.DBParams.ID, + ClientName: params.DBParams.ClientName, + } + + // TODO: pass max idle, open, lifetime? + db, err := postgres.NewDB(params.DBParams.ConnectionURL, postgres.ConnectionConfig{}, info) + if err != nil { + return err + } + indexer = ind.NewStateDiffIndexer(blockChain.Config(), db) + } + workers := params.NumWorkers + if workers == 0 { + workers = 1 + } + sds := &Service{ + Mutex: sync.Mutex{}, + BlockChain: blockChain, + Builder: NewBuilder(blockChain.StateCache()), + QuitChan: make(chan bool), + Subscriptions: make(map[common.Hash]map[rpc.ID]Subscription), + SubscriptionTypes: make(map[common.Hash]Params), + BlockCache: NewBlockCache(workers), + indexer: indexer, + enableWriteLoop: params.EnableWriteLoop, + numWorkers: workers, + } + stack.RegisterLifecycle(sds) + stack.RegisterAPIs(sds.APIs()) + return nil +} + +// Protocols exports the services p2p protocols, this service has none +func (sds *Service) Protocols() []p2p.Protocol { + return []p2p.Protocol{} +} + +// APIs returns the RPC descriptors the statediff.Service offers +func (sds *Service) APIs() []rpc.API { + return []rpc.API{ + { + Namespace: APIName, + Version: APIVersion, + Service: NewPublicStateDiffAPI(sds), + Public: true, + }, + } +} + +// Return the parent block of currentBlock, using the cached block if available; +// and cache the passed block +func (lbc *blockCache) getParentBlock(currentBlock *types.Block, bc blockChain) *types.Block { + lbc.Lock() + parentHash := currentBlock.ParentHash() + var parentBlock *types.Block + if block, ok := lbc.blocks[parentHash]; ok { + parentBlock = block + if len(lbc.blocks) > int(lbc.maxSize) { + delete(lbc.blocks, parentHash) + } + } else { + parentBlock = bc.GetBlockByHash(parentHash) + } + lbc.blocks[currentBlock.Hash()] = currentBlock + lbc.Unlock() + return parentBlock +} + +type workerParams struct { + chainEventCh <-chan core.ChainEvent + errCh <-chan error + wg *sync.WaitGroup + id uint +} + +func (sds *Service) WriteLoop(chainEventCh chan core.ChainEvent) { + chainEventSub := sds.BlockChain.SubscribeChainEvent(chainEventCh) + defer chainEventSub.Unsubscribe() + errCh := chainEventSub.Err() + var wg sync.WaitGroup + // Process metrics for chain events, then forward to workers + chainEventFwd := make(chan core.ChainEvent, chainEventChanSize) + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case chainEvent := <-chainEventCh: + statediffMetrics.lastEventHeight.Update(int64(chainEvent.Block.Number().Uint64())) + statediffMetrics.writeLoopChannelLen.Update(int64(len(chainEventCh))) + chainEventFwd <- chainEvent + case <-sds.QuitChan: + return + } + } + }() + wg.Add(int(sds.numWorkers)) + for worker := uint(0); worker < sds.numWorkers; worker++ { + params := workerParams{chainEventCh: chainEventFwd, errCh: errCh, wg: &wg, id: worker} + go sds.writeLoopWorker(params) + } + wg.Wait() +} + +func (sds *Service) writeGenesisStateDiff(currBlock *types.Block, workerId uint) { + // For genesis block we need to return the entire state trie hence we diff it with an empty trie. + log.Info("Writing state diff", "block height", genesisBlockNumber, "worker", workerId) + err := sds.writeStateDiff(currBlock, common.Hash{}, writeLoopParams) + if err != nil { + log.Error("statediff.Service.WriteLoop: processing error", "block height", + genesisBlockNumber, "error", err.Error(), "worker", workerId) + return + } + statediffMetrics.lastStatediffHeight.Update(genesisBlockNumber) +} + +func (sds *Service) writeLoopWorker(params workerParams) { + defer params.wg.Done() + for { + select { + //Notify chain event channel of events + case chainEvent := <-params.chainEventCh: + log.Debug("WriteLoop(): chain event received", "event", chainEvent) + currentBlock := chainEvent.Block + parentBlock := sds.BlockCache.getParentBlock(currentBlock, sds.BlockChain) + if parentBlock == nil { + log.Error("Parent block is nil, skipping this block", "block height", currentBlock.Number()) + continue + } + + // chainEvent streams block from block 1, but we also need to include data from the genesis block. + if parentBlock.Number().Uint64() == genesisBlockNumber { + sds.writeGenesisStateDiff(parentBlock, params.id) + } + + log.Info("Writing state diff", "block height", currentBlock.Number().Uint64(), "worker", params.id) + err := sds.writeStateDiff(currentBlock, parentBlock.Root(), writeLoopParams) + if err != nil { + log.Error("statediff.Service.WriteLoop: processing error", "block height", currentBlock.Number().Uint64(), "error", err.Error(), "worker", params.id) + continue + } + // TODO: how to handle with concurrent workers + statediffMetrics.lastStatediffHeight.Update(int64(currentBlock.Number().Uint64())) + case err := <-params.errCh: + log.Warn("Error from chain event subscription", "error", err, "worker", params.id) + sds.close() + return + case <-sds.QuitChan: + log.Info("Quitting the statediff writing process", "worker", params.id) + sds.close() + return + } + } +} + +// Loop is the main processing method +func (sds *Service) Loop(chainEventCh chan core.ChainEvent) { + chainEventSub := sds.BlockChain.SubscribeChainEvent(chainEventCh) + defer chainEventSub.Unsubscribe() + errCh := chainEventSub.Err() + for { + select { + //Notify chain event channel of events + case chainEvent := <-chainEventCh: + statediffMetrics.serviceLoopChannelLen.Update(int64(len(chainEventCh))) + log.Debug("Loop(): chain event received", "event", chainEvent) + // if we don't have any subscribers, do not process a statediff + if atomic.LoadInt32(&sds.subscribers) == 0 { + log.Debug("Currently no subscribers to the statediffing service; processing is halted") + continue + } + currentBlock := chainEvent.Block + parentBlock := sds.BlockCache.getParentBlock(currentBlock, sds.BlockChain) + + if parentBlock == nil { + log.Error("Parent block is nil, skipping this block", "block height", currentBlock.Number()) + continue + } + + // chainEvent streams block from block 1, but we also need to include data from the genesis block. + if parentBlock.Number().Uint64() == genesisBlockNumber { + // For genesis block we need to return the entire state trie hence we diff it with an empty trie. + sds.streamStateDiff(parentBlock, common.Hash{}) + } + + sds.streamStateDiff(currentBlock, parentBlock.Root()) + case err := <-errCh: + log.Warn("Error from chain event subscription", "error", err) + sds.close() + return + case <-sds.QuitChan: + log.Info("Quitting the statediffing process") + sds.close() + return + } + } +} + +// streamStateDiff method builds the state diff payload for each subscription according to their subscription type and sends them the result +func (sds *Service) streamStateDiff(currentBlock *types.Block, parentRoot common.Hash) { + sds.Lock() + for ty, subs := range sds.Subscriptions { + params, ok := sds.SubscriptionTypes[ty] + if !ok { + log.Error("no parameter set associated with this subscription", "subscription type", ty.Hex()) + sds.closeType(ty) + continue + } + // create payload for this subscription type + payload, err := sds.processStateDiff(currentBlock, parentRoot, params) + if err != nil { + log.Error("statediff processing error", "block height", currentBlock.Number().Uint64(), "parameters", params, "error", err.Error()) + continue + } + for id, sub := range subs { + select { + case sub.PayloadChan <- *payload: + log.Debug("sending statediff payload at head", "height", currentBlock.Number(), "subscription id", id) + default: + log.Info("unable to send statediff payload; channel has no receiver", "subscription id", id) + } + } + } + sds.Unlock() +} + +// StateDiffAt returns a state diff object payload at the specific blockheight +// This operation cannot be performed back past the point of db pruning; it requires an archival node for historical data +func (sds *Service) StateDiffAt(blockNumber uint64, params Params) (*Payload, error) { + currentBlock := sds.BlockChain.GetBlockByNumber(blockNumber) + log.Info("sending state diff", "block height", blockNumber) + if blockNumber == 0 { + return sds.processStateDiff(currentBlock, common.Hash{}, params) + } + parentBlock := sds.BlockChain.GetBlockByHash(currentBlock.ParentHash()) + return sds.processStateDiff(currentBlock, parentBlock.Root(), params) +} + +// StateDiffFor returns a state diff object payload for the specific blockhash +// This operation cannot be performed back past the point of db pruning; it requires an archival node for historical data +func (sds *Service) StateDiffFor(blockHash common.Hash, params Params) (*Payload, error) { + currentBlock := sds.BlockChain.GetBlockByHash(blockHash) + log.Info("sending state diff", "block hash", blockHash) + if currentBlock.NumberU64() == 0 { + return sds.processStateDiff(currentBlock, common.Hash{}, params) + } + parentBlock := sds.BlockChain.GetBlockByHash(currentBlock.ParentHash()) + return sds.processStateDiff(currentBlock, parentBlock.Root(), params) +} + +// processStateDiff method builds the state diff payload from the current block, parent state root, and provided params +func (sds *Service) processStateDiff(currentBlock *types.Block, parentRoot common.Hash, params Params) (*Payload, error) { + stateDiff, err := sds.Builder.BuildStateDiffObject(Args{ + NewStateRoot: currentBlock.Root(), + OldStateRoot: parentRoot, + BlockHash: currentBlock.Hash(), + BlockNumber: currentBlock.Number(), + }, params) + // allow dereferencing of parent, keep current locked as it should be the next parent + sds.BlockChain.UnlockTrie(parentRoot) + if err != nil { + return nil, err + } + stateDiffRlp, err := rlp.EncodeToBytes(stateDiff) + if err != nil { + return nil, err + } + log.Info("state diff size", "at block height", currentBlock.Number().Uint64(), "rlp byte size", len(stateDiffRlp)) + return sds.newPayload(stateDiffRlp, currentBlock, params) +} + +func (sds *Service) newPayload(stateObject []byte, block *types.Block, params Params) (*Payload, error) { + payload := &Payload{ + StateObjectRlp: stateObject, + } + if params.IncludeBlock { + blockBuff := new(bytes.Buffer) + if err := block.EncodeRLP(blockBuff); err != nil { + return nil, err + } + payload.BlockRlp = blockBuff.Bytes() + } + if params.IncludeTD { + payload.TotalDifficulty = sds.BlockChain.GetTdByHash(block.Hash()) + } + if params.IncludeReceipts { + receiptBuff := new(bytes.Buffer) + receipts := sds.BlockChain.GetReceiptsByHash(block.Hash()) + if err := rlp.Encode(receiptBuff, receipts); err != nil { + return nil, err + } + payload.ReceiptsRlp = receiptBuff.Bytes() + } + return payload, nil +} + +// StateTrieAt returns a state trie object payload at the specified blockheight +// This operation cannot be performed back past the point of db pruning; it requires an archival node for historical data +func (sds *Service) StateTrieAt(blockNumber uint64, params Params) (*Payload, error) { + currentBlock := sds.BlockChain.GetBlockByNumber(blockNumber) + log.Info("sending state trie", "block height", blockNumber) + return sds.processStateTrie(currentBlock, params) +} + +func (sds *Service) processStateTrie(block *types.Block, params Params) (*Payload, error) { + stateNodes, err := sds.Builder.BuildStateTrieObject(block) + if err != nil { + return nil, err + } + stateTrieRlp, err := rlp.EncodeToBytes(stateNodes) + if err != nil { + return nil, err + } + log.Info("state trie size", "at block height", block.Number().Uint64(), "rlp byte size", len(stateTrieRlp)) + return sds.newPayload(stateTrieRlp, block, params) +} + +// Subscribe is used by the API to subscribe to the service loop +func (sds *Service) Subscribe(id rpc.ID, sub chan<- Payload, quitChan chan<- bool, params Params) { + log.Info("Subscribing to the statediff service") + if atomic.CompareAndSwapInt32(&sds.subscribers, 0, 1) { + log.Info("State diffing subscription received; beginning statediff processing") + } + // Subscription type is defined as the hash of the rlp-serialized subscription params + by, err := rlp.EncodeToBytes(params) + if err != nil { + log.Error("State diffing params need to be rlp-serializable") + return + } + subscriptionType := crypto.Keccak256Hash(by) + // Add subscriber + sds.Lock() + if sds.Subscriptions[subscriptionType] == nil { + sds.Subscriptions[subscriptionType] = make(map[rpc.ID]Subscription) + } + sds.Subscriptions[subscriptionType][id] = Subscription{ + PayloadChan: sub, + QuitChan: quitChan, + } + sds.SubscriptionTypes[subscriptionType] = params + sds.Unlock() +} + +// Unsubscribe is used to unsubscribe from the service loop +func (sds *Service) Unsubscribe(id rpc.ID) error { + log.Info("Unsubscribing from the statediff service", "subscription id", id) + sds.Lock() + for ty := range sds.Subscriptions { + delete(sds.Subscriptions[ty], id) + if len(sds.Subscriptions[ty]) == 0 { + // If we removed the last subscription of this type, remove the subscription type outright + delete(sds.Subscriptions, ty) + delete(sds.SubscriptionTypes, ty) + } + } + if len(sds.Subscriptions) == 0 { + if atomic.CompareAndSwapInt32(&sds.subscribers, 1, 0) { + log.Info("No more subscriptions; halting statediff processing") + } + } + sds.Unlock() + return nil +} + +// Start is used to begin the service +func (sds *Service) Start() error { + log.Info("Starting statediff service") + + chainEventCh := make(chan core.ChainEvent, chainEventChanSize) + go sds.Loop(chainEventCh) + + if sds.enableWriteLoop { + log.Info("Starting statediff DB write loop", "params", writeLoopParams) + chainEventCh := make(chan core.ChainEvent, chainEventChanSize) + go sds.WriteLoop(chainEventCh) + } + + return nil +} + +// Stop is used to close down the service +func (sds *Service) Stop() error { + log.Info("Stopping statediff service") + close(sds.QuitChan) + return nil +} + +// close is used to close all listening subscriptions +func (sds *Service) close() { + sds.Lock() + for ty, subs := range sds.Subscriptions { + for id, sub := range subs { + select { + case sub.QuitChan <- true: + log.Info("closing subscription", "id", id) + default: + log.Info("unable to close subscription; channel has no receiver", "subscription id", id) + } + delete(sds.Subscriptions[ty], id) + } + delete(sds.Subscriptions, ty) + delete(sds.SubscriptionTypes, ty) + } + sds.Unlock() +} + +// closeType is used to close all subscriptions of given type +// closeType needs to be called with subscription access locked +func (sds *Service) closeType(subType common.Hash) { + subs := sds.Subscriptions[subType] + for id, sub := range subs { + sendNonBlockingQuit(id, sub) + } + delete(sds.Subscriptions, subType) + delete(sds.SubscriptionTypes, subType) +} + +func sendNonBlockingQuit(id rpc.ID, sub Subscription) { + select { + case sub.QuitChan <- true: + log.Info("closing subscription", "id", id) + default: + log.Info("unable to close subscription; channel has no receiver", "subscription id", id) + } +} + +// StreamCodeAndCodeHash subscription method for extracting all the codehash=>code mappings that exist in the trie at the provided height +func (sds *Service) StreamCodeAndCodeHash(blockNumber uint64, outChan chan<- CodeAndCodeHash, quitChan chan<- bool) { + current := sds.BlockChain.GetBlockByNumber(blockNumber) + log.Info("sending code and codehash", "block height", blockNumber) + currentTrie, err := sds.BlockChain.StateCache().OpenTrie(current.Root()) + if err != nil { + log.Error("error creating trie for block", "block height", current.Number(), "err", err) + close(quitChan) + return + } + it := currentTrie.NodeIterator([]byte{}) + leafIt := trie.NewIterator(it) + go func() { + defer close(quitChan) + for leafIt.Next() { + select { + case <-sds.QuitChan: + return + default: + } + account := new(state.Account) + if err := rlp.DecodeBytes(leafIt.Value, account); err != nil { + log.Error("error decoding state account", "err", err) + return + } + codeHash := common.BytesToHash(account.CodeHash) + code, err := sds.BlockChain.StateCache().ContractCode(common.Hash{}, codeHash) + if err != nil { + log.Error("error collecting contract code", "err", err) + return + } + outChan <- CodeAndCodeHash{ + Hash: codeHash, + Code: code, + } + } + }() +} + +// WriteStateDiffAt writes a state diff at the specific blockheight directly to the database +// This operation cannot be performed back past the point of db pruning; it requires an archival node +// for historical data +func (sds *Service) WriteStateDiffAt(blockNumber uint64, params Params) error { + currentBlock := sds.BlockChain.GetBlockByNumber(blockNumber) + parentRoot := common.Hash{} + if blockNumber != 0 { + parentBlock := sds.BlockChain.GetBlockByHash(currentBlock.ParentHash()) + parentRoot = parentBlock.Root() + } + return sds.writeStateDiff(currentBlock, parentRoot, params) +} + +// WriteStateDiffFor writes a state diff for the specific blockhash directly to the database +// This operation cannot be performed back past the point of db pruning; it requires an archival node +// for historical data +func (sds *Service) WriteStateDiffFor(blockHash common.Hash, params Params) error { + currentBlock := sds.BlockChain.GetBlockByHash(blockHash) + parentRoot := common.Hash{} + if currentBlock.NumberU64() != 0 { + parentBlock := sds.BlockChain.GetBlockByHash(currentBlock.ParentHash()) + parentRoot = parentBlock.Root() + } + return sds.writeStateDiff(currentBlock, parentRoot, params) +} + +// Writes a state diff from the current block, parent state root, and provided params +func (sds *Service) writeStateDiff(block *types.Block, parentRoot common.Hash, params Params) error { + // log.Info("Writing state diff", "block height", block.Number().Uint64()) + var totalDifficulty *big.Int + var receipts types.Receipts + var err error + var tx *ind.BlockTx + if params.IncludeTD { + totalDifficulty = sds.BlockChain.GetTdByHash(block.Hash()) + } + if params.IncludeReceipts { + receipts = sds.BlockChain.GetReceiptsByHash(block.Hash()) + } + tx, err = sds.indexer.PushBlock(block, receipts, totalDifficulty) + if err != nil { + return err + } + // defer handling of commit/rollback for any return case + defer tx.Close(err) + output := func(node StateNode) error { + return sds.indexer.PushStateNode(tx, node) + } + codeOutput := func(c CodeAndCodeHash) error { + return sds.indexer.PushCodeAndCodeHash(tx, c) + } + err = sds.Builder.WriteStateDiffObject(StateRoots{ + NewStateRoot: block.Root(), + OldStateRoot: parentRoot, + }, params, output, codeOutput) + + // allow dereferencing of parent, keep current locked as it should be the next parent + sds.BlockChain.UnlockTrie(parentRoot) + if err != nil { + return err + } + return nil +} diff --git a/statediff/service_test.go b/statediff/service_test.go new file mode 100644 index 000000000000..ca9a483a5967 --- /dev/null +++ b/statediff/service_test.go @@ -0,0 +1,291 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package statediff_test + +import ( + "bytes" + "math/big" + "math/rand" + "reflect" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/trie" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + statediff "github.com/ethereum/go-ethereum/statediff" + "github.com/ethereum/go-ethereum/statediff/testhelpers/mocks" +) + +func TestServiceLoop(t *testing.T) { + testErrorInChainEventLoop(t) + testErrorInBlockLoop(t) +} + +var ( + eventsChannel = make(chan core.ChainEvent, 1) + + parentRoot1 = common.HexToHash("0x01") + parentRoot2 = common.HexToHash("0x02") + parentHeader1 = types.Header{Number: big.NewInt(rand.Int63()), Root: parentRoot1} + parentHeader2 = types.Header{Number: big.NewInt(rand.Int63()), Root: parentRoot2} + + parentBlock1 = types.NewBlock(&parentHeader1, nil, nil, nil, new(trie.Trie)) + parentBlock2 = types.NewBlock(&parentHeader2, nil, nil, nil, new(trie.Trie)) + + parentHash1 = parentBlock1.Hash() + parentHash2 = parentBlock2.Hash() + + testRoot1 = common.HexToHash("0x03") + testRoot2 = common.HexToHash("0x04") + testRoot3 = common.HexToHash("0x04") + header1 = types.Header{ParentHash: parentHash1, Root: testRoot1, Number: big.NewInt(1)} + header2 = types.Header{ParentHash: parentHash2, Root: testRoot2, Number: big.NewInt(2)} + header3 = types.Header{ParentHash: common.HexToHash("parent hash"), Root: testRoot3, Number: big.NewInt(3)} + + testBlock1 = types.NewBlock(&header1, nil, nil, nil, new(trie.Trie)) + testBlock2 = types.NewBlock(&header2, nil, nil, nil, new(trie.Trie)) + testBlock3 = types.NewBlock(&header3, nil, nil, nil, new(trie.Trie)) + + receiptRoot1 = common.HexToHash("0x05") + receiptRoot2 = common.HexToHash("0x06") + receiptRoot3 = common.HexToHash("0x07") + testReceipts1 = []*types.Receipt{types.NewReceipt(receiptRoot1.Bytes(), false, 1000), types.NewReceipt(receiptRoot2.Bytes(), false, 2000)} + testReceipts2 = []*types.Receipt{types.NewReceipt(receiptRoot3.Bytes(), false, 3000)} + + event1 = core.ChainEvent{Block: testBlock1} + event2 = core.ChainEvent{Block: testBlock2} + event3 = core.ChainEvent{Block: testBlock3} + + defaultParams = statediff.Params{ + IncludeBlock: true, + IncludeReceipts: true, + IncludeTD: true, + } +) + +func testErrorInChainEventLoop(t *testing.T) { + //the first chain event causes and error (in blockchain mock) + builder := mocks.Builder{} + blockChain := mocks.BlockChain{} + serviceQuit := make(chan bool) + service := statediff.Service{ + Mutex: sync.Mutex{}, + Builder: &builder, + BlockChain: &blockChain, + QuitChan: serviceQuit, + Subscriptions: make(map[common.Hash]map[rpc.ID]statediff.Subscription), + SubscriptionTypes: make(map[common.Hash]statediff.Params), + BlockCache: statediff.NewBlockCache(1), + } + payloadChan := make(chan statediff.Payload, 2) + quitChan := make(chan bool) + service.Subscribe(rpc.NewID(), payloadChan, quitChan, defaultParams) + testRoot2 = common.HexToHash("0xTestRoot2") + blockMapping := make(map[common.Hash]*types.Block) + blockMapping[parentBlock1.Hash()] = parentBlock1 + blockMapping[parentBlock2.Hash()] = parentBlock2 + blockChain.SetBlocksForHashes(blockMapping) + blockChain.SetChainEvents([]core.ChainEvent{event1, event2, event3}) + blockChain.SetReceiptsForHash(testBlock1.Hash(), testReceipts1) + blockChain.SetReceiptsForHash(testBlock2.Hash(), testReceipts2) + + payloads := make([]statediff.Payload, 0, 2) + wg := new(sync.WaitGroup) + go func() { + wg.Add(1) + for i := 0; i < 2; i++ { + select { + case payload := <-payloadChan: + payloads = append(payloads, payload) + case <-quitChan: + } + } + wg.Done() + }() + service.Loop(eventsChannel) + wg.Wait() + if len(payloads) != 2 { + t.Error("Test failure:", t.Name()) + t.Logf("Actual number of payloads does not equal expected.\nactual: %+v\nexpected: 3", len(payloads)) + } + + testReceipts1Rlp, err := rlp.EncodeToBytes(testReceipts1) + if err != nil { + t.Error(err) + } + testReceipts2Rlp, err := rlp.EncodeToBytes(testReceipts2) + if err != nil { + t.Error(err) + } + expectedReceiptsRlp := [][]byte{testReceipts1Rlp, testReceipts2Rlp, nil} + for i, payload := range payloads { + if !bytes.Equal(payload.ReceiptsRlp, expectedReceiptsRlp[i]) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual receipt rlp for payload %d does not equal expected.\nactual: %+v\nexpected: %+v", i, payload.ReceiptsRlp, expectedReceiptsRlp[i]) + } + } + + if !reflect.DeepEqual(builder.Params, defaultParams) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual params does not equal expected.\nactual:%+v\nexpected: %+v", builder.Params, defaultParams) + } + if !bytes.Equal(builder.Args.BlockHash.Bytes(), testBlock2.Hash().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual blockhash does not equal expected.\nactual:%x\nexpected: %x", builder.Args.BlockHash.Bytes(), testBlock2.Hash().Bytes()) + } + if !bytes.Equal(builder.Args.OldStateRoot.Bytes(), parentBlock2.Root().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual root does not equal expected.\nactual:%x\nexpected: %x", builder.Args.OldStateRoot.Bytes(), parentBlock2.Root().Bytes()) + } + if !bytes.Equal(builder.Args.NewStateRoot.Bytes(), testBlock2.Root().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual root does not equal expected.\nactual:%x\nexpected: %x", builder.Args.NewStateRoot.Bytes(), testBlock2.Root().Bytes()) + } + //look up the parent block from its hash + expectedHashes := []common.Hash{testBlock1.ParentHash(), testBlock2.ParentHash()} + if !reflect.DeepEqual(blockChain.HashesLookedUp, expectedHashes) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual looked up parent hashes does not equal expected.\nactual:%+v\nexpected: %+v", blockChain.HashesLookedUp, expectedHashes) + } +} + +func testErrorInBlockLoop(t *testing.T) { + //second block's parent block can't be found + builder := mocks.Builder{} + blockChain := mocks.BlockChain{} + service := statediff.Service{ + Builder: &builder, + BlockChain: &blockChain, + QuitChan: make(chan bool), + Subscriptions: make(map[common.Hash]map[rpc.ID]statediff.Subscription), + SubscriptionTypes: make(map[common.Hash]statediff.Params), + BlockCache: statediff.NewBlockCache(1), + } + payloadChan := make(chan statediff.Payload) + quitChan := make(chan bool) + service.Subscribe(rpc.NewID(), payloadChan, quitChan, defaultParams) + blockMapping := make(map[common.Hash]*types.Block) + blockMapping[parentBlock1.Hash()] = parentBlock1 + blockChain.SetBlocksForHashes(blockMapping) + blockChain.SetChainEvents([]core.ChainEvent{event1, event2}) + // Need to have listeners on the channels or the subscription will be closed and the processing halted + go func() { + select { + case <-payloadChan: + case <-quitChan: + } + }() + service.Loop(eventsChannel) + if !reflect.DeepEqual(builder.Params, defaultParams) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual params does not equal expected.\nactual:%+v\nexpected: %+v", builder.Params, defaultParams) + } + if !bytes.Equal(builder.Args.BlockHash.Bytes(), testBlock1.Hash().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual blockhash does not equal expected.\nactual:%+v\nexpected: %x", builder.Args.BlockHash.Bytes(), testBlock1.Hash().Bytes()) + } + if !bytes.Equal(builder.Args.OldStateRoot.Bytes(), parentBlock1.Root().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual old state root does not equal expected.\nactual:%+v\nexpected: %x", builder.Args.OldStateRoot.Bytes(), parentBlock1.Root().Bytes()) + } + if !bytes.Equal(builder.Args.NewStateRoot.Bytes(), testBlock1.Root().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual new state root does not equal expected.\nactual:%+v\nexpected: %x", builder.Args.NewStateRoot.Bytes(), testBlock1.Root().Bytes()) + } +} + +func TestGetStateDiffAt(t *testing.T) { + testErrorInStateDiffAt(t) +} + +func testErrorInStateDiffAt(t *testing.T) { + mockStateDiff := statediff.StateObject{ + BlockNumber: testBlock1.Number(), + BlockHash: testBlock1.Hash(), + } + expectedStateDiffRlp, err := rlp.EncodeToBytes(mockStateDiff) + if err != nil { + t.Error(err) + } + expectedReceiptsRlp, err := rlp.EncodeToBytes(testReceipts1) + if err != nil { + t.Error(err) + } + expectedBlockRlp, err := rlp.EncodeToBytes(testBlock1) + if err != nil { + t.Error(err) + } + expectedStateDiffPayload := statediff.Payload{ + StateObjectRlp: expectedStateDiffRlp, + ReceiptsRlp: expectedReceiptsRlp, + BlockRlp: expectedBlockRlp, + } + expectedStateDiffPayloadRlp, err := rlp.EncodeToBytes(expectedStateDiffPayload) + if err != nil { + t.Error(err) + } + builder := mocks.Builder{} + builder.SetStateDiffToBuild(mockStateDiff) + blockChain := mocks.BlockChain{} + blockMapping := make(map[common.Hash]*types.Block) + blockMapping[parentBlock1.Hash()] = parentBlock1 + blockChain.SetBlocksForHashes(blockMapping) + blockChain.SetBlockForNumber(testBlock1, testBlock1.NumberU64()) + blockChain.SetReceiptsForHash(testBlock1.Hash(), testReceipts1) + service := statediff.Service{ + Mutex: sync.Mutex{}, + Builder: &builder, + BlockChain: &blockChain, + QuitChan: make(chan bool), + Subscriptions: make(map[common.Hash]map[rpc.ID]statediff.Subscription), + SubscriptionTypes: make(map[common.Hash]statediff.Params), + BlockCache: statediff.NewBlockCache(1), + } + stateDiffPayload, err := service.StateDiffAt(testBlock1.NumberU64(), defaultParams) + if err != nil { + t.Error(err) + } + stateDiffPayloadRlp, err := rlp.EncodeToBytes(stateDiffPayload) + if err != nil { + t.Error(err) + } + if !reflect.DeepEqual(builder.Params, defaultParams) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual params does not equal expected.\nactual:%+v\nexpected: %+v", builder.Params, defaultParams) + } + if !bytes.Equal(builder.Args.BlockHash.Bytes(), testBlock1.Hash().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual blockhash does not equal expected.\nactual:%+v\nexpected: %x", builder.Args.BlockHash.Bytes(), testBlock1.Hash().Bytes()) + } + if !bytes.Equal(builder.Args.OldStateRoot.Bytes(), parentBlock1.Root().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual old state root does not equal expected.\nactual:%+v\nexpected: %x", builder.Args.OldStateRoot.Bytes(), parentBlock1.Root().Bytes()) + } + if !bytes.Equal(builder.Args.NewStateRoot.Bytes(), testBlock1.Root().Bytes()) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual new state root does not equal expected.\nactual:%+v\nexpected: %x", builder.Args.NewStateRoot.Bytes(), testBlock1.Root().Bytes()) + } + if !bytes.Equal(expectedStateDiffPayloadRlp, stateDiffPayloadRlp) { + t.Error("Test failure:", t.Name()) + t.Logf("Actual state diff payload does not equal expected.\nactual:%+v\nexpected: %+v", expectedStateDiffPayload, stateDiffPayload) + } +} diff --git a/statediff/testhelpers/helpers.go b/statediff/testhelpers/helpers.go new file mode 100644 index 000000000000..7fd320b25bb5 --- /dev/null +++ b/statediff/testhelpers/helpers.go @@ -0,0 +1,124 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package testhelpers + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +// MakeChain creates a chain of n blocks starting at and including parent. +// the returned hash chain is ordered head->parent. +func MakeChain(n int, parent *types.Block, chainGen func(int, *core.BlockGen)) ([]*types.Block, *core.BlockChain) { + config := params.TestChainConfig + blocks, _ := core.GenerateChain(config, parent, ethash.NewFaker(), Testdb, n, chainGen) + chain, _ := core.NewBlockChain(Testdb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, nil) + return blocks, chain +} + +func TestSelfDestructChainGen(i int, block *core.BlockGen) { + signer := types.HomesteadSigner{} + switch i { + case 0: + // Block 1 is mined by Account1Addr + // Account1Addr creates a new contract + block.SetCoinbase(TestBankAddress) + tx, _ := types.SignTx(types.NewContractCreation(0, big.NewInt(0), 1000000, big.NewInt(0), ContractCode), signer, TestBankKey) + ContractAddr = crypto.CreateAddress(TestBankAddress, 0) + block.AddTx(tx) + case 1: + // Block 2 is mined by Account1Addr + // Account1Addr self-destructs the contract + block.SetCoinbase(TestBankAddress) + data := common.Hex2Bytes("43D726D6") + tx, _ := types.SignTx(types.NewTransaction(1, ContractAddr, big.NewInt(0), 100000, nil, data), signer, TestBankKey) + block.AddTx(tx) + } +} + +func TestChainGen(i int, block *core.BlockGen) { + signer := types.HomesteadSigner{} + switch i { + case 0: + // In block 1, the test bank sends account #1 some ether. + tx, _ := types.SignTx(types.NewTransaction(block.TxNonce(TestBankAddress), Account1Addr, big.NewInt(10000), params.TxGas, nil, nil), signer, TestBankKey) + block.AddTx(tx) + case 1: + // In block 2, the test bank sends some more ether to account #1. + // Account1Addr passes it on to account #2. + // Account1Addr creates a test contract. + tx1, _ := types.SignTx(types.NewTransaction(block.TxNonce(TestBankAddress), Account1Addr, big.NewInt(1000), params.TxGas, nil, nil), signer, TestBankKey) + nonce := block.TxNonce(Account1Addr) + tx2, _ := types.SignTx(types.NewTransaction(nonce, Account2Addr, big.NewInt(1000), params.TxGas, nil, nil), signer, Account1Key) + nonce++ + tx3, _ := types.SignTx(types.NewContractCreation(nonce, big.NewInt(0), 1000000, big.NewInt(0), ContractCode), signer, Account1Key) + ContractAddr = crypto.CreateAddress(Account1Addr, nonce) + block.AddTx(tx1) + block.AddTx(tx2) + block.AddTx(tx3) + case 2: + // Block 3 has a single tx from the bankAccount to the contract, that transfers no value + // Block 3 is mined by Account2Addr + block.SetCoinbase(Account2Addr) + //put function: c16431b9 + //close function: 43d726d6 + data := common.Hex2Bytes("C16431B900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003") + tx, _ := types.SignTx(types.NewTransaction(block.TxNonce(TestBankAddress), ContractAddr, big.NewInt(0), 100000, nil, data), signer, TestBankKey) + block.AddTx(tx) + case 3: + // Block 4 has three txs from bankAccount to the contract, that transfer no value + // Two set the two original slot positions to 0 and one sets another position to a new value + // Block 4 is mined by Account2Addr + block.SetCoinbase(Account2Addr) + data1 := common.Hex2Bytes("C16431B900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + data2 := common.Hex2Bytes("C16431B900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000") + data3 := common.Hex2Bytes("C16431B900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000009") + + nonce := block.TxNonce(TestBankAddress) + tx1, _ := types.SignTx(types.NewTransaction(nonce, ContractAddr, big.NewInt(0), 100000, nil, data1), signer, TestBankKey) + nonce++ + tx2, _ := types.SignTx(types.NewTransaction(nonce, ContractAddr, big.NewInt(0), 100000, nil, data2), signer, TestBankKey) + nonce++ + tx3, _ := types.SignTx(types.NewTransaction(nonce, ContractAddr, big.NewInt(0), 100000, nil, data3), signer, TestBankKey) + block.AddTx(tx1) + block.AddTx(tx2) + block.AddTx(tx3) + case 4: + // Block 5 has one tx from bankAccount to the contract, that transfers no value + // It sets the remaining storage value to zero + // Block 5 is mined by Account1Addr + block.SetCoinbase(Account1Addr) + data := common.Hex2Bytes("C16431B900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000") + nonce := block.TxNonce(TestBankAddress) + tx, _ := types.SignTx(types.NewTransaction(nonce, ContractAddr, big.NewInt(0), 100000, nil, data), signer, TestBankKey) + block.AddTx(tx) + case 5: + // Block 6 has a tx from Account1Key which self-destructs the contract, it transfers no value + // Block 6 is mined by Account2Addr + block.SetCoinbase(Account2Addr) + data := common.Hex2Bytes("43D726D6") + tx, _ := types.SignTx(types.NewTransaction(block.TxNonce(Account1Addr), ContractAddr, big.NewInt(0), 100000, nil, data), signer, Account1Key) + block.AddTx(tx) + } +} diff --git a/statediff/testhelpers/mocks/blockchain.go b/statediff/testhelpers/mocks/blockchain.go new file mode 100644 index 000000000000..b0111e64c915 --- /dev/null +++ b/statediff/testhelpers/mocks/blockchain.go @@ -0,0 +1,134 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package mocks + +import ( + "errors" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/core/state" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// BlockChain is a mock blockchain for testing +type BlockChain struct { + HashesLookedUp []common.Hash + blocksToReturnByHash map[common.Hash]*types.Block + blocksToReturnByNumber map[uint64]*types.Block + callCount int + ChainEvents []core.ChainEvent + Receipts map[common.Hash]types.Receipts + TDByHash map[common.Hash]*big.Int +} + +// SetBlocksForHashes mock method +func (blockChain *BlockChain) SetBlocksForHashes(blocks map[common.Hash]*types.Block) { + if blockChain.blocksToReturnByHash == nil { + blockChain.blocksToReturnByHash = make(map[common.Hash]*types.Block) + } + blockChain.blocksToReturnByHash = blocks +} + +// GetBlockByHash mock method +func (blockChain *BlockChain) GetBlockByHash(hash common.Hash) *types.Block { + blockChain.HashesLookedUp = append(blockChain.HashesLookedUp, hash) + + var block *types.Block + if len(blockChain.blocksToReturnByHash) > 0 { + block = blockChain.blocksToReturnByHash[hash] + } + + return block +} + +// SetChainEvents mock method +func (blockChain *BlockChain) SetChainEvents(chainEvents []core.ChainEvent) { + blockChain.ChainEvents = chainEvents +} + +// SubscribeChainEvent mock method +func (blockChain *BlockChain) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { + subErr := errors.New("subscription error") + + var eventCounter int + subscription := event.NewSubscription(func(quit <-chan struct{}) error { + for _, chainEvent := range blockChain.ChainEvents { + if eventCounter > 1 { + time.Sleep(250 * time.Millisecond) + return subErr + } + select { + case ch <- chainEvent: + case <-quit: + return nil + } + eventCounter++ + } + return nil + }) + + return subscription +} + +// SetReceiptsForHash test method +func (blockChain *BlockChain) SetReceiptsForHash(hash common.Hash, receipts types.Receipts) { + if blockChain.Receipts == nil { + blockChain.Receipts = make(map[common.Hash]types.Receipts) + } + blockChain.Receipts[hash] = receipts +} + +// GetReceiptsByHash mock method +func (blockChain *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts { + return blockChain.Receipts[hash] +} + +// SetBlockForNumber test method +func (blockChain *BlockChain) SetBlockForNumber(block *types.Block, number uint64) { + if blockChain.blocksToReturnByNumber == nil { + blockChain.blocksToReturnByNumber = make(map[uint64]*types.Block) + } + blockChain.blocksToReturnByNumber[number] = block +} + +// GetBlockByNumber mock method +func (blockChain *BlockChain) GetBlockByNumber(number uint64) *types.Block { + return blockChain.blocksToReturnByNumber[number] +} + +// GetTdByHash mock method +func (blockChain *BlockChain) GetTdByHash(hash common.Hash) *big.Int { + return blockChain.TDByHash[hash] +} + +func (blockChain *BlockChain) SetTdByHash(hash common.Hash, td *big.Int) { + if blockChain.TDByHash == nil { + blockChain.TDByHash = make(map[common.Hash]*big.Int) + } + blockChain.TDByHash[hash] = td +} + +func (blockChain *BlockChain) UnlockTrie(root common.Hash) {} + +func (BlockChain *BlockChain) StateCache() state.Database { + return nil +} diff --git a/statediff/testhelpers/mocks/builder.go b/statediff/testhelpers/mocks/builder.go new file mode 100644 index 000000000000..ff9faf3ec657 --- /dev/null +++ b/statediff/testhelpers/mocks/builder.go @@ -0,0 +1,67 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package mocks + +import ( + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/statediff" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" +) + +// Builder is a mock state diff builder +type Builder struct { + Args statediff.Args + Params statediff.Params + StateRoots statediff.StateRoots + stateDiff statediff.StateObject + block *types.Block + stateTrie statediff.StateObject + builderError error +} + +// BuildStateDiffObject mock method +func (builder *Builder) BuildStateDiffObject(args statediff.Args, params statediff.Params) (statediff.StateObject, error) { + builder.Args = args + builder.Params = params + + return builder.stateDiff, builder.builderError +} + +// BuildStateDiffObject mock method +func (builder *Builder) WriteStateDiffObject(args statediff.StateRoots, params statediff.Params, output sdtypes.StateNodeSink, codeOutput sdtypes.CodeSink) error { + builder.StateRoots = args + builder.Params = params + + return builder.builderError +} + +// BuildStateTrieObject mock method +func (builder *Builder) BuildStateTrieObject(block *types.Block) (statediff.StateObject, error) { + builder.block = block + + return builder.stateTrie, builder.builderError +} + +// SetStateDiffToBuild mock method +func (builder *Builder) SetStateDiffToBuild(stateDiff statediff.StateObject) { + builder.stateDiff = stateDiff +} + +// SetBuilderError mock method +func (builder *Builder) SetBuilderError(err error) { + builder.builderError = err +} diff --git a/statediff/testhelpers/mocks/service.go b/statediff/testhelpers/mocks/service.go new file mode 100644 index 000000000000..e4195eb10ee3 --- /dev/null +++ b/statediff/testhelpers/mocks/service.go @@ -0,0 +1,334 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package mocks + +import ( + "bytes" + "errors" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/statediff" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" +) + +// MockStateDiffService is a mock state diff service +type MockStateDiffService struct { + sync.Mutex + Builder statediff.Builder + BlockChain *BlockChain + ReturnProtocol []p2p.Protocol + ReturnAPIs []rpc.API + BlockChan chan *types.Block + ParentBlockChan chan *types.Block + QuitChan chan bool + Subscriptions map[common.Hash]map[rpc.ID]statediff.Subscription + SubscriptionTypes map[common.Hash]statediff.Params +} + +// Protocols mock method +func (sds *MockStateDiffService) Protocols() []p2p.Protocol { + return []p2p.Protocol{} +} + +// APIs mock method +func (sds *MockStateDiffService) APIs() []rpc.API { + return []rpc.API{ + { + Namespace: statediff.APIName, + Version: statediff.APIVersion, + Service: statediff.NewPublicStateDiffAPI(sds), + Public: true, + }, + } +} + +// Loop mock method +func (sds *MockStateDiffService) Loop(chan core.ChainEvent) { + //loop through chain events until no more + for { + select { + case block := <-sds.BlockChan: + currentBlock := block + parentBlock := <-sds.ParentBlockChan + parentHash := parentBlock.Hash() + if parentBlock == nil { + log.Error("Parent block is nil, skipping this block", + "parent block hash", parentHash.String(), + "current block number", currentBlock.Number()) + continue + } + sds.streamStateDiff(currentBlock, parentBlock.Root()) + case <-sds.QuitChan: + log.Debug("Quitting the statediff block channel") + sds.close() + return + } + } +} + +// streamStateDiff method builds the state diff payload for each subscription according to their subscription type and sends them the result +func (sds *MockStateDiffService) streamStateDiff(currentBlock *types.Block, parentRoot common.Hash) { + sds.Lock() + for ty, subs := range sds.Subscriptions { + params, ok := sds.SubscriptionTypes[ty] + if !ok { + log.Error(fmt.Sprintf("subscriptions type %s do not have a parameter set associated with them", ty.Hex())) + sds.closeType(ty) + continue + } + // create payload for this subscription type + payload, err := sds.processStateDiff(currentBlock, parentRoot, params) + if err != nil { + log.Error(fmt.Sprintf("statediff processing error for subscriptions with parameters: %+v", params)) + sds.closeType(ty) + continue + } + for id, sub := range subs { + select { + case sub.PayloadChan <- *payload: + log.Debug(fmt.Sprintf("sending statediff payload to subscription %s", id)) + default: + log.Info(fmt.Sprintf("unable to send statediff payload to subscription %s; channel has no receiver", id)) + } + } + } + sds.Unlock() +} + +// StateDiffAt mock method +func (sds *MockStateDiffService) StateDiffAt(blockNumber uint64, params statediff.Params) (*statediff.Payload, error) { + currentBlock := sds.BlockChain.GetBlockByNumber(blockNumber) + log.Info(fmt.Sprintf("sending state diff at %d", blockNumber)) + if blockNumber == 0 { + return sds.processStateDiff(currentBlock, common.Hash{}, params) + } + parentBlock := sds.BlockChain.GetBlockByHash(currentBlock.ParentHash()) + return sds.processStateDiff(currentBlock, parentBlock.Root(), params) +} + +// StateDiffFor mock method +func (sds *MockStateDiffService) StateDiffFor(blockHash common.Hash, params statediff.Params) (*statediff.Payload, error) { + // TODO: something useful here + return nil, nil +} + +// processStateDiff method builds the state diff payload from the current block, parent state root, and provided params +func (sds *MockStateDiffService) processStateDiff(currentBlock *types.Block, parentRoot common.Hash, params statediff.Params) (*statediff.Payload, error) { + stateDiff, err := sds.Builder.BuildStateDiffObject(statediff.Args{ + NewStateRoot: currentBlock.Root(), + OldStateRoot: parentRoot, + BlockHash: currentBlock.Hash(), + BlockNumber: currentBlock.Number(), + }, params) + if err != nil { + return nil, err + } + stateDiffRlp, err := rlp.EncodeToBytes(stateDiff) + if err != nil { + return nil, err + } + return sds.newPayload(stateDiffRlp, currentBlock, params) +} + +func (sds *MockStateDiffService) newPayload(stateObject []byte, block *types.Block, params statediff.Params) (*statediff.Payload, error) { + payload := &statediff.Payload{ + StateObjectRlp: stateObject, + } + if params.IncludeBlock { + blockBuff := new(bytes.Buffer) + if err := block.EncodeRLP(blockBuff); err != nil { + return nil, err + } + payload.BlockRlp = blockBuff.Bytes() + } + if params.IncludeTD { + payload.TotalDifficulty = sds.BlockChain.GetTdByHash(block.Hash()) + } + if params.IncludeReceipts { + receiptBuff := new(bytes.Buffer) + receipts := sds.BlockChain.GetReceiptsByHash(block.Hash()) + if err := rlp.Encode(receiptBuff, receipts); err != nil { + return nil, err + } + payload.ReceiptsRlp = receiptBuff.Bytes() + } + return payload, nil +} + +// WriteStateDiffAt mock method +func (sds *MockStateDiffService) WriteStateDiffAt(blockNumber uint64, params statediff.Params) error { + // TODO: something useful here + return nil +} + +// WriteStateDiffFor mock method +func (sds *MockStateDiffService) WriteStateDiffFor(blockHash common.Hash, params statediff.Params) error { + // TODO: something useful here + return nil +} + +// Loop mock method +func (sds *MockStateDiffService) WriteLoop(chan core.ChainEvent) { + //loop through chain events until no more + for { + select { + case block := <-sds.BlockChan: + currentBlock := block + parentBlock := <-sds.ParentBlockChan + parentHash := parentBlock.Hash() + if parentBlock == nil { + log.Error("Parent block is nil, skipping this block", + "parent block hash", parentHash.String(), + "current block number", currentBlock.Number()) + continue + } + // TODO: + // sds.writeStateDiff(currentBlock, parentBlock.Root(), statediff.Params{}) + case <-sds.QuitChan: + log.Debug("Quitting the statediff block channel") + sds.close() + return + } + } +} + +// StateTrieAt mock method +func (sds *MockStateDiffService) StateTrieAt(blockNumber uint64, params statediff.Params) (*statediff.Payload, error) { + currentBlock := sds.BlockChain.GetBlockByNumber(blockNumber) + log.Info(fmt.Sprintf("sending state trie at %d", blockNumber)) + return sds.stateTrieAt(currentBlock, params) +} + +func (sds *MockStateDiffService) stateTrieAt(block *types.Block, params statediff.Params) (*statediff.Payload, error) { + stateNodes, err := sds.Builder.BuildStateTrieObject(block) + if err != nil { + return nil, err + } + stateTrieRlp, err := rlp.EncodeToBytes(stateNodes) + if err != nil { + return nil, err + } + return sds.newPayload(stateTrieRlp, block, params) +} + +// Subscribe is used by the API to subscribe to the service loop +func (sds *MockStateDiffService) Subscribe(id rpc.ID, sub chan<- statediff.Payload, quitChan chan<- bool, params statediff.Params) { + // Subscription type is defined as the hash of the rlp-serialized subscription params + by, err := rlp.EncodeToBytes(params) + if err != nil { + return + } + subscriptionType := crypto.Keccak256Hash(by) + // Add subscriber + sds.Lock() + if sds.Subscriptions[subscriptionType] == nil { + sds.Subscriptions[subscriptionType] = make(map[rpc.ID]statediff.Subscription) + } + sds.Subscriptions[subscriptionType][id] = statediff.Subscription{ + PayloadChan: sub, + QuitChan: quitChan, + } + sds.SubscriptionTypes[subscriptionType] = params + sds.Unlock() +} + +// Unsubscribe is used to unsubscribe from the service loop +func (sds *MockStateDiffService) Unsubscribe(id rpc.ID) error { + sds.Lock() + for ty := range sds.Subscriptions { + delete(sds.Subscriptions[ty], id) + if len(sds.Subscriptions[ty]) == 0 { + // If we removed the last subscription of this type, remove the subscription type outright + delete(sds.Subscriptions, ty) + delete(sds.SubscriptionTypes, ty) + } + } + sds.Unlock() + return nil +} + +// close is used to close all listening subscriptions +func (sds *MockStateDiffService) close() { + sds.Lock() + for ty, subs := range sds.Subscriptions { + for id, sub := range subs { + select { + case sub.QuitChan <- true: + log.Info(fmt.Sprintf("closing subscription %s", id)) + default: + log.Info(fmt.Sprintf("unable to close subscription %s; channel has no receiver", id)) + } + delete(sds.Subscriptions[ty], id) + } + delete(sds.Subscriptions, ty) + delete(sds.SubscriptionTypes, ty) + } + sds.Unlock() +} + +// Start mock method +func (sds *MockStateDiffService) Start() error { + log.Info("Starting mock statediff service") + if sds.ParentBlockChan == nil || sds.BlockChan == nil { + return errors.New("MockStateDiffingService needs to be configured with a MockParentBlockChan and MockBlockChan") + } + chainEventCh := make(chan core.ChainEvent, 10) + go sds.Loop(chainEventCh) + + return nil +} + +// Stop mock method +func (sds *MockStateDiffService) Stop() error { + log.Info("Stopping mock statediff service") + close(sds.QuitChan) + return nil +} + +// closeType is used to close all subscriptions of given type +// closeType needs to be called with subscription access locked +func (sds *MockStateDiffService) closeType(subType common.Hash) { + subs := sds.Subscriptions[subType] + for id, sub := range subs { + sendNonBlockingQuit(id, sub) + } + delete(sds.Subscriptions, subType) + delete(sds.SubscriptionTypes, subType) +} + +func (sds *MockStateDiffService) StreamCodeAndCodeHash(blockNumber uint64, outChan chan<- sdtypes.CodeAndCodeHash, quitChan chan<- bool) { + panic("implement me") +} + +func sendNonBlockingQuit(id rpc.ID, sub statediff.Subscription) { + select { + case sub.QuitChan <- true: + log.Info(fmt.Sprintf("closing subscription %s", id)) + default: + log.Info("unable to close subscription %s; channel has no receiver", id) + } +} diff --git a/statediff/testhelpers/mocks/service_test.go b/statediff/testhelpers/mocks/service_test.go new file mode 100644 index 000000000000..e618268fd32c --- /dev/null +++ b/statediff/testhelpers/mocks/service_test.go @@ -0,0 +1,247 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package mocks + +import ( + "bytes" + "fmt" + "math/big" + "os" + "sort" + "sync" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/statediff" + "github.com/ethereum/go-ethereum/statediff/testhelpers" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" +) + +var ( + emptyStorage = make([]sdtypes.StorageNode, 0) + block0, block1 *types.Block + minerLeafKey = testhelpers.AddressToLeafKey(common.HexToAddress("0x0")) + account1, _ = rlp.EncodeToBytes(state.Account{ + Nonce: uint64(0), + Balance: big.NewInt(10000), + CodeHash: common.HexToHash("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").Bytes(), + Root: common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + }) + account1LeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3926db69aaced518e9b9f0f434a473e7174109c943548bb8f23be41ca76d9ad2"), + account1, + }) + minerAccount, _ = rlp.EncodeToBytes(state.Account{ + Nonce: uint64(0), + Balance: big.NewInt(2000000000000000000), + CodeHash: common.HexToHash("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").Bytes(), + Root: common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + }) + minerAccountLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("3380c7b7ae81a58eb98d9c78de4a1fd7fd9535fc953ed2be602daaa41767312a"), + minerAccount, + }) + bankAccount, _ = rlp.EncodeToBytes(state.Account{ + Nonce: uint64(1), + Balance: big.NewInt(testhelpers.TestBankFunds.Int64() - 10000), + CodeHash: common.HexToHash("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").Bytes(), + Root: common.HexToHash("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + }) + bankAccountLeafNode, _ = rlp.EncodeToBytes([]interface{}{ + common.Hex2Bytes("30bf49f440a1cd0527e4d06e2765654c0f56452257516d793a9b8d604dcfdf2a"), + bankAccount, + }) + mockTotalDifficulty = big.NewInt(1337) + params = statediff.Params{ + IntermediateStateNodes: false, + IncludeTD: true, + IncludeBlock: true, + IncludeReceipts: true, + } +) + +func init() { + if os.Getenv("MODE") != "statediff" { + fmt.Println("Skipping statediff test") + os.Exit(0) + } +} + +func TestAPI(t *testing.T) { + testSubscriptionAPI(t) + testHTTPAPI(t) +} + +func testSubscriptionAPI(t *testing.T) { + blocks, chain := testhelpers.MakeChain(1, testhelpers.Genesis, testhelpers.TestChainGen) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + expectedBlockRlp, _ := rlp.EncodeToBytes(block1) + mockReceipt := &types.Receipt{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + } + expectedReceiptBytes, _ := rlp.EncodeToBytes(types.Receipts{mockReceipt}) + expectedStateDiff := statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountLeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountLeafNode, + StorageNodes: emptyStorage, + }, + }, + } + expectedStateDiffBytes, _ := rlp.EncodeToBytes(expectedStateDiff) + blockChan := make(chan *types.Block) + parentBlockChain := make(chan *types.Block) + serviceQuitChan := make(chan bool) + mockBlockChain := &BlockChain{} + mockBlockChain.SetReceiptsForHash(block1.Hash(), types.Receipts{mockReceipt}) + mockBlockChain.SetTdByHash(block1.Hash(), mockTotalDifficulty) + mockService := MockStateDiffService{ + Mutex: sync.Mutex{}, + Builder: statediff.NewBuilder(chain.StateCache()), + BlockChan: blockChan, + BlockChain: mockBlockChain, + ParentBlockChan: parentBlockChain, + QuitChan: serviceQuitChan, + Subscriptions: make(map[common.Hash]map[rpc.ID]statediff.Subscription), + SubscriptionTypes: make(map[common.Hash]statediff.Params), + } + mockService.Start() + id := rpc.NewID() + payloadChan := make(chan statediff.Payload) + quitChan := make(chan bool) + mockService.Subscribe(id, payloadChan, quitChan, params) + blockChan <- block1 + parentBlockChain <- block0 + + sort.Slice(expectedStateDiffBytes, func(i, j int) bool { return expectedStateDiffBytes[i] < expectedStateDiffBytes[j] }) + select { + case payload := <-payloadChan: + if !bytes.Equal(payload.BlockRlp, expectedBlockRlp) { + t.Errorf("payload does not have expected block\r\nactual block rlp: %v\r\nexpected block rlp: %v", payload.BlockRlp, expectedBlockRlp) + } + sort.Slice(payload.StateObjectRlp, func(i, j int) bool { return payload.StateObjectRlp[i] < payload.StateObjectRlp[j] }) + if !bytes.Equal(payload.StateObjectRlp, expectedStateDiffBytes) { + t.Errorf("payload does not have expected state diff\r\nactual state diff rlp: %v\r\nexpected state diff rlp: %v", payload.StateObjectRlp, expectedStateDiffBytes) + } + if !bytes.Equal(expectedReceiptBytes, payload.ReceiptsRlp) { + t.Errorf("payload does not have expected receipts\r\nactual receipt rlp: %v\r\nexpected receipt rlp: %v", payload.ReceiptsRlp, expectedReceiptBytes) + } + if !bytes.Equal(payload.TotalDifficulty.Bytes(), mockTotalDifficulty.Bytes()) { + t.Errorf("payload does not have expected total difficulty\r\nactual td: %d\r\nexpected td: %d", payload.TotalDifficulty.Int64(), mockTotalDifficulty.Int64()) + } + case <-quitChan: + t.Errorf("channel quit before delivering payload") + } +} + +func testHTTPAPI(t *testing.T) { + blocks, chain := testhelpers.MakeChain(1, testhelpers.Genesis, testhelpers.TestChainGen) + defer chain.Stop() + block0 = testhelpers.Genesis + block1 = blocks[0] + expectedBlockRlp, _ := rlp.EncodeToBytes(block1) + mockReceipt := &types.Receipt{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + } + expectedReceiptBytes, _ := rlp.EncodeToBytes(types.Receipts{mockReceipt}) + expectedStateDiff := statediff.StateObject{ + BlockNumber: block1.Number(), + BlockHash: block1.Hash(), + Nodes: []sdtypes.StateNode{ + { + Path: []byte{'\x05'}, + NodeType: sdtypes.Leaf, + LeafKey: minerLeafKey, + NodeValue: minerAccountLeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x0e'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.Account1LeafKey, + NodeValue: account1LeafNode, + StorageNodes: emptyStorage, + }, + { + Path: []byte{'\x00'}, + NodeType: sdtypes.Leaf, + LeafKey: testhelpers.BankLeafKey, + NodeValue: bankAccountLeafNode, + StorageNodes: emptyStorage, + }, + }, + } + expectedStateDiffBytes, _ := rlp.EncodeToBytes(expectedStateDiff) + mockBlockChain := &BlockChain{} + mockBlockChain.SetBlocksForHashes(map[common.Hash]*types.Block{ + block0.Hash(): block0, + block1.Hash(): block1, + }) + mockBlockChain.SetBlockForNumber(block1, block1.Number().Uint64()) + mockBlockChain.SetReceiptsForHash(block1.Hash(), types.Receipts{mockReceipt}) + mockBlockChain.SetTdByHash(block1.Hash(), big.NewInt(1337)) + mockService := MockStateDiffService{ + Mutex: sync.Mutex{}, + Builder: statediff.NewBuilder(chain.StateCache()), + BlockChain: mockBlockChain, + } + payload, err := mockService.StateDiffAt(block1.Number().Uint64(), params) + if err != nil { + t.Error(err) + } + sort.Slice(payload.StateObjectRlp, func(i, j int) bool { return payload.StateObjectRlp[i] < payload.StateObjectRlp[j] }) + sort.Slice(expectedStateDiffBytes, func(i, j int) bool { return expectedStateDiffBytes[i] < expectedStateDiffBytes[j] }) + if !bytes.Equal(payload.BlockRlp, expectedBlockRlp) { + t.Errorf("payload does not have expected block\r\nactual block rlp: %v\r\nexpected block rlp: %v", payload.BlockRlp, expectedBlockRlp) + } + if !bytes.Equal(payload.StateObjectRlp, expectedStateDiffBytes) { + t.Errorf("payload does not have expected state diff\r\nactual state diff rlp: %v\r\nexpected state diff rlp: %v", payload.StateObjectRlp, expectedStateDiffBytes) + } + if !bytes.Equal(expectedReceiptBytes, payload.ReceiptsRlp) { + t.Errorf("payload does not have expected receipts\r\nactual receipt rlp: %v\r\nexpected receipt rlp: %v", payload.ReceiptsRlp, expectedReceiptBytes) + } + if !bytes.Equal(payload.TotalDifficulty.Bytes(), mockTotalDifficulty.Bytes()) { + t.Errorf("paylaod does not have the expected total difficulty\r\nactual td: %d\r\nexpected td: %d", payload.TotalDifficulty.Int64(), mockTotalDifficulty.Int64()) + } +} diff --git a/statediff/testhelpers/test_data.go b/statediff/testhelpers/test_data.go new file mode 100644 index 000000000000..cc5374da9730 --- /dev/null +++ b/statediff/testhelpers/test_data.go @@ -0,0 +1,73 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package testhelpers + +import ( + "math/big" + "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +// AddressToLeafKey hashes an returns an address +func AddressToLeafKey(address common.Address) []byte { + return crypto.Keccak256(address[:]) +} + +// AddressToEncodedPath hashes an address and appends the even-number leaf flag to it +func AddressToEncodedPath(address common.Address) []byte { + addrHash := crypto.Keccak256(address[:]) + decodedPath := append(EvenLeafFlag, addrHash...) + return decodedPath +} + +// Test variables +var ( + EvenLeafFlag = []byte{byte(2) << 4} + BlockNumber = big.NewInt(rand.Int63()) + BlockHash = "0xfa40fbe2d98d98b3363a778d52f2bcd29d6790b9b3f3cab2b167fd12d3550f73" + NullCodeHash = crypto.Keccak256Hash([]byte{}) + StoragePath = common.HexToHash("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").Bytes() + StorageKey = common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001").Bytes() + StorageValue = common.Hex2Bytes("0x03") + NullHash = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000") + + Testdb = rawdb.NewMemoryDatabase() + TestBankKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + TestBankAddress = crypto.PubkeyToAddress(TestBankKey.PublicKey) //0x71562b71999873DB5b286dF957af199Ec94617F7 + BankLeafKey = AddressToLeafKey(TestBankAddress) + TestBankFunds = big.NewInt(100000000) + Genesis = core.GenesisBlockForTesting(Testdb, TestBankAddress, TestBankFunds) + + Account1Key, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a") + Account2Key, _ = crypto.HexToECDSA("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee") + Account1Addr = crypto.PubkeyToAddress(Account1Key.PublicKey) //0x703c4b2bD70c169f5717101CaeE543299Fc946C7 + Account2Addr = crypto.PubkeyToAddress(Account2Key.PublicKey) //0x0D3ab14BBaD3D99F4203bd7a11aCB94882050E7e + Account1LeafKey = AddressToLeafKey(Account1Addr) + Account2LeafKey = AddressToLeafKey(Account2Addr) + ContractCode = common.Hex2Bytes("608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506040518060200160405280600160ff16815250600190600161007492919061007a565b506100e4565b82606481019282156100ae579160200282015b828111156100ad578251829060ff1690559160200191906001019061008d565b5b5090506100bb91906100bf565b5090565b6100e191905b808211156100dd5760008160009055506001016100c5565b5090565b90565b6101ca806100f36000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806343d726d61461003b578063c16431b914610045575b600080fd5b61004361007d565b005b61007b6004803603604081101561005b57600080fd5b81019080803590602001909291908035906020019092919050505061015c565b005b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610122576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101746022913960400191505060405180910390fd5b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b806001836064811061016a57fe5b0181905550505056fe4f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6e2ea265627a7a72305820e3747183708fb6bff3f6f7a80fb57dcc1c19f83f9cb25457a3ed5c0424bde66864736f6c634300050a0032") + ByteCodeAfterDeployment = common.Hex2Bytes("608060405234801561001057600080fd5b50600436106100365760003560e01c806343d726d61461003b578063c16431b914610045575b600080fd5b61004361007d565b005b61007b6004803603604081101561005b57600080fd5b81019080803590602001909291908035906020019092919050505061015c565b005b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610122576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101746022913960400191505060405180910390fd5b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b806001836064811061016a57fe5b0181905550505056fe4f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6e2ea265627a7a72305820e3747183708fb6bff3f6f7a80fb57dcc1c19f83f9cb25457a3ed5c0424bde66864736f6c634300050a0032") + CodeHash = common.HexToHash("0xaaea5efba4fd7b45d7ec03918ac5d8b31aa93b48986af0e6b591f0f087c80127") + ContractAddr common.Address + + EmptyRootNode, _ = rlp.EncodeToBytes([]byte{}) + EmptyContractRoot = crypto.Keccak256Hash(EmptyRootNode) +) diff --git a/statediff/types.go b/statediff/types.go new file mode 100644 index 000000000000..148567dd7df8 --- /dev/null +++ b/statediff/types.go @@ -0,0 +1,113 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains a batch of utility type declarations used by the tests. As the node +// operates on unique types, a lot of them are needed to check various features. + +package statediff + +import ( + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/statediff/types" +) + +// Subscription struct holds our subscription channels +type Subscription struct { + PayloadChan chan<- Payload + QuitChan chan<- bool +} + +// DBParams holds params for Postgres db connection +type DBParams struct { + ConnectionURL string + ID string + ClientName string +} + +// Params is used to carry in parameters from subscribing/requesting clients configuration +type Params struct { + IntermediateStateNodes bool + IntermediateStorageNodes bool + IncludeBlock bool + IncludeReceipts bool + IncludeTD bool + IncludeCode bool + WatchedAddresses []common.Address + WatchedStorageSlots []common.Hash +} + +// Args bundles the arguments for the state diff builder +type Args struct { + OldStateRoot, NewStateRoot, BlockHash common.Hash + BlockNumber *big.Int +} + +type StateRoots struct { + OldStateRoot, NewStateRoot common.Hash +} + +// Payload packages the data to send to statediff subscriptions +type Payload struct { + BlockRlp []byte `json:"blockRlp"` + TotalDifficulty *big.Int `json:"totalDifficulty"` + ReceiptsRlp []byte `json:"receiptsRlp"` + StateObjectRlp []byte `json:"stateObjectRlp" gencodec:"required"` + + encoded []byte + err error +} + +func (sd *Payload) ensureEncoded() { + if sd.encoded == nil && sd.err == nil { + sd.encoded, sd.err = json.Marshal(sd) + } +} + +// Length to implement Encoder interface for Payload +func (sd *Payload) Length() int { + sd.ensureEncoded() + return len(sd.encoded) +} + +// Encode to implement Encoder interface for Payload +func (sd *Payload) Encode() ([]byte, error) { + sd.ensureEncoded() + return sd.encoded, sd.err +} + +// StateObject is the final output structure from the builder +type StateObject struct { + BlockNumber *big.Int `json:"blockNumber" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Nodes []types.StateNode `json:"nodes" gencodec:"required"` + CodeAndCodeHashes []types.CodeAndCodeHash `json:"codeMapping"` +} + +// AccountMap is a mapping of hex encoded path => account wrapper +type AccountMap map[string]accountWrapper + +// accountWrapper is used to temporary associate the unpacked node with its raw values +type accountWrapper struct { + Account *state.Account + NodeType types.NodeType + Path []byte + NodeValue []byte + LeafKey []byte +} diff --git a/statediff/types/types.go b/statediff/types/types.go new file mode 100644 index 000000000000..08e2124fa5c7 --- /dev/null +++ b/statediff/types/types.go @@ -0,0 +1,61 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains a batch of utility type declarations used by the tests. As the node +// operates on unique types, a lot of them are needed to check various features. + +package types + +import "github.com/ethereum/go-ethereum/common" + +// NodeType for explicitly setting type of node +type NodeType string + +const ( + Unknown NodeType = "Unknown" + Leaf NodeType = "Leaf" + Extension NodeType = "Extension" + Branch NodeType = "Branch" + Removed NodeType = "Removed" // used to represent pathes which have been emptied +) + +// StateNode holds the data for a single state diff node +type StateNode struct { + NodeType NodeType `json:"nodeType" gencodec:"required"` + Path []byte `json:"path" gencodec:"required"` + NodeValue []byte `json:"value" gencodec:"required"` + StorageNodes []StorageNode `json:"storage"` + LeafKey []byte `json:"leafKey"` +} + +// StorageNode holds the data for a single storage diff node +type StorageNode struct { + NodeType NodeType `json:"nodeType" gencodec:"required"` + Path []byte `json:"path" gencodec:"required"` + NodeValue []byte `json:"value" gencodec:"required"` + LeafKey []byte `json:"leafKey"` +} + +// CodeAndCodeHash struct for holding codehash => code mappings +// we can't use an actual map because they are not rlp serializable +type CodeAndCodeHash struct { + Hash common.Hash `json:"codeHash"` + Code []byte `json:"code"` +} + +type StateNodeSink func(StateNode) error +type StorageNodeSink func(StorageNode) error +type CodeSink func(CodeAndCodeHash) error diff --git a/trie/encoding.go b/trie/encoding.go index 8ee0022ef3a0..ace45700cde0 100644 --- a/trie/encoding.go +++ b/trie/encoding.go @@ -34,6 +34,11 @@ package trie // in the case of an odd number. All remaining nibbles (now an even number) fit properly // into the remaining bytes. Compact encoding is used for nodes stored on disk. +// HexToCompact converts a hex path to the compact encoded format +func HexToCompact(hex []byte) []byte { + return hexToCompact(hex) +} + func hexToCompact(hex []byte) []byte { terminator := byte(0) if hasTerm(hex) { @@ -80,6 +85,11 @@ func hexToCompactInPlace(hex []byte) int { return binLen } +// CompactToHex converts a compact encoded path to hex format +func CompactToHex(compact []byte) []byte { + return compactToHex(compact) +} + func compactToHex(compact []byte) []byte { if len(compact) == 0 { return compact @@ -105,9 +115,9 @@ func keybytesToHex(str []byte) []byte { return nibbles } -// hexToKeybytes turns hex nibbles into key bytes. +// hexToKeyBytes turns hex nibbles into key bytes. // This can only be used for keys of even length. -func hexToKeybytes(hex []byte) []byte { +func hexToKeyBytes(hex []byte) []byte { if hasTerm(hex) { hex = hex[:len(hex)-1] } diff --git a/trie/encoding_test.go b/trie/encoding_test.go index 16393313f743..7ade0a095dcb 100644 --- a/trie/encoding_test.go +++ b/trie/encoding_test.go @@ -71,8 +71,8 @@ func TestHexKeybytes(t *testing.T) { if h := keybytesToHex(test.key); !bytes.Equal(h, test.hexOut) { t.Errorf("keybytesToHex(%x) -> %x, want %x", test.key, h, test.hexOut) } - if k := hexToKeybytes(test.hexIn); !bytes.Equal(k, test.key) { - t.Errorf("hexToKeybytes(%x) -> %x, want %x", test.hexIn, k, test.key) + if k := hexToKeyBytes(test.hexIn); !bytes.Equal(k, test.key) { + t.Errorf("hexToKeyBytes(%x) -> %x, want %x", test.hexIn, k, test.key) } } } @@ -135,6 +135,6 @@ func BenchmarkKeybytesToHex(b *testing.B) { func BenchmarkHexToKeybytes(b *testing.B) { testBytes := []byte{7, 6, 6, 5, 7, 2, 6, 2, 16} for i := 0; i < b.N; i++ { - hexToKeybytes(testBytes) + hexToKeyBytes(testBytes) } } diff --git a/trie/iterator.go b/trie/iterator.go index 406f216c2296..da63a2eec638 100644 --- a/trie/iterator.go +++ b/trie/iterator.go @@ -184,7 +184,7 @@ func (it *nodeIterator) Leaf() bool { func (it *nodeIterator) LeafKey() []byte { if len(it.stack) > 0 { if _, ok := it.stack[len(it.stack)-1].node.(valueNode); ok { - return hexToKeybytes(it.path) + return hexToKeyBytes(it.path) } } panic("not at leaf") diff --git a/trie/sync.go b/trie/sync.go index 3a6076ff8f7a..7c19312c391a 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -82,7 +82,7 @@ func newSyncPath(path []byte) SyncPath { if len(path) < 64 { return SyncPath{hexToCompact(path)} } - return SyncPath{hexToKeybytes(path[:64]), hexToCompact(path[64:])} + return SyncPath{hexToKeyBytes(path[:64]), hexToCompact(path[64:])} } // SyncResult is a response with requested data along with it's hash. @@ -400,10 +400,10 @@ func (s *Sync) children(req *request, object node) ([]*request, error) { if node, ok := (child.node).(valueNode); ok { var paths [][]byte if len(child.path) == 2*common.HashLength { - paths = append(paths, hexToKeybytes(child.path)) + paths = append(paths, hexToKeyBytes(child.path)) } else if len(child.path) == 4*common.HashLength { - paths = append(paths, hexToKeybytes(child.path[:2*common.HashLength])) - paths = append(paths, hexToKeybytes(child.path[2*common.HashLength:])) + paths = append(paths, hexToKeyBytes(child.path[:2*common.HashLength])) + paths = append(paths, hexToKeyBytes(child.path[2*common.HashLength:])) } if err := req.callback(paths, child.path, node, req.hash); err != nil { return nil, err