Merge pull request #5 from coinbase/patrick/initial-commit

Initial Commit
This commit is contained in:
Patrick O'Grady 2020-09-18 08:17:32 -07:00 committed by GitHub
commit bd55ee7bbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 10411 additions and 1 deletions

103
.circleci/config.yml Normal file
View file

@ -0,0 +1,103 @@
# Copyright 2020 Coinbase, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
version: 2.1
executors:
default:
docker:
- image: circleci/golang:1.13
user: root # go directory is owned by root
working_directory: /go/src/github.com/coinbase/rosetta-sdk-go
environment:
- GO111MODULE: "on"
fast-checkout: &fast-checkout
attach_workspace:
at: /go
jobs:
setup:
executor:
name: default
steps:
- checkout
- run: make deps
- persist_to_workspace:
root: /go
paths:
- src
- bin
- pkg/mod/cache
test:
executor:
name: default
steps:
- *fast-checkout
- run: make test
lint:
executor:
name: default
steps:
- *fast-checkout
- run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.27.0
- run: make lint
check-license:
executor:
name: default
steps:
- *fast-checkout
- run: make check-license
check-format:
executor:
name: default
steps:
- *fast-checkout
- run: make check-format
coverage:
executor:
name: default
steps:
- *fast-checkout
- run: make coverage
salus:
machine: true
steps:
- checkout
- run: make salus
workflows:
version: 2
build:
jobs:
- setup
- test:
requires:
- setup
- lint:
requires:
- setup
- check-license:
requires:
- setup
- check-format:
requires:
- setup
- coverage:
requires:
- setup
- salus
notify:
webhooks:
- url: https://coveralls.io/webhook?repo_token=$COVERALLS_TOKEN

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
rosetta-bitcoin
bitcoin-data

19
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,19 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

16
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,16 @@
Fixes # .
### Motivation
<!--
Does this solve a bug? Enable a new use-case? Improve an existing behavior? Concrete examples are helpful here.
-->
### Solution
<!--
What is the solution here from a high level. What are the key technical decisions and why were they made?
-->
### Open questions
<!--
(optional) Any open questions or feedback on design desired?
-->

21
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,21 @@
# Copyright 2020 Coinbase, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
rosetta-bitcoin
bitcoin-data

53
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,53 @@
# Contributing to rosetta-bitcoin
## Code of Conduct
All interactions with this project follow our [Code of Conduct][code-of-conduct].
By participating, you are expected to honor this code. Violators can be banned
from further participation in this project, or potentially all Coinbase projects.
[code-of-conduct]: https://github.com/coinbase/code-of-conduct
## Bug Reports
* Ensure your issue [has not already been reported][1]. It may already be fixed!
* Include the steps you carried out to produce the problem.
* Include the behavior you observed along with the behavior you expected, and
why you expected it.
* Include any relevant stack traces or debugging output.
## Feature Requests
We welcome feedback with or without pull requests. If you have an idea for how
to improve the project, great! All we ask is that you take the time to write a
clear and concise explanation of what need you are trying to solve. If you have
thoughts on _how_ it can be solved, include those too!
The best way to see a feature added, however, is to submit a pull request.
## Pull Requests
* Before creating your pull request, it's usually worth asking if the code
you're planning on writing will actually be considered for merging. You can
do this by [opening an issue][1] and asking. It may also help give the
maintainers context for when the time comes to review your code.
* Ensure your [commit messages are well-written][2]. This can double as your
pull request message, so it pays to take the time to write a clear message.
* Add tests for your feature. You should be able to look at other tests for
examples. If you're unsure, don't hesitate to [open an issue][1] and ask!
* Submit your pull request!
## Support Requests
For security reasons, any communication referencing support tickets for Coinbase
products will be ignored. The request will have its content redacted and will
be locked to prevent further discussion.
All support requests must be made via [our support team][3].
[1]: https://github.com/coinbase/rosetta-bitcoin/issues
[2]: https://medium.com/brigade-engineering/the-secrets-to-great-commit-messages-106fc0a92a25
[3]: https://support.coinbase.com/customer/en/portal/articles/2288496-how-can-i-contact-coinbase-support-

89
Dockerfile Normal file
View file

@ -0,0 +1,89 @@
# Copyright 2020 Coinbase, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Build bitcoind
FROM ubuntu:18.04 as bitcoind-builder
RUN mkdir -p /app \
&& chown -R nobody:nogroup /app
WORKDIR /app
RUN apt-get update && apt-get install -y make gcc g++ autoconf autotools-dev bsdmainutils build-essential git libboost-all-dev \
libcurl4-openssl-dev libdb++-dev libevent-dev libssl-dev libtool pkg-config python python-pip libzmq3-dev wget
# VERSION: Bitcoin Core 0.20.1
RUN git clone https://github.com/bitcoin/bitcoin \
&& cd bitcoin \
&& git checkout 7ff64311bee570874c4f0dfa18f518552188df08
RUN cd bitcoin \
&& ./autogen.sh \
&& ./configure --enable-glibc-back-compat --disable-tests --without-miniupnpc --without-gui --with-incompatible-bdb --disable-hardening --disable-zmq --disable-bench --disable-wallet \
&& make
RUN mv bitcoin/src/bitcoind /app/bitcoind \
&& rm -rf bitcoin
# Build Rosetta Server Components
FROM ubuntu:18.04 as rosetta-builder
RUN mkdir -p /app \
&& chown -R nobody:nogroup /app
WORKDIR /app
RUN apt-get update && apt-get install -y curl make gcc g++
ENV GOLANG_VERSION 1.15.2
ENV GOLANG_DOWNLOAD_SHA256 b49fda1ca29a1946d6bb2a5a6982cf07ccd2aba849289508ee0f9918f6bb4552
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
&& echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
&& tar -C /usr/local -xzf golang.tar.gz \
&& rm golang.tar.gz
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
# Use native remote build context to build in any directory
COPY . src
RUN cd src \
&& go build \
&& cd .. \
&& mv src/rosetta-bitcoin /app/rosetta-bitcoin \
&& mv src/assets/* /app \
&& rm -rf src
## Build Final Image
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y libevent-dev libboost-system-dev libboost-filesystem-dev libboost-test-dev libboost-thread-dev
RUN mkdir -p /app \
&& chown -R nobody:nogroup /app \
&& mkdir -p /data \
&& chown -R nobody:nogroup /data
WORKDIR /app
# Copy binaries from build containers
COPY --from=bitcoind-builder /app/* /app/
# Copy configuration files and set permissions
COPY --from=rosetta-builder /app/* /app/
# Set permissions for everything added to /app
RUN chmod -R 755 /app/*
CMD ["/app/rosetta-bitcoin"]

201
LICENSE.txt Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Coinbase, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

84
Makefile Normal file
View file

@ -0,0 +1,84 @@
.PHONY: deps build run lint mocks run-mainnet-online run-mainnet-offline run-testnet-online \
run-testnet-offline check-comments add-license check-license shorten-lines test \
coverage spellcheck salus build-local coverage-local format check-format
ADDLICENSE_CMD=go run github.com/google/addlicense
ADDLICENCE_SCRIPT=${ADDLICENSE_CMD} -c "Coinbase, Inc." -l "apache" -v
SPELLCHECK_CMD=go run github.com/client9/misspell/cmd/misspell
GOLINES_CMD=go run github.com/segmentio/golines
GOLINT_CMD=go run golang.org/x/lint/golint
GOIMPORTS_CMD=go run golang.org/x/tools/cmd/goimports
GO_PACKAGES=./services/... ./indexer/... ./bitcoin/... ./configuration/...
GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g")
TEST_SCRIPT=go test ${GO_PACKAGES}
LINT_SETTINGS=golint,misspell,gocyclo,gocritic,whitespace,goconst,gocognit,bodyclose,unconvert,lll,unparam
PWD=$(shell pwd)
NOFILE=100000
deps:
go get ./...
build:
docker build -t rosetta-bitcoin:latest https://github.com/coinbase/rosetta-bitcoin.git
build-local:
docker build -t rosetta-bitcoin:latest .
run-mainnet-online:
docker run -d --ulimit "nofile=${NOFILE}:${NOFILE}" -v "${PWD}/bitcoin-data:/data" -e "MODE=ONLINE" -e "NETWORK=MAINNET" -e "PORT=8080" -p 8080:8080 -p 8333:8333 rosetta-bitcoin:latest
run-mainnet-offline:
docker run -d -e "MODE=OFFLINE" -e "NETWORK=MAINNET" -e "PORT=8081" -p 8081:8081 rosetta-bitcoin:latest
run-testnet-online:
docker run -d --ulimit "nofile=${NOFILE}:${NOFILE}" -v "${PWD}/bitcoin-data:/data" -e "MODE=ONLINE" -e "NETWORK=TESTNET" -e "PORT=8080" -p 8080:8080 -p 18333:18333 rosetta-bitcoin:latest
run-testnet-offline:
docker run -d -e "MODE=OFFLINE" -e "NETWORK=TESTNET" -e "PORT=8081" -p 8081:8081 rosetta-bitcoin:latest
train:
./zstd-train.sh $(network) transaction $(data-directory)
check-comments:
${GOLINT_CMD} -set_exit_status ${GO_FOLDERS} .
lint: | check-comments
golangci-lint run -v -E ${LINT_SETTINGS},gomnd
add-license:
${ADDLICENCE_SCRIPT} .
check-license:
${ADDLICENCE_SCRIPT} -check .
shorten-lines:
${GOLINES_CMD} -w --shorten-comments ${GO_FOLDERS} .
format:
gofmt -s -w -l .
${GOIMPORTS_CMD} -w .
check-format:
! gofmt -s -l . | read
! ${GOIMPORTS_CMD} -l . | read
test:
${TEST_SCRIPT}
coverage:
if [ "${COVERALLS_TOKEN}" ]; then ${TEST_SCRIPT} -coverprofile=c.out -covermode=count; ${GOVERALLS_CMD} -coverprofile=c.out -repotoken ${COVERALLS_TOKEN}; fi
coverage-local:
${TEST_SCRIPT} -cover
salus:
docker run --rm -t -v ${PWD}:/home/repo coinbase/salus
spellcheck:
${SPELLCHECK_CMD} -error .
mocks:
rm -rf mocks;
mockery --dir indexer --all --case underscore --outpkg indexer --output mocks/indexer;
mockery --dir services --all --case underscore --outpkg services --output mocks/services;
${ADDLICENCE_SCRIPT} .;

190
README.md
View file

@ -1 +1,189 @@
rosetta-bitcoin
<p align="center">
<a href="https://www.rosetta-api.org">
<img width="90%" alt="Rosetta" src="https://www.rosetta-api.org/img/rosetta_header.png">
</a>
</p>
<h3 align="center">
Rosetta Bitcoin
</h3>
<p align="center">
<a href="https://circleci.com/gh/coinbase/rosetta-bitcoin/tree/master"><img src="https://circleci.com/gh/coinbase/rosetta-bitcoin/tree/master.svg?style=shield" /></a>
<a href="https://coveralls.io/github/coinbase/rosetta-bitcoin"><img src="https://coveralls.io/repos/github/coinbase/rosetta-bitcoin/badge.svg" /></a>
<a href="https://goreportcard.com/report/github.com/coinbase/rosetta-bitcoin"><img src="https://goreportcard.com/badge/github.com/coinbase/rosetta-bitcoin" /></a>
<a href="https://github.com/coinbase/rosetta-bitcoin/blob/master/LICENSE.txt"><img src="https://img.shields.io/github/license/coinbase/rosetta-bitcoin.svg" /></a>
<a href="https://pkg.go.dev/github.com/coinbase/rosetta-bitcoin?tab=overview"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=shield" /></a>
</p>
<p align="center"><b>
ROSETTA-BITCOIN IS CONSIDERED <a href="https://en.wikipedia.org/wiki/Software_release_life_cycle#Alpha">ALPHA SOFTWARE</a>.
USE AT YOUR OWN RISK! COINBASE ASSUMES NO RESPONSIBILITY NOR LIABILITY IF THERE IS A BUG IN THIS IMPLEMENTATION.
</b></p>
## Overview
`rosetta-bitcoin` provides a reference implementation of the Rosetta API for
Bitcoin in Golang. If you haven't heard of the Rosetta API, you can find more
information [here](https://rosetta-api.org).
## Features
* Rosetta API implementation (both Data API and Construction API)
* UTXO cache for all accounts (accessible using `/account/balance`)
* Stateless, offline, curve-based transaction construction from any SegWit-Bech32 Address
## Usage
As specified in the [Rosetta API Principles](https://www.rosetta-api.org/docs/automated_deployment.html),
all Rosetta implementations must be deployable via Docker and support running via either an
[`online` or `offline` mode](https://www.rosetta-api.org/docs/node_deployment.html#multiple-modes).
To build a Docker image from this repository, run the command `make build`. To start
`rosetta-bitcoin`, you can run:
* `make run-mainnet-online`
* `make run-mainnet-offline`
* `make run-testnet-online`
* `make run testnet-offline`
By default, running these commands will create a data directory at `<working directory>/bitcoin-data`
and start the `rosetta-bitcoin` server at port `8080`.
## System Requirements
`rosetta-bitcoin` has been tested on an [AWS c5.2xlarge instance](https://aws.amazon.com/ec2/instance-types/c5).
This instance type has 8 vCPU and 16 GB of RAM. If you use a computer with less than 16 GB of RAM,
it is possible that `rosetta-bitcoin` will exit with an OOM error.
### Recommended OS Settings
To increase the load `rosetta-bitcoin` can handle, it is recommended to tune your OS
settings to allow for more connections. On a linux-based OS, you can run the following
commands ([source](http://www.tweaked.io/guide/kernel)):
```text
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_max_syn_backlog=10000
sysctl -w net.core.somaxconn=10000
sysctl -p (when done)
```
_We have not tested `rosetta-bitcoin` with `net.ipv4.tcp_tw_recycle` and do not recommend
enabling it._
You should also modify your open file settings to `100000`. This can be done on a linux-based OS
with the command: `ulimit -n 100000`.
### Optimizations
* Automatically prune bitcoind while indexing blocks
* Reduce sync time with concurrent block indexing
* Use [Zstandard compression](https://github.com/facebook/zstd) to reduce the size of data stored on disk
without needing to write a manual byte-level encoding
#### Concurrent Block Syncing
To speed up indexing, `rosetta-bitcoin` uses concurrent block processing
with a "wait free" design (using channels instead of sleeps to signal
which threads are unblocked). This allows `rosetta-bitcoin` to fetch
multiple inputs from disk while it waits for inputs that appeared
in recently processed blocks to save to disk.
```text
+----------+
| bitcoind |
+-----+----+
|
|
+---------+ fetch block data / unpopulated txs |
| block 1 <------------------------------------+
+---------+ |
+--> tx 1 | |
| +---------+ |
| | tx 2 | |
| +----+----+ |
| | |
| | +---------+ |
| | | block 2 <-------------------+
| | +---------+ |
| +-----------> tx 3 +--+ |
| +---------+ | |
+-------------------> tx 4 | | |
| +---------+ | |
| | |
| retrieve previously synced | +---------+ |
| inputs needed for future | | block 3 <--+
| blocks while waiting for | +---------+
| populated blocks to save to +---> tx 5 |
| disk +---------+
+------------------------------------> tx 6 |
| +---------+
|
|
+------+--------+
| coin_storage |
+---------------+
```
### Architecture
`rosetta-bitcoin` uses the `syncer`, `storage`, `parser`, and `server` package
from [`rosetta-sdk-go`](https://github.com/coinbase/rosetta-sdk-go) instead
of a new Bitcoin-specific implementation of packages of similar functionality. Below
you can find a high-level overview of how everything fits together:
```text
+------------------------------------------------------------------+
| |
| +--------------------------------------+ |
| | | |
| | indexer | |
| | | |
| | +--------+ | |
+-------------------+ pruner <----------+ | |
| | +--------+ | | |
+-----v----+ | | | |
| bitcoind | | +------+--------+ | |
+-----+----+ | +--------> block_storage <----+ | |
| | | +---------------+ | | |
| | +---+----+ | | |
+-------------------> syncer | | | |
| +---+----+ | | |
| | +--------------+ | | |
| +--------> coin_storage | | | |
| +------^-------+ | | |
| | | | |
+--------------------------------------+ |
| | |
+-------------------------------------------------------------------------------------------+ |
| | | | |
| +------------------------------------------------------------+ | | |
| | | | |
| | +---------------------+-----------------------+------+ | |
| | | | | | |
| +-------+---------+ +-------+---------+ +-------+-------+ +-----------+----------+ | |
| | account_service | | network_service | | block_service | | construction_service +--------+
| +-----------------+ +-----------------+ +---------------+ +----------------------+ |
| |
| server |
| |
+-------------------------------------------------------------------------------------------+
```
## Testing with rosetta-cli
To validate `rosetta-bitcoin`, [install `rosetta-cli`](https://github.com/coinbase/rosetta-cli#install)
and run one of the following commands:
* `rosetta-cli check:data --configuration-file rosetta-cli-conf/bitcoin_testnet.json`
* `rosetta-cli check:construction --configuration-file rosetta-cli-conf/bitcoin_testnet.json`
* `rosetta-cli check:data --configuration-file rosetta-cli-conf/bitcoin_mainnet.json`
* `rosetta-cli check:construction --configuration-file rosetta-cli-conf/bitcoin_mainnet.json`
## Future Work
* Publish benchamrks for sync speed, storage usage, and load testing
* Rosetta API `/mempool/*` implementation
* Add CI test using `rosetta-cli` to run on each PR (likely on a regtest network)
* Add performance mode to use unlimited RAM (implementation currently optimized to use <= 16 GB of RAM)
* Support Multi-Sig Sends
_Please reach out on our [community](https://community.rosetta-api.org) if you want to tackle anything on this list!_
## Development
* `make deps` to install dependencies
* `make test` to run tests
* `make lint` to lint the source code
* `make salus` to check for security concerns
* `make build-local` to build a Docker image from the local context
* `make coverage-local` to generate a coverage report
## License
This project is available open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0).
© 2020 Coinbase

View file

@ -0,0 +1,26 @@
##
## bitcoin.conf configuration file. Lines beginning with # are comments.
##
# DO NOT USE THIS CONFIGURATION FILE IF YOU PLAN TO EXPOSE
# BITCOIND'S RPC PORT PUBLICALLY (THESE INSECURE CREDENTIALS
# COULD LEAD TO AN ATTACK). ROSETTA-BITCOIN USES THE RPC PORT
# FOR INDEXING AND TRANSACTION BROADCAST BUT NEVER PROVIDES THE
# CALLER ACCESS TO BITCOIND'S RPC PORT.
datadir=/data/bitcoind
bind=0.0.0.0
rpcbind=0.0.0.0
bantime=15
rpcallowip=0.0.0.0/0
rpcthreads=16
rpcworkqueue=1000
disablewallet=1
txindex=0
port=8333
rpcport=8332
rpcuser=rosetta
rpcpassword=rosetta
# allow manual pruning
prune=1

View file

@ -0,0 +1,29 @@
##
## bitcoin.conf configuration file. Lines beginning with # are comments.
##
# DO NOT USE THIS CONFIGURATION FILE IF YOU PLAN TO EXPOSE
# BITCOIND'S RPC PORT PUBLICALLY (THESE INSECURE CREDENTIALS
# COULD LEAD TO AN ATTACK). ROSETTA-BITCOIN USES THE RPC PORT
# FOR INDEXING AND TRANSACTION BROADCAST BUT NEVER PROVIDES THE
# CALLER ACCESS TO BITCOIND'S RPC PORT.
datadir=/data/bitcoind
bantime=15
rpcallowip=0.0.0.0/0
rpcthreads=16
rpcworkqueue=1000
disablewallet=1
txindex=0
rpcuser=rosetta
rpcpassword=rosetta
# allow manual pruning
prune=1
testnet=1
[test]
port=18333
bind=0.0.0.0
rpcport=18332
rpcbind=0.0.0.0

17
assets/bitcoin.json Normal file
View file

@ -0,0 +1,17 @@
{
"network": {
"blockchain": "Bitcoin",
"network": "Mainnet"
},
"online_url": "http://localhost:8080",
"data_directory": "",
"http_timeout": 10,
"sync_concurrency": 8,
"transaction_concurrency": 16,
"tip_delay": 300,
"disable_memory_limit": false,
"log_configuration": false,
"data": {
"historical_balance_disabled": true
}
}

Binary file not shown.

Binary file not shown.

834
bitcoin/client.go Normal file
View file

@ -0,0 +1,834 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitcoin
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"strconv"
"time"
bitcoinUtils "github.com/coinbase/rosetta-bitcoin/utils"
"github.com/btcsuite/btcutil"
"github.com/coinbase/rosetta-sdk-go/storage"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/coinbase/rosetta-sdk-go/utils"
)
const (
// genesisBlockIndex is the height of the block we consider to be the
// genesis block of the bitcoin blockchain for polling
genesisBlockIndex = 0
// requestID is the JSON-RPC request ID we use for making requests.
// We don't need unique request IDs because we're processing all of
// our requests synchronously.
requestID = 1
// jSONRPCVersion is the JSON-RPC version we use for making requests
jSONRPCVersion = "2.0"
// blockVerbosity represents the verbose level used when fetching blocks
// * 0 returns the hex representation
// * 1 returns the JSON representation
// * 2 returns the JSON representation with included Transaction data
blockVerbosity = 2
)
type requestMethod string
const (
// https://bitcoin.org/en/developer-reference#getblock
requestMethodGetBlock requestMethod = "getblock"
// https://bitcoin.org/en/developer-reference#getblockhash
requestMethodGetBlockHash requestMethod = "getblockhash"
// https://bitcoin.org/en/developer-reference#getblockchaininfo
requestMethodGetBlockchainInfo requestMethod = "getblockchaininfo"
// https://developer.bitcoin.org/reference/rpc/getpeerinfo.html
requestMethodGetPeerInfo requestMethod = "getpeerinfo"
// https://developer.bitcoin.org/reference/rpc/pruneblockchain.html
requestMethodPruneBlockchain requestMethod = "pruneblockchain"
// https://developer.bitcoin.org/reference/rpc/sendrawtransaction.html
requestMethodSendRawTransaction requestMethod = "sendrawtransaction"
// https://developer.bitcoin.org/reference/rpc/estimatesmartfee.html
requestMethodEstimateSmartFee requestMethod = "estimatesmartfee"
// blockNotFoundErrCode is the RPC error code when a block cannot be found
blockNotFoundErrCode = -5
)
const (
defaultTimeout = 100 * time.Second
dialTimeout = 5 * time.Second
// timeMultiplier is used to multiply the time
// returned in Bitcoin blocks to be milliseconds.
timeMultiplier = 1000
// rpc credentials are fixed in rosetta-bitcoin
// because we never expose access to the raw bitcoind
// endpoints (that could be used perform an attack, like
// changing our peers).
rpcUsername = "rosetta"
rpcPassword = "rosetta"
)
var (
// ErrBlockNotFound is returned by when the requested block
// cannot be found by the node
ErrBlockNotFound = errors.New("unable to find block")
// ErrJSONRPCError is returned when receiving an error from a JSON-RPC response
ErrJSONRPCError = errors.New("JSON-RPC error")
)
// Client is used to fetch blocks from bitcoind and
// to parse Bitcoin block data into Rosetta types.
//
// We opted not to use existing Bitcoin RPC libraries
// because they don't allow providing context
// in each request.
type Client struct {
baseURL string
genesisBlockIdentifier *types.BlockIdentifier
currency *types.Currency
httpClient *http.Client
}
// LocalhostURL returns the URL to use
// for a client that is running at localhost.
func LocalhostURL(rpcPort int) string {
return fmt.Sprintf("http://localhost:%d", rpcPort)
}
// NewClient creates a new Bitcoin client.
func NewClient(
baseURL string,
genesisBlockIdentifier *types.BlockIdentifier,
currency *types.Currency,
) *Client {
return &Client{
baseURL: baseURL,
genesisBlockIdentifier: genesisBlockIdentifier,
currency: currency,
httpClient: newHTTPClient(defaultTimeout),
}
}
// newHTTPClient returns a new HTTP client
func newHTTPClient(timeout time.Duration) *http.Client {
var netTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: dialTimeout,
}).Dial,
}
httpClient := &http.Client{
Timeout: timeout,
Transport: netTransport,
}
return httpClient
}
// NetworkStatus returns the *types.NetworkStatusResponse for
// bitcoind.
func (b *Client) NetworkStatus(ctx context.Context) (*types.NetworkStatusResponse, error) {
rawBlock, err := b.getBlock(ctx, nil)
if err != nil {
return nil, fmt.Errorf("%w: unable to get current block", err)
}
currentBlock, err := b.parseBlockData(rawBlock)
if err != nil {
return nil, fmt.Errorf("%w: unable to parse current block", err)
}
peers, err := b.GetPeers(ctx)
if err != nil {
return nil, err
}
return &types.NetworkStatusResponse{
CurrentBlockIdentifier: currentBlock.BlockIdentifier,
CurrentBlockTimestamp: currentBlock.Timestamp,
GenesisBlockIdentifier: b.genesisBlockIdentifier,
Peers: peers,
}, nil
}
// GetPeers fetches the list of peer nodes
func (b *Client) GetPeers(ctx context.Context) ([]*types.Peer, error) {
info, err := b.getPeerInfo(ctx)
if err != nil {
return nil, err
}
peers := make([]*types.Peer, len(info))
for i, peerInfo := range info {
metadata, err := types.MarshalMap(peerInfo)
if err != nil {
return nil, fmt.Errorf("%w: unable to marshal peer info", err)
}
peers[i] = &types.Peer{
PeerID: peerInfo.Addr,
Metadata: metadata,
}
}
return peers, nil
}
// GetRawBlock fetches a block (block) by *types.PartialBlockIdentifier.
func (b *Client) GetRawBlock(
ctx context.Context,
identifier *types.PartialBlockIdentifier,
) (*Block, []string, error) {
block, err := b.getBlock(ctx, identifier)
if err != nil {
return nil, nil, err
}
coins := []string{}
blockTxHashes := []string{}
for txIndex, tx := range block.Txs {
blockTxHashes = append(blockTxHashes, tx.Hash)
for inputIndex, input := range tx.Inputs {
txHash, vout, ok := b.getInputTxHash(input, txIndex, inputIndex)
if !ok {
continue
}
// If any transactions spent in the same block they are created, don't include them
// in previousTxHashes to fetch.
if !utils.ContainsString(blockTxHashes, txHash) {
coins = append(coins, CoinIdentifier(txHash, vout))
}
}
}
return block, coins, nil
}
// ParseBlock returns a parsed bitcoin block given a raw bitcoin
// block and a map of transactions containing inputs.
func (b *Client) ParseBlock(
ctx context.Context,
block *Block,
coins map[string]*storage.AccountCoin,
) (*types.Block, error) {
rblock, err := b.parseBlockData(block)
if err != nil {
return nil, err
}
txs, err := b.parseTransactions(ctx, block, coins)
if err != nil {
return nil, err
}
rblock.Transactions = txs
return rblock, nil
}
// SendRawTransaction submits a serialized transaction
// to bitcoind.
func (b *Client) SendRawTransaction(
ctx context.Context,
serializedTx string,
) (string, error) {
// Parameters:
// 1. hextring
// 2. maxfeerate (0 means accept any fee)
params := []interface{}{serializedTx, 0}
response := &sendRawTransactionResponse{}
if err := b.post(ctx, requestMethodSendRawTransaction, params, response); err != nil {
return "", fmt.Errorf("%w: error submitting raw transaction", err)
}
return response.Result, nil
}
// SuggestedFeeRate estimates the approximate fee per vKB needed
// to get a transaction in a block within conf_target.
func (b *Client) SuggestedFeeRate(
ctx context.Context,
confTarget int64,
) (float64, error) {
// Parameters:
// 1. conf_target (confirmation target in blocks)
params := []interface{}{confTarget}
response := &suggestedFeeRateResponse{}
if err := b.post(ctx, requestMethodEstimateSmartFee, params, response); err != nil {
return -1, fmt.Errorf("%w: error getting fee estimate", err)
}
return response.Result.FeeRate, nil
}
// PruneBlockchain prunes up to the provided height.
// https://bitcoincore.org/en/doc/0.20.0/rpc/blockchain/pruneblockchain
func (b *Client) PruneBlockchain(
ctx context.Context,
height int64,
) (int64, error) {
// Parameters:
// 1. Height
// https://developer.bitcoin.org/reference/rpc/pruneblockchain.html#argument-1-height
params := []interface{}{height}
response := &pruneBlockchainResponse{}
if err := b.post(ctx, requestMethodPruneBlockchain, params, response); err != nil {
return -1, fmt.Errorf("%w: error pruning blockchain", err)
}
return response.Result, nil
}
// getPeerInfo performs the `getpeerinfo` JSON-RPC request
func (b *Client) getPeerInfo(
ctx context.Context,
) ([]*PeerInfo, error) {
params := []interface{}{}
response := &peerInfoResponse{}
if err := b.post(ctx, requestMethodGetPeerInfo, params, response); err != nil {
return nil, fmt.Errorf("%w: error posting to JSON-RPC", err)
}
return response.Result, nil
}
// getBlock returns a Block for the specified identifier
func (b *Client) getBlock(
ctx context.Context,
identifier *types.PartialBlockIdentifier,
) (*Block, error) {
hash, err := b.getBlockHash(ctx, identifier)
if err != nil {
return nil, fmt.Errorf("%w: error getting block hash by identifier", err)
}
// Parameters:
// 1. Block hash (string, required)
// 2. Verbosity (integer, optional, default=1)
// https://bitcoin.org/en/developer-reference#getblock
params := []interface{}{hash, blockVerbosity}
response := &blockResponse{}
if err := b.post(ctx, requestMethodGetBlock, params, response); err != nil {
return nil, fmt.Errorf("%w: error fetching block by hash %s", err, hash)
}
return response.Result, nil
}
// getBlockchainInfo performs the `getblockchaininfo` JSON-RPC request
func (b *Client) getBlockchainInfo(
ctx context.Context,
) (*BlockchainInfo, error) {
params := []interface{}{}
response := &blockchainInfoResponse{}
if err := b.post(ctx, requestMethodGetBlockchainInfo, params, response); err != nil {
return nil, fmt.Errorf("%w: unbale to get blockchain info", err)
}
return response.Result, nil
}
// getBlockHash returns the hash for a specified block identifier.
// If the identifier includes a hash it will return that hash.
// If the identifier only includes an index, if will fetch the hash that corresponds to
// that block height from the node.
func (b *Client) getBlockHash(
ctx context.Context,
identifier *types.PartialBlockIdentifier,
) (string, error) {
// Lookup best block if no PartialBlockIdentifier provided.
if identifier == nil || (identifier.Hash == nil && identifier.Index == nil) {
info, err := b.getBlockchainInfo(ctx)
if err != nil {
return "", fmt.Errorf("%w: unable to get blockchain info", err)
}
return info.BestBlockHash, nil
}
if identifier.Hash != nil {
return *identifier.Hash, nil
}
return b.getHashFromIndex(ctx, *identifier.Index)
}
// parseBlock returns a *types.Block from a Block
func (b *Client) parseBlockData(block *Block) (*types.Block, error) {
if block == nil {
return nil, errors.New("error parsing nil block")
}
blockIndex := block.Height
previousBlockIndex := blockIndex - 1
previousBlockHash := block.PreviousBlockHash
// the genesis block's predecessor is itself
if blockIndex == genesisBlockIndex {
previousBlockIndex = genesisBlockIndex
previousBlockHash = block.Hash
}
metadata, err := block.Metadata()
if err != nil {
return nil, fmt.Errorf("%w: unable to create block metadata", err)
}
return &types.Block{
BlockIdentifier: &types.BlockIdentifier{
Hash: block.Hash,
Index: blockIndex,
},
ParentBlockIdentifier: &types.BlockIdentifier{
Hash: previousBlockHash,
Index: previousBlockIndex,
},
Timestamp: block.Time * timeMultiplier,
Metadata: metadata,
}, nil
}
// getHashFromIndex performs the `getblockhash` JSON-RPC request for the specified
// block index, and returns the hash.
// https://bitcoin.org/en/developer-reference#getblockhash
func (b *Client) getHashFromIndex(
ctx context.Context,
index int64,
) (string, error) {
// Parameters:
// 1. Block height (numeric, required)
// https://bitcoin.org/en/developer-reference#getblockhash
params := []interface{}{index}
response := &blockHashResponse{}
if err := b.post(ctx, requestMethodGetBlockHash, params, response); err != nil {
return "", fmt.Errorf(
"%w: error fetching block hash by index: %d",
err,
index,
)
}
return response.Result, nil
}
// skipTransactionOperations is used to skip operations on transactions that
// contain duplicate UTXOs (which are no longer possible after BIP-30). This
// function mirrors the behavior of a similar commit in bitcoin-core.
//
// Source: https://github.com/bitcoin/bitcoin/commit/ab91bf39b7c11e9c86bb2043c24f0f377f1cf514
func skipTransactionOperations(blockNumber int64, blockHash string, transactionHash string) bool {
if blockNumber == 91842 && blockHash == "00000000000a4d0a398161ffc163c503763b1f4360639393e0e4c8e300e0caec" &&
transactionHash == "d5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599" {
return true
}
if blockNumber == 91880 && blockHash == "00000000000743f190a18c5577a3c2d2a1f610ae9601ac046a38084ccb7cd721" &&
transactionHash == "e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468" {
return true
}
return false
}
// parseTransactions returns the transactions for a specified `Block`
func (b *Client) parseTransactions(
ctx context.Context,
block *Block,
coins map[string]*storage.AccountCoin,
) ([]*types.Transaction, error) {
logger := bitcoinUtils.ExtractLogger(ctx, "client")
if block == nil {
return nil, errors.New("error parsing nil block")
}
txs := make([]*types.Transaction, len(block.Txs))
for index, transaction := range block.Txs {
txOps, err := b.parseTxOperations(transaction, index, coins)
if err != nil {
return nil, fmt.Errorf("%w: error parsing transaction operations", err)
}
if skipTransactionOperations(block.Height, block.Hash, transaction.Hash) {
logger.Warnw(
"skipping transaction",
"block index", block.Height,
"block hash", block.Hash,
"transaction hash", transaction.Hash,
)
for _, op := range txOps {
op.Status = SkippedStatus
}
}
metadata, err := transaction.Metadata()
if err != nil {
return nil, fmt.Errorf("%w: unable to get metadata for transaction", err)
}
tx := &types.Transaction{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: transaction.Hash,
},
Operations: txOps,
Metadata: metadata,
}
txs[index] = tx
// In some cases, a transaction will spent an output
// from the same block.
for _, op := range tx.Operations {
if op.CoinChange == nil {
continue
}
if op.CoinChange.CoinAction != types.CoinCreated {
continue
}
coins[op.CoinChange.CoinIdentifier.Identifier] = &storage.AccountCoin{
Coin: &types.Coin{
CoinIdentifier: op.CoinChange.CoinIdentifier,
Amount: op.Amount,
},
Account: op.Account,
}
}
}
return txs, nil
}
// parseTransactions returns the transaction operations for a specified transaction.
// It uses a map of previous transactions to properly hydrate the input operations.
func (b *Client) parseTxOperations(
tx *Transaction,
txIndex int,
coins map[string]*storage.AccountCoin,
) ([]*types.Operation, error) {
txOps := []*types.Operation{}
for networkIndex, input := range tx.Inputs {
if bitcoinIsCoinbaseInput(input, txIndex, networkIndex) {
txOp, err := b.coinbaseTxOperation(input, int64(len(txOps)), int64(networkIndex))
if err != nil {
return nil, err
}
txOps = append(txOps, txOp)
break
}
// Fetch the *storage.AccountCoin the input is associated with
accountCoin, ok := coins[CoinIdentifier(input.TxHash, input.Vout)]
if !ok {
return nil, fmt.Errorf(
"error finding previous tx: %s, for tx: %s, input index: %d",
input.TxHash,
tx.Hash,
networkIndex,
)
}
// Parse the input transaction operation
txOp, err := b.parseInputTransactionOperation(
input,
int64(len(txOps)),
int64(networkIndex),
accountCoin,
)
if err != nil {
return nil, fmt.Errorf("%w: error parsing tx input", err)
}
txOps = append(txOps, txOp)
}
for networkIndex, output := range tx.Outputs {
txOp, err := b.parseOutputTransactionOperation(
output,
tx.Hash,
int64(len(txOps)),
int64(networkIndex),
)
if err != nil {
return nil, fmt.Errorf(
"%w: error parsing tx output, hash: %s, index: %d",
err,
tx.Hash,
networkIndex,
)
}
txOps = append(txOps, txOp)
}
return txOps, nil
}
// parseOutputTransactionOperation returns the types.Operation for the specified
// `bitcoinOutput` transaction output.
func (b *Client) parseOutputTransactionOperation(
output *Output,
txHash string,
index int64,
networkIndex int64,
) (*types.Operation, error) {
amount, err := b.parseAmount(output.Value)
if err != nil {
return nil, fmt.Errorf(
"%w: error parsing output value, hash: %s, index: %d",
err,
txHash,
index,
)
}
metadata, err := output.Metadata()
if err != nil {
return nil, fmt.Errorf("%w: unable to get output metadata", err)
}
coinChange := &types.CoinChange{
CoinIdentifier: &types.CoinIdentifier{
Identifier: fmt.Sprintf("%s:%d", txHash, networkIndex),
},
CoinAction: types.CoinCreated,
}
// If we are unable to parse the output account (i.e. bitcoind
// returns a blank/nonstandard ScriptPubKey), we create an address as the
// concatenation of the tx hash and index.
//
// Example: 4852fe372ff7534c16713b3146bbc1e86379c70bea4d5c02fb1fa0112980a081:1
// on testnet
account := b.parseOutputAccount(output.ScriptPubKey)
if len(account.Address) == 0 {
account.Address = fmt.Sprintf("%s:%d", txHash, networkIndex)
}
// If this is an OP_RETURN locking script,
// we don't create a coin because it is provably unspendable.
if output.ScriptPubKey.Type == NullData {
coinChange = nil
}
return &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: index,
NetworkIndex: &networkIndex,
},
Type: OutputOpType,
Status: SuccessStatus,
Account: account,
Amount: &types.Amount{
Value: strconv.FormatInt(int64(amount), 10),
Currency: b.currency,
},
CoinChange: coinChange,
Metadata: metadata,
}, nil
}
// getInputTxHash returns the transaction hash corresponding to an inputs previous
// output. If the input is a coinbase input, then no previous transaction is associated
// with the input.
func (b *Client) getInputTxHash(
input *Input,
txIndex int,
inputIndex int,
) (string, int64, bool) {
if bitcoinIsCoinbaseInput(input, txIndex, inputIndex) {
return "", -1, false
}
return input.TxHash, input.Vout, true
}
// bitcoinIsCoinbaseInput returns whether the specified input is
// the coinbase input. The coinbase input is always the first input in the first
// transaction, and does not contain a previous transaction hash.
func bitcoinIsCoinbaseInput(input *Input, txIndex int, inputIndex int) bool {
return txIndex == 0 && inputIndex == 0 && input.TxHash == "" && input.Coinbase != ""
}
// parseInputTransactionOperation returns the types.Operation for the specified
// Input transaction input.
func (b *Client) parseInputTransactionOperation(
input *Input,
index int64,
networkIndex int64,
accountCoin *storage.AccountCoin,
) (*types.Operation, error) {
metadata, err := input.Metadata()
if err != nil {
return nil, fmt.Errorf("%w: unable to get input metadata", err)
}
newValue, err := types.NegateValue(accountCoin.Coin.Amount.Value)
if err != nil {
return nil, fmt.Errorf("%w: unable to negate previous output", err)
}
return &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: index,
NetworkIndex: &networkIndex,
},
Type: InputOpType,
Status: SuccessStatus,
Account: accountCoin.Account,
Amount: &types.Amount{
Value: newValue,
Currency: b.currency,
},
CoinChange: &types.CoinChange{
CoinIdentifier: &types.CoinIdentifier{
Identifier: fmt.Sprintf("%s:%d", input.TxHash, input.Vout),
},
CoinAction: types.CoinSpent,
},
Metadata: metadata,
}, nil
}
// parseAmount returns the atomic value of the specified amount.
// https://godoc.org/github.com/btcsuite/btcutil#NewAmount
func (b *Client) parseAmount(amount float64) (uint64, error) {
atomicAmount, err := btcutil.NewAmount(amount)
if err != nil {
return uint64(0), fmt.Errorf("%w: error parsing amount", err)
}
if atomicAmount < 0 {
return uint64(0), fmt.Errorf("error unexpected negative amount: %d", atomicAmount)
}
return uint64(atomicAmount), nil
}
// parseOutputAccount parses a bitcoinScriptPubKey and returns an account
// identifier. The account identifier's address corresponds to the first
// address encoded in the script.
func (b *Client) parseOutputAccount(
scriptPubKey *ScriptPubKey,
) *types.AccountIdentifier {
if len(scriptPubKey.Addresses) != 1 {
return &types.AccountIdentifier{Address: scriptPubKey.Hex}
}
return &types.AccountIdentifier{Address: scriptPubKey.Addresses[0]}
}
// coinbaseTxOperation constructs a transaction operation for the coinbase input.
// This reflects an input that does not correspond to a previous output.
func (b *Client) coinbaseTxOperation(
input *Input,
index int64,
networkIndex int64,
) (*types.Operation, error) {
metadata, err := input.Metadata()
if err != nil {
return nil, fmt.Errorf("%w: unable to get input metadata", err)
}
return &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: index,
NetworkIndex: &networkIndex,
},
Type: CoinbaseOpType,
Status: SuccessStatus,
Metadata: metadata,
}, nil
}
// post makes a HTTP request to a Bitcoin node
func (b *Client) post(
ctx context.Context,
method requestMethod,
params []interface{},
response jSONRPCResponse,
) error {
rpcRequest := &request{
JSONRPC: jSONRPCVersion,
ID: requestID,
Method: string(method),
Params: params,
}
requestBody, err := json.Marshal(rpcRequest)
if err != nil {
return fmt.Errorf("%w: error marshalling RPC request", err)
}
req, err := http.NewRequest(http.MethodPost, b.baseURL, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("%w: error constructing request", err)
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(rpcUsername, rpcPassword)
// Perform the post request
res, err := b.httpClient.Do(req.WithContext(ctx))
if err != nil {
return fmt.Errorf("%w: error posting to rpc-api", err)
}
defer res.Body.Close()
// We expect JSON-RPC responses to return `200 OK` statuses
if res.StatusCode != http.StatusOK {
val, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("invalid response: %s %s", res.Status, string(val))
}
if err = json.NewDecoder(res.Body).Decode(response); err != nil {
return fmt.Errorf("%w: error decoding response body", err)
}
// Handle errors that are returned in JSON-RPC responses with `200 OK` statuses
return response.Err()
}

View file

@ -0,0 +1,8 @@
{
"result": {
"feerate": 0.00001,
"blocks": 2
},
"error": null,
"id": "curltest"
}

View file

@ -0,0 +1,8 @@
{
"result": null,
"error": {
"code": -8,
"message": "Block height out of range"
},
"id": "curltext"
}

View file

@ -0,0 +1,5 @@
{
"result": "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09",
"error": null,
"id": "curltext"
}

View file

@ -0,0 +1,8 @@
{
"result": null,
"error": {
"code": -5,
"message": "Block not found"
},
"id": 1
}

View file

@ -0,0 +1,88 @@
{
"result": {
"hash": "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09",
"confirmations": 643039,
"strippedsize": 216,
"size": 216,
"weight": 864,
"height": 1000,
"version": 1,
"versionHex": "00000001",
"merkleroot": "fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33",
"tx": [
{
"txid": "fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33",
"hash": "fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33",
"version": 1,
"size": 135,
"vsize": 135,
"weight": 540,
"locktime": 0,
"vin": [
{
"coinbase": "04ffff001d02fd04",
"sequence": 4294967295
}
],
"vout": [
{
"value": 50,
"n": 0,
"scriptPubKey": {
"asm": "04f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446a OP_CHECKSIG",
"hex": "4104f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446aac",
"type": "pubkey"
}
}
],
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d02fd04ffffffff0100f2052a01000000434104f5eeb2b10c944c6b9fbcfff94c35bdeecd93df977882babc7f3a2cf7f5c81d3b09a68db7f0e04f21de5d4230e75e6dbe7ad16eefe0d4325a62067dc6f369446aac00000000"
},
{
"txid": "4852fe372ff7534c16713b3146bbc1e86379c70bea4d5c02fb1fa0112980a081",
"hash": "4852fe372ff7534c16713b3146bbc1e86379c70bea4d5c02fb1fa0112980a081",
"version": 1,
"size": 1408,
"vsize": 1408,
"weight": 5632,
"locktime": 0,
"vin": [],
"vout": [
{
"value": 0.0381,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 45db0b779c0b9fa207f12a8218c94fc77aff5045 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a91445db0b779c0b9fa207f12a8218c94fc77aff504588ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"mmtKKnjqTPdkBnBMbNt5Yu2SCwpMaEshEL"
]
}
},
{
"value": 0.5,
"n": 1,
"scriptPubKey": {
"asm": "",
"hex": "",
"type": "nonstandard"
}
}
],
"hex": "01000000081cefd96060ecb1c4fbe675ad8a4f8bdc61d634c52b3a1c4116dee23749fe80ff000000009300493046022100866859c21f306538152e83f115bcfbf59ab4bb34887a88c03483a5dff9895f96022100a6dfd83caa609bf0516debc2bf65c3df91813a4842650a1858b3f61cfa8af249014730440220296d4b818bb037d0f83f9f7111665f49532dfdcbec1e6b784526e9ac4046eaa602204acf3a5cb2695e8404d80bf49ab04828bcbe6fc31d25a2844ced7a8d24afbdff01ffffffff1cefd96060ecb1c4fbe675ad8a4f8bdc61d634c52b3a1c4116dee23749fe80ff020000009400483045022100e87899175991aa008176cb553c6f2badbb5b741f328c9845fcab89f8b18cae2302200acce689896dc82933015e7230e5230d5cff8a1ffe82d334d60162ac2c5b0c9601493046022100994ad29d1e7b03e41731a4316e5f4992f0d9b6e2efc40a1ccd2c949b461175c502210099b69fdc2db00fbba214f16e286f6a49e2d8a0d5ffc6409d87796add475478d601ffffffff1e4a6d2d280ea06680d6cf8788ac90344a9c67cca9b06005bbd6d3f6945c8272010000009500493046022100a27400ba52fd842ce07398a1de102f710a10c5599545e6c95798934352c2e4df022100f6383b0b14c9f64b6718139f55b6b9494374755b86bae7d63f5d3e583b57255a01493046022100fdf543292f34e1eeb1703b264965339ec4a450ec47585009c606b3edbc5b617b022100a5fbb1c8de8aaaa582988cdb23622838e38de90bebcaab3928d949aa502a65d401ffffffff1e4a6d2d280ea06680d6cf8788ac90344a9c67cca9b06005bbd6d3f6945c8272020000009400493046022100ac626ac3051f875145b4fe4cfe089ea895aac73f65ab837b1ac30f5d875874fa022100bc03e79fa4b7eb707fb735b95ff6613ca33adeaf3a0607cdcead4cfd3b51729801483045022100b720b04a5c5e2f61b7df0fcf334ab6fea167b7aaede5695d3f7c6973496adbf1022043328c4cc1cdc3e5db7bb895ccc37133e960b2fd3ece98350f774596badb387201ffffffff23a8733e349c97d6cd90f520fdd084ba15ce0a395aad03cd51370602bb9e5db3010000004a00483045022100e8556b72c5e9c0da7371913a45861a61c5df434dfd962de7b23848e1a28c86ca02205d41ceda00136267281be0974be132ac4cda1459fe2090ce455619d8b91045e901ffffffff6856d609b881e875a5ee141c235e2a82f6b039f2b9babe82333677a5570285a6000000006a473044022040a1c631554b8b210fbdf2a73f191b2851afb51d5171fb53502a3a040a38d2c0022040d11cf6e7b41fe1b66c3d08f6ada1aee07a047cb77f242b8ecc63812c832c9a012102bcfad931b502761e452962a5976c79158a0f6d307ad31b739611dac6a297c256ffffffff6856d609b881e875a5ee141c235e2a82f6b039f2b9babe82333677a5570285a601000000930048304502205b109df098f7e932fbf71a45869c3f80323974a826ee2770789eae178a21bfc8022100c0e75615e53ee4b6e32b9bb5faa36ac539e9c05fa2ae6b6de5d09c08455c8b9601483045022009fb7d27375c47bea23b24818634df6a54ecf72d52e0c1268fb2a2c84f1885de022100e0ed4f15d62e7f537da0d0f1863498f9c7c0c0a4e00e4679588c8d1a9eb20bb801ffffffffa563c3722b7b39481836d5edfc1461f97335d5d1e9a23ade13680d0e2c1c371f030000006c493046022100ecc38ae2b1565643dc3c0dad5e961a5f0ea09cab28d024f92fa05c922924157e022100ebc166edf6fbe4004c72bfe8cf40130263f98ddff728c8e67b113dbd621906a601210211a4ed241174708c07206601b44a4c1c29e5ad8b1f731c50ca7e1d4b2a06dc1fffffffff02d0223a00000000001976a91445db0b779c0b9fa207f12a8218c94fc77aff504588ac80f0fa02000000000000000000"
}
],
"time": 1232346882,
"mediantime": 1232344831,
"nonce": 2595206198,
"bits": "1d00ffff",
"difficulty": 1,
"chainwork": "000000000000000000000000000000000000000000000000000003e903e903e9",
"nTx": 1,
"previousblockhash": "0000000008e647742775a230787d66fdf92c46a48c896bfbc85cdc8acc67e87d",
"nextblockhash": "00000000a2887344f8db859e372e7e4bc26b23b9de340f725afbf2edb265b4c6"
},
"error": null,
"id": "curltest"
}

View file

@ -0,0 +1,174 @@
{
"result": {
"hash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506",
"confirmations": 544053,
"strippedsize": 957,
"size": 957,
"weight": 3828,
"height": 100000,
"version": 1,
"versionHex": "00000001",
"merkleroot": "f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766",
"tx": [
{
"txid": "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
"hash": "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
"version": 1,
"size": 135,
"vsize": 135,
"weight": 540,
"locktime": 0,
"vin": [
{
"coinbase": "044c86041b020602",
"sequence": 4294967295
}
],
"vout": [
{
"value": 15.89351625,
"n": 0,
"scriptPubKey": {
"asm": "OP_HASH160 228f554bbf766d6f9cc828de1126e3d35d15e5fe OP_EQUAL",
"hex": "a914228f554bbf766d6f9cc828de1126e3d35d15e5fe87",
"reqSigs": 1,
"type": "scripthash",
"addresses": [
"34qkc2iac6RsyxZVfyE2S5U5WcRsbg2dpK"
]
}
},
{
"value": 0,
"n": 1,
"scriptPubKey": {
"asm": "OP_RETURN aa21a9ed10109f4b82aa3ed7ec9d02a2a90246478b3308c8b85daf62fe501d58d05727a4",
"hex": "6a24aa21a9ed10109f4b82aa3ed7ec9d02a2a90246478b3308c8b85daf62fe501d58d05727a4",
"type": "nulldata"
}
}
],
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac00000000"
},
{
"txid": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
"hash": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
"version": 1,
"size": 259,
"vsize": 259,
"weight": 1036,
"locktime": 0,
"vin": [
{
"txid": "87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03",
"vout": 0,
"scriptSig": {
"asm": "3046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748[ALL] 04f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3",
"hex": "493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3"
},
"sequence": 4294967295
}
],
"vout": [
{
"value": 5.56,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 c398efa9c392ba6013c5e04ee729755ef7f58b32 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn"
]
}
},
{
"value": 44.44,
"n": 1,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 948c765a6914d43f2a7ac177da2c2f6b52de3d7c OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"1EYTGtG4LnFfiMvjJdsU7GMGCQvsRSjYhx"
]
}
}
],
"hex": "0100000001032e38e9c0a84c6046d687d10556dcacc41d275ec55fc00779ac88fdf357a187000000008c493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3ffffffff0200e32321000000001976a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac000fe208010000001976a914948c765a6914d43f2a7ac177da2c2f6b52de3d7c88ac00000000"
},
{
"txid": "fake",
"hash": "fake",
"version": 2,
"size": 421,
"vsize": 612,
"weight": 129992,
"locktime": 10,
"vin": [
{
"txid": "503e4e9824282eb06f1a328484e2b367b5f4f93a405d6e7b97261bafabfb53d5",
"vout": 0,
"scriptSig": {
"asm": "00142b2296c588ec413cebd19c3cbc04ea830ead6e78",
"hex": "1600142b2296c588ec413cebd19c3cbc04ea830ead6e78"
},
"txinwitness": [
"304402205f39ccbab38b644acea0776d18cb63ce3e37428cbac06dc23b59c61607aef69102206b8610827e9cb853ea0ba38983662034bd3575cc1ab118fb66d6a98066fa0bed01",
"0304c01563d46e38264283b99bb352b46e69bf132431f102d4bd9a9d8dab075e7f"
],
"sequence": 4294967295
},
{
"txid": "503e4e9824282eb06f1a328484e2b367b5f4f93a405d6e7b97261bafabfb53d5",
"vout": 1,
"scriptSig": {
"asm": "00142b2296c588ec413cebd19c3cbc04ea830ead6e78",
"hex": "1600142b2296c588ec413cebd19c3cbc04ea830ead6e78"
},
"sequence": 4294967295
},
{
"txid": "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
"vout": 0,
"scriptSig": {
"asm": "3046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748[ALL] 04f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3",
"hex": "493046022100c352d3dd993a981beba4a63ad15c209275ca9470abfcd57da93b58e4eb5dce82022100840792bc1f456062819f15d33ee7055cf7b5ee1af1ebcc6028d9cdb1c3af7748014104f46db5e9d61a9dc27b8d64ad23e7383a4e6ca164593c2527c038c0857eb67ee8e825dca65046b82c9331586c82e0fd1f633f25f87c161bc6f8a630121df2b3d3"
},
"sequence": 4294967295
}
],
"vout": [
{
"value": 200.56,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 c398efa9c392ba6013c5e04ee729755ef7f58b32 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914c398efa9c392ba6013c5e04ee729755ef7f58b3288ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn",
"1EYTGtG4LnFfiMvjJdsU7GMGCQvsRSjYhx"
]
}
}
],
"hex": "fake hex"
}
],
"time": 1293623863,
"mediantime": 1293622620,
"nonce": 274148111,
"bits": "1b04864c",
"difficulty": 14484.1623612254,
"chainwork": "0000000000000000000000000000000000000000000000000644cb7f5234089e",
"nTx": 4,
"previousblockhash": "000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250",
"nextblockhash": "00000000000080b66c911bd5ba14a74260057311eaeb1982802f7010f1a9f090"
},
"error": null,
"id": "curltest"
}

View file

@ -0,0 +1,45 @@
{
"result": {
"chain": "main",
"blocks": 1000,
"headers": 1000,
"bestblockhash": "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09",
"difficulty": 16947802333946.61,
"mediantime": 1597603357,
"verificationprogress": 0.9999978065942465,
"initialblockdownload": false,
"chainwork": "0000000000000000000000000000000000000000127a25606c744d562654d78c",
"size_on_disk": 333786409564,
"pruned": false,
"softforks": {
"bip34": {
"type": "buried",
"active": true,
"height": 227931
},
"bip66": {
"type": "buried",
"active": true,
"height": 363725
},
"bip65": {
"type": "buried",
"active": true,
"height": 388381
},
"csv": {
"type": "buried",
"active": true,
"height": 419328
},
"segwit": {
"type": "buried",
"active": true,
"height": 481824
}
},
"warnings": ""
},
"error": null,
"id": "curltest"
}

View file

@ -0,0 +1,145 @@
{
"result": [
{
"id": 4,
"addr": "77.93.223.9:8333",
"addrlocal": "34.234.104.214:28956",
"addrbind": "172.19.0.2:52608",
"services": "000000000000000d",
"servicesnames": [
"NETWORK",
"BLOOM",
"WITNESS"
],
"relaytxes": true,
"lastsend": 1597606676,
"lastrecv": 1597606677,
"bytessent": 29042690,
"bytesrecv": 144225864,
"conntime": 1597353296,
"timeoffset": -2,
"pingtime": 0.100113,
"minping": 0.09692,
"version": 70015,
"subver": "/Satoshi:0.14.2/",
"inbound": false,
"addnode": false,
"startingheight": 643579,
"banscore": 0,
"synced_headers": 644046,
"synced_blocks": 644046,
"inflight": [],
"whitelisted": false,
"permissions": [],
"minfeefilter": 0.00001,
"bytessent_per_msg": {
"addr": 62140,
"feefilter": 32,
"getaddr": 24,
"getblocktxn": 774,
"getdata": 8198251,
"getheaders": 13689,
"headers": 21836,
"inv": 20160356,
"notfound": 6524,
"ping": 67552,
"pong": 67584,
"sendcmpct": 132,
"sendheaders": 24,
"tx": 443622,
"verack": 24,
"version": 126
},
"bytesrecv_per_msg": {
"*other*": 231,
"addr": 68292,
"blocktxn": 1070719,
"cmpctblock": 995040,
"feefilter": 32,
"getdata": 22504,
"getheaders": 1053,
"headers": 39150,
"inv": 16723092,
"notfound": 41549,
"ping": 67584,
"pong": 67552,
"sendcmpct": 66,
"sendheaders": 24,
"tx": 125128826,
"verack": 24,
"version": 126
}
},
{
"id": 6,
"addr": "172.105.93.179:8333",
"addrlocal": "34.234.104.214:37443",
"addrbind": "172.19.0.2:33074",
"services": "000000000000040d",
"servicesnames": [
"NETWORK",
"BLOOM",
"WITNESS",
"NETWORK_LIMITED"
],
"relaytxes": true,
"lastsend": 1597606678,
"lastrecv": 1597606676,
"bytessent": 33137375,
"bytesrecv": 98074000,
"conntime": 1597353319,
"timeoffset": 0,
"pingtime": 0.091882,
"minping": 0.091026,
"version": 70015,
"subver": "/Satoshi:0.18.1/",
"inbound": false,
"addnode": false,
"startingheight": 643579,
"banscore": 0,
"synced_headers": 644046,
"synced_blocks": 644046,
"inflight": [],
"whitelisted": false,
"permissions": [],
"minfeefilter": 0.00001,
"bytessent_per_msg": {
"addr": 61685,
"feefilter": 32,
"getaddr": 24,
"getdata": 5604084,
"getheaders": 1053,
"headers": 26606,
"inv": 22199034,
"notfound": 14167,
"ping": 67552,
"pong": 67552,
"sendcmpct": 132,
"sendheaders": 24,
"tx": 5095280,
"verack": 24,
"version": 126
},
"bytesrecv_per_msg": {
"*other*": 1401,
"addr": 76492,
"cmpctblock": 655743,
"feefilter": 32,
"getdata": 392811,
"getheaders": 1053,
"headers": 37679,
"inv": 14752875,
"notfound": 18254,
"ping": 67552,
"pong": 67552,
"sendcmpct": 66,
"sendheaders": 24,
"tx": 82002316,
"verack": 24,
"version": 126
}
}
],
"error": null,
"id": "curltest"
}

View file

@ -0,0 +1,8 @@
{
"result": null,
"error": {
"code": -8,
"message": "Invalid conf_target, must be between 1 - 1008"
},
"id": "curltest"
}

View file

@ -0,0 +1,8 @@
{
"result": null,
"error": {
"code": -28,
"message": "rpc in warmup"
},
"id": 1
}

1259
bitcoin/client_test.go Normal file

File diff suppressed because it is too large Load diff

103
bitcoin/node.go Normal file
View file

@ -0,0 +1,103 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitcoin
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/coinbase/rosetta-bitcoin/utils"
"golang.org/x/sync/errgroup"
)
const (
bitcoindLogger = "bitcoind"
bitcoindStdErrLogger = "bitcoind stderr"
)
func logPipe(ctx context.Context, pipe io.ReadCloser, identifier string) error {
logger := utils.ExtractLogger(ctx, identifier)
reader := bufio.NewReader(pipe)
for {
str, err := reader.ReadString('\n')
if err != nil {
logger.Warnw("closing logger", "error", err)
return err
}
message := strings.Replace(str, "\n", "", -1)
messages := strings.SplitAfterN(message, " ", 2)
// Trim the timestamp from the log if it exists
if len(messages) > 1 {
message = messages[1]
}
// Print debug log if from bitcoindLogger
if identifier == bitcoindLogger {
logger.Debugw(message)
continue
}
logger.Warnw(message)
}
}
// StartBitcoind starts a bitcoind daemon in another goroutine
// and logs the results to the console.
func StartBitcoind(ctx context.Context, configPath string, g *errgroup.Group) error {
logger := utils.ExtractLogger(ctx, "bitcoind")
cmd := exec.Command(
"/app/bitcoind",
fmt.Sprintf("--conf=%s", configPath),
) // #nosec G204
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
g.Go(func() error {
return logPipe(ctx, stdout, bitcoindLogger)
})
g.Go(func() error {
return logPipe(ctx, stderr, bitcoindStdErrLogger)
})
if err := cmd.Start(); err != nil {
return fmt.Errorf("%w: unable to start bitcoind", err)
}
g.Go(func() error {
<-ctx.Done()
logger.Warnw("sending interrupt to bitcoind")
return cmd.Process.Signal(os.Interrupt)
})
return cmd.Wait()
}

493
bitcoin/types.go Normal file
View file

@ -0,0 +1,493 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitcoin
import (
"fmt"
"strings"
"github.com/btcsuite/btcd/chaincfg"
"github.com/coinbase/rosetta-sdk-go/types"
)
const (
// Blockchain is Bitcoin.
Blockchain string = "Bitcoin"
// MainnetNetwork is the value of the network
// in MainnetNetworkIdentifier.
MainnetNetwork string = "Mainnet"
// TestnetNetwork is the value of the network
// in TestnetNetworkIdentifier.
TestnetNetwork string = "Testnet3"
// Decimals is the decimals value
// used in Currency.
Decimals = 8
// SatoshisInBitcoin is the number of
// Satoshis in 1 BTC (10^8).
SatoshisInBitcoin = 100000000
// InputOpType is used to describe
// INPUT.
InputOpType = "INPUT"
// OutputOpType is used to describe
// OUTPUT.
OutputOpType = "OUTPUT"
// CoinbaseOpType is used to describe
// Coinbase.
CoinbaseOpType = "COINBASE"
// SuccessStatus is the status of all
// Bitcoin operations because anything
// on-chain is considered successful.
SuccessStatus = "SUCCESS"
// SkippedStatus is the status of all
// operations that are skipped because
// of BIP-30. You can read more about these
// types of operations in BIP-30.
SkippedStatus = "SKIPPED"
// TransactionHashLength is the length
// of any transaction hash in Bitcoin.
TransactionHashLength = 64
// NullData is returned by bitcoind
// as the ScriptPubKey.Type for OP_RETURN
// locking scripts.
NullData = "nulldata"
)
// Fee estimate constants
// Source: https://bitcoinops.org/en/tools/calc-size/
const (
MinFeeRate = float64(0.00001) // nolint:gomnd
TransactionOverhead = 12 // 4 version, 2 segwit flag, 1 vin, 1 vout, 4 lock time
InputSize = 68 // 4 prev index, 32 prev hash, 4 sequence, 1 script size, ~27 script witness
OutputOverhead = 9 // 8 value, 1 script size
P2PKHScriptPubkeySize = 25 // P2PKH size
)
var (
// MainnetGenesisBlockIdentifier is the genesis block for mainnet.
MainnetGenesisBlockIdentifier = &types.BlockIdentifier{
Hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
}
// MainnetParams are the params for mainnet.
MainnetParams = &chaincfg.MainNetParams
// MainnetCurrency is the *types.Currency for mainnet.
MainnetCurrency = &types.Currency{
Symbol: "BTC",
Decimals: Decimals,
}
// TestnetGenesisBlockIdentifier is the genesis block for testnet.
TestnetGenesisBlockIdentifier = &types.BlockIdentifier{
Hash: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
}
// TestnetParams are the params for testnet.
TestnetParams = &chaincfg.TestNet3Params
// TestnetCurrency is the *types.Currency for testnet.
TestnetCurrency = &types.Currency{
Symbol: "tBTC",
Decimals: Decimals,
}
// OperationTypes are all supported operation.Types.
OperationTypes = []string{
InputOpType,
OutputOpType,
CoinbaseOpType,
}
// OperationStatuses are all supported operation.Status.
OperationStatuses = []*types.OperationStatus{
{
Status: SuccessStatus,
Successful: true,
},
{
Status: SkippedStatus,
Successful: false,
},
}
)
// ScriptPubKey is a script placed on the output operations
// of a Bitcoin transaction that must be satisfied to spend
// the output.
type ScriptPubKey struct {
ASM string `json:"asm"`
Hex string `json:"hex"`
RequiredSigs int64 `json:"reqSigs,omitempty"`
Type string `json:"type"`
Addresses []string `json:"addresses,omitempty"`
}
// ScriptSig is a script on the input operations of a
// Bitcoin transaction that satisfies the ScriptPubKey
// on an output being spent.
type ScriptSig struct {
ASM string `json:"asm"`
Hex string `json:"hex"`
}
// BlockchainInfo is information about the Bitcoin network.
// This struct only contains the information necessary for
// this implementation.
type BlockchainInfo struct {
Chain string `json:"chain"`
Blocks int64 `json:"blocks"`
BestBlockHash string `json:"bestblockhash"`
}
// PeerInfo is a collection of relevant info about a particular peer.
type PeerInfo struct {
Addr string `json:"addr"`
Version int64 `json:"version"`
SubVer string `json:"subver"`
StartingHeight int64 `json:"startingheight"`
RelayTxes bool `json:"relaytxes"`
LastSend int64 `json:"lastsend"`
LastRecv int64 `json:"lastrecv"`
BanScore int64 `json:"banscore"`
SyncedBlocks int64 `json:"synced_blocks"`
SyncedHeaders int64 `json:"synced_headers"`
}
// Block is a raw Bitcoin block (with verbosity == 2).
type Block struct {
Hash string `json:"hash"`
Height int64 `json:"height"`
PreviousBlockHash string `json:"previousblockhash"`
Time int64 `json:"time"`
MedianTime int64 `json:"mediantime"`
Nonce int64 `json:"nonce"`
MerkleRoot string `json:"merkleroot"`
Version int32 `json:"version"`
Size int64 `json:"size"`
Weight int64 `json:"weight"`
Bits string `json:"bits"`
Difficulty float64 `json:"difficulty"`
Txs []*Transaction `json:"tx"`
}
// Metadata returns the metadata for a block.
func (b Block) Metadata() (map[string]interface{}, error) {
m := &BlockMetadata{
Nonce: b.Nonce,
MerkleRoot: b.MerkleRoot,
Version: b.Version,
Size: b.Size,
Weight: b.Weight,
MedianTime: b.MedianTime,
Bits: b.Bits,
Difficulty: b.Difficulty,
}
return types.MarshalMap(m)
}
// BlockMetadata is a collection of useful
// metadata in a block.
type BlockMetadata struct {
Nonce int64 `json:"nonce,omitempty"`
MerkleRoot string `json:"merkleroot,omitempty"`
Version int32 `json:"version,omitempty"`
Size int64 `json:"size,omitempty"`
Weight int64 `json:"weight,omitempty"`
MedianTime int64 `json:"mediantime,omitempty"`
Bits string `json:"bits,omitempty"`
Difficulty float64 `json:"difficulty,omitempty"`
}
// Transaction is a raw Bitcoin transaction.
type Transaction struct {
Hex string `json:"hex"`
Hash string `json:"txid"`
Size int64 `json:"size"`
Vsize int64 `json:"vsize"`
Version int32 `json:"version"`
Locktime int64 `json:"locktime"`
Weight int64 `json:"weight"`
Inputs []*Input `json:"vin"`
Outputs []*Output `json:"vout"`
}
// Metadata returns the metadata for a transaction.
func (t Transaction) Metadata() (map[string]interface{}, error) {
m := &TransactionMetadata{
Size: t.Size,
Vsize: t.Vsize,
Version: t.Version,
Locktime: t.Locktime,
Weight: t.Weight,
}
return types.MarshalMap(m)
}
// TransactionMetadata is a collection of useful
// metadata in a transaction.
type TransactionMetadata struct {
Size int64 `json:"size,omitempty"`
Vsize int64 `json:"vsize,omitempty"`
Version int32 `json:"version,omitempty"`
Locktime int64 `json:"locktime,omitempty"`
Weight int64 `json:"weight,omitempty"`
}
// Input is a raw input in a Bitcoin transaction.
type Input struct {
TxHash string `json:"txid"`
Vout int64 `json:"vout"`
ScriptSig *ScriptSig `json:"scriptSig"`
Sequence int64 `json:"sequence"`
TxInWitness []string `json:"txinwitness"`
// Relevant when the input is the coinbase input
Coinbase string `json:"coinbase"`
}
// Metadata returns the metadata for an input.
func (i Input) Metadata() (map[string]interface{}, error) {
m := &OperationMetadata{
ScriptSig: i.ScriptSig,
Sequence: i.Sequence,
TxInWitness: i.TxInWitness,
Coinbase: i.Coinbase,
}
return types.MarshalMap(m)
}
// Output is a raw output in a Bitcoin transaction.
type Output struct {
Value float64 `json:"value"`
Index int64 `json:"n"`
ScriptPubKey *ScriptPubKey `json:"scriptPubKey"`
}
// Metadata returns the metadata for an output.
func (o Output) Metadata() (map[string]interface{}, error) {
m := &OperationMetadata{
ScriptPubKey: o.ScriptPubKey,
}
return types.MarshalMap(m)
}
// OperationMetadata is a collection of useful
// metadata from Bitcoin inputs and outputs.
type OperationMetadata struct {
// Coinbase Metadata
Coinbase string `json:"coinbase,omitempty"`
// Input Metadata
ScriptSig *ScriptSig `json:"scriptsig,omitempty"`
Sequence int64 `json:"sequence,omitempty"`
TxInWitness []string `json:"txinwitness,omitempty"`
// Output Metadata
ScriptPubKey *ScriptPubKey `json:"scriptPubKey,omitempty"`
}
// request represents the JSON-RPC request body
type request struct {
JSONRPC string `json:"jsonrpc"`
ID int `json:"id"`
Method string `json:"method"`
Params []interface{} `json:"params"`
}
func (r request) GetVersion() string { return r.JSONRPC }
func (r request) GetID() int { return r.ID }
func (r request) GetMethod() string { return r.Method }
func (r request) GetParams() []interface{} { return r.Params }
// Responses
// jSONRPCResponse represents an interface for generic JSON-RPC responses
type jSONRPCResponse interface {
Err() error
}
type responseError struct {
Code int64 `json:"code"`
Message string `json:"message"`
}
// BlockResponse is the response body for `getblock` requests
type blockResponse struct {
Result *Block `json:"result"`
Error *responseError `json:"error"`
}
func (b blockResponse) Err() error {
if b.Error == nil {
return nil
}
if b.Error.Code == blockNotFoundErrCode {
return ErrBlockNotFound
}
return fmt.Errorf(
"%w: error JSON RPC response, code: %d, message: %s",
ErrJSONRPCError,
b.Error.Code,
b.Error.Message,
)
}
type pruneBlockchainResponse struct {
Result int64 `json:"result"`
Error *responseError `json:"error"`
}
func (p pruneBlockchainResponse) Err() error {
if p.Error == nil {
return nil
}
return fmt.Errorf(
"%w: error JSON RPC response, code: %d, message: %s",
ErrJSONRPCError,
p.Error.Code,
p.Error.Message,
)
}
type blockchainInfoResponse struct {
Result *BlockchainInfo `json:"result"`
Error *responseError `json:"error"`
}
func (b blockchainInfoResponse) Err() error {
if b.Error == nil {
return nil
}
return fmt.Errorf(
"%w: error JSON RPC response, code: %d, message: %s",
ErrJSONRPCError,
b.Error.Code,
b.Error.Message,
)
}
type peerInfoResponse struct {
Result []*PeerInfo `json:"result"`
Error *responseError `json:"error"`
}
func (p peerInfoResponse) Err() error {
if p.Error == nil {
return nil
}
return fmt.Errorf(
"%w: error JSON RPC response, code: %d, message: %s",
ErrJSONRPCError,
p.Error.Code,
p.Error.Message,
)
}
// blockHashResponse is the response body for `getblockhash` requests
type blockHashResponse struct {
Result string `json:"result"`
Error *responseError `json:"error"`
}
func (b blockHashResponse) Err() error {
if b.Error == nil {
return nil
}
return fmt.Errorf(
"%w: error JSON RPC response, code: %d, message: %s",
ErrJSONRPCError,
b.Error.Code,
b.Error.Message,
)
}
// sendRawTransactionResponse is the response body for `sendrawtransaction` requests
type sendRawTransactionResponse struct {
Result string `json:"result"`
Error *responseError `json:"error"`
}
func (s sendRawTransactionResponse) Err() error {
if s.Error == nil {
return nil
}
return fmt.Errorf(
"%w: error JSON RPC response, code: %d, message: %s",
ErrJSONRPCError,
s.Error.Code,
s.Error.Message,
)
}
type suggestedFeeRate struct {
FeeRate float64 `json:"feerate"`
}
// suggestedFeeRateResponse is the response body for `estimatesmartfee` requests
type suggestedFeeRateResponse struct {
Result *suggestedFeeRate `json:"result"`
Error *responseError `json:"error"`
}
func (s suggestedFeeRateResponse) Err() error {
if s.Error == nil {
return nil
}
return fmt.Errorf(
"%w: error JSON RPC response, code: %d, message: %s",
ErrJSONRPCError,
s.Error.Code,
s.Error.Message,
)
}
// CoinIdentifier converts a tx hash and vout into
// the canonical CoinIdentifier.Identifier used in
// rosetta-bitcoin.
func CoinIdentifier(hash string, vout int64) string {
return fmt.Sprintf("%s:%d", hash, vout)
}
// TransactionHash extracts the transaction hash
// from a CoinIdentifier.Identifier.
func TransactionHash(identifier string) string {
vals := strings.Split(identifier, ":")
return vals[0]
}

70
bitcoin/utils.go Normal file
View file

@ -0,0 +1,70 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bitcoin
import (
"fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
"github.com/coinbase/rosetta-sdk-go/types"
)
// ParseCoinIdentifier returns the corresponding hash and index associated
// with a *types.CoinIdentifier.
func ParseCoinIdentifier(coinIdentifier *types.CoinIdentifier) (*chainhash.Hash, uint32, error) {
utxoSpent := strings.Split(coinIdentifier.Identifier, ":")
outpointHash := utxoSpent[0]
if len(outpointHash) != TransactionHashLength {
return nil, 0, fmt.Errorf("outpoint_hash %s is not length 64", outpointHash)
}
hash, err := chainhash.NewHashFromStr(outpointHash)
if err != nil {
return nil, 0, fmt.Errorf("%w unable to construct has from string %s", err, outpointHash)
}
outpointIndex, err := strconv.ParseUint(utxoSpent[1], 10, 32)
if err != nil {
return nil, 0, fmt.Errorf("%w unable to parse outpoint_index", err)
}
return hash, uint32(outpointIndex), nil
}
// ParseSingleAddress extracts a single address from a pkscript or
// throws an error.
func ParseSingleAddress(
chainParams *chaincfg.Params,
script []byte,
) (txscript.ScriptClass, btcutil.Address, error) {
class, addresses, nRequired, err := txscript.ExtractPkScriptAddrs(script, chainParams)
if err != nil {
return 0, nil, fmt.Errorf("%w unable to extract script addresses", err)
}
if nRequired != 1 {
return 0, nil, fmt.Errorf("expecting 1 address, got %d", nRequired)
}
address := addresses[0]
return class, address, nil
}

View file

@ -0,0 +1,220 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package configuration
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"time"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/btcsuite/btcd/chaincfg"
"github.com/coinbase/rosetta-sdk-go/storage"
"github.com/coinbase/rosetta-sdk-go/types"
)
// Mode is the setting that determines if
// the implementation is "online" or "offline".
type Mode string
const (
// Online is when the implementation is permitted
// to make outbound connections.
Online Mode = "ONLINE"
// Offline is when the implementation is not permitted
// to make outbound connections.
Offline Mode = "OFFLINE"
// Mainnet is the Bitcoin Mainnet.
Mainnet string = "MAINNET"
// Testnet is Bitcoin Testnet3.
Testnet string = "TESTNET"
// mainnetConfigPath is the path of the Bitcoin
// configuration file for mainnet.
mainnetConfigPath = "/app/bitcoin-mainnet.conf"
// testnetConfigPath is the path of the Bitcoin
// configuration file for testnet.
testnetConfigPath = "/app/bitcoin-testnet.conf"
// Zstandard compression dictionaries
transactionNamespace = "transaction"
testnetTransactionDictionary = "/app/testnet-transaction.zstd"
mainnetTransactionDictionary = "/app/mainnet-transaction.zstd"
mainnetRPCPort = 8332
testnetRPCPort = 18332
// min prune depth is 288:
// https://github.com/bitcoin/bitcoin/blob/ad2952d17a2af419a04256b10b53c7377f826a27/src/validation.h#L84
pruneDepth = int64(10000) //nolint
// min prune height (on mainnet):
// https://github.com/bitcoin/bitcoin/blob/62d137ac3b701aae36c1aa3aa93a83fd6357fde6/src/chainparams.cpp#L102
minPruneHeight = int64(100000) //nolint
// attempt to prune once an hour
pruneFrequency = 60 * time.Minute
// DataDirectory is the default location for all
// persistent data.
DataDirectory = "/data"
bitcoindPath = "bitcoind"
indexerPath = "indexer"
// allFilePermissions specifies anyone can do anything
// to the file.
allFilePermissions = 0777
// ModeEnv is the environment variable read
// to determine mode.
ModeEnv = "MODE"
// NetworkEnv is the environment variable
// read to determine network.
NetworkEnv = "NETWORK"
// PortEnv is the environment variable
// read to determine the port for the Rosetta
// implementation.
PortEnv = "PORT"
)
// PruningConfiguration is the configuration to
// use for pruning in the indexer.
type PruningConfiguration struct {
Frequency time.Duration
Depth int64
MinHeight int64
}
// Configuration determines how
type Configuration struct {
Mode Mode
Network *types.NetworkIdentifier
Params *chaincfg.Params
Currency *types.Currency
GenesisBlockIdentifier *types.BlockIdentifier
Port int
RPCPort int
ConfigPath string
Pruning *PruningConfiguration
IndexerPath string
BitcoindPath string
Compressors []*storage.CompressorEntry
}
// LoadConfiguration attempts to create a new Configuration
// using the ENVs in the environment.
func LoadConfiguration(baseDirectory string) (*Configuration, error) {
config := &Configuration{}
config.Pruning = &PruningConfiguration{
Frequency: pruneFrequency,
Depth: pruneDepth,
MinHeight: minPruneHeight,
}
modeValue := Mode(os.Getenv(ModeEnv))
switch modeValue {
case Online:
config.Mode = Online
config.IndexerPath = path.Join(baseDirectory, indexerPath)
if err := ensurePathExists(config.IndexerPath); err != nil {
return nil, fmt.Errorf("%w: unable to create indexer path", err)
}
config.BitcoindPath = path.Join(baseDirectory, bitcoindPath)
if err := ensurePathExists(config.BitcoindPath); err != nil {
return nil, fmt.Errorf("%w: unable to create bitcoind path", err)
}
case Offline:
config.Mode = Offline
case "":
return nil, errors.New("MODE must be populated")
default:
return nil, fmt.Errorf("%s is not a valid mode", modeValue)
}
networkValue := os.Getenv(NetworkEnv)
switch networkValue {
case Mainnet:
config.Network = &types.NetworkIdentifier{
Blockchain: bitcoin.Blockchain,
Network: bitcoin.MainnetNetwork,
}
config.GenesisBlockIdentifier = bitcoin.MainnetGenesisBlockIdentifier
config.Params = bitcoin.MainnetParams
config.Currency = bitcoin.MainnetCurrency
config.ConfigPath = mainnetConfigPath
config.RPCPort = mainnetRPCPort
config.Compressors = []*storage.CompressorEntry{
{
Namespace: transactionNamespace,
DictionaryPath: mainnetTransactionDictionary,
},
}
case Testnet:
config.Network = &types.NetworkIdentifier{
Blockchain: bitcoin.Blockchain,
Network: bitcoin.TestnetNetwork,
}
config.GenesisBlockIdentifier = bitcoin.TestnetGenesisBlockIdentifier
config.Params = bitcoin.TestnetParams
config.Currency = bitcoin.TestnetCurrency
config.ConfigPath = testnetConfigPath
config.RPCPort = testnetRPCPort
config.Compressors = []*storage.CompressorEntry{
{
Namespace: transactionNamespace,
DictionaryPath: testnetTransactionDictionary,
},
}
case "":
return nil, errors.New("NETWORK must be populated")
default:
return nil, fmt.Errorf("%s is not a valid network", networkValue)
}
portValue := os.Getenv(PortEnv)
if len(portValue) == 0 {
return nil, errors.New("PORT must be populated")
}
port, err := strconv.Atoi(portValue)
if err != nil || len(portValue) == 0 || port <= 0 {
return nil, fmt.Errorf("%w: unable to parse port %s", err, portValue)
}
config.Port = port
return config, nil
}
// ensurePathsExist directories along
// a path if they do not exist.
func ensurePathExists(path string) error {
if err := os.MkdirAll(path, os.FileMode(allFilePermissions)); err != nil {
return fmt.Errorf("%w: unable to create %s directory", err, path)
}
return nil
}

View file

@ -0,0 +1,152 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package configuration
import (
"errors"
"os"
"path"
"testing"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-sdk-go/storage"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/coinbase/rosetta-sdk-go/utils"
"github.com/stretchr/testify/assert"
)
func TestLoadConfiguration(t *testing.T) {
tests := map[string]struct {
Mode string
Network string
Port string
cfg *Configuration
err error
}{
"no envs set": {
err: errors.New("MODE must be populated"),
},
"only mode set": {
Mode: string(Online),
err: errors.New("NETWORK must be populated"),
},
"only mode and network set": {
Mode: string(Online),
Network: Mainnet,
err: errors.New("PORT must be populated"),
},
"all set (mainnet)": {
Mode: string(Online),
Network: Mainnet,
Port: "1000",
cfg: &Configuration{
Mode: Online,
Network: &types.NetworkIdentifier{
Network: bitcoin.MainnetNetwork,
Blockchain: bitcoin.Blockchain,
},
Params: bitcoin.MainnetParams,
Currency: bitcoin.MainnetCurrency,
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
Port: 1000,
RPCPort: mainnetRPCPort,
ConfigPath: mainnetConfigPath,
Pruning: &PruningConfiguration{
Frequency: pruneFrequency,
Depth: pruneDepth,
MinHeight: minPruneHeight,
},
Compressors: []*storage.CompressorEntry{
{
Namespace: transactionNamespace,
DictionaryPath: mainnetTransactionDictionary,
},
},
},
},
"all set (testnet)": {
Mode: string(Online),
Network: Testnet,
Port: "1000",
cfg: &Configuration{
Mode: Online,
Network: &types.NetworkIdentifier{
Network: bitcoin.TestnetNetwork,
Blockchain: bitcoin.Blockchain,
},
Params: bitcoin.TestnetParams,
Currency: bitcoin.TestnetCurrency,
GenesisBlockIdentifier: bitcoin.TestnetGenesisBlockIdentifier,
Port: 1000,
RPCPort: testnetRPCPort,
ConfigPath: testnetConfigPath,
Pruning: &PruningConfiguration{
Frequency: pruneFrequency,
Depth: pruneDepth,
MinHeight: minPruneHeight,
},
Compressors: []*storage.CompressorEntry{
{
Namespace: transactionNamespace,
DictionaryPath: testnetTransactionDictionary,
},
},
},
},
"invalid mode": {
Mode: "bad mode",
Network: Testnet,
Port: "1000",
err: errors.New("bad mode is not a valid mode"),
},
"invalid network": {
Mode: string(Offline),
Network: "bad network",
Port: "1000",
err: errors.New("bad network is not a valid network"),
},
"invalid port": {
Mode: string(Offline),
Network: Testnet,
Port: "bad port",
err: errors.New("unable to parse port bad port"),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
newDir, err := utils.CreateTempDir()
assert.NoError(t, err)
defer utils.RemoveTempDir(newDir)
os.Setenv(ModeEnv, test.Mode)
os.Setenv(NetworkEnv, test.Network)
os.Setenv(PortEnv, test.Port)
cfg, err := LoadConfiguration(newDir)
if test.err != nil {
assert.Nil(t, cfg)
assert.Contains(t, err.Error(), test.err.Error())
} else {
test.cfg.IndexerPath = path.Join(newDir, "indexer")
test.cfg.BitcoindPath = path.Join(newDir, "bitcoind")
assert.Equal(t, test.cfg, cfg)
assert.NoError(t, err)
}
})
}
}

15
go.mod Normal file
View file

@ -0,0 +1,15 @@
module github.com/coinbase/rosetta-bitcoin
go 1.13
require (
github.com/btcsuite/btcd v0.21.0-beta
github.com/btcsuite/btcutil v1.0.2
github.com/coinbase/rosetta-sdk-go v0.4.4
github.com/dgraph-io/badger/v2 v2.2007.2
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1
github.com/stretchr/testify v1.6.1
go.uber.org/zap v1.15.0
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
google.golang.org/appengine v1.6.6 // indirect
)

507
go.sum Normal file
View file

@ -0,0 +1,507 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M=
github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd h1:qdGvebPBDuYDPGi1WCPjy1tGyMpmDK8IEapSsszn7HE=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723 h1:ZA/jbKoGcVAnER6pCHPEkGdZOV7U1oLUedErBHCUMs0=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coinbase/rosetta-sdk-go v0.4.4 h1:zTUS4bVlTfD4xq/o6JtsuU+g9sf3+S3Nnn2A24Ycow4=
github.com/coinbase/rosetta-sdk-go v0.4.4/go.mod h1:Luv0AhzZH81eul2hYZ3w0hBGwmFPiexwbntYxihEZck=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/dave/dst v0.23.1 h1:2obX6c3RqALrEOp6u01qsqPvwp0t+RpOp9O4Bf9KhXs=
github.com/dave/dst v0.23.1/go.mod h1:LjPcLEauK4jC5hQ1fE/wr05O41zK91Pr4Qs22Ljq7gs=
github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ=
github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8=
github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/dgraph-io/badger/v2 v2.2007.2 h1:EjjK0KqwaFMlPin1ajhP943VPENHJdEz1KLIegjaI3k=
github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI=
github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ethereum/go-ethereum v1.9.21 h1:8qRlhzrItnmUGdVlBzZLI2Tb46S0RdSNjFwICo781ws=
github.com/ethereum/go-ethereum v1.9.21/go.mod h1:RXAVzbGrSGmDkDnHymruTAIEjUR3E4TX0EOpaj702sI=
github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26 h1:lMm2hD9Fy0ynom5+85/pbdkiYcBqM1JWmhpAXLmy0fw=
github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/addlicense v0.0.0-20200827091314-d1655b921368 h1:Ds6gDZHoviaQM7r7oMx/cG2qwZc3l5u7cg6gTkxOZNE=
github.com/google/addlicense v0.0.0-20200827091314-d1655b921368/go.mod h1:EMjYTRimagHs1FwlIqKyX3wAM0u3rA+McvlIIWmSamA=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.1-0.20190629185528-ae1634f6a989/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1 h1:V59tBiPuMkySHwJkuq/OYkK0WnOLwCwD3UkTbEMr12U=
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/holiman/uint256 v1.1.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89 h1:12K8AlpT0/6QUXSfV0yi4Q0jkbq8NDtIKFtF61AoqV0=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasjones/reggen v0.0.0-20180717132126-cdb49ff09d77 h1:6xiz3+ZczT3M4+I+JLpcPGG1bQKm8067HktB17EDWEE=
github.com/lucasjones/reggen v0.0.0-20180717132126-cdb49ff09d77/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c h1:1RHs3tNxjXGHeul8z2t6H2N2TlAqpKe5yryJztRx4Jk=
github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.6.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793 h1:rhR7esJSmty+9ST6Gsp7mlQHkpISw2DiYjuFaz3dRDg=
github.com/segmentio/golines v0.0.0-20200824192126-7f30d3046793/go.mod h1:bQSh5qdVR67XiCKbaVvYO41s50c5hQo+3cY/1CQQ3xQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil v2.20.5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q=
github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw=
github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3/go.mod h1:hpGUWaI9xL8pRQCTXQgocU38Qw1g0Us7n5PxxTwTCYU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws=
github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U=
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk=
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI=
github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b h1:GgiSbuUyC0BlbUmHQBgFqu32eiRR/CEYdjOjOd4zE6Y=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191024172528-b4ff53e7a1cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181127232545-e782529d0ddd/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03 h1:4gtJXHJ9ud0q8MNSDxJsRU/WH+afypbe4Vk4zq+8qow=
golang.org/x/tools v0.0.0-20191024220359-3d91e92cde03/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729181040-64cdafbe085c/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200731060945-b5fad4ed8dd6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858 h1:xLt+iB5ksWcZVxqc+g9K41ZHy+6MKWfXCDsjSThnsPA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools/gopls v0.4.4/go.mod h1:zhyGzA+CAtREUwwq/btQxEx2FHnGzDwJvGs5YqdVCbE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75k=
honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f/go.mod h1:9VQ397fNXEnF84t90W4r4TRCQK+pg9f8ugVfyj+S26w=
mvdan.cc/gofumpt v0.0.0-20200802201014-ab5a8192947d/go.mod h1:bzrjFmaD6+xqohD3KYP0H2FEuxknnBmyyOxdhLdaIws=
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=

738
indexer/indexer.go Normal file
View file

@ -0,0 +1,738 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package indexer
import (
"context"
"errors"
"fmt"
"time"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/coinbase/rosetta-bitcoin/services"
"github.com/coinbase/rosetta-bitcoin/utils"
"github.com/coinbase/rosetta-sdk-go/asserter"
"github.com/coinbase/rosetta-sdk-go/storage"
"github.com/coinbase/rosetta-sdk-go/syncer"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/dgraph-io/badger/v2"
)
const (
// indexPlaceholder is provided to the syncer
// to indicate we should both start from the
// last synced block and that we should sync
// blocks until exit (instead of stopping at
// a particular height).
indexPlaceholder = -1
retryDelay = 10 * time.Second
retryLimit = 5
nodeWaitSleep = 3 * time.Second
missingTransactionDelay = 200 * time.Millisecond
// sizeMultiplier is used to multiply the memory
// estimate for pre-fetching blocks. In other words,
// this is the estimated memory overhead for each
// block fetched by the indexer.
sizeMultiplier = 15
// BadgerDB options overrides
defaultBlockSize = 1 << 20 // use large blocks so less table indexes (1 MB)
defaultValueThreshold = 0 // put almost everything in value logs (only use table for key)
defaultIndexCacheSize = 5 << 30 // 5 GB
)
var (
errMissingTransaction = errors.New("missing transaction")
)
// Client is used by the indexer to sync blocks.
type Client interface {
NetworkStatus(context.Context) (*types.NetworkStatusResponse, error)
PruneBlockchain(context.Context, int64) (int64, error)
GetRawBlock(context.Context, *types.PartialBlockIdentifier) (*bitcoin.Block, []string, error)
ParseBlock(
context.Context,
*bitcoin.Block,
map[string]*storage.AccountCoin,
) (*types.Block, error)
}
var _ syncer.Handler = (*Indexer)(nil)
var _ syncer.Helper = (*Indexer)(nil)
var _ services.Indexer = (*Indexer)(nil)
var _ storage.CoinStorageHelper = (*Indexer)(nil)
// Indexer caches blocks and provides balance query functionality.
type Indexer struct {
cancel context.CancelFunc
network *types.NetworkIdentifier
pruningConfig *configuration.PruningConfiguration
client Client
asserter *asserter.Asserter
database storage.Database
blockStorage *storage.BlockStorage
coinStorage *storage.CoinStorage
workers []storage.BlockWorker
waiter *waitTable
}
// CloseDatabase closes a storage.Database. This should be called
// before exiting.
func (i *Indexer) CloseDatabase(ctx context.Context) {
logger := utils.ExtractLogger(ctx, "")
err := i.database.Close(ctx)
if err != nil {
logger.Fatalw("unable to close indexer database", "error", err)
}
logger.Infow("database closed successfully")
}
// defaultBadgerOptions returns a set of badger.Options optimized
// for running a Rosetta implementation. After extensive research
// and profiling, we determined that the bottleneck to high RPC
// load was fetching table indexes from disk on an index cache miss.
// Thus, we increased the default block size to be much larger than
// the Badger default so we have a lot less indexes to store. We also
// ensure all values are stored in value log files (as to minimize
// table bloat at the cost of some performance).
func defaultBadgerOptions(path string) badger.Options {
defaultOps := storage.DefaultBadgerOptions(path)
defaultOps.BlockSize = defaultBlockSize
defaultOps.ValueThreshold = defaultValueThreshold
defaultOps.IndexCacheSize = defaultIndexCacheSize
return defaultOps
}
// Initialize returns a new Indexer.
func Initialize(
ctx context.Context,
cancel context.CancelFunc,
config *configuration.Configuration,
client Client,
) (*Indexer, error) {
localStore, err := storage.NewBadgerStorage(
ctx,
config.IndexerPath,
storage.WithCompressorEntries(config.Compressors),
storage.WithCustomSettings(defaultBadgerOptions(config.IndexerPath)),
)
if err != nil {
return nil, fmt.Errorf("%w: unable to initialize storage", err)
}
blockStorage := storage.NewBlockStorage(localStore)
asserter, err := asserter.NewClientWithOptions(
config.Network,
config.GenesisBlockIdentifier,
bitcoin.OperationTypes,
bitcoin.OperationStatuses,
services.Errors,
)
if err != nil {
return nil, fmt.Errorf("%w: unable to initialize asserter", err)
}
i := &Indexer{
cancel: cancel,
network: config.Network,
pruningConfig: config.Pruning,
client: client,
database: localStore,
blockStorage: blockStorage,
waiter: newWaitTable(),
asserter: asserter,
}
coinStorage := storage.NewCoinStorage(localStore, i, asserter)
i.coinStorage = coinStorage
i.workers = []storage.BlockWorker{coinStorage}
return i, nil
}
// waitForNode returns once bitcoind is ready to serve
// block queries.
func (i *Indexer) waitForNode(ctx context.Context) error {
logger := utils.ExtractLogger(ctx, "indexer")
for {
_, err := i.client.NetworkStatus(ctx)
if err == nil {
return nil
}
logger.Infow("waiting for bitcoind...")
if err := utils.ContextSleep(ctx, nodeWaitSleep); err != nil {
return err
}
}
}
// Sync attempts to index Bitcoin blocks using
// the bitcoin.Client until stopped.
func (i *Indexer) Sync(ctx context.Context) error {
if err := i.waitForNode(ctx); err != nil {
return fmt.Errorf("%w: failed to wait for node", err)
}
i.blockStorage.Initialize(i.workers)
startIndex := int64(indexPlaceholder)
head, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
if err == nil {
startIndex = head.Index + 1
}
// Load in previous blocks into syncer cache to handle reorgs.
// If previously processed blocks exist in storage, they are fetched.
// Otherwise, none are provided to the cache (the syncer will not attempt
// a reorg if the cache is empty).
pastBlocks := i.blockStorage.CreateBlockCache(ctx)
syncer := syncer.New(
i.network,
i,
i,
i.cancel,
syncer.WithCacheSize(syncer.DefaultCacheSize),
syncer.WithSizeMultiplier(sizeMultiplier),
syncer.WithPastBlocks(pastBlocks),
)
return syncer.Sync(ctx, startIndex, indexPlaceholder)
}
// Prune attempts to prune blocks in bitcoind every
// pruneFrequency.
func (i *Indexer) Prune(ctx context.Context) error {
logger := utils.ExtractLogger(ctx, "pruner")
tc := time.NewTicker(i.pruningConfig.Frequency)
defer tc.Stop()
for {
select {
case <-ctx.Done():
logger.Warnw("exiting pruner")
return ctx.Err()
case <-tc.C:
head, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
if err != nil {
continue
}
// Must meet pruning conditions in bitcoin core
// Source:
// https://github.com/bitcoin/bitcoin/blob/a63a26f042134fa80356860c109edb25ac567552/src/rpc/blockchain.cpp#L953-L960
pruneHeight := head.Index - i.pruningConfig.Depth
if pruneHeight <= i.pruningConfig.MinHeight {
logger.Infow("waiting to prune", "min prune height", i.pruningConfig.MinHeight)
continue
}
logger.Infow("attempting to prune bitcoind", "prune height", pruneHeight)
prunedHeight, err := i.client.PruneBlockchain(ctx, pruneHeight)
if err != nil {
logger.Warnw(
"unable to prune bitcoind",
"prune height", pruneHeight,
"error", err,
)
} else {
logger.Infow("pruned bitcoind", "prune height", prunedHeight)
}
}
}
}
// BlockAdded is called by the syncer when a block is added.
func (i *Indexer) BlockAdded(ctx context.Context, block *types.Block) error {
logger := utils.ExtractLogger(ctx, "indexer")
err := i.blockStorage.AddBlock(ctx, block)
if err != nil {
return fmt.Errorf(
"%w: unable to add block to storage %s:%d",
err,
block.BlockIdentifier.Hash,
block.BlockIdentifier.Index,
)
}
ops := 0
// Close channels of all blocks waiting.
i.waiter.Lock()
for _, transaction := range block.Transactions {
ops += len(transaction.Operations)
txHash := transaction.TransactionIdentifier.Hash
val, ok := i.waiter.Get(txHash, false)
if !ok {
continue
}
if val.channelClosed {
logger.Debugw(
"channel already closed",
"hash", block.BlockIdentifier.Hash,
"index", block.BlockIdentifier.Index,
"channel", txHash,
)
continue
}
// Closing channel will cause all listeners to continue
val.channelClosed = true
close(val.channel)
}
// Look for all remaining waiting transactions associated
// with the next block that have not yet been closed. We should
// abort these waits as they will never be closed by a new transaction.
for txHash, val := range i.waiter.table {
if val.earliestBlock == block.BlockIdentifier.Index+1 && !val.channelClosed {
logger.Debugw(
"aborting channel",
"hash", block.BlockIdentifier.Hash,
"index", block.BlockIdentifier.Index,
"channel", txHash,
)
val.channelClosed = true
val.aborted = true
close(val.channel)
}
}
i.waiter.Unlock()
logger.Debugw(
"block added",
"hash", block.BlockIdentifier.Hash,
"index", block.BlockIdentifier.Index,
"transactions", len(block.Transactions),
"ops", ops,
)
return nil
}
// BlockRemoved is called by the syncer when a block is removed.
func (i *Indexer) BlockRemoved(
ctx context.Context,
blockIdentifier *types.BlockIdentifier,
) error {
logger := utils.ExtractLogger(ctx, "indexer")
logger.Debugw(
"block removed",
"hash", blockIdentifier.Hash,
"index", blockIdentifier.Index,
)
err := i.blockStorage.RemoveBlock(ctx, blockIdentifier)
if err != nil {
return fmt.Errorf(
"%w: unable to remove block from storage %s:%d",
err,
blockIdentifier.Hash,
blockIdentifier.Index,
)
}
return nil
}
// NetworkStatus is called by the syncer to get the current
// network status.
func (i *Indexer) NetworkStatus(
ctx context.Context,
network *types.NetworkIdentifier,
) (*types.NetworkStatusResponse, error) {
return i.client.NetworkStatus(ctx)
}
func (i *Indexer) findCoin(
ctx context.Context,
btcBlock *bitcoin.Block,
coinIdentifier string,
) (*types.Coin, *types.AccountIdentifier, error) {
for ctx.Err() == nil {
databaseTransaction := i.database.NewDatabaseTransaction(ctx, false)
defer databaseTransaction.Discard(ctx)
coinHeadBlock, err := i.blockStorage.GetHeadBlockIdentifierTransactional(
ctx,
databaseTransaction,
)
if errors.Is(err, storage.ErrHeadBlockNotFound) {
if err := utils.ContextSleep(ctx, missingTransactionDelay); err != nil {
return nil, nil, err
}
continue
}
if err != nil {
return nil, nil, fmt.Errorf(
"%w: unable to get transactional head block identifier",
err,
)
}
// Attempt to find coin
coin, owner, err := i.coinStorage.GetCoinTransactional(
ctx,
databaseTransaction,
&types.CoinIdentifier{
Identifier: coinIdentifier,
},
)
if err == nil {
return coin, owner, nil
}
if !errors.Is(err, storage.ErrCoinNotFound) {
return nil, nil, fmt.Errorf("%w: unable to lookup coin %s", err, coinIdentifier)
}
// Locking here prevents us from adding sending any done
// signals while we are determining whether or not to add
// to the WaitTable.
i.waiter.Lock()
// Check to see if head block has increased since
// we created our databaseTransaction.
currHeadBlock, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
if err != nil {
return nil, nil, fmt.Errorf("%w: unable to get head block identifier", err)
}
// If the block has changed, we try to look up the transaction
// again.
if types.Hash(currHeadBlock) != types.Hash(coinHeadBlock) {
i.waiter.Unlock()
continue
}
// Put Transaction in WaitTable if doesn't already exist (could be
// multiple listeners)
transactionHash := bitcoin.TransactionHash(coinIdentifier)
val, ok := i.waiter.Get(transactionHash, false)
if !ok {
val = &waitTableEntry{
channel: make(chan struct{}),
earliestBlock: btcBlock.Height,
}
}
if val.earliestBlock > btcBlock.Height {
val.earliestBlock = btcBlock.Height
}
val.listeners++
i.waiter.Set(transactionHash, val, false)
i.waiter.Unlock()
return nil, nil, errMissingTransaction
}
return nil, nil, ctx.Err()
}
func (i *Indexer) checkHeaderMatch(
ctx context.Context,
btcBlock *bitcoin.Block,
) error {
headBlock, err := i.blockStorage.GetHeadBlockIdentifier(ctx)
if err != nil && !errors.Is(err, storage.ErrHeadBlockNotFound) {
return fmt.Errorf("%w: unable to lookup head block", err)
}
// If block we are trying to process is next but it is not connected, we
// should return syncer.ErrOrphanHead to manually trigger a reorg.
if headBlock != nil &&
btcBlock.Height == headBlock.Index+1 &&
btcBlock.PreviousBlockHash != headBlock.Hash {
return syncer.ErrOrphanHead
}
return nil
}
func (i *Indexer) findCoins(
ctx context.Context,
btcBlock *bitcoin.Block,
coins []string,
) (map[string]*storage.AccountCoin, error) {
if err := i.checkHeaderMatch(ctx, btcBlock); err != nil {
return nil, fmt.Errorf("%w: check header match failed", err)
}
coinMap := map[string]*storage.AccountCoin{}
remainingCoins := []string{}
for _, coinIdentifier := range coins {
coin, owner, err := i.findCoin(
ctx,
btcBlock,
coinIdentifier,
)
if err == nil {
coinMap[coinIdentifier] = &storage.AccountCoin{
Account: owner,
Coin: coin,
}
continue
}
if errors.Is(err, errMissingTransaction) {
remainingCoins = append(remainingCoins, coinIdentifier)
continue
}
return nil, fmt.Errorf("%w: unable to find coin %s", err, coinIdentifier)
}
if len(remainingCoins) == 0 {
return coinMap, nil
}
// Wait for remaining transactions
shouldAbort := false
for _, coinIdentifier := range remainingCoins {
// Wait on Channel
txHash := bitcoin.TransactionHash(coinIdentifier)
entry, ok := i.waiter.Get(txHash, true)
if !ok {
return nil, fmt.Errorf("transaction %s not in waiter", txHash)
}
select {
case <-entry.channel:
case <-ctx.Done():
return nil, ctx.Err()
}
// Delete Transaction from WaitTable if last listener
i.waiter.Lock()
val, ok := i.waiter.Get(txHash, false)
if !ok {
return nil, fmt.Errorf("transaction %s not in waiter", txHash)
}
// Don't exit right away to make sure
// we remove all closed entries from the
// waiter.
if val.aborted {
shouldAbort = true
}
val.listeners--
if val.listeners == 0 {
i.waiter.Delete(txHash, false)
} else {
i.waiter.Set(txHash, val, false)
}
i.waiter.Unlock()
}
// Wait to exit until we have decremented our listeners
if shouldAbort {
return nil, syncer.ErrOrphanHead
}
// In the case of a reorg, we may still not be able to find
// the transactions. So, we need to repeat this same process
// recursively until we find the transactions we are looking for.
foundCoins, err := i.findCoins(ctx, btcBlock, remainingCoins)
if err != nil {
return nil, fmt.Errorf("%w: unable to get remaining transactions", err)
}
for k, v := range foundCoins {
coinMap[k] = v
}
return coinMap, nil
}
// Block is called by the syncer to fetch a block.
func (i *Indexer) Block(
ctx context.Context,
network *types.NetworkIdentifier,
blockIdentifier *types.PartialBlockIdentifier,
) (*types.Block, error) {
// get raw block
var btcBlock *bitcoin.Block
var coins []string
var err error
retries := 0
for ctx.Err() == nil {
btcBlock, coins, err = i.client.GetRawBlock(ctx, blockIdentifier)
if err == nil {
break
}
retries++
if retries > retryLimit {
return nil, fmt.Errorf("%w: unable to get raw block %+v", err, blockIdentifier)
}
if err := utils.ContextSleep(ctx, retryDelay); err != nil {
return nil, err
}
}
// determine which coins must be fetched and get from coin storage
coinMap, err := i.findCoins(ctx, btcBlock, coins)
if err != nil {
return nil, fmt.Errorf("%w: unable to find input transactions", err)
}
// provide to block parsing
block, err := i.client.ParseBlock(ctx, btcBlock, coinMap)
if err != nil {
return nil, fmt.Errorf("%w: unable to parse block %+v", err, blockIdentifier)
}
// ensure block is valid
if err := i.asserter.Block(block); err != nil {
return nil, fmt.Errorf("%w: block is not valid %+v", err, blockIdentifier)
}
return block, nil
}
// GetScriptPubKeys gets the ScriptPubKey for
// a collection of *types.CoinIdentifier. It also
// confirms that the amount provided with each coin
// is valid.
func (i *Indexer) GetScriptPubKeys(
ctx context.Context,
coins []*types.Coin,
) ([]*bitcoin.ScriptPubKey, error) {
databaseTransaction := i.database.NewDatabaseTransaction(ctx, false)
defer databaseTransaction.Discard(ctx)
scripts := make([]*bitcoin.ScriptPubKey, len(coins))
for j, coin := range coins {
coinIdentifier := coin.CoinIdentifier
transactionHash, networkIndex, err := bitcoin.ParseCoinIdentifier(coinIdentifier)
if err != nil {
return nil, fmt.Errorf("%w: unable to parse coin identifier", err)
}
_, transaction, err := i.blockStorage.FindTransaction(
ctx,
&types.TransactionIdentifier{Hash: transactionHash.String()},
databaseTransaction,
)
if err != nil || transaction == nil {
return nil, fmt.Errorf(
"%w: unable to find transaction %s",
err,
transactionHash.String(),
)
}
for _, op := range transaction.Operations {
if op.Type != bitcoin.OutputOpType {
continue
}
if *op.OperationIdentifier.NetworkIndex != int64(networkIndex) {
continue
}
var opMetadata bitcoin.OperationMetadata
if err := types.UnmarshalMap(op.Metadata, &opMetadata); err != nil {
return nil, fmt.Errorf(
"%w: unable to unmarshal operation metadata %+v",
err,
op.Metadata,
)
}
if types.Hash(op.Amount.Currency) != types.Hash(coin.Amount.Currency) {
return nil, fmt.Errorf(
"currency expected %s does not match coin %s",
types.PrintStruct(coin.Amount.Currency),
types.PrintStruct(op.Amount.Currency),
)
}
addition, err := types.AddValues(op.Amount.Value, coin.Amount.Value)
if err != nil {
return nil, fmt.Errorf("%w: unable to add op amount and coin amount", err)
}
if addition != "0" {
return nil, fmt.Errorf(
"coin amount does not match expected with difference %s",
addition,
)
}
scripts[j] = opMetadata.ScriptPubKey
break
}
if scripts[j] == nil {
return nil, fmt.Errorf("unable to find script for coin %s", coinIdentifier.Identifier)
}
}
return scripts, nil
}
// GetBlockLazy returns a *types.BlockResponse from the indexer's block storage.
// All transactions in a block must be fetched individually.
func (i *Indexer) GetBlockLazy(
ctx context.Context,
blockIdentifier *types.PartialBlockIdentifier,
) (*types.BlockResponse, error) {
return i.blockStorage.GetBlockLazy(ctx, blockIdentifier)
}
// GetBlockTransaction returns a *types.Transaction if it is in the provided
// *types.BlockIdentifier.
func (i *Indexer) GetBlockTransaction(
ctx context.Context,
blockIdentifier *types.BlockIdentifier,
transactionIdentifier *types.TransactionIdentifier,
) (*types.Transaction, error) {
return i.blockStorage.GetBlockTransaction(ctx, blockIdentifier, transactionIdentifier)
}
// GetCoins returns all unspent coins for a particular *types.AccountIdentifier.
func (i *Indexer) GetCoins(
ctx context.Context,
accountIdentifier *types.AccountIdentifier,
) ([]*types.Coin, *types.BlockIdentifier, error) {
return i.coinStorage.GetCoins(ctx, accountIdentifier)
}
// CurrentBlockIdentifier returns the current head block identifier
// and is used to comply with the CoinStorageHelper interface.
func (i *Indexer) CurrentBlockIdentifier(
ctx context.Context,
transaction storage.DatabaseTransaction,
) (*types.BlockIdentifier, error) {
return i.blockStorage.GetHeadBlockIdentifierTransactional(ctx, transaction)
}

839
indexer/indexer_test.go Normal file
View file

@ -0,0 +1,839 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package indexer
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"math/rand"
"testing"
"time"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
mocks "github.com/coinbase/rosetta-bitcoin/mocks/indexer"
"github.com/coinbase/rosetta-sdk-go/storage"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/coinbase/rosetta-sdk-go/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
func getBlockHash(index int64) string {
return fmt.Sprintf("block %d", index)
}
var (
index0 = int64(0)
)
func TestIndexer_Pruning(t *testing.T) {
// Create Indexer
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
newDir, err := utils.CreateTempDir()
assert.NoError(t, err)
defer utils.RemoveTempDir(newDir)
mockClient := &mocks.Client{}
pruneDepth := int64(10)
minHeight := int64(200)
cfg := &configuration.Configuration{
Network: &types.NetworkIdentifier{
Network: bitcoin.MainnetNetwork,
Blockchain: bitcoin.Blockchain,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
Pruning: &configuration.PruningConfiguration{
Frequency: 50 * time.Millisecond,
Depth: pruneDepth,
MinHeight: minHeight,
},
IndexerPath: newDir,
}
i, err := Initialize(ctx, cancel, cfg, mockClient)
assert.NoError(t, err)
// Waiting for bitcoind...
mockClient.On("NetworkStatus", ctx).Return(nil, errors.New("not ready")).Once()
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{}, nil).Once()
// Sync to 1000
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
CurrentBlockIdentifier: &types.BlockIdentifier{
Index: 1000,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
}, nil)
// Timeout on first request
mockClient.On(
"PruneBlockchain",
mock.Anything,
mock.Anything,
).Return(
int64(-1),
errors.New("connection timeout"),
).Once()
// Requests after should work
mockClient.On(
"PruneBlockchain",
mock.Anything,
mock.Anything,
).Return(
int64(100),
nil,
).Run(
func(args mock.Arguments) {
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
currBlock := currBlockResponse.Block
assert.NoError(t, err)
pruningIndex := args.Get(1).(int64)
assert.True(t, currBlock.BlockIdentifier.Index-pruningIndex >= pruneDepth)
assert.True(t, pruningIndex >= minHeight)
},
)
// Add blocks
waitForCheck := make(chan struct{})
for i := int64(0); i <= 1000; i++ {
identifier := &types.BlockIdentifier{
Hash: getBlockHash(i),
Index: i,
}
parentIdentifier := &types.BlockIdentifier{
Hash: getBlockHash(i - 1),
Index: i - 1,
}
if parentIdentifier.Index < 0 {
parentIdentifier.Index = 0
parentIdentifier.Hash = getBlockHash(0)
}
block := &bitcoin.Block{
Hash: identifier.Hash,
Height: identifier.Index,
PreviousBlockHash: parentIdentifier.Hash,
}
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
block,
[]string{},
nil,
).Once()
blockReturn := &types.Block{
BlockIdentifier: identifier,
ParentBlockIdentifier: parentIdentifier,
Timestamp: 1599002115110,
}
if i != 200 {
mockClient.On(
"ParseBlock",
mock.Anything,
block,
map[string]*storage.AccountCoin{},
).Return(
blockReturn,
nil,
).Once()
} else {
mockClient.On(
"ParseBlock",
mock.Anything,
block,
map[string]*storage.AccountCoin{},
).Return(
blockReturn,
nil,
).Run(func(args mock.Arguments) {
close(waitForCheck)
}).Once()
}
}
go func() {
err := i.Sync(ctx)
assert.True(t, errors.Is(err, context.Canceled))
}()
go func() {
err := i.Prune(ctx)
assert.True(t, errors.Is(err, context.Canceled))
}()
<-waitForCheck
waitForFinish := make(chan struct{})
go func() {
for {
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
if currBlockResponse == nil {
time.Sleep(1 * time.Second)
continue
}
currBlock := currBlockResponse.Block
assert.NoError(t, err)
if currBlock.BlockIdentifier.Index == 1000 {
cancel()
close(waitForFinish)
return
}
time.Sleep(1 * time.Second)
}
}()
<-waitForFinish
mockClient.AssertExpectations(t)
}
func TestIndexer_Transactions(t *testing.T) {
// Create Indexer
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
newDir, err := utils.CreateTempDir()
assert.NoError(t, err)
defer utils.RemoveTempDir(newDir)
mockClient := &mocks.Client{}
cfg := &configuration.Configuration{
Network: &types.NetworkIdentifier{
Network: bitcoin.MainnetNetwork,
Blockchain: bitcoin.Blockchain,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
IndexerPath: newDir,
}
i, err := Initialize(ctx, cancel, cfg, mockClient)
assert.NoError(t, err)
// Sync to 1000
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
CurrentBlockIdentifier: &types.BlockIdentifier{
Index: 1000,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
}, nil)
// Add blocks
waitForCheck := make(chan struct{})
type coinBankEntry struct {
Script *bitcoin.ScriptPubKey
Coin *types.Coin
Account *types.AccountIdentifier
}
coinBank := map[string]*coinBankEntry{}
for i := int64(0); i <= 1000; i++ {
identifier := &types.BlockIdentifier{
Hash: getBlockHash(i),
Index: i,
}
parentIdentifier := &types.BlockIdentifier{
Hash: getBlockHash(i - 1),
Index: i - 1,
}
if parentIdentifier.Index < 0 {
parentIdentifier.Index = 0
parentIdentifier.Hash = getBlockHash(0)
}
transactions := []*types.Transaction{}
for j := 0; j < 5; j++ {
rawHash := fmt.Sprintf("block %d transaction %d", i, j)
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
coinIdentifier := fmt.Sprintf("%s:%d", hash, index0)
scriptPubKey := &bitcoin.ScriptPubKey{
ASM: coinIdentifier,
}
marshal, err := types.MarshalMap(scriptPubKey)
assert.NoError(t, err)
tx := &types.Transaction{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: hash,
},
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
NetworkIndex: &index0,
},
Status: bitcoin.SuccessStatus,
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: rawHash,
},
Amount: &types.Amount{
Value: fmt.Sprintf("%d", rand.Intn(1000)),
Currency: bitcoin.TestnetCurrency,
},
CoinChange: &types.CoinChange{
CoinAction: types.CoinCreated,
CoinIdentifier: &types.CoinIdentifier{
Identifier: coinIdentifier,
},
},
Metadata: map[string]interface{}{
"scriptPubKey": marshal,
},
},
},
}
coinBank[coinIdentifier] = &coinBankEntry{
Script: scriptPubKey,
Coin: &types.Coin{
CoinIdentifier: &types.CoinIdentifier{
Identifier: coinIdentifier,
},
Amount: tx.Operations[0].Amount,
},
Account: &types.AccountIdentifier{
Address: rawHash,
},
}
transactions = append(transactions, tx)
}
block := &bitcoin.Block{
Hash: identifier.Hash,
Height: identifier.Index,
PreviousBlockHash: parentIdentifier.Hash,
}
// Require coins that will make the indexer
// wait.
requiredCoins := []string{}
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
for k := i - 1; k >= 0 && k > i-20; k-- {
rawHash := fmt.Sprintf("block %d transaction %d", k, rand.Intn(5))
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
requiredCoins = append(requiredCoins, hash+":0")
}
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
block,
requiredCoins,
nil,
).Once()
blockReturn := &types.Block{
BlockIdentifier: identifier,
ParentBlockIdentifier: parentIdentifier,
Timestamp: 1599002115110,
Transactions: transactions,
}
coinMap := map[string]*storage.AccountCoin{}
for _, coinIdentifier := range requiredCoins {
coinMap[coinIdentifier] = &storage.AccountCoin{
Account: coinBank[coinIdentifier].Account,
Coin: coinBank[coinIdentifier].Coin,
}
}
if i != 200 {
mockClient.On(
"ParseBlock",
mock.Anything,
block,
coinMap,
).Return(
blockReturn,
nil,
).Once()
} else {
mockClient.On("ParseBlock", mock.Anything, block, coinMap).Return(blockReturn, nil).Run(func(args mock.Arguments) {
close(waitForCheck)
}).Once()
}
}
go func() {
err := i.Sync(ctx)
assert.True(t, errors.Is(err, context.Canceled))
}()
<-waitForCheck
waitForFinish := make(chan struct{})
go func() {
for {
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
if currBlockResponse == nil {
time.Sleep(1 * time.Second)
continue
}
currBlock := currBlockResponse.Block
assert.NoError(t, err)
if currBlock.BlockIdentifier.Index == 1000 {
// Ensure ScriptPubKeys are accessible.
allCoins := []*types.Coin{}
expectedPubKeys := []*bitcoin.ScriptPubKey{}
for k, v := range coinBank {
allCoins = append(allCoins, &types.Coin{
CoinIdentifier: &types.CoinIdentifier{Identifier: k},
Amount: &types.Amount{
Value: fmt.Sprintf("-%s", v.Coin.Amount.Value),
Currency: bitcoin.TestnetCurrency,
},
})
expectedPubKeys = append(expectedPubKeys, v.Script)
}
pubKeys, err := i.GetScriptPubKeys(ctx, allCoins)
assert.NoError(t, err)
assert.Equal(t, expectedPubKeys, pubKeys)
cancel()
close(waitForFinish)
return
}
time.Sleep(1 * time.Second)
}
}()
<-waitForFinish
mockClient.AssertExpectations(t)
}
func TestIndexer_Reorg(t *testing.T) {
// Create Indexer
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
newDir, err := utils.CreateTempDir()
assert.NoError(t, err)
defer utils.RemoveTempDir(newDir)
mockClient := &mocks.Client{}
cfg := &configuration.Configuration{
Network: &types.NetworkIdentifier{
Network: bitcoin.MainnetNetwork,
Blockchain: bitcoin.Blockchain,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
IndexerPath: newDir,
}
i, err := Initialize(ctx, cancel, cfg, mockClient)
assert.NoError(t, err)
// Sync to 1000
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
CurrentBlockIdentifier: &types.BlockIdentifier{
Index: 1000,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
}, nil)
// Add blocks
waitForCheck := make(chan struct{})
type coinBankEntry struct {
Script *bitcoin.ScriptPubKey
Coin *types.Coin
Account *types.AccountIdentifier
}
coinBank := map[string]*coinBankEntry{}
for i := int64(0); i <= 1000; i++ {
identifier := &types.BlockIdentifier{
Hash: getBlockHash(i),
Index: i,
}
parentIdentifier := &types.BlockIdentifier{
Hash: getBlockHash(i - 1),
Index: i - 1,
}
if parentIdentifier.Index < 0 {
parentIdentifier.Index = 0
parentIdentifier.Hash = getBlockHash(0)
}
transactions := []*types.Transaction{}
for j := 0; j < 5; j++ {
rawHash := fmt.Sprintf("block %d transaction %d", i, j)
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
coinIdentifier := fmt.Sprintf("%s:%d", hash, index0)
scriptPubKey := &bitcoin.ScriptPubKey{
ASM: coinIdentifier,
}
marshal, err := types.MarshalMap(scriptPubKey)
assert.NoError(t, err)
tx := &types.Transaction{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: hash,
},
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
NetworkIndex: &index0,
},
Status: bitcoin.SuccessStatus,
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: rawHash,
},
Amount: &types.Amount{
Value: fmt.Sprintf("%d", rand.Intn(1000)),
Currency: bitcoin.TestnetCurrency,
},
CoinChange: &types.CoinChange{
CoinAction: types.CoinCreated,
CoinIdentifier: &types.CoinIdentifier{
Identifier: coinIdentifier,
},
},
Metadata: map[string]interface{}{
"scriptPubKey": marshal,
},
},
},
}
coinBank[coinIdentifier] = &coinBankEntry{
Script: scriptPubKey,
Coin: &types.Coin{
CoinIdentifier: &types.CoinIdentifier{
Identifier: coinIdentifier,
},
Amount: tx.Operations[0].Amount,
},
Account: &types.AccountIdentifier{
Address: rawHash,
},
}
transactions = append(transactions, tx)
}
block := &bitcoin.Block{
Hash: identifier.Hash,
Height: identifier.Index,
PreviousBlockHash: parentIdentifier.Hash,
}
// Require coins that will make the indexer
// wait.
requiredCoins := []string{}
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
for k := i - 1; k >= 0 && k > i-20; k-- {
rawHash := fmt.Sprintf("block %d transaction %d", k, rand.Intn(5))
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
requiredCoins = append(requiredCoins, hash+":0")
}
if i == 400 {
// we will need to call 400 twice
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
block,
requiredCoins,
nil,
).Once()
}
if i == 401 {
// require non-existent coins that will never be
// found to ensure we re-org via abort (with no change
// in block identifiers)
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
block,
[]string{"blah:1", "blah2:2"},
nil,
).Once()
}
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
block,
requiredCoins,
nil,
).Once()
blockReturn := &types.Block{
BlockIdentifier: identifier,
ParentBlockIdentifier: parentIdentifier,
Timestamp: 1599002115110,
Transactions: transactions,
}
coinMap := map[string]*storage.AccountCoin{}
for _, coinIdentifier := range requiredCoins {
coinMap[coinIdentifier] = &storage.AccountCoin{
Account: coinBank[coinIdentifier].Account,
Coin: coinBank[coinIdentifier].Coin,
}
}
if i == 400 {
mockClient.On(
"ParseBlock",
mock.Anything,
block,
coinMap,
).Return(
blockReturn,
nil,
).Once()
}
if i != 200 {
mockClient.On(
"ParseBlock",
mock.Anything,
block,
coinMap,
).Return(
blockReturn,
nil,
).Once()
} else {
mockClient.On("ParseBlock", mock.Anything, block, coinMap).Return(blockReturn, nil).Run(func(args mock.Arguments) {
close(waitForCheck)
}).Once()
}
}
go func() {
err := i.Sync(ctx)
assert.True(t, errors.Is(err, context.Canceled))
}()
<-waitForCheck
waitForFinish := make(chan struct{})
go func() {
for {
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
if currBlockResponse == nil {
time.Sleep(1 * time.Second)
continue
}
currBlock := currBlockResponse.Block
assert.NoError(t, err)
if currBlock.BlockIdentifier.Index == 1000 {
cancel()
close(waitForFinish)
return
}
time.Sleep(1 * time.Second)
}
}()
<-waitForFinish
assert.Len(t, i.waiter.table, 0)
mockClient.AssertExpectations(t)
}
func TestIndexer_HeaderReorg(t *testing.T) {
// Create Indexer
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
newDir, err := utils.CreateTempDir()
assert.NoError(t, err)
defer utils.RemoveTempDir(newDir)
mockClient := &mocks.Client{}
cfg := &configuration.Configuration{
Network: &types.NetworkIdentifier{
Network: bitcoin.MainnetNetwork,
Blockchain: bitcoin.Blockchain,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
IndexerPath: newDir,
}
i, err := Initialize(ctx, cancel, cfg, mockClient)
assert.NoError(t, err)
// Sync to 1000
mockClient.On("NetworkStatus", ctx).Return(&types.NetworkStatusResponse{
CurrentBlockIdentifier: &types.BlockIdentifier{
Index: 1000,
},
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
}, nil)
// Add blocks
waitForCheck := make(chan struct{})
for i := int64(0); i <= 1000; i++ {
identifier := &types.BlockIdentifier{
Hash: getBlockHash(i),
Index: i,
}
parentIdentifier := &types.BlockIdentifier{
Hash: getBlockHash(i - 1),
Index: i - 1,
}
if parentIdentifier.Index < 0 {
parentIdentifier.Index = 0
parentIdentifier.Hash = getBlockHash(0)
}
transactions := []*types.Transaction{}
block := &bitcoin.Block{
Hash: identifier.Hash,
Height: identifier.Index,
PreviousBlockHash: parentIdentifier.Hash,
}
requiredCoins := []string{}
if i == 400 {
// we will need to call 400 twice
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
block,
requiredCoins,
nil,
).Once()
}
if i == 401 {
// mess up previous block hash to trigger a re-org
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
&bitcoin.Block{
Hash: identifier.Hash,
Height: identifier.Index,
PreviousBlockHash: "blah",
},
[]string{},
nil,
).After(5 * time.Second).Once() // we delay to ensure we are at tip here
}
mockClient.On(
"GetRawBlock",
mock.Anything,
&types.PartialBlockIdentifier{Index: &identifier.Index},
).Return(
block,
requiredCoins,
nil,
).Once()
blockReturn := &types.Block{
BlockIdentifier: identifier,
ParentBlockIdentifier: parentIdentifier,
Timestamp: 1599002115110,
Transactions: transactions,
}
coinMap := map[string]*storage.AccountCoin{}
if i == 400 {
mockClient.On(
"ParseBlock",
mock.Anything,
block,
coinMap,
).Return(
blockReturn,
nil,
).Once()
}
if i != 200 {
mockClient.On(
"ParseBlock",
mock.Anything,
block,
coinMap,
).Return(
blockReturn,
nil,
).Once()
} else {
mockClient.On("ParseBlock", mock.Anything, block, coinMap).Return(blockReturn, nil).Run(func(args mock.Arguments) {
close(waitForCheck)
}).Once()
}
}
go func() {
err := i.Sync(ctx)
assert.True(t, errors.Is(err, context.Canceled))
}()
<-waitForCheck
waitForFinish := make(chan struct{})
go func() {
for {
currBlockResponse, err := i.GetBlockLazy(ctx, nil)
if currBlockResponse == nil {
time.Sleep(1 * time.Second)
continue
}
currBlock := currBlockResponse.Block
assert.NoError(t, err)
if currBlock.BlockIdentifier.Index == 1000 {
cancel()
close(waitForFinish)
return
}
time.Sleep(1 * time.Second)
}
}()
<-waitForFinish
assert.Len(t, i.waiter.table, 0)
mockClient.AssertExpectations(t)
}

81
indexer/wait_table.go Normal file
View file

@ -0,0 +1,81 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package indexer
import (
"sync"
)
type waitTable struct {
table map[string]*waitTableEntry
// lock is held when we want to perform multiple
// reads/writes to waiter.
lock sync.Mutex
}
func newWaitTable() *waitTable {
return &waitTable{
table: map[string]*waitTableEntry{},
}
}
func (t *waitTable) Lock() {
t.lock.Lock()
}
func (t *waitTable) Unlock() {
t.lock.Unlock()
}
func (t *waitTable) Get(key string, safe bool) (*waitTableEntry, bool) {
if safe {
t.lock.Lock()
defer t.lock.Unlock()
}
v, ok := t.table[key]
if !ok {
return nil, false
}
return v, true
}
func (t *waitTable) Set(key string, value *waitTableEntry, safe bool) {
if safe {
t.lock.Lock()
defer t.lock.Unlock()
}
t.table[key] = value
}
func (t *waitTable) Delete(key string, safe bool) {
if safe {
t.lock.Lock()
defer t.lock.Unlock()
}
delete(t.table, key)
}
type waitTableEntry struct {
listeners int // need to know when to delete entry (i.e. when no listeners)
channel chan struct{}
channelClosed bool // needed to know if we should abort (can't read if channel is closed)
aborted bool
earliestBlock int64
}

203
main.go Normal file
View file

@ -0,0 +1,203 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/coinbase/rosetta-bitcoin/indexer"
"github.com/coinbase/rosetta-bitcoin/services"
"github.com/coinbase/rosetta-bitcoin/utils"
"github.com/coinbase/rosetta-sdk-go/asserter"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
const (
// readTimeout is the maximum duration for reading the entire
// request, including the body.
readTimeout = 5 * time.Second
// writeTimeout is the maximum duration before timing out
// writes of the response. It is reset whenever a new
// request's header is read.
writeTimeout = 15 * time.Second
// idleTimeout is the maximum amount of time to wait for the
// next request when keep-alives are enabled.
idleTimeout = 30 * time.Second
// maxHeapUsage is the size of the heap in MB before we manually
// trigger garbage collection.
maxHeapUsage = 8500 // ~8.5 GB
)
var (
signalReceived = false
)
// handleSignals handles OS signals so we can ensure we close database
// correctly. We call multiple sigListeners because we
// may need to cancel more than 1 context.
func handleSignals(ctx context.Context, listeners []context.CancelFunc) {
logger := utils.ExtractLogger(ctx, "signal handler")
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
logger.Warnw("received signal", "signal", sig)
signalReceived = true
for _, listener := range listeners {
listener()
}
}()
}
func startOnlineDependencies(
ctx context.Context,
cancel context.CancelFunc,
cfg *configuration.Configuration,
g *errgroup.Group,
) (*bitcoin.Client, *indexer.Indexer, error) {
client := bitcoin.NewClient(
bitcoin.LocalhostURL(cfg.RPCPort),
cfg.GenesisBlockIdentifier,
cfg.Currency,
)
g.Go(func() error {
return bitcoin.StartBitcoind(ctx, cfg.ConfigPath, g)
})
i, err := indexer.Initialize(ctx, cancel, cfg, client)
if err != nil {
return nil, nil, fmt.Errorf("%w: unable to initialize indexer", err)
}
g.Go(func() error {
return i.Sync(ctx)
})
g.Go(func() error {
return i.Prune(ctx)
})
return client, i, nil
}
func main() {
loggerRaw, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer func() {
_ = loggerRaw.Sync()
}()
ctx := context.Background()
ctx = ctxzap.ToContext(ctx, loggerRaw)
ctx, cancel := context.WithCancel(ctx)
go handleSignals(ctx, []context.CancelFunc{cancel})
logger := loggerRaw.Sugar().Named("main")
cfg, err := configuration.LoadConfiguration(configuration.DataDirectory)
if err != nil {
logger.Fatalw("unable to load configuration", "error", err)
}
logger.Infow("loaded configuration", "configuration", types.PrintStruct(cfg))
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return utils.MonitorMemoryUsage(ctx, maxHeapUsage)
})
var i *indexer.Indexer
var client *bitcoin.Client
if cfg.Mode == configuration.Online {
client, i, err = startOnlineDependencies(ctx, cancel, cfg, g)
if err != nil {
logger.Fatalw("unable to start online dependencies", "error", err)
}
}
// The asserter automatically rejects incorrectly formatted
// requests.
asserter, err := asserter.NewServer(
bitcoin.OperationTypes,
false,
[]*types.NetworkIdentifier{cfg.Network},
)
if err != nil {
logger.Fatalw("unable to create new server asserter", "error", err)
}
router := services.NewBlockchainRouter(cfg, client, i, asserter)
loggedRouter := services.LoggerMiddleware(loggerRaw, router)
corsRouter := server.CorsMiddleware(loggedRouter)
server := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Handler: corsRouter,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
g.Go(func() error {
logger.Infow("server listening", "port", cfg.Port)
return server.ListenAndServe()
})
g.Go(func() error {
// If we don't shutdown server in errgroup, it will
// never stop because server.ListenAndServe doesn't
// take any context.
<-ctx.Done()
return server.Shutdown(ctx)
})
err = g.Wait()
// We always want to attempt to close the database, regardless of the error.
// We also want to do this after all indexer goroutines have stopped.
if i != nil {
i.CloseDatabase(ctx)
}
if signalReceived {
logger.Fatalw("rosetta-bitcoin halted")
}
if err != nil {
logger.Fatalw("rosetta-bitcoin sync failed", "error", err)
}
}

119
mocks/indexer/client.go Normal file
View file

@ -0,0 +1,119 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package indexer
import (
context "context"
bitcoin "github.com/coinbase/rosetta-bitcoin/bitcoin"
mock "github.com/stretchr/testify/mock"
storage "github.com/coinbase/rosetta-sdk-go/storage"
types "github.com/coinbase/rosetta-sdk-go/types"
)
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
// GetRawBlock provides a mock function with given fields: _a0, _a1
func (_m *Client) GetRawBlock(_a0 context.Context, _a1 *types.PartialBlockIdentifier) (*bitcoin.Block, []string, error) {
ret := _m.Called(_a0, _a1)
var r0 *bitcoin.Block
if rf, ok := ret.Get(0).(func(context.Context, *types.PartialBlockIdentifier) *bitcoin.Block); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*bitcoin.Block)
}
}
var r1 []string
if rf, ok := ret.Get(1).(func(context.Context, *types.PartialBlockIdentifier) []string); ok {
r1 = rf(_a0, _a1)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]string)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, *types.PartialBlockIdentifier) error); ok {
r2 = rf(_a0, _a1)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// NetworkStatus provides a mock function with given fields: _a0
func (_m *Client) NetworkStatus(_a0 context.Context) (*types.NetworkStatusResponse, error) {
ret := _m.Called(_a0)
var r0 *types.NetworkStatusResponse
if rf, ok := ret.Get(0).(func(context.Context) *types.NetworkStatusResponse); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.NetworkStatusResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ParseBlock provides a mock function with given fields: _a0, _a1, _a2
func (_m *Client) ParseBlock(_a0 context.Context, _a1 *bitcoin.Block, _a2 map[string]*storage.AccountCoin) (*types.Block, error) {
ret := _m.Called(_a0, _a1, _a2)
var r0 *types.Block
if rf, ok := ret.Get(0).(func(context.Context, *bitcoin.Block, map[string]*storage.AccountCoin) *types.Block); ok {
r0 = rf(_a0, _a1, _a2)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.Block)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *bitcoin.Block, map[string]*storage.AccountCoin) error); ok {
r1 = rf(_a0, _a1, _a2)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PruneBlockchain provides a mock function with given fields: _a0, _a1
func (_m *Client) PruneBlockchain(_a0 context.Context, _a1 int64) (int64, error) {
ret := _m.Called(_a0, _a1)
var r0 int64
if rf, ok := ret.Get(0).(func(context.Context, int64) int64); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

81
mocks/services/client.go Normal file
View file

@ -0,0 +1,81 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package services
import (
context "context"
mock "github.com/stretchr/testify/mock"
types "github.com/coinbase/rosetta-sdk-go/types"
)
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
// NetworkStatus provides a mock function with given fields: _a0
func (_m *Client) NetworkStatus(_a0 context.Context) (*types.NetworkStatusResponse, error) {
ret := _m.Called(_a0)
var r0 *types.NetworkStatusResponse
if rf, ok := ret.Get(0).(func(context.Context) *types.NetworkStatusResponse); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.NetworkStatusResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendRawTransaction provides a mock function with given fields: _a0, _a1
func (_m *Client) SendRawTransaction(_a0 context.Context, _a1 string) (string, error) {
ret := _m.Called(_a0, _a1)
var r0 string
if rf, ok := ret.Get(0).(func(context.Context, string) string); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SuggestedFeeRate provides a mock function with given fields: _a0, _a1
func (_m *Client) SuggestedFeeRate(_a0 context.Context, _a1 int64) (float64, error) {
ret := _m.Called(_a0, _a1)
var r0 float64
if rf, ok := ret.Get(0).(func(context.Context, int64) float64); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Get(0).(float64)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

119
mocks/services/indexer.go Normal file
View file

@ -0,0 +1,119 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package services
import (
context "context"
bitcoin "github.com/coinbase/rosetta-bitcoin/bitcoin"
mock "github.com/stretchr/testify/mock"
types "github.com/coinbase/rosetta-sdk-go/types"
)
// Indexer is an autogenerated mock type for the Indexer type
type Indexer struct {
mock.Mock
}
// GetBlockLazy provides a mock function with given fields: _a0, _a1
func (_m *Indexer) GetBlockLazy(_a0 context.Context, _a1 *types.PartialBlockIdentifier) (*types.BlockResponse, error) {
ret := _m.Called(_a0, _a1)
var r0 *types.BlockResponse
if rf, ok := ret.Get(0).(func(context.Context, *types.PartialBlockIdentifier) *types.BlockResponse); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.BlockResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *types.PartialBlockIdentifier) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBlockTransaction provides a mock function with given fields: _a0, _a1, _a2
func (_m *Indexer) GetBlockTransaction(_a0 context.Context, _a1 *types.BlockIdentifier, _a2 *types.TransactionIdentifier) (*types.Transaction, error) {
ret := _m.Called(_a0, _a1, _a2)
var r0 *types.Transaction
if rf, ok := ret.Get(0).(func(context.Context, *types.BlockIdentifier, *types.TransactionIdentifier) *types.Transaction); ok {
r0 = rf(_a0, _a1, _a2)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*types.Transaction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *types.BlockIdentifier, *types.TransactionIdentifier) error); ok {
r1 = rf(_a0, _a1, _a2)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCoins provides a mock function with given fields: _a0, _a1
func (_m *Indexer) GetCoins(_a0 context.Context, _a1 *types.AccountIdentifier) ([]*types.Coin, *types.BlockIdentifier, error) {
ret := _m.Called(_a0, _a1)
var r0 []*types.Coin
if rf, ok := ret.Get(0).(func(context.Context, *types.AccountIdentifier) []*types.Coin); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*types.Coin)
}
}
var r1 *types.BlockIdentifier
if rf, ok := ret.Get(1).(func(context.Context, *types.AccountIdentifier) *types.BlockIdentifier); ok {
r1 = rf(_a0, _a1)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*types.BlockIdentifier)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, *types.AccountIdentifier) error); ok {
r2 = rf(_a0, _a1)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// GetScriptPubKeys provides a mock function with given fields: _a0, _a1
func (_m *Indexer) GetScriptPubKeys(_a0 context.Context, _a1 []*types.Coin) ([]*bitcoin.ScriptPubKey, error) {
ret := _m.Called(_a0, _a1)
var r0 []*bitcoin.ScriptPubKey
if rf, ok := ret.Get(0).(func(context.Context, []*types.Coin) []*bitcoin.ScriptPubKey); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*bitcoin.ScriptPubKey)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []*types.Coin) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View file

@ -0,0 +1,380 @@
{
"network": {
"blockchain": "Bitcoin",
"network": "Mainnet"
},
"data_directory": "bitcoin-mainnet-data",
"http_timeout": 300,
"max_retries": 5,
"retry_elapsed_time": 0,
"max_online_connections": 0,
"max_sync_concurrency": 0,
"tip_delay": 1800,
"log_configuration": false,
"construction": {
"max_offline_connections": 0,
"stale_depth": 0,
"broadcast_limit": 0,
"ignore_broadcast_failures": false,
"clear_broadcasts": false,
"broadcast_behind_tip": false,
"block_broadcast_limit": 0,
"rebroadcast_all": false,
"workflows": [
{
"name": "request_funds",
"concurrency": 1,
"scenarios": [
{
"name": "find_account",
"actions": [
{
"input": "{\"symbol\":\"BTC\", \"decimals\":8}",
"type": "set_variable",
"output_path": "currency"
},
{
"input": "{\"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\":1}",
"type": "find_balance",
"output_path": "random_account"
}
]
},
{
"name": "request",
"actions": [
{
"input": "{\"account_identifier\": {{random_account.account_identifier}}, \"minimum_balance\":{\"value\": \"1000000\", \"currency\": {{currency}}}}",
"type": "find_balance",
"output_path": "loaded_account"
}
]
}
]
},
{
"name": "create_account",
"concurrency": 1,
"scenarios": [
{
"name": "create_account",
"actions": [
{
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "network"
},
{
"input": "{\"curve_type\": \"secp256k1\"}",
"type": "generate_key",
"output_path": "key"
},
{
"input": "{\"network_identifier\": {{network}}, \"public_key\": {{key.public_key}}}",
"type": "derive",
"output_path": "account"
},
{
"input": "{\"account_identifier\": {{account.account_identifier}}, \"keypair\": {{key}}}",
"type": "save_account"
}
]
}
]
},
{
"name": "transfer",
"concurrency": 10,
"scenarios": [
{
"name": "transfer_dry_run",
"actions": [
{
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer_dry_run.network"
},
{
"input": "{\"symbol\":\"BTC\", \"decimals\":8}",
"type": "set_variable",
"output_path": "currency"
},
{
"input": "\"600\"",
"type": "set_variable",
"output_path": "dust_amount"
},
{
"input": "\"1200\"",
"type": "set_variable",
"output_path": "max_fee_amount"
},
{
"input": "{\"operation\":\"addition\", \"left_value\": {{dust_amount}}, \"right_value\": {{max_fee_amount}}}",
"type": "math",
"output_path": "send_buffer"
},
{
"input": "\"2400\"",
"type": "set_variable",
"output_path": "reserved_amount"
},
{
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
"type": "find_balance",
"output_path": "sender"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{send_buffer}}}",
"type": "math",
"output_path": "available_amount"
},
{
"input": "{\"minimum\": {{dust_amount}}, \"maximum\": {{available_amount}}}",
"type": "random_number",
"output_path": "recipient_amount"
},
{
"input": "{\"recipient_amount\":{{recipient_amount}}}",
"type": "print_message"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{recipient_amount}}}",
"type": "math",
"output_path": "total_change_amount"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{max_fee_amount}}}",
"type": "math",
"output_path": "change_amount"
},
{
"input": "{\"change_amount\":{{change_amount}}}",
"type": "print_message"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
"type": "math",
"output_path": "sender_amount"
},
{
"input": "{\"not_account_identifier\":[{{sender.account_identifier}}], \"not_coins\":[{{sender.coin}}], \"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\": 100, \"create_probability\": 50}",
"type": "find_balance",
"output_path": "recipient"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer_dry_run.confirmation_depth"
},
{
"input": "\"true\"",
"type": "set_variable",
"output_path": "transfer_dry_run.dry_run"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer_dry_run.operations"
},
{
"input": "{{transfer_dry_run.operations}}",
"type": "print_message"
}
]
},
{
"name": "transfer",
"actions": [
{
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
"type": "find_currency_amount",
"output_path": "suggested_fee"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{suggested_fee.value}}}",
"type": "math",
"output_path": "change_amount"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{change_amount}}, \"right_value\": {{dust_amount}}}",
"type": "math",
"output_path": "change_minus_dust"
},
{
"input": "{{change_minus_dust}}",
"type": "assert"
},
{
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer.network"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer.confirmation_depth"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer.operations"
},
{
"input": "{{transfer.operations}}",
"type": "print_message"
}
]
}
]
},
{
"name": "return_funds",
"concurrency": 10,
"scenarios": [
{
"name": "transfer_dry_run",
"actions": [
{
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer_dry_run.network"
},
{
"input": "{\"symbol\":\"BTC\", \"decimals\":8}",
"type": "set_variable",
"output_path": "currency"
},
{
"input": "\"1200\"",
"type": "set_variable",
"output_path": "max_fee_amount"
},
{
"input": "\"1800\"",
"type": "set_variable",
"output_path": "reserved_amount"
},
{
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
"type": "find_balance",
"output_path": "sender"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{max_fee_amount}}}",
"type": "math",
"output_path": "recipient_amount"
},
{
"input": "{\"recipient_amount\":{{recipient_amount}}}",
"type": "print_message"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
"type": "math",
"output_path": "sender_amount"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer_dry_run.confirmation_depth"
},
{
"input": "\"true\"",
"type": "set_variable",
"output_path": "transfer_dry_run.dry_run"
},
{
"input": "{\"address\": \"mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt\"}",
"type": "set_variable",
"output_path": "recipient"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer_dry_run.operations"
},
{
"input": "{{transfer_dry_run.operations}}",
"type": "print_message"
}
]
},
{
"name": "transfer",
"actions": [
{
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
"type": "find_currency_amount",
"output_path": "suggested_fee"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{suggested_fee.value}}}",
"type": "math",
"output_path": "recipient_amount"
},
{
"input": "\"600\"",
"type": "set_variable",
"output_path": "dust_amount"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{recipient_amount}}, \"right_value\": {{dust_amount}}}",
"type": "math",
"output_path": "recipient_minus_dust"
},
{
"input": "{{recipient_minus_dust}}",
"type": "assert"
},
{
"input": "{\"network\":\"Mainnet\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer.network"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer.confirmation_depth"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer.operations"
},
{
"input": "{{transfer.operations}}",
"type": "print_message"
}
]
}
]
}
],
"end_conditions": {
"create_account": 10,
"transfer": 10
}
},
"data": {
"active_reconciliation_concurrency": 0,
"inactive_reconciliation_concurrency": 0,
"inactive_reconciliation_frequency": 0,
"log_blocks": false,
"log_transactions": false,
"log_balance_changes": false,
"log_reconciliations": false,
"ignore_reconciliation_error": false,
"exempt_accounts": "",
"bootstrap_balances": "",
"interesting_accounts": "",
"reconciliation_disabled": false,
"inactive_discrepency_search_disabled": false,
"balance_tracking_disabled": false,
"coin_tracking_disabled": false,
"end_conditions": {
"reconciliation_coverage": 0.95
},
"results_output_file": ""
}
}

View file

@ -0,0 +1,380 @@
{
"network": {
"blockchain": "Bitcoin",
"network": "Testnet3"
},
"data_directory": "bitcoin-testnet-data",
"http_timeout": 300,
"max_retries": 5,
"retry_elapsed_time": 0,
"max_online_connections": 0,
"max_sync_concurrency": 0,
"tip_delay": 1800,
"log_configuration": false,
"construction": {
"max_offline_connections": 0,
"stale_depth": 0,
"broadcast_limit": 0,
"ignore_broadcast_failures": false,
"clear_broadcasts": false,
"broadcast_behind_tip": false,
"block_broadcast_limit": 0,
"rebroadcast_all": false,
"workflows": [
{
"name": "request_funds",
"concurrency": 1,
"scenarios": [
{
"name": "find_account",
"actions": [
{
"input": "{\"symbol\":\"tBTC\", \"decimals\":8}",
"type": "set_variable",
"output_path": "currency"
},
{
"input": "{\"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\":1}",
"type": "find_balance",
"output_path": "random_account"
}
]
},
{
"name": "request",
"actions": [
{
"input": "{\"account_identifier\": {{random_account.account_identifier}}, \"minimum_balance\":{\"value\": \"1000000\", \"currency\": {{currency}}}}",
"type": "find_balance",
"output_path": "loaded_account"
}
]
}
]
},
{
"name": "create_account",
"concurrency": 1,
"scenarios": [
{
"name": "create_account",
"actions": [
{
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "network"
},
{
"input": "{\"curve_type\": \"secp256k1\"}",
"type": "generate_key",
"output_path": "key"
},
{
"input": "{\"network_identifier\": {{network}}, \"public_key\": {{key.public_key}}}",
"type": "derive",
"output_path": "account"
},
{
"input": "{\"account_identifier\": {{account.account_identifier}}, \"keypair\": {{key}}}",
"type": "save_account"
}
]
}
]
},
{
"name": "transfer",
"concurrency": 10,
"scenarios": [
{
"name": "transfer_dry_run",
"actions": [
{
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer_dry_run.network"
},
{
"input": "{\"symbol\":\"tBTC\", \"decimals\":8}",
"type": "set_variable",
"output_path": "currency"
},
{
"input": "\"600\"",
"type": "set_variable",
"output_path": "dust_amount"
},
{
"input": "\"1200\"",
"type": "set_variable",
"output_path": "max_fee_amount"
},
{
"input": "{\"operation\":\"addition\", \"left_value\": {{dust_amount}}, \"right_value\": {{max_fee_amount}}}",
"type": "math",
"output_path": "send_buffer"
},
{
"input": "\"2400\"",
"type": "set_variable",
"output_path": "reserved_amount"
},
{
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
"type": "find_balance",
"output_path": "sender"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{send_buffer}}}",
"type": "math",
"output_path": "available_amount"
},
{
"input": "{\"minimum\": {{dust_amount}}, \"maximum\": {{available_amount}}}",
"type": "random_number",
"output_path": "recipient_amount"
},
{
"input": "{\"recipient_amount\":{{recipient_amount}}}",
"type": "print_message"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{recipient_amount}}}",
"type": "math",
"output_path": "total_change_amount"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{max_fee_amount}}}",
"type": "math",
"output_path": "change_amount"
},
{
"input": "{\"change_amount\":{{change_amount}}}",
"type": "print_message"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
"type": "math",
"output_path": "sender_amount"
},
{
"input": "{\"not_account_identifier\":[{{sender.account_identifier}}], \"not_coins\":[{{sender.coin}}], \"minimum_balance\":{\"value\": \"0\", \"currency\": {{currency}}}, \"create_limit\": 100, \"create_probability\": 50}",
"type": "find_balance",
"output_path": "recipient"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer_dry_run.confirmation_depth"
},
{
"input": "\"true\"",
"type": "set_variable",
"output_path": "transfer_dry_run.dry_run"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer_dry_run.operations"
},
{
"input": "{{transfer_dry_run.operations}}",
"type": "print_message"
}
]
},
{
"name": "transfer",
"actions": [
{
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
"type": "find_currency_amount",
"output_path": "suggested_fee"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{total_change_amount}}, \"right_value\": {{suggested_fee.value}}}",
"type": "math",
"output_path": "change_amount"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{change_amount}}, \"right_value\": {{dust_amount}}}",
"type": "math",
"output_path": "change_minus_dust"
},
{
"input": "{{change_minus_dust}}",
"type": "assert"
},
{
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer.network"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer.confirmation_depth"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient.account_identifier}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}, {\"operation_identifier\":{\"index\":2},\"type\":\"OUTPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{change_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer.operations"
},
{
"input": "{{transfer.operations}}",
"type": "print_message"
}
]
}
]
},
{
"name": "return_funds",
"concurrency": 10,
"scenarios": [
{
"name": "transfer_dry_run",
"actions": [
{
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer_dry_run.network"
},
{
"input": "{\"symbol\":\"tBTC\", \"decimals\":8}",
"type": "set_variable",
"output_path": "currency"
},
{
"input": "\"1200\"",
"type": "set_variable",
"output_path": "max_fee_amount"
},
{
"input": "\"1800\"",
"type": "set_variable",
"output_path": "reserved_amount"
},
{
"input": "{\"require_coin\":true, \"minimum_balance\":{\"value\": {{reserved_amount}}, \"currency\": {{currency}}}}",
"type": "find_balance",
"output_path": "sender"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{max_fee_amount}}}",
"type": "math",
"output_path": "recipient_amount"
},
{
"input": "{\"recipient_amount\":{{recipient_amount}}}",
"type": "print_message"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": \"0\", \"right_value\":{{sender.balance.value}}}",
"type": "math",
"output_path": "sender_amount"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer_dry_run.confirmation_depth"
},
{
"input": "\"true\"",
"type": "set_variable",
"output_path": "transfer_dry_run.dry_run"
},
{
"input": "{\"address\": \"mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt\"}",
"type": "set_variable",
"output_path": "recipient"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer_dry_run.operations"
},
{
"input": "{{transfer_dry_run.operations}}",
"type": "print_message"
}
]
},
{
"name": "transfer",
"actions": [
{
"input": "{\"currency\":{{currency}}, \"amounts\":{{transfer_dry_run.suggested_fee}}}",
"type": "find_currency_amount",
"output_path": "suggested_fee"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{sender.balance.value}}, \"right_value\": {{suggested_fee.value}}}",
"type": "math",
"output_path": "recipient_amount"
},
{
"input": "\"600\"",
"type": "set_variable",
"output_path": "dust_amount"
},
{
"input": "{\"operation\":\"subtraction\", \"left_value\": {{recipient_amount}}, \"right_value\": {{dust_amount}}}",
"type": "math",
"output_path": "recipient_minus_dust"
},
{
"input": "{{recipient_minus_dust}}",
"type": "assert"
},
{
"input": "{\"network\":\"Testnet3\", \"blockchain\":\"Bitcoin\"}",
"type": "set_variable",
"output_path": "transfer.network"
},
{
"input": "\"1\"",
"type": "set_variable",
"output_path": "transfer.confirmation_depth"
},
{
"input": "[{\"operation_identifier\":{\"index\":0},\"type\":\"INPUT\",\"account\":{{sender.account_identifier}},\"amount\":{\"value\":{{sender_amount}},\"currency\":{{currency}}}, \"coin_change\":{\"coin_action\":\"coin_spent\", \"coin_identifier\":{{sender.coin}}}},{\"operation_identifier\":{\"index\":1},\"type\":\"OUTPUT\",\"account\":{{recipient}},\"amount\":{\"value\":{{recipient_amount}},\"currency\":{{currency}}}}]",
"type": "set_variable",
"output_path": "transfer.operations"
},
{
"input": "{{transfer.operations}}",
"type": "print_message"
}
]
}
]
}
],
"end_conditions": {
"create_account": 10,
"transfer": 10
}
},
"data": {
"active_reconciliation_concurrency": 0,
"inactive_reconciliation_concurrency": 0,
"inactive_reconciliation_frequency": 0,
"log_blocks": false,
"log_transactions": false,
"log_balance_changes": false,
"log_reconciliations": false,
"ignore_reconciliation_error": false,
"exempt_accounts": "",
"bootstrap_balances": "",
"interesting_accounts": "",
"reconciliation_disabled": false,
"inactive_discrepency_search_disabled": false,
"balance_tracking_disabled": false,
"coin_tracking_disabled": false,
"end_conditions": {
"reconciliation_coverage": 0.95
},
"results_output_file": ""
}
}

View file

@ -0,0 +1,75 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
)
// AccountAPIService implements the server.AccountAPIServicer interface.
type AccountAPIService struct {
config *configuration.Configuration
i Indexer
}
// NewAccountAPIService returns a new *AccountAPIService.
func NewAccountAPIService(
config *configuration.Configuration,
i Indexer,
) server.AccountAPIServicer {
return &AccountAPIService{
config: config,
i: i,
}
}
// AccountBalance implements /account/balance.
func (s *AccountAPIService) AccountBalance(
ctx context.Context,
request *types.AccountBalanceRequest,
) (*types.AccountBalanceResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
coins, block, err := s.i.GetCoins(ctx, request.AccountIdentifier)
if err != nil {
return nil, wrapErr(ErrUnableToGetCoins, err)
}
balance := "0"
for _, coin := range coins {
balance, err = types.AddValues(balance, coin.Amount.Value)
if err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
}
return &types.AccountBalanceResponse{
BlockIdentifier: block,
Coins: coins,
Balances: []*types.Amount{
{
Value: balance,
Currency: s.config.Currency,
},
},
}, nil
}

View file

@ -0,0 +1,106 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"testing"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/stretchr/testify/assert"
)
func TestAccountBalance_Offline(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.Offline,
}
mockIndexer := &mocks.Indexer{}
servicer := NewAccountAPIService(cfg, mockIndexer)
ctx := context.Background()
bal, err := servicer.AccountBalance(ctx, &types.AccountBalanceRequest{})
assert.Nil(t, bal)
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
mockIndexer.AssertExpectations(t)
}
func TestAccountBalance_Online(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.Online,
Currency: bitcoin.MainnetCurrency,
}
mockIndexer := &mocks.Indexer{}
servicer := NewAccountAPIService(cfg, mockIndexer)
ctx := context.Background()
account := &types.AccountIdentifier{
Address: "hello",
}
coins := []*types.Coin{
{
Amount: &types.Amount{
Value: "10",
},
CoinIdentifier: &types.CoinIdentifier{
Identifier: "coin 1",
},
},
{
Amount: &types.Amount{
Value: "15",
},
CoinIdentifier: &types.CoinIdentifier{
Identifier: "coin 2",
},
},
{
Amount: &types.Amount{
Value: "0",
},
CoinIdentifier: &types.CoinIdentifier{
Identifier: "coin 3",
},
},
}
block := &types.BlockIdentifier{
Index: 1000,
Hash: "block 1000",
}
mockIndexer.On("GetCoins", ctx, account).Return(coins, block, nil).Once()
bal, err := servicer.AccountBalance(ctx, &types.AccountBalanceRequest{
AccountIdentifier: account,
})
assert.Nil(t, err)
assert.Equal(t, &types.AccountBalanceResponse{
BlockIdentifier: block,
Coins: coins,
Balances: []*types.Amount{
{
Value: "25",
Currency: bitcoin.MainnetCurrency,
},
},
}, bal)
mockIndexer.AssertExpectations(t)
}

81
services/block_service.go Normal file
View file

@ -0,0 +1,81 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
)
// BlockAPIService implements the server.BlockAPIServicer interface.
type BlockAPIService struct {
config *configuration.Configuration
i Indexer
}
// NewBlockAPIService creates a new instance of a BlockAPIService.
func NewBlockAPIService(
config *configuration.Configuration,
i Indexer,
) server.BlockAPIServicer {
return &BlockAPIService{
config: config,
i: i,
}
}
// Block implements the /block endpoint.
func (s *BlockAPIService) Block(
ctx context.Context,
request *types.BlockRequest,
) (*types.BlockResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
blockResponse, err := s.i.GetBlockLazy(ctx, request.BlockIdentifier)
if err != nil {
return nil, wrapErr(ErrBlockNotFound, err)
}
return blockResponse, nil
}
// BlockTransaction implements the /block/transaction endpoint.
func (s *BlockAPIService) BlockTransaction(
ctx context.Context,
request *types.BlockTransactionRequest,
) (*types.BlockTransactionResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
transaction, err := s.i.GetBlockTransaction(
ctx,
request.BlockIdentifier,
request.TransactionIdentifier,
)
if err != nil {
return nil, wrapErr(ErrTransactionNotFound, err)
}
return &types.BlockTransactionResponse{
Transaction: transaction,
}, nil
}

View file

@ -0,0 +1,120 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"testing"
"github.com/coinbase/rosetta-bitcoin/configuration"
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/stretchr/testify/assert"
)
func TestBlockService_Offline(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.Offline,
}
mockIndexer := &mocks.Indexer{}
servicer := NewBlockAPIService(cfg, mockIndexer)
ctx := context.Background()
block, err := servicer.Block(ctx, &types.BlockRequest{})
assert.Nil(t, block)
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
assert.Equal(t, ErrUnavailableOffline.Message, err.Message)
blockTransaction, err := servicer.BlockTransaction(ctx, &types.BlockTransactionRequest{})
assert.Nil(t, blockTransaction)
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
assert.Equal(t, ErrUnavailableOffline.Message, err.Message)
mockIndexer.AssertExpectations(t)
}
func TestBlockService_Online(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.Online,
}
mockIndexer := &mocks.Indexer{}
servicer := NewBlockAPIService(cfg, mockIndexer)
ctx := context.Background()
block := &types.Block{
BlockIdentifier: &types.BlockIdentifier{
Index: 100,
Hash: "block 100",
},
}
blockResponse := &types.BlockResponse{
Block: block,
OtherTransactions: []*types.TransactionIdentifier{
{
Hash: "tx1",
},
},
}
transaction := &types.Transaction{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: "tx1",
},
}
t.Run("nil identifier", func(t *testing.T) {
mockIndexer.On(
"GetBlockLazy",
ctx,
(*types.PartialBlockIdentifier)(nil),
).Return(
blockResponse,
nil,
).Once()
b, err := servicer.Block(ctx, &types.BlockRequest{})
assert.Nil(t, err)
assert.Equal(t, blockResponse, b)
})
t.Run("populated identifier", func(t *testing.T) {
pbIdentifier := types.ConstructPartialBlockIdentifier(block.BlockIdentifier)
mockIndexer.On("GetBlockLazy", ctx, pbIdentifier).Return(blockResponse, nil).Once()
b, err := servicer.Block(ctx, &types.BlockRequest{
BlockIdentifier: pbIdentifier,
})
assert.Nil(t, err)
assert.Equal(t, blockResponse, b)
mockIndexer.On(
"GetBlockTransaction",
ctx,
blockResponse.Block.BlockIdentifier,
transaction.TransactionIdentifier,
).Return(
transaction,
nil,
).Once()
blockTransaction, err := servicer.BlockTransaction(ctx, &types.BlockTransactionRequest{
BlockIdentifier: blockResponse.Block.BlockIdentifier,
TransactionIdentifier: transaction.TransactionIdentifier,
})
assert.Nil(t, err)
assert.Equal(t, transaction, blockTransaction.Transaction)
})
mockIndexer.AssertExpectations(t)
}

View file

@ -0,0 +1,788 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/coinbase/rosetta-sdk-go/parser"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
)
const (
// bytesInKB is the number of bytes in a KB. In Bitcoin, this is
// considered to be 1000.
bytesInKb = float64(1000) // nolint:gomnd
// defaultConfirmationTarget is the number of blocks we would
// like our transaction to be included by.
defaultConfirmationTarget = int64(2) // nolint:gomnd
)
// ConstructionAPIService implements the server.ConstructionAPIServicer interface.
type ConstructionAPIService struct {
config *configuration.Configuration
client Client
i Indexer
}
// NewConstructionAPIService creates a new instance of a ConstructionAPIService.
func NewConstructionAPIService(
config *configuration.Configuration,
client Client,
i Indexer,
) server.ConstructionAPIServicer {
return &ConstructionAPIService{
config: config,
client: client,
i: i,
}
}
// ConstructionDerive implements the /construction/derive endpoint.
func (s *ConstructionAPIService) ConstructionDerive(
ctx context.Context,
request *types.ConstructionDeriveRequest,
) (*types.ConstructionDeriveResponse, *types.Error) {
addr, err := btcutil.NewAddressWitnessPubKeyHash(
btcutil.Hash160(request.PublicKey.Bytes),
s.config.Params,
)
if err != nil {
return nil, wrapErr(ErrUnableToDerive, err)
}
return &types.ConstructionDeriveResponse{
AccountIdentifier: &types.AccountIdentifier{
Address: addr.EncodeAddress(),
},
}, nil
}
// estimateSize returns the estimated size of a transaction in vBytes.
func (s *ConstructionAPIService) estimateSize(operations []*types.Operation) float64 {
size := bitcoin.TransactionOverhead
for _, operation := range operations {
switch operation.Type {
case bitcoin.InputOpType:
size += bitcoin.InputSize
case bitcoin.OutputOpType:
size += bitcoin.OutputOverhead
addr, err := btcutil.DecodeAddress(operation.Account.Address, s.config.Params)
if err != nil {
size += bitcoin.P2PKHScriptPubkeySize
continue
}
script, err := txscript.PayToAddrScript(addr)
if err != nil {
size += bitcoin.P2PKHScriptPubkeySize
continue
}
size += len(script)
}
}
return float64(size)
}
// ConstructionPreprocess implements the /construction/preprocess
// endpoint.
func (s *ConstructionAPIService) ConstructionPreprocess(
ctx context.Context,
request *types.ConstructionPreprocessRequest,
) (*types.ConstructionPreprocessResponse, *types.Error) {
descriptions := &parser.Descriptions{
OperationDescriptions: []*parser.OperationDescription{
{
Type: bitcoin.InputOpType,
Account: &parser.AccountDescription{
Exists: true,
},
Amount: &parser.AmountDescription{
Exists: true,
Sign: parser.NegativeAmountSign,
Currency: s.config.Currency,
},
CoinAction: types.CoinSpent,
AllowRepeats: true,
},
},
}
matches, err := parser.MatchOperations(descriptions, request.Operations)
if err != nil {
return nil, wrapErr(ErrUnclearIntent, err)
}
coins := make([]*types.Coin, len(matches[0].Operations))
for i, input := range matches[0].Operations {
if input.CoinChange == nil {
return nil, wrapErr(ErrUnclearIntent, errors.New("CoinChange cannot be nil"))
}
coins[i] = &types.Coin{
CoinIdentifier: input.CoinChange.CoinIdentifier,
Amount: input.Amount,
}
}
options, err := types.MarshalMap(&preprocessOptions{
Coins: coins,
EstimatedSize: s.estimateSize(request.Operations),
FeeMultiplier: request.SuggestedFeeMultiplier,
})
if err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
return &types.ConstructionPreprocessResponse{
Options: options,
}, nil
}
// ConstructionMetadata implements the /construction/metadata endpoint.
func (s *ConstructionAPIService) ConstructionMetadata(
ctx context.Context,
request *types.ConstructionMetadataRequest,
) (*types.ConstructionMetadataResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
var options preprocessOptions
if err := types.UnmarshalMap(request.Options, &options); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
// Determine feePerKB and ensure it is not below the minimum fee
// relay rate.
feePerKB, err := s.client.SuggestedFeeRate(ctx, defaultConfirmationTarget)
if err != nil {
return nil, wrapErr(ErrCouldNotGetFeeRate, err)
}
if options.FeeMultiplier != nil {
feePerKB *= *options.FeeMultiplier
}
if feePerKB < bitcoin.MinFeeRate {
feePerKB = bitcoin.MinFeeRate
}
// Calculated the estimated fee in Satoshis
satoshisPerB := (feePerKB * float64(bitcoin.SatoshisInBitcoin)) / bytesInKb
estimatedFee := satoshisPerB * options.EstimatedSize
suggestedFee := &types.Amount{
Value: fmt.Sprintf("%d", int64(estimatedFee)),
Currency: s.config.Currency,
}
scripts, err := s.i.GetScriptPubKeys(ctx, options.Coins)
if err != nil {
return nil, wrapErr(ErrScriptPubKeysMissing, err)
}
metadata, err := types.MarshalMap(&constructionMetadata{ScriptPubKeys: scripts})
if err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
return &types.ConstructionMetadataResponse{
Metadata: metadata,
SuggestedFee: []*types.Amount{suggestedFee},
}, nil
}
// ConstructionPayloads implements the /construction/payloads endpoint.
func (s *ConstructionAPIService) ConstructionPayloads(
ctx context.Context,
request *types.ConstructionPayloadsRequest,
) (*types.ConstructionPayloadsResponse, *types.Error) {
descriptions := &parser.Descriptions{
OperationDescriptions: []*parser.OperationDescription{
{
Type: bitcoin.InputOpType,
Account: &parser.AccountDescription{
Exists: true,
},
Amount: &parser.AmountDescription{
Exists: true,
Sign: parser.NegativeAmountSign,
Currency: s.config.Currency,
},
AllowRepeats: true,
CoinAction: types.CoinSpent,
},
{
Type: bitcoin.OutputOpType,
Account: &parser.AccountDescription{
Exists: true,
},
Amount: &parser.AmountDescription{
Exists: true,
Sign: parser.PositiveAmountSign,
Currency: s.config.Currency,
},
AllowRepeats: true,
},
},
ErrUnmatched: true,
}
matches, err := parser.MatchOperations(descriptions, request.Operations)
if err != nil {
return nil, wrapErr(ErrUnclearIntent, err)
}
tx := wire.NewMsgTx(wire.TxVersion)
for _, input := range matches[0].Operations {
if input.CoinChange == nil {
return nil, wrapErr(ErrUnclearIntent, errors.New("CoinChange cannot be nil"))
}
transactionHash, index, err := bitcoin.ParseCoinIdentifier(input.CoinChange.CoinIdentifier)
if err != nil {
return nil, wrapErr(ErrInvalidCoin, err)
}
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *transactionHash,
Index: index,
},
SignatureScript: nil,
Sequence: wire.MaxTxInSequenceNum,
})
}
for i, output := range matches[1].Operations {
addr, err := btcutil.DecodeAddress(output.Account.Address, s.config.Params)
if err != nil {
return nil, wrapErr(ErrUnableToDecodeAddress, fmt.Errorf(
"%w unable to decode address %s",
err,
output.Account.Address,
),
)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to construct payToAddrScript", err),
)
}
tx.AddTxOut(&wire.TxOut{
Value: matches[1].Amounts[i].Int64(),
PkScript: pkScript,
})
}
// Create Signing Payloads (must be done after entire tx is constructed
// or hash will not be correct).
inputAmounts := make([]string, len(tx.TxIn))
inputAddresses := make([]string, len(tx.TxIn))
payloads := make([]*types.SigningPayload, len(tx.TxIn))
var metadata constructionMetadata
if err := types.UnmarshalMap(request.Metadata, &metadata); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
for i := range tx.TxIn {
address := matches[0].Operations[i].Account.Address
script, err := hex.DecodeString(metadata.ScriptPubKeys[i].Hex)
if err != nil {
return nil, wrapErr(ErrUnableToDecodeScriptPubKey, err)
}
class, _, err := bitcoin.ParseSingleAddress(s.config.Params, script)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse address for utxo %d", err, i),
)
}
inputAddresses[i] = address
inputAmounts[i] = matches[0].Amounts[i].String()
absAmount := new(big.Int).Abs(matches[0].Amounts[i]).Int64()
switch class {
case txscript.WitnessV0PubKeyHashTy:
hash, err := txscript.CalcWitnessSigHash(
script,
txscript.NewTxSigHashes(tx),
txscript.SigHashAll,
tx,
i,
absAmount,
)
if err != nil {
return nil, wrapErr(ErrUnableToCalculateSignatureHash, err)
}
payloads[i] = &types.SigningPayload{
AccountIdentifier: &types.AccountIdentifier{
Address: address,
},
Bytes: hash,
SignatureType: types.Ecdsa,
}
default:
return nil, wrapErr(
ErrUnsupportedScriptType,
fmt.Errorf("unupported script type: %s", class),
)
}
}
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
if err := tx.Serialize(buf); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
rawTx, err := json.Marshal(&unsignedTransaction{
Transaction: hex.EncodeToString(buf.Bytes()),
ScriptPubKeys: metadata.ScriptPubKeys,
InputAmounts: inputAmounts,
InputAddresses: inputAddresses,
})
if err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
}
return &types.ConstructionPayloadsResponse{
UnsignedTransaction: hex.EncodeToString(rawTx),
Payloads: payloads,
}, nil
}
func normalizeSignature(signature []byte) []byte {
sig := btcec.Signature{ // signature is in form of R || S
R: new(big.Int).SetBytes(signature[:32]),
S: new(big.Int).SetBytes(signature[32:64]),
}
return append(sig.Serialize(), byte(txscript.SigHashAll))
}
// ConstructionCombine implements the /construction/combine
// endpoint.
func (s *ConstructionAPIService) ConstructionCombine(
ctx context.Context,
request *types.ConstructionCombineRequest,
) (*types.ConstructionCombineResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.UnsignedTransaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var unsigned unsignedTransaction
if err := json.Unmarshal(decodedTx, &unsigned); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal bitcoin transaction", err),
)
}
decodedCoreTx, err := hex.DecodeString(unsigned.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(decodedCoreTx)); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to deserialize tx", err),
)
}
for i := range tx.TxIn {
decodedScript, err := hex.DecodeString(unsigned.ScriptPubKeys[i].Hex)
if err != nil {
return nil, wrapErr(ErrUnableToDecodeScriptPubKey, err)
}
class, _, err := bitcoin.ParseSingleAddress(s.config.Params, decodedScript)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse address for script", err),
)
}
pkData := request.Signatures[i].PublicKey.Bytes
fullsig := normalizeSignature(request.Signatures[i].Bytes)
switch class {
case txscript.WitnessV0PubKeyHashTy:
tx.TxIn[i].Witness = wire.TxWitness{fullsig, pkData}
default:
return nil, wrapErr(
ErrUnsupportedScriptType,
fmt.Errorf("unupported script type: %s", class),
)
}
}
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
if err := tx.Serialize(buf); err != nil {
return nil, wrapErr(ErrUnableToParseIntermediateResult, fmt.Errorf("%w serialize tx", err))
}
rawTx, err := json.Marshal(&signedTransaction{
Transaction: hex.EncodeToString(buf.Bytes()),
InputAmounts: unsigned.InputAmounts,
})
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to serialize signed tx", err),
)
}
return &types.ConstructionCombineResponse{
SignedTransaction: hex.EncodeToString(rawTx),
}, nil
}
// ConstructionHash implements the /construction/hash endpoint.
func (s *ConstructionAPIService) ConstructionHash(
ctx context.Context,
request *types.ConstructionHashRequest,
) (*types.TransactionIdentifierResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.SignedTransaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w signed transaction cannot be decoded", err),
)
}
var signed signedTransaction
if err := json.Unmarshal(decodedTx, &signed); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
)
}
bytesTx, err := hex.DecodeString(signed.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to decode hex transaction", err),
)
}
tx, err := btcutil.NewTxFromBytes(bytesTx)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to parse transaction", err),
)
}
return &types.TransactionIdentifierResponse{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: tx.Hash().String(),
},
}, nil
}
func (s *ConstructionAPIService) parseUnsignedTransaction(
request *types.ConstructionParseRequest,
) (*types.ConstructionParseResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var unsigned unsignedTransaction
if err := json.Unmarshal(decodedTx, &unsigned); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal bitcoin transaction", err),
)
}
decodedCoreTx, err := hex.DecodeString(unsigned.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w transaction cannot be decoded", err),
)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(decodedCoreTx)); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to deserialize tx", err),
)
}
ops := []*types.Operation{}
for i, input := range tx.TxIn {
networkIndex := int64(i)
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.InputOpType,
Account: &types.AccountIdentifier{
Address: unsigned.InputAddresses[i],
},
Amount: &types.Amount{
Value: unsigned.InputAmounts[i],
Currency: s.config.Currency,
},
CoinChange: &types.CoinChange{
CoinAction: types.CoinSpent,
CoinIdentifier: &types.CoinIdentifier{
Identifier: fmt.Sprintf(
"%s:%d",
input.PreviousOutPoint.Hash.String(),
input.PreviousOutPoint.Index,
),
},
},
})
}
for i, output := range tx.TxOut {
networkIndex := int64(i)
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, output.PkScript)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse output address", err),
)
}
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: addr.String(),
},
Amount: &types.Amount{
Value: strconv.FormatInt(output.Value, 10),
Currency: s.config.Currency,
},
})
}
return &types.ConstructionParseResponse{
Operations: ops,
AccountIdentifierSigners: []*types.AccountIdentifier{},
}, nil
}
func (s *ConstructionAPIService) parseSignedTransaction(
request *types.ConstructionParseRequest,
) (*types.ConstructionParseResponse, *types.Error) {
decodedTx, err := hex.DecodeString(request.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w signed transaction cannot be decoded", err),
)
}
var signed signedTransaction
if err := json.Unmarshal(decodedTx, &signed); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
)
}
serializedTx, err := hex.DecodeString(signed.Transaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to decode hex transaction", err),
)
}
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(serializedTx)); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to decode msgTx", err),
)
}
ops := []*types.Operation{}
signers := []*types.AccountIdentifier{}
for i, input := range tx.TxIn {
pkScript, err := txscript.ComputePkScript(input.SignatureScript, input.Witness)
if err != nil {
return nil, wrapErr(
ErrUnableToComputePkScript,
fmt.Errorf("%w: unable to compute pk script", err),
)
}
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, pkScript.Script())
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to decode address", err),
)
}
networkIndex := int64(i)
signers = append(signers, &types.AccountIdentifier{
Address: addr.EncodeAddress(),
})
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.InputOpType,
Account: &types.AccountIdentifier{
Address: addr.EncodeAddress(),
},
Amount: &types.Amount{
Value: signed.InputAmounts[i],
Currency: s.config.Currency,
},
CoinChange: &types.CoinChange{
CoinAction: types.CoinSpent,
CoinIdentifier: &types.CoinIdentifier{
Identifier: fmt.Sprintf(
"%s:%d",
input.PreviousOutPoint.Hash.String(),
input.PreviousOutPoint.Index,
),
},
},
})
}
for i, output := range tx.TxOut {
networkIndex := int64(i)
_, addr, err := bitcoin.ParseSingleAddress(s.config.Params, output.PkScript)
if err != nil {
return nil, wrapErr(
ErrUnableToDecodeAddress,
fmt.Errorf("%w unable to parse output address", err),
)
}
ops = append(ops, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(len(ops)),
NetworkIndex: &networkIndex,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: addr.String(),
},
Amount: &types.Amount{
Value: strconv.FormatInt(output.Value, 10),
Currency: s.config.Currency,
},
})
}
return &types.ConstructionParseResponse{
Operations: ops,
AccountIdentifierSigners: signers,
}, nil
}
// ConstructionParse implements the /construction/parse endpoint.
func (s *ConstructionAPIService) ConstructionParse(
ctx context.Context,
request *types.ConstructionParseRequest,
) (*types.ConstructionParseResponse, *types.Error) {
if request.Signed {
return s.parseSignedTransaction(request)
}
return s.parseUnsignedTransaction(request)
}
// ConstructionSubmit implements the /construction/submit endpoint.
func (s *ConstructionAPIService) ConstructionSubmit(
ctx context.Context,
request *types.ConstructionSubmitRequest,
) (*types.TransactionIdentifierResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
decodedTx, err := hex.DecodeString(request.SignedTransaction)
if err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w signed transaction cannot be decoded", err),
)
}
var signed signedTransaction
if err := json.Unmarshal(decodedTx, &signed); err != nil {
return nil, wrapErr(
ErrUnableToParseIntermediateResult,
fmt.Errorf("%w unable to unmarshal signed bitcoin transaction", err),
)
}
txHash, err := s.client.SendRawTransaction(ctx, signed.Transaction)
if err != nil {
return nil, wrapErr(ErrBitcoind, fmt.Errorf("%w unable to submit transaction", err))
}
return &types.TransactionIdentifierResponse{
TransactionIdentifier: &types.TransactionIdentifier{
Hash: txHash,
},
}, nil
}

View file

@ -0,0 +1,399 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"encoding/hex"
"testing"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/stretchr/testify/assert"
)
func forceHexDecode(t *testing.T, s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
t.Fatalf("could not decode hex %s", s)
}
return b
}
func forceMarshalMap(t *testing.T, i interface{}) map[string]interface{} {
m, err := types.MarshalMap(i)
if err != nil {
t.Fatalf("could not marshal map %s", types.PrintStruct(i))
}
return m
}
func TestConstructionService(t *testing.T) {
networkIdentifier = &types.NetworkIdentifier{
Network: bitcoin.TestnetNetwork,
Blockchain: bitcoin.Blockchain,
}
cfg := &configuration.Configuration{
Mode: configuration.Online,
Network: networkIdentifier,
Params: bitcoin.TestnetParams,
Currency: bitcoin.TestnetCurrency,
}
mockIndexer := &mocks.Indexer{}
mockClient := &mocks.Client{}
servicer := NewConstructionAPIService(cfg, mockClient, mockIndexer)
ctx := context.Background()
// Test Derive
publicKey := &types.PublicKey{
Bytes: forceHexDecode(
t,
"0325c9a4252789b31dbb3454ec647e9516e7c596bcde2bd5da71a60fab8644e438",
),
CurveType: types.Secp256k1,
}
deriveResponse, err := servicer.ConstructionDerive(ctx, &types.ConstructionDeriveRequest{
NetworkIdentifier: networkIdentifier,
PublicKey: publicKey,
})
assert.Nil(t, err)
assert.Equal(t, &types.ConstructionDeriveResponse{
AccountIdentifier: &types.AccountIdentifier{
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
},
}, deriveResponse)
// Test Preprocess
ops := []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: bitcoin.InputOpType,
Account: &types.AccountIdentifier{
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
},
Amount: &types.Amount{
Value: "-1000000",
Currency: bitcoin.TestnetCurrency,
},
CoinChange: &types.CoinChange{
CoinIdentifier: &types.CoinIdentifier{
Identifier: "b14157a5c50503c8cd202a173613dd27e0027343c3d50cf85852dd020bf59c7f:1",
},
CoinAction: types.CoinSpent,
},
},
{
OperationIdentifier: &types.OperationIdentifier{
Index: 1,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: "tb1q3r8xjf0c2yazxnq9ey3wayelygfjxpfqjvj5v7",
},
Amount: &types.Amount{
Value: "954843",
Currency: bitcoin.TestnetCurrency,
},
},
{
OperationIdentifier: &types.OperationIdentifier{
Index: 2,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: "tb1qjsrjvk2ug872pdypp33fjxke62y7awpgefr6ua",
},
Amount: &types.Amount{
Value: "44657",
Currency: bitcoin.TestnetCurrency,
},
},
}
feeMultiplier := float64(0.75)
preprocessResponse, err := servicer.ConstructionPreprocess(
ctx,
&types.ConstructionPreprocessRequest{
NetworkIdentifier: networkIdentifier,
Operations: ops,
SuggestedFeeMultiplier: &feeMultiplier,
},
)
assert.Nil(t, err)
options := &preprocessOptions{
Coins: []*types.Coin{
{
CoinIdentifier: &types.CoinIdentifier{
Identifier: "b14157a5c50503c8cd202a173613dd27e0027343c3d50cf85852dd020bf59c7f:1",
},
Amount: &types.Amount{
Value: "-1000000",
Currency: bitcoin.TestnetCurrency,
},
},
},
EstimatedSize: 142,
FeeMultiplier: &feeMultiplier,
}
assert.Equal(t, &types.ConstructionPreprocessResponse{
Options: forceMarshalMap(t, options),
}, preprocessResponse)
// Test Metadata
metadata := &constructionMetadata{
ScriptPubKeys: []*bitcoin.ScriptPubKey{
{
ASM: "0 c005b00ad075d30b89a7b65b7dad8899ba6a9c55",
Hex: "0014c005b00ad075d30b89a7b65b7dad8899ba6a9c55",
RequiredSigs: 1,
Type: "witness_v0_keyhash",
Addresses: []string{
"tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
},
},
},
}
// Normal Fee
mockIndexer.On(
"GetScriptPubKeys",
ctx,
options.Coins,
).Return(
metadata.ScriptPubKeys,
nil,
).Once()
mockClient.On(
"SuggestedFeeRate",
ctx,
defaultConfirmationTarget,
).Return(
bitcoin.MinFeeRate*10,
nil,
).Once()
metadataResponse, err := servicer.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{
NetworkIdentifier: networkIdentifier,
Options: forceMarshalMap(t, options),
})
assert.Nil(t, err)
assert.Equal(t, &types.ConstructionMetadataResponse{
Metadata: forceMarshalMap(t, metadata),
SuggestedFee: []*types.Amount{
{
Value: "1065", // 1,420 * 0.75
Currency: bitcoin.TestnetCurrency,
},
},
}, metadataResponse)
// Low Fee
mockIndexer.On(
"GetScriptPubKeys",
ctx,
options.Coins,
).Return(
metadata.ScriptPubKeys,
nil,
).Once()
mockClient.On(
"SuggestedFeeRate",
ctx,
defaultConfirmationTarget,
).Return(
bitcoin.MinFeeRate,
nil,
).Once()
metadataResponse, err = servicer.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{
NetworkIdentifier: networkIdentifier,
Options: forceMarshalMap(t, options),
})
assert.Nil(t, err)
assert.Equal(t, &types.ConstructionMetadataResponse{
Metadata: forceMarshalMap(t, metadata),
SuggestedFee: []*types.Amount{
{
Value: "142", // we don't go below minimum fee rate
Currency: bitcoin.TestnetCurrency,
},
},
}, metadataResponse)
// Test Payloads
unsignedRaw := "7b227472616e73616374696f6e223a2230313030303030303031376639636635306230326464353235386638306364356333343337333032653032376464313333363137326132306364633830333035633561353537343162313031303030303030303066666666666666663032646239313065303030303030303030303136303031343838636536393235663835313361323334633035633932326565393333663232313332333035323037316165303030303030303030303030313630303134393430373236353935633431666361306234383130633632393931616439643238396565623832383030303030303030222c227363726970745075624b657973223a5b7b2261736d223a22302063303035623030616430373564333062383961376236356237646164383839396261366139633535222c22686578223a223030313463303035623030616430373564333062383961376236356237646164383839396261366139633535222c2272657153696773223a312c2274797065223a227769746e6573735f76305f6b657968617368222c22616464726573736573223a5b227462317163717a6d717a6b7377686673687a64386b6564686d7476676e78617834387a34666b6c68766d225d7d5d2c22696e7075745f616d6f756e7473223a5b222d31303030303030225d2c22696e7075745f616464726573736573223a5b227462317163717a6d717a6b7377686673687a64386b6564686d7476676e78617834387a34666b6c68766d225d7d" // nolint
payloadsResponse, err := servicer.ConstructionPayloads(ctx, &types.ConstructionPayloadsRequest{
NetworkIdentifier: networkIdentifier,
Operations: ops,
Metadata: forceMarshalMap(t, metadata),
})
val0 := int64(0)
val1 := int64(1)
parseOps := []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
NetworkIndex: &val0,
},
Type: bitcoin.InputOpType,
Account: &types.AccountIdentifier{
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
},
Amount: &types.Amount{
Value: "-1000000",
Currency: bitcoin.TestnetCurrency,
},
CoinChange: &types.CoinChange{
CoinIdentifier: &types.CoinIdentifier{
Identifier: "b14157a5c50503c8cd202a173613dd27e0027343c3d50cf85852dd020bf59c7f:1",
},
CoinAction: types.CoinSpent,
},
},
{
OperationIdentifier: &types.OperationIdentifier{
Index: 1,
NetworkIndex: &val0,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: "tb1q3r8xjf0c2yazxnq9ey3wayelygfjxpfqjvj5v7",
},
Amount: &types.Amount{
Value: "954843",
Currency: bitcoin.TestnetCurrency,
},
},
{
OperationIdentifier: &types.OperationIdentifier{
Index: 2,
NetworkIndex: &val1,
},
Type: bitcoin.OutputOpType,
Account: &types.AccountIdentifier{
Address: "tb1qjsrjvk2ug872pdypp33fjxke62y7awpgefr6ua",
},
Amount: &types.Amount{
Value: "44657",
Currency: bitcoin.TestnetCurrency,
},
},
}
assert.Nil(t, err)
signingPayload := &types.SigningPayload{
Bytes: forceHexDecode(
t,
"7b98f8b77fa6ef34044f320073118033afdffbd3fd3f8423889d9e5953ff4a30",
),
AccountIdentifier: &types.AccountIdentifier{
Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm",
},
SignatureType: types.Ecdsa,
}
assert.Equal(t, &types.ConstructionPayloadsResponse{
UnsignedTransaction: unsignedRaw,
Payloads: []*types.SigningPayload{signingPayload},
}, payloadsResponse)
// Test Parse Unsigned
parseUnsignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{
NetworkIdentifier: networkIdentifier,
Signed: false,
Transaction: unsignedRaw,
})
assert.Nil(t, err)
assert.Equal(t, &types.ConstructionParseResponse{
Operations: parseOps,
AccountIdentifierSigners: []*types.AccountIdentifier{},
}, parseUnsignedResponse)
// Test Combine
signedRaw := "7b227472616e73616374696f6e223a22303130303030303030303031303137663963663530623032646435323538663830636435633334333733303265303237646431333336313732613230636463383033303563356135353734316231303130303030303030306666666666666666303264623931306530303030303030303030313630303134383863653639323566383531336132333463303563393232656539333366323231333233303532303731616530303030303030303030303031363030313439343037323635393563343166636130623438313063363239393161643964323839656562383238303234373330343430323230323538373665633862396635316433343361356135366163353439633063383238303035656634356562653964613136366462363435633039313537323233663032323034636430386237323738613838383961383131333539313562636531306431656633626239326232313766383161306465376537396666623364666436616335303132313033323563396134323532373839623331646262333435346563363437653935313665376335393662636465326264356461373161363066616238363434653433383030303030303030222c22696e7075745f616d6f756e7473223a5b222d31303030303030225d7d" // nolint
combineResponse, err := servicer.ConstructionCombine(ctx, &types.ConstructionCombineRequest{
NetworkIdentifier: networkIdentifier,
UnsignedTransaction: unsignedRaw,
Signatures: []*types.Signature{
{
Bytes: forceHexDecode(
t,
"25876ec8b9f51d343a5a56ac549c0c828005ef45ebe9da166db645c09157223f4cd08b7278a8889a81135915bce10d1ef3bb92b217f81a0de7e79ffb3dfd6ac5", // nolint
),
SigningPayload: signingPayload,
PublicKey: publicKey,
SignatureType: types.Ecdsa,
},
},
})
assert.Nil(t, err)
assert.Equal(t, &types.ConstructionCombineResponse{
SignedTransaction: signedRaw,
}, combineResponse)
// Test Parse Signed
parseSignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{
NetworkIdentifier: networkIdentifier,
Signed: true,
Transaction: signedRaw,
})
assert.Nil(t, err)
assert.Equal(t, &types.ConstructionParseResponse{
Operations: parseOps,
AccountIdentifierSigners: []*types.AccountIdentifier{
{Address: "tb1qcqzmqzkswhfshzd8kedhmtvgnxax48z4fklhvm"},
},
}, parseSignedResponse)
// Test Hash
transactionIdentifier := &types.TransactionIdentifier{
Hash: "6d87ad0e26025128f5a8357fa423b340cbcffb9703f79f432f5520fca59cd20b",
}
hashResponse, err := servicer.ConstructionHash(ctx, &types.ConstructionHashRequest{
NetworkIdentifier: networkIdentifier,
SignedTransaction: signedRaw,
})
assert.Nil(t, err)
assert.Equal(t, &types.TransactionIdentifierResponse{
TransactionIdentifier: transactionIdentifier,
}, hashResponse)
// Test Submit
bitcoinTransaction := "010000000001017f9cf50b02dd5258f80cd5c3437302e027dd1336172a20cdc80305c5a55741b10100000000ffffffff02db910e000000000016001488ce6925f8513a234c05c922ee933f221323052071ae000000000000160014940726595c41fca0b4810c62991ad9d289eeb82802473044022025876ec8b9f51d343a5a56ac549c0c828005ef45ebe9da166db645c09157223f02204cd08b7278a8889a81135915bce10d1ef3bb92b217f81a0de7e79ffb3dfd6ac501210325c9a4252789b31dbb3454ec647e9516e7c596bcde2bd5da71a60fab8644e43800000000" // nolint
mockClient.On(
"SendRawTransaction",
ctx,
bitcoinTransaction,
).Return(
transactionIdentifier.Hash,
nil,
)
submitResponse, err := servicer.ConstructionSubmit(ctx, &types.ConstructionSubmitRequest{
NetworkIdentifier: networkIdentifier,
SignedTransaction: signedRaw,
})
assert.Nil(t, err)
assert.Equal(t, &types.TransactionIdentifierResponse{
TransactionIdentifier: transactionIdentifier,
}, submitResponse)
mockClient.AssertExpectations(t)
mockIndexer.AssertExpectations(t)
}

193
services/errors.go Normal file
View file

@ -0,0 +1,193 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"github.com/coinbase/rosetta-sdk-go/types"
)
var (
// Errors contains all errors that could be returned
// by this Rosetta implementation.
Errors = []*types.Error{
ErrUnimplemented,
ErrUnavailableOffline,
ErrNotReady,
ErrBitcoind,
ErrBlockNotFound,
ErrUnableToDerive,
ErrUnclearIntent,
ErrUnableToParseIntermediateResult,
ErrScriptPubKeysMissing,
ErrInvalidCoin,
ErrUnableToDecodeAddress,
ErrUnableToDecodeScriptPubKey,
ErrUnableToCalculateSignatureHash,
ErrUnsupportedScriptType,
ErrUnableToComputePkScript,
ErrUnableToGetCoins,
ErrTransactionNotFound,
ErrCouldNotGetFeeRate,
}
// ErrUnimplemented is returned when an endpoint
// is called that is not implemented.
ErrUnimplemented = &types.Error{
Code: 0, //nolint
Message: "Endpoint not implemented",
}
// ErrUnavailableOffline is returned when an endpoint
// is called that is not available offline.
ErrUnavailableOffline = &types.Error{
Code: 1, //nolint
Message: "Endpoint unavailable offline",
}
// ErrNotReady is returned when bitcoind is not
// yet ready to serve queries.
ErrNotReady = &types.Error{
Code: 2, //nolint
Message: "Bitcoind is not ready",
}
// ErrBitcoind is returned when bitcoind
// errors on a request.
ErrBitcoind = &types.Error{
Code: 3, //nolint
Message: "Bitcoind error",
}
// ErrBlockNotFound is returned when a block
// is not available in the indexer.
ErrBlockNotFound = &types.Error{
Code: 4, //nolint
Message: "Block not found",
}
// ErrUnableToDerive is returned when an address
// cannot be derived from a provided public key.
ErrUnableToDerive = &types.Error{
Code: 5, //nolint
Message: "Unable to derive address",
}
// ErrUnclearIntent is returned when operations
// provided in /construction/preprocess or /construction/payloads
// are not valid.
ErrUnclearIntent = &types.Error{
Code: 6, //nolint
Message: "Unable to parse intent",
}
// ErrUnableToParseIntermediateResult is returned
// when a data structure passed between Construction
// API calls is not valid.
ErrUnableToParseIntermediateResult = &types.Error{
Code: 7, //nolint
Message: "Unable to parse intermediate result",
}
// ErrScriptPubKeysMissing is returned when
// the indexer cannot populate the required
// bitcoin.ScriptPubKeys to construct a transaction.
ErrScriptPubKeysMissing = &types.Error{
Code: 8, //nolint
Message: "Missing ScriptPubKeys",
}
// ErrInvalidCoin is returned when a *types.Coin
// cannot be parsed during construction.
ErrInvalidCoin = &types.Error{
Code: 9, //nolint
Message: "Coin is invalid",
}
// ErrUnableToDecodeAddress is returned when an address
// cannot be parsed during construction.
ErrUnableToDecodeAddress = &types.Error{
Code: 10, //nolint
Message: "Unable to decode address",
}
// ErrUnableToDecodeScriptPubKey is returned when a
// bitcoin.ScriptPubKey cannot be parsed during construction.
ErrUnableToDecodeScriptPubKey = &types.Error{
Code: 11, //nolint
Message: "Unable to decode ScriptPubKey",
}
// ErrUnableToCalculateSignatureHash is returned
// when some payload to sign cannot be generated.
ErrUnableToCalculateSignatureHash = &types.Error{
Code: 12, //nolint
Message: "Unable to calculate signature hash",
}
// ErrUnsupportedScriptType is returned when
// trying to sign an input with an unsupported
// script type.
ErrUnsupportedScriptType = &types.Error{
Code: 13, //nolint
Message: "Script type is not supported",
}
// ErrUnableToComputePkScript is returned
// when trying to compute the PkScript in
// ConsructionParse.
ErrUnableToComputePkScript = &types.Error{
Code: 14, //nolint
Message: "Unable to compute PK script",
}
// ErrUnableToGetCoins is returned by the indexer
// when it is not possible to get the coins
// owned by a *types.AccountIdentifier.
ErrUnableToGetCoins = &types.Error{
Code: 15, //nolint
Message: "Unable to get coins",
}
// ErrTransactionNotFound is returned by the indexer
// when it is not possible to find a transaction.
ErrTransactionNotFound = &types.Error{
Code: 16, // nolint
Message: "Transaction not found",
}
// ErrCouldNotGetFeeRate is returned when the fetch
// to get the suggested fee rate fails.
ErrCouldNotGetFeeRate = &types.Error{
Code: 17, // nolint
Message: "Could not get suggested fee rate",
}
)
// wrapErr adds details to the types.Error provided. We use a function
// to do this so that we don't accidentially overrwrite the standard
// errors.
func wrapErr(rErr *types.Error, err error) *types.Error {
newErr := &types.Error{
Code: rErr.Code,
Message: rErr.Message,
}
if err != nil {
newErr.Details = map[string]interface{}{
"context": err.Error(),
}
}
return newErr
}

40
services/errors_test.go Normal file
View file

@ -0,0 +1,40 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestErrors(t *testing.T) {
for i := 0; i < len(Errors); i++ {
assert.Equal(t, int32(i), Errors[i].Code)
}
}
func TestWrapErr(t *testing.T) {
err := errors.New("testing")
typedErr := wrapErr(ErrUnclearIntent, err)
assert.Equal(t, ErrUnclearIntent.Code, typedErr.Code)
assert.Equal(t, ErrUnclearIntent.Message, typedErr.Message)
assert.Equal(t, err.Error(), typedErr.Details["context"])
// Assert we don't overwrite our reference.
assert.Nil(t, ErrUnclearIntent.Details)
}

64
services/logger.go Normal file
View file

@ -0,0 +1,64 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"net/http"
"time"
"go.uber.org/zap"
)
// StatusRecorder is used to surface the status
// code of a HTTP response. We must use this wrapping
// because the status code is not exposed by the
// http.ResponseWriter in the http.HandlerFunc.
//
// Inspired by:
// https://stackoverflow.com/questions/53272536/how-do-i-get-response-statuscode-in-golang-middleware
type StatusRecorder struct {
http.ResponseWriter
Code int
}
// NewStatusRecorder returns a new *StatusRecorder.
func NewStatusRecorder(w http.ResponseWriter) *StatusRecorder {
return &StatusRecorder{w, http.StatusOK}
}
// WriteHeader stores the status code of a response.
func (r *StatusRecorder) WriteHeader(code int) {
r.Code = code
r.ResponseWriter.WriteHeader(code)
}
// LoggerMiddleware is a simple logger middleware that prints the requests in
// an ad-hoc fashion to the stdlib's log.
func LoggerMiddleware(loggerRaw *zap.Logger, inner http.Handler) http.Handler {
logger := loggerRaw.Sugar().Named("server")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
recorder := NewStatusRecorder(w)
inner.ServeHTTP(recorder, r)
logger.Debugw(
r.Method,
"code", recorder.Code,
"uri", r.RequestURI,
"time", time.Since(start),
)
})
}

View file

@ -0,0 +1,46 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
)
// MempoolAPIService implements the server.MempoolAPIServicer interface.
type MempoolAPIService struct{}
// NewMempoolAPIService creates a new instance of a MempoolAPIService.
func NewMempoolAPIService() server.MempoolAPIServicer {
return &MempoolAPIService{}
}
// Mempool implements the /mempool endpoint.
func (s *MempoolAPIService) Mempool(
ctx context.Context,
request *types.NetworkRequest,
) (*types.MempoolResponse, *types.Error) {
return nil, wrapErr(ErrUnimplemented, nil)
}
// MempoolTransaction implements the /mempool/transaction endpoint.
func (s *MempoolAPIService) MempoolTransaction(
ctx context.Context,
request *types.MempoolTransactionRequest,
) (*types.MempoolTransactionResponse, *types.Error) {
return nil, wrapErr(ErrUnimplemented, nil)
}

View file

@ -0,0 +1,37 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMempoolEndpoints(t *testing.T) {
servicer := NewMempoolAPIService()
ctx := context.Background()
mem, err := servicer.Mempool(ctx, nil)
assert.Nil(t, mem)
assert.Equal(t, ErrUnimplemented.Code, err.Code)
assert.Equal(t, ErrUnimplemented.Message, err.Message)
memTransaction, err := servicer.MempoolTransaction(ctx, nil)
assert.Nil(t, memTransaction)
assert.Equal(t, ErrUnimplemented.Code, err.Code)
assert.Equal(t, ErrUnimplemented.Message, err.Message)
}

View file

@ -0,0 +1,99 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
)
// NetworkAPIService implements the server.NetworkAPIServicer interface.
type NetworkAPIService struct {
config *configuration.Configuration
client Client
i Indexer
}
// NewNetworkAPIService creates a new instance of a NetworkAPIService.
func NewNetworkAPIService(
config *configuration.Configuration,
client Client,
i Indexer,
) server.NetworkAPIServicer {
return &NetworkAPIService{
config: config,
client: client,
i: i,
}
}
// NetworkList implements the /network/list endpoint
func (s *NetworkAPIService) NetworkList(
ctx context.Context,
request *types.MetadataRequest,
) (*types.NetworkListResponse, *types.Error) {
return &types.NetworkListResponse{
NetworkIdentifiers: []*types.NetworkIdentifier{
s.config.Network,
},
}, nil
}
// NetworkStatus implements the /network/status endpoint.
func (s *NetworkAPIService) NetworkStatus(
ctx context.Context,
request *types.NetworkRequest,
) (*types.NetworkStatusResponse, *types.Error) {
if s.config.Mode != configuration.Online {
return nil, wrapErr(ErrUnavailableOffline, nil)
}
rawStatus, err := s.client.NetworkStatus(ctx)
if err != nil {
return nil, wrapErr(ErrBitcoind, err)
}
cachedBlockResponse, err := s.i.GetBlockLazy(ctx, nil)
if err != nil {
return nil, wrapErr(ErrNotReady, nil)
}
rawStatus.CurrentBlockIdentifier = cachedBlockResponse.Block.BlockIdentifier
return rawStatus, nil
}
// NetworkOptions implements the /network/options endpoint.
func (s *NetworkAPIService) NetworkOptions(
ctx context.Context,
request *types.NetworkRequest,
) (*types.NetworkOptionsResponse, *types.Error) {
return &types.NetworkOptionsResponse{
Version: &types.Version{
RosettaVersion: "1.4.2",
NodeVersion: "0.0.1",
},
Allow: &types.Allow{
OperationStatuses: bitcoin.OperationStatuses,
OperationTypes: bitcoin.OperationTypes,
Errors: Errors,
},
}, nil
}

View file

@ -0,0 +1,126 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"testing"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-bitcoin/configuration"
mocks "github.com/coinbase/rosetta-bitcoin/mocks/services"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/stretchr/testify/assert"
)
var (
defaultNetworkOptions = &types.NetworkOptionsResponse{
Version: &types.Version{
RosettaVersion: "1.4.2",
NodeVersion: "0.0.1",
},
Allow: &types.Allow{
OperationStatuses: bitcoin.OperationStatuses,
OperationTypes: bitcoin.OperationTypes,
Errors: Errors,
},
}
networkIdentifier = &types.NetworkIdentifier{
Network: bitcoin.MainnetNetwork,
Blockchain: bitcoin.Blockchain,
}
)
func TestNetworkEndpoints_Offline(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.Offline,
Network: networkIdentifier,
}
mockIndexer := &mocks.Indexer{}
mockClient := &mocks.Client{}
servicer := NewNetworkAPIService(cfg, mockClient, mockIndexer)
ctx := context.Background()
networkList, err := servicer.NetworkList(ctx, nil)
assert.Nil(t, err)
assert.Equal(t, []*types.NetworkIdentifier{
networkIdentifier,
}, networkList.NetworkIdentifiers)
networkStatus, err := servicer.NetworkStatus(ctx, nil)
assert.Nil(t, networkStatus)
assert.Equal(t, ErrUnavailableOffline.Code, err.Code)
assert.Equal(t, ErrUnavailableOffline.Message, err.Message)
networkOptions, err := servicer.NetworkOptions(ctx, nil)
assert.Nil(t, err)
assert.Equal(t, defaultNetworkOptions, networkOptions)
mockIndexer.AssertExpectations(t)
mockClient.AssertExpectations(t)
}
func TestNetworkEndpoints_Online(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.Online,
Network: networkIdentifier,
}
mockIndexer := &mocks.Indexer{}
mockClient := &mocks.Client{}
servicer := NewNetworkAPIService(cfg, mockClient, mockIndexer)
ctx := context.Background()
networkList, err := servicer.NetworkList(ctx, nil)
assert.Nil(t, err)
assert.Equal(t, []*types.NetworkIdentifier{
networkIdentifier,
}, networkList.NetworkIdentifiers)
rawStatus := &types.NetworkStatusResponse{
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
}
blockResponse := &types.BlockResponse{
Block: &types.Block{
BlockIdentifier: &types.BlockIdentifier{
Index: 100,
Hash: "block 100",
},
},
}
mockClient.On("NetworkStatus", ctx).Return(rawStatus, nil)
mockIndexer.On(
"GetBlockLazy",
ctx,
(*types.PartialBlockIdentifier)(nil),
).Return(
blockResponse,
nil,
)
networkStatus, err := servicer.NetworkStatus(ctx, nil)
assert.Nil(t, err)
assert.Equal(t, &types.NetworkStatusResponse{
GenesisBlockIdentifier: bitcoin.MainnetGenesisBlockIdentifier,
CurrentBlockIdentifier: blockResponse.Block.BlockIdentifier,
}, networkStatus)
networkOptions, err := servicer.NetworkOptions(ctx, nil)
assert.Nil(t, err)
assert.Equal(t, defaultNetworkOptions, networkOptions)
mockIndexer.AssertExpectations(t)
mockClient.AssertExpectations(t)
}

71
services/router.go Normal file
View file

@ -0,0 +1,71 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"net/http"
"github.com/coinbase/rosetta-bitcoin/configuration"
"github.com/coinbase/rosetta-sdk-go/asserter"
"github.com/coinbase/rosetta-sdk-go/server"
)
// NewBlockchainRouter creates a Mux http.Handler from a collection
// of server controllers.
func NewBlockchainRouter(
config *configuration.Configuration,
client Client,
i Indexer,
asserter *asserter.Asserter,
) http.Handler {
networkAPIService := NewNetworkAPIService(config, client, i)
networkAPIController := server.NewNetworkAPIController(
networkAPIService,
asserter,
)
blockAPIService := NewBlockAPIService(config, i)
blockAPIController := server.NewBlockAPIController(
blockAPIService,
asserter,
)
accountAPIService := NewAccountAPIService(config, i)
accountAPIController := server.NewAccountAPIController(
accountAPIService,
asserter,
)
constructionAPIService := NewConstructionAPIService(config, client, i)
constructionAPIController := server.NewConstructionAPIController(
constructionAPIService,
asserter,
)
mempoolAPIService := NewMempoolAPIService()
mempoolAPIController := server.NewMempoolAPIController(
mempoolAPIService,
asserter,
)
return server.NewRouter(
networkAPIController,
blockAPIController,
accountAPIController,
constructionAPIController,
mempoolAPIController,
)
}

77
services/types.go Normal file
View file

@ -0,0 +1,77 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package services
import (
"context"
"github.com/coinbase/rosetta-bitcoin/bitcoin"
"github.com/coinbase/rosetta-sdk-go/types"
)
// Client is used by the servicers to get Peer information
// and to submit transactions.
type Client interface {
NetworkStatus(context.Context) (*types.NetworkStatusResponse, error)
SendRawTransaction(context.Context, string) (string, error)
SuggestedFeeRate(context.Context, int64) (float64, error)
}
// Indexer is used by the servicers to get block and account data.
type Indexer interface {
GetBlockLazy(context.Context, *types.PartialBlockIdentifier) (*types.BlockResponse, error)
GetBlockTransaction(
context.Context,
*types.BlockIdentifier,
*types.TransactionIdentifier,
) (*types.Transaction, error)
GetCoins(
context.Context,
*types.AccountIdentifier,
) ([]*types.Coin, *types.BlockIdentifier, error)
GetScriptPubKeys(
context.Context,
[]*types.Coin,
) ([]*bitcoin.ScriptPubKey, error)
}
type unsignedTransaction struct {
Transaction string `json:"transaction"`
ScriptPubKeys []*bitcoin.ScriptPubKey `json:"scriptPubKeys"`
InputAmounts []string `json:"input_amounts"`
InputAddresses []string `json:"input_addresses"`
}
type preprocessOptions struct {
Coins []*types.Coin `json:"coins"`
EstimatedSize float64 `json:"estimated_size"`
FeeMultiplier *float64 `json:"fee_multiplier,omitempty"`
}
type constructionMetadata struct {
ScriptPubKeys []*bitcoin.ScriptPubKey `json:"script_pub_keys"`
}
type signedTransaction struct {
Transaction string `json:"transaction"`
InputAmounts []string `json:"input_amounts"`
}
// ParseOperationMetadata is returned from
// ConstructionParse.
type ParseOperationMetadata struct {
ScriptPubKey *bitcoin.ScriptPubKey `json:"scriptPubKey"`
}

104
utils/utils.go Normal file
View file

@ -0,0 +1,104 @@
// Copyright 2020 Coinbase, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"context"
"runtime"
"time"
sdkUtils "github.com/coinbase/rosetta-sdk-go/utils"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
)
const (
// monitorMemorySleep is how long we should sleep
// between checking memory stats.
monitorMemorySleep = 50 * time.Millisecond
)
// ExtractLogger returns a sugared logger with the origin
// tag added.
func ExtractLogger(ctx context.Context, origin string) *zap.SugaredLogger {
logger := ctxzap.Extract(ctx)
if len(origin) > 0 {
logger = logger.Named(origin)
}
return logger.Sugar()
}
// ContextSleep sleeps for the provided duration and returns
// an error if context is canceled.
func ContextSleep(ctx context.Context, duration time.Duration) error {
timer := time.NewTimer(duration)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
}
// MonitorMemoryUsage periodically logs memory usage
// stats and triggers garbage collection when heap allocations
// surpass maxHeapUsage.
func MonitorMemoryUsage(
ctx context.Context,
maxHeapUsage int,
) error {
logger := ExtractLogger(ctx, "memory")
maxHeap := float64(0)
garbageCollections := uint32(0)
for ctx.Err() == nil {
var m runtime.MemStats
runtime.ReadMemStats(&m)
heapAlloc := float64(m.HeapAlloc)
if heapAlloc > maxHeap {
maxHeap = heapAlloc
}
heapAllocMB := sdkUtils.BtoMb(heapAlloc)
if heapAllocMB > float64(maxHeapUsage) {
runtime.GC()
}
if m.NumGC > garbageCollections {
garbageCollections = m.NumGC
logger.Debugw(
"stats",
"heap (MB)", heapAllocMB,
"max heap (MB)", sdkUtils.BtoMb(maxHeap),
"stack (MB)", sdkUtils.BtoMb(float64(m.StackInuse)),
"system (MB)", sdkUtils.BtoMb(float64(m.Sys)),
"garbage collections", m.NumGC,
)
}
if err := ContextSleep(ctx, monitorMemorySleep); err != nil {
return err
}
}
return ctx.Err()
}

24
zstd-train.sh Executable file
View file

@ -0,0 +1,24 @@
#!/bin/bash
# Copyright 2020 Coinbase, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
NETWORK=$1;
NAMESPACE=$2;
DATA_DIRECTORY=$3;
MAX_ITEMS=150000;
DATA_PATH="${DATA_DIRECTORY}/indexer";
DICT_PATH="assets/${NETWORK}-${NAMESPACE}.zstd";
rosetta-cli utils:train-zstd "${NAMESPACE}" "${DATA_PATH}" "${DICT_PATH}" "${MAX_ITEMS}" "${DICT_PATH}";