From 9f3e4f1102d7775c12a5feeb44481f23854e79f8 Mon Sep 17 00:00:00 2001 From: Ian Norden Date: Sat, 20 Jun 2020 10:55:34 -0500 Subject: [PATCH] finish updating documentation after refactor --- README.md | 318 ++++++++++++------ documentation/{super_node => }/apis.md | 82 ++--- documentation/architecture.md | 136 ++++++++ documentation/contributing.md | 52 --- documentation/custom-transformers.md | 212 ------------ documentation/data-syncing.md | 27 -- documentation/diagrams/vdb-overview.png | Bin 53018 -> 0 bytes documentation/generic-transformer.md | 160 --------- documentation/postgraphile.md | 34 -- documentation/{super_node => }/resync.md | 10 +- documentation/super_node/architecture.md | 138 -------- documentation/super_node/setup.md | 160 --------- documentation/{super_node => }/watcher.md | 0 .../integration_test_suite_test.go | 36 -- pkg/client/eth_client.go | 63 ---- test_config/test_config.go | 112 ------ 16 files changed, 390 insertions(+), 1150 deletions(-) rename documentation/{super_node => }/apis.md (66%) create mode 100644 documentation/architecture.md delete mode 100644 documentation/contributing.md delete mode 100644 documentation/custom-transformers.md delete mode 100644 documentation/data-syncing.md delete mode 100644 documentation/diagrams/vdb-overview.png delete mode 100644 documentation/generic-transformer.md delete mode 100644 documentation/postgraphile.md rename documentation/{super_node => }/resync.md (88%) delete mode 100644 documentation/super_node/architecture.md delete mode 100644 documentation/super_node/setup.md rename documentation/{super_node => }/watcher.md (100%) delete mode 100644 integration_test/integration_test_suite_test.go delete mode 100644 pkg/client/eth_client.go delete mode 100644 test_config/test_config.go diff --git a/README.md b/README.md index fe2c90b72..e99f426dc 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,40 @@ # Vulcanize DB -[![Build Status](https://travis-ci.org/vulcanize/vulcanizedb.svg?branch=master)](https://travis-ci.org/vulcanize/vulcanizedb) -[![Go Report Card](https://goreportcard.com/badge/github.com/vulcanize/ipfs-chain-watcher)](https://goreportcard.com/report/github.com/vulcanize/ipfs-chain-watcher) - -> Vulcanize DB is a set of tools that make it easier for developers to write application-specific indexes and caches for dapps built on Ethereum. +[![Go Report Card](https://goreportcard.com/badge/github.com/vulcanize/ipfs-blockchain-watcher)](https://goreportcard.com/report/github.com/vulcanize/ipfs-blockchain-watcher) +> Tool for extracting and indexing blockchain data on PG-IPFS ## Table of Contents 1. [Background](#background) +1. [Architecture](#architecture) 1. [Install](#install) 1. [Usage](#usage) 1. [Contributing](#contributing) 1. [License](#license) - ## Background -The same data structures and encodings that make Ethereum an effective and trust-less distributed virtual machine -complicate data accessibility and usability for dApp developers. VulcanizeDB improves Ethereum data accessibility by -providing a suite of tools to ease the extraction and transformation of data into a more useful state, including -allowing for exposing aggregate data from a suite of smart contracts. +ipfs-blockchain-watcher is a collection of interfaces that are used to extract, process, and store in Postgres-IPFS +all chain data. The raw data indexed by ipfs-blockchain-watcher serves as the basis for more specific watchers and applications. -VulanizeDB includes processes that sync, transform and expose data. Syncing involves -querying an Ethereum node and then persisting core data into a Postgres database. Transforming focuses on using previously synced data to -query for and transform log event and storage data for specifically configured smart contract addresses. Exposing data is a matter of getting -data from VulcanizeDB's underlying Postgres database and making it accessible. +Currently the service supports complete processing of all Bitcoin and Ethereum data. -![VulcanizeDB Overview Diagram](documentation/diagrams/vdb-overview.png) +## Architecture +More details on the design of ipfs-blockchain-watcher can be found in [here](./documentation/architecture.md) ## Install - -1. [Dependencies](#dependencies) -1. [Building the project](#building-the-project) -1. [Setting up the database](#setting-up-the-database) -1. [Configuring a synced Ethereum node](#configuring-a-synced-ethereum-node) - -### Dependencies - - Go 1.12+ - - Postgres 11.2 - - Ethereum Node - - [Go Ethereum](https://ethereum.github.io/go-ethereum/downloads/) (1.8.23+) - - [Parity 1.8.11+](https://github.com/paritytech/parity/releases) - -### Building the project -Download the codebase to your local `GOPATH` via: - -`go get github.com/vulcanize/ipfs-chain-watcher` - -Move to the project directory: - -`cd $GOPATH/src/github.com/vulcanize/ipfs-chain-watcher` - -Be sure you have enabled Go Modules (`export GO111MODULE=on`), and build the executable with: - -`make build` - -If you need to use a different dependency than what is currently defined in `go.mod`, it may helpful to look into [the replace directive](https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive). -This instruction enables you to point at a fork or the local filesystem for dependency resolution. - -If you are running into issues at this stage, ensure that `GOPATH` is defined in your shell. -If necessary, `GOPATH` can be set in `~/.bashrc` or `~/.bash_profile`, depending upon your system. -It can be additionally helpful to add `$GOPATH/bin` to your shell's `$PATH`. - -### Setting up the database -1. Install Postgres +1. [Postgres](#postgres) +1. [Goose](#goose) +1. [IPFS](#ipfs) +1. [Blockchain](#blockchain) +1. [Watcher](#watcher) + +### Postgres +1. [Install Postgres](https://wiki.postgresql.org/wiki/Detailed_installation_guides) 1. Create a superuser for yourself and make sure `psql --list` works without prompting for a password. 1. `createdb vulcanize_public` -1. `cd $GOPATH/src/github.com/vulcanize/ipfs-chain-watcher` +1. `cd $GOPATH/src/github.com/vulcanize/ipfs-blockchain-watcher` 1. Run the migrations: `make migrate HOST_NAME=localhost NAME=vulcanize_public PORT=5432` - - There is an optional var `USER=username` if the database user is not the default user `postgres` + - There are optional vars `USER=username` and `PASS=password` if the database user is not the default user `postgres` and/or a password is present - To rollback a single step: `make rollback NAME=vulcanize_public` - To rollback to a certain migration: `make rollback_to MIGRATION=n NAME=vulcanize_public` - To see status of migrations: `make migration_status NAME=vulcanize_public` @@ -79,76 +46,207 @@ localhost. To allow access on Ubuntu, set localhost connections via hostname, ip (It should be noted that trusted auth should only be enabled on systems without sensitive data in them: development and local test databases) -### Configuring a synced Ethereum node -- To use a local Ethereum node, copy `environments/public.toml.example` to - `environments/public.toml` and update the `ipcPath` and `levelDbPath`. - - `ipcPath` should match the local node's IPC filepath: - - For Geth: - - The IPC file is called `geth.ipc`. - - The geth IPC file path is printed to the console when you start geth. - - The default location is: - - Mac: `/Library/Ethereum/geth.ipc` - - Linux: `/ethereum/geth.ipc` - - Note: the geth.ipc file may not exist until you've started the geth process - - - For Parity: - - The IPC file is called `jsonrpc.ipc`. - - The default location is: - - Mac: `/Library/Application\ Support/io.parity.ethereum/` - - Linux: `/local/share/io.parity.ethereum/` - - - `levelDbPath` should match Geth's chaindata directory path. - - The geth LevelDB chaindata path is printed to the console when you start geth. - - The default location is: - - Mac: `/Library/Ethereum/geth/chaindata` - - Linux: `/ethereum/geth/chaindata` - - `levelDbPath` is irrelevant (and `coldImport` is currently unavailable) if only running parity. +### Goose +We use [goose](https://github.com/pressly/goose) as our migration management tool. While it is not necessary to use `goose` for manual setup, it +is required for running the automated tests. +### IPFS +We use IPFS to store IPLD objects for each type of data we extract from on chain. -## Usage -As mentioned above, VulcanizeDB's processes can be split into three categories: syncing, transforming and exposing data. - -### Data syncing -To provide data for transformations, raw Ethereum data must first be synced into VulcanizeDB. -This is accomplished through the use of the `headerSync` command. -These commands are described in detail [here](documentation/data-syncing.md). - -### Data transformation -Data transformation uses the raw data that has been synced into Postgres to filter out and apply transformations to -specific data of interest. Since there are different types of data that may be useful for observing smart contracts, it -follows that there are different ways to transform this data. We've started by categorizing this into Generic and -Custom transformers: - -- Generic Contract Transformer: Generic contract transformation can be done using a built-in command, -`contractWatcher`, which transforms contract events provided the contract's ABI is available. It also -provides some state variable coverage by automating polling of public methods, with some restrictions. -`contractWatcher` is described further [here](documentation/generic-transformer.md). - -- Custom Transformers: In many cases custom transformers will need to be written to provide -more comprehensive coverage of contract data. In this case we have provided the `compose`, `execute`, and -`composeAndExecute` commands for running custom transformers from external repositories. Documentation on how to write, -build and run custom transformers as Go plugins can be found -[here](documentation/custom-transformers.md). +To start, download and install [IPFS](https://github.com/vulcanize/go-ipfs): -### Exposing the data -[Postgraphile](https://www.graphile.org/postgraphile/) is used to expose GraphQL endpoints for our database schemas, this is described in detail [here](documentation/postgraphile.md). +`go get github.com/ipfs/go-ipfs` + +`cd $GOPATH/src/github.com/ipfs/go-ipfs` + +`make install` + +If we want to use Postgres as our backing datastore, we need to use the vulcanize fork of go-ipfs. + +Start by adding the fork and switching over to it: + +`git remote add vulcanize https://github.com/vulcanize/go-ipfs.git` + +`git fetch vulcanize` + +`git checkout -b postgres_update vulcanize/postgres_update` + +Now install this fork of ipfs, first be sure to remove any previous installation: + +`make install` + +Check that is installed properly by running: + +`ipfs` + +You should see the CLI info/help output. + +And now we initialize with the `postgresds` profile. +If ipfs was previously initialized we will need to remove the old profile first. +We also need to provide env variables for the postgres connection: + +We can either set these manually, e.g. +```bash +export IPFS_PGHOST= +export IPFS_PGUSER= +export IPFS_PGDATABASE= +export IPFS_PGPORT= +export IPFS_PGPASSWORD= +``` + +And then run the ipfs command: + +`ipfs init --profile=postgresds` + +Or we can use the pre-made script at `GOPATH/src/github.com/ipfs/go-ipfs/misc/utility/ipfs_postgres.sh` +which has usage: +`./ipfs_postgres.sh "` -### Tests -- Replace the empty `ipcPath` in the `environments/testing.toml` with a path to a full node's eth_jsonrpc endpoint (e.g. local geth node ipc path or infura url) - - Note: must be mainnet - - Note: integration tests require configuration with an archival node -- `make test` will run the unit tests and skip the integration tests -- `make integrationtest` will run just the integration tests -- `make test` and `make integrationtest` setup a clean `vulcanize_testing` db +and will ask us to enter the password, avoiding storing it to an ENV variable. +Once we have initialized ipfs, that is all we need to do with it- we do not need to run a daemon during the subsequent processes. + +### Blockchain +This section describes how to setup an Ethereum or Bitcoin node to serve as a data source for ipfs-blockchain-watcher + +#### Ethereum +For Ethereum, we currently *require* [a special fork of go-ethereum](https://github.com/vulcanize/go-ethereum/tree/statediff_at_anyblock-1.9.11). This can be setup as follows. +Skip this steps if you already have access to a node that displays the statediffing endpoints. + +Begin by downloading geth and switching to the vulcanize/rpc_statediffing branch: + +`go get github.com/ethereum/go-ethereum` + +`cd $GOPATH/src/github.com/ethereum/go-ethereum` + +`git remote add vulcanize https://github.com/vulcanize/go-ethereum.git` + +`git fetch vulcanize` + +`git checkout -b statediffing vulcanize/statediff_at_anyblock-1.9.11` + +Now, install this fork of geth (make sure any old versions have been uninstalled/binaries removed first): + +`make geth` + +And run the output binary with statediffing turned on: + +`cd $GOPATH/src/github.com/ethereum/go-ethereum/build/bin` + +`./geth --statediff --statediff.streamblock --ws --syncmode=full` + +Note: if you wish to access historical data (perform `backFill`) then the node will need to operate as an archival node (`--gcmode=archive`) + +Note: other CLI options- statediff specific ones included- can be explored with `./geth help` + +The output from geth should mention that it is `Starting statediff service` and block synchronization should begin shortly thereafter. +Note that until it receives a subscriber, the statediffing process does nothing but wait for one. Once a subscription is received, this +will be indicated in the output and node will begin processing and sending statediffs. + +Also in the output will be the endpoints that we will use to interface with the node. +The default ws url is "127.0.0.1:8546" and the default http url is "127.0.0.1:8545". +These values will be used as the `ethereum.wsPath` and `ethereum.httpPath` in the config, respectively. + +#### Bitcoin +For Bitcoin, ipfs-blockchain-watcher is able to operate entirely through the universally exposed JSON-RPC interfaces. +This means we can use any of the standard full nodes (e.g. bitcoind, btcd) as our data source. + +Point at a remote node or set one up locally using the instructions for [bitcoind](https://github.com/bitcoin/bitcoin) and [btcd](https://github.com/btcsuite/btcd). + +The default http url is "127.0.0.1:8332". We will use the http endpoint as both the `bitcoin.wsPath` and `bitcoin.httpPath` +(bitcoind does not support websocket endpoints, we are currently using a "subscription" wrapper around the http endpoints) + +### Watcher +Finally, we can setup the watcher process itself. + +Start by downloading vulcanizedb and moving into the repo: + +`go get github.com/vulcanize/ipfs-chain-watcher` + +`cd $GOPATH/src/github.com/vulcanize/ipfs-chain-watcher` + +Then, build the binary: + +`make build` + +## Usage +After building the binary, run as + +`./ipfs-blockchain-watcher watch --config= Open from* and choose the location of the diagram you want to update. - 1. Once open in draw.io, you may update it. - 1. Export the diagram to this repository's directory and commit it. - - -## Generating the Changelog -We use [github-changelog-generator](https://github.com/github-changelog-generator/github-changelog-generator) to generate release Changelogs. To be consistent with previous Changelogs, the following flags should be passed to the command: - -``` ---user vulcanize ---project vulcanizedb ---token {YOUR_GITHUB_TOKEN} ---no-issues ---usernames-as-github-logins ---since-tag {PREVIOUS_RELEASE_TAG} -``` - -For more information on why your github token is needed, and how to generate it see [https://github -.com/github-changelog-generator/github-changelog-generator#github-token](https://github.com/github-changelog-generator/github-changelog-generator#github-token). - -## Code of Conduct -VulcanizeDB follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct). diff --git a/documentation/custom-transformers.md b/documentation/custom-transformers.md deleted file mode 100644 index edde070e7..000000000 --- a/documentation/custom-transformers.md +++ /dev/null @@ -1,212 +0,0 @@ -# Custom Transformers -When the capabilities of the generic `contractWatcher` are not sufficient, custom transformers tailored to a specific -purpose can be leveraged. - -Individual custom transformers can be composed together from any number of external repositories and executed as a -single process using the `compose` and `execute` commands or the `composeAndExecute` command. This is accomplished by -generating a Go plugin which allows the `vulcanizedb` binary to link to the external transformers, so long as they -abide by one of the standard [interfaces](../staging/libraries/shared/transformer). - -## Writing custom transformers -For help with writing different types of custom transformers please see below: - -Storage Transformers: transform data derived from contract storage tries - * [Guide](../../staging/libraries/shared/factories/storage/README.md) - * [Example](../../staging/libraries/shared/factories/storage/EXAMPLE.md) - -Event Transformers: transform data derived from Ethereum log events - * [Guide](../../staging/libraries/shared/factories/event/README.md) - * [Example 1](https://github.com/vulcanize/ens_transformers/tree/master/transformers/registar) - * [Example 2](https://github.com/vulcanize/ens_transformers/tree/master/transformers/registry) - * [Example 3](https://github.com/vulcanize/ens_transformers/tree/master/transformers/resolver) - -Contract Transformers: transform data derived from Ethereum log events and use it to poll public contract methods - * [Example 1](https://github.com/vulcanize/account_transformers) - * [Example 2](https://github.com/vulcanize/ens_transformers/tree/master/transformers/domain_records) - -## Preparing custom transformers to work as part of a plugin -To plug in an external transformer we need to: - -1. Create a package that exports a variable `TransformerInitializer`, `StorageTransformerInitializer`, or `ContractTransformerInitializer` that are of type [TransformerInitializer](../staging/libraries/shared/transformer/event_transformer.go#L33) -or [StorageTransformerInitializer](../../staging/libraries/shared/transformer/storage_transformer.go#L31), -or [ContractTransformerInitializer](../../staging/libraries/shared/transformer/contract_transformer.go#L31), respectively -2. Design the transformers to work in the context of their [event](../staging/libraries/shared/watcher/event_watcher.go#L83), -[storage](../../staging/libraries/shared/watcher/storage_watcher.go#L53), -or [contract](../../staging/libraries/shared/watcher/contract_watcher.go#L68) watcher execution modes -3. Create db migrations to run against vulcanizeDB so that we can store the transformer output - * Do not `goose fix` the transformer migrations, this is to ensure they are always ran after the core vulcanizedb migrations which are kept in their fixed form - * Specify migration locations for each transformer in the config with the `exporter.transformer.migrations` fields - * If the base vDB migrations occupy this path as well, they need to be in their `goose fix`ed form - as they are [here](../../staging/db/migrations) - -To update a plugin repository with changes to the core vulcanizedb repository, use your dependency manager to install the desired version of vDB. - -## Building and Running Custom Transformers -### Commands -* The `compose`, `execute`, `composeAndExecute` commands require Go 1.11+ and use [Go plugins](https://golang -.org/pkg/plugin/) which only work on Unix-based systems. - -* There is an ongoing [conflict](https://github.com/golang/go/issues/20481) between Go plugins and the use of vendored -dependencies which imposes certain limitations on how the plugins are built. - -* Separate `compose` and `execute` commands allow pre-building and linking to the pre-built .so file. So, if -these are run independently, instead of using `composeAndExecute`, a couple of things need to be considered: - * It is necessary that the .so file was built with the same exact dependencies that are present in the execution - environment, i.e. we need to `compose` and `execute` the plugin .so file with the same exact version of vulcanizeDB. - * The plugin migrations are run during the plugin's composition. As such, if `execute` is used to run a prebuilt .so - in a different environment than the one it was composed in, then the database structure will need to be loaded - into the environment's Postgres database. This can either be done by manually loading the plugin's schema into - Postgres, or by manually running the plugin's migrations. - -* The `compose` and `composeAndExecute` commands assume you are in the vulcanizdb directory located at your system's -`$GOPATH`, and that the plugin dependencies are present at their `$GOPATH` directories. - -* The `execute` command does not require the plugin transformer dependencies be located in their `$GOPATH` directories, -instead it expects a .so file (of the name specified in the config file) to be in -`$GOPATH/src/github.com/vulcanize/ipfs-chain-watcher/plugins/` and, as noted above, also expects the plugin db migrations to - have already been ran against the database. - - * Usage: - * compose: `./vulcanizedb compose --config=environments/config_name.toml` - - * execute: `./vulcanizedb execute --config=environments/config_name.toml` - - * composeAndExecute: `./vulcanizedb composeAndExecute --config=environments/config_name.toml` - -### Flags -The `execute` and `composeAndExecute` commands can be passed optional flags to specify the operation of the watchers: - -- `--recheck-headers`/`-r` - specifies whether to re-check headers for events after the header has already been queried for watched logs. -Can be useful for redundancy if you suspect that your node is not always returning all desired logs on every query. -Argument is expected to be a boolean: e.g. `-r=true`. -Defaults to `false`. - -- `query-recheck-interval`/`-q` - specifies interval for re-checking storage diffs that haven been queued for later processing -(by default, the storage watched queues storage diffs if transformer execution fails, on the assumption that subsequent data derived from the event transformers may enable us to decode storage keys that we don't recognize right now). -Argument is expected to be a duration (integer measured in nanoseconds): e.g. `-q=10m30s` (for 10 minute, 30 second intervals). -Defaults to `5m` (5 minutes). - -### Configuration -A .toml config file is specified when executing the commands. -The config provides information for composing a set of transformers from external repositories: - -```toml -[database] - name = "vulcanize_public" - hostname = "localhost" - user = "vulcanize" - password = "vulcanize" - port = 5432 - -[client] - ipcPath = "/Users/user/Library/Ethereum/geth.ipc" - wsPath = "ws://127.0.0.1:8546" - -[exporter] - home = "github.com/vulcanize/ipfs-chain-watcher" - name = "exampleTransformerExporter" - save = false - transformerNames = [ - "transformer1", - "transformer2", - "transformer3", - "transformer4", - ] - [exporter.transformer1] - path = "path/to/transformer1" - type = "eth_event" - repository = "github.com/account/repo" - migrations = "db/migrations" - rank = "0" - [exporter.transformer2] - path = "path/to/transformer2" - type = "eth_contract" - repository = "github.com/account/repo" - migrations = "db/migrations" - rank = "0" - [exporter.transformer3] - path = "path/to/transformer3" - type = "eth_event" - repository = "github.com/account/repo" - migrations = "db/migrations" - rank = "0" - [exporter.transformer4] - path = "path/to/transformer4" - type = "eth_storage" - repository = "github.com/account2/repo2" - migrations = "to/db/migrations" - rank = "1" -``` -- `home` is the name of the package you are building the plugin for, in most cases this is github.com/vulcanize/ipfs-chain-watcher -- `name` is the name used for the plugin files (.so and .go) -- `save` indicates whether or not the user wants to save the .go file instead of removing it after .so compilation. Sometimes useful for debugging/trouble-shooting purposes. -- `transformerNames` is the list of the names of the transformers we are composing together, so we know how to access their submaps in the exporter map -- `exporter.`s are the sub-mappings containing config info for the transformers - - `repository` is the path for the repository which contains the transformer and its `TransformerInitializer` - - `path` is the relative path from `repository` to the transformer's `TransformerInitializer` directory (initializer package). - - Transformer repositories need to be cloned into the user's $GOPATH (`go get`) - - `type` is the type of the transformer; indicating which type of watcher it works with (for now, there are only two options: `eth_event` and `eth_storage`) - - `eth_storage` indicates the transformer works with the [storage watcher](../../staging/libraries/shared/watcher/storage_watcher.go) - that fetches state and storage diffs from an ETH node (instead of, for example, from IPFS) - - `eth_event` indicates the transformer works with the [event watcher](../../staging/libraries/shared/watcher/event_watcher.go) - that fetches event logs from an ETH node - - `eth_contract` indicates the transformer works with the [contract watcher](../staging/libraries/shared/watcher/contract_watcher.go) - that is made to work with [contract_watcher pkg](../../staging/pkg/contract_watcher) - based transformers which work with either a header or full sync vDB to watch events and poll public methods ([example1](https://github.com/vulcanize/account_transformers/tree/master/transformers/account/light), [example2](https://github.com/vulcanize/ens_transformers/tree/working/transformers/domain_records)) - - `migrations` is the relative path from `repository` to the db migrations directory for the transformer - - `rank` determines the order that migrations are ran, with lower ranked migrations running first - - this is to help isolate any potential conflicts between transformer migrations - - start at "0" - - use strings - - don't leave gaps - - transformers with identical migrations/migration paths should share the same rank -- Note: If any of the imported transformers need additional config variables those need to be included as well -- Note: If the storage transformers are processing storage diffs from geth, we need to configure the websocket endpoint `client.wsPath` for them - -This information is used to write and build a Go plugin which exports the configured transformers. -These transformers are loaded onto their specified watchers and executed. - -Transformers of different types can be run together in the same command using a single config file or in separate instances using different config files - -The general structure of a plugin .go file, and what we would see built with the above config is shown below - -```go -package main - -import ( - interface1 "github.com/vulcanize/ipfs-chain-watcher/libraries/shared/transformer" - transformer1 "github.com/account/repo/path/to/transformer1" - transformer2 "github.com/account/repo/path/to/transformer2" - transformer3 "github.com/account/repo/path/to/transformer3" - transformer4 "github.com/account2/repo2/path/to/transformer4" -) - -type exporter string - -var Exporter exporter - -func (e exporter) Export() []interface1.EventTransformerInitializer, []interface1.StorageTransformerInitializer, []interface1.ContractTransformerInitializer { - return []interface1.TransformerInitializer{ - transformer1.TransformerInitializer, - transformer3.TransformerInitializer, - }, []interface1.StorageTransformerInitializer{ - transformer4.StorageTransformerInitializer, - }, []interface1.ContractTransformerInitializer{ - transformer2.TransformerInitializer, - } -} -``` - -### Storage backfilling -Storage transformers stream data from a geth subscription or parity csv file where the storage diffs are produced and emitted as the -full sync progresses. If the transformers have missed consuming a range of diffs due to lag in the startup of the processes or due to misalignment of the sync, -we can configure our storage transformers to backfill missing diffs from a [modified archival geth client](https://github.com/vulcanize/go-ethereum/tree/statediff_at). - -To do so, add the following field to the config file. -```toml -[storageBackFill] - on = false -``` -- `on` is set to `true` to turn the backfill process on - -This process uses the regular `client.ipcPath` rpc path, it assumes that it is either an http or ipc path that supports the `StateDiffAt` endpoint. diff --git a/documentation/data-syncing.md b/documentation/data-syncing.md deleted file mode 100644 index 42803080d..000000000 --- a/documentation/data-syncing.md +++ /dev/null @@ -1,27 +0,0 @@ -# Syncing commands -These commands are used to sync raw Ethereum data into Postgres, with varying levels of data granularity. - -## headerSync -Syncs block headers from a running Ethereum node into the VulcanizeDB table `headers`. -- Queries the Ethereum node using RPC calls. -- Validates headers from the last 15 blocks to ensure that data is up to date. -- Useful when you want a minimal baseline from which to track targeted data on the blockchain (e.g. individual smart -contract storage values or event logs). -- Handles chain reorgs by [validating the most recent blocks' hashes](../pkg/history/header_validator.go). If the hash is -different from what we have already stored in the database, the header record will be updated. - -#### Usage -- Run: `./vulcanizedb headerSync --config --starting-block-number ` -- The config file must be formatted as follows, and should contain an ipc path to a running Ethereum node: -```toml -[database] - name = "vulcanize_public" - hostname = "localhost" - user = "vulcanize" - password = "vulcanize" - port = 5432 - -[client] - ipcPath = -``` -- Alternatively, the ipc path can be passed as a flag instead `--client-ipcPath`. diff --git a/documentation/diagrams/vdb-overview.png b/documentation/diagrams/vdb-overview.png deleted file mode 100644 index fe4e4d67101be00e59a5d43fbe7bc968a541c7a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53018 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 JE^i+2e*mfm*(U%1 diff --git a/documentation/generic-transformer.md b/documentation/generic-transformer.md deleted file mode 100644 index 6ec9207ae..000000000 --- a/documentation/generic-transformer.md +++ /dev/null @@ -1,160 +0,0 @@ -# Generic Transformer -The `contractWatcher` command is a built-in generic contract watcher. It can watch any and all events for a given contract provided the contract's ABI is available. -It also provides some state variable coverage by automating polling of public methods, with some restrictions: -1. The method must have 2 or less arguments -1. The method's arguments must all be of type address or bytes32 (hash) -1. The method must return a single value - -This command operates in two modes- `header` and `full`- which require a header or full-synced vulcanizeDB, respectively. - -This command requires the contract ABI be available on Etherscan if it is not provided in the config file by the user. - -If method polling is turned on we require an archival node at the ETH ipc endpoint in our config, whether or not we are operating in `header` or `full` mode. -Otherwise we only need to connect to a full node. - -## Configuration -This command takes a config of the form: - -```toml - [database] - name = "vulcanize_public" - hostname = "localhost" - port = 5432 - - [client] - ipcPath = "/Users/user/Library/Ethereum/geth.ipc" - - [contract] - network = "" - addresses = [ - "contractAddress1", - "contractAddress2" - ] - [contract.contractAddress1] - abi = 'ABI for contract 1' - startingBlock = 982463 - [contract.contractAddress2] - abi = 'ABI for contract 2' - events = [ - "event1", - "event2" - ] - eventArgs = [ - "arg1", - "arg2" - ] - methods = [ - "method1", - "method2" - ] - methodArgs = [ - "arg1", - "arg2" - ] - startingBlock = 4448566 - piping = true -```` - -- The `contract` section defines which contracts we want to watch and with which conditions. -- `network` is only necessary if the ABIs are not provided and wish to be fetched from Etherscan. - - Empty or nil string indicates mainnet - - "ropsten", "kovan", and "rinkeby" indicate their respective networks -- `addresses` lists the contract addresses we are watching and is used to load their individual configuration parameters -- `contract.` are the sub-mappings which contain the parameters specific to each contract address - - `abi` is the ABI for the contract; if none is provided the application will attempt to fetch one from Etherscan using the provided address and network - - `events` is the list of events to watch - - If this field is omitted or no events are provided then by defualt ALL events extracted from the ABI will be watched - - If event names are provided then only those events will be watched - - `eventArgs` is the list of arguments to filter events with - - If this field is omitted or no eventArgs are provided then by default watched events are not filtered by their argument values - - If eventArgs are provided then only those events which emit at least one of these values as an argument are watched - - `methods` is the list of methods to poll - - If this is omitted or no methods are provided then by default NO methods are polled - - If method names are provided then those methods will be polled, provided - 1) Method has two or less arguments - 1) Arguments are all of address or hash types - 1) Method returns a single value - - `methodArgs` is the list of arguments to limit polling methods to - - If this field is omitted or no methodArgs are provided then by default methods will be polled with every combination of the appropriately typed values that have been collected from watched events - - If methodArgs are provided then only those values will be used to poll methods - - `startingBlock` is the block we want to begin watching the contract, usually the deployment block of that contract - - `piping` is a boolean flag which indicates whether or not we want to pipe return method values forward as arguments to subsequent method calls - -At the very minimum, for each contract address an ABI and a starting block number need to be provided (or just the starting block if the ABI can be reliably fetched from Etherscan). -With just this information we will be able to watch all events at the contract, but with no additional filters and no method polling. - -## Output - -Transformed events and polled method results are committed to Postgres in schemas and tables generated according to the contract abi. - -Schemas are created for each contract using the naming convention `_` -Under this schema, tables are generated for watched events as `_event` and for polled methods as `_method` -The 'method' and 'event' identifiers are tacked onto the end of the table names to prevent collisions between methods and events of the same lowercase name - -## Example: - -Modify `./environments/example.toml` to replace the empty `ipcPath` with a path that points to an ethjson_rpc endpoint (e.g. a local geth node ipc path or an Infura url). -This endpoint should be for an archival eth node if we want to perform method polling as this configuration is currently set up to do. To work with a non-archival full node, -remove the `balanceOf` method from the `0x8dd5fbce2f6a956c3022ba3663759011dd51e73e` (TrueUSD) contract. - -If you are operating a header sync vDB, run: - - `./vulcanizedb contractWatcher --config=./environments/example.toml --mode=header` - -If instead you are operating a full sync vDB and provided an archival node IPC path, run in full mode: - - `./vulcanizedb contractWatcher --config=./environments/example.toml --mode=full` - -This will run the contractWatcher and configures it to watch the contracts specified in the config file. Note that -by default we operate in `header` mode but the flag is included here to demonstrate its use. - -The example config we link to in this example watches two contracts, the ENS Registry (0x314159265dD8dbb310642f98f50C066173C1259b) and TrueUSD (0x8dd5fbCe2F6a956C3022bA3663759011Dd51e73E). - -Because the ENS Registry is configured with only an ABI and a starting block, we will watch all events for this contract and poll none of its methods. Note that the ENS Registry is an example -of a contract which does not have its ABI available over Etherscan and must have it included in the config file. - -The TrueUSD contract is configured with two events (`Transfer` and `Mint`) and a single method (`balanceOf`), as such it will watch these two events and use any addresses it collects emitted from them -to poll the `balanceOf` method with those addresses at every block. Note that we do not provide an ABI for TrueUSD as its ABI can be fetched from Etherscan. - -For the ENS contract, it produces and populates a schema with four tables" -`header_0x314159265dd8dbb310642f98f50c066173c1259b.newowner_event` -`header_0x314159265dd8dbb310642f98f50c066173c1259b.newresolver_event` -`header_0x314159265dd8dbb310642f98f50c066173c1259b.newttl_event` -`header_0x314159265dd8dbb310642f98f50c066173c1259b.transfer_event` - -For the TrusUSD contract, it produces and populates a schema with three tables: - -`header_0x8dd5fbce2f6a956c3022ba3663759011dd51e73e.transfer_event` -`header_0x8dd5fbce2f6a956c3022ba3663759011dd51e73e.mint_event` -`header_0x8dd5fbce2f6a956c3022ba3663759011dd51e73e.balanceof_method` - -Column ids and types for these tables are generated based on the event and method argument names and types and method return types, resulting in tables such as: - -Table "header_0x8dd5fbce2f6a956c3022ba3663759011dd51e73e.transfer_event" - -| Column | Type | Collation | Nullable | Default | Storage | Stats target | Description -|:----------:|:---------------------:|:---------:|:--------:|:--------------------------------------------------------------------------------------------:|:--------:|:------------:|:-----------:| -| id | integer | | not null | nextval('header_0x8dd5fbce2f6a956c3022ba3663759011dd51e73e.transfer_event_id_seq'::regclass) | plain | | | -| header_id | integer | | not null | | plain | | | -| token_name | character varying(66) | | not null | | extended | | | -| raw_log | jsonb | | | | extended | | | -| log_idx | integer | | not null | | plain | | | -| tx_idx | integer | | not null | | plain | | | -| from_ | character varying(66) | | not null | | extended | | | -| to_ | character varying(66) | | not null | | extended | | | -| value_ | numeric | | not null | | main | | | - - -Table "header_0x8dd5fbce2f6a956c3022ba3663759011dd51e73e.balanceof_method" - -| Column | Type | Collation | Nullable | Default | Storage | Stats target | Description | -|:----------:|:---------------------:|:---------:|:--------:|:----------------------------------------------------------------------------------------------:|:--------:|:------------:|:-----------:| -| id | integer | | not null | nextval('header_0x8dd5fbce2f6a956c3022ba3663759011dd51e73e.balanceof_method_id_seq'::regclass) | plain | | | -| token_name | character varying(66) | | not null | | extended | | | -| block | integer | | not null | | plain | | | -| who_ | character varying(66) | | not null | | extended | | | -| returned | numeric | | not null | | main | | | - -The addition of '_' after table names is to prevent collisions with reserved Postgres words. - -Also notice that the contract address used for the schema name has been down-cased. \ No newline at end of file diff --git a/documentation/postgraphile.md b/documentation/postgraphile.md deleted file mode 100644 index 08f5051c2..000000000 --- a/documentation/postgraphile.md +++ /dev/null @@ -1,34 +0,0 @@ -# Postgraphile - -You can expose VulcanizeDB data via [Postgraphile](https://github.com/graphile/postgraphile). -Check out [their documentation](https://www.graphile.org/postgraphile/) for the most up-to-date instructions on installing, running, and customizing Postgraphile. - -## Simple Setup - -As of April 30, 2019, you can run Postgraphile pointed at the default `vulcanize_public` database with the following commands: - -``` -npm install -g postgraphile -postgraphile --connection postgres://localhost/vulcanize_public --schema=public,custom --disable-default-mutations --no-ignore-rbac -``` - -Arguments: -- `--connection` specifies the database. The above command connects to the default `vulcanize_public` database -defined in [the example config](../environments/public.toml.example). -- `--schema` defines what schema(s) to expose. The above exposes the `public` schema (for core VulcanizeDB data) as well as a `custom` schema (where `custom` is the name of a schema defined in executed transformers). -- `--disable-default-mutations` prevents Postgraphile from exposing create, update, and delete operations on your data, which are otherwise enabled by default. -- `--no-ignore-rbac` ensures that Postgraphile will only expose the tables, columns, fields, and query functions that the user has explicit access to. - -## Customizing Postgraphile - -By default, Postgraphile will expose queries for all tables defined in your chosen database/schema(s), including [filtering](https://www.graphile.org/postgraphile/filtering/) and [auto-discovered relations](https://www.graphile.org/postgraphile/relations/). - -If you'd like to expose more customized windows into your data, there are some techniques you can apply when writing migrations: - -- [Computed columns](https://www.graphile.org/postgraphile/computed-columns/) enable you to derive additional fields from types defined in your database. -For example, you could write a function to expose a block header's state root over Postgraphile with a computed column - without modifying the `public.headers` table. -- [Custom queries](https://www.graphile.org/postgraphile/custom-queries/) enable you to provide on-demand access to more complex data (e.g. the product of joining and filtering several tables' data based on a passed argument). -For example, you could write a custom query to get the block timestamp for every transaction originating from a given address. -- [Subscriptions](https://www.graphile.org/postgraphile/subscriptions/) enable you to publish data as it is coming into your database. - -The above list is not exhaustive - please see the Postgraphile documentation for a more comprehensive and up-to-date description of available features. \ No newline at end of file diff --git a/documentation/super_node/resync.md b/documentation/resync.md similarity index 88% rename from documentation/super_node/resync.md rename to documentation/resync.md index d3936fd9f..8d4cb1d25 100644 --- a/documentation/super_node/resync.md +++ b/documentation/resync.md @@ -1,5 +1,5 @@ -## VulcanizeDB Super Node Resync -The `resync` command is made available for directing the resyncing of super node data within specified ranges. +## ipfs-blockchain-watcher resync +The `resync` command is made available for directing the resyncing of ipfs-blockchain-watcherdata within specified ranges. It also contains a utility for cleaning out old data, and resetting the validation level of data. ### Rational @@ -8,15 +8,15 @@ Manual resyncing of data is useful when we want to re-validate data within speci Cleaning out data is useful when we need to remove bad/deprecated data or prepare for breaking changes to the db schemas. -Resetting the validation level of data is useful for designating ranges of data for resyncing by an ongoing super node +Resetting the validation level of data is useful for designating ranges of data for resyncing by an ongoing ipfs-blockchain-watcher backfill process. ### Command -Usage: `./vulcanizedb resync --config={config.toml}` +Usage: `./ipfs-blockchain-watcher resync --config={config.toml}` Configuration can also be done through CLI options and/or environmental variables. -CLI options can be found using `./vulcanizedb resync --help`. +CLI options can be found using `./ipfs-blockchain-watcher resync --help`. ### Config diff --git a/documentation/super_node/architecture.md b/documentation/super_node/architecture.md deleted file mode 100644 index 3bd32a332..000000000 --- a/documentation/super_node/architecture.md +++ /dev/null @@ -1,138 +0,0 @@ -# VulcanizeDB Super Node Architecture -The VulcanizeDB super node is a collection of interfaces that are used to extract, process, and store in Postgres-IPFS -all chain data. The raw data indexed by the super node serves as the basis for more specific watchers and applications. - -Currently the service supports complete processing of all Bitcoin and Ethereum data. - -## Table of Contents -1. [Processes](#processes) -1. [Command](#command) -1. [Configuration](#config) -1. [Database](#database) -1. [APIs](#apis) -1. [Resync](#resync) -1. [IPFS Considerations](#ipfs-considerations) - -## Processes -The [super node service](../../pkg/super_node/service.go#L61) is a watcher comprised of the following interfaces: - -* [Payload Fetcher](../../pkg/super_node/shared/interfaces.go#L29): Fetches raw chain data from a half-duplex endpoint (HTTP/IPC), used for historical data fetching. ([BTC](../../pkg/super_node/btc/payload_fetcher.go), [ETH](../../pkg/super_node/eth/payload_fetcher.go)). -* [Payload Streamer](../../pkg/super_node/shared/interfaces.go#L24): Streams raw chain data from a full-duplex endpoint (WebSocket/IPC), used for syncing data at the head of the chain in real-time. ([BTC](../../pkg/super_node/btc/http_streamer.go), [ETH](../../pkg/super_node/eth/streamer.go)). -* [Payload Converter](../../pkg/super_node/shared/interfaces.go#L34): Converters raw chain data to an intermediary form prepared for IPFS publishing. ([BTC](../../pkg/super_node/btc/converter.go), [ETH](../../pkg/super_node/eth/converter.go)). -* [IPLD Publisher](../../pkg/super_node/shared/interfaces.go#L39): Publishes the converted data to IPFS, returning their CIDs and associated metadata for indexing. ([BTC](../../pkg/super_node/btc/publisher.go), [ETH](../../pkg/super_node/eth/publisher.go)). -* [CID Indexer](../../pkg/super_node/shared/interfaces.go#L44): Indexes CIDs in Postgres with their associated metadata. This metadata is chain specific and selected based on utility. ([BTC](../../pkg/super_node/btc/indexer.go), [ETH](../../pkg/super_node/eth/indexer.go)). -* [CID Retriever](../../pkg/super_node/shared/interfaces.go#L54): Retrieves CIDs from Postgres by searching against their associated metadata, is used to lookup data to serve API requests/subscriptions. ([BTC](../../pkg/super_node/btc/retriever.go), [ETH](../../pkg/super_node/eth/retriever.go)). -* [IPLD Fetcher](../../pkg/super_node/shared/interfaces.go#L62): Fetches the IPLDs needed to service API requests/subscriptions from IPFS using retrieved CIDS; can route through a IPFS block-exchange to search for objects that are not directly available. ([BTC](../../pkg/super_node/btc/ipld_fetcher.go), [ETH](../../pkg/super_node/eth/ipld_fetcher.go)) -* [Response Filterer](../../pkg/super_node/shared/interfaces.go#L49): Filters converted data payloads served to API subscriptions; filters according to the subscriber provided parameters. ([BTC](../../pkg/super_node/btc/filterer.go), [ETH](../../pkg/super_node/eth/filterer.go)). -* [API](https://github.com/ethereum/go-ethereum/blob/master/rpc/types.go#L31): Expose RPC methods for clients to interface with the data. Chain-specific APIs should aim to recapitulate as much of the native API as possible. ([VDB](../../pkg/super_node/api.go), [ETH](../../pkg/super_node/eth/api.go)). - - -Appropriating the service for a new chain is done by creating underlying types to satisfy these interfaces for -the specifics of that chain. - -The service uses these interfaces to operate in any combination of three modes: sync, serve, and backfill. -* Sync: Streams raw chain data at the head, converts and publishes it to IPFS, and indexes the resulting set of CIDs in Postgres with useful metadata. -* BackFill: Automatically searches for and detects gaps in the DB; fetches, converts, publishes, and indexes the data to fill these gaps. -* Serve: Opens up IPC, HTTP, and WebSocket servers on top of the superNode DB and any concurrent sync and/or backfill processes. - - -These three modes are all operated through a single vulcanizeDB command: `superNode` - -## Command - -Usage: `./vulcanizedb superNode --config={config.toml}` - -Configuration can also be done through CLI options and/or environmental variables. -CLI options can be found using `./vulcanizedb superNode --help`. - -## Config - -Below is the set of universal config parameters for the superNode command, in .toml form, with the respective environmental variables commented to the side. -This set of parameters needs to be set no matter the chain type. - -```toml -[database] - name = "vulcanize_public" # $DATABASE_NAME - hostname = "localhost" # $DATABASE_HOSTNAME - port = 5432 # $DATABASE_PORT - user = "vdbm" # $DATABASE_USER - password = "" # $DATABASE_PASSWORD - -[ipfs] - path = "~/.ipfs" # $IPFS_PATH - -[superNode] - chain = "bitcoin" # $SUPERNODE_CHAIN - server = true # $SUPERNODE_SERVER - ipcPath = "~/.vulcanize/vulcanize.ipc" # $SUPERNODE_IPC_PATH - wsPath = "127.0.0.1:8082" # $SUPERNODE_WS_PATH - httpPath = "127.0.0.1:8083" # $SUPERNODE_HTTP_PATH - sync = true # $SUPERNODE_SYNC - workers = 1 # $SUPERNODE_WORKERS - backFill = true # $SUPERNODE_BACKFILL - frequency = 45 # $SUPERNODE_FREQUENCY - batchSize = 1 # $SUPERNODE_BATCH_SIZE - batchNumber = 50 # $SUPERNODE_BATCH_NUMBER - timeout = 300 # $HTTP_TIMEOUT - validationLevel = 1 # $SUPERNODE_VALIDATION_LEVEL -``` - -Additional parameters need to be set depending on the specific chain. - -For Bitcoin: - -```toml -[bitcoin] - wsPath = "127.0.0.1:8332" # $BTC_WS_PATH - httpPath = "127.0.0.1:8332" # $BTC_HTTP_PATH - pass = "password" # $BTC_NODE_PASSWORD - user = "username" # $BTC_NODE_USER - nodeID = "ocd0" # $BTC_NODE_ID - clientName = "Omnicore" # $BTC_CLIENT_NAME - genesisBlock = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" # $BTC_GENESIS_BLOCK - networkID = "0xD9B4BEF9" # $BTC_NETWORK_ID -``` - -For Ethereum: - -```toml -[ethereum] - wsPath = "127.0.0.1:8546" # $ETH_WS_PATH - httpPath = "127.0.0.1:8545" # $ETH_HTTP_PATH - nodeID = "arch1" # $ETH_NODE_ID - clientName = "Geth" # $ETH_CLIENT_NAME - genesisBlock = "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" # $ETH_GENESIS_BLOCK - networkID = "1" # $ETH_NETWORK_ID -``` - -## Database - -Currently, the super node persists all data to a single Postgres database. The migrations for this DB can be found [here](../../db/migrations). -Chain-specific data is populated under a chain-specific schema (e.g. `eth` and `btc`) while shared data- such as the IPFS blocks table- is populated under the `public` schema. -Subsequent watchers which act on the raw chain data should build and populate their own schemas or separate databases entirely. - -In the future, we will be moving to a foreign table based architecture wherein a single db is used for shared data while each watcher uses -its own database and accesses and acts on the shared data through foreign tables. Isolating watchers to their own databases will prevent complications and -conflicts between watcher db migrations. - - -## APIs - -The super node provides mutliple types of APIs by which to interface with its data. -More detailed information on the APIs can be found [here](apis.md). - -## Resync - -A separate command `resync` is available for directing the resyncing of data within specified ranges. -This is useful if we want to re-validate a range of data using a new source or clean out bad/deprecated data. -More detailed information on this command can be found [here](resync.md). - -## IPFS Considerations - -Currently, the IPLD Publisher and Fetcher use internalized IPFS processes which interface directly with a local IPFS repository. -This circumvents the need to run a full IPFS daemon with a [go-ipld-eth](https://github.com/ipfs/go-ipld-eth) plugin, but can lead to issues -with lock-contention on the IPFS repo if another IPFS process is configured and running at the same $IPFS_PATH. This also necessitates the need for -a locally configured IPFS repository. - -Once go-ipld-eth has been updated to work with a modern version of PG-IPFS, an additional option will be provided to direct -all publishing and fetching of IPLD objects through a remote IPFS daemon. \ No newline at end of file diff --git a/documentation/super_node/setup.md b/documentation/super_node/setup.md deleted file mode 100644 index fa95a6efb..000000000 --- a/documentation/super_node/setup.md +++ /dev/null @@ -1,160 +0,0 @@ -# VulcanizeDB Super Node Setup -Step-by-step instructions for manually setting up and running a VulcanizeDB super node. - -Steps: -1. [Postgres](#postgres) -1. [Goose](#goose) -1. [IPFS](#ipfs) -1. [Blockchain](#blockchain) -1. [VulcanizeDB](#vulcanizedb) - -### Postgres -A postgresDB is needed to storing all of the data in the vulcanizedb system. -Postgres is used as the backing datastore for IPFS, and is used to index the CIDs for all of the chain data stored on IPFS. -Follow the guides [here](https://wiki.postgresql.org/wiki/Detailed_installation_guides) for setting up Postgres. - -Once the Postgres server is running, we will need to make a database for vulcanizedb, e.g. `vulcanize_public`. - -`createdb vulcanize_public` - -For running the automated tests, also create a database named `vulcanize_testing`. - -`createdb vulcanize_testing` - -### Goose -We use [goose](https://github.com/pressly/goose) as our migration management tool. While it is not necessary to use `goose` for manual setup, it -is required for running the automated tests. - - -### IPFS -We use IPFS to store IPLD objects for each type of data we extract from on chain. - -To start, download and install [IPFS](https://github.com/vulcanize/go-ipfs): - -`go get github.com/ipfs/go-ipfs` - -`cd $GOPATH/src/github.com/ipfs/go-ipfs` - -`make install` - -If we want to use Postgres as our backing datastore, we need to use the vulcanize fork of go-ipfs. - -Start by adding the fork and switching over to it: - -`git remote add vulcanize https://github.com/vulcanize/go-ipfs.git` - -`git fetch vulcanize` - -`git checkout -b postgres_update vulcanize/postgres_update` - -Now install this fork of ipfs, first be sure to remove any previous installation: - -`make install` - -Check that is installed properly by running: - -`ipfs` - -You should see the CLI info/help output. - -And now we initialize with the `postgresds` profile. -If ipfs was previously initialized we will need to remove the old profile first. -We also need to provide env variables for the postgres connection: - -We can either set these manually, e.g. -```bash -export IPFS_PGHOST= -export IPFS_PGUSER= -export IPFS_PGDATABASE= -export IPFS_PGPORT= -export IPFS_PGPASSWORD= -``` - -And then run the ipfs command: - -`ipfs init --profile=postgresds` - -Or we can use the pre-made script at `GOPATH/src/github.com/ipfs/go-ipfs/misc/utility/ipfs_postgres.sh` -which has usage: - -`./ipfs_postgres.sh "` - -and will ask us to enter the password, avoiding storing it to an ENV variable. - -Once we have initialized ipfs, that is all we need to do with it- we do not need to run a daemon during the subsequent processes (in fact, we can't). - -### Blockchain -This section describes how to setup an Ethereum or Bitcoin node to serve as a data source for the super node - -#### Ethereum -For Ethereum, we currently *require* [a special fork of go-ethereum](https://github.com/vulcanize/go-ethereum/tree/statediff_at_anyblock-1.9.11). This can be setup as follows. -Skip this steps if you already have access to a node that displays the statediffing endpoints. - -Begin by downloading geth and switching to the vulcanize/rpc_statediffing branch: - -`go get github.com/ethereum/go-ethereum` - -`cd $GOPATH/src/github.com/ethereum/go-ethereum` - -`git remote add vulcanize https://github.com/vulcanize/go-ethereum.git` - -`git fetch vulcanize` - -`git checkout -b statediffing vulcanize/statediff_at_anyblock-1.9.11` - -Now, install this fork of geth (make sure any old versions have been uninstalled/binaries removed first): - -`make geth` - -And run the output binary with statediffing turned on: - -`cd $GOPATH/src/github.com/ethereum/go-ethereum/build/bin` - -`./geth --statediff --statediff.streamblock --ws --syncmode=full` - -Note: if you wish to access historical data (perform `backFill`) then the node will need to operate as an archival node (`--gcmode=archive`) - -Note: other CLI options- statediff specific ones included- can be explored with `./geth help` - -The output from geth should mention that it is `Starting statediff service` and block synchronization should begin shortly thereafter. -Note that until it receives a subscriber, the statediffing process does nothing but wait for one. Once a subscription is received, this -will be indicated in the output and node will begin processing and sending statediffs. - -Also in the output will be the endpoints that we will use to interface with the node. -The default ws url is "127.0.0.1:8546" and the default http url is "127.0.0.1:8545". -These values will be used as the `ethereum.wsPath` and `ethereum.httpPath` in the super node config, respectively. - -#### Bitcoin -For Bitcoin, the super node is able to operate entirely through the universally exposed JSON-RPC interfaces. -This means we can use any of the standard full nodes (e.g. bitcoind, btcd) as our data source. - -Point at a remote node or set one up locally using the instructions for [bitcoind](https://github.com/bitcoin/bitcoin) and [btcd](https://github.com/btcsuite/btcd). - -The default http url is "127.0.0.1:8332". We will use the http endpoint as both the `bitcoin.wsPath` and `bitcoin.httpPath` -(bitcoind does not support websocket endpoints, we are currently using a "subscription" wrapper around the http endpoints) - -### Vulcanizedb -Finally, we can begin the vulcanizeDB process itself. - -Start by downloading vulcanizedb and moving into the repo: - -`go get github.com/vulcanize/ipfs-chain-watcher` - -`cd $GOPATH/src/github.com/vulcanize/ipfs-chain-watcher` - -Run the db migrations against the Postgres database we created for vulcanizeDB: - -`goose -dir=./db/migrations postgres postgres://localhost:5432/vulcanize_public?sslmode=disable up` - -At this point, if we want to run the automated tests: - -`make test` -`make integration_test` - -Then, build the vulcanizedb binary: - -`go build` - -And run the super node command with a provided [config](architecture.md/#): - -`./vulcanizedb superNode --config=. - -package integration_test - -import ( - "io/ioutil" - "testing" - - "github.com/sirupsen/logrus" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestIntegrationTest(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "IntegrationTest Suite") -} - -var _ = BeforeSuite(func() { - logrus.SetOutput(ioutil.Discard) -}) diff --git a/pkg/client/eth_client.go b/pkg/client/eth_client.go deleted file mode 100644 index 4e66efa2d..000000000 --- a/pkg/client/eth_client.go +++ /dev/null @@ -1,63 +0,0 @@ -// 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 client - -import ( - "context" - "math/big" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" -) - -type EthClient struct { - client *ethclient.Client -} - -func NewEthClient(client *ethclient.Client) EthClient { - return EthClient{client: client} -} - -func (client EthClient) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - return client.client.BlockByNumber(ctx, number) -} - -func (client EthClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { - return client.client.CallContract(ctx, msg, blockNumber) -} - -func (client EthClient) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { - return client.client.FilterLogs(ctx, q) -} - -func (client EthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { - return client.client.HeaderByNumber(ctx, number) -} - -func (client EthClient) TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) { - return client.client.TransactionSender(ctx, tx, block, index) -} - -func (client EthClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { - return client.client.TransactionReceipt(ctx, txHash) -} - -func (client EthClient) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { - return client.client.BalanceAt(ctx, account, blockNumber) -} diff --git a/test_config/test_config.go b/test_config/test_config.go deleted file mode 100644 index e3ac847f8..000000000 --- a/test_config/test_config.go +++ /dev/null @@ -1,112 +0,0 @@ -// 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 test_config - -import ( - "errors" - "fmt" - "os" - - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "github.com/vulcanize/ipfs-chain-watcher/pkg/config" - "github.com/vulcanize/ipfs-chain-watcher/pkg/core" - "github.com/vulcanize/ipfs-chain-watcher/pkg/postgres" -) - -var TestConfig *viper.Viper -var DBConfig config.Database -var TestClient config.Client -var ABIFilePath string - -func init() { - setTestConfig() - setABIPath() -} - -func setTestConfig() { - TestConfig = viper.New() - TestConfig.SetConfigName("testing") - TestConfig.AddConfigPath("$GOPATH/src/github.com/vulcanize/ipfs-chain-watcher/environments/") - err := TestConfig.ReadInConfig() - if err != nil { - logrus.Fatal(err) - } - ipc := TestConfig.GetString("client.ipcPath") - - // If we don't have an ipc path in the config file, check the env variable - if ipc == "" { - TestConfig.BindEnv("url", "INFURA_URL") - ipc = TestConfig.GetString("url") - } - if ipc == "" { - logrus.Fatal(errors.New("testing.toml IPC path or $INFURA_URL env variable need to be set")) - } - - hn := TestConfig.GetString("database.hostname") - port := TestConfig.GetInt("database.port") - name := TestConfig.GetString("database.name") - - DBConfig = config.Database{ - Hostname: hn, - Name: name, - Port: port, - } - TestClient = config.Client{ - IPCPath: ipc, - } -} - -func setABIPath() { - gp := os.Getenv("GOPATH") - ABIFilePath = gp + "/src/github.com/vulcanize/ipfs-chain-watcher/pkg/eth/testing/" -} - -func NewTestDB(node core.Node) *postgres.DB { - db, err := postgres.NewDB(DBConfig, node) - if err != nil { - panic(fmt.Sprintf("Could not create new test db: %v", err)) - } - return db -} - -func CleanTestDB(db *postgres.DB) { - // can't delete from nodes since this function is called after the required node is persisted - db.MustExec("DELETE FROM goose_db_version") - db.MustExec("DELETE FROM header_sync_logs") - db.MustExec("DELETE FROM header_sync_receipts") - db.MustExec("DELETE FROM header_sync_transactions") - db.MustExec("DELETE FROM headers") - db.MustExec("DELETE FROM queued_storage") - db.MustExec("DELETE FROM storage_diff") -} - -func CleanCheckedHeadersTable(db *postgres.DB, columnNames []string) { - for _, name := range columnNames { - db.MustExec("ALTER TABLE checked_headers DROP COLUMN IF EXISTS " + name) - } -} - -// Returns a new test node, with the same ID -func NewTestNode() core.Node { - return core.Node{ - GenesisBlock: "GENESIS", - NetworkID: "1", - ID: "b6f90c0fdd8ec9607aed8ee45c69322e47b7063f0bfb7a29c8ecafab24d0a22d24dd2329b5ee6ed4125a03cb14e57fd584e67f9e53e6c631055cbbd82f080845", - ClientName: "Geth/v1.7.2-stable-1db4ecdc/darwin-amd64/go1.9", - } -}