From b96f6c4d31387e93a4d0933b8281f9cb4818662a Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Tue, 7 May 2019 16:21:08 -0500 Subject: [PATCH 01/19] Various README updates --- README.md | 20 +++++++++++------ documentation/composeAndExecute.md | 2 +- documentation/sync.md | 35 ++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1f926281..3f613b71 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ The same data structures and encodings that make Ethereum an effective and trust 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. +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. ## Dependencies - Go 1.11+ @@ -80,8 +84,9 @@ In some cases (such as recent Ubuntu systems), it may be necessary to overcome f - 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` + - 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`. @@ -98,10 +103,10 @@ In some cases (such as recent Ubuntu systems), it may be necessary to overcome f ## Usage -Usage is broken up into two processes: +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 vDB. +To provide data for transformations, raw Ethereum data must first be synced into VulcanizeDB. This is accomplished through the use of the `headerSync`, `sync`, or `coldImport` commands. These commands are described in detail [here](../staging/documentation/sync.md). @@ -118,6 +123,10 @@ Usage of the `compose`, `execute`, and `composeAndExecute` commands is described Documentation on how to build custom transformers to work with these commands can be found [here](../staging/documentation/transformers.md). +### 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](../staging/documentation/postgraphile.md). + + ## Tests - Replace the empty `ipcPath` in the `environments/infura.toml` with a path to a full node's eth_jsonrpc endpoint (e.g. local geth node ipc path or infura url) - Note: integration tests require configuration with an archival node @@ -126,9 +135,6 @@ Documentation on how to build custom transformers to work with these commands ca - `make test` will run the unit tests and skip the integration tests - `make integrationtest` will run just the integration tests -## API -[Postgraphile](https://www.graphile.org/postgraphile/) is used to expose GraphQL endpoints for our database schemas, this is described in detail [here](../staging/documentation/postgraphile.md). - ## Contributing Contributions are welcome! For more on this, please see [here](../staging/documentation/contributing.md). @@ -137,4 +143,4 @@ Small note: If editing the Readme, please conform to the [standard-readme specif ## License -[AGPL-3.0](../staging/LICENSE) © Vulcanize Inc \ No newline at end of file +[AGPL-3.0](../staging/LICENSE) © Vulcanize Inc diff --git a/documentation/composeAndExecute.md b/documentation/composeAndExecute.md index 8ea1abc9..ac963392 100644 --- a/documentation/composeAndExecute.md +++ b/documentation/composeAndExecute.md @@ -1,6 +1,6 @@ # composeAndExecute The `composeAndExecute` command is used to compose and execute over an arbitrary set of custom transformers. -This is accomplished by generating a Go pluggin which allows the `vulcanizedb` binary to link to external transformers, so +This is accomplished by generating a Go plugin which allows the `vulcanizedb` binary to link to external transformers, so long as they abide by one of the standard [interfaces](../staging/libraries/shared/transformer). Additionally, there are separate `compose` and `execute` commands to allow pre-building and linking to a pre-built .so file. diff --git a/documentation/sync.md b/documentation/sync.md index e5e715a4..558cf13b 100644 --- a/documentation/sync.md +++ b/documentation/sync.md @@ -1,22 +1,35 @@ # Syncing commands -These commands are used to sync raw Ethereum data into Postgres. +These commands are used to sync raw Ethereum data into Postgres, with varying levels of data granularity. ## headerSync -Syncs VulcanizeDB with the configured Ethereum node, populating only block headers. -This command is 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). -1. Start Ethereum node +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). + +##### Usage +1. Start Ethereum node. 1. In a separate terminal start VulcanizeDB: - - `./vulcanizedb headerSync --config --starting-block-number ` - +`./vulcanizedb headerSync --config --starting-block-number ` + ## sync -Syncs VulcanizeDB with the configured Ethereum node, populating blocks, transactions, receipts, and logs. -This command is useful when you want to maintain a broad cache of what's happening on the blockchain. -1. Start Ethereum node (**if fast syncing your Ethereum node, wait for initial sync to finish**) +Syncs blocks, transactions, receipts and logs from a running Ethereum node into VulcanizeDB tables named +`blocks`, `uncles`, `full_sync_transactions`, `full_sync_receipts` and `logs`. +- 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 to maintain a broad cache of what's happening on the blockchain. + +##### Usage +1. Start Ethereum node (**if fast syncing your Ethereum node, wait for initial sync to finish**). 1. In a separate terminal start VulcanizeDB: - - `./vulcanizedb sync --config --starting-block-number ` +`./vulcanizedb sync --config --starting-block-number ` ## coldImport -Sync VulcanizeDB from the LevelDB underlying a Geth node. +Syncs VulcanizeDB from Geth's underlying LevelDB datastore and persists Ethereum blocks, +transactions, receipts and logs into VulcanizeDB tables named `blocks`, `uncles`, +`full_sync_transactions`, `full_sync_receipts` and `logs` respectively. + +##### Usage 1. Assure node is not running, and that it has synced to the desired block height. 1. Start vulcanize_db - `./vulcanizedb coldImport --config ` From ba81766f6c9453e25af379835a35dff8859d32ae Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Tue, 7 May 2019 16:21:46 -0500 Subject: [PATCH 02/19] Small spelling fix --- pkg/contract_watcher/full/transformer/transformer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/contract_watcher/full/transformer/transformer.go b/pkg/contract_watcher/full/transformer/transformer.go index ad2bba69..42a95d21 100644 --- a/pkg/contract_watcher/full/transformer/transformer.go +++ b/pkg/contract_watcher/full/transformer/transformer.go @@ -164,7 +164,7 @@ func (tr *Transformer) Init() error { // Iterates through stored, initialized contract objects // Iterates through contract's event filters, grabbing watched event logs // Uses converter to convert logs into custom log type -// Persists converted logs into custuom postgres tables +// Persists converted logs into custom postgres tables // Calls selected methods, using token holder address generated during event log conversion func (tr *Transformer) Execute() error { if len(tr.Contracts) == 0 { From e1a0d894a2fd5fc8ed2dd68b6338febc6d54ad68 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Tue, 7 May 2019 17:24:10 -0500 Subject: [PATCH 03/19] Update blockchain method GetHeaderByNumbers -> GetHeadersByNumbers --- pkg/core/blockchain.go | 2 +- pkg/fakes/mock_blockchain.go | 2 +- pkg/geth/blockchain.go | 2 +- pkg/geth/blockchain_test.go | 4 ++-- pkg/history/populate_headers.go | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index be2fe9eb..68de3cc1 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -30,7 +30,7 @@ type BlockChain interface { GetBlockByNumber(blockNumber int64) (Block, error) GetEthLogsWithCustomQuery(query ethereum.FilterQuery) ([]types.Log, error) GetHeaderByNumber(blockNumber int64) (Header, error) - GetHeaderByNumbers(blockNumbers []int64) ([]Header, error) + GetHeadersByNumbers(blockNumbers []int64) ([]Header, error) GetLogs(contract Contract, startingBlockNumber *big.Int, endingBlockNumber *big.Int) ([]Log, error) GetTransactions(transactionHashes []common.Hash) ([]TransactionModel, error) LastBlock() (*big.Int, error) diff --git a/pkg/fakes/mock_blockchain.go b/pkg/fakes/mock_blockchain.go index d09a91b3..8ac7f825 100644 --- a/pkg/fakes/mock_blockchain.go +++ b/pkg/fakes/mock_blockchain.go @@ -98,7 +98,7 @@ func (chain *MockBlockChain) GetHeaderByNumber(blockNumber int64) (core.Header, return core.Header{BlockNumber: blockNumber}, nil } -func (chain *MockBlockChain) GetHeaderByNumbers(blockNumbers []int64) ([]core.Header, error) { +func (chain *MockBlockChain) GetHeadersByNumbers(blockNumbers []int64) ([]core.Header, error) { var headers []core.Header for _, blockNumber := range blockNumbers { var header = core.Header{BlockNumber: int64(blockNumber)} diff --git a/pkg/geth/blockchain.go b/pkg/geth/blockchain.go index 5d2c156c..7048487b 100644 --- a/pkg/geth/blockchain.go +++ b/pkg/geth/blockchain.go @@ -79,7 +79,7 @@ func (blockChain *BlockChain) GetHeaderByNumber(blockNumber int64) (header core. return blockChain.getPOWHeader(blockNumber) } -func (blockChain *BlockChain) GetHeaderByNumbers(blockNumbers []int64) (header []core.Header, err error) { +func (blockChain *BlockChain) GetHeadersByNumbers(blockNumbers []int64) (header []core.Header, err error) { if blockChain.node.NetworkID == core.KOVAN_NETWORK_ID { return blockChain.getPOAHeaders(blockNumbers) } diff --git a/pkg/geth/blockchain_test.go b/pkg/geth/blockchain_test.go index d70eda92..e0a8e9c2 100644 --- a/pkg/geth/blockchain_test.go +++ b/pkg/geth/blockchain_test.go @@ -93,7 +93,7 @@ var _ = Describe("Geth blockchain", func() { }) It("fetches headers with multiple blocks", func() { - _, err := blockChain.GetHeaderByNumbers([]int64{100, 99}) + _, err := blockChain.GetHeadersByNumbers([]int64{100, 99}) Expect(err).NotTo(HaveOccurred()) mockRpcClient.AssertBatchCalledWith("eth_getBlockByNumber", 2) @@ -139,7 +139,7 @@ var _ = Describe("Geth blockchain", func() { blockNumber := hexutil.Big(*big.NewInt(100)) mockRpcClient.SetReturnPOAHeaders([]vulcCore.POAHeader{{Number: &blockNumber}}) - _, err := blockChain.GetHeaderByNumbers([]int64{100, 99}) + _, err := blockChain.GetHeadersByNumbers([]int64{100, 99}) Expect(err).NotTo(HaveOccurred()) mockRpcClient.AssertBatchCalledWith("eth_getBlockByNumber", 2) diff --git a/pkg/history/populate_headers.go b/pkg/history/populate_headers.go index e545fd23..c75d4213 100644 --- a/pkg/history/populate_headers.go +++ b/pkg/history/populate_headers.go @@ -48,8 +48,8 @@ func PopulateMissingHeaders(blockchain core.BlockChain, headerRepository datasto return len(blockNumbers), nil } -func RetrieveAndUpdateHeaders(chain core.BlockChain, headerRepository datastore.HeaderRepository, blockNumbers []int64) (int, error) { - headers, err := chain.GetHeaderByNumbers(blockNumbers) +func RetrieveAndUpdateHeaders(blockchain core.BlockChain, headerRepository datastore.HeaderRepository, blockNumbers []int64) (int, error) { + headers, err := blockchain.GetHeadersByNumbers(blockNumbers) for _, header := range headers { _, err = headerRepository.CreateOrUpdateHeader(header) if err != nil { From f7d520c933f7c8d18d3c0cfaa9517368694a4f3b Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Tue, 7 May 2019 17:27:21 -0500 Subject: [PATCH 04/19] Update header sync transformer alias in ContractWatcher --- cmd/contractWatcher.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/contractWatcher.go b/cmd/contractWatcher.go index a8c103ed..5e2cddc9 100644 --- a/cmd/contractWatcher.go +++ b/cmd/contractWatcher.go @@ -26,7 +26,7 @@ import ( st "github.com/vulcanize/vulcanizedb/libraries/shared/transformer" ft "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/full/transformer" - lt "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/header/transformer" + ht "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/header/transformer" "github.com/vulcanize/vulcanizedb/utils" ) @@ -99,7 +99,7 @@ func contractWatcher() { con.PrepConfig() switch mode { case "header": - t = lt.NewTransformer(con, blockChain, &db) + t = ht.NewTransformer(con, blockChain, &db) case "full": t = ft.NewTransformer(con, blockChain, &db) default: From 436d9b9cb1a180f9975e7688f68dbca28f1cad58 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Wed, 8 May 2019 11:44:04 -0500 Subject: [PATCH 05/19] Add VDB overview diagram to README --- README.md | 7 +++++-- documentation/vdb-overview.png | Bin 0 -> 53018 bytes 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 documentation/vdb-overview.png diff --git a/README.md b/README.md index 3f613b71..02d83da4 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,17 @@ ## 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. +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. 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. +![VulcanizeDB Overview Diagram](../documentation-updates/documentation/diagrams/vdb-overview.png) + ## Dependencies - Go 1.11+ - Postgres 10.6 diff --git a/documentation/vdb-overview.png b/documentation/vdb-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..fe4e4d67101be00e59a5d43fbe7bc968a541c7a7 GIT binary patch literal 53018 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 JE^i+2e*mfm*(U%1 literal 0 HcmV?d00001 From a49f5d7582dbb039bc802ab8c98989f4c155a05e Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Wed, 8 May 2019 12:49:56 -0500 Subject: [PATCH 06/19] Add repository maintenance documentation --- documentation/{ => diagrams}/vdb-overview.png | Bin documentation/repository-maintenance.md | 27 ++++++++++++++++++ 2 files changed, 27 insertions(+) rename documentation/{ => diagrams}/vdb-overview.png (100%) create mode 100644 documentation/repository-maintenance.md diff --git a/documentation/vdb-overview.png b/documentation/diagrams/vdb-overview.png similarity index 100% rename from documentation/vdb-overview.png rename to documentation/diagrams/vdb-overview.png diff --git a/documentation/repository-maintenance.md b/documentation/repository-maintenance.md new file mode 100644 index 00000000..317ad92f --- /dev/null +++ b/documentation/repository-maintenance.md @@ -0,0 +1,27 @@ +# Repository Maintenance + +## Diagrams +- Diagrams were created with [draw.io](draw.io). +- To update a diagram: + 1. Go to [draw.io](draw.io). + 1. Click on *File > 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 add 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} +``` + +More information on why your github token is needed, and how to generate it here:https://github +.com/github-changelog-generator/github-changelog-generator#github-token \ No newline at end of file From 5d1ba5903cb72374ab815c7c3668e9e92d1851e6 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Wed, 8 May 2019 13:19:32 -0500 Subject: [PATCH 07/19] Update contributing guidelines --- README.md | 7 +++---- documentation/contributing.md | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 02d83da4..94075541 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,9 @@ Documentation on how to build custom transformers to work with these commands ca ## Contributing -Contributions are welcome! For more on this, please see [here](../staging/documentation/contributing.md). - -Small note: If editing the Readme, please conform to the [standard-readme specification](https://github.com/RichardLitt/standard-readme). +Contributions are welcome! +For more on this, please see [here](../staging/documentation/contributing.md). ## License -[AGPL-3.0](../staging/LICENSE) © Vulcanize Inc +[AGPL-3.0](../staging/LICENSE) © Vulcanize Inc \ No newline at end of file diff --git a/documentation/contributing.md b/documentation/contributing.md index db255507..2dfa2102 100644 --- a/documentation/contributing.md +++ b/documentation/contributing.md @@ -1,11 +1,23 @@ # Contribution guidelines -Contributions are welcome! In addition to core contributions, developers are encouraged to build their own custom transformers which +Contributions are welcome! Please open an Issues or Pull Request for any changes. + +In addition to core contributions, developers are encouraged to build their own custom transformers which can be run together with other custom transformers using the [composeAndExeucte](../../staging/documentation/composeAndExecute.md) command. +## Pull Requests +- `go fmt` is run as part of `make test` and `make integrationtest`, please make sure to check in the format changes. +- Ensure that new code is well tested, including integration testing if applicable. +- Make sure the build is passing. +- Update the README or any [documentation files](./) as necessary. If editing the Readme, please +conform to the +[standard-readme specification](https://github.com/RichardLitt/standard-readme). +- You may merge a Pull Request once you have an approval from core developer. + ## Creating a new migration file 1. `make new_migration NAME=add_columnA_to_table1` - This will create a new timestamped migration file in `db/migrations` 1. Write the migration code in the created file, under the respective `goose` pragma - Goose automatically runs each migration in a transaction; don't add `BEGIN` and `COMMIT` statements. -1. Core migrations should be committed in their `goose fix`ed form. \ No newline at end of file +1. Core migrations should be committed in their `goose fix`ed form. To do this, run `make version_migrations` which +converts timestamped migrations to migrations versioned by an incremented integer. From ade1429ce32ebeb87c3bf22ea8000708fef5ed44 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Wed, 8 May 2019 17:16:36 -0500 Subject: [PATCH 08/19] Update transformer documentation broken into generic and custom --- README.md | 26 +++--- ...seAndExecute.md => custom-transformers.md} | 91 +++++++++++++------ ...tractWatcher.md => generic-transformer.md} | 2 +- documentation/transformers.md | 40 -------- 4 files changed, 79 insertions(+), 80 deletions(-) rename documentation/{composeAndExecute.md => custom-transformers.md} (55%) rename documentation/{contractWatcher.md => generic-transformer.md} (99%) delete mode 100644 documentation/transformers.md diff --git a/README.md b/README.md index 94075541..04163b7b 100644 --- a/README.md +++ b/README.md @@ -111,20 +111,24 @@ As mentioned above, VulcanizeDB's processes can be split into three categories: ### 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`, `sync`, or `coldImport` commands. -These commands are described in detail [here](../staging/documentation/sync.md). +These commands are described in detail [here](../documentation-updates/documentation/sync.md). ### Data transformation -Contract watchers use the raw data that has been synced into Postgres to filter out and apply transformations to specific data of interest. +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: -There is a built-in `contractWatcher` command which provides generic transformation of most contract data. -The `contractWatcher` command is described further [here](../staging/documentation/contractWatcher.md). +- 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-updates/documentation/generic-transformer.md). -In many cases a custom transformer or set of transformers will need to be written to provide complete or more comprehensive coverage or to optimize other aspects of the output for a specific end-use. -In this case we have provided the `compose`, `execute`, and `composeAndExecute` commands for running custom transformers from external repositories. - -Usage of the `compose`, `execute`, and `composeAndExecute` commands is described further [here](../staging/documentation/composeAndExecute.md). - -Documentation on how to build custom transformers to work with these commands can be found [here](../staging/documentation/transformers.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-updates/documentation/custom-transformers.md). ### 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](../staging/documentation/postgraphile.md). @@ -142,7 +146,7 @@ Documentation on how to build custom transformers to work with these commands ca ## Contributing Contributions are welcome! -For more on this, please see [here](../staging/documentation/contributing.md). +For more on this, please see [here](../documentation-updates/documentation/contributing.md). ## License [AGPL-3.0](../staging/LICENSE) © Vulcanize Inc \ No newline at end of file diff --git a/documentation/composeAndExecute.md b/documentation/custom-transformers.md similarity index 55% rename from documentation/composeAndExecute.md rename to documentation/custom-transformers.md index ac963392..41d0a449 100644 --- a/documentation/composeAndExecute.md +++ b/documentation/custom-transformers.md @@ -1,43 +1,78 @@ -# composeAndExecute -The `composeAndExecute` command is used to compose and execute over an arbitrary set of custom transformers. -This is accomplished by generating a Go plugin which allows the `vulcanizedb` binary to link to external transformers, so -long as they abide by one of the standard [interfaces](../staging/libraries/shared/transformer). +# Custom Transformers +When the capabilities of the generic `contractWatcher` are not sufficient, custom transformers tailored to a specific +purpose can be leveraged. -Additionally, there are separate `compose` and `execute` commands to allow pre-building and linking to a pre-built .so file. +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). -**NOTE:** -1. 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. -1. 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 migrations for that plugin will first need to be manually ran against that environment's Postgres database. +## Writing custom transformers +For help with writing different types of custom transformers please see below: -These commands require Go 1.11+ and use [Go plugins](https://golang.org/pkg/plugin/) which only work on Unix-based systems. -There is also an ongoing [conflict](https://github.com/golang/go/issues/20481) between Go plugins and the use vendored dependencies which -imposes certain limitations on how the plugins are built. +Storage Transformers + * [Guide](../../staging/libraries/shared/factories/storage/README.md) + * [Example](../../staging/libraries/shared/factories/storage/EXAMPLE.md) -## Commands -The `compose` and `composeAndExecute` commands assume you are in the vulcanizdb directory located at your system's `$GOPATH`, -and that all of the transformer repositories for building the plugin are present at their `$GOPATH` directories. +Event Transformers + * [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) -The `execute` command does not require the plugin transformer dependencies be located in their -`$GOPATH` directories, instead it expects a prebuilt .so file (of the name specified in the config file) -to be in `$GOPATH/src/github.com/vulcanize/vulcanizedb/plugins/` and, as noted above, also expects the plugin -db migrations to have already been ran against the database. +Contract Transformers + * [Example 1](https://github.com/vulcanize/account_transformers) + * [Example 2](https://github.com/vulcanize/ens_transformers/tree/master/transformers/domain_records) -compose: +## Preparing custom transformers to work as part of a plugin +To plug in an external transformer we need to: -`./vulcanizedb compose --config=./environments/config_name.toml` +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) -execute: +To update a plugin repository with changes to the core vulcanizedb repository, run `dep ensure` to update its dependencies. -`./vulcanizedb execute --config=./environments/config_name.toml` +## 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. -composeAndExecute: +* There is an ongoing [conflict](https://github.com/golang/go/issues/20481) between Go plugins and the use vendored +dependencies which imposes certain limitations on how the plugins are built. -`./vulcanizedb composeAndExecute --config=./environments/config_name.toml` +* Separate `compose` and `execute` commands allow pre-building and linking to a 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 migrations for that plugin will first need to + be manually ran against that environment's Postgres database. + +* The `compose` and `composeAndExecute` commands assume you are in the vulcanizdb directory located at your system's +`$GOPATH`, and that all of the transformer repositories for building the plugin are present at their `$GOPATH` directories. -## Flags +* The `execute` command does not require the plugin transformer dependencies be located in their `$GOPATH` directories, +instead it expects a prebuilt .so file (of the name specified in the config file) to be in +`$GOPATH/src/github.com/vulcanize/vulcanizedb/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 `compose` 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. @@ -50,7 +85,7 @@ Defaults to `false`. 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 +### Configuration A .toml config file is specified when executing the commands. The config provides information for composing a set of transformers from external repositories: diff --git a/documentation/contractWatcher.md b/documentation/generic-transformer.md similarity index 99% rename from documentation/contractWatcher.md rename to documentation/generic-transformer.md index 7eff6603..6ec9207a 100644 --- a/documentation/contractWatcher.md +++ b/documentation/generic-transformer.md @@ -1,4 +1,4 @@ -# contractWatcher +# 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 diff --git a/documentation/transformers.md b/documentation/transformers.md deleted file mode 100644 index b9a26ffd..00000000 --- a/documentation/transformers.md +++ /dev/null @@ -1,40 +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 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. - -## Writing custom transformers -For help with writing different types of custom transformers for the `composeAndExecute` set of commands, please see the below: - -Storage Transformers - * [Guide](../../staging/libraries/shared/factories/storage/README.md) - * [Example](../../staging/libraries/shared/factories/storage/EXAMPLE.md) - -Event Transformers - * [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 - * [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, run `dep ensure` to update its dependencies. From 7d1b334bda6e17e4fdc91a9630a0204865e67d23 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Fri, 10 May 2019 09:35:44 -0500 Subject: [PATCH 09/19] Update postgraphile documentation to mention no-ignore-rbac flag --- documentation/postgraphile.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/postgraphile.md b/documentation/postgraphile.md index 8a77b8ce..06bf11ab 100644 --- a/documentation/postgraphile.md +++ b/documentation/postgraphile.md @@ -9,13 +9,14 @@ As of April 30, 2019, you can run Postgraphile pointed at the default `vulcanize ``` npm install -g postgraphile -postgraphile --connection postgres://localhost/vulcanize_public --schema=public,custom --disable-default-mutations +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](../staging/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 From 8dc9ddf65e4004bcb430892594f4cc999a0e6393 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Fri, 10 May 2019 10:07:25 -0500 Subject: [PATCH 10/19] Add Code of Conduct --- README.md | 4 +++- documentation/contributing.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 04163b7b..13c54f2d 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,9 @@ build and run custom transformers as Go plugins can be found ## Contributing Contributions are welcome! -For more on this, please see [here](../documentation-updates/documentation/contributing.md). +VulcanizeDB follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct). + +For more information on contributing, please see [here](../documentation/contributing.md). ## License [AGPL-3.0](../staging/LICENSE) © Vulcanize Inc \ No newline at end of file diff --git a/documentation/contributing.md b/documentation/contributing.md index 2dfa2102..369cbd95 100644 --- a/documentation/contributing.md +++ b/documentation/contributing.md @@ -21,3 +21,5 @@ conform to the - Goose automatically runs each migration in a transaction; don't add `BEGIN` and `COMMIT` statements. 1. Core migrations should be committed in their `goose fix`ed form. To do this, run `make version_migrations` which converts timestamped migrations to migrations versioned by an incremented integer. + +VulcanizeDB follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct). From bd3e841e213ee8d9cd655df52b321f86f42b4867 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Fri, 10 May 2019 10:21:08 -0500 Subject: [PATCH 11/19] Update links in README to be relative to current branch --- README.md | 38 +++++++++++++++++------------------ documentation/postgraphile.md | 3 ++- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 13c54f2d..30dad4de 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,13 @@ ## Table of Contents -1. [Background](../staging/README.md#background) -1. [Dependencies](../staging/README.md#dependencies) -1. [Install](../staging/README.md#install) -1. [Usage](../staging/README.md#usage) -1. [Tests](../staging/README.md#tests) -1. [API](../staging/README.md#API) -1. [Contributing](../staging/README.md#contributing) -1. [License](../staging/README.md#license) +1. [Background](#background) +1. [Dependencies](#dependencies) +1. [Install](#install) +1. [Usage](#usage) +1. [Tests](#tests) +1. [Contributing](#contributing) +1. [License](#license) ## Background @@ -28,7 +27,7 @@ querying an Ethereum node and then persisting core data into a Postgres database 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. -![VulcanizeDB Overview Diagram](../documentation-updates/documentation/diagrams/vdb-overview.png) +![VulcanizeDB Overview Diagram](documentation/diagrams/vdb-overview.png) ## Dependencies - Go 1.11+ @@ -39,9 +38,9 @@ data from VulcanizeDB's underlying Postgres database and making it accessible. ## Install -1. [Building the project](../staging/README.md#building-the-project) -1. [Setting up the database](../staging/README.md#setting-up-the-database) -1. [Configuring a synced Ethereum node](../staging/README.md#configuring-a-synced-ethereum-node) +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) ### Building the project Download the codebase to your local `GOPATH` via: @@ -75,7 +74,8 @@ It can be additionally helpful to add `$GOPATH/bin` to your shell's `$PATH`. * See below for configuring additional environments -In some cases (such as recent Ubuntu systems), it may be necessary to overcome failures of password authentication from localhost. To allow access on Ubuntu, set localhost connections via hostname, ipv4, and ipv6 from peer/md5 to trust in: /etc/postgresql//pg_hba.conf +In some cases (such as recent Ubuntu systems), it may be necessary to overcome failures of password authentication from +localhost. To allow access on Ubuntu, set localhost connections via hostname, ipv4, and ipv6 from peer/md5 to trust in: /etc/postgresql//pg_hba.conf (It should be noted that trusted auth should only be enabled on systems without sensitive data in them: development and local test databases) @@ -111,7 +111,7 @@ As mentioned above, VulcanizeDB's processes can be split into three categories: ### 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`, `sync`, or `coldImport` commands. -These commands are described in detail [here](../documentation-updates/documentation/sync.md). +These commands are described in detail [here](documentation/sync.md). ### Data transformation Data transformation uses the raw data that has been synced into Postgres to filter out and apply transformations to @@ -122,16 +122,16 @@ 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-updates/documentation/generic-transformer.md). +`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-updates/documentation/custom-transformers.md). +[here](documentation/custom-transformers.md). ### 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](../staging/documentation/postgraphile.md). +[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). ## Tests @@ -148,7 +148,7 @@ Contributions are welcome! VulcanizeDB follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct). -For more information on contributing, please see [here](../documentation/contributing.md). +For more information on contributing, please see [here](documentation/contributing.md). ## License -[AGPL-3.0](../staging/LICENSE) © Vulcanize Inc \ No newline at end of file +[AGPL-3.0](LICENSE) © Vulcanize Inc \ No newline at end of file diff --git a/documentation/postgraphile.md b/documentation/postgraphile.md index 06bf11ab..08f5051c 100644 --- a/documentation/postgraphile.md +++ b/documentation/postgraphile.md @@ -13,7 +13,8 @@ postgraphile --connection postgres://localhost/vulcanize_public --schema=public, ``` Arguments: -- `--connection` specifies the database. The above command connects to the default `vulcanize_public` database defined in [the example config](../staging/environments/public.toml.example). +- `--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. From fa03716cb2024e38978f660dbf1808133a9c740a Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Fri, 10 May 2019 11:28:20 -0500 Subject: [PATCH 12/19] Address small PR comments --- documentation/contributing.md | 2 +- documentation/custom-transformers.md | 14 +++++++------- pkg/history/populate_headers.go | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/documentation/contributing.md b/documentation/contributing.md index 369cbd95..a15f0b8a 100644 --- a/documentation/contributing.md +++ b/documentation/contributing.md @@ -12,7 +12,7 @@ can be run together with other custom transformers using the [composeAndExeucte] - Update the README or any [documentation files](./) as necessary. If editing the Readme, please conform to the [standard-readme specification](https://github.com/RichardLitt/standard-readme). -- You may merge a Pull Request once you have an approval from core developer. +- Once a Pull Request has received two approvals it can be merged in by a core developer. ## Creating a new migration file 1. `make new_migration NAME=add_columnA_to_table1` diff --git a/documentation/custom-transformers.md b/documentation/custom-transformers.md index 41d0a449..a29aabd4 100644 --- a/documentation/custom-transformers.md +++ b/documentation/custom-transformers.md @@ -46,10 +46,10 @@ To update a plugin repository with changes to the core vulcanizedb repository, r * 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 vendored +* 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 a pre-built .so file. So, if +* 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. @@ -61,19 +61,19 @@ these are run independently, instead of using `composeAndExecute`, a couple of t `$GOPATH`, and that all of the transformer repositories for building the plugin 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 prebuilt .so file (of the name specified in the config file) to be in +instead it expects a .so file (of the name specified in the config file) to be in `$GOPATH/src/github.com/vulcanize/vulcanizedb/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` + * compose: `./vulcanizedb compose --config=environments/config_name.toml` - * execute: `./vulcanizedb execute --config=./environments/config_name.toml` + * execute: `./vulcanizedb execute --config=environments/config_name.toml` - * composeAndExecute: `./vulcanizedb composeAndExecute --config=./environments/config_name.toml` + * composeAndExecute: `./vulcanizedb composeAndExecute --config=environments/config_name.toml` ### Flags -The `compose` and `composeAndExecute` commands can be passed optional flags to specify the operation of the watchers: +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. diff --git a/pkg/history/populate_headers.go b/pkg/history/populate_headers.go index c75d4213..dd8028ce 100644 --- a/pkg/history/populate_headers.go +++ b/pkg/history/populate_headers.go @@ -24,14 +24,14 @@ import ( "github.com/vulcanize/vulcanizedb/pkg/datastore/postgres/repositories" ) -func PopulateMissingHeaders(blockchain core.BlockChain, headerRepository datastore.HeaderRepository, startingBlockNumber int64) (int, error) { - lastBlock, err := blockchain.LastBlock() +func PopulateMissingHeaders(blockChain core.BlockChain, headerRepository datastore.HeaderRepository, startingBlockNumber int64) (int, error) { + lastBlock, err := blockChain.LastBlock() if err != nil { log.Error("PopulateMissingHeaders: Error getting last block: ", err) return 0, err } - blockNumbers, err := headerRepository.MissingBlockNumbers(startingBlockNumber, lastBlock.Int64(), blockchain.Node().ID) + blockNumbers, err := headerRepository.MissingBlockNumbers(startingBlockNumber, lastBlock.Int64(), blockChain.Node().ID) if err != nil { log.Error("PopulateMissingHeaders: Error getting missing block numbers: ", err) return 0, err @@ -40,7 +40,7 @@ func PopulateMissingHeaders(blockchain core.BlockChain, headerRepository datasto } log.Printf("Backfilling %d blocks\n\n", len(blockNumbers)) - _, err = RetrieveAndUpdateHeaders(blockchain, headerRepository, blockNumbers) + _, err = RetrieveAndUpdateHeaders(blockChain, headerRepository, blockNumbers) if err != nil { log.Error("PopulateMissingHeaders: Error getting/updating headers:", err) return 0, err @@ -48,8 +48,8 @@ func PopulateMissingHeaders(blockchain core.BlockChain, headerRepository datasto return len(blockNumbers), nil } -func RetrieveAndUpdateHeaders(blockchain core.BlockChain, headerRepository datastore.HeaderRepository, blockNumbers []int64) (int, error) { - headers, err := blockchain.GetHeadersByNumbers(blockNumbers) +func RetrieveAndUpdateHeaders(blockChain core.BlockChain, headerRepository datastore.HeaderRepository, blockNumbers []int64) (int, error) { + headers, err := blockChain.GetHeadersByNumbers(blockNumbers) for _, header := range headers { _, err = headerRepository.CreateOrUpdateHeader(header) if err != nil { From 829a581727e7d87e962a97485cd3e3718ae19e11 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Mon, 13 May 2019 13:31:23 -0500 Subject: [PATCH 13/19] Rename sync documentation file --- documentation/{sync.md => data-syncing.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename documentation/{sync.md => data-syncing.md} (100%) diff --git a/documentation/sync.md b/documentation/data-syncing.md similarity index 100% rename from documentation/sync.md rename to documentation/data-syncing.md From 7ddf728db2073b5f78171270082d3b478c9f7227 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Mon, 13 May 2019 13:40:46 -0500 Subject: [PATCH 14/19] Descript different custom sync transformer types --- documentation/custom-transformers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/custom-transformers.md b/documentation/custom-transformers.md index a29aabd4..8e60a471 100644 --- a/documentation/custom-transformers.md +++ b/documentation/custom-transformers.md @@ -10,17 +10,17 @@ abide by one of the standard [interfaces](../staging/libraries/shared/transforme ## Writing custom transformers For help with writing different types of custom transformers please see below: -Storage Transformers +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 +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 +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) From eba52442eccfade0815ac98f7c63c2620f3b95b3 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Mon, 13 May 2019 13:44:20 -0500 Subject: [PATCH 15/19] Restructure README to conform with standard readme spec --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 30dad4de..c7181d16 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,8 @@ ## Table of Contents 1. [Background](#background) -1. [Dependencies](#dependencies) 1. [Install](#install) 1. [Usage](#usage) -1. [Tests](#tests) 1. [Contributing](#contributing) 1. [License](#license) @@ -29,19 +27,20 @@ data from VulcanizeDB's underlying Postgres database and making it accessible. ![VulcanizeDB Overview Diagram](documentation/diagrams/vdb-overview.png) -## Dependencies +## 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.11+ - Postgres 10.6 - Ethereum Node - [Go Ethereum](https://ethereum.github.io/go-ethereum/downloads/) (1.8.23+) - [Parity 1.8.11+](https://github.com/paritytech/parity/releases) - -## Install -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) - ### Building the project Download the codebase to your local `GOPATH` via: @@ -134,7 +133,7 @@ build and run custom transformers as Go plugins can be found [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). -## Tests +### Tests - Replace the empty `ipcPath` in the `environments/infura.toml` with a path to a full node's eth_jsonrpc endpoint (e.g. local geth node ipc path or infura url) - Note: integration tests require configuration with an archival node - `createdb vulcanize_private` will create the test db From 4580f0fc2e2d6b92abecfb898e9218937320d29c Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Mon, 13 May 2019 13:57:20 -0500 Subject: [PATCH 16/19] Updates to custom-transformers doc --- documentation/custom-transformers.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/documentation/custom-transformers.md b/documentation/custom-transformers.md index 8e60a471..772988eb 100644 --- a/documentation/custom-transformers.md +++ b/documentation/custom-transformers.md @@ -54,11 +54,12 @@ these are run independently, instead of using `composeAndExecute`, a couple of t * 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 migrations for that plugin will first need to - be manually ran against that environment's Postgres database. + 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 all of the transformer repositories for building the plugin are present at their `$GOPATH` directories. +`$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 From 481988cb083b1268c39381a4498a470943f919d2 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Mon, 13 May 2019 15:52:17 -0500 Subject: [PATCH 17/19] Update date-syncing doc --- documentation/data-syncing.md | 61 ++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/documentation/data-syncing.md b/documentation/data-syncing.md index 558cf13b..e838e47c 100644 --- a/documentation/data-syncing.md +++ b/documentation/data-syncing.md @@ -7,10 +7,21 @@ Syncs block headers from a running Ethereum node into the VulcanizeDB table `hea - 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). -##### Usage -1. Start Ethereum node. -1. In a separate terminal start VulcanizeDB: -`./vulcanizedb headerSync --config --starting-block-number ` +#### 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`. ## sync Syncs blocks, transactions, receipts and logs from a running Ethereum node into VulcanizeDB tables named @@ -19,21 +30,47 @@ Syncs blocks, transactions, receipts and logs from a running Ethereum node into - Validates headers from the last 15 blocks to ensure that data is up to date. - Useful when you want to maintain a broad cache of what's happening on the blockchain. -##### Usage -1. Start Ethereum node (**if fast syncing your Ethereum node, wait for initial sync to finish**). -1. In a separate terminal start VulcanizeDB: -`./vulcanizedb sync --config --starting-block-number ` +#### Usage +- Run `./vulcanizedb sync --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`. + +*Please note, that if you are fast syncing your Ethereum node, wait for the initial sync to finish.* ## coldImport Syncs VulcanizeDB from Geth's underlying LevelDB datastore and persists Ethereum blocks, transactions, receipts and logs into VulcanizeDB tables named `blocks`, `uncles`, `full_sync_transactions`, `full_sync_receipts` and `logs` respectively. -##### Usage -1. Assure node is not running, and that it has synced to the desired block height. -1. Start vulcanize_db - - `./vulcanizedb coldImport --config ` +#### Usage +1. Ensure the Ethereum node you're point at is not running, and that it has synced to the desired block height. +1. Run `./vulcanizedb coldImport --config ` 1. Optional flags: - `--starting-block-number `/`-s `: block number to start syncing from - `--ending-block-number `/`-e `: block number to sync to - `--all`/`-a`: sync all missing blocks + +The config file can be formatted as follows, and must contain the LevelDB path. + +```toml +[database] + name = "vulcanize_public" + hostname = "localhost" + user = "vulcanize" + password = "vulcanize" + port = 5432 + +[client] + leveldbpath = "/Users/user/Library/Ethereum/geth/chaindata" +``` From d947c8f30af8f0d62bfaa3205886450a6eee20a7 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Mon, 13 May 2019 16:04:53 -0500 Subject: [PATCH 18/19] Rename sync command to fullSync --- README.md | 4 ++-- cmd/{sync.go => fullSync.go} | 22 +++++++++++----------- documentation/data-syncing.md | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) rename cmd/{sync.go => fullSync.go} (83%) diff --git a/README.md b/README.md index c7181d16..132ad108 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,8 @@ As mentioned above, VulcanizeDB's processes can be split into three categories: ### 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`, `sync`, or `coldImport` commands. -These commands are described in detail [here](documentation/sync.md). +This is accomplished through the use of the `headerSync`, `fullSync`, or `coldImport` commands. +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 diff --git a/cmd/sync.go b/cmd/fullSync.go similarity index 83% rename from cmd/sync.go rename to cmd/fullSync.go index 6dd2be07..4d29c43a 100644 --- a/cmd/sync.go +++ b/cmd/fullSync.go @@ -29,14 +29,14 @@ import ( "github.com/vulcanize/vulcanizedb/utils" ) -// syncCmd represents the sync command -var syncCmd = &cobra.Command{ - Use: "sync", +// fullSyncCmd represents the fullSync command +var fullSyncCmd = &cobra.Command{ + Use: "fullSync", Short: "Syncs VulcanizeDB with local ethereum node", Long: `Syncs VulcanizeDB with local ethereum node. Populates Postgres with blocks, transactions, receipts, and logs. -./vulcanizedb sync --starting-block-number 0 --config public.toml +./vulcanizedb fullSync --starting-block-number 0 --config public.toml Expects ethereum node to be running and requires a .toml config: @@ -49,14 +49,14 @@ Expects ethereum node to be running and requires a .toml config: ipcPath = "/Users/user/Library/Ethereum/geth.ipc" `, Run: func(cmd *cobra.Command, args []string) { - sync() + fullSync() }, } func init() { - rootCmd.AddCommand(syncCmd) + rootCmd.AddCommand(fullSyncCmd) - syncCmd.Flags().Int64VarP(&startingBlockNumber, "starting-block-number", "s", 0, "Block number to start syncing from") + fullSyncCmd.Flags().Int64VarP(&startingBlockNumber, "starting-block-number", "s", 0, "Block number to start syncing from") } func backFillAllBlocks(blockchain core.BlockChain, blockRepository datastore.BlockRepository, missingBlocksPopulated chan int, startingBlockNumber int64) { @@ -67,20 +67,20 @@ func backFillAllBlocks(blockchain core.BlockChain, blockRepository datastore.Blo missingBlocksPopulated <- populated } -func sync() { +func fullSync() { ticker := time.NewTicker(pollingInterval) defer ticker.Stop() blockChain := getBlockChain() lastBlock, err := blockChain.LastBlock() if err != nil { - log.Error("sync: Error getting last block: ", err) + log.Error("fullSync: Error getting last block: ", err) } if lastBlock.Int64() == 0 { log.Fatal("geth initial: state sync not finished") } if startingBlockNumber > lastBlock.Int64() { - log.Fatal("sync: starting block number > current block number") + log.Fatal("fullSync: starting block number > current block number") } db := utils.LoadPostgres(databaseConfig, blockChain.Node()) @@ -94,7 +94,7 @@ func sync() { case <-ticker.C: window, err := validator.ValidateBlocks() if err != nil { - log.Error("sync: error in validateBlocks: ", err) + log.Error("fullSync: error in validateBlocks: ", err) } log.Info(window.GetString()) case <-missingBlocksPopulated: diff --git a/documentation/data-syncing.md b/documentation/data-syncing.md index e838e47c..99564540 100644 --- a/documentation/data-syncing.md +++ b/documentation/data-syncing.md @@ -23,7 +23,7 @@ Syncs block headers from a running Ethereum node into the VulcanizeDB table `hea ``` - Alternatively, the ipc path can be passed as a flag instead `--client-ipcPath`. -## sync +## fullSync Syncs blocks, transactions, receipts and logs from a running Ethereum node into VulcanizeDB tables named `blocks`, `uncles`, `full_sync_transactions`, `full_sync_receipts` and `logs`. - Queries the Ethereum node using RPC calls. @@ -31,7 +31,7 @@ Syncs blocks, transactions, receipts and logs from a running Ethereum node into - Useful when you want to maintain a broad cache of what's happening on the blockchain. #### Usage -- Run `./vulcanizedb sync --config --starting-block-number ` +- Run `./vulcanizedb fullSync --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] From 622ea44a6f5542a20f04ae9ec3d7941dbbaf6fe2 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman Date: Mon, 13 May 2019 16:29:06 -0500 Subject: [PATCH 19/19] Mention reorgs in data-sync documentation --- documentation/data-syncing.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/documentation/data-syncing.md b/documentation/data-syncing.md index 99564540..52785ba5 100644 --- a/documentation/data-syncing.md +++ b/documentation/data-syncing.md @@ -5,7 +5,10 @@ These commands are used to sync raw Ethereum data into Postgres, with varying le 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). +- 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 ` @@ -29,6 +32,8 @@ Syncs blocks, transactions, receipts and logs from a running Ethereum node into - 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 to maintain a broad cache of what's happening on the blockchain. +- 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 fullSync --config --starting-block-number `